93 Commits

Author SHA1 Message Date
229f930538 feat(education): add secure content export jobs 2026-08-01 15:19:18 +08:00
ca7409a68b chore: ignore local agent tooling files 2026-08-01 15:08:45 +08:00
0af91622d3 chore: bump Vben submodule with searchable education selects 2026-08-01 15:03:27 +08:00
ddd95ec512 feat(education): add subject and content-entry reference lists for UI selects 2026-08-01 15:00:32 +08:00
35376df79b fix(education): remove duplicated pay/mall menus from education subtree and add tenant_id to new tables 2026-08-01 14:44:29 +08:00
4586747331 fix(education): create missing member/promotion/pay-demo tables, dedupe menus, add quartz tables 2026-08-01 14:36:18 +08:00
a8587ad08f fix(server): align logic-delete config with boolean deleted columns 2026-08-01 14:14:21 +08:00
fc990bc624 fix(server): enable education module controllers by default 2026-08-01 14:08:42 +08:00
9a2bb6b020 fix(education): make education root menu visible in sidebar 2026-08-01 14:06:22 +08:00
a294ce791d test(education): implement authoring contract test stubs for formalized interfaces 2026-08-01 12:33:41 +08:00
dffbb5ff6a chore: bump Vben submodule with ad-free education UI 2026-08-01 12:31:28 +08:00
d5300ed752 fix(pay/trade): restore chainable setters used by fluent call sites 2026-08-01 12:31:10 +08:00
8d8dc7bf1d fix(education): implement tenant question page/get and formalize authoring service contracts 2026-08-01 12:31:05 +08:00
dc88984a8b chore: replace devcontainer with Vben submodule and add tooling ignore rules 2026-08-01 12:20:02 +08:00
6db64917a2 chore(server): enable pay/mall modules and local compose/flyway setup 2026-08-01 12:19:53 +08:00
a841652fd4 feat(education): add authoring, import, operations, supervision, badges, appearance and activation codes 2026-08-01 12:19:37 +08:00
c409cf47e5 feat(mall): tenant-scope product, promotion and trade modules 2026-08-01 12:19:02 +08:00
abf82f86ab feat(pay): tenant-scope native pay and add legacy account/transaction import 2026-08-01 12:18:10 +08:00
891c461aff feat(member): make education point ledger writes idempotent via unique key 2026-08-01 12:18:01 +08:00
04361a5880 feat(infra): add ClamAV file scanner and fail-closed scan configuration 2026-08-01 12:17:54 +08:00
0eba587459 docs(education): update migration status and add EDU-017~033 tickets 2026-08-01 12:17:00 +08:00
453193e857 feat(education): add production file scanning 2026-07-31 22:42:20 +08:00
c8a221acbc docs(education): record migration closure evidence 2026-07-31 16:26:39 +08:00
184452404d fix(education): align capability and catalog boundaries 2026-07-31 16:26:39 +08:00
4573c41c0e fix(education): enforce migration role separation 2026-07-31 16:26:39 +08:00
26f9bcf696 fix(education): fence exam reminder delivery claims 2026-07-31 16:26:39 +08:00
bd956bcb35 fix(education): tighten entitlement integration contracts 2026-07-31 16:26:39 +08:00
fce2c6bc4a fix(education): complete classroom member contract 2026-07-31 16:26:39 +08:00
d81c297381 fix(education): consolidate question import persistence 2026-07-31 16:26:39 +08:00
5ddfba2045 test(education): activate paid collection fixture 2026-07-31 13:37:29 +08:00
68815b1fda test(education): activate entitlement fixtures transactionally 2026-07-31 13:35:25 +08:00
3cfe205dfa fix(education): align integrated entitlement fixtures 2026-07-31 13:32:59 +08:00
83d5e9dfae fix(education): preserve practice slice test composition 2026-07-31 13:29:26 +08:00
a267786e5a fix(education): keep entitlement integration optional in slices 2026-07-31 13:22:50 +08:00
5aba83561e fix(education): resolve integrated migration and telemetry conflicts 2026-07-31 13:18:05 +08:00
ed24350443 fix(education): reconcile integrated module contracts 2026-07-31 13:14:55 +08:00
268a29364b fix(education): integrate EDU-011 bounded capability 2026-07-31 13:07:04 +08:00
7f12dd35c8 feat(education): admit secure import assets 2026-07-31 13:06:30 +08:00
b6a30e0b7c fix(education): align import job failure state 2026-07-31 13:06:21 +08:00
82a0a7fc50 feat(education): add question import job application slice 2026-07-31 13:06:21 +08:00
3744c8c3be feat(education): define content export redaction contract 2026-07-31 13:06:21 +08:00
2a53e8e2c0 feat(education): add content import persistence 2026-07-31 13:06:21 +08:00
26fa2dfaa2 docs(education): document bounded EDU-011 capability 2026-07-31 13:05:59 +08:00
e6202187ec docs(education): close EDU-010 publication scope 2026-07-31 13:05:16 +08:00
be2c22e440 test(education): verify category blueprint publication 2026-07-31 13:04:19 +08:00
b8b06e0292 feat(education): add category lifecycle authoring 2026-07-31 13:04:03 +08:00
efaab4e03a feat(education): add practice blueprint lifecycle 2026-07-31 13:03:38 +08:00
5bc1e9e634 feat(education): add bounded commercialization entitlements 2026-07-31 13:02:16 +08:00
8312859b39 feat(education): deliver bounded EDU-014 learning wave 2026-07-31 13:00:41 +08:00
f42ef76527 feat(education): establish operational independence contracts 2026-07-31 12:56:12 +08:00
428d4e10fd feat(education): add class relationships 2026-07-31 12:53:47 +08:00
3fdc4edfe0 fix(education): keep capability manifest self-contained 2026-07-31 12:26:31 +08:00
f65a39b902 feat(education): expose migration theme status 2026-07-31 12:22:56 +08:00
bc2d555eb3 fix(education): reject public collection inserts 2026-07-31 12:12:30 +08:00
b531be0d5b fix(education): atomically read collection questions 2026-07-31 12:12:30 +08:00
b569371005 fix(education): clear optional collection metadata 2026-07-31 12:12:30 +08:00
2df6b3f26c fix(education): lock collection migration adoption 2026-07-31 12:12:30 +08:00
c3c244a6f5 fix(education): preserve manual collection order 2026-07-31 12:12:30 +08:00
eae5a6bb3a fix(education): close collection release gate gaps 2026-07-31 12:12:30 +08:00
1be0b420c8 fix(education): enforce collection target integrity 2026-07-31 12:12:29 +08:00
588dd82638 fix(education): validate collection adoption history 2026-07-31 12:12:29 +08:00
5bd2f6758f fix(education): guard collection question deletion 2026-07-31 12:12:29 +08:00
c6b482fa2b fix(education): version collection membership replacements 2026-07-31 12:12:29 +08:00
08825ddb12 fix(education): secure collection publication boundaries 2026-07-31 12:12:29 +08:00
e272b8ec5f fix(education): close collection integrity races 2026-07-31 12:12:29 +08:00
9a4d72bba4 test(education): align question fixtures with node lifecycle 2026-07-31 12:12:29 +08:00
10a4e087ea test(education): verify collection permission seeds 2026-07-31 12:12:29 +08:00
de7fe04c0c fix(education): harden manual collection authoring 2026-07-31 12:12:29 +08:00
8861b6c8dc feat(education): add manual question collections 2026-07-31 12:12:29 +08:00
f445c61f92 fix(education): harden content node lifecycle 2026-07-31 02:27:32 +08:00
62bbdd4b87 docs(education): define manual collection contract 2026-07-31 01:27:15 +08:00
2daeb43c5a fix(education): validate content node migration 2026-07-30 23:57:53 +08:00
f84aecf8f5 fix(education): control content node visibility 2026-07-30 23:57:53 +08:00
e0a695978c feat(education): add tenant content node lifecycle 2026-07-30 23:57:53 +08:00
2a40cbd69e feat(education): add tenant question placement 2026-07-30 22:28:08 +08:00
34bc1fe41e feat(education): add native question publication lifecycle 2026-07-30 21:31:08 +08:00
4db4a7d371 feat(education): enforce catalog graph scope 2026-07-30 19:53:05 +08:00
55a991d3e4 style(education): fix favorite test formatting 2026-07-30 14:38:23 +08:00
bf24589424 fix(education): fail closed on unsafe question data 2026-07-30 14:30:04 +08:00
4be9c8147e fix(education): validate adopted unique indexes 2026-07-30 14:23:36 +08:00
191811b643 fix(education): align Flyway delivery review 2026-07-30 14:11:03 +08:00
2d97858680 docs: document Gitea tea workflow 2026-07-30 12:10:00 +08:00
79a5799502 feat(education): complete Flyway migration and atomic submit 2026-07-30 12:06:55 +08:00
ce02f8acb4 feat(education): complete student core loop delivery 2026-07-28 14:58:14 +08:00
93f02df68c feat(education): add question favorites 2026-07-28 00:21:35 +08:00
12676dfbec feat(education): project and review wrong questions 2026-07-27 23:43:16 +08:00
4cb18b844a feat(education): submit sessions and persist reports 2026-07-27 22:39:39 +08:00
046bb4efee feat(education): save practice answers idempotently 2026-07-27 21:49:02 +08:00
a43a58513a feat(education): create resumable practice sessions 2026-07-27 21:06:57 +08:00
73b2a8edcc feat(education): question browsing and practice configuration preview 2026-07-27 20:13:27 +08:00
478d3d65b7 feat(education): add scalar catalog adapter 2026-07-27 18:41:09 +08:00
0f846fdaf5 feat(education): resolve student tenant context 2026-07-27 17:33:04 +08:00
11e9cc6854 feat(education): add module application shell 2026-07-27 16:33:24 +08:00
826 changed files with 62333 additions and 428 deletions

View File

@@ -0,0 +1,115 @@
---
name: flyway-postgresql
description: Flyway PostgreSQL migrations for this project. Use when adding or changing database schema, indexes, constraints, required seed data, migration baselines, or Flyway configuration.
---
# Flyway PostgreSQL migrations
Use a **forward-only** migration process. Treat `flyway_schema_history` as immutable release history.
## 1. Inspect the migration state
Before editing:
1. Read `CLAUDE.md` PostgreSQL and Flyway rules.
2. Inspect `yudao-server/src/main/resources/application-{local,dev}.yaml` and `yudao-server/pom.xml` when configuration is involved.
3. List every `db/migration` directory and versioned migration across active modules.
4. Inspect the target table DO, Mapper, service use, and relevant PostgreSQL DDL.
5. Check the working tree so existing uncommitted work is preserved.
**Complete when:** the active Flyway locations, baseline, highest migration version, affected database objects, and pending user changes are known.
## 2. Choose the migration branch
### New schema change
Create a new versioned SQL migration under:
```text
<module>/src/main/resources/db/migration/<module>/
```
Use the next unused project-wide version after `V4010`. Leave gaps of 10 for normal changes when practical:
```text
V4020__add_student_progress.sql
V4030__add_practice_report_index.sql
```
### Fix an executed migration
Create a higher version that repairs or reverses the prior change. Preserve the executed file byte-for-byte.
### Existing database adoption
The current baseline is `4009`; `V4010__initialize_education_flyway.sql` is the first managed migration. Keep `baseline-on-migrate` only while existing environments are being adopted. After every existing environment has a baseline record, change it to `false` in a separate reviewed change.
### Configuration change
Flyway must target the dynamic datasource `master`, never `slave`. Keep these safeguards enabled:
```yaml
validate-on-migrate: true
clean-disabled: true
out-of-order: false
```
Allow `FLYWAY_URL`, `FLYWAY_USER`, and `FLYWAY_PASSWORD` to override master credentials.
**Complete when:** exactly one branch is selected and its version/configuration does not conflict with the current repository state.
## 3. Write PostgreSQL-native SQL
Follow these project conventions:
- Identity primary key: `BIGINT GENERATED BY DEFAULT AS IDENTITY`.
- Time: `TIMESTAMP`; use `CURRENT_TIMESTAMP` for defaults.
- Boolean: `BOOLEAN NOT NULL DEFAULT false` where appropriate.
- Idempotency/upsert: `ON CONFLICT ... DO NOTHING` or `DO UPDATE SET ... EXCLUDED.column`.
- Null fallback: `COALESCE`.
- Date formatting: `TO_CHAR`; date parts: `EXTRACT`.
- Bounded delete: delete by IDs selected in an ordered, limited subquery or CTE.
- Add comments for business tables and non-obvious columns.
- Add indexes from observed query and conflict targets, not speculation.
- Required seed data must be deterministic and idempotent.
- Put `CREATE INDEX CONCURRENTLY` in its own non-transactional migration; otherwise prefer transactional PostgreSQL DDL.
Keep verification queries in comments when useful. Put rollback notes in the change description or a separate operational document; production recovery is another forward migration.
**Complete when:** every affected object, data backfill, constraint, index, and application assumption is represented in PostgreSQL-native SQL.
## 4. Align application code
Update all affected DOs, Mappers, services, tests, and fixtures. Search Java annotations and MyBatis XML for stale column names and incompatible SQL. For a new identity/sequence-backed DO, follow the surrounding projects `@TableId` and `@KeySequence` pattern.
**Complete when:** every code reference agrees with the post-migration schema and no active runtime SQL depends on the previous shape.
## 5. Verify
Run, in order:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
Confirm each migration is present under the owning modules `target/classes/db/migration/...` after compilation. If an authorized disposable PostgreSQL database is available, run Flyway against it and inspect:
```sql
SELECT installed_rank, version, description, script, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
```
Run focused tests for the affected module. Report any skipped database execution separately from compilation success.
**Complete when:** formatting and compilation pass, migration packaging is confirmed, focused tests pass or their exact blocker is reported, and any real-database migration status is stated truthfully.
## Release rules
- Version numbers are project-wide across every Flyway location.
- One committed migration version has one immutable meaning.
- Production migrations move forward; recovery is a higher version.
- `clean` remains disabled.
- Demo/test seed data lives outside production migrations.
- Do not copy the legacy `sql/postgresql/ruoyi-vue-pro.sql` dump into a versioned runtime migration; it contains destructive bootstrap statements and embedded transactions. Use it only to initialize a disposable empty database or to establish the pre-Flyway baseline.

View File

@@ -1,17 +0,0 @@
FROM mcr.microsoft.com/devcontainers/java:3-25-bookworm
ARG INSTALL_MAVEN="true"
ARG MAVEN_VERSION=""
ARG INSTALL_GRADLE="false"
ARG GRADLE_VERSION=""
RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \
&& if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

@@ -1,28 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/java-postgres
{
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
"name": "Java & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
// "forwardPorts": [5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "java -version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -1,59 +0,0 @@
volumes:
postgres-data:
services:
app:
container_name: javadev
build:
context: .
dockerfile: Dockerfile
environment:
# NOTE: POSTGRES_DB/USER/PASSWORD should match values in db container
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_HOSTNAME: db
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Use proper Docker networking instead of network_mode: service:db
# to ensure reliable DNS resolution in all environments
depends_on:
db:
condition: service_healthy
networks:
- app-network
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
container_name: postgresdb
image: postgres:latest
restart: unless-stopped
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
- postgres-data:/var/lib/postgresql
environment:
# NOTE: POSTGRES_DB/USER/PASSWORD should match values in app container
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
networks:
app-network:
driver: bridge

15
.gitignore vendored
View File

@@ -7,9 +7,22 @@
target/
!.mvn/wrapper/maven-wrapper.jar
bin/
.flattened-pom.xml
######################################################################
# Tooling caches
.pnpm-store/
######################################################################
# Local agent tooling (never commit)
.agents/
skills-lock.json
.serena/
######################################################################
# IDE
@@ -52,4 +65,4 @@ application-my.yaml
/yudao-ui-app/unpackage/
.DS_Store
**/.DS_Store
**/.DS_Store

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "yudao-ui/yudao-ui-admin-vben"]
path = yudao-ui/yudao-ui-admin-vben
url = https://git.gongxue100.com/wangziqi/yudao-ui-admin-vben.git

88
CLAUDE.md Normal file
View File

@@ -0,0 +1,88 @@
# CLAUDE.md — 恭学教育
## 项目定位
恭学教育是基于 RuoYi-Vue-Pro 的教育 SaaS 平台,后端技术栈为 Spring Boot 4、MyBatis-Plus、PostgreSQL、Redis。
主要开发入口:
- `yudao-module-education/`:教育业务模块
- `yudao-server/`:应用启动模块,默认端口 `48080`
- `sql/postgresql/`PostgreSQL 初始化与历史 SQL
- `tools/education-student-harness/`:学生端 Playwright E2E 测试
分支、工作区状态、运行中的容器和临时任务属于动态信息;执行任务时从 Git、配置文件和运行环境读取不在本文档固化。
## 工作方式
### Serena 优先
编码任务开始前调用 Serena `initial_instructions` 并激活项目。优先使用 Serena 完成符号检索、引用分析和结构化编辑Serena 不可用或不适合时再使用通用文件与命令工具。互不依赖的查询或编辑应批量调用。
### 遵循现有代码
- 修改前检查工作区,保留用户已有的未提交修改。
- 代码风格、命名、注释密度和分层方式与相邻代码保持一致。
- 优先复用框架现有能力,避免为单一场景建立平行抽象。
- 只修改当前任务需要的文件;发现相邻问题时先判断是否影响本次交付。
### Gitea 与仓库操作
项目托管在自建 Gitea。远程仓库、Issue 和 Pull Request 操作统一使用 `tea` CLI不使用 GitHub CLI`gh`)。执行创建或查看 Pull Request 等操作前,从当前 Git remote 与 `tea` 登录配置读取仓库和实例信息。
### Skills
项目级 Skills 位于 `.claude/skills/`,已有规范索引见 `.claude/skills/index.yaml`
数据库结构、索引、约束、数据回填、必要种子数据、基线或 Flyway 配置发生变化时,使用项目 Skill
```text
/flyway-postgresql
```
Flyway 的版本分配、接管策略、验证步骤以 `.claude/skills/flyway-postgresql/SKILL.md` 为唯一事实来源。
## 架构约束
### Education 模块
- `yudao.education.catalog-mode` 控制目录数据源:
- `SCALAR_READ`:通过 `ScalarCatalogProvider` 访问 HTTP 数据源。
- `JAVA_READ`:通过 `JavaCatalogProvider` 直连 PostgreSQL。
- `QuestionCatalogServiceImpl` 返回前端前必须剥离答案与解析等敏感字段。
- 题目不可见或数据源不可用时采用 fail-closed不进行静默降级。
- 多租户业务 DO 继承 `TenantBaseDO`,由 MyBatis-Plus 注入 `tenant_id`
### PostgreSQL
项目运行数据库为 PostgreSQL。Java 注解 SQL、MyBatis XML、测试 SQL 和运行配置均使用 PostgreSQL 方言。
- 主键:`BIGINT GENERATED BY DEFAULT AS IDENTITY`
- 时间:`TIMESTAMP`,默认当前时间使用 `CURRENT_TIMESTAMP`
- 布尔:`BOOLEAN`,按需使用 `NOT NULL DEFAULT false`
- 幂等插入:`ON CONFLICT ... DO NOTHING`
- Upsert`ON CONFLICT (...) DO UPDATE SET ... EXCLUDED.column`
- 空值兜底:`COALESCE`;时间格式化:`TO_CHAR`;日期字段提取:`EXTRACT`
- 有界删除使用有序、限量的主键子查询或 CTE。
- DO 主键遵循项目既有的 `@TableId``@KeySequence("{table}_seq")` 模式。
### Flyway
运行时 migration 放在所属模块:
```text
<module>/src/main/resources/db/migration/<module>/
```
已在共享环境执行的 migration 是不可变发布历史。数据库修复通过更高版本的向前 migration 完成,生产环境保持 `clean-disabled: true`
## 验证
按改动范围执行最小充分验证。后端主链路至少运行:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
涉及行为变化时运行对应模块的聚焦测试;涉及数据库 migration 时还要确认脚本被打包到模块的 `target/classes/db/migration/`。只有实际执行过 PostgreSQL migration才能报告数据库迁移成功否则明确说明仅完成静态检查或编译验证。

11
CONTEXT-MAP.md Normal file
View File

@@ -0,0 +1,11 @@
# Context Map
## Contexts
- [Education](./yudao-module-education/CONTEXT.md) — owns education content, practice, assessment, and learning-state language.
## Relationships
- **Education → Member**: Education references the authenticated Member user as the student identity; it does not own credentials or generic user accounts.
- **Education → System**: Education consumes tenant and authorization capabilities; it does not own generic tenants or RBAC.
- **Education → Infra**: Education composes file, job, messaging, and audit capabilities for education workflows.

36
compose.yaml Normal file
View File

@@ -0,0 +1,36 @@
name: gongxue-local
services:
postgres:
image: postgres:17-alpine
ports:
- "127.0.0.1::5432"
environment:
POSTGRES_DB: ruoyi-vue-pro
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
volumes:
- postgres-data:/var/lib/postgresql/data
- ./sql/postgresql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/000-platform.sql:ro
- ./script/docker/init-local-roles.sql:/docker-entrypoint-initdb.d/010-local-roles.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d ruoyi-vue-pro"]
interval: 2s
timeout: 5s
retries: 30
redis:
image: redis:6-alpine
ports:
- "127.0.0.1::6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 5s
retries: 30
volumes:
postgres-data:
redis-data:

View File

@@ -0,0 +1,5 @@
# Native question authoring follows the active catalog authority
Education exposes local question authoring only when `catalog-mode=JAVA_READ`; `SCALAR_READ` and other modes fail with an explicit business error before any database access. Tenant questions start as `DRAFT`, retain immutable content versions, are placed optimistically on an allowed Content Node, move only through `DRAFT → PUBLISHED → ARCHIVED`, and append an Education-owned lifecycle audit in the same transaction, because a local write under Scalar authority would be invisible to students and the platform's asynchronous operation log cannot prove atomic publication.
Question Placement has its own optimistic version, is available only to current-tenant `TENANT_OWNED` drafts, and becomes immutable at publication. Publication requires a stable active, visible, selectable PUBLIC or same-tenant Content Node. `ARCHIVED` is terminal for the current command surface, and `RETIRED` is not introduced until its distinct business and restoration semantics are decided. Direct question visibility follows the question's own publication state; entry, node, and collection state gates discovery through those routes rather than rewriting the question lifecycle.

View File

@@ -0,0 +1,48 @@
# Current State
> Phase 0 static assessment originally generated on 2026-07-29. Current delivery status updated on 2026-08-01 through V4450/EDU-033; the original investigation remains below as provenance.
## Current implementation summary
The target branch now contains the student core loop, tenant/identity enforcement, bounded content/operations/appearance/activation-code capabilities, and module-owned PostgreSQL Flyway migrations V4010V4450. Native RuoYi Pay, Product, Coupon, normal Trade order, Discount/Reward checkout, delivery, after-sale, brokerage, Seckill, and Combination administration are activated with tenant-composite persistence and their existing APIs, RBAC, services, and Vben pages; Education does not own shadow financial, catalog, order, delivery, refund, commission, or promotion ledgers. The full Flyway suite passes 52 PostgreSQL scenarios through V4450; EDU-031 proves tenant-isolated two-level commission/statistics, EDU-032 proves same-number cross-tenant seckill state plus atomic last-stock competition, and EDU-033 proves tenant-isolated group records plus atomic last-place competition and Trade Order/head consistency. The required non-clean Maven compile chain and complete Vben typecheck pass without interrupting the running server. This is isolated disposable-schema evidence, not proof that a shared Pilot or production database was migrated. Explicit legacy Product/coupon/order/refund mapping, source referral CRM and settlement proof/export import, Bargain/Point and other special-order Promotion families, production deployment/data, automatic purchase fulfillment/refund revocation, and selected deferred learning/platform families remain open.
This document retains the original Phase 0 findings below as provenance. Current ticket status is authoritative in [`issues/README.md`](issues/README.md), and rollout evidence is tracked by [`../pilot-acceptance-runbook.md`](../pilot-acceptance-runbook.md).
## Original Phase 0 executive summary
Verified evidence showed the target on `feature/education-core-loop` with a heavily dirty worktree, while the source checkout had no local or remote branch with that name and remained at `033701a`. The assessment identified a meaningful committed core-loop slice plus substantial provider/catalog/session work that still required classification. It selected provider-neutral fail-closed question handling, tenant/principal enforcement, atomic idempotency, PostgreSQL/Flyway takeover, and catalog graph integrity as the immediate blockers. Those bounded blockers have since been implemented and verified; this paragraph is retained only as historical context.
## Original Phase 0 decisions and unknowns
The remaining sections are the immutable investigation record from 2026-07-29. Items phrased as pending may now be resolved by later tickets and migrations; use the current summary and ticket index above for delivery status.
### Decisions recorded during Phase 0
- Verified source provenance is limited: /Users/tiku1/code/tiku-backend has only main and origin/main at 033701a785c7012139e7f86995eea6041225592e; no local or remote feature/education-core-loop ref exists. Use main/033701a provisionally only, or obtain explicit approval for that baseline.
- Verified target branch is feature/education-core-loop and its worktree is dirty. Current read-only inventory reports 65 modified tracked files and 97 untracked entries; preserve all, and do not rely on an older 21-untracked count.
- Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work. ce02f8a is committed core-loop evidence; native provider/catalog and much of the schema are dirty.
- Verified V4010 is SELECT 1 and V4020 is native catalog only. Practice/report/idempotency/wrong/favorite DDL in sql/postgresql/education is untracked/manual and not proven active Flyway. Convert required DDL to immutable module-owned PostgreSQL Flyway migrations before claiming schema delivery; never modify published migrations.
- Keep PostgreSQL/Flyway as the only new schema delivery mechanism. Historical MySQL files and root SQL are not active delivery unless explicitly labeled archival/manual and removed from operational runbooks.
- Treat public tenant resolution origin-binding absence as a P0 correction, not merely a richer-legacy gap. Separately acknowledge that TenantSecurityWebFilter already rejects authenticated tenant/header mismatch; the remaining principal issue is missing Member/UserType enforcement in /education/context.
- Treat hostname port handling as a verified internal contradiction requiring alignment across implementation, properties, API documentation, System lookup normalization, and tests.
- Treat native catalog isolation as an intentional TenantUtils.executeIgnore/manual-scope boundary, not evidence of a current leak. Make mapper audit and tenant/scope-consistent graph constraints concrete blockers before authoring.
- Make the first slice provider-neutral or cover both providers because SCALAR_READ is the verified default and Java provider is conditional. The slice must include fresh browsing and persisted session restoration, with a common option-schema contract and fail-closed behavior.
- Do not treat submit idempotency as complete: check-then-insert is not an atomic claim. Reserve keys atomically and define crash recovery before the submit slice.
- Add first-class Auth/Profile/extended Learning, tenant appearance/integrations/secrets/codes, and granular platform-admin capability groups so every required legacy cluster has a disposition.
- Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
- No tests, builds, PostgreSQL connections, Flyway execution, or runtime verification were performed; all conclusions are static repository evidence unless explicitly marked otherwise.
### Unknowns recorded during Phase 0
- EDU-003 decided the public resolver threat model and contract. Browser headers are forgeable context claims; a constrained Public Tenant Handle supports headless clients; success discloses tenant existence; unknown/disabled/expired failures are identical; exact errors, canonical websites, local activation, Member-only context, and abuse controls are assigned to EDU-004.
- Whether a future System-owned immutable Tenant Code or authenticated/signed locator is required beyond the accepted public-handle contract.
- The valid option schema for each question type, including whether absent options are legal; whether malformed published content is omitted or produces a controlled source failure.
- Whether PUBLIC tenant_id=0 rows may reference only PUBLIC parents, whether tenant-owned rows may reference global rows, and the precise composite constraint/trigger strategy.
- Whether untracked /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education files are intended for promotion into Flyway or are design/manual artifacts.
- Whether V4010/V4020 or any manual core-loop DDL has ever run successfully in PostgreSQL; no runtime migration evidence exists.
- Whether target test H2 MODE=MYSQL is test-only and compatible with PostgreSQL-only delivery.
- Which Auth/Profile/extended Learning semantics are replaced by Member/System/Infra versus Education-owned, including vocabulary, leaderboard, stats, trend, feedback, exam dates, notifications, points, and badges.
- Whether tenant appearance, domains, payment accounts, auth providers, secrets, activation codes, coupons, integrations, marketing, public-bank grants, and sync are in scope or explicitly retired.
- Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
- Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
- Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.

View File

@@ -0,0 +1,41 @@
# Capability Migration Matrix
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
> Execution update 2026-08-01: the `Auth, student profile, and extended learning` row's original `pending migration` cell is superseded by EDU-014 and is now **partially migrated**. V4160V4200 deliver Member/System-backed auth context, vocabulary, reminders, fixed learning awards, feedback submission, bounded summaries and leaderboard; V4250V4280 add idempotent Member point business keys, tenant-admin feedback handling/audit, resolved-feedback rewards, configurable badge definitions, automatic practice/vocabulary/feedback rules, lifetime-once grants, System notifications, learning-risk supervision and follow-up tasks, RBAC/data scope, and Vben UI. Generic check-in/point tasks/exchange, check-in/mock-exam/activity badge triggers, automatic scheduled supervision execution, broader trends/exports, and identity-history import remain open and must not be treated as complete.
> Execution update 2026-08-01: the `Tenant education operations, appearance, integrations, secrets, and codes` row's original `pending migration` cell is now **partially migrated**. V4140 delivered classes, Member relationships, and invitations; V4230 delivered the first class UI; V4270 adds native learning-risk preview, supervision rules, idempotent follow-up tasks, System AdminUser/RBAC/department-self data scopes, and Vben UI. V4290 adds System-fallback branding, public/admin settings, three platform theme templates, optimistic draft/publish lifecycle, a tenant-context public projection, closed secret/rendering validation, four independent permissions, PostgreSQL evidence, and the fourteenth custom Education Vben page. V4300 exposes Pay application/channel and tenant-scoped System social-client administration below Education using their original controllers, permissions, and existing Vben pages; V4320 makes Pay App/Channel operational with fail-closed tenant-scoped PostgreSQL tables and parent validation. V4330/EDU-021 adds a Pay-owned redacted audit plus explicit single-account import for safe `tenant_collect` WeChat/Alipay manifests. V4340/EDU-022 activates tenant-scoped native Pay order/extension/refund/notification tables, callbacks/retry processing, and the three existing Pay administration pages without an Education financial ledger. V4350/EDU-023 adds terminal-only reconciled legacy order/payment/refund aggregate import into those Pay-owned ledgers, a redacted event-digest audit, and an import/history modal on the native order page. V4360/EDU-024 activates empty tenant-scoped native Transfer/Wallet ledgers, tenant-qualified locks, safe amount-changing paths, granular permissions, and the three existing Pay Transfer/Wallet Vben pages; it does not infer historical balances. V4310 adds tenant-owned learning activation-code batches/codes and the fifteenth custom Education Vben page while composing Mall-owned SPU binding, Member principal, and the idempotent Education entitlement pipeline. Plaintext activation codes are returned once; only SHA-256 digest and mask persist. Domains remain System-owned and coupons remain Mall Promotion-owned. Production bulk financial export/runbooks and reviewed opening balances, automatic purchase fulfillment/refund revocation, tenant SMS/PNVS, encrypted secret rotation, non-equivalent payment modes/providers, legacy activation-code import, coupons, scheduled production rule execution, and the rest of tenant operations remain open.
> Execution update 2026-08-01: V4370/EDU-025 enables the Mall reactor and Product server module, changes all nine Product records to `TenantBaseDO`, creates tenant-composite PostgreSQL catalog tables and constraints, and reuses the five native Product Vben pages with their exact controller permissions.
> EDU-025 supersedes older matrix cells that list Mall Product activation as open. V4370 activates the tenant-scoped native brand/category/property/SPU/SKU/comment/favorite/history catalog and five existing Product administration pages. It deliberately does not infer SPUs, SKUs, prices, stock, brands, or properties from the legacy display-only `products` projection.
> Execution update 2026-08-01: V4380/EDU-026 enables the Promotion server module, changes native `CouponTemplateDO` and `CouponDO` to `TenantBaseDO`, creates tenant-composite coupon template/instance persistence, and reuses the two native Promotion coupon Vben pages with exact controller permissions. It does not reinterpret legacy code campaigns/redemptions as pre-issued member coupons. EDU-027 subsequently activates the normal Trade order core; Statistics, other Promotion/Trade table families, and explicit legacy Product/code-coupon import remain separate.
> Execution update 2026-08-01: V4390/EDU-027 enables the native Trade server module, tenantizes order/cart/config records, supplies tenant-composite order-core persistence, and reuses the native order/config Vben pages and permissions. Legacy orders remain unimported because verified Member, SPU/SKU, price-allocation, and lifecycle mappings are absent. Trade after-sale/delivery/brokerage tables, special-order Promotion families, Statistics, and explicit legacy order import remain separate.
> Execution update 2026-08-01: V4400/EDU-028 tenantizes native Discount Activity/Product and Reward Activity records, creates the three PostgreSQL tables queried by every normal Trade price calculation, and reuses the native discount/reward APIs, exact permissions, and two existing Vben pages. Empty real API lookups now succeed against PostgreSQL; delivery, configured payment runtime, special-order Promotion families, and end-to-end checkout evidence remain separate.
> Execution update 2026-08-01: V4410/EDU-029 tenantizes all five native Trade delivery records, creates express-company/template/charge/free/pickup persistence, connects Product templates and pickup orders through tenant-qualified references, and reuses the three existing Vben pages with exact permissions. A real Spring/MyBatis calculation resolves a persisted tenant template and adds the expected freight. Configured Pay runtime, after-sale/brokerage, special orders, and target-environment checkout evidence remain separate.
> Execution update 2026-08-01: V4420/EDU-030 tenantizes native Trade after-sale and log records, creates their PostgreSQL persistence and tenant-qualified Order/Order Item/Product/Pay Refund/Delivery references, and reuses the native app/admin state machine plus the corrected Vben list/detail page with five exact permissions. A real Spring/MyBatis test proves tenant-isolated create/page/detail/log behavior. Source aggregate UUID refunds remain unimported until verified legacy Member/Product/Order Item mappings exist; Trade brokerage, special orders, fulfillment/revocation orchestration, and target-environment refund evidence remain separate.
> Execution update 2026-08-01: V4430/EDU-031 tenantizes native Trade brokerage user/record/withdrawal records, creates tenant-composite PostgreSQL relationships and Pay Transfer/Order references, and reuses native Member-backed two-level commissions, freeze/unfreeze, withdrawal APIs/jobs, eight exact permissions, and three corrected Vben pages. A real Spring/MyBatis test proves same-ID cross-tenant teams, balances, summaries, and annotated ranking SQL remain isolated. Source referral CRM, UUID settlement aggregates, and proof/export events remain unimported pending explicit Member/lead/Order/evidence mapping.
> Execution update 2026-08-01: V4440/EDU-032 tenantizes native Promotion seckill configuration/activity/product records, creates tenant-composite PostgreSQL Product/Trade references and consistency triggers, and reuses native time-slot/activity/atomic-stock services, nine exact permissions, and two corrected Vben pages. A real Spring/MyBatis test proves same-ID cross-tenant records, isolated reads, atomic last-stock competition, restoration, close propagation, and used-slot protection. The source has no seckill capability, so the native tables intentionally start empty; later Combination activation and remaining families are tracked separately.
> Execution update 2026-08-01: V4450/EDU-033 tenantizes native Promotion Combination activity/product/record objects, creates tenant-composite Product/Trade/record/head references and capacity/snapshot triggers, and reuses native activity/group/job services, six exact permissions, and corrected Vben pages. A real Spring/MyBatis test proves same-ID cross-tenant activities/products, isolated page/record/summary reads, snapshot propagation, real head IDs, atomic last-place competition, order/head consistency, and record-backed deletion protection. The source `combination` token is an education question type rather than group buying, so native tables intentionally start empty; Bargain, Point, and other special-order families remain separate.
Statuses are restricted to the Goal vocabulary. Evidence marked as verified is static repository evidence.
| Legacy capability | Legacy code location | Legacy database objects | Business value | Target module | Existing capability to reuse | Education gap | Other-module change | Priority | Risk | Verification | Status | Evidence | Open decision |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant resolution, identity, and student context | /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/resolver.ts | System tenant and website/domain records are authoritative.<br>Legacy tenant, domain, branding, identity, membership, and RLS objects are reference-only and must not be copied mechanically. | Resolves the pre-login tenant and authenticated student context needed to route users into the correct education tenant without trusting client identity. | System + Member + framework tenant support with an Education context adapter | System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations. | Verified: EducationTenantController currently accepts caller-supplied hostname or tenantName under @PermitAll. EDU-003 establishes that Origin/Referer are forgeable browser-context claims rather than trusted identity, success discloses available-tenant existence, and legacy tenantName is unsupported. Verified: TenantSecurityWebFilter already rejects authenticated LoginUser/request-tenant mismatches and requires a tenant on non-ignored URLs. Verified: EducationContextController checks login presence and tenant validity but not LoginUser.userType. Verified: hostname documentation says no port while implementation preserves legal ports and tests expect port preservation. Decision: EDU-004 must implement the exact Public Tenant Handle, canonical website, secure local flag, wire-error, Member-principal, redaction, and abuse-control contract. | System/framework tenant and security boundaries remain authoritative; Education adapts them. EDU-003 retained generic System lookup APIs and selected Member-only context enforcement. | P0 | High: wrong pre-login tenant selection, tenant discovery, or treating an admin ID as a Member ID can cross security boundaries. | Add exact Origin/Referer claim, forged-header threat-model, requested-host conflict, explicit handle, legacy tenantName rejection, lifecycle-indistinguishability, canonical website, local-flag, normalization, anonymous, missing-tenant, mismatch, Member/admin-principal, redaction, and abuse-control tests. Static audit is not runtime proof. | partially migrated | Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:51-63,109-132,159-177.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java:66-105.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/EducationContextController.java:43-57.<br>Verified source baseline /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts:90-151 requires production origin/request-host checks.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantResolveIntegrationTest.java:80-89. | EDU-003 accepted: Origin/Referer are forgeable browser-context claims; headless lookup uses the constrained System-name Public Tenant Handle; successful lookup discloses existence while unknown/disabled/expired are indistinguishable; host identity and canonical stored websites are host-only; local fallback has a secure-default flag; context is Member-only. Future signed locator or immutable System Tenant Code remains optional later scope. Source feature ref remains unavailable. |
| Student core learning loop | /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-113<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/learning/use-cases.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/learning/access.ts | Education catalog and practice/report/idempotency/wrong-question/favorite tables.<br>Verified: native catalog V4020 is a dirty module resource; practice/report/idempotency DDL is currently in untracked sql/postgresql/education files rather than proven active Flyway history. | Provides the student loop from published catalog browsing through practice creation, answer saving, restore, submission, report, wrong questions, and favorites. | Education | Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims. | Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established. | Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision. | P0 | High: corrupt assessment content, mode-dependent behavior, duplicate state transitions, answer leakage, or unauthorized access. | Provider-neutral tests across Scalar and Java, browsing/collection/practice-create/restore safe projections, malformed/unavailable/unpublished fail-closed cases, cross-tenant cases, and PostgreSQL concurrent same-key/different-key submit tests. | partially migrated | Verified ce02f8a, target repository, for committed EducationAccessService, core controllers/services, projections, and HTTP/service tests.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProperties.java:34-36 defaults to SCALAR_READ.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/integration/scalar/config/ScalarAutoConfiguration.java:30-35 selects Scalar for SCALAR_READ/missing mode.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java:397-417, ScalarCatalogProvider.java:736-751, QuestionCatalogServiceImpl.java:182-202, SessionResponseAssembler.java:69-89.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/practice/PracticeSessionServiceImpl.java:294-307,397-409.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education/009-education-idempotency-unified.sql:81, but no execution evidence. | Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice. |
| Education catalog and question content | /Users/tiku1/code/tiku-backend/apps/api/src/nest/catalog.module.ts:74-138<br>/Users/tiku1/code/tiku-backend/apps/nest/tenant-content.module.ts | V4020 native catalog tables for regions, schools, majors, subjects, categories, banks, questions, versions, content, collections, blueprints, and bindings.<br>Legacy catalog/question/content/asset tables, constraints, functions, triggers, grants, and RLS are reference objects requiring semantic mapping. | Supplies reusable published catalog, question, classification, and content reads for student and future admin workflows. | Education | Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets. | Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement. | Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided. | P0 | High if manual scope is bypassed or invalid cross-tenant/public relationships are admitted. | Inventory every mapper, provider contract tests, invalid graph insert tests, malformed/unpublished tests, PostgreSQL Flyway syntax/resource-packaging checks, and runtime migration evidence only when executed. | partially migrated | Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java:82-89.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/CatalogScopeQuery.java:12-20.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/resources/db/migration/education/V4020__create_native_catalog.sql:38-123,133-152,160-265,324-386.<br>Verified /Users/tiku1/code/tiku-backend/supabase/migrations/202606210008_content_navigation_practice.sql:3-178. | Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual. |
| Auth, student profile, and extended learning | /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/profile/ | Legacy auth/session/verification/OAuth/phone-binding objects.<br>Legacy profile/check-in/points/tasks/exchange/notifications/badges/feedback/exam-countdown objects.<br>Legacy leaderboard/history/report/stats/trend/vocabulary progress/review/favorites/stats objects. | Covers broader student learning and profile experiences beyond the core loop, preserving discoverable legacy behavior and its disposition. | Member + System + Education, with Infra composition | Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization. | Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified. | Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership. | P1 | High for auth compatibility and medium for omitted student progress/profile behavior. | Endpoint/API mapping, principal and tenant tests, profile redaction, progress/report compatibility, vocabulary state transitions, and explicit retired/product-decision checks. | pending migration | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts:31-56.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts:38-87.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-60,70-113.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/docs/education/migration/GOAL.md:126-141,393-405. | For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges. |
| Tenant education operations, appearance, integrations, secrets, and codes | /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-classes.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-appearance.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-integrations.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-secrets.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-codes.module.ts | Legacy classes, student relationships, supervision, roles/configuration, branding/settings/themes, domains, payment accounts, auth-provider configuration, tenant secrets, activation codes, coupons/redemptions, integrations, and marketing objects. | Enables tenant administrators to operate education organizations while preserving separate security and ownership boundaries. | Education + System + Member + Mall/Pay + Infra | System Tenant/RBAC/DataPermission/AdminUserApi, tenant-scoped System Social Client, native Pay App/Channel/Order/Refund/Notify/Transfer/Wallet, Member relationships, Mall Product, Promotion Coupon/Seckill/Combination, Trade Order/Cart/Config/Delivery/After Sale/Brokerage, Education entitlement events, and Infra secret/file/message/audit facilities. | V4290 owns only presentation extensions. V4300V4330 reuse native Pay/System configuration and bounded Pay account import. V4340V4360 activate Pay transactions and Transfer/Wallet; V4370 activates Product; V4380 activates coupon templates/instances; V4390 activates the native Trade order/cart/config core; V4400 activates always-invoked Discount/Reward rules; V4410 activates delivery; V4420 activates native after-sale; V4430 activates native brokerage; V4440 activates native Seckill; V4450 activates native Combination activity/product/group records, capacity protection, Trade bridge, and corrected Vben pages. V4310 owns only activation-code issuance/redemption and composes product binding/entitlement. Production bulk tooling and reviewed opening balances, legacy product/code-coupon/order/refund and referral/settlement-proof import, Bargain/Point and other Promotion families, fulfillment/refund revocation, System-global SMS versus tenant PNVS, encrypted secrets, public-bank grants, and other marketing remain distinct gaps. | System RBAC/DataPermission and tenant configuration are reused; Pay owns payment configuration and ledgers, System owns social providers, Mall owns SPUs/coupons/promotions/orders/delivery/after-sale/brokerage, Member owns principals, Education owns activation-code state and learning-entitlement orchestration, and Infra owns secrets/messaging/files where applicable. | P1 | High: admin scope, secret leakage, payment/order/refund/commission/promotion lifecycle, and code redemption errors. | Native permission/menu shape, Pay/System/Mall tenant-scope tests, provider-data import tests, native Pay/Product/Coupon/Trade/Promotion regressions, secret redaction/rotation, activation/code-coupon idempotency, stock/capacity concurrency, audit, and cross-tenant tests. | partially migrated | Verified V4290 appearance through V4450 Promotion Combination contracts, including native Pay/Product/Coupon/Trade/Promotion UI and tenant-aware records.<br>Verified provider mapping, reconciliation, replay/conflict, audit redaction, composite tenant references, wallet locks/amount safety, delivery calculation, after-sale service/log isolation, brokerage relationship/commission/statistics isolation, seckill stock concurrency, combination group capacity/order consistency, and fail-closed global-table adoption.<br>Verified digest-only activation-code persistence, tenant isolation, atomic redemption, dependency rejection, and concurrent unique winner.<br>Verified System Social Client remains tenant-aware and legacy integration/secrets/codes controllers remain inventoried. | Keep domains in System; use Pay for payment and System Social Client for supported OAuth providers. Activation codes are Education learning-access credentials; Product, coupons, promotions, orders, delivery, after-sale, brokerage, and special-order promotion remain Mall-owned. Continue other special-order Promotion families, production financial/order/refund reconciliation, explicit legacy commerce/referral/settlement-proof import, automatic fulfillment/refund revocation, non-equivalent provider replacement, tenant PNVS, encrypted secrets, public-bank grants, and other marketing separately. |
| Platform administration and governance | /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-*.module.ts | Legacy platform staff, tenant lifecycle/billing profiles, public question-bank grant/sync, SaaS plan/invoice/usage/overage/dunning, audit/export/alert/notification-channel, and permission objects. | Provides platform staff and governance over tenants, staff lifecycle, public-bank grants, SaaS plans, billing, usage, dunning, audits, alerts, and permissions. | System + Pay + Mall + Infra + CRM with Education extensions | System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging. | Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete. | System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration. | P1 | High access-control and financial-governance risk. | Permission matrix, platform-admin integration, cross-tenant negative, audit-redaction, billing/usage reconciliation, and alert/export tests. | pending migration | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java:12-20.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java:7-57. | Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement. |
| Commercialization and growth | /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-orders.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-payments.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-growth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-crm.module.ts | Legacy product/order/payment/event/entitlement/coupon/refund/reconciliation/commission/referral/points/dunning objects.<br>Target Mall/Pay/Member tables remain authoritative; Education adds only binding/entitlement records. | Supports paid products, fulfillment, entitlements, refunds, reconciliation, commissions, referrals, and CRM conversion without recreating platform ledgers. | Mall + Pay + Member + CRM with Education binding | Native Pay, Mall Product/Promotion Coupon/Seckill/Combination/Trade Order/Brokerage APIs, controllers, jobs, permissions and Vben pages; Member identity/points, CRM, Infra Job/MQ/audit. | Bounded Education binding/entitlement and activation redemption are proven. V4340V4360 activate/import Pay; V4370 activates Product; V4380 activates Coupon; V4390V4420 activate normal Trade order/checkout/delivery/after-sale; V4430 activates native two-level brokerage; V4440 activates empty native Seckill; V4450 activates empty native Combination activity/product/group state because the source has no group-buying equivalent. The source combination-question token remains Education content. Explicit legacy Product/code-coupon/order/refund/referral/settlement-proof import, automatic fulfillment/refund revocation, Bargain/Point and other Promotion families, dunning, and source CRM conversion remain open. | Mall Product/Promotion/Trade, Pay, Member, CRM, and Infra remain owners; Education owns product-to-learning binding, activation credentials, and access orchestration only. | P2 | High financial and authorization risk. | Delivered native Pay/Product/Coupon/Trade/Seckill/Combination tenant and PostgreSQL tests; still require reviewed legacy mapping, production reconciliation/runbooks, callback/idempotency, entitlement lifecycle, source settlement/proof/CRM mapping, and fulfillment tests. | partially migrated | Verified V4130 binding/entitlement through V4450 native Combination, native module tenant contracts, exact permissions/routes, composite references, unsafe-state checks, real service/statistics/stock/capacity isolation, and fail-closed legacy/deferred table adoption.<br>Verified native Pay, Product, Promotion Coupon/Seckill/Combination, Trade Order/After Sale/Brokerage APIs/pages are the public seams. | Keep activation codes separate from coupons; complete Bargain/Point and other Promotion families, explicit legacy commerce/referral/settlement-proof mapping, production reconciliation/runbooks and reviewed balances, automatic issuance/fulfillment/refund revocation, dunning, and CRM conversion contracts before broader paid-practice fulfillment. |
| Background processing, assets, and operational platform | /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts<br>/Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts<br>/Users/tiku1/code/tiku-backend/apps/worker/src/jobs/exports.ts<br>/Users/tiku1/code/tiku-backend/apps/asset-scanner/src/ | Legacy worker queues, leases, retries/dead letters, imports/exports, reconciliation, notification/audit, usage, and security scan state.<br>Target owns business state in domain modules and uses platform execution primitives; do not copy queue tables wholesale. | Preserves operational reliability for imports, exports, payments, CRM, scanning, notifications, retries, and audit while removing dependence on NestJS workers. | Infra platform plus owning domain modules | Infra Job, Redis MQ, File, locks, idempotency, logging, tracing, Excel utilities, tenant propagation. | Verified target primitives exist, but durable claim/lease/heartbeat/retry and malware-scanner equivalence are not proven. Education import/export business state is absent or not verified. | Infra Job/MQ/File/logging/observability plus owning Education/Pay/Mall/CRM handlers; scanner deployment or adapter ownership must be decided. | P1 | High operational and security risk. | Concurrent claim/lease/recovery, retries/dead letters, scan fail-closed, file access, tenant propagation, audit, and deployment smoke tests. | partially migrated | Verified /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts:24-220 and /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts:87-260.<br>Verified /Users/tiku1/code/tiku-backend/apps/asset-scanner/src/scanner.service.ts:23-50.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/RedisMQTemplate.java.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java. | Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing. |
| Secondary learning, media, AI, and engagement | /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/profile/ | Legacy scoreline, vocabulary, handbook, video entitlement/progress, recommendation, notification, badge, exam-date, and analytics objects. | Delivers selected recommendation, scoreline, vocabulary, handbook, video, AI, notification, badge, exam-date, and engagement experiences after ownership and priority are explicit. | Education plus AI/Infra/Member/System | AI services, Infra File/notifications, Member points/levels, Education authorization/projections. | Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition. | AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions. | P3 | Medium-to-high due to entitlement, media access, sensitive reporting, and unclear scope. | Per-capability contract, authorization, entitlement, export/redaction, and migration compatibility tests. | product decision required | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts:207-226.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts:431-464.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts:120-139.<br>Verified /Users/tiku1/code/tiku-backend/supabase/migrations/202606210009_content_import_vocabulary_handbook.sql.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java. | For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements. |

View File

@@ -0,0 +1,130 @@
# Legacy API Mapping
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
> Runtime mapping update 2026-08-01: EDU-027 keeps native Trade order/config contracts authoritative, EDU-028 keeps native Discount/Reward APIs authoritative, EDU-029 activates delivery, EDU-030 activates after-sale/Pay Refund, EDU-031 activates Member-backed brokerage, EDU-032 activates native Promotion Seckill, and EDU-033 activates native Promotion Combination activity/group/Trade Order contracts. Education adds no shadow commerce, refund, commission, or special-order API. Configured target Pay runtime, Bargain/Point and other special orders, legacy commerce/referral/settlement-proof mapping, fulfillment/revocation, and deployed checkout/refund/commission/promotion evidence remain open.
This Phase 0 artifact maps API families rather than all 342 operations. Endpoint-level method/path/request/response mapping remains required before implementing each family.
## Tenant resolution, identity, and student context
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/resolver.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** System + Member + framework tenant support with an Education context adapter
- **Reuse:** System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations.
- **Migration conclusion:** decision complete; implementation pending EDU-004
- **Selected contract:** Public inputs are **Tenant Locator Claims**, not authenticated identity. Production browser routing derives a host-only claim from valid HTTP(S) `Origin`, falling back to `Referer`; both are forgeable by non-browser callers and provide browser UX consistency only. A supplied hostname may confirm that claim, and disagreement is a conflict. Headless clients use `tenantHandle`, explicitly defined as the target's unique System tenant `name` under a case-sensitive constrained public contract because no distinct stable Tenant Code exists. Legacy public `tenantName` is rejected rather than silently aliased.
- **Threat model:** Successful resolution returns tenant ID/display name and therefore permits available-tenant existence probing. Only unknown, disabled, and expired tenants are indistinguishable. Reuse public throttling/ingress controls and structured abuse metrics; a deployment requiring spoof resistance needs a future authenticated/signed locator, not trust in Origin/Referer or forwarding-header controls.
- **Normalization and compatibility:** Host identity is lowercase, trimmed, trailing-dot-free, bracket-free for IPv6, and independent of all ports. DNS hosts, IPv4, and IPv6 are accepted when valid; credentials, paths, multi-value input, malformed authorities, and unsupported schemes are rejected. Canonical `system_tenant.websites` values for this resolver are host-only. Scheme/path/port-bearing stored values do not silently normalize or match; configuration correction is required, or a separate Flyway/data ticket if automated correction is later approved.
- **Local activation:** Only `yudao.education.tenant-resolution.local-development-enabled=true` enables local/request-host fallback; default and absence are false, and profiles are not authoritative. With the flag true, code-less configured local-host resolution is allowed and an explicit handle takes precedence.
- **Authenticated context:** `/education/context` accepts only a Student Principal: an authenticated `LoginUser` with `userType == UserTypeEnum.MEMBER`. User and tenant IDs remain security/tenant-context derived. `TenantSecurityWebFilter` already fills a missing tenant from the authenticated principal, rejects principal/request-tenant mismatch, requires a tenant for non-ignored URLs, and validates tenant availability; EDU-004 preserves rather than duplicates these checks.
- **Ownership and seam:** Login-method metadata belongs to Member authentication, not System tenant metadata or Education. EDU-004 removes/deprecates Education `loginMethods` unless a minimal Member-owned interface is proven necessary. Retain generic System-owned `TenantCommonApi`; make lookup methods required and add focused `TenantApiImpl` contract tests. `EducationTenantController` remains the public claim-consistency/redaction adapter.
- **Exact public wire contract:** Framework business responses remain HTTP 200. Invalid/malformed/missing/local-forbidden claim is code `1005001003`, message `租户识别请求无效`, null data. Domain/handle or requested-host conflict is code `1005001008`, message `租户识别信息冲突`, null data. Unknown/disabled/expired is code `1005001004`, message `当前租户不可用`, null data, with identical shape. Success is code 0 and data contains only `tenantId` and `displayName`; never expose or echo handle, websites, expiry, package, status, private config, or login methods.
- **Required EDU-004 verification:** Education HTTP tests cover Origin resolution, Referer fallback, forged-header threat-model naming, malformed Origin, Origin/requested-host conflict, untrusted arbitrary hostname, explicit handle, legacy tenantName rejection, unavailable-state exact wire equivalence, host normalization, canonical/non-canonical website behavior, local flag false/true and precedence, domain/handle conflict, anonymous/ADMIN/MEMBER context, redaction, and abuse-control attachment/metrics where a reusable seam exists. System owns adapter contract tests; framework owns existing missing-tenant and authenticated mismatch tests.
- **Decision evidence:** [`issues/EDU-003-tenant-resolution-decision.md`](issues/EDU-003-tenant-resolution-decision.md). Static only; no production or database change was made.
## Student core learning loop
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-113, /Users/tiku1/code/tiku-backend/apps/api/src/features/learning/use-cases.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/learning/access.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education
- **Reuse:** Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established.
- **Required verification:** Provider-neutral tests across Scalar and Java, browsing/collection/practice-create/restore safe projections, malformed/unavailable/unpublished fail-closed cases, cross-tenant cases, and PostgreSQL concurrent same-key/different-key submit tests.
- **Open decision:** Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice.
## Education catalog and question content
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/catalog.module.ts:74-138, /Users/tiku1/code/tiku-backend/apps/nest/tenant-content.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education
- **Reuse:** Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement.
- **Required verification:** Inventory every mapper, provider contract tests, invalid graph insert tests, malformed/unpublished tests, PostgreSQL Flyway syntax/resource-packaging checks, and runtime migration evidence only when executed.
- **Open decision:** Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual.
## Auth, student profile, and extended learning
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/profile/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Member + System + Education, with Infra composition
- **Reuse:** Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization.
- **Migration conclusion:** pending migration
- **Contract gap:** Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified.
- **Required verification:** Endpoint/API mapping, principal and tenant tests, profile redaction, progress/report compatibility, vocabulary state transitions, and explicit retired/product-decision checks.
- **Open decision:** For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges.
## Tenant education operations, appearance, integrations, secrets, and codes
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-classes.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-appearance.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-integrations.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-secrets.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-codes.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education + System + Member + Mall/Pay + Infra
- **Reuse:** System Tenant/RBAC/DataPermission/AdminUserApi, Member relationships, Mall/Pay/Member APIs, Infra secret/file/message/audit facilities.
- **Migration conclusion:** partially migrated — class/supervision, appearance, native Pay/System integration and ledgers, learning activation codes, native Mall Product/Coupon, and V4390V4450 normal Trade order/checkout/delivery/after-sale/brokerage/seckill/combination activation delivered
- **Delivered activation-code contracts:** Admin `GET /education/activation-code/batch/page`, `POST /batch`, `PUT /batch/{id}`, `POST /batch/{id}/generate`, `GET /code/page`, and `PUT /code/{id}/disable`; Member-only app `POST /education/activation-code/check` and `POST /education/activation-code/redeem`. Query/manage/generate permissions are independent. Generation returns plaintext once, while persistence and later reads expose only digest/mask. Redemption locks the code row and composes `EducationEntitlementService` with `sourceSystem=ACTIVATION_CODE`.
- **Delivered legacy Pay import contracts:** Pay-owned `POST /pay/legacy-account-import/import` requires App+Channel create permissions and maps one explicitly reviewed `tenant_collect` WeChat/Alipay manifest through native Pay services. `GET /pay/legacy-account-import/page` requires both query permissions and returns the tenant-filtered redacted audit. Same source-account/checksum replays; checksum conflicts, non-equivalent modes/providers, channel-family mismatch, ambiguous rotating keys, and unsafe Alipay endpoints fail closed. The existing Pay App Vben page owns the import modal.
- **Delivered native Pay transaction contracts:** Existing `/pay/order`, `/pay/refund`, and `/pay/notify` query/detail/export/callback contracts and the `pay/order/index`, `pay/refund/index`, and `pay/notify/index` Vben pages are reused. V4340 supplies tenant-scoped PostgreSQL order/extension/refund/notification tables and composite tenant foreign keys; callback processing retains channel-derived `TenantUtils` context and notification retries retain `@TenantJob` execution.
- **Delivered legacy transaction bridge:** `POST /pay/legacy-transaction-import/import` accepts one terminal, reconciled order/payment/refund manifest under `pay:legacy-transaction:import`; `GET /pay/legacy-transaction-import/page` exposes redacted tenant audit under `pay:legacy-transaction:query`. It requires an EDU-021 account mapping, exact cent/status/provider reconciliation, source UUID/checksum idempotency, and explicit optional native Member ID. It writes native Pay ledgers without SDK calls, callbacks, notifications, raw payloads, or error originals. The native order page owns the import/history modal.
- **Delivered native Transfer/Wallet contracts:** Existing `/pay/transfer`, `/pay/wallet`, `/pay/wallet-transaction`, `/pay/wallet-recharge`, and `/pay/wallet-recharge-package` controllers remain authoritative. V4360 activates empty tenant-scoped native ledgers and existing `pay/transfer/index`, `pay/wallet/balance/index`, and `pay/wallet/rechargePackage/index` pages. Five data objects use `TenantBaseDO`, Transfer sync retains `@TenantJob`, wallet locks include tenant ID, administrator reductions use conditional subtraction, and recharge refund has a dedicated permission. No legacy wallet balance is inferred.
- **Delivered native Product contracts:** Existing `/product/brand`, `/product/category`, `/product/property`, `/product/property/value`, `/product/spu`, `/product/comment`, `/product/favorite`, and `/product/browse-history` controllers remain authoritative. V4370 activates nine tenant-scoped Product tables and the existing SPU, Category, Brand, Property, and Comment Vben pages. Nine Product data objects use `TenantBaseDO`; PostgreSQL composite tenant references enforce the catalog graph. The legacy display-only `products` projection is not automatically imported.
- **Delivered native Coupon contracts:** Existing `/promotion/coupon-template`, `/promotion/coupon`, and app coupon controllers remain authoritative. V4380 activates tenant-scoped template/issued-instance persistence, Product SPU/category scope validation, Member lookup/issuance, registration issuance, expiry processing, and the existing template/record Vben pages. Both coupon records use `TenantBaseDO`; the template reference is tenant-qualified. Legacy code campaigns/redemptions are not automatically imported.
- **Delivered native Trade contracts:** Existing `/trade/order`, `/trade/config`, `/app-api/trade/order`, and `/app-api/trade/cart` controllers remain authoritative. V4390 activates tenant-scoped Order/Item/Log/Cart/Config persistence, native `TradeOrderApiImpl`, exact order/config permissions, and the existing Vben pages. Legacy aggregate orders are not automatically imported.
- **Delivered native checkout Promotion contracts:** Existing `/promotion/discount-activity`, `/promotion/reward-activity`, `DiscountActivityApi`, and `RewardActivityApi` remain authoritative. V4400 activates tenant-scoped Discount Activity/Product and Reward Activity persistence, exact action permissions, and the two existing Vben pages. Real empty API lookups are proven on PostgreSQL; no legacy campaigns are inferred.
- **Delivered native delivery contracts:** Existing `/trade/delivery/express`, `/trade/delivery/express-template`, `/trade/delivery/pick-up-store`, app delivery reads, and `TradeDeliveryPriceCalculator` remain authoritative. V4410 activates tenant-scoped company/template/rule/store persistence, Product/Order references, exact permissions, three existing Vben pages, and a real PostgreSQL express-fee calculation; no source delivery data is inferred.
- **Delivered native after-sale contracts:** Existing Member application/cancel/delivery reads, `/trade/after-sale/page`, `/get-detail`, `/agree`, `/disagree`, `/receive`, `/refuse`, `/refund`, Pay refund callback handling, and operation logs remain authoritative. V4420 supplies tenant-scoped persistence/references and exact permissions. Vben now sends `auditReason`, requires `refuseMemo`, shows the application `createTime`, and permission-guards every action.
- **Legacy refund mapping:** `commerce_refund_requests` and `commerce_refund_events` are aggregate UUID records without verified native Member, Order Item, Product/SKU, return-logistics, or Pay Refund identities. V4420 deliberately imports none; mapping follows explicit legacy Product/Member/Order Item reconciliation.
- **Delivered native brokerage contracts:** Existing app/admin relationship, eligibility, team/rank, commission-record, freeze/unfreeze/cancel, withdrawal/audit, Pay Transfer callback, and scheduled job contracts remain authoritative. V4430 supplies tenant-owned persistence, references, exact eight permissions, and corrected user/record/withdrawal pages. Immediate settlements now participate in time-range statistics.
- **Legacy referral/settlement mapping:** Source referral codes/leads/team edges/tracks/QR/CRM assignment and UUID settlement/item/proof/export rows lack verified native Member, Order, relationship, Pay Transfer, and evidence identities. V4430 deliberately imports none; they remain explicit mapping/import work rather than being treated as native-equivalent.
- **Delivered native Seckill contracts:** Existing `/promotion/seckill-config`, `/promotion/seckill-activity`, supporting app reads, Product lookups, atomic stock updates, and Trade Order seckill fields remain authoritative. V4440 supplies empty tenant-owned time/activity/product persistence, Product/Trade references, consistency triggers, exact nine permissions, and corrected activity/config pages. Duplicate SKU, price/stock overrun, invalid time/limit inputs, unsafe restoration, and deletion of an in-use slot fail closed.
- **Source Seckill disposition:** Repository-wide source inventory found no seckill capability. V4440 deliberately starts empty; ordinary products, coupons, and aggregate orders are not reinterpreted as activities.
- **Delivered native Combination contracts:** Existing `/promotion/combination-activity`, `/promotion/combination-record`, supporting app reads/jobs, Product/Member lookups, and Trade Order combination fields remain authoritative. V4450 supplies empty tenant-owned activity/product/record persistence, capacity and reference triggers, exact six permissions, and corrected activity/record pages. Duplicate/mismatched SKUs, price/time/limit errors, cross-activity heads, over-capacity joins, inconsistent orders, and deletion with records fail closed.
- **Source Combination disposition:** The source `combination` token is an education combination-question type, not group buying. V4450 deliberately starts empty; no content question, ordinary product, or aggregate order is reinterpreted as a promotion group.
- **Contract gap:** Native Pay, Product, Coupon, normal Trade order, Promotion discount/reward/seckill/combination, Trade delivery, Trade after-sale, and native Trade brokerage administration are operational. Production bulk export/runbooks, reviewed UUID-to-Member/opening-balance artifacts, explicit legacy commerce/referral/settlement-proof mapping, Bargain/Point and other special-order activation, provider settlement equivalence, XPay/Xunhu replacement, and generic credential encryption remain open. V4310 does not claim legacy activation-code data import. Domains remain System Tenant websites. Tenant PNVS, private encrypted secrets, fulfillment, refund-to-entitlement revocation, and other marketing surfaces remain separate.
- **Required verification:** Permission matrix, row-scope negatives, secret redaction/rotation, integration authorization, legacy import idempotency/audit, and cross-tenant tests. Pay/Coupon/Trade/Seckill/Combination tests are delivered through V4450, including composite after-sale/brokerage/special-order references, state/amount/stock/capacity validation, exact menus, real tenant-isolated service/statistics/concurrency reads and writes, and fail-closed adoption.
- **Open decision:** Compose payments through Pay, products/coupons/promotions/orders/refunds/commissions through Mall Product/Promotion/Trade, and auth providers through System/Member. Continue Bargain/Point and other Promotion families, explicit legacy commerce/referral/settlement-proof imports, production financial/order/refund runbooks, reviewed balances, non-equivalent provider replacement, private secret rotation, automatic fulfillment, and refund revocation separately. Activation codes remain Education-owned learning credentials composed with Mall SPU binding and the entitlement pipeline.
## Platform administration and governance
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-*.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** System + Pay + Mall + Infra + CRM with Education extensions
- **Reuse:** System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging.
- **Migration conclusion:** pending migration
- **Contract gap:** Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete.
- **Required verification:** Permission matrix, platform-admin integration, cross-tenant negative, audit-redaction, billing/usage reconciliation, and alert/export tests.
- **Open decision:** Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement.
## Commercialization and growth
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-orders.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-payments.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-growth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-crm.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Mall + Pay + Member + CRM with Education binding
- **Reuse:** Mall/Pay DTO APIs, Member identity/entitlement/points, CRM services, Infra Job/MQ/audit.
- **Migration conclusion:** partially migrated — bounded Education binding/entitlement and activation redemption, native Pay ledgers, terminal legacy aggregate import, tenant-scoped native Mall Product persistence/UI, and native Promotion Coupon template/instance persistence/UI are proven; commerce orchestration remains open
- **Contract gap:** V4340 provides tenant-scoped native transaction ledgers, callbacks, retry tasks, and existing admin UI; V4350 adds bounded terminal import; V4360 activates native Transfer/Wallet ledgers; V4370 activates native Product; V4380 activates native coupon templates/instances without translating legacy code campaigns. These slices do not connect successful purchases to Education entitlements. Explicit legacy product/code-coupon import, production bulk migration, reviewed opening balances, automatic Pay/Mall fulfillment, refund-driven entitlement revocation, legacy activation-code import, Trade/other Promotion families, settlement reconciliation, commissions, dunning, and referral semantics remain unproven. One prior evidence path was malformed; corrected source location is /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- **Required verification:** Native order/refund/notify service, tenant-database, and terminal amount/status/provider mapping contracts are delivered. Production export/reconciliation evidence, callback/idempotency integration, entitlement lifecycle, settlement reconciliation, and education fulfillment contract tests remain required.
- **Open decision:** Keep V4310 activation codes separate from Mall Promotion coupons; confirm automatic issuance, callback, refund, reconciliation, coupon, commission, and referral contracts before broader commerce migration.
## Background processing, assets, and operational platform
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts, /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts, /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/exports.ts, /Users/tiku1/code/tiku-backend/apps/asset-scanner/src/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Infra platform plus owning domain modules
- **Reuse:** Infra Job, Redis MQ, File, locks, idempotency, logging, tracing, Excel utilities, tenant propagation.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified target primitives exist, but durable claim/lease/heartbeat/retry and malware-scanner equivalence are not proven. Education import/export business state is absent or not verified.
- **Required verification:** Concurrent claim/lease/recovery, retries/dead letters, scan fail-closed, file access, tenant propagation, audit, and deployment smoke tests.
- **Open decision:** Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing.
## Secondary learning, media, AI, and engagement
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/profile/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education plus AI/Infra/Member/System
- **Reuse:** AI services, Infra File/notifications, Member points/levels, Education authorization/projections.
- **Migration conclusion:** product decision required
- **Contract gap:** Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition.
- **Required verification:** Per-capability contract, authorization, entitlement, export/redaction, and migration compatibility tests.
- **Open decision:** For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements.

View File

@@ -0,0 +1,147 @@
# Database Object Mapping
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
> Runtime mapping update 2026-08-01: V4390/EDU-027 owns the native Trade order core, V4400/EDU-028 owns normal-checkout Discount/Reward persistence, V4410/EDU-029 owns delivery, V4420/EDU-030 owns after-sale/log persistence, and V4430/EDU-031 owns brokerage persistence. V4440/EDU-032 adds tenant-composite Seckill tables; V4450/EDU-033 adds tenant-composite `promotion_combination_activity`, `promotion_combination_product`, and `promotion_combination_record` plus tenant-qualified Product, Trade Order, order-record, and head references. The source has neither seckill nor group-buying state, so these tables start empty; its combination-question token stays Education content. Legacy aggregate orders/refunds/campaigns and UUID referral/settlement/proof objects remain unmapped; Bargain/Point and other Promotion tables require dedicated tenant-safe migrations.
## Global disposition rules
- Ordinary education tables and constraints become immutable module-owned PostgreSQL Flyway migrations.
- Supabase RLS maps primarily to framework tenant isolation and application authorization, not copied RLS.
- RPCs/functions map to Java services unless database execution is demonstrably the better boundary.
- Triggers map to transactions, events, jobs, or audit facilities unless database enforcement is required.
- Storage maps to Infra File; auth schema maps to Member/System.
- Current manual SQL is not considered executed or active Flyway history without runtime evidence.
## Tenant resolution, identity, and student context
### Source and target objects
- System tenant and website/domain records are authoritative.
- Legacy tenant, domain, branding, identity, membership, and RLS objects are reference-only and must not be copied mechanically.
### Disposition
- **Target owner:** System + Member + framework tenant support with an Education context adapter
- **Current status:** partially migrated
- **Required cross-module treatment:** System/framework tenant and security boundaries remain authoritative; Education adapts them. EDU-003 retains generic required `TenantCommonApi` lookup methods and assigns Member-only context enforcement to EDU-004.
- **Risk:** High: the unauthenticated resolver permits available-tenant existence probing, while wrong authenticated principal/tenant handling can cross security boundaries.
- **Accepted decision:** Browser headers are forgeable context claims; headless clients use the constrained System-name Public Tenant Handle; unknown/disabled/expired failures are identical. Canonical website values for Public Tenant Resolution are normalized host-only strings. Scheme/path/port-bearing stored values are not silently normalized and require configuration correction. If automated correction is later required, create a separate Flyway/data ticket and invoke `flyway-postgresql`; EDU-003/EDU-004 authorize no database change.
## Student core learning loop
### Source and target objects
- Education catalog and practice/report/idempotency/wrong-question/favorite tables.
- Verified: native catalog V4020 is a dirty module resource; practice/report/idempotency DDL is currently in untracked sql/postgresql/education files rather than proven active Flyway history.
### Disposition
- **Target owner:** Education
- **Current status:** partially migrated
- **Required cross-module treatment:** Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision.
- **Risk:** High: corrupt assessment content, mode-dependent behavior, duplicate state transitions, answer leakage, or unauthorized access.
- **Decision still required:** Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice.
## Education catalog and question content
### Source and target objects
- V4020 native catalog tables for regions, schools, majors, subjects, categories, banks, questions, versions, content, collections, blueprints, and bindings.
- Legacy catalog/question/content/asset tables, constraints, functions, triggers, grants, and RLS are reference objects requiring semantic mapping.
### Disposition
- **Target owner:** Education
- **Current status:** partially migrated
- **Required cross-module treatment:** Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided.
- **Risk:** High if manual scope is bypassed or invalid cross-tenant/public relationships are admitted.
- **Decision still required:** Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual.
## Auth, student profile, and extended learning
### Source and target objects
- Legacy auth/session/verification/OAuth/phone-binding objects.
- Legacy profile/check-in/points/tasks/exchange/notifications/badges/feedback/exam-countdown objects.
- Legacy leaderboard/history/report/stats/trend/vocabulary progress/review/favorites/stats objects.
### Disposition
- **Target owner:** Member + System + Education, with Infra composition
- **Current status:** pending migration
- **Required cross-module treatment:** Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership.
- **Risk:** High for auth compatibility and medium for omitted student progress/profile behavior.
- **Decision still required:** For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges.
## Tenant education operations, appearance, integrations, secrets, and codes
### Source and target objects
- Legacy classes, student relationships, supervision, roles/configuration, branding/settings/themes, domains, payment accounts, auth-provider configuration, tenant secrets, activation codes, coupons/redemptions, integrations, and marketing objects.
### Disposition
- **Target owner:** Education + System + Member + Mall/Pay + Infra
- **Current status:** partially migrated — V4290 owns appearance/settings/theme extensions; V4300 reuses native Pay/System administration; V4310 adds activation-code tables; V4320V4360 create/adopt native Pay configuration, transaction, Transfer, and Wallet tables plus bounded import audits; V4370 creates native Product; V4380 creates Coupon; V4390V4430 create tenant-scoped normal Trade order, checkout Promotion, delivery, after-sale/log, and brokerage tables; V4440 creates Seckill tables and V4450 creates Combination activity/product/record tables plus their Trade bridges
- **Required cross-module treatment:** System RBAC/DataPermission and tenant configuration are reused. Native Pay tables remain Pay-owned and tenant-aware. Native Product tables remain Mall-owned, all nine records use `TenantBaseDO`, and composite tenant references protect the catalog graph. Native Promotion Coupon, Seckill, and Combination tables remain Mall-owned and tenant-qualified; Product validates SPU/SKU scope, Member supplies principals, and Trade owns order lifecycle. V4330 stores digests/mapping notes but no credentials. Tenant-scoped `system_social_client` remains System-owned. Global `system_sms_channel` is not a safe substitute for tenant PNVS. Education owns resource binding and entitlement state; activation-code rows store digest/mask, never plaintext.
- **Risk:** High: admin scope, secret leakage, payment configuration, promotion stock, and code redemption errors.
- **Decision still required:** Domains stay in System. EDU-021EDU-033 resolve bounded Pay/Product/Coupon/normal-Trade/delivery/after-sale/native-brokerage/Seckill/Combination activation. Decide Bargain/Point and other Promotion families, explicit legacy product/code-coupon/order/refund/referral/settlement-proof import, production export/runbooks, Member mapping and reviewed opening balances, non-equivalent providers, fulfillment/refund revocation, legacy activation-code import, tenant PNVS, encrypted generic private secrets, public-bank grants, and other marketing as separate contracts.
## Platform administration and governance
### Source and target objects
- Legacy platform staff, tenant lifecycle/billing profiles, public question-bank grant/sync, SaaS plan/invoice/usage/overage/dunning, audit/export/alert/notification-channel, and permission objects.
### Disposition
- **Target owner:** System + Pay + Mall + Infra + CRM with Education extensions
- **Current status:** pending migration
- **Required cross-module treatment:** System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration.
- **Risk:** High access-control and financial-governance risk.
- **Decision still required:** Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement.
## Commercialization and growth
### Source and target objects
- Legacy product/order/payment/event/entitlement/coupon/refund/reconciliation/commission/referral/points/dunning objects.
- Target Mall/Pay/Member tables remain authoritative; Education may add minimal binding records.
### Disposition
- **Target owner:** Mall + Pay + Member + CRM with Education binding
- **Current status:** partially migrated — product binding, entitlement events, activation-code redemption, native Pay ledgers/import, Product/Coupon, and V4390V4450 normal Trade order/checkout/delivery/after-sale/brokerage/Seckill/Combination persistence/UI are implemented; explicit legacy product/code-coupon/order/refund/referral/settlement-proof import, Bargain/Point and other Promotion families, production bulk migration, and broader commerce orchestration remain open
- **Required cross-module treatment:** Mall Trade/Product/Promotion, Pay transaction/callback APIs, Member entitlement/points, CRM, Infra jobs/events/audit; Education owns product-to-education bindings and fulfillment orchestration only. Legacy financial rows may enter Pay only through EDU-023's reviewed aggregate contract; incompatible legacy code campaigns may not be silently copied into native coupon instances.
- **Risk:** High financial and authorization risk.
- **Decision still required:** Keep activation codes as Education learning credentials and coupons/seckill as Mall Promotion objects; native Trade owns new two-level commissions and special-order order fields, while other Promotion families, source CRM referral and settlement-proof import, automatic issuance, revocation callbacks, refunds, and reconciliation require separate contracts before broader paid-practice fulfillment.
## Background processing, assets, and operational platform
### Source and target objects
- Legacy worker queues, leases, retries/dead letters, imports/exports, reconciliation, notification/audit, usage, and security scan state.
- Target owns business state in domain modules and uses platform execution primitives; do not copy queue tables wholesale.
### Disposition
- **Target owner:** Infra platform plus owning domain modules
- **Current status:** partially migrated
- **Required cross-module treatment:** Infra Job/MQ/File/logging/observability plus owning Education/Pay/Mall/CRM handlers; scanner deployment or adapter ownership must be decided.
- **Risk:** High operational and security risk.
- **Decision still required:** Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing.
## Secondary learning, media, AI, and engagement
### Source and target objects
- Legacy scoreline, vocabulary, handbook, video entitlement/progress, recommendation, notification, badge, exam-date, and analytics objects.
### Disposition
- **Target owner:** Education plus AI/Infra/Member/System
- **Current status:** product decision required
- **Required cross-module treatment:** AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions.
- **Risk:** Medium-to-high due to entitlement, media access, sensitive reporting, and unclear scope.
- **Decision still required:** For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements.

View File

@@ -0,0 +1,71 @@
# Module Reuse Map
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
> Runtime reuse update 2026-08-01: EDU-027 activates Mall Trade order/cart/config, EDU-028 reuses native Promotion Discount/Reward, EDU-029 reuses delivery, EDU-030 reuses after-sale/Pay Refund, and EDU-031 reuses native Trade brokerage. EDU-032 reuses native Promotion Seckill; EDU-033 reuses native Combination app/admin controllers, services, mappers, jobs, Product/Member lookups, group lifecycle, Trade Order bridge, RBAC, and corrected Vben pages while supplying tenant-safe PostgreSQL persistence. Education owns no duplicate commerce, promotion, delivery, refund, commission, stock, or group aggregate.
Education must call public APIs, framework extension points, or events. It must not depend on another module's internal ServiceImpl, Mapper, or DO.
## Tenant resolution, identity, and student context
- **Target owner:** System + Member + framework tenant support with an Education context adapter
- **Public/framework capability to reuse:** System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations.
- **Education-owned gap:** Verified: EducationTenantController accepts caller-supplied hostname or tenantName under @PermitAll and does not enforce the source resolver's production Origin/Referer/request-host binding. Verified: TenantSecurityWebFilter already rejects authenticated LoginUser/request-tenant mismatches and requires a tenant on non-ignored URLs. Verified: EducationContextController checks login presence and tenant validity but not LoginUser.userType. Verified: hostname documentation says no port while implementation preserves legal ports and tests expect port preservation. Inference: arbitrary tenant discovery, admin-principal acceptance, and port inconsistency are P0/P1 trust-boundary risks until policy is fixed.
- **Allowed external-module change:** System/framework tenant and security boundaries remain authoritative; Education should only adapt them. A generic tenant lookup contract and possibly Member-only context policy require explicit decisions.
## Student core learning loop
- **Target owner:** Education
- **Public/framework capability to reuse:** Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims.
- **Education-owned gap:** Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established.
- **Allowed external-module change:** Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision.
## Education catalog and question content
- **Target owner:** Education
- **Public/framework capability to reuse:** Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets.
- **Education-owned gap:** Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement.
- **Allowed external-module change:** Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided.
## Auth, student profile, and extended learning
- **Target owner:** Member + System + Education, with Infra composition
- **Public/framework capability to reuse:** Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization.
- **Education-owned gap:** Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified.
- **Allowed external-module change:** Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership.
## Tenant education operations, appearance, integrations, secrets, and codes
- **Target owner:** Education + System + Member + Mall/Pay + Infra
- **Public/framework capability to reuse:** System Tenant/RBAC/DataPermission/AdminUserApi, Member relationships, Mall/Pay/Member APIs, Infra secret/file/message/audit facilities.
- **Education-owned gap:** V4290 owns presentation extensions. V4300V4380 expose and activate native Pay/System/Product/Promotion administration without Education shadow ledgers. V4390V4430 activate native Trade order core, Discount/Reward checkout dependencies, delivery, after-sale, and brokerage; V4440 activates native Seckill and V4450 activates native Combination activity, SKU pricing, group records, capacity protection, and Trade bridge with their real APIs/services and existing pages. Education adds no financial, product, coupon, cart, order, promotion, delivery, refund, commission, stock, or group ledger. V4310 adds only activation-code state and composes Mall-owned SPU binding, Member authentication, and the existing entitlement event. Explicit legacy product/code-coupon/order/refund/referral/settlement-proof import, Bargain/Point and other Promotion families, production bulk export/runbooks and reviewed wallet opening balances, non-equivalent provider/mode replacement, automatic fulfillment/refund revocation, and legacy activation-code import remain unhandled; global System SMS Channel cannot satisfy per-tenant PNVS. Infra/private generic secrets remain separate gaps.
- **Allowed external-module change:** System RBAC/DataPermission and tenant configuration are reused; Mall owns products/coupons/promotions/orders, Pay owns payment, Member owns principals, and Infra owns secrets/messaging/files. Education owns learning relationships/configuration, activation-code credentials, and access orchestration without duplicating those platform ledgers.
## Platform administration and governance
- **Target owner:** System + Pay + Mall + Infra + CRM with Education extensions
- **Public/framework capability to reuse:** System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging.
- **Education-owned gap:** Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete.
- **Allowed external-module change:** System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration.
## Commercialization and growth
- **Target owner:** Mall + Pay + Member + CRM with Education binding
- **Public/framework capability to reuse:** Mall/Pay DTO APIs, Member identity/entitlement/points, CRM services, Infra Job/MQ/audit.
- **Education-owned gap:** Bounded resource binding, entitlement issuance/revocation, activation-code redemption, native Pay ledgers/import, native Product/Coupon, and V4390V4450 Trade order/checkout/delivery/after-sale/brokerage/Seckill/Combination persistence/admin are proven. Legacy display-only Product, incompatible code campaigns, aggregate orders/refunds, and UUID referral/settlement/proof objects are not imported; the source has no seckill or group-buying state to import, and its combination-question token remains Education content. Bargain/Point and other Promotion families, production bulk migration and reviewed balances remain open, and successful native Pay/Trade events are not yet composed into automatic Education fulfillment or refund revocation. Native two-level commission, Seckill stock isolation, and Combination capacity isolation are proven, but source CRM referral, proof/export, dunning, and settlement equivalence remain unproven. One prior evidence path was malformed; corrected source location is /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- **Allowed external-module change:** Mall Trade/Product/Promotion, Pay, Member entitlement/points, CRM, Infra jobs/events/audit; Education owns product-to-education bindings and fulfillment orchestration only.
## Background processing, assets, and operational platform
- **Target owner:** Infra platform plus owning domain modules
- **Public/framework capability to reuse:** Public Infra File APIs and framework tenant context; generic locks, logging, tracing, and scheduling remain platform capabilities when exposed through public contracts.
- **Education-owned delivered scope:** EDU-011 owns tenant import asset metadata and import jobs, including the five states `PREVIEW`, `PENDING`, `PROCESSING`, `COMPLETED`, and `FAILED`; lease/heartbeat/expired-lease recovery; bounded attempts; and duplicate safety. V4130 is the only delivered EDU-011 migration. Scanner absence defaults to fail-closed `UNAVAILABLE`; CSV/XLSX preview is metadata-only when no parser is available; execution requires both a clean scan and executable parsed content.
- **Deferred scope:** The export boundary currently defines request redaction only—answers and private fields are excluded—but generates no export file or export job. Production scanner integration, full parser availability, retention automation, dead-letter/operator tooling, partial-row reporting, and legacy asset migration remain deferred.
- **Allowed external-module change:** Education may call public Infra APIs only. It must not depend on Infra DOs, mappers, `ServiceImpl` classes, or implementation packages, and it does not move Education job state into Infra.
## Secondary learning, media, AI, and engagement
- **Target owner:** Education plus AI/Infra/Member/System
- **Public/framework capability to reuse:** AI services, Infra File/notifications, Member points/levels, Education authorization/projections.
- **Education-owned gap:** Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition.
- **Allowed external-module change:** AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions.

View File

@@ -0,0 +1,34 @@
# Commit Review: 11e9cc68547cf271b9d5de60bf41b3f71899d1db (11e9cc6), target repository
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Retain
- Education reactor registration, Member/server activation, Education server dependency, allocated error-code range, and XML correction unless later evidence disproves necessity.
## Adjust
- Treat historical MySQL schema/seed/rollback as archival or explicitly manual only; it is not a PostgreSQL/Flyway delivery path.
- If global menu entries 6800/6801 remain required, replace them with an approved Education-owned PostgreSQL Flyway seed migration.
- Remove operational documentation that invokes mysql or destructive rollback scripts; use forward correction and audited administrative procedures.
## Replace
- Required seed behavior with higher-version immutable PostgreSQL Flyway migration.
## Remove by forward correction
- README/runbook claims of MySQL active delivery and destructive rollback.
- The MySQL menu seed as active delivery after approved Flyway replacement.
## Pending decisions
- Whether menu entries 6800/6801 remain product scope.
- Whether historical sql/mysql/education artifacts remain labeled archival/manual material.
## Evidence
- Verified /Users/tiku1/code/ruoyi-vue-pro/pom.xml:18-19.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-server/pom.xml:35-47.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java:31-46.
- Verified /Users/tiku1/code/ruoyi-vue-pro/sql/mysql/education/000-education-schema.sql:1-5, 000-education-seed.sql:1-15, 000-education-rollback.sql:1-8.

View File

@@ -0,0 +1,37 @@
# Commit Review: 0f846fdaf5377f00347b05b9950b53f92e4dc6df (0f846fd), target repository
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Retain
- TenantApiImpl public-service dependency, @PermitAll/@TenantIgnore pre-login route subject to security tests, and context derived from framework state rather than request tenant/user IDs.
## Adjust
- Retain TenantCommonApi/TenantApiImpl as a candidate generic lookup seam, but add contract tests and resolve DTO/null/serialization compatibility.
- Replace unsupported default methods with abstract methods or a separate optional capability interface unless compatibility evidence requires them.
- Make public resolution origin-binding, principal policy, port policy, and loginMethods ownership explicit.
- Correct contradictory hostname documentation and test filter-chain/API behavior.
## Replace
- Unsupported default-method expansion with the selected interface design.
## Remove by forward correction
- Contradictory hostname/loginMethods documentation after policy decision.
## Pending decisions
- Public origin-binding versus intentionally public lookup.
- Member-only context versus System/admin support.
- Host-only versus authority-with-port identity.
- Platform ownership of loginMethods and public error taxonomy.
## Evidence
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java:28-56.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/dto/TenantRespDTO.java:14-42.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/tenant/TenantApiImpl.java:19-49.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:51-177 and /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/EducationContextController.java:43-57.
- Verified mismatch rejection in /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java:66-105.

View File

@@ -0,0 +1,190 @@
# Architecture and Product Decisions
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Decisions and constraints established by static evidence
### EDU-003 accepted decision — tenant resolution and student principal
The durable rationale, threat model, exact wire contract, compatibility policy, and EDU-004 test matrix are recorded in [`issues/EDU-003-tenant-resolution-decision.md`](issues/EDU-003-tenant-resolution-decision.md).
1. A **Tenant Locator Claim** is unauthenticated input, not identity. Browser `Origin` then `Referer` provides browser-context consistency evidence but is forgeable by non-browser callers.
2. A supplied hostname may only confirm browser-context evidence; disagreement is a conflict. Preserving browser headers or rejecting forwarding-header forgery does not authenticate `Origin`/`Referer`.
3. Headless clients use `tenantHandle`. The target currently has no distinct stable Tenant Code, so this is explicitly the unique System tenant `name` under a case-sensitive `^[A-Za-z0-9._-]{2,64}$`, operationally immutable public contract. Legacy public `tenantName` is rejected rather than aliased.
4. Public resolution discloses tenant existence on success. The guarantee is only that unknown, disabled, and expired tenants share one response. Reuse public throttling/ingress controls and emit structured probing metrics; spoof-resistant deployments require a future signed/authenticated locator.
5. Host identity is lowercase, trimmed, trailing-dot-free, bracket-free for IPv6, and port-independent. Canonical System website entries for this resolver are host-only; non-canonical scheme/path/port entries do not match and require configuration correction or a separately scoped Flyway/data ticket.
6. Local fallback is enabled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not enable it. With the flag true, code-less configured local-host resolution is permitted, while an explicit handle takes precedence.
7. `/education/context` is Member-only, obtains the full `LoginUser`, and continues deriving IDs only from security and tenant contexts. `TenantSecurityWebFilter` remains responsible for authenticated missing-tenant, mismatch, and availability checks.
8. Login-method metadata belongs to Member authentication. EDU-004 removes/deprecates Education `loginMethods` unless a minimal Member-owned interface is first proven necessary.
9. Exact public business failures use HTTP 200/CommonResult: invalid locator `1005001003` / `租户识别请求无效`; conflict `1005001008` / `租户识别信息冲突`; unknown/disabled/expired `1005001004` / `当前租户不可用`; all have null data and redacted detail. Success is code 0 and only `tenantId` plus `displayName`.
10. Retain `TenantCommonApi` as the generic System-owned seam. EDU-004 makes lookup methods required and adds System-owned `TenantApiImpl` contract tests; no Education locator concept enters System.
11. EDU-003 made no production, database, or Flyway change; verification is static only.
### EDU-005 accepted decision — PostgreSQL/Flyway takeover
The durable artifact classification, adoption matrix, version allocation, backfill policy, documentation corrections, and real-PostgreSQL verification gates are recorded in [`issues/EDU-005-flyway-takeover-decision.md`](issues/EDU-005-flyway-takeover-decision.md).
1. Education schema delivery is exclusively module-owned PostgreSQL Flyway under `yudao-module-education/src/main/resources/db/migration/education/`; root PostgreSQL scripts are manual bootstrap/design history and MySQL scripts are obsolete archives.
2. V4010 (`SELECT 1`) and V4020 (native catalog) remain byte-for-byte frozen because execution outside the inspected environment is unverified. The next planned project-wide version is V4030, subject to a fresh version scan at implementation time.
3. V4030 owns the final Practice core-loop schema, including sessions/questions, reports/details, wrong questions, favorites, and unified `education_idempotency`. Fresh schema does not create legacy answer/submit idempotency tables.
4. Existing manually bootstrapped databases require explicit schema comparison and adoption. A verified V4020-equivalent catalog may use an environment-specific 4020 baseline; incompatible environments require a higher-version correction, never falsified history.
5. Legacy idempotency data is backfilled into the unified table before any later forward cleanup. Legacy tables are preserved during initial adoption.
6. The earlier V4040 capability-seed plan was superseded after V4040 was allocated to the submit-claim lease. V4080/V4090 conditionally seed the approved capability plus question author/classify/publish/archive permissions when `system_menu` exists; role assignment is not seeded.
7. Docker/manual SQL initialization and MySQL rollback runbooks must be removed from active operations when EDU-006 lands. EDU-016's temporary test bridge becomes Flyway-driven after equivalence is proven.
8. The inspected local disposable `postgresdb` had no Flyway history and no Education tables. EDU-005 ran no migration and makes no migration-success claim.
### Other established constraints
1. Verified source provenance is limited: /Users/tiku1/code/tiku-backend has only main and origin/main at 033701a785c7012139e7f86995eea6041225592e; no local or remote feature/education-core-loop ref exists. Use main/033701a provisionally only, or obtain explicit approval for that baseline.
2. Verified target branch is feature/education-core-loop and its worktree is dirty. Preserve all existing changes and classify current state at execution time.
3. Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work. ce02f8a is committed core-loop evidence; native provider/catalog and much of the schema are dirty.
4. Verified V4010 is SELECT 1 and V4020 is native catalog only. Practice/report/idempotency/wrong/favorite DDL in sql/postgresql/education is untracked/manual and not proven active Flyway. Convert required DDL to immutable module-owned PostgreSQL Flyway migrations before claiming schema delivery; never modify published migrations.
5. Keep PostgreSQL/Flyway as the only new schema delivery mechanism. Historical MySQL files and root SQL are not active delivery unless explicitly labeled archival/manual and removed from operational runbooks.
6. Treat public tenant resolution as an unauthenticated disclosure surface requiring exact redaction and abuse controls. Separately preserve `TenantSecurityWebFilter` authenticated mismatch checks; the remaining principal issue is Member/UserType enforcement in `/education/context`.
7. Treat hostname port and stored-website representation as verified contradictions requiring alignment across implementation, properties, API documentation, System lookup behavior, configuration, and tests.
8. Treat native catalog isolation as an intentional TenantUtils.executeIgnore/manual-scope boundary, not evidence of a current leak. Make mapper audit and tenant/scope-consistent graph constraints concrete blockers before authoring.
9. Make the first slice provider-neutral or cover both providers because SCALAR_READ is the verified default and Java provider is conditional. The slice must include fresh browsing and persisted session restoration, with a common option-schema contract and fail-closed behavior.
10. Do not treat submit idempotency as complete: check-then-insert is not an atomic claim. Reserve keys atomically and define crash recovery before the submit slice.
11. Add first-class Auth/Profile/extended Learning, tenant appearance/integrations/secrets/codes, and granular platform-admin capability groups so every required legacy cluster has a disposition.
12. Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
13. No tests, builds, PostgreSQL connections, Flyway execution, or runtime verification were performed by the Phase 0 assessment unless a later ticket explicitly records otherwise.
## Resolved decisions (post-Phase 0)
### Provider-authority decision (resolved 2026-07-30)
**Decision:** Coexistence with Scalar as the authoritative primary. ScalarCatalogProvider (HTTP data source, `SCALAR_READ`) is the authoritative primary for catalog read operations. JavaCatalogProvider (PostgreSQL direct, `JAVA_READ`) is the conditionally-activated secondary for tenants that have completed a catalog data migration into native PostgreSQL tables.
**Rules:**
1. `SCALAR_READ` is the default and authoritative mode. It is the source of real tenant catalog content from the running tiku-backend NestJS service.
2. `JAVA_READ` is conditionally activated only when `yudao.education.catalog-mode=JAVA_READ` is explicitly set.
3. Both providers implement the same `CatalogProvider` + `QuestionCatalogProvider` interfaces; `QuestionContentSafety` and `QuestionCatalogServiceImpl` enforce provider-neutral fail-closed rules.
4. JavaCatalogProvider must not be promoted to default without: (a) runtime-verified V4020 catalog tables, (b) dedicated contract test suite, (c) mapper audit of `TenantUtils.executeIgnore` boundary.
5. Provider mode is currently global; per-tenant override is a future concern.
**Rationale:** Scalar is the running production data source with 1084 lines of dedicated tests; Java native catalog is dirty, uncommitted, runtime-unverified. The code already implements coexistence through the provider interface — this decision formalizes the existing architecture.
**Actions:**
- Document as ADR in `docs/education/migration/decisions/provider-authority.md`
- Add `JavaCatalogProviderTest` contract test suite
- Add provider-neutral integration test for equivalent safe projections
- Update `CatalogProviderMode` Javadoc
**Evidence:** Multi-agent code audit of ScalarCatalogProvider (1084 lines of tests, 13 catalog endpoints, comprehensive error handling), JavaCatalogProvider (0 dedicated tests, `TenantUtils.executeIgnore` pattern, dirty V4020), provider switch mechanism.
### PUBLIC graph-semantics decision (resolved 2026-07-30)
**Decision:** Combination enforcement via database triggers, immutable ownership scope, and application-layer validation.
**Rules:**
1. **PUBLIC rows (tenant_id=0, scope='PUBLIC')** may only reference PUBLIC parents. PUBLIC is a self-contained content tree.
2. **Tenant-owned rows (tenant_id>0, scope='TENANT_OWNED')** may reference PUBLIC rows — this is the primary content-sharing mechanism.
3. **Tenant-owned rows referencing other-tenant rows** is forbidden.
4. Composite foreign keys `(tenant_id, parent_id)` are NOT used because the `tenant_id=0` sentinel naturally breaks FK matching for tenant→PUBLIC references.
5. A catalog row's `tenant_id` and `scope` are immutable after insert. Moving content between tenant-owned and PUBLIC scope requires an explicit copy/adoption workflow; publication changes lifecycle, not ownership.
**Enforcement strategy (combination):**
- **Database triggers** (primary guard): `education_check_reference_scope()` function with BEFORE INSERT/UPDATE triggers on every FK column of every catalog table. SECURITY DEFINER, fail-closed on violation.
- **Ownership immutability triggers** on all 11 catalog tables prevent parent mutations from invalidating existing references and remove the concurrent child-insert/parent-move race.
- **Application-layer validation** in service write paths (first line of defense).
- **Migration pre-validation** DO block that fails if existing data contains cross-scope violations.
**Flyway delivery:**
- V4070: Create reference and ownership-scope guard functions, attach triggers, then run historical pre-validation while migration DDL locks prevent concurrent writes.
**Rationale:** Legacy Supabase enforced tenant graph integrity via RLS policies and staged composite FK migration. The target's `tenant_id=0` sentinel breaks composite FK for tenant→PUBLIC references — only triggers handle all three reference scenarios correctly. Application-only enforcement is insufficient per fail-closed requirements (GOAL.md §2.3). Implementation review rejected redundant `UNIQUE (tenant_id, id)` constraints because V4020 IDs are already globally unique primary keys and no current query, conflict target, or composite FK consumes those indexes.
**Actions:**
- Done: create V4070 (19 reference guards, 11 immutable-scope guards, and historical pre-validation) via `flyway-postgresql`
- Add application-layer scope validation in catalog write services
- Extend `CatalogScopeQuery` with explicit tenant_id/disallowed-scope predicates
- Add focused integration tests for all reference scenarios
**Evidence:** Schema graph analysis of V4020 (11 catalog tables, all single-column FKs, no composite tenant enforcement), legacy RLS/trigger audit (Supabase RLS + staged composite FK migration), current CHECK constraints provide row-level but not cross-row enforcement.
### Native question authoring authority and lifecycle (resolved 2026-07-30)
**Decision:** Local tenant question authoring is available only under explicit `JAVA_READ` authority and fails before persistence access in `SCALAR_READ`. New questions are `DRAFT`; the current command surface permits only `DRAFT → PUBLISHED → ARCHIVED`, with no restore or retire command.
**Rules:**
1. Create, place, publish, and archive are explicit commands with separate System RBAC permissions; clients cannot submit tenant, scope, lifecycle, or actor fields.
2. Tenant authoring always creates `TENANT_OWNED` content for the current framework tenant. PUBLIC/platform-curator authoring is not part of this slice.
3. Question Content Versions are immutable. Publication changes lifecycle state for the current version and never changes `tenant_id`, `scope`, or content version.
4. Education appends a lifecycle audit in the same transaction as each state transition. The asynchronous platform operation log remains supplementary, not proof of atomic publication.
5. Direct question visibility follows the question's Publication State. Entry, node, and collection availability gates their own discovery routes and does not mutate the question lifecycle or historical practice snapshots.
6. Historical `HIDDEN`, `INACTIVE`, and `PUBLISHED/is_published=false` rows map to `ARCHIVED`; historical `DRAFT` rows become non-published; only consistent published rows remain `PUBLISHED`.
7. V4080 adds Question Version → Question as the twentieth guarded catalog edge. A version must have exactly the same tenant and scope as its question, and the version table becomes the twelfth catalog table whose ownership cannot change after insert.
8. Question Placement means assigning a DRAFT question's `node_id`; it does not mean Category CRUD. The `education_category` table has no Question relationship in the current target model.
9. Placement targets must be visible, active, selectable PUBLIC or same-tenant Content Nodes. Placement has an independent optimistic version, becomes immutable after publication, and publication CAS includes that version.
10. Node-route student visibility is evaluated atomically in the Question/Content Node query. Node availability gates that route but does not change direct question visibility or historical snapshots.
11. Tenant Content Node authoring is JAVA_READ-only, current-tenant TENANT_OWNED-only, and uses `DRAFT → ACTIVE → ARCHIVED` with one CAS `authoring_version` shared by revisions and transitions.
12. Content Node activation requires a structurally available entry and parent. Student discovery and Question Placement treat only ACTIVE nodes as available. Content Node lifecycle audit is transactional and append-only.
13. Content Node permissions are `education:content-node:author`, `education:content-node:publish`, and `education:content-node:archive`; V4100 seeds no permission, menu, role, or role grant.
14. The next bounded EDU-010 slice is a JAVA_READ-only current-tenant `TENANT_OWNED` Manual Question Collection attached to one existing ACTIVE, visible Content Node. Content Entry creation is excluded, so the target node and its structurally available Content Entry must be pre-provisioned.
15. A Manual Question Collection uses `DRAFT → ACTIVE → ARCHIVED` with one optimistic authoring version. Only DRAFT metadata and membership may change; ACTIVE collection content and membership are immutable, and ARCHIVED is terminal.
16. Membership is an ordered replace-all command available only in DRAFT. Every member must be a current-tenant `TENANT_OWNED` PUBLISHED Question, and duplicate Question IDs are rejected rather than collapsed or upserted.
17. Activation exposes the collection route only when the collection and its existing Content Node are available. Archiving closes only collection-route discovery; it does not alter direct Question visibility or historical Practice Question Snapshots.
18. `access_rules` is descriptive reserved metadata in this slice, not entitlement enforcement. PUBLIC Question curation, dynamic filters, paid/private access, Category, Practice Blueprint, and Content Entry creation are excluded.
19. Protected lifecycle transition tokens rely on PostgreSQL object ownership separation: an explicit Flyway owner role creates V4080/V4100/V4110 token tables, while the runtime master datasource role must neither own nor write them. Local/dev configuration requires explicit `FLYWAY_USER`/`FLYWAY_PASSWORD`; startup fails closed when any protected table is missing, runtime-owned, inherited through role membership, or runtime-writable. Deployment automation provisions roles and grants outside Flyway because migrations cannot safely create shared login roles or know credential policy.
20. V4120 closes the bounded EDU-010 Category and Practice Blueprint scope. Category is a standalone Subject-scoped catalog aggregate, not Question classification. Blueprint authoring is limited to exactly one current-tenant NODE or active Manual COLLECTION target; target ownership, route, and counts are server-derived.
21. Category and Practice Blueprint use the same `DRAFT → ACTIVE → ARCHIVED`, `authoring_version` CAS, transactional append-only audit, current-tenant ownership, and `JAVA_READ` authority rules as Content Node and Collection. Student reads require ACTIVE lifecycle plus route availability.
22. The six added permissions are `education:category:{author,publish,archive}` and `education:practice-blueprint:{author,publish,archive}`. V4120 conditionally seeds permission rows at IDs 6809-6814, fails on conflicting IDs, and assigns no roles.
**Permission and data-scope matrix:**
| Actor capability | Permission | Allowed rows | Data scope |
|---|---|---|---|
| Author | `education:question:author` | Create current-tenant `TENANT_OWNED` drafts | Current tenant; no request-supplied owner or tenant |
| Classifier | `education:question:classify` | Place or re-place current-tenant `TENANT_OWNED` drafts on allowed Content Nodes | Tenant-wide shared catalog asset; PUBLIC/same-tenant targets only |
| Publisher | `education:question:publish` | Current-tenant `TENANT_OWNED` drafts | Tenant-wide shared catalog asset; department/self DataPermission does not expand access |
| Archiver | `education:question:archive` | Current-tenant `TENANT_OWNED` published questions | Tenant-wide shared catalog asset; department/self DataPermission does not expand access |
| Content Node author | `education:content-node:author` | Create/revise current-tenant TENANT_OWNED drafts | Current tenant; structural entry/parent validation |
| Content Node publisher | `education:content-node:publish` | Activate current-tenant TENANT_OWNED drafts | Tenant-wide shared catalog asset; authoring-version CAS |
| Content Node archiver | `education:content-node:archive` | Archive current-tenant TENANT_OWNED active nodes | Tenant-wide shared catalog asset; authoring-version CAS |
| Collection author | `education:collection:author` | Create/revise current-tenant TENANT_OWNED Manual Question Collection drafts and replace ordered membership | Current tenant; DRAFT-only; existing ACTIVE node and PUBLISHED tenant questions |
| Collection publisher | `education:collection:publish` | Activate current-tenant TENANT_OWNED Manual Question Collection drafts | Tenant-wide shared catalog asset; authoring-version CAS |
| Collection archiver | `education:collection:archive` | Archive current-tenant TENANT_OWNED active Manual Question Collections | Tenant-wide shared catalog asset; authoring-version CAS |
| Category author | `education:category:author` | Create/revise current-tenant TENANT_OWNED Category drafts | Current tenant; active PUBLIC or same-tenant Subject reference |
| Category publisher | `education:category:publish` | Activate current-tenant TENANT_OWNED Category drafts | Tenant-wide shared catalog asset; authoring-version CAS |
| Category archiver | `education:category:archive` | Archive current-tenant TENANT_OWNED active Categories | Tenant-wide shared catalog asset; authoring-version CAS |
| Practice Blueprint author | `education:practice-blueprint:author` | Create/revise bounded NODE or COLLECTION blueprint drafts | Current tenant; target ownership and counts derived server-side |
| Practice Blueprint publisher | `education:practice-blueprint:publish` | Activate current-tenant TENANT_OWNED blueprint drafts | Tenant-wide shared catalog asset; target revalidation and CAS |
| Practice Blueprint archiver | `education:practice-blueprint:archive` | Archive current-tenant TENANT_OWNED active blueprints | Tenant-wide shared catalog asset; authoring-version CAS |
| Platform curator | none in this slice | No PUBLIC writes through these endpoints | Fail closed |
V4080/V4090 conditionally seed the five Education permissions (`education:capability` plus author/classify/publish/archive). V4110 conditionally seeds the collection author/publish/archive permissions at IDs 6806-6808. Seeds run only when `system_menu` exists, fail when a fixed ID is occupied by a different permission, and deliberately assign no role.
**Rationale:** A successful PostgreSQL write while Scalar remains authoritative would be student-invisible. Department/self DataPermission has no truthful meaning for tenant-wide shared catalog rows without a separate ownership model, so action permissions plus framework tenant isolation are explicit rather than applying a misleading annotation.
**ADR:** `docs/adr/0001-native-question-authoring-authority.md`.
The accepted Manual Question Collection slice does not warrant a separate ADR: it applies the already-recorded native-authority, tenant-ownership, lifecycle, optimistic-concurrency, route-gating, and snapshot-preservation decisions to one narrower aggregate, without adding a hard-to-reverse architectural trade-off.
### EDU-011 bounded import/export-assets capability (resolved 2026-07-31)
**Decision:** Education owns tenant import asset metadata and durable import-job business state. Infra remains the owner of generic private-file/platform facilities and is consumed only through public APIs.
1. The import job has exactly five persisted states: `PREVIEW`, `PENDING`, `PROCESSING`, `COMPLETED`, and `FAILED`.
2. Processing uses atomic claim, fenced lease token, heartbeat, expired-lease recovery, bounded attempts, and duplicate-safe tenant command keys.
3. Scanning fails closed. With no configured scanner adapter the result is `UNAVAILABLE`, never implicitly clean.
4. CSV/XLSX may produce metadata-only preview when no parser is available. Such a preview is not executable; execute requires a clean scan and explicit executable parsed content.
5. Export scope is request redaction policy only: answers and private fields must be excluded. No generated export file, export worker, or export-job persistence is delivered.
6. V4130 is the only EDU-011 migration. Production scanner integration, full parser delivery, retention/deletion automation, dead-letter/operator tooling, partial-row reporting, and legacy asset migration remain deferred.
7. Education may use public Infra APIs but may not depend on Infra DOs, mappers, `ServiceImpl` classes, or private implementation packages.
The bounded slice does not require a separate ADR: it keeps domain job state with Education and applies the repository's established public-module-boundary rule without introducing a new platform abstraction.
## Unresolved decisions
1. Which Auth/Profile/extended Learning semantics are replaced by Member/System/Infra versus Education-owned, including vocabulary, leaderboard, stats, trend, feedback, exam dates, notifications, points, and badges.
2. Whether tenant appearance, domains, payment accounts, auth providers, secrets, activation codes, coupons, integrations, marketing, public-bank grants, and sync are in scope or explicitly retired.
3. Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
4. Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
5. Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
6. Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
7. Which shared environments already carry V4010/V4020 or manual equivalents and what explicit baseline/adoption record each requires. The root `sql/postgresql/education/` classification and the module-owned Flyway authority are resolved; disposable PostgreSQL execution evidence exists, but no shared-environment migration is claimed.
## Decision rule
Questions answerable from code, Git history, configuration, tests, or documentation must be investigated. Only genuine product choices should be escalated. Hard-to-reverse decisions should become ADRs before dependent implementation begins.

View File

@@ -0,0 +1,166 @@
# Vertical Slice Roadmap
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
Tickets are vertical behaviors, not technical layers. Work blockers first and use a fresh implementation context per ticket.
## EDU-P0-S0 — Baseline, completeness, and architecture decision gate
- **Outcome:** Verified baseline, capability/API/database/module-reuse maps, commit reviews, decision register, corrected documentation plan, and bounded first-slice specification.
- **Risk:** High provenance and operational risk; no implementation should begin from an unclassified dirty baseline.
- **Blockers:**
- Obtain or explicitly approve source baseline main/033701a because no source feature/education-core-loop ref exists.
- Record current target status and preserve all 65 modified tracked and 97 untracked entries.
- Complete the ten GOAL.md Phase 0 artifact dispositions and separate committed, dirty, absent, and runtime-unverified behavior.
- Decide tenant origin-binding, principal policy, provider authority, option schema, and PUBLIC graph semantics.
- **Verification:**
- Read GOAL.md and target/source rules and module documentation.
- Record both Git statuses, histories, and refs without destructive commands.
- Review 11e9cc6, 0f846fd, and committed core-loop evidence ce02f8a.
- Confirm no tests, builds, PostgreSQL connections, Flyway migrations, or runtime flows were executed.
- Inventory Auth/Profile/extended Learning, granular tenant-admin, platform-admin, worker/scanner, and database object surfaces.
## EDU-P0-S1 — Provider-neutral safe question and session restoration
- **Outcome:** Both provider modes and restored sessions fail closed for malformed/unavailable published question content while preserving tenant scope and safe projections.
- **Risk:** Medium implementation risk and high assessment-integrity/security risk if any alternate path remains fail-open.
- **Blockers:**
- EDU-P0-S0 provider and option-contract decisions.
- Existing dirty provider/session files must be separated from unrelated work.
- Safe response and question-type option semantics must be fixed.
- **Verification:**
- Provider contract tests for Scalar and Java.
- Browsing, collection, practice-create/restore, malformed, unavailable, unpublished, cross-tenant, PUBLIC, and sensitive-field tests.
- Run focused tests/compile/diff checks only after authorization and report exact results.
## EDU-P2-S2 — Create and restore practice
- **Outcome:** A student creates and restores a tenant-scoped practice session from valid published content without client-supplied identity or tenant IDs.
- **Risk:** Medium; session ownership, graph scope, and schema packaging require negative tests.
- **Blockers:**
- EDU-P0-S1 safe-content contract.
- Core-loop schema must be promoted into active module-owned Flyway history.
- Provider authority and existing dirty session implementation classification.
- **Verification:**
- Tenant/user context and Member-principal tests.
- Restore ownership, cross-tenant denial, PUBLIC-scope tests.
- PostgreSQL uniqueness, transaction, packaging, and migration execution checks if schema changes are approved.
## EDU-P2-S3 — Idempotent answer save
- **Outcome:** A student saves one answer idempotently with explicit duplicate/conflicting-payload semantics and no sensitive-field exposure.
- **Risk:** Medium-to-high due to concurrent writes, stale versions, and answer leakage.
- **Blockers:**
- EDU-P2-S2 session state.
- Existing answer/idempotency schema and option snapshot contract.
- Entitlement decision for non-public/private content.
- **Verification:**
- Same-payload duplicate and conflicting-payload tests.
- Concurrent PostgreSQL uniqueness/transaction tests.
- Tenant isolation, stale-version, safe-response, and malformed-snapshot tests.
## EDU-P2-S4 — Atomic submit, report, wrong questions, and favorites
- **Outcome:** A student atomically claims and submits a session, reads an immutable report, and receives consistent wrong-question/favorite projections.
- **Risk:** High; current check-then-insert submit idempotency is not sufficient.
- **Blockers:**
- EDU-P2-S3 answer state.
- Atomic submit-key reservation and crash recovery design.
- Scoring/report immutability and entitlement decisions.
- **Verification:**
- ON CONFLICT/atomic claim concurrency tests.
- Processing-row crash recovery and retry semantics.
- Immutable report/scoring, duplicate submission, wrong-question/favorite idempotency, sensitive-field, tenant, and unauthorized tests.
## EDU-P3-S5 — Tenant content publication and graph integrity
- **Outcome:** Tenant administrators author, classify, publish, and safely retire question content with tenant-consistent graph integrity.
- **Risk:** High due to publication, admin scope, public graph, and student-read consistency.
- **Blockers:**
- Core loop verified.
- Provider and education content model decisions.
- System RBAC/DataPermission policy.
- **Verification:**
- Admin permission/row-scope tests.
- Composite tenant/scope relationship constraint or equivalent enforcement tests.
- Publication visibility/provider consistency and safe-projection regression tests.
## EDU-P3-S6 — Content imports, exports, assets, and scanning
- **Outcome:** Tenant administrators import/export education content with durable business state, leases, retries, duplicate-safe processing, file security, and audit.
- **Risk:** High operational and security risk.
- **Blockers:**
- Publication model.
- Infra File contract and scanner ownership.
- Infra Job/MQ durable claim/lease semantics.
- **Verification:**
- Preview/execute state machine.
- Atomic claim/lease/heartbeat/expiry/retry/dead-letter tests.
- MIME/size/object-key/scan fail-closed tests.
- Tenant propagation, audit redaction, and partial-failure tests.
## EDU-P4-S7 — Classes and education relationships
- **Outcome:** Tenant administrators manage classes, education student relationships, invitations, supervision, and education operations with explicit scope.
- **Risk:** High authorization risk.
- **Blockers:**
- Education relationship model.
- System RBAC/DataPermission scope rules.
- Member relationship contract and CRM supervision decision.
- **Verification:**
- Student/teacher/class permission matrix.
- Cross-class/cross-tenant negative and duplicate invitation tests.
- Audit redaction and operation-log tests.
## EDU-P4-S8 — Tenant configuration, integrations, and access operations
- **Progress:** V4290/EDU-017 completed bounded appearance, public settings, and theme lifecycle. V4300V4360 reuse and activate native Pay/System integration, transaction, import, Transfer, and Wallet capabilities. V4310/EDU-019 delivers secure learning activation codes and entitlement composition. V4370/EDU-025 activates Product; V4380/EDU-026 activates coupons; V4390/EDU-027 activates normal Trade order; V4400/EDU-028 activates Discount/Reward; V4410/EDU-029 activates delivery; V4420/EDU-030 activates after-sale/Pay Refund; V4430/EDU-031 activates native brokerage; V4440/EDU-032 activates tenant-aware Seckill; V4450/EDU-033 activates tenant-aware Combination activities, SKU pricing, group records, atomic capacity, Trade Order/head bridges, exact permissions, and corrected Vben pages. Domains remain with their RuoYi owners; source referral CRM/settlement-proof import, Bargain/Point and other Promotion families, explicit legacy Product/code-coupon/order/refund import, production bulk export/runbooks and reviewed balances, automatic fulfillment/refund revocation, non-equivalent provider/mode replacement, tenant PNVS, legacy activation-code import, generic private secrets, and public-bank access remain separate.
- **Outcome:** Selected tenant appearance, integrations, secrets, activation codes, coupons, and public-bank access capabilities have explicit owners and safe contracts.
- **Risk:** High because secret, payment configuration, redemption, and public-bank synchronization boundaries differ.
- **Blockers:**
- Appearance/domain/integration/secrets/codes ownership decisions.
- System tenant configuration and secret APIs.
- Mall/Pay/Member entitlement and coupon contracts; EDU-019 resolves bounded new activation codes, EDU-021EDU-024 resolve bounded Pay activation/import/ledgers, EDU-025 resolves Product, EDU-026 resolves coupons, EDU-027 resolves normal Trade order, EDU-028 resolves Discount/Reward, EDU-029 resolves delivery, EDU-030 resolves after-sale, EDU-031 resolves native brokerage, EDU-032 resolves native Seckill, and EDU-033 resolves native Combination activation. Explicit legacy Product/code-coupon/order/refund/referral/settlement-proof import, Bargain/Point and other Promotion families, legacy activation-code import, production financial bulk migration and reviewed opening balances, fulfillment, and refund revocation remain open.
- **Verification:**
- Secret redaction/rotation and authorization tests.
- Domain/auth-provider/payment-account configuration tests; EDU-021 covers bounded account mapping/replay/redaction, EDU-022 covers transaction tenant inheritance, composite database references, notification uniqueness, native service regressions, and menu shape, EDU-023 covers terminal reconciliation, redacted audit, idempotency/conflict, and native order-page UI, EDU-024 covers Transfer/Wallet tenant inheritance, tenant-qualified locks, amount safety, composite references, and native UI/menu shape, and EDU-025 covers nine Product tenant records, catalog graph references, category parent isolation, amount/score safety, global-table refusal, and native UI/menu shape.
- Native coupon tenant/reference/counter/discount/use-state and menu-shape tests are delivered by V4380. V4390 adds Trade tenant inheritance, composite Order/Cart/Product references, state/amount checks, global/deferred-table refusal, sequences, and exact order/config UI/menu shape. Legacy code and order import/reconciliation remain open. V4310 already covers activation-code digest, tenant, replay, conflict, disabled dependency, entitlement-event, and concurrent-winner behavior.
- Public-bank grant/sync and cross-tenant tests.
## EDU-P5-S9 — Education commercialization binding
- **Outcome:** Education products bind to commerce purchases and Member entitlements without duplicated financial ledgers.
- **Risk:** High financial and authorization risk.
- **Blockers:**
- Product binding model; EDU-025 supplies the tenant-scoped native Product owner but binding authoring/import semantics remain open.
- Mall/Pay public APIs; Product, native coupons, normal Trade order/delivery/after-sale, native brokerage, Seckill, and Combination are active, while Bargain/Point and other Promotion families, source referral/settlement-proof mapping, and purchase/refund-to-entitlement composition remain open.
- Member entitlement decision and callback/refund semantics.
- **Verification:**
- Order/payment/refund callback contracts.
- Entitlement issuance/revocation/expiry and idempotent fulfillment.
- Reconciliation, commission/referral, authorization, and audit tests.
## EDU-P5-S10 — Extended student and secondary learning waves
- **Outcome:** Selected Auth/Profile/extended Learning/scoreline/vocabulary/video/AI/notification/badge/exam capabilities are migrated, replaced, retired, or deferred with traceable decisions.
- **Risk:** Medium-to-high due to omitted student contracts, media entitlement, and unclear ownership.
- **Blockers:**
- Explicit scope for each secondary capability.
- AI/File/Member/System/Infra contracts and entitlement model.
- **Verification:**
- Per-capability endpoint/data/authorization contract tests.
- Progress/report/vocabulary state tests.
- Media entitlement, safe export/redaction, tenant isolation, and retirement compatibility tests.
## EDU-P6-S11 — Operational independence and legacy exit
- **Outcome:** Background and platform operations run independently of NestJS with documented retries, scanning, audit, notifications, observability, and deployment evidence.
- **Risk:** High deployment and reliability risk.
- **Blockers:**
- All owner and contract decisions.
- Operational deployment, scanner, observability, and legacy exit plan.
- **Verification:**
- Worker/job deployment smoke tests.
- At-least-once duplicate/dead-letter and scanner health/security tests.
- PostgreSQL migration execution evidence.
- Runbook and documentation consistency review.

View File

@@ -0,0 +1,51 @@
# First Recommended Slice: EDU-P0-S1
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Provider-neutral fail-closed safe question browsing and session restoration
### Rationale
The initial native-only slice was corrected because SCALAR_READ is the verified default and JavaCatalogProvider is conditional. A provider-neutral safe-content contract is the smallest observable correction that addresses both active and optional paths, avoids provider-authority assumptions, protects the dirty worktree, and covers the second fail-open path in restored sessions.
### Scope
- Define and document the common option validity contract, including whether absent options are legal for each question type; invalid published question payloads must be omitted or return a controlled failure, never an apparently valid empty-options question.
- Apply and test the contract for both ScalarCatalogProvider and JavaCatalogProvider, despite SCALAR_READ being the current default, so provider mode cannot change safety behavior.
- Apply and test safe projection for single-question browsing and collection/catalog browsing paths.
- Make SessionResponseAssembler reject, mark unavailable, or otherwise fail closed on malformed persisted snapshots; do not silently convert parse failure to an empty list.
- Verify publication/status/visibility and unavailable-provider fail-closed behavior at the service boundary.
- Add cross-tenant and PUBLIC-scope negative tests, while recording that current native reads use an intentional TenantUtils.executeIgnore/manual predicate boundary.
- Separate committed behavior from dirty behavior in the implementation report; do not claim runtime verification.
- Critical files for implementation: /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/ScalarCatalogProvider.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/question/QuestionCatalogServiceImpl.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/practice/SessionResponseAssembler.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/catalog/CatalogServiceImplTest.java.
### Reuse boundaries
- Reuse /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/CatalogProvider.java and existing provider selection; do not choose a new provider in this slice.
- Reuse existing safe question DTO/assembler boundaries in QuestionCatalogServiceImpl; never return DOs, answers, explanations, correctness flags, or admin metadata.
- Apply one shared, provider-neutral option validation contract at the provider-to-safe-question/session-snapshot boundary; do not duplicate divergent validation rules.
- Reuse framework TenantContextHolder and the existing explicit CatalogScopeQuery predicate. Do not broaden TenantUtils.executeIgnore or add a custom tenant bypass.
- Keep changes inside Education unless a proven public contract gap requires a minimal separately owned interface; do not alter System, Member, Scalar infrastructure, or database schema speculatively.
- Preserve and classify the existing dirty tree; implementation must be serial and must not overwrite unrelated files.
### Database change
No database change for the bounded correctness slice. Do not modify V4010 or V4020. Do not promote untracked SQL during this slice. If later graph-integrity or core-loop schema work is approved, create new immutable module-owned PostgreSQL Flyway migrations under /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/resources/db/migration/education/ after schema review; never claim execution without a real PostgreSQL run.
### Test plan
- Focused unit/provider contract tests for null, absent, empty, malformed, structurally invalid, blank-label, duplicate/order-invalid options, and valid question-type-specific payloads.
- Service tests proving malformed published content cannot produce a student-visible apparently valid safe question.
- Session create/restore tests proving malformed persisted snapshots fail closed and valid snapshots retain safe fields only.
- Tests for single browse, collection browse, unpublished/invisible content, unavailable provider, disabled feature, cross-tenant access, and tenant_id=0 PUBLIC scope.
- If both providers cannot yet share a concrete contract, add contract tests parameterized over each implementation and record the remaining provider decision rather than silently selecting one.
- No concurrency test is required for this read-only slice, but atomic submit-idempotency reservation and crash recovery must block the later submit slice.
- After implementation authorization only: run focused Education tests, git diff --check, and mvn -pl yudao-server -am -DskipTests clean compile; if schema files remain unchanged, do not claim Flyway execution.
### Rollback
Application-level rollback is configuration/provider disablement or restoration of the prior provider behavior after review, without destructive Git or database rollback. No database rollback applies because this slice has no schema change. If malformed persisted snapshots are encountered, fail closed with a controlled unavailable/corrupt-content outcome rather than silently restoring an empty-options question.
### Authorization gate
This document selects and specifies the first slice; it does not authorize broad implementation. Before editing, re-read the dirty working tree, isolate existing user changes, state the exact files to touch, and execute the slice test-first.

View File

@@ -0,0 +1,15 @@
# Documentation Corrections
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
These corrections are identified but not applied by the read-only discovery workflow.
- Correct /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/README.md:4-20,171-192,393-401 to separate committed versus dirty work, remove MySQL/rollback delivery claims, document PostgreSQL/Flyway forward-only operations, and state that V4010/V4020 and core-loop schema execution are unverified.
- Correct /Users/tiku1/code/ruoyi-vue-pro/docs/education/student-core-learning-loop-prd.md:20-26,142-143,200-201 so it does not claim RuoYi/MySQL, ordered reversible SQL, or absent Flyway.
- Correct /Users/tiku1/code/ruoyi-vue-pro/docs/education/pilot-acceptance-runbook.md:29-37,63-68,78-85 to require actual PostgreSQL/Flyway execution evidence and forward correction rather than MySQL rollback.
- Align /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:55-59 documentation, /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProperties.java:53-59, implementation, System website normalization, and tests on port policy.
- Document the provider-neutral malformed-option contract across JavaCatalogProvider, ScalarCatalogProvider, QuestionCatalogServiceImpl, and SessionResponseAssembler, including question-type-specific absent-option policy.
- Document that native catalog scope is a deliberate TenantUtils.executeIgnore/manual predicate boundary and add mapper-audit plus graph-integrity design notes.
- Label /Users/tiku1/code/ruoyi-vue-pro/sql/mysql/education/ and /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education/ according to actual operational status; do not present either as active delivery until the latter is promoted into Flyway.
- Correct the commercialization legacy path to /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- Create or maintain the ten required Phase 0 artifacts named by /Users/tiku1/code/ruoyi-vue-pro/docs/education/migration/GOAL.md:147-160, without claiming they exist unless verified.

View File

@@ -0,0 +1,174 @@
# Provider-neutral question content safety contract
> Status: proposed for EDU-P0-S1
>
> Evidence basis: current Education providers and services plus the legacy question import and learning rules. This document defines the implementation contract; it does not report tests as executed.
## Decision
Student-visible question content is validated by one provider-neutral contract before it is projected as a safe question or persisted/restored as a practice snapshot. Provider-specific parsing may reject malformed transport data earlier, but switching between Scalar and Java must not change whether the same logical question is considered safe.
Invalid or unsupported content fails closed. It must not be normalized into an apparently valid question with an empty option list.
## Question-type families
Type matching is case-insensitive after trimming. Persisted and returned canonical values remain an implementation concern; validation uses the following families.
### Option-backed
```text
choice
multi
multi_choice
judge
image
```
An option-backed question requires:
- at least two options;
- every option object to be non-null;
- a non-blank string `label`;
- a non-blank string `content`;
- labels unique after trimming;
- each `order`, when present, to be a finite number;
- no duplicate non-null order value;
- no answer-bearing field in the student-safe projection or snapshot.
Options are emitted in deterministic order: numeric `order` first, preserving source order only when order is absent. This slice does not infer or repair missing labels, contents, or order values.
### Optionless
```text
fill
text
terms
short_answer
composition
discuss
translation
case_analysis
brief_analysis
calculation
analysis_design
combination
solution
```
An optionless question may have null or empty options. If options are supplied, the content is inconsistent with its type and fails closed rather than silently discarding them.
This slice only establishes safe display and snapshot behavior. It does not add text-answer submission or scoring semantics. Existing answer-saving behavior must not pretend an optionless question is option-backed.
### Composite
```text
reading
```
A composite question requires sub-questions in the legacy model. The current target `CatalogQuestionDTO`, safe response, and practice snapshot do not carry a supported sub-question contract. Therefore a top-level `reading` question is unsupported by EDU-P0-S1 and fails closed rather than appearing as an optionless standalone question.
Supporting composite questions requires a later explicit model and API contract.
### Unknown or missing type
A null, blank, or unknown type fails closed. The target must not default unknown content to `choice`, because doing so can turn malformed content into a different assessment.
## Common option shape
The provider-neutral safe option shape is:
```text
label: non-blank string, unique after trim
content: non-blank string
order: optional finite number, unique when present
```
Provider DTOs may temporarily contain `isCorrect` for internal scoring or migration needs, but that field and all answer-bearing fields are discarded before creation of a Safe Question or the student-visible Question Snapshot JSON. A separately stored Protected Answer Key may retain correctness and explanation data for stable server-side scoring, but it is never part of the safe snapshot projection or a pre-submit response.
The safety validator must not require `isCorrect`, because safe restored snapshots intentionally do not store it. Correct-answer completeness is a content-authoring/scoring concern and is outside this read-only display contract.
## Failure semantics
### Fresh provider content
Any of the following makes the provider result unsafe:
- malformed options transport or JSON;
- null option element;
- wrong field types;
- fewer than two options for an option-backed question;
- options on an optionless question;
- blank or duplicate labels;
- blank content;
- non-finite or duplicate explicit order;
- unsupported composite content;
- null, blank, or unknown type.
A single-question request returns the existing controlled unsafe/malformed content error. Page and collection requests fail the response closed rather than silently changing totals or returning a partial assessment set.
### Persisted practice snapshot
A null/empty options value is valid only for an optionless type. Malformed JSON or a shape that violates the type family makes the snapshot unavailable. Session restoration must return a controlled error for the session/question; it must not convert parse failure into `[]`.
No automatic repair is performed during read or restore. Historical repair, quarantine, or backfill requires a separately reviewed migration or administrative process.
## Boundary placement
The contract is applied at two domain boundaries:
1. provider `CatalogQuestionDTO` → student-safe projection or practice-session creation;
2. persisted `PracticeQuestionDO` snapshot → practice-session response.
Both boundaries use the same type classification and option-shape rules. Scalar and Java adapters remain responsible only for transport/database parsing and mapping; they must not define divergent business validity.
## Compatibility notes
- Legacy source evidence identifies `choice`, `multi`, `judge`, and `image` as objective/option-backed and requires at least two options.
- Legacy source recognizes `reading` as a composite type with sub-questions.
- Legacy source recognizes the optionless types listed above and requires answer text for authoring; answer text is intentionally not exposed by the safe read contract.
- Current target tests sometimes construct `choice` questions with null, one, or empty options. Those fixtures describe previous permissive behavior and must be corrected where they cross a student-visible or snapshot boundary.
- `multi_choice` is retained as a target compatibility alias because current submit tests use it, while the legacy canonical type is `multi`.
## Required tests
### Shared contract
- each recognized type is classified correctly;
- type matching trims and ignores case;
- null, blank, and unknown types fail;
- option-backed types reject null, empty, and one-option lists;
- optionless types accept null/empty and reject supplied options;
- `reading` fails as unsupported composite content;
- null elements, wrong field types, blank labels, duplicate labels, blank contents, non-finite orders, and duplicate explicit orders fail;
- valid options preserve safe fields and never expose `isCorrect`.
### Provider paths
Run the same logical contract cases against Scalar and Java mappings. Transport-specific malformed data may fail earlier, but no provider may turn malformed input into an empty valid list.
### Service paths
- single-question browsing fails closed for unsafe content;
- page browsing fails the whole response for unsafe content;
- collection browsing fails the whole response for unsafe content;
- unpublished, hidden, inactive, disabled-source, and unavailable-source behavior remains fail-closed;
- successful output contains no answer-bearing fields.
### Practice paths
- session creation rejects unsafe source questions before persisting snapshots;
- a valid option-backed snapshot restores its options;
- a valid optionless snapshot restores an empty option list;
- malformed or type-inconsistent snapshots fail closed;
- snapshot JSON contains only `label`, `content`, and `order`;
- cross-tenant and ownership protections remain unchanged.
## Out of scope
- choosing Scalar or Java as the authoritative provider;
- adding composite/sub-question APIs;
- adding subjective answer submission or scoring;
- validating that correct answers exist or are unique;
- repairing historical snapshots;
- changing database schema or Flyway migrations;
- submit-idempotency redesign.

View File

@@ -0,0 +1,282 @@
# Education Admin UI
> Implemented on 2026-07-31 as the first two Vben management UI slices. This record covers build-time and disposable PostgreSQL evidence only; it is not Pilot or production runtime evidence.
## Delivered pages
The Vben admin submodule now contains the four routes seeded by Education Flyway migration V4220:
| Menu component | Capability |
|---|---|
| `education/content-node/index` | Page, create, revise, activate, and archive tenant content nodes |
| `education/question/index` | Page, create/revise drafts, place on a content node, publish, and archive questions |
| `education/collection/index` | Page, create/revise drafts, replace ordered question membership, activate, and archive collections |
| `education/practice-blueprint/index` | Page, create/revise NODE or COLLECTION blueprints, activate, and archive blueprints |
Flyway migration V4230 adds four further routes over existing backend contracts:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/import-job/index` | Infra File-backed upload and malware scan, parser preview, explicit execution, and job lookup |
| `education/classroom/index` | Class creation, Member user membership display, and expiring idempotent invitations |
| `education/commercialization/index` | Question-collection binding to Mall SPU and idempotent Member entitlement events suitable for Pay/Mall callbacks |
| `education/operations/index` | Read-only dependency, Worker, Scanner, and dead-letter health with server-sanitized diagnostics |
Flyway migration V4240 adds the remaining bounded admin contracts:
| Menu component | Capability |
|---|---|
| `education/category/index` | Tenant category page/detail reads plus create, revise, activate, and archive lifecycle |
| `education/content-export/index` | Export field-policy evaluation with separate answer-inclusion permission; explicitly does not claim artifact generation |
Member migration V4250 and Education migration V4260 add the first member-learning operations slice:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/learning-operations/index` | Tenant learning summary, student feedback handling/audit, resolved-feedback point rewards, and learning award/badge projection. Member remains authoritative for users, levels, balances, and point records; System remains authoritative for admin identity and notifications. |
V4250 adds a fail-closed unique business key for Education-owned entries in the Member point ledger. `MemberPointApi.addPointOnce` treats only a verified matching ledger row as an idempotent replay, so a retry cannot double-credit a member and unrelated unique-key failures are not hidden. V4260 adds tenant-owned feedback events, optimistic feedback versions, reward delivery state, separate query/feedback/reward RBAC permissions, and menu rows.
Education migration V4270 adds the tenant student-supervision slice:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/supervision/index` | Risk-student preview over native practice reports, wrong questions, active sessions, and vocabulary progress; configurable supervision rules; idempotent follow-up generation; and optimistic follow-up handling. Member remains authoritative for student accounts. System AdminUser, RBAC, department data scopes, and simple-list selectors remain authoritative for operators. |
V4270 adds `dept_id` and `owner_user_id` authorization projections to Education classes and registers classes, supervision rules, and follow-ups with RuoYi `DeptDataPermissionRule`. It does not copy System users, departments, roles, or Member profiles. Stable `(tenant_id, batch_key, student_user_id)` uniqueness plus PostgreSQL `ON CONFLICT DO NOTHING` prevents both sequential and concurrent retries from duplicating work without aborting the surrounding transaction. The risk query keeps `education_class` in the main select so RuoYi's department/self interceptor can scope candidate students; a real `LoginUser` and `DeptDataPermissionRespDTO` PostgreSQL test verifies the negative department case.
Education migration V4280 adds configurable tenant badges and the thirteenth Education admin page:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/badge/index` | Badge definition filtering, creation, optimistic editing, manual Member grant, and grant-history audit. Member remains authoritative for student identity; System remains authoritative for administrator identity, RBAC, and the `education_badge_granted` notify template/message. |
V4280 extends the existing `education_learning_award` ledger instead of creating a second user-badge table. `(tenant_id, user_id, badge_definition_id)` makes a badge a lifetime-once grant and PostgreSQL `ON CONFLICT DO NOTHING` makes manual and automatic replay safe. Automatic evaluation is attached only to real migrated events: practice submission, vocabulary review, and feedback resolution/reward. Check-in, mock-exam, and activity-reward triggers remain unavailable until those source capabilities are migrated; the rule editor does not advertise invented event sources.
Education migration V4290 adds tenant appearance/settings/theme lifecycle and the fourteenth Education admin page:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/tenant-appearance/index` | Branding, public/admin JSON settings, three platform theme templates, draft preview, and explicit publication. System Tenant remains authoritative for tenant name and websites; System RBAC, tenant validation, AdminUser projection, and operation/access logging are reused. |
V4290 adds one tenant-owned optimistic configuration row and one global platform-template table. `classic`, `focus`, and `high-contrast` use the exact legacy theme token/assets payloads. The anonymous public appearance endpoint stays under the normal validated `tenant-id` context and excludes admin flags, drafts, and operator data. It is deliberately separate from `/education/tenant/resolve`, whose minimal two-field locator response remains unchanged. Public JSON recursively rejects sensitive key names except `secretRef`, while renderable theme fields, CSS variables, icons, URLs, modes, densities, colors, and radii are validated by a closed policy at both preview and publication boundaries.
Education migration V4300 adds two Education-menu entry points while reusing existing native UI and backend contracts:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `pay/app/index` | Tenant payment applications and channels through Pay's existing controllers, V4320 tenant-scoped App/Channel persistence, channel configuration forms, eight granular app/channel permissions, and V4330's explicit legacy-account import modal. |
| `system/social/client/index.vue` | Tenant-scoped third-party login clients through System's existing controller, `TenantBaseDO`, Vben page, and four granular permissions. |
V4300 creates no Education page, endpoint, payment account, auth-provider, or credential table. Its unique route names allow the native components to coexist with their original menu locations. System SMS Channel remains a platform-global `@TenantIgnore` object and does not provide legacy tenant-level PNVS equivalence, so V4300 deliberately does not expose it as a migrated tenant auth provider.
V4330/EDU-021 extends the same native Pay page rather than adding a sixteenth Education page. Operators paste one reviewed manifest containing source IDs/checksum, explicit native channel, new business callbacks, and provider configuration. The modal warns that only `tenant_collect` WeChat/Alipay is supported and that native Pay retains channel credentials using its existing storage. The action is visible only when both App-create and Channel-create permissions are present, matching the backend AND check. Its template is instructional and contains placeholders that must be replaced. The tenant-filtered audit API stores mappings and digests but no second raw configuration or credential copy; import request-body logging is also disabled across access, non-production, and unexpected-error logs. Account-import audit history does not yet have a dedicated table view.
Education migration V4340 adds three more Education-menu entry points while continuing to reuse native Pay UI and controllers:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `pay/order/index` | Tenant-scoped native Pay order/detail queries and export using `pay:order:query` / `pay:order:export`. |
| `pay/refund/index` | Tenant-scoped native Pay refund queries and export using `pay:refund:query` / `pay:refund:export`. |
| `pay/notify/index` | Tenant-scoped callback task/detail/log inspection using `pay:notify:query`. |
V4340/EDU-022 adds no custom Education transaction page. It activates Pay-owned PostgreSQL order, extension, refund, notification-task, and notification-log persistence, makes every corresponding data object tenant-aware, and preserves channel-derived callback context plus the native tenant job. The pages begin with empty ledgers: no legacy order/payment/refund row is imported by inference.
V4350/EDU-023 extends the native `pay/order/index` page with **迁移旧支付交易**. The modal accepts one reviewed terminal aggregate JSON manifest, warns that live states and inconsistent totals fail closed, locks submission while importing, and switches to a compact recent-audit table after success. Import and audit-query permissions are independent; an audit-only operator can open the history tab without receiving write access. The importer does not call channel SDKs, callbacks, or notification jobs, and the UI never renders raw payloads or error originals. The template remains instructional: source UUID/checksum, optional explicit native Member ID, and event digests must come from a controlled export/reconciliation process.
Education migration V4360/EDU-024 adds three more Education-menu entry points while continuing to reuse native Pay UI and controllers:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `pay/transfer/index` | Tenant-scoped native transfer query/detail/export using `pay:transfer:query` and `pay:transfer:export`; the existing sync job remains tenant-aware. |
| `pay/wallet/balance/index` | Tenant-scoped native member wallet and transaction inspection using `pay:wallet:query`; the existing Member page retains the guarded `pay:wallet:update-balance` action. |
| `pay/wallet/rechargePackage/index` | Native recharge-package create/update/delete administration with independent CRUD permissions; recharge refund uses `pay:wallet-recharge:refund`. |
V4360 creates empty Transfer/Wallet ledgers and never derives balances from legacy payments. Tenant-qualified Redis locks, conditional administrator subtraction, positive-amount validation, composite tenant foreign keys, and non-negative database constraints protect the existing UI operations without adding an Education financial page or API.
Education migration V4370/EDU-025 adds a nested `商品中心` and five Education-menu entry points while reusing native Mall Product UI and controllers:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `mall/product/spu/index` | Native SPU/SKU create, update, status, delete, query, and export using the original Product services and five granular action permissions. |
| `mall/product/category/index` | Tenant-scoped two-level category tree administration with query/create/update/delete permissions. |
| `mall/product/brand/index` | Tenant-scoped brand query/create/update/delete administration. |
| `mall/product/property/index` | Native property and property-value query/create/update/delete administration. |
| `mall/product/comment/index` | Native comment query, visibility changes, and merchant replies using query/update permissions. |
V4370 activates nine Product-owned PostgreSQL tables and makes all corresponding data objects tenant-aware. It creates an empty catalog: the legacy `products` endpoint exposes display labels, links, and media but no authoritative SKU, integer price, stock, brand, property, or delivery facts, so no automatic product import is performed. The existing SPU form was normalized by oxfmt; no new UI component or runtime dependency was introduced.
Education migration V4380/EDU-026 adds a nested `优惠券中心` and reuses two native Mall Promotion pages:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `mall/promotion/coupon/template/index` | Native coupon-template query/create/update/status/delete with Product SPU/category scope validation and four granular template permissions. |
| `mall/promotion/coupon/index` | Native issued-coupon/member records, administrator send, query, and safe recovery using three granular coupon permissions. |
V4380 activates tenant-scoped `promotion_coupon_template` and `promotion_coupon`, including composite tenant references, counter/validity/discount/use-state checks, registration issuance and expiry-job compatibility. It starts empty because legacy code campaigns/redemptions are not equivalent to pre-issued native member coupons. No Education coupon API or custom UI page was added.
Education migration V4390/EDU-027 adds a nested `交易中心` and reuses two native Mall Trade pages:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `mall/trade/order/index` | Tenant-scoped native order page/summary/detail, remark, price/address update, delivery and pick-up verification using `trade:order:query`, `trade:order:update`, and `trade:order:pick-up`. |
| `mall/trade/config/index` | One active tenant-owned Trade configuration using `trade:config:query` and `trade:config:save`. |
V4390 activates tenant-scoped `trade_config`, `trade_cart`, `trade_order`, `trade_order_item`, and `trade_order_log`, replaces Promotion's temporary absent-Trade adapter with the native `TradeOrderApiImpl`, and starts the order ledger empty. Existing legacy payment totals do not prove normalized member/SPU/SKU line items or order lifecycle, so no automatic order import or Education order API/UI was added. Delivery master data is activated by EDU-029; after-sale, brokerage, and special-order tables remain later slices.
Education migration V4400/EDU-028 adds a nested `营销活动` group and reuses two native Mall Promotion pages:
| Menu component | Capability |
|---|---|
| `mall/promotion/discountActivity/index` | Native limited-time SKU discount query/create/update/close/delete with the original five action permissions. |
| `mall/promotion/rewardActivity/index` | Native full-reduction/gift rule query/create/update/close/delete with the original five action permissions. |
V4400 activates the three Promotion-owned tables consulted by normal Trade price calculation, tenantizes their native records, and preserves the existing PostgreSQL-compatible `findInSet` mapper path. No new frontend component or Education promotion API is introduced.
Education migration V4410/EDU-029 adds `配送管理` below the native Trade group and reuses three native pages:
| Menu component | Capability |
|---|---|
| `mall/trade/delivery/express/index` | Tenant-scoped express-company query/create/update/delete/export with the original five permissions. |
| `mall/trade/delivery/expressTemplate/index` | Express template and area-based charge/free rule query/create/update/delete with four permissions. |
| `mall/trade/delivery/pickUpStore/index` | Pickup-store query/create/update/delete and verifier binding through the existing native controller. |
V4410 activates all five Trade-owned delivery tables, connects Product SPUs to templates and pickup orders to stores through tenant-qualified references, and drives the native express calculator over real PostgreSQL persistence. The source has no physical-delivery master data, so no companies, stores, rules, or Product assignments are fabricated and no Education delivery API/UI is introduced.
Education migration V4420/EDU-030 adds `售后退款` below the native Trade group and reuses the native list/detail page:
| Menu component | Capability |
|---|---|
| `mall/trade/afterSale/index` | Tenant-scoped after-sale query/detail, agree/disagree, return receipt/refusal, Pay Refund handling, and operation logs using the five exact `trade:after-sale:*` permissions. |
V4420 activates Trade-owned `trade_after_sale` and `trade_after_sale_log`, connects Order, Order Item, Product, Pay Refund, Delivery, logs, and the order-item back-reference through tenant-qualified constraints, and retains the native app/admin services. The Vben page now sends `auditReason`, collects required `refuseMemo` in a validated locked modal, displays `createTime` as the application time, and applies exact permission guards to all actions. Source aggregate UUID refunds are not imported because they do not prove native Member, line-item, Product/SKU, return-logistics, or Pay Refund identities; no Education refund API/UI is introduced.
Education migration V4310 adds secure learning activation codes and the fifteenth custom Education admin page:
| Menu component | Capability and reused RuoYi module |
|---|---|
| `education/activation-code/index` | Batch filtering/creation/optimistic editing, one-time plaintext generation, masked status queries, and confirmed disable. Mall-owned SPUs are referenced through the existing Education resource-product binding; Member supplies the redeeming principal; the existing Education entitlement event pipeline grants access. |
The page separates query, management, and generation permissions. A generated plaintext set exists only in the controlled modal state: operators receive a prominent one-time warning, copy/download actions, and a second confirmation before discarding an unsaved set. Closing the modal clears plaintext; later tables return only masks. Batch product, duration, and prefix become immutable after the first code is generated. The app check/redeem endpoints reject administrator principals and use only the authenticated Member ID.
The operations page also loads `/education/capability` and shows feature flags, capabilities, migration themes, evidence levels, blockers, owning modules, and remaining legacy dependencies.
Custom Education HTTP contracts are isolated under `apps/web-antd/src/api/education/`; the Pay-owned legacy bridge remains under `apps/web-antd/src/api/pay/legacy-account-import/`. Page actions use the exact System RBAC permissions declared by the corresponding controllers and migration menu rows. Mutations use server-returned content, placement, or authoring versions rather than client-invented values.
## UI contract
- Uses the existing Vben, Ant Design Vue, `requestClient`, VXE Grid, and `TableAction` interfaces.
- Preserves the existing application typography and theme; no new runtime design dependency or external font was added.
- Shows explicit lifecycle labels and confirmations for publish/activate/archive actions.
- Locks modal submissions during requests and reports validation or request failures through the existing message layer.
- Rejects invalid question-option JSON and option-level correctness flags before submission.
- Normalizes optional metadata/access-rule JSON objects and validates ordered membership IDs.
- Enforces blueprint target selection and `minimum <= suggested <= maximum` before submission.
- Keeps import execution separate from upload and preview, surfaces scan/parser state, and never writes an uploaded file directly into the question catalog.
- Uses Member user IDs for classroom and entitlement subjects instead of introducing a second education account table.
- Uses server-returned product-binding versions for deactivation and source-system event IDs for entitlement idempotency.
- Keeps operational health read-only and displays only the bounded, sanitized detail returned by the backend.
- Displays the module's honest migration/capability manifest alongside health instead of hiding deferred dependencies.
- Separates `education:content-export` from the stronger `education:content-export:answers` permission and labels artifact generation as not yet implemented.
- Keeps member lookup and point enrichment behind `MemberUserApi`/`MemberPointApi`; no Education account, level, balance, or generic point-ledger table was added.
- Requires a feedback to be `RESOLVED` before a bounded 1100 point reward can be scheduled, records retry state in Education, and uses the feedback ID as the stable Member ledger business key.
- Uses optimistic feedback versions, independent feedback/reward permissions, and immutable tenant-owned status events for administrator handling.
- Applies RuoYi department/self data permission to classes, supervision rules, candidate class scope, and follow-up tasks, while keeping action permissions independent for query, rule authoring, generation, and handling.
- Computes supervision evidence from Education's existing learning tables, enriches students through `MemberUserApi`, validates assignees through `AdminUserApi`, and never creates a duplicate student or administrator directory.
- Uses optimistic follow-up versions and a tenant/batch/member database key so stale handling and duplicate generation both fail closed.
- Separates badge query, definition-write, and manual-grant permissions; definition updates use optimistic versions and disabled badges cannot be granted.
- Validates manual recipients through `MemberUserApi`, enriches grant history through Member/System public APIs, and sends badge messages through `NotifyMessageSendApi` without rolling back a durable grant when notification delivery fails.
- Keeps System Tenant name and websites authoritative; Education stores only presentation extensions and never adds branding/theme fields to the public locator contract.
- Separates appearance query, branding, settings, and theme permissions; every mutation uses a server-returned optimistic version.
- Keeps platform templates global while tenant drafts/published state uses RuoYi tenant injection; public projection contains neither admin feature flags nor drafts.
- Applies recursive public-secret rejection and a closed renderable-theme policy on the server, with matching JSON-object and obvious-secret preflight checks in Vben.
- Uses responsive Ant Design grids, visible labels, loading states, confirmation before publication, and keyboard-operable template choices without adding a new UI dependency or font.
- Separates activation-code query, management, and generation permissions; all updates/generation/disable actions submit server-returned optimistic versions.
- Never lists activation-code plaintext after generation. The one-time modal offers explicit copy/download, warns before unsaved dismissal, clears plaintext after close, and confirms permanent disable actions.
- Uses Mall SPU IDs, Member principals, and the existing Education resource binding/entitlement event pipeline instead of adding shadow product, account, coupon, or access-ledger models.
- Reuses native Pay order/refund/notify pages and permissions over composite-tenant PostgreSQL tables; Education does not own a duplicate financial ledger, callback controller, retry worker, or export implementation.
- Reuses the native order page for EDU-023 terminal aggregate import and recent redacted audit; Pay owns the importer, permissions, native ledgers, and audit tables.
- Reuses native Pay Transfer, Wallet Balance, and Recharge Package pages for EDU-024; V4360 supplies tenant-scoped empty ledgers and granular permissions, while Member administration retains the existing balance-adjustment form.
- Reuses native Mall Product SPU/SKU, Category, Brand, Property, and Comment pages for EDU-025; V4370 supplies tenant-scoped catalog persistence, composite graph references, and the original granular Product permissions without adding an Education product API.
- Reuses native Mall Promotion Coupon Template and Issued Coupon pages for EDU-026; V4380 supplies tenant-scoped template/instance persistence, Product/Member composition, exact coupon permissions, and fail-closed adoption without adding an Education coupon API.
- Reuses native Mall Trade Order and Config pages for EDU-027; V4390 supplies tenant-scoped order/cart/config persistence, Pay/Product/Coupon composition, exact Trade permissions, and fail-closed deferred-table adoption without adding an Education order API.
- Reuses native Mall Promotion Discount Activity and Reward Activity pages for EDU-028; V4400 supplies tenant-scoped persistence, Product references, validated rule JSON, exact permissions, and real PostgreSQL API lookup evidence without adding an Education promotion API.
- Reuses native Trade Express, Express Template, and Pickup Store pages for EDU-029; V4410 supplies tenant-scoped persistence, Product/Order references, exact permissions, and real PostgreSQL freight-calculation evidence without adding an Education delivery API.
- Reuses the native Trade After Sale list/detail page for EDU-030; V4420 supplies tenant-scoped state/log persistence, Order/Product/Pay/Delivery references, exact permissions, and real PostgreSQL service isolation. Required audit/refusal fields, application time, loading locks, responsive forms, and action guards match the backend contract without adding an Education refund API.
## Backend contract correction
The question revise endpoint previously delegated to the default `TenantQuestionLifecycleService.reviseDraft` implementation and could throw `UnsupportedOperationException`. It now performs tenant-scoped DRAFT/content-version CAS, increments `content_version`, and appends an immutable `education_question_version` snapshot. V4220 protects the same invariant in PostgreSQL. V4220 also backfills the education root and early question permission rows when a `system_menu` table is introduced after V4080/V4090, while still failing closed on conflicting IDs.
## Verification
Successful commands from the Vben submodule root:
```text
node --max-old-space-size=8192 node_modules/vue-tsc/bin/vue-tsc.js --noEmit --skipLibCheck -p apps/web-antd/tsconfig.json
./node_modules/.bin/oxfmt --check apps/web-antd/src/api/education apps/web-antd/src/views/education
./node_modules/.bin/oxlint apps/web-antd/src/api/education apps/web-antd/src/views/education
node --max-old-space-size=8192 ../../node_modules/vite/bin/vite.js build --mode production # from apps/web-antd
```
All four commands passed. Production builds emit independent chunks for all fifteen Education pages, including `learning-operations-*.js`, `supervision-*.js`, `badge-*.js`, `tenant-appearance-*.js`, and `activation-code-*.js`, and only report the existing non-blocking Lightning CSS warnings for unrelated `:deep` selectors.
Backend evidence:
```text
mvn -pl yudao-server -am -DskipTests compile
mvn -pl yudao-module-education -am -DskipTests test-compile
mvn -pl yudao-module-education -am -Dtest=EducationFlywayMigrationIntegrationTest -Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=/Users/tiku1/.sdkman/candidates/java/21.0.12-amzn \
mvn -pl yudao-module-education -am \
-Dtest=TenantQuestionLifecycleServiceImplTest,InfraFileImportObjectScanGatewayTest,StandardQuestionImportParserTest,QuestionImportJobServiceImplTest \
-Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=/Users/tiku1/.sdkman/candidates/java/21.0.12-amzn \
mvn -pl yudao-module-education -am \
-Dtest=MemberPointApiImplTest,LearningOperationsAdminServiceImplTest,LearningOperationsAdminControllerContractTest \
-Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=/Users/tiku1/.sdkman/candidates/java/21.0.12-amzn \
mvn -pl yudao-module-education -am \
-Dtest=StudentSupervisionAdminServiceImplTest,StudentSupervisionAdminControllerContractTest,StudentSupervisionPostgreSqlIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=/Users/tiku1/.sdkman/candidates/java/21.0.12-amzn \
mvn -pl yudao-module-education -am \
-Dtest=BadgeAdminControllerContractTest,BadgeAdminServiceImplTest,BadgeGrantServiceImplTest,BadgePostgreSqlIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=/Users/tiku1/.sdkman/candidates/java/21.0.12-amzn \
mvn -pl yudao-module-education -am \
-Dtest=TenantAppearanceAdminControllerContractTest,TenantAppearancePolicyTest,TenantAppearanceServiceImplTest,TenantAppearancePostgreSqlIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl yudao-module-education -am \
-Dtest=ActivationCodeServiceImplTest,ActivationCodeAdminControllerContractTest,ActivationCodeAppControllerHttpTest,ActivationCodePostgreSqlIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl yudao-module-pay -am \
-Dtest=PayLegacyAccountImportServiceImplTest,PayLegacyAccountImportControllerContractTest,PayChannelServiceTest,PayAppTenantContractTest \
-Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl yudao-module-pay -am \
-Dtest=PayOrderServiceTest,PayRefundServiceTest,PayNotifyServiceTest,PayTransactionTenantContractTest \
-Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl yudao-module-pay -am \
-Dtest=PayTransferServiceTest,PayTransferWalletTenantContractTest,PayWalletLockRedisDAOTest,PayWalletControllerTest,PayWalletServiceImplTest,PayWalletRechargeServiceImplTest,WalletPayClientTest \
-Dsurefire.failIfNoSpecifiedTests=false test
```
Compilation and test compilation passed. All 46 Flyway integration tests, all 35 selected question/import tests, all 13 selected category/export tests, all 8 selected Member point / learning-operations tests, all 8 selected supervision service/controller/PostgreSQL tests, all 7 selected badge controller/service/PostgreSQL tests, all 14 selected appearance controller/policy/service/PostgreSQL tests, and all 11 selected activation-code controller/service/PostgreSQL tests passed. The prior combined regression run executed 101 tests with no failures; V4310 then added the 11 focused activation-code checks. The EDU-021 Pay selection passes 29 tests: 17 Pay channel checks, two tenant/permission contracts, nine V4330 importer checks, and one request-log/dual-permission controller contract. The EDU-022 native transaction selection passes 86 tests: 46 order, 28 refund, 11 notify, and one tenant-inheritance contract. EDU-023 adds nine focused terminal importer/controller checks. EDU-024 adds 12 focused Transfer/Wallet checks. EDU-025 adds the Product tenant contract and the 44th Flyway scenario. EDU-026 adds the Promotion coupon tenant contract and the 45th Flyway scenario. EDU-027 adds the Trade tenant contract and the 46th Flyway scenario. V4390 evidence covers five tenant-aware Trade records, cross-tenant ID reuse and Cart/Order Item reference rejection, order/config state safety, exact order/config permissions/routes, five explicit sequences, and fail-closed global/deferred Trade adoption. V4380 evidence covers tenant-owned coupon templates/instances, cross-tenant identifier reuse and template-reference rejection, issue/use counter and discount/use-state safety, exact coupon permissions/routes, explicit identity sequences, and fail-closed global Coupon adoption. V4370 evidence covers nine tenant-scoped Product tables, cross-tenant identifier reuse and reference rejection, category-parent isolation, non-negative prices/stock/sales/commission/browse counts, rating bounds, active uniqueness, exact Product permissions/routes, and fail-closed global Product adoption. V4360 evidence covers cross-tenant composite references, per-tenant identifier reuse, same-tenant wallet uniqueness, negative-balance rejection, fail-closed global-wallet adoption, exact permission mappings, tenant-qualified Redis locking, safe administrator subtraction, recharge-refund wallet identity, and correct wallet-transfer lookup. V4350 evidence covers cross-tenant references, per-target-tenant source reuse, event count/digest consistency, sensitive-column absence, fail-closed global audit tables, and exact permission mappings. V4340 evidence covers composite tenant references, same merchant identifier across tenants, notification-task uniqueness and soft-delete recreation, fail-closed global transaction tables, and exact native menu/permission mappings. V4330 evidence covers provider/config mapping, safe disable mapping, replay/checksum conflict, mode/provider/channel rejection, unsafe endpoint/key rejection, audit/log redaction, composite tenant targets, and fail-closed global-audit adoption. V4310 evidence covers independent permissions, Member-only app principals, digest/mask-only persistence, tenant isolation, atomic entitlement/event creation, same-member replay, different-member conflict, disabled batch/binding rejection, and a concurrent unique winner. The current JDK emits Mockito's forward-looking dynamic-agent warning but does not fail the tests.
The normal `pnpm --filter @vben/web-antd run typecheck` entry point completed successfully with the configured workspace toolchain.
EDU-028 advances the PostgreSQL total to 47 passing Flyway scenarios and adds one real Spring/MyBatis Promotion API lookup test plus the Promotion activity tenant contract. The reused Discount/Reward page set passes Vben typecheck, scoped oxlint, and scoped oxfmt checks; V4400 proves exact routes/permissions, three explicit sequences, tenant-qualified Product references, PostgreSQL scope matching, rule validation, and fail-closed global-table adoption.
EDU-029 advances the PostgreSQL total to 48 passing Flyway scenarios and adds the five-record delivery tenant contract plus a real Spring/MyBatis template-service and `TradeDeliveryPriceCalculator` test. The reused delivery page set passes Vben typecheck and scoped lint/format checks; V4410 proves exact routes/permissions, five explicit sequences, tenant-qualified Product/Order references, area/location/amount/state constraints, persisted freight calculation, and fail-closed global-table adoption.
EDU-030 advances the PostgreSQL total to 49 passing Flyway scenarios and adds the two-record after-sale tenant contract plus a real Spring/MyBatis service/log integration test with the production tenant SQL interceptor. The corrected after-sale page passes Vben typecheck, scoped oxlint, and scoped oxfmt checks; V4420 proves exact route/permissions, two explicit sequences, tenant-qualified Order/Order Item/Product/Pay Refund/Delivery/log references, state/JSON/audit/return/refund constraints, isolated create/page/detail/log reads, and fail-closed global-table adoption.
## Remaining scope
- Run browser/API integration against a target deployment with V4220 applied and roles explicitly granted.
- Add management pages and backend contracts for each later capability selected from the deferred legacy families.
- Add real check-in, mock-exam, and activity-reward domains before exposing those legacy badge triggers; V4280 intentionally supports only migrated event sources.
- Keep domains in System Tenant websites. V4300V4330 expose operational Pay configuration and bounded account import; V4340V4360 activate native Pay transaction/Transfer/Wallet ledgers and pages; V4370 activates Product; V4380 activates coupons; V4390V4420 activate the normal Trade order, Promotion, delivery, and after-sale dependencies/pages. Explicit legacy Product/code-coupon/order/refund import, Trade brokerage and special-order Promotion families, production export/Member-map/opening-balance/runbook evidence, non-equivalent payment modes/providers, tenant PNVS, generic private secret storage/rotation, automatic Mall/Pay fulfillment, and refund-to-entitlement revocation remain dedicated decisions. V4310 resolves new activation-code ownership but not legacy-code import.
- Add the scheduled worker adapter for `DAILY`/`WEEKLY` supervision rules; V4270 persists the schedule and supports safe manual/interactive execution, but does not claim an automatic production worker deployment.
- Replace raw relationship IDs with searchable selectors when stable simple-list contracts are exposed by Education.
- Commit the UI changes in the UI repository and then advance the parent gitlink as part of the normal integration workflow.

View File

@@ -0,0 +1,424 @@
# Education SaaS Migration Goal
> Status: active goal
>
> Source system: `/Users/tiku1/code/tiku-backend`
>
> Target system: `/Users/tiku1/code/ruoyi-vue-pro`
>
> Target branch at goal creation: `feature/education-core-loop`
>
> Created: 2026-07-29
## 1. Mission
Migrate the valuable business capabilities, data models, rules, state machines, authorization semantics, idempotency guarantees, and API contracts from `tiku-backend` into the RuoYi-Vue-Pro architecture.
This is a capability migration, not a file-by-file TypeScript-to-Java translation.
The resulting system must be a multi-tenant education SaaS backend that:
1. Places education-specific behavior in `yudao-module-education`.
2. Reuses RuoYi-Vue-Pro platform modules before adding new infrastructure.
3. Uses the framework's tenant, authentication, RBAC, logging, file, job, messaging, payment, and membership capabilities.
4. Uses PostgreSQL and module-owned Flyway migrations for all forward database changes.
5. Is independently buildable, testable, migratable, and progressively deployable by vertical slice.
6. Does not require the old NestJS service after migration, except for explicitly documented temporary adapters with an exit plan.
## 2. Non-negotiable architecture rules
### 2.1 Reuse before building
| Legacy capability | Target capability to evaluate first |
|---|---|
| Login, token, refresh, logout, verification | Member/System authentication |
| Student and administrator accounts | Member/System users; Education stores domain extensions only |
| Tenant lookup, status, and isolation | System Tenant and framework tenant support |
| Roles, menus, permissions, data permission | System RBAC |
| Payment, refund, channel, callback | Pay |
| Generic products and orders | Mall and Pay |
| Membership, level, entitlement, points | Member first; Education only orchestrates domain rules |
| Notifications, SMS, email | System/Infra messaging capabilities |
| Uploads and object storage | Infra File |
| Scheduled and background work | Infra Job or existing messaging facilities |
| Audit and operation logs | System/Infra logging |
| AI generation and recommendation | AI |
| CRM leads and customer follow-up | CRM |
| Questions, practice, exams, wrong questions, favorites, reports | Education |
Reuse means depending on public APIs, framework extension points, or events. Education must not depend on another module's internal `ServiceImpl`, Mapper, or DO and must not copy platform implementations.
A change outside Education is allowed only when the existing public capability cannot satisfy the need and the new interface is minimal, generic, backward-compatible, tested, and owned by the module that provides the capability.
### 2.2 Multi-tenancy and identity
- Tenant business DOs inherit `TenantBaseDO`.
- MyBatis-Plus tenant injection remains the normal isolation mechanism.
- Request bodies and query parameters are never trusted for `tenantId` or current `userId`.
- The current tenant and user come from framework security context.
- Student identity reuses Member; administrator identity reuses System.
- Education stores education profiles and relationships, not passwords, tokens, or generic accounts.
- Cross-tenant platform operations use existing tenant-ignore mechanisms with strict permissions; no custom bypass.
- Unique constraints include `tenant_id` whenever uniqueness is tenant-scoped.
### 2.3 Security
- Controllers never return DOs directly.
- Question responses strip answers, explanations, scoring rules, correctness flags, and administrative metadata before leaving the service boundary.
- Invisible questions, unavailable tenants, disabled features, and unavailable catalog sources fail closed.
- Logs do not contain tokens, passwords, verification codes, answers, or payment secrets.
- Student App, Tenant Admin, Platform Admin, public, and internal APIs have explicit and separate authorization models.
- Existing System RBAC and permission annotations are used for admin endpoints.
### 2.4 PostgreSQL and Flyway
All new or changed schema, indexes, constraints, required seed data, backfills, baselines, and Flyway configuration must use the project `flyway-postgresql` skill.
Required conventions include:
- `BIGINT GENERATED BY DEFAULT AS IDENTITY`
- `TIMESTAMP` and `CURRENT_TIMESTAMP`
- PostgreSQL `BOOLEAN`
- `ON CONFLICT ... DO NOTHING`
- `ON CONFLICT (...) DO UPDATE SET ... EXCLUDED.column`
- `COALESCE`, `TO_CHAR`, and `EXTRACT` where applicable
- module migration path: `<module>/src/main/resources/db/migration/<module>/`
Published migrations are immutable. Corrections use higher-version forward migrations. Historical `sql/mysql/education` files are not the delivery mechanism for new database changes. Application-layer tenant isolation must not be replaced by copied Supabase RLS.
Only an actual successful run against PostgreSQL may be reported as a successful database migration. Static SQL review, compilation, packaging, or resource copying must be described accurately as such.
## 3. Required Phase 0 investigation
Do not start broad feature implementation before completing this investigation.
### 3.1 Repository and rule inspection
Read and obey:
- target `CLAUDE.md`;
- target `yudao-module-education/README.md`;
- source `README.md`;
- applicable `AGENTS.md`, module READMEs, database documentation, and `.claude/skills/index.yaml`;
- actual runtime configuration and Git state.
Inspect both repositories' working trees and histories. Preserve all existing uncommitted work: no reset, destructive checkout, clean, or unrelated rewrite.
### 3.2 Historical commit review
Review these commits and determine whether their non-Education changes remain justified:
- `11e9cc6 feat(education): add module application shell`
- `0f846fd feat(education): resolve student tenant context`
Review at least:
- root `pom.xml`;
- `yudao-server/pom.xml`;
- `ServiceErrorCodeRange`;
- `TenantCommonApi`;
- `TenantRespDTO`;
- `TenantApiImpl`;
- historical `sql/mysql/education` artifacts;
- Education tenant-resolution logic.
Classify each design as retain, adjust, replace, remove by forward correction, or pending decision. Do not revert whole commits merely because one part is unsuitable.
### 3.3 Legacy capability inventory
Scan at least:
```text
apps/api/src/features
apps/api/src/nest
apps/worker/src
apps/asset-scanner/src
packages
supabase/migrations
supabase/seed*
docs
```
Cluster capabilities rather than mechanically mapping every endpoint. Cover Auth, Tenant, Profile, Learning, Catalog, Scoreline, Video, AI, Tenant Content, Tenant Admin, Platform Admin, Referral, Commerce, Worker, Asset Scanner, tables, indexes, constraints, RLS, functions, triggers, and seeds.
### 3.4 Target capability inventory
Inspect Education's current implementation and reusable capabilities in System, Member, Pay, Mall, Infra, AI, CRM, framework starters, and Server integration. Account for both committed and uncommitted implementation; do not rebuild existing slices.
## 4. Required migration artifacts
Maintain these artifacts under `docs/education/migration/` or a reviewed scratch equivalent while discovery is incomplete:
1. `current-state.md` — verified implementation and working-tree state.
2. `capability-matrix.md` — grouped legacy-to-target capability matrix.
3. `api-mapping.md` — legacy method/path and authorization to target contract.
4. `database-object-mapping.md` — table/RLS/function/trigger/storage disposition.
5. `module-reuse-map.md` — reusable public APIs and identified gaps.
6. `commit-review-11e9cc6.md`.
7. `commit-review-0f846fd.md`.
8. `decisions.md` — unresolved product or architecture decisions and ADR links.
9. `slice-roadmap.md` — vertical slices with blocking edges.
10. `first-slice.md` — first incomplete, bounded, low-risk delivery slice.
Each capability-matrix row must include:
```text
legacy capability
legacy code location
legacy database objects
business value
target module
existing capability to reuse
Education gap
whether another module must change
priority
risk
verification method
current status
evidence
open decision
```
Allowed status values:
- replaced by RuoYi-Vue-Pro;
- migrated;
- partially migrated;
- pending migration;
- explicitly retired;
- product decision required.
## 5. Delivery phases
### Phase 0 — inventory and architecture mapping
Complete the artifacts above, review the two historical commits, assess current uncommitted work, correct confirmed obsolete documentation, and select the first incomplete vertical slice.
### Phase 1 — tenant, identity, and permission baseline
Reuse System Tenant and Member/System Auth, unify Student/Tenant Admin/Platform Admin identity rules, verify cross-tenant protections, and decide whether the `TenantCommonApi` extension is a valid generic API.
### Phase 2 — student core learning loop
Verify and complete only genuine gaps in:
```text
catalog browsing
→ safe question browsing
→ create practice
→ save answer
→ restore practice
→ submit
→ report
→ wrong questions
→ favorites
```
The current branch may already implement much of this phase. Review before adding anything.
### Phase 3 — education content management
Question banks, questions, classifications, catalogs, publishing, imports/exports, resource associations, question videos, and content access control.
### Phase 4 — tenant education management
Classes, education student relationships, invitations, education roles, tenant education configuration, education points/badges, learning insight, and operations metrics. Generic users, roles, and tenants remain in Member/System.
### Phase 5 — commercialization
Products, orders, payments, refunds, subscriptions or entitlements, reconciliation, collection, commission, and referral relationships. Prefer composition of Mall, Pay, Member, and CRM. Education owns only education-domain bindings and orchestration.
### Phase 6 — asynchronous and operational capabilities
Imports/exports, content processing, billing work, notifications, resource scanning, audit, retries, and observability using Infra Job, messaging, File, and logging capabilities.
Each phase must be independently compilable, testable, deployable, and reversible at the application/configuration level.
## 6. Workflow operating model
This goal is executed as a decision-first, multi-session program:
```text
Phase 0 read-only multi-agent discovery
→ Wayfinder-style decision map
→ domain modeling and module-boundary design
→ migration specification
→ blocker-aware vertical-slice tickets
→ one fresh implementation context per ticket
→ TDD and PostgreSQL/Flyway when applicable
→ standards/spec review
→ security review
→ simplification
→ focused and integration verification
```
### 6.1 Multi-agent use
Use multi-agent workflows for broad read-only discovery, independent commit reviews, module capability mapping, adversarial verification, and completeness checks.
Do not allow multiple agents to edit the current dirty working tree concurrently. Implementation is serial by default. Isolated worktrees are permitted only for independent file sets with an explicit integration plan.
### 6.2 Ticket shape
Tickets are vertical behaviors, not technical layers. A ticket may include migration, DO, Mapper, Service, Controller, tests, and documentation needed to deliver one observable capability.
Good examples:
- a student can browse published catalog content in the current tenant;
- a student can create and restore a practice session;
- a student can idempotently save one answer;
- a student can idempotently submit and read an immutable report;
- a tenant administrator can publish a question.
Avoid tickets such as “create all DOs” or “create all Controllers.” Declare blocking edges explicitly and implement blockers first.
### 6.3 Skill selection
- `flyway-postgresql`: every database or Flyway change.
- `mattpocock-skills:domain-modeling`: ambiguous or overloaded education language.
- `mattpocock-skills:codebase-design`: module interfaces, provider/adapter seams, and public API boundaries.
- `mattpocock-skills:research`: external primary-source research, not local repository inventory.
- `mattpocock-skills:prototype`: throwaway executable exploration for a single unresolved design question.
- `mattpocock-skills:tdd`: red-green implementation of a concrete behavior.
- `mattpocock-skills:diagnosing-bugs`: hard defects after establishing a reliable failing command.
- `mattpocock-skills:code-review`: standards and specification review from a fixed Git point.
- `security-review`: tenant, identity, authorization, secret, answer, payment, and file boundaries.
- `simplify`: reuse and structural cleanup after correctness review.
- `run` and `webapp-testing`: real application and student-flow verification.
If a named planning skill is unavailable, preserve the same artifacts and gates using repository documents, issue files, and the workflow tool rather than skipping the phase.
## 7. Implementation rules
Follow the target layering:
```text
controller
service
dal/dataobject
dal/mysql
convert
enums
api
framework/integration
```
- Controllers perform protocol adaptation and validation.
- Services own transactions, state transitions, authorization-relevant domain checks, and idempotency semantics.
- Mappers own data access only.
- VO, DTO, and DO responsibilities remain distinct.
- Use project `CommonResult`, paging, validation, conversion, exception, error-code, Redis, lock, transaction, and audit facilities.
- External or legacy coexistence is hidden behind explicit Provider/Adapter boundaries.
- Do not introduce NestJS runtime dependencies or reproduce NestJS Guard/Decorator architecture.
Idempotency and consistency requirements:
- database uniqueness is the final idempotency guard;
- critical writes are transactional;
- do not rely only on check-then-insert;
- duplicate-request response semantics are explicit;
- concurrency is tested;
- external payment, notification, and file calls do not create long database transactions;
- at-least-once consumers define duplicate handling.
## 8. Verification gates
Every implementation slice runs the minimum sufficient focused tests plus at least:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
Behavior changes require focused tests in Education and every affected module. Database changes additionally require PostgreSQL syntax validation, module packaging, and confirmation that migration files appear under `target/classes/db/migration/`.
Test applicable negative and concurrent scenarios:
- cross-tenant access;
- unauthenticated access;
- unauthorized access;
- duplicate request;
- concurrent request;
- sensitive-field leakage;
- catalog source failure;
- disabled feature;
- historical-data compatibility.
## 9. Per-slice reporting contract
Before implementation, report:
1. legacy capability and evidence;
2. legacy files and database objects;
3. target module;
4. RuoYi-Vue-Pro capabilities reused;
5. why another module will or will not change;
6. database changes;
7. tests;
8. risks and rollback method.
After implementation, report:
1. changed files;
2. reused modules;
3. new Education domain capability;
4. reasons for every non-Education change;
5. replaced legacy code;
6. unmigrated capabilities;
7. commands actually run and results;
8. whether PostgreSQL migration was actually executed;
9. known risks and recommended next slice.
Do not state “complete” without verifiable files and command results.
## 10. Prohibitions
Do not:
- embed the old NestJS project;
- mechanically translate all TypeScript files or all 342 APIs;
- duplicate authentication, tenant, RBAC, payment, membership, notification, file, job, or audit platforms in Education;
- trust client `userId` or `tenantId`;
- expose answers or explanations;
- introduce MySQL dialect or new MySQL delivery scripts;
- bypass Flyway or modify published migrations;
- depend on internal implementations of other modules;
- weaken security for backward compatibility;
- overwrite unrelated uncommitted work;
- use destructive Git commands;
- claim tests or migrations succeeded without running them;
- begin a broad implementation before Phase 0 identifies the actual gaps.
## 11. Definition of done
The migration is complete only when:
1. every legacy capability has a reuse, migration, retirement, or decision status;
2. Education-specific behavior resides in Education;
3. platform capabilities are reused through appropriate boundaries;
4. every non-Education modification has a necessity statement and tests;
5. tenant isolation and sensitive-question-field controls are verified;
6. database changes use PostgreSQL Flyway;
7. core vertical flows have automated tests;
8. required compile and diff checks pass;
9. documentation matches the current PostgreSQL/Flyway architecture;
10. existing user changes have not been overwritten;
11. the target can run without the old service, or every temporary dependency has an owner and exit plan.
## 12. Immediate execution directive
Phase 0 inventory and the first safe-question slice have been executed. Continue through the blocker-aware tickets under [`docs/education/migration/issues/`](issues/README.md).
Current execution order is maintained in [`docs/education/migration/issues/README.md`](issues/README.md). Do not duplicate the live order here; completed bounded tickets remain historical dependencies, while current work follows the ticket index and its blockers.
Before every ticket:
1. read this Goal, the ticket, relevant decisions, and current Git status;
2. preserve all existing uncommitted work;
3. state the legacy capability, reuse boundary, database impact, tests, risk, and rollback;
4. use a fresh implementation context and work serially in the dirty tree;
5. use `flyway-postgresql` for any database or Flyway change;
6. finish with focused tests, `git diff --check`, and `mvn -pl yudao-server -am -DskipTests clean compile`;
7. report exact results and never claim PostgreSQL migration success without a real successful run.
Questions that can be answered from code, Git history, configuration, tests, or documentation must be investigated rather than asked. Ask only for genuine product decisions whose outcomes materially change implementation.

View File

@@ -0,0 +1,37 @@
# EDU-000 — Phase 0 inventory and architecture map
- **Status:** done
- **Type:** discovery
- **Phase:** 0
- **Blockers:** none
## Outcome
A verified static map of the source system, target capabilities, historical commits, database objects, reusable modules, unresolved decisions, and vertical-slice roadmap exists under `docs/education/migration/`.
## Delivered artifacts
- `00-current-state.md`
- `01-capability-matrix.md`
- `02-api-mapping.md`
- `03-database-object-mapping.md`
- `04-module-reuse-map.md`
- `05-commit-review-11e9cc6.md`
- `06-commit-review-0f846fd.md`
- `07-decisions.md`
- `08-slice-roadmap.md`
- `09-first-slice.md`
- `10-documentation-corrections.md`
## Evidence and caveats
- Investigation was read-only and multi-agent.
- No PostgreSQL migration was executed during discovery.
- The source baseline is provisionally `main` at `033701a`; no source `feature/education-core-loop` ref was found.
- The target worktree is dirty and must remain protected.
- Completing this ticket did not resolve the product and architecture decisions recorded in `07-decisions.md`.
## Verification
- Artifacts generated and inspected.
- `git diff --check -- docs/education/migration` passed at delivery time.

View File

@@ -0,0 +1,55 @@
# EDU-001 — Provider-neutral safe question content
- **Status:** done with recorded follow-up coverage
- **Type:** implementation
- **Phase:** 0 / core-loop prerequisite
- **Blockers:** EDU-000
## Student outcome
A student cannot receive or restore an apparently valid question when its type, visibility, or options are malformed. Student-visible question content and practice snapshot JSON do not expose answer-bearing fields.
## Scope delivered
- Shared question-type and option-shape contract.
- Option-backed, optionless, unsupported composite, and unknown type handling.
- Fail-closed single/page/collection safe projection.
- Fail-closed practice creation for disabled/unavailable provider, invisible question, and unsafe options.
- Strict practice snapshot restoration.
- Answer-free option snapshot JSON.
## Relevant files
- `docs/education/migration/11-question-content-safety-contract.md`
- `yudao-module-education/CONTEXT.md`
- `service/question/QuestionContentSafety.java`
- `service/question/QuestionCatalogServiceImpl.java`
- `service/practice/PracticeSessionServiceImpl.java`
- `service/practice/SessionResponseAssembler.java`
- corresponding focused tests
## Acceptance criteria
- [x] Invalid option-backed content fails closed.
- [x] Valid optionless content may have no options.
- [x] `reading` and unknown types fail closed until modeled.
- [x] Safe responses and option snapshot JSON exclude correctness and explanation fields.
- [x] Malformed persisted snapshots do not become empty valid options.
- [x] Disabled/unavailable providers and invisible questions cannot create sessions.
- [x] Focused safety tests pass.
- [x] Required compile and diff checks pass.
## Follow-up coverage
- Add a clean JavaCatalogProvider public-seam/PostgreSQL contract test when the native catalog test harness is established.
- Add explicit cross-tenant and `tenant_id=0` PUBLIC graph tests in the native catalog/graph-integrity slice.
- Do not test private provider parsing through reflection.
## Verification recorded
```text
Focused tests: 112 run, 0 failures, 0 errors
git diff --check: passed
yudao-server clean compile: BUILD SUCCESS
PostgreSQL migration: not applicable and not executed
```

View File

@@ -0,0 +1,103 @@
# EDU-002 — Restore the full Practice regression baseline
- **Status:** completed as test-context repair; PostgreSQL persistence coverage moved to EDU-016
- **Type:** test-enablement vertical slice
- **Phase:** 0 / Phase 2 prerequisite
- **Blockers:** EDU-001
## Outcome
The complete Practice test set starts reliably and distinguishes test-context failures from real behavior regressions across create, answer, restore, submit, report, wrong-question, and favorite flows.
## Why this is next
The direct EDU-001 tests pass, but broader Practice tests currently fail during Spring test-context creation because test configurations that import `PracticeSessionServiceImpl` do not consistently provide its current `ScoringService` dependency. Some tests also use name-based `@Resource` injection against Mapper proxies, producing type mismatches. Continuing core-loop work without this feedback loop would hide regressions.
## Existing code and data
- No legacy capability is being newly migrated.
- No database object changes are required.
- Existing Education test SQL and Mapper test infrastructure are reused.
## Scope
1. Inventory every test that imports, instantiates, or indirectly creates `PracticeSessionServiceImpl`.
2. For each test context, choose one explicit dependency strategy:
- import the real `ScoringServiceImpl` when scoring behavior is under test; or
- provide `@MockitoBean ScoringService` when the test is outside the scoring seam.
3. Replace ambiguous name-based Mapper injection only where it currently prevents the target tests from starting.
4. Run the complete focused Practice regression set.
5. Classify remaining failures as:
- test assembly defect;
- existing product defect;
- expected contract change from EDU-001;
- unrelated dirty-worktree issue.
6. Fix only test-assembly defects in this ticket. Create separate tickets for product defects.
## Reuse boundaries
- Reuse `BaseDbUnitTest`, existing Education test SQL, Spring `@Import`, and `@MockitoBean`.
- Do not create a parallel test framework.
- Do not modify System, Member, database schema, or production state machines.
- Do not weaken assertions merely to make tests green.
## Target test set
```text
PracticeSessionServiceImplTest
PracticeAnswerServiceImplTest
PracticeSubmitServiceImplTest
PracticeSubmitProjectionIntegrationTest
PracticeSessionControllerHttpTest
PracticeAnswerControllerHttpTest
PracticeSessionControllerSubmitHttpTest
WrongQuestionServiceImplTest
FavoriteServiceImplTest
```
## Acceptance criteria
- [x] Every target class starts its Spring/JUnit context.
- [x] No target class fails because `ScoringService` is missing.
- [x] No target class fails from avoidable Mapper bean-name/type injection ambiguity.
- [x] EDU-001 safe-content assertions remain green.
- [x] Any actual behavior failure is documented with reproducible command and assigned a separate ticket.
- [x] No production behavior or database schema is changed unless a failing regression proves it is necessary and the ticket is explicitly amended.
## Test command
```bash
mvn -pl yudao-module-education \
-Dtest='PracticeSessionServiceImplTest,PracticeAnswerServiceImplTest,PracticeSubmitServiceImplTest,PracticeSubmitProjectionIntegrationTest,PracticeSessionControllerHttpTest,PracticeAnswerControllerHttpTest,PracticeSessionControllerSubmitHttpTest,WrongQuestionServiceImplTest,FavoriteServiceImplTest' \
-Dsurefire.failIfNoSpecifiedTests=false \
test
```
Then:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
## Risk and rollback
- **Risk:** Low production risk; medium risk of exposing pre-existing behavior defects.
- **Rollback:** Revert only this ticket's test assembly changes. There is no database rollback.
## Completion result
Test-context assembly was repaired:
- `ScoringService` is now explicitly mocked in Practice contexts that are not testing scoring itself.
- `PracticeQuestionMapper` fields use type-based injection where name-based `@Resource` resolved the wrong MyBatis proxy.
- Controller tests and `PracticeSessionServiceImplTest` start and pass.
The expanded regression run then exposed a separate infrastructure limitation rather than a remaining Spring context defect: H2 cannot execute the production PostgreSQL `ON CONFLICT` statements, and the unified `education_idempotency` test table was missing. A temporary H2 table definition was added so table absence no longer masks the dialect issue, but PostgreSQL conflict semantics cannot be made truthful on H2. The required follow-up is [`EDU-016`](EDU-016-postgresql-persistence-tests.md).
## Verification result
- Controller Practice tests: 39 passed.
- `PracticeSessionServiceImplTest`: 24 passed before PostgreSQL-dialect persistence paths were included.
- Full targeted suite starts after dependency/injection repair, then fails on confirmed H2/PostgreSQL dialect mismatch and downstream assertions.
- No production behavior was changed by EDU-002.

View File

@@ -0,0 +1,160 @@
# EDU-003 — Decide tenant resolution and student-principal policy
- **Status:** done — corrected policy and EDU-004 test seams are implementation-ready
- **Type:** decision
- **Phase:** 1
- **Blockers:** EDU-000
## Decision outcome
Public tenant resolution accepts caller-supplied locator claims and is not an authentication boundary. Browser `Origin`/`Referer` evidence improves browser-context consistency but is forgeable by non-browser clients. The accepted contract therefore documents tenant-existence disclosure, uses one redacted unavailable response for unknown/disabled/expired tenants, requires abuse controls, and reserves authenticated/signed locators for deployments that require spoof resistance. Authenticated Education context is Member-only. This ticket records policy and tests-to-write only; it changes no production behavior.
## Domain language
- A **Tenant Locator Claim** is an unauthenticated pre-login value used to request tenant selection: either a browser-context hostname claim or an explicit Public Tenant Handle. It is not proof of caller identity or tenant authorization.
- **Browser-context evidence** is a normalized host derived from `Origin`, falling back to `Referer`. It can bind browser UX inputs consistently, but any HTTP client can forge it.
- A **Public Tenant Handle** is the current System tenant's unique `name` used as an exact public lookup key because the target has no separate stable tenant-code capability. It is not called a Tenant Code. It is case-sensitive, must match `^[A-Za-z0-9._-]{2,64}$`, and administrators must treat it as immutable after publication. A future mutable display label must be a separate field.
- A **Student Principal** is an authenticated identity whose `LoginUser.userType` is `UserTypeEnum.MEMBER`. A generic authenticated account is not necessarily a Student Principal.
- **Public Tenant Resolution** maps a Tenant Locator Claim to minimal login-routing fields. It intentionally discloses existence when a claim succeeds; it does not disclose whether a failed tenant is unknown, disabled, or expired.
## Evidence reviewed
- Legacy `apps/api/src/features/tenant/locator.ts` parses and compares `Origin`, `Referer`, request hosts, and a tenant code, but does not authenticate header provenance.
- Legacy `resolver.ts` compares the source `slug` with the explicit code, proving the source Tenant Code was distinct from its display name.
- The target has no System-owned stable tenant-code field or API. `system_tenant.name` is unique and mutable through administration; it is the only current exact generic lookup key.
- Current `EducationTenantController` accepts arbitrary public `hostname` or `tenantName`, preserves ports, returns status and Education-configured `loginMethods`, and exposes distinct unknown/disabled/expired errors.
- `EducationProperties.hostnameTenantMap` documents lowercase host-only keys without ports, while current implementation and tests preserve ports.
- `system_tenant.websites` is an exact string-list lookup and existing target tests demonstrate values containing a scheme. No normalization seam currently makes those values host-only.
- `EducationContextController` derives IDs from framework contexts but reads only the user ID and therefore does not reject an authenticated ADMIN principal.
- `TenantSecurityWebFilter` already fills a missing request tenant from the authenticated principal, rejects authenticated principal/request-tenant mismatch, requires a tenant for non-ignored URLs, and validates tenant availability.
- `TenantCommonApi` exposes generic System-owned tenant lookup methods; `TenantApiImpl` implements them, but the interface currently hides missing adapters behind `UnsupportedOperationException` defaults and has no focused owning-module contract test.
- No verified Member public interface advertises enabled login methods. `MemberConfigApi` currently exposes points configuration only.
## ADR: public tenant-resolution and student-principal policy
### Status
Accepted for EDU-004.
### Context and trade-off
The resolver is public and cannot authenticate `Origin`, `Referer`, or ordinary query/header values. Browser headers are useful for consistent browser routing, not identity. A successful lookup necessarily distinguishes an available tenant from a failed candidate when it returns routing fields. The contract can hide lifecycle state among failures, but cannot honestly promise general non-enumeration without an unguessable or signed locator.
The target also lacks the source system's distinct stable tenant code. Adding one would require a separately designed System-owned capability and likely data work. For the current slice, the existing unique System tenant `name` is explicitly exposed as a constrained Public Tenant Handle; it is no longer mislabeled as a Tenant Code.
### Decision
1. **Production browser-context binding, not trusted identity**
- Derive browser-context evidence from a valid HTTP(S) `Origin`; if absent, use a valid HTTP(S) `Referer`.
- A supplied `hostname` may only confirm that evidence. A mismatch is a public locator conflict.
- `Origin` and `Referer` are untrusted caller claims. Proxy preservation and forwarding-header controls do not make them authentic and are not cited as spoofing protection.
- A non-browser/headless caller can forge either header and probe hostnames. This accepted threat is handled through the public disclosure policy and abuse controls below.
- A deployment requiring spoof resistance must replace this public mode with an authenticated/signed locator or a host value supplied through a separately designed trusted-proxy boundary. That stronger mode is not implemented by EDU-004.
2. **Explicit headless handle and legacy query compatibility**
- A headless client may submit `tenantHandle`, defined above as the existing System tenant unique `name` under a constrained public contract.
- The legacy public `tenantName` query is unsupported and must be rejected, not silently aliased. EDU-004 adds a compatibility test for its rejection/removal.
- A browser domain claim and explicit `tenantHandle` may be supplied together only when both resolve to the same tenant; disagreement is a public locator conflict.
3. **Local-development activation seam**
- The sole authority is `yudao.education.tenant-resolution.local-development-enabled`.
- Its secure default is `false`; absence means production-safe behavior. Spring profile names and environment names do not implicitly enable it.
- Only developer workstations and automated tests may set it to `true`; shared, staging, and production deployments must keep it `false`.
- When enabled, a configured local request host (`localhost`, `*.localhost`, loopback IPv4, `0.0.0.0`, or `::1`) may resolve without a handle. If `tenantHandle` is also present, the explicit handle takes precedence.
- EDU-004 tests code-less local host, local host plus handle, and rejection of local/request-host fallback when the flag is absent or false.
4. **Hostname identity and normalization**
- Tenant hostname identity is host-only: trim whitespace, lowercase, remove one trailing dot, remove IPv6 brackets, and discard default or non-default ports.
- Accept valid DNS hosts, IPv4, and IPv6; reject credentials, paths, comma-separated/multi-value input, malformed authorities, and unsupported schemes.
- `localhost:48080` normalizes to `localhost`.
- Canonical `system_tenant.websites` entries used for this resolver are host-only values in the same normalized form. Entries containing a scheme, path, credentials, comma-separated values, or a port are legacy/non-canonical configuration and are not matched by Public Tenant Resolution.
- EDU-004 implements canonical exact lookup and focused tests; it does not silently normalize legacy stored candidates at read time. Tenant administrators must correct non-canonical website configuration before enabling domain resolution. If later inventory requires automated data correction, that becomes a separately scoped Flyway/data ticket using `flyway-postgresql`; EDU-004 must not claim such correction.
5. **Authenticated Education context**
- `/education/context` obtains the full `LoginUser`, rejects missing authentication, and rejects `userType != UserTypeEnum.MEMBER`.
- User and tenant IDs continue to come only from security and tenant contexts.
- EDU-004 preserves and does not duplicate or bypass `TenantSecurityWebFilter` mismatch and availability checks.
6. **Login-method metadata ownership**
- Login-method metadata belongs to Member authentication, not System tenant metadata and not Education.
- EDU-004 removes `loginMethods` from Education resolution and deprecates Education configuration/documentation that presents it as authoritative.
- If later routing proves it necessary, introduce only a minimal Member-owned public interface with focused Member tests; do not create tenant-specific auth configuration in Education.
7. **Exact external wire contract**
- The target framework represents business failures as HTTP `200 OK` with a `CommonResult` envelope. EDU-004 keeps that convention; tests assert both transport status and envelope.
- Malformed, missing, locally forbidden, or otherwise unsupported locator claim: HTTP `200`; `CommonResult.code = 1005001003`; `msg = "租户识别请求无效"`; `data = null`.
- Domain/handle or browser-evidence/requested-host conflict: HTTP `200`; `CommonResult.code = 1005001008` (new stable Education business code); `msg = "租户识别信息冲突"`; `data = null`.
- Unknown, disabled, or expired tenant: HTTP `200`; `CommonResult.code = 1005001004`; `msg = "当前租户不可用"`; `data = null`.
- Messages contain no rejected host/handle, lifecycle status, System exception text, or lookup detail. Logs may record a reason category and correlation metadata but must not log secrets or echo unsanitized header values.
- Unknown, disabled, and expired paths must have identical status, code, message, JSON field set, null-data shape, and no intentional timing distinction. System errors remain internal.
- Success is HTTP `200`, `code = 0`, `msg = ""`, and data contains only `tenantId` and `displayName`. `displayName` currently comes from the System tenant `name`; because that same field is the current Public Tenant Handle, an exact handle lookup necessarily returns the submitted handle as `displayName`. A future non-echoing mutable label requires a separate System-owned public display field. The response contains no separate handle field, raw status, websites, expiry, package, private configuration, internal lifecycle detail, or `loginMethods`.
8. **Disclosure and abuse threat model**
- The resolver is not generally non-enumerating: a valid Public Tenant Handle or domain claim yields success with tenant ID/display name, while an unavailable candidate yields the generic failure.
- The accepted guarantee is only unknown/disabled/expired indistinguishability.
- EDU-004 must attach the public resolver to the repository's existing public API rate-limiting/ingress mechanism where available, emit structured success/failure-category security metrics, and document alerting for sustained candidate probing. If no reusable limiter seam exists, EDU-004 records that operational blocker rather than inventing an Education-only limiter.
9. **System seam**
- Retain `TenantCommonApi` as the generic System-owned seam; do not add Education-specific locator, branding, redaction, or login-method concepts.
- Replace `UnsupportedOperationException` lookup defaults with required abstract methods and add focused `TenantApiImpl` contract tests.
- `EducationTenantController` remains the public adapter applying claim consistency, canonical website policy, availability coarsening, exact errors, and redaction.
### Rejected alternatives
- Treating `Origin` or `Referer` as authenticated tenant identity.
- Claiming forwarding-header ingress controls authenticate browser headers.
- Claiming general non-enumeration while successful lookup returns identifying fields.
- Silently aliasing System tenant `name` to the distinct Tenant Code domain term.
- Continuing the legacy `tenantName` public query.
- Silently normalizing scheme/path/port-bearing stored website values during lookup.
- Port-sensitive tenant identity.
- ADMIN accepted as Student Principal.
- Education-owned login methods or an Education-specific System API.
### Consequences
- EDU-004 intentionally changes current query, response, error, local-mode, website, and port behavior.
- Published Public Tenant Handles use the System tenant unique `name`; renaming one is a breaking login-routing change until a genuine stable System-owned code exists.
- Non-canonical website entries require configuration correction before domain resolution is enabled; no database change is authorized here.
- Public existence disclosure is accepted and must be monitored and throttled. Strong spoof resistance requires a future signed/authenticated locator design.
## EDU-004 exact test matrix
| Scenario | Exact expected behavior | Owning test seam |
|---|---|---|
| Valid production `Origin` | Resolve normalized domain claim; success HTTP 200/code 0 | Education controller HTTP test |
| Missing `Origin`, valid `Referer` | Resolve normalized Referer host; success HTTP 200/code 0 | Education controller HTTP test |
| Forged but syntactically valid browser header | Documented as accepted untrusted claim; no authenticity assertion | Education controller test name/documentation |
| Malformed `Origin` | HTTP 200/code 1005001003/generic message/null data; no fallback | Education controller HTTP test |
| Origin/requested-host mismatch | HTTP 200/code 1005001008/generic conflict/null data | Education controller HTTP test |
| Arbitrary production hostname without browser evidence | HTTP 200/code 1005001003 | Education controller HTTP test |
| Explicit `tenantHandle` | Exact case-sensitive constrained System-name lookup | Education controller HTTP test |
| Legacy `tenantName` query | Rejected/unsupported; HTTP 200/code 1005001003 | Education controller compatibility HTTP test |
| Unknown, disabled, expired | Identical HTTP 200/code 1005001004/message/JSON/null data | Education controller HTTP parameterized test |
| Host case/trailing dot/IPv4/IPv6/ports | Canonical host-only identity; all ports discarded | Education normalization/HTTP tests |
| Non-canonical stored website candidate | Not matched; generic unavailable response | System adapter fixture plus Education HTTP test |
| Local flag absent/false | Local/request-host fallback rejected with code 1005001003 | Education controller HTTP test |
| Local flag true, code-less local host | Configured local host may resolve | Education controller HTTP test |
| Local flag true, local host plus handle | Explicit handle takes precedence | Education controller HTTP test |
| Domain/handle agreement | Resolve one tenant | Education controller HTTP test |
| Domain/handle conflict | HTTP 200/code 1005001008 | Education controller HTTP test |
| Anonymous `/education/context` | Existing unauthorized contract | Education context HTTP test |
| Missing tenant or authenticated mismatch | Existing filter behavior remains active | Framework `TenantSecurityWebFilter` tests |
| ADMIN/MEMBER principal | ADMIN rejected; MEMBER accepted with context-derived IDs | Education context HTTP tests |
| Public field redaction | Success has only tenantId/displayName; failure has code/msg/data only | Education controller HTTP test |
| `TenantCommonApi` adapters | Required methods delegate/map; no unsupported defaults | System `TenantApiImpl` contract test |
| Abuse controls | Reused limiter/ingress attachment and structured category metric proven, or blocker recorded | Configuration/integration test where seam exists |
## Acceptance criteria
- [x] Browser headers are described as forgeable consistency evidence, not trusted identity.
- [x] Public existence disclosure and the narrower lifecycle-indistinguishability guarantee are explicit.
- [x] Public Tenant Handle is distinguished from the source Tenant Code and has exact mutability/case/format semantics.
- [x] Local behavior has one named, secure-default configuration seam and unambiguous precedence.
- [x] Canonical stored website compatibility policy is selected without claiming data migration.
- [x] Every error category has exact HTTP status, stable `CommonResult` code, message, data shape, and redaction rules.
- [x] EDU-004 has exact production/test seams and legacy `tenantName` compatibility coverage.
## Verification
Static design review only. Reviewed legacy `locator.ts`/`resolver.ts`, current Education controller/properties/error codes, `CommonResult` and global error handling, `TenantSecurityWebFilter`, `TenantCommonApi`/`TenantApiImpl`, System tenant name/website storage and tests, Member public interfaces, the completed EDU-016 ticket, tracker, and dirty working tree. No production implementation, build, database connection, Flyway execution, or migration was performed by EDU-003.

View File

@@ -0,0 +1,139 @@
# EDU-004 — Enforce tenant resolution and student identity boundaries
- **Status:** done — accepted tenant-locator and Member-principal policy implemented and verified; ingress/IP-only probing throttle remains an operational blocker
- **Type:** implementation
- **Phase:** 1
- **Blockers:** EDU-003
## Selected policy from EDU-003
- `Origin` then `Referer` supplies forgeable browser-context evidence, not trusted identity. It binds browser UX inputs but does not prevent non-browser spoofing.
- Headless clients use `tenantHandle`, the existing System tenant unique `name` under an exact constrained public contract; do not call it a Tenant Code.
- Reject/remove the legacy public `tenantName` query as a separate compatibility behavior.
- Production domain/handle agreement is checked; disagreement is a locator conflict.
- Host identity removes case, whitespace, trailing dot, IPv6 brackets, and all ports.
- Local fallback is controlled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not implicitly enable it.
- Canonical System website values for this resolver are normalized host-only strings. Scheme/path/port-bearing stored candidates are not silently normalized and require configuration correction or a separate future Flyway/data ticket.
- `/education/context` accepts only an authenticated `UserTypeEnum.MEMBER` Student Principal.
- Unknown/disabled/expired failures are identical, but successful resolution still discloses tenant existence. Reuse rate limiting/ingress controls and emit abuse-monitoring metrics.
- Login-method metadata is Member-owned; remove/deprecate Education `loginMethods` unless a minimal Member public interface is first proven necessary.
- Retain generic `TenantCommonApi`, make its lookup methods required, and add System-owned `TenantApiImpl` contract tests.
See [`EDU-003-tenant-resolution-decision.md`](EDU-003-tenant-resolution-decision.md) for rationale and exact threat model.
## Implementation result
Implemented the accepted public HTTP contracts for tenant resolution and Member-only Education context. The resolver treats `Origin`, `Referer`, `hostname`, and `tenantHandle` only as forgeable locator claims; successful responses expose only `tenantId` and `displayName`, and unknown/disabled/expired tenants share the exact unavailable envelope. Local fallback is controlled exclusively by the secure-default property, including browser-derived loopback hosts, and only configured local hosts use the Education mapping. Production domains always use the canonical System website lookup. `TenantCommonApi` lookup methods are now required and System-owned adapter tests prove exact delegation and DTO mapping. Existing `TenantSecurityWebFilter` production behavior was unchanged and is covered by focused filter-boundary regression tests.
No adequate reusable candidate-probing rate-limit attachment was found. The existing `ClientIpRateLimiterKeyResolver` includes attacker-controlled method arguments in its key, so attaching it would create per-candidate limits rather than an IP-only probing limit. No Education-only limiter was introduced; ingress/IP-only throttling and alerting remain an operational blocker. The Education module also has no direct generic Micrometer dependency seam, so structured resolver metrics remain part of the same operational blocker rather than adding an Education-only dependency or abstraction.
System currently has no separate public tenant display field: its unique `name` is both the Public Tenant Handle and the only public label available through `TenantCommonApi`. Therefore handle-based success returns that same value as `displayName`; tests now model this real exact-name adapter behavior. Suppressing that value requires a future generic System-owned display-field capability, not an Education workaround.
No database or Flyway change was made or executed.
## Student outcome
A student receives minimal login-routing data for an available tenant, while authenticated Education endpoints reject wrong tenants and non-Member principals. The public resolver does not claim that caller-supplied locator headers authenticate tenant identity.
## Scope
- browser-context domain claim selection from `Origin`, then `Referer`;
- requested-host confirmation and domain/handle conflict handling;
- explicit `tenantHandle` exact lookup and legacy `tenantName` rejection/removal;
- host-only normalization and canonical stored-website behavior;
- secure-default local-development configuration seam and precedence;
- Member/student principal enforcement for `/education/context`;
- exact public `CommonResult` contract and safe response fields;
- minimal generic `TenantCommonApi` adjustment with System-owned tests;
- reuse of public resolver rate limiting/ingress controls and structured abuse metrics where an existing seam is available.
## Exact external contract
All business outcomes use the framework convention of HTTP `200 OK` with `CommonResult`:
| Category | HTTP | `CommonResult.code` | `msg` | `data` |
|---|---:|---:|---|---|
| Malformed/missing/untrusted/locally forbidden locator | 200 | `1005001003` | `租户识别请求无效` | `null` |
| Requested-host/domain/handle conflict | 200 | `1005001008` | `租户识别信息冲突` | `null` |
| Unknown, disabled, or expired tenant | 200 | `1005001004` | `当前租户不可用` | `null` |
| Success | 200 | `0` | empty string | object containing only `tenantId`, `displayName`; current `displayName` is System tenant `name` and therefore equals a successful handle claim |
Failure messages and JSON shape never contain the rejected host/handle, lifecycle state, System exception detail, or lookup reason. Unknown, disabled, and expired paths must be byte-shape equivalent after normal serialization and have no intentional timing distinction.
## Reuse boundaries
- Reuse `TenantSecurityWebFilter`, `TenantContextHolder`, System Tenant public APIs, Member/System security context, `UserTypeEnum`, and an existing public rate-limit/ingress seam if present.
- Do not duplicate tenant tables, token logic, login methods, RBAC, or a generic rate-limiter in Education.
- Education must not depend on System internal Services, Mappers, or DOs.
- A non-Education change must be generic, minimal, backward-compatible, and tested in its owning module.
- If no reusable abuse-control seam exists, record the operational blocker; do not invent an Education-only infrastructure abstraction.
## Required TDD tests
### Education tenant controller HTTP tests
- valid production `Origin`; valid `Referer` fallback;
- syntactically valid forged header is treated only as an untrusted claim, with no authenticity assertion;
- malformed Origin and no attacker-selected fallback: exact HTTP/code/msg/data;
- Origin/requested-host mismatch: exact conflict contract;
- arbitrary production hostname without browser evidence: exact invalid contract;
- explicit case-sensitive `tenantHandle` with `^[A-Za-z0-9._-]{2,64}$` validation;
- legacy `tenantName` query rejected/removed independently;
- domain/handle agreement and conflict;
- unknown/disabled/expired exact identical wire shape;
- case, trailing dot, DNS, IPv4, bracketed IPv6, default and non-default port normalization;
- local flag absent/false rejects local/request-host fallback;
- local flag true permits code-less configured local host;
- local flag true plus handle gives the handle precedence;
- non-canonical stored website candidate does not match;
- success exposes only `tenantId` and System tenant `name` as `displayName`; for handle lookup this necessarily equals the submitted handle until System owns a separate public display field; no separate handle field, status, websites, expiry, package, private config, or `loginMethods`.
### Education context HTTP tests
- unauthenticated context rejected;
- ADMIN principal rejected;
- MEMBER principal accepted and IDs derived only from security/tenant contexts.
### System contract tests
- `TenantCommonApi` lookup methods are required, not optional unsupported defaults;
- `TenantApiImpl` exact name and canonical website lookups delegate and map DTOs;
- non-canonical website candidates are not silently normalized by the Public Tenant Resolution path.
### Framework/configuration tests
- existing missing-tenant and authenticated tenant/header mismatch filter behavior remains active;
- local-development flag defaults false and is not inferred from a Spring profile;
- reusable limiter/ingress attachment and structured success/failure-category metric are verified where an existing seam is identified.
## Acceptance criteria
- [x] Client `tenantId` and `userId` are never authoritative.
- [x] Browser headers are documented and implemented as forgeable context claims, not authentication.
- [x] Public existence disclosure is accepted; only unknown/disabled/expired status is indistinguishable.
- [x] `tenantHandle` is not mislabeled as Tenant Code; mutability, case, and format match EDU-003.
- [x] Legacy `tenantName` is rejected/removed and covered by a compatibility test.
- [x] Local fallback uses the named secure-default property and unambiguous precedence.
- [x] Canonical website compatibility policy is implemented without silent legacy normalization.
- [x] Exact HTTP/CommonResult code/message/data contracts are asserted.
- [x] Authenticated Student context enforces Member principal type.
- [x] Existing framework mismatch checks remain active and are not bypassed.
- [x] Public abuse-control gap is truthfully recorded; no inadequate or Education-only limiter was introduced.
- [x] Every non-Education modification has a necessity explanation and focused owning-module tests.
- [x] API documentation matches implementation and tests.
## Verification
Run focused Education, System, framework, and configuration tests as applicable, then:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
No database change is in scope. If investigation proves automated website data correction is required, stop that part, create a separate blocked data/Flyway ticket, and invoke `flyway-postgresql` before any schema/data work.
## Risk and rollback
- **Risk:** High security, disclosure, and login-routing impact.
- **Rollback:** Application/configuration rollback to the previous resolver adapter; keep local mode disabled by default and do not weaken authenticated tenant checks. No destructive tenant-data operation.

View File

@@ -0,0 +1,160 @@
# EDU-005 — Decide PostgreSQL/Flyway takeover strategy
- **Status:** done — forward-only takeover and EDU-006 version plan accepted
- **Type:** decision
- **Phase:** 1 / Phase 2 prerequisite
- **Blockers:** EDU-002
## Decision outcome
Education schema ownership moves exclusively to module-owned PostgreSQL Flyway migrations under `yudao-module-education/src/main/resources/db/migration/education/`. The existing root `sql/postgresql/education/` files are classified as manual bootstrap/design history, not Flyway history. The MySQL files are obsolete archival artifacts and must not remain in operational instructions.
`V4010__initialize_education_flyway.sql` and `V4020__create_native_catalog.sql` are uncommitted working-tree resources in this checkout, and the inspected local disposable PostgreSQL database has no `flyway_schema_history` table and no Education tables. This proves neither migration ran in that database, but it does not prove they never ran in another environment. To avoid assigning a second meaning to a potentially distributed version, EDU-006 must preserve both files byte-for-byte and allocate new work from `V4030`.
No migration or production schema change was executed by EDU-005.
## Evidence and classification
| Artifact | Verified state | Classification | Forward disposition |
|---|---|---|---|
| `V4010__initialize_education_flyway.sql` | Untracked module resource; contains only `SELECT 1`; packaged in current `target/classes` | Potentially distributed Flyway history; execution unverified | Freeze byte-for-byte; do not repurpose |
| `V4020__create_native_catalog.sql` | Untracked module resource; owns 11 native catalog tables; differs materially from manual `008` | Potentially distributed Flyway history; execution unverified | Freeze byte-for-byte; do not replace with manual `008` |
| `sql/postgresql/education/002``005`, `007`, `009` | Untracked manual scripts implementing the current Practice/report/wrong/favorite/unified-idempotency model in stages | Manual bootstrap/design history, not active Flyway | Consolidate the approved final state into higher Flyway versions; do not copy the obsolete intermediate tables as fresh schema |
| `sql/postgresql/education/008` | Manual native-catalog design predating/diverging from V4020 | Superseded manual design | V4020 remains the only intended Flyway owner of native catalog schema |
| `sql/postgresql/education/000``001` | Placeholder schema plus menu/tenant seed scripts | Manual bootstrap/seed history | Do not create a separate `education` schema; evaluate the still-used `education:capability` menu seed separately |
| `sql/mysql/education/**` | Historical MySQL schema, seeds, and rollback scripts | Obsolete archive | Remove from runbooks; retain only if explicitly labeled non-operational archive |
| `src/test/resources/sql/postgresql/create_tables.sql` | EDU-016 disposable test bridge; 140 PostgreSQL persistence tests passed against it | Temporary test fixture | Replace with Flyway-driven test setup after EDU-006 proves equivalent schema |
| Docker init mounts for manual Education SQL | Dirty Docker configuration mounts `000``009` directly | Obsolete delivery path | Remove Education manual mounts after Flyway takeover; the server owns migration execution |
## Approved schema owner map
### V4020 owner — unchanged
V4020 exclusively owns the native catalog tables:
- `education_region`, `education_school`, `education_major`, `education_subject`, `education_category`;
- `education_content_entry`, `education_content_node`;
- `education_question_collection`, `education_question`, `education_practice_blueprint`;
- `education_question_collection_question`.
EDU-006 does not fold Practice schema into V4020 and does not silently substitute manual `008`.
### EDU-006 new owner — Practice final state
The new Practice migration owns the final runtime shape of:
- `education_practice_session` and `education_practice_question`;
- `education_practice_report` and `education_practice_report_detail`;
- `education_wrong_question` and `education_wrong_question_idempotency`;
- `education_favorite`;
- `education_idempotency`.
The approved fresh schema does **not** create `education_answer_idempotency` or `education_submit_idempotency`. Current production services use `IdempotencyStoreMapper` and `education_idempotency`; the old DOs/Mappers are unused compatibility residue and must be removed or explicitly isolated during EDU-006.
The migration must include all columns already required by runtime code and the proven EDU-016 bridge, including `client_sequence`, `last_client_sequence`, `review_fingerprint`, protected answer snapshots, report content snapshots, JSONB fields, tenant IDs, logical-delete fields, and observed conflict/query indexes.
## Version plan for EDU-006
Version numbers are project-wide. With V4020 frozen, the next allocated version is:
1. **V4030 — Practice core-loop final schema and adoption**
- Create the final tables, columns, constraints, and indexes listed above.
- Be adoption-aware for databases that contain manually bootstrapped Practice tables.
- Validate existing column types and required uniqueness before treating existing objects as compatible; fail closed on incompatible shapes rather than silently accepting them.
- Backfill the unified idempotency table from legacy answer/submit tables when those tables exist.
- Preserve old idempotency tables during the initial adoption migration; do not make data destruction a prerequisite for application rollout.
2. **V4040 — deterministic Education capability seed, only if still approved**
- Seed menu IDs `6800`/`6801` idempotently if the existing `EducationCapabilityController` remains an exposed administrator capability.
- Keep role assignment outside the migration.
- If the capability endpoint/menu is retired before EDU-006, omit this migration rather than seeding dead UI.
3. **Later forward cleanup migration**
- Drop legacy `education_answer_idempotency` and `education_submit_idempotency` only after every adopted environment has verified backfill counts, the application no longer contains active references, and a separately reviewed forward cleanup is approved.
If repository-wide migration inventory changes before implementation, EDU-006 must re-run the version scan and use the next unused project-wide version instead of blindly taking V4030/V4040.
## Existing-environment takeover classes
`baseline-on-migrate=true` with baseline `4009` is an adoption aid, not proof that Education objects match Flyway.
1. **Empty or platform-only database, no Education tables**
- Use baseline `4009` only when the non-empty platform schema requires adoption.
- Run V4010, V4020, then V4030+ normally.
2. **Manual Practice tables exist, native catalog tables do not**
- Baseline `4009` may be used.
- V4020 creates catalog objects; V4030 validates/adopts Practice objects and performs required backfills.
3. **Manual catalog tables equivalent to V4020 already exist, no Flyway history**
- Do not run V4020 into colliding tables.
- First compare the actual schema with the frozen V4020 contract.
- For a verified equivalent environment, use a one-time environment-specific baseline at `4020`, then run V4030+. This records adoption, not execution of V4020, and must be documented per environment.
- If the schema is not equivalent, correct it through an explicit higher-version adoption path; do not falsify history or edit V4020.
4. **Flyway history already contains V4010 and/or V4020**
- Compare script/checksum/success with the frozen resources.
- Never edit an executed script. Any mismatch or failed row blocks rollout until an environment-specific repair decision is reviewed.
5. **Unknown shared environment**
- No migration rollout is authorized until its Education tables and `flyway_schema_history` are inventoried.
After all existing environments carry an explicit baseline/history record, changing `baseline-on-migrate` to `false` is a separate reviewed configuration ticket. `validate-on-migrate=true`, `clean-disabled=true`, and `out-of-order=false` remain mandatory.
## Data compatibility and backfill rules
- Copy legacy answer and submit idempotency rows into `education_idempotency` with deterministic operation values and `ON CONFLICT ... DO NOTHING` only after verifying duplicate-key/request-hash compatibility.
- Preserve original IDs only if required by references; otherwise allow identity allocation and verify semantic row counts by operation.
- Existing Practice tables must be compared with the final DO/Mapper contract, not merely checked for table-name existence.
- JSON snapshot fields use PostgreSQL `JSONB` where current runtime/test behavior expects JSONB normalization.
- Tenant-scoped uniqueness includes `tenant_id` where the business key is tenant-local. `education_practice_report` uses `(tenant_id, session_id)` as the final unique report key.
- Do not copy Supabase RLS. Framework tenant isolation remains primary; database constraints enforce integrity and idempotency.
- No destructive rollback SQL is delivered. Recovery is application rollback plus a higher-version forward correction.
## Documentation and operational corrections
EDU-006 must update operational documentation in the same slice:
- replace `yudao-module-education/README.md` MySQL apply/rollback commands with Flyway/PostgreSQL forward-only instructions;
- remove the manual Education SQL mounts from `script/docker/docker-compose.yml` so a fresh Docker database is not initialized outside Flyway before server startup;
- label `sql/postgresql/education/` and `sql/mysql/education/` as non-operational history or move them to an explicitly archival location without rewriting history;
- replace the EDU-016 temporary PostgreSQL schema bridge with Flyway-driven setup after equivalence is proven;
- state per environment whether Flyway was actually run, validated, or only packaged/compiled.
## Required EDU-006 verification
Static and build gates:
```bash
git diff --check
mvn -pl yudao-module-education -am -DskipTests clean package
find yudao-module-education/target/classes/db/migration/education -type f -print
mvn -pl yudao-server -am -DskipTests clean compile
```
Real disposable PostgreSQL gate:
1. initialize a disposable platform database or approved baseline fixture;
2. run Flyway migrate using the server's exact migration locations and PostgreSQL driver;
3. run Flyway validate;
4. inspect `flyway_schema_history` with version, script, checksum, and success;
5. inspect all approved tables, columns, constraints, indexes, and backfill counts;
6. run the EDU-016 PostgreSQL persistence suite against the migrated schema;
7. test at least the empty/platform-only path and one representative manually bootstrapped adoption path.
Only these real successful executions may be reported as migration success.
## Acceptance criteria
- [x] No published or potentially distributed migration is authorized for editing.
- [x] Every required table/index/constraint/seed has one intended Flyway owner and version range.
- [x] Manual SQL is not silently treated as executed history.
- [x] The plan includes migration packaging and real PostgreSQL execution evidence requirements.
- [x] Documentation correction scope is explicit.
## Verification performed by EDU-005
- Read project Flyway rules, local/dev Flyway configuration, server dependencies, all active migration locations, manual PostgreSQL/MySQL artifacts, core-loop DOs/Mappers, PostgreSQL test bridge, Docker initialization, Git history, and dirty-tree state.
- Inspected the reachable disposable `postgresdb` container. Target identity was database `postgres`, user `postgres`, schema `public`; it contained no `flyway_schema_history` relation and no Education tables. Container startup logs state that `/docker-entrypoint-initdb.d/*` was ignored because the volume was already initialized.
- Confirmed V4010/V4020 are currently packaged under `target/classes/db/migration/education/` from a prior build.
- Did not modify migration SQL, application code, server configuration, Docker configuration, or any database object.
- Did not run Flyway migrate/validate and does not claim a successful database migration.
## Risk and rollback
- **Risk:** High. Existing manually initialized databases may have partially overlapping or divergent table shapes, and a false baseline can hide incompatibility.
- **Rollback:** No rollback is required for this decision-only ticket. EDU-006 uses forward migrations, preserves legacy idempotency tables during first adoption, and supports application rollback without `flyway clean` or destructive down scripts.

View File

@@ -0,0 +1,84 @@
# EDU-006 — Deliver Practice schema through module-owned Flyway
- **Status:** done — V4030/V4040 delivered and verified; V4050 adds forward-only column documentation
- **Type:** database implementation
- **Phase:** 2 prerequisite
- **Blockers:** EDU-005
## Implementation result
`V4030__create_and_adopt_practice_schema.sql` now owns the final Practice session/question, report/detail, wrong-question/idempotency, favorite, and unified idempotency schema. Fresh databases do not create legacy answer/submit idempotency tables. Compatible manually initialized Practice tables receive missing final columns and JSONB alignment; legacy answer/submit idempotency rows are backfilled into `education_idempotency` while the source tables remain untouched. A conflicting request hash fails the migration transactionally.
The EDU-016 test seam now runs the real Flyway chain in a random disposable PostgreSQL schema. Its temporary `create_tables.sql` bridge was removed. The established 140 persistence tests and five migration-contract scenarios pass together.
Manual Education SQL mounts were removed from Docker Compose, and the active Education README and Pilot runbook now describe PostgreSQL/Flyway forward-only delivery. V4010/V4020 remained byte-for-byte unchanged. V4040 adds the submit-claim token and lease timestamp required by EDU-009 crash recovery; it also normalizes adopted successful submit rows without changing V4030 release history.
No shared or production database was migrated. Successful migration evidence applies only to the isolated no-volume PostgreSQL container and random schemas used by the tests.
## Scope
Implement only the objects approved in EDU-005, potentially covering:
- practice sessions and question snapshots;
- answer and submit idempotency;
- reports and report details;
- wrong questions and favorites;
- tenant-scoped unique constraints;
- indexes required by verified query paths;
- necessary menu/permission seed data;
- compatible backfills for existing development data.
Exact objects and versions are determined by EDU-005 and `flyway-postgresql`.
## Architecture rules
- Use PostgreSQL dialect only.
- Tenant-scoped uniqueness includes `tenant_id` where required.
- Database uniqueness is the final idempotency guard.
- Use `ON CONFLICT` where approved by the design.
- Do not copy Supabase auth/RLS as the application isolation model.
- Do not add destructive rollback migrations.
## Acceptance criteria
- [x] New migrations use versions allocated by `flyway-postgresql`.
- [x] V4010/V4020 remain unchanged as potentially distributed history.
- [x] Migrations are packaged under `target/classes/db/migration/education/`.
- [x] Annotated SQL and Mapper behavior match PostgreSQL constraints.
- [x] Focused repository/integration tests cover uniqueness and tenant scope.
- [x] Real PostgreSQL Flyway migrate/validate succeeds in an isolated disposable test environment.
- [x] Forward migration V4180 validates adopted Practice scalar types, required nullability, and identifier lengths without modifying V4030V4060.
- [x] Operational docs no longer instruct users to apply MySQL or manual Education SQL for these objects.
## Verification
At minimum:
```bash
git diff --check
mvn -pl yudao-module-education -am -DskipTests clean package
find yudao-module-education/target/classes/db/migration/education -type f
mvn -pl yudao-server -am -DskipTests clean compile
```
Also run the PostgreSQL commands prescribed by `flyway-postgresql` when an authorized database is available.
## Verification performed
Against an isolated PostgreSQL container bound only to `127.0.0.1`, the focused Flyway suite exercised fresh migration, baseline-4009 adoption, compatible session/question plus legacy-idempotency adoption, conflict with existing unified history, and conflicting duplicate legacy keys. Together with the existing persistence suite: 145 tests passed, 0 failures, 0 errors, 0 skipped. Every test schema was dropped and the no-volume container was stopped.
Also completed:
```text
mvn -pl yudao-module-education -am -DskipTests clean package — BUILD SUCCESS
V4010, V4020, V4030, V4040 present under target/classes/db/migration/education/ at the recorded verification point; V4050 must be included in the next verification run
mvn -pl yudao-server -am -DskipTests clean compile — BUILD SUCCESS
git diff --check — passed
```
V4010 and V4020 retained their pre-ticket SHA-256 values. V4050 was added later as a metadata-only forward migration to document protected snapshot and idempotency state columns. V4060 validates every adopted Practice unique index against its required uniqueness and ordered key columns, failing closed when a stale same-named index would weaken tenant or idempotency guarantees. The corrected Education package, PostgreSQL Flyway suite, persistence suite, and server compile are rerun after each forward migration. No shared or production PostgreSQL database was changed.
## Risk and rollback
- **Risk:** High data compatibility and deployment-order risk.
- **Rollback:** Forward correction migration plus application rollback. Never use `clean` or destructive rollback in shared environments.

View File

@@ -0,0 +1,57 @@
# EDU-007 — Verify tenant-scoped practice creation and restoration
- **Status:** done — bounded create/restore contract verified against V4030 PostgreSQL
- **Type:** implementation/verification
- **Phase:** 2
- **Blockers:** EDU-004, EDU-006
## Implementation result
The existing create/restore aggregate was retained and re-verified rather than rebuilt. Practice endpoints now require a `UserTypeEnum.MEMBER` Student Principal and continue deriving user/tenant only from the authenticated principal. Concurrent creation now uses the existing PostgreSQL `ON CONFLICT DO NOTHING` mapper seam, avoiding a query inside an aborted duplicate-key transaction. Real PostgreSQL tests prove identical concurrent requests return one session and conflicting fingerprints produce one winner plus one idempotency mismatch.
Focused tests also prove provider failure leaves no partial state and restore uses the persisted Question Snapshot after source content changes. The service create/restore suite now runs on the V4030 Flyway-owned PostgreSQL schema.
PUBLIC catalog read predicates and provider-neutral safety remain unchanged. Cross-scope PUBLIC graph-integrity semantics remain an explicit later architecture decision; EDU-007 does not claim or invent those constraints. Legacy entitlement/quota, timed practice, rich blueprint/random/review modes, and discovery of multiple active sessions are outside this bounded ticket.
## Scope
- Verify or complete session creation idempotency.
- Verify session ownership and tenant isolation.
- Restore immutable safe question snapshots.
- Preserve provider-neutral question safety from EDU-001.
- Verify PUBLIC catalog reads continue using the existing explicit scope predicates; tenant-consistent PUBLIC graph constraints remain blocked on the graph decision.
- Remove no existing core-loop behavior unless a regression proves it invalid.
## Acceptance criteria
- [x] Current user and tenant derive from a Member security principal at the controller boundary.
- [x] Duplicate client session ID with identical fingerprint returns the existing session.
- [x] Conflicting fingerprint, user, or tenant does not expose the existing session.
- [x] Underfilled, invisible, malformed, disabled, and unavailable content fails closed.
- [x] Restored content remains stable after source content changes.
- [x] Cross-tenant and wrong-user access is denied.
- [x] Provider-neutral question safety and existing PUBLIC read predicates are preserved; graph-integrity enforcement remains blocked on the recorded architecture decision.
- [x] PostgreSQL uniqueness and transaction behavior are tested against the EDU-006 schema.
## Verification
Focused create/restore service, Mapper, controller, and PostgreSQL tests; then required diff/compile gates.
## Verification performed
Against an isolated PostgreSQL database using the V4010/V4020/V4030 Flyway chain:
```text
PracticeSessionControllerHttpTest: 15 passed
PracticeSessionServiceImplTest: 25 passed
PracticeSessionServicePostgreSqlIntegrationTest: 2 passed
Total: 42 passed
Failures/errors/skipped: 0
```
The PostgreSQL concurrency tests use bounded latches and prove both identical and conflicting fingerprint races. No database migration was added or changed by EDU-007.
## Risk and rollback
- **Risk:** Medium session-ownership and compatibility risk.
- **Rollback:** Application rollback; retain forward-compatible schema.

View File

@@ -0,0 +1,53 @@
# EDU-008 — Verify idempotent answer saving
- **Status:** done — answer idempotency and rollback contract verified on PostgreSQL
- **Type:** implementation/verification
- **Phase:** 2
- **Blockers:** EDU-007
## Implementation result
The existing unified PostgreSQL idempotency claim remains the final guard. Matching keys replay only a complete, valid stored response; null, blank, malformed, or structurally incomplete replay data now fails closed without re-executing the answer mutation. Completion of a newly claimed response must update exactly one idempotency row before session/question state changes.
Answer saving remains limited to Option-backed Questions. Optionless/free-text behavior is explicitly rejected until a separate subjective-answer contract is designed. The answer HTTP seam now uses the EDU-007 Member principal and TenantContextHolder boundary and rejects ADMIN principals.
PostgreSQL tests prove same-key/same-payload replay, same-key/different-payload conflict including concurrent requests, different-key CAS serialization, stale version/sequence rejection, user/tenant/state/question ownership checks, full rollback of answer/session/claim state, refresh recovery, and no answer-key/explanation leakage. No migration was added or executed by EDU-008.
## Scope
- Same key and same canonical payload replays the original result.
- Same key and different payload returns a conflict.
- Database uniqueness is the final idempotency guard.
- Session version and client sequence prevent stale updates.
- Selected options are validated against the safe snapshot contract.
- Optionless answer behavior remains blocked until explicitly designed; do not pretend it is option-backed.
## Acceptance criteria
- [x] Same-payload duplicate semantics are explicit and tested.
- [x] Conflicting payload is rejected.
- [x] Concurrent same-key and different-key behavior is tested on PostgreSQL.
- [x] Stale session version and stale per-session command sequence are rejected.
- [x] Wrong user, tenant, session state, or question membership is rejected.
- [x] Responses and restored sessions contain no answer key or explanation.
- [x] Failure does not partially update answer, sequence, session version, or the idempotency claim.
## Verification
Focused answer service/controller/Mapper tests, PostgreSQL concurrency tests, EDU-001 regressions, and required diff/compile gates.
## Verification performed
```text
PracticeAnswerControllerHttpTest: 16 passed
PracticeAnswerServiceImplTest: 37 passed on PostgreSQL/V4030
Total focused: 53 passed
Failures/errors/skipped: 0
```
The service suite includes concurrent same-key identical and conflicting payloads plus exact different-key winner/loser assertions. Incomplete replay rows fail closed, and rollback assertions cover session version, session sequence, question state, and claim removal.
## Risk and rollback
- **Risk:** Medium-to-high concurrency and offline-retry risk.
- **Rollback:** Application rollback with schema retained; forward migration for any constraint correction.

View File

@@ -0,0 +1,69 @@
# EDU-009 — Atomic submit, immutable report, wrong questions, and favorites
- **Status:** done
- **Type:** implementation
- **Phase:** 2
- **Blockers:** EDU-008 (done)
## Student outcome
A student can retry submission safely, receive exactly one immutable report, and see consistent wrong-question and favorite projections.
## Core defect to resolve
The current submit path has evidence of check-then-insert idempotency. This ticket must define and implement an atomic initial claim with crash recovery before treating submission as complete.
## Scope
- Atomic submit-key reservation using PostgreSQL uniqueness/`ON CONFLICT` or the approved project mechanism.
- Explicit processing/completed/failed or equivalent recovery semantics.
- Same-key replay and conflicting-payload behavior.
- Single state transition from active session to submitted.
- Immutable scoring/report snapshot.
- Duplicate-safe wrong-question projection.
- Favorite behavior remains independent and tenant/user scoped.
- No external calls inside a long database transaction.
## Acceptance criteria
- [x] Concurrent same-key same-payload requests converge on one report.
- [x] Same key with different payload is rejected.
- [x] Different keys racing on one session produce at most one committed submit.
- [x] A crash after claim has a documented retry/recovery result.
- [x] Report content remains stable after question mutation.
- [x] Wrong-question projection is idempotent.
- [x] Report and history access enforce user and tenant ownership.
- [x] Pre-submit responses never expose protected answer data; post-submit response follows the approved report contract.
## Recovery contract
- The submit key is serialized with a PostgreSQL transaction-level advisory lock, then reserved with
`INSERT ... ON CONFLICT DO NOTHING` before session/report writes.
- The claim uses `PROCESSING` with a unique token and lease timestamp; report, report detail,
wrong-question projection, session CAS, and token-checked claim completion run in the same transaction.
- A normal processing failure rolls the full transaction back, including a newly inserted claim. If a previously
committed/manual `PROCESSING` claim exists (for example after legacy partial persistence), a retry can take over
the matching claim after the 120-second lease expires. A mismatched payload can never take over the claim.
- A completed claim stores `report_id` and the complete immutable response as `COMPLETED`; same-key retries
replay only a structurally complete matching response. Malformed or incomplete committed rows fail closed.
- A different key that loses the session race is completed against the immutable winner report, so retries of
either accepted key remain stable.
No external provider call occurs in the submit transaction: scoring uses the persisted session question snapshot.
V4040 adds the claim token/timestamp columns and normalizes migrated successful `SUBMIT_SESSION` rows from
legacy `ACCEPTED` to `COMPLETED` without changing V4030 release history.
## Verification
PostgreSQL concurrency tests are mandatory, along with service/controller/projection tests and required diff/compile gates.
Focused PostgreSQL verification on 2026-07-30:
- `PracticeSubmitServiceImplTest`: 38 passed.
- Submit/report controller, projection, wrong-question, and favorite suites: 129 passed total.
- Failures, errors, skipped: 0.
## Risk and rollback
- **Risk:** High state-machine and data-consistency risk.
- **Rollback:** Disable practice writes or roll back application version; repair through forward migration only.

View File

@@ -0,0 +1,169 @@
# EDU-010 — Tenant content publication and graph integrity
- **Status:** done — bounded JAVA_READ tenant Question, Placement, Content Node, Manual Collection, Category, and Practice Blueprint authoring/publication lifecycles are delivered; broader excluded workflows remain separately scoped
- **Type:** implementation program
- **Phase:** 3
- **Blockers:** EDU-004 ✓ (done), EDU-009 ✓ (done), provider-authority decision ✓ (resolved 2026-07-30, see decisions.md), PUBLIC graph-semantics decision ✓ (resolved 2026-07-30, see decisions.md)
## Tenant-admin outcome
Authorized tenant administrators can author, classify, publish, archive, and retire education content without creating cross-tenant or invalid PUBLIC/tenant relationships, and student reads remain consistent with publication state.
## Scope
- Question banks, questions, versions, classifications, catalogs, collections, blueprints, and bindings.
- Draft/published/archived lifecycle.
- System RBAC/DataPermission enforcement.
- Tenant-consistent graph constraints or equivalent transactional enforcement.
- Provider consistency between authoring source and student reads.
- Safe projections preserved from EDU-001.
## Acceptance criteria
- [x] Admin permission and data-scope matrix is explicit.
- [x] Cross-tenant graph relationships cannot be persisted in the current V4020 catalog graph.
- [x] PUBLIC and tenant-owned reference rules are enforced across the current 19 reference edges, with exact trigger-mapping and focused behavior tests.
- [x] V4080 extends the graph guard to the new Question Version → Question edge and makes the version table the twelfth ownership-protected catalog table.
- [x] Unpublished/archived content is never student-visible.
- [x] Publication is transactional and auditable.
- [x] Tenant Question Placement is permission-separated, tenant-safe, optimistic, and frozen after publication.
- [x] Database graph-integrity changes use `flyway-postgresql` and forward migrations V4070/V4090.
- [x] The next Manual Question Collection bounded contract, exclusions, pre-provisioning constraint, and readiness gates are explicit before implementation.
- [x] A current-tenant `TENANT_OWNED` Manual Question Collection can be authored only in `JAVA_READ` on an existing ACTIVE, visible Content Node.
- [x] Collection lifecycle is `DRAFT → ACTIVE → ARCHIVED` with one optimistic authoring version; ACTIVE content/membership is immutable and archive is terminal.
- [x] DRAFT membership is ordered replace-all, accepts only current-tenant `TENANT_OWNED` PUBLISHED Questions, and rejects duplicates.
- [x] Collection archive gates only collection-route discovery while direct Question visibility and historical Practice snapshots remain unchanged.
## Delivery progress — graph-integrity slice (2026-07-30)
Delivered:
- `V4070__enforce_catalog_reference_scope.sql` guards all 19 current foreign-key edges and makes ownership scope immutable on all 11 catalog tables:
- PUBLIC children may reference only PUBLIC parents;
- tenant-owned children may reference PUBLIC or same-tenant parents;
- cross-tenant and PUBLIC-to-tenant references fail closed;
- `tenant_id` and `scope` cannot change after insert, so parent mutations and concurrent ownership moves cannot invalidate existing children.
- V4070 installs write guards before historical pre-validation, eliminating the migration-time validation/write window. The reference trigger function uses a fixed `search_path`, locks referenced rows during validation, and exposes no PUBLIC execute grant.
- Real PostgreSQL/Flyway integration tests cover fresh and 4009-baselined migration histories, allowed and rejected references, parent ownership immutability, exact configuration of all 19 reference guards and 11 scope guards, function ACLs, and historical invalid-data failure.
No non-Education module changed. Application rollback can disable native authoring when it is introduced; database recovery remains a higher forward migration and must preserve existing content.
## Publication-slice constraints (resolved)
Do not add a local-only admin write path while `SCALAR_READ` remains the default authoritative provider: a successful PostgreSQL write would not be visible to students. The implemented bounded slice is a `JAVA_READ`-only tenant question `draft → publish → archive → student read` path that fails closed in unsupported provider modes.
The slice resolves the prerequisite decisions as follows:
1. the command surface is `DRAFT → PUBLISHED → ARCHIVED`; `RETIRED` remains undefined, and V4020 `HIDDEN/INACTIVE` rows map to `ARCHIVED`;
2. Education owns immutable question versions and transactional lifecycle audit;
3. a question's state controls direct visibility while entry/node/collection availability gates only route-specific discovery;
4. tenant admin action permissions are separate, tenant-wide, and tenant-bound; platform-curator/PUBLIC writes fail closed in this slice.
## Delivery progress — JAVA_READ tenant-question lifecycle slice (2026-07-30)
Delivered:
- Explicit Admin APIs and independent RBAC permissions for tenant question author, publish, and archive operations. Requests cannot choose tenant, scope, lifecycle, or actor identity:
- `POST /admin-api/education/questions/drafts``education:question:author`;
- `PUT /admin-api/education/questions/{id}/publish``education:question:publish`;
- `PUT /admin-api/education/questions/{id}/archive``education:question:archive`.
- Provider-authority guard: authoring is accepted only for `JAVA_READ`; `SCALAR_READ` fails before any Mapper access.
- V4080 changes native defaults to `DRAFT/false`, normalizes legacy visibility states, fails closed on unsafe historical published content, constrains the single direction `DRAFT → PUBLISHED → ARCHIVED`, creates immutable Question Content Versions, and creates append-only lifecycle audit facts.
- V4080 adds the Question Version → Question reference as the twentieth guarded catalog edge. In addition to the generic scope rule, a version's tenant and scope must exactly equal its question's ownership; the version table is the twelfth catalog table protected against ownership mutation.
- When the adopted platform schema contains `system_menu`, V4080 conditionally seeds the Education capability plus author/publish/archive permissions. A fixed ID already occupied by a different permission aborts migration, and no role assignment is seeded.
- Publication and archive use tenant/scope/expected-state CAS updates and append audit facts in the same transaction. Audit failure rolls back the state change.
- PostgreSQL integration-test coverage exercises the student read-after-write seam: draft is invisible, published content is returned only through the existing Safe Question projection, and archived content is invisible while stored snapshots remain independent.
- Direct question visibility is controlled by the question lifecycle. Container availability is a route-level discovery gate; Question Placement is delivered separately below, while Category, Collection, and platform-curator write paths remain outside the lifecycle slice.
Permission and data-scope matrix is recorded in `07-decisions.md`. The three lifecycle endpoints manage tenant-wide shared catalog assets within the current framework tenant; department/self DataPermission does not grant additional row access, PUBLIC authoring fails closed, and the V4080 seed intentionally grants no role.
The listed acceptance criteria are satisfied for the native catalog graph, tenant-question lifecycle, and Question Placement slices. EDU-010 remains in progress because its tenant-admin outcome also includes Category, Collection, Blueprint, Retire, and platform-curator workflows that are not part of these bounded slices.
## Delivery progress — JAVA_READ Question Placement slice (2026-07-30)
Delivered:
- `PUT /admin-api/education/questions/{id}/placement` uses the independent `education:question:classify` permission. The request contains only `nodeId` and `expectedPlacementVersion`; tenant, scope, actor, lifecycle, and ownership remain server-controlled.
- Only current-tenant `TENANT_OWNED` drafts can be placed. A target must be a visible, active, selectable PUBLIC or same-tenant Content Node. `SCALAR_READ` fails before Mapper access, and tenant endpoints cannot manage PUBLIC questions.
- Placement uses a monotonic optimistic version and tenant/scope/status/version CAS. Repeating the current placement is rejected as a conflict; two writers using the same expected version have one winner.
- Publication requires a stable available placement and includes the validated placement version in its lifecycle CAS. V4090 prevents direct PUBLIC placement, requires exact placement-version advancement, freezes placement after publication, and rejects publication without an available node.
- Student node-route reads join Question and Content Node in one PostgreSQL statement. A hidden, inactive, non-selectable, cross-scope, or deleted node cannot expose questions through that route; direct question visibility still follows the question Publication State.
- V4090 conditionally seeds `education:question:classify` at fixed ID 6805, fails closed on conflicting rows, and grants no role.
Category CRUD, Collection/Blueprint authoring, PUBLIC curator workflows, and a distinct Retire state remain outside the delivered slices. In the target domain, `education_category` is not connected to Question; classification remains Question Placement through `question.node_id`.
## Delivery progress — JAVA_READ tenant Content Node lifecycle slice (2026-07-30)
Delivered:
- Current-tenant `TENANT_OWNED` Content Nodes expose draft create/revise, activate, and archive commands only in `JAVA_READ`. PUBLIC writes and Category CRUD fail closed/outside the surface.
- The command surface is strictly `DRAFT → ACTIVE → ARCHIVED`. One `authoring_version` CAS advances on every draft revision and lifecycle transition; ACTIVE content and ARCHIVED rows are immutable.
- Entry and parent validation accepts only active, visible PUBLIC or same-tenant graph parents, requires the parent to belong to the same entry, and rejects self-parenting.
- `education:content-node:author`, `education:content-node:publish`, and `education:content-node:archive` are independent controller permissions. V4100 deliberately seeds neither permissions nor roles.
- Activation/archive append an actor/version/status audit in the same transaction. V4100 makes that audit append-only and uses a deferred constraint trigger to reject lifecycle changes without the matching audit.
- Student catalog discovery and Question Placement continue to require `is_active=true`; V4100 constrains that flag to `publication_status='ACTIVE'`, so drafts/archives cannot appear or accept placement.
## Next slice — Manual Question Collection bounded contract (accepted 2026-07-31)
This section records domain and readiness decisions only. No production Java, SQL, Flyway migration, permission seed, or runtime behavior is delivered by this documentation step.
Accepted boundary:
1. Native authority only: every collection command is available only in `JAVA_READ`; unsupported modes must fail before persistence access.
2. Tenant ownership only: tenant APIs manage current-tenant `TENANT_OWNED` collections. PUBLIC Questions, PUBLIC collection curation, and platform-curator workflows are excluded.
3. Placement boundary: a collection belongs to one existing ACTIVE, visible Content Node. Content Entry creation is excluded, so that node and its structurally available Content Entry must already be provisioned before collection authoring.
4. Manual assembly only: membership is an explicit ordered list, not a dynamic filter, Category result, Question Bank query, node-descendant query, or Practice Blueprint.
5. Lifecycle and concurrency: collections move only `DRAFT → ACTIVE → ARCHIVED`. One optimistic authoring version covers accepted draft revisions, ordered membership replacement, activation, and archive. ACTIVE collection fields and membership are immutable; ARCHIVED is terminal.
6. Membership replacement: only a DRAFT collection accepts an ordered replace-all membership command. Every member must be a current-tenant `TENANT_OWNED` PUBLISHED Question. Duplicate Question IDs are a request conflict and are not deduplicated or upserted.
7. Visibility: an ACTIVE collection may be discovered only through its available Content Node. Archive closes collection listing and collection-question discovery, but does not archive member Questions, change their direct visibility, or invalidate immutable Practice Question Snapshots already captured.
8. Access metadata: `access_rules` remains descriptive reserved metadata. This slice does not interpret it as paid, private, member, SVIP, quota, or other entitlement enforcement.
9. Explicit exclusions: dynamic filters, paid/private access, Category CRUD, Practice Blueprint authoring, Content Entry creation, PUBLIC content/curation, Retire, and legacy section/score/required membership extensions.
### Readiness and implementation gates
- [x] Current target evidence identifies `education_question_collection_question` as the sole collection-membership fact, with tenant-scoped duplicate prevention and deterministic membership order.
- [x] Legacy evidence confirms manual collections, transactional replace-all membership, server-derived counts, active-parent route gating, and an independent `DRAFT/ACTIVE/ARCHIVED` lifecycle; richer dynamic/filter and per-member scoring semantics are deliberately excluded.
- [x] Provider-neutral option safety is already resolved by EDU-001 and `QuestionContentSafety`; it is not an EDU-010 blocker.
- [x] Root Education SQL is already classified as non-operational manual/design history by EDU-005; module-owned PostgreSQL Flyway remains authoritative. Shared-environment adoption inventory remains an operational rollout gate, not an unresolved schema-owner decision.
- [x] Before implementation, allocate V4110 through `flyway-postgresql` and deliver forward adoption from the boolean collection availability fields.
- [x] Collection listing and collection-question reads validate ACTIVE collection lifecycle and Content Node availability at the route boundary.
- [x] Separate collection author/publish/archive permissions, tenant-bound commands, transactional lifecycle audit, CAS conflicts, server-derived `question_count`, and transaction boundaries are implemented.
- [x] Focused service, method-security, and real-PostgreSQL tests cover duplicate/ineligible membership, stale versions, ACTIVE immutability, terminal archive, route gating, and direct Question visibility.
No new ADR is added: this bounded slice consistently applies the existing native-authority and lifecycle decisions and does not introduce a separate hard-to-reverse architectural trade-off.
## Delivery progress — JAVA_READ Category and Practice Blueprint slice (2026-07-31)
Delivered:
- Category draft create/revise, activate, and archive commands manage only current-tenant `TENANT_OWNED` rows. Subject ownership is server-validated against active PUBLIC or same-tenant Subjects; student category discovery requires `publication_status='ACTIVE'` and `is_active=true`.
- Practice Blueprint draft create/revise, activate, and archive commands support only bounded `NODE` and `COLLECTION` modes. Exactly one target is selected by the request, while `entry_id`, effective node, tenant/scope, eligible/total counts, and lifecycle are server-controlled.
- NODE blueprints require an existing ACTIVE visible current-tenant Content Node and count current-tenant Published Questions on that node. COLLECTION blueprints require an ACTIVE current-tenant Manual Question Collection and derive counts from its maintained membership count.
- Both aggregates use `DRAFT → ACTIVE → ARCHIVED`, one monotonic `authoring_version` CAS, immutable ACTIVE content, terminal ARCHIVED state, transactional actor/version/status audit, append-only audit tables, and fail-before-mapper `JAVA_READ` authority checks.
- Separate Category and Practice Blueprint author/publish/archive permissions are conditionally seeded by V4120 without assigning any role. PUBLIC/platform-curator writes remain unavailable.
- Student blueprint lookup uses one availability query across the blueprint, Content Node, Content Entry, and optional Collection so draft, archived, or route-unavailable blueprints fail closed.
- `V4120__add_category_and_practice_blueprint_authoring.sql` is the only new migration version and was executed by focused real-PostgreSQL tests.
## Remaining exclusions after EDU-010
EDU-010 intentionally does not deliver PUBLIC/platform-curator authoring, Content Entry authoring, Question Bank authoring, dynamic/filter blueprints, mixed or descendant-node blueprint selection, type/difficulty-specific authoring semantics, paid/private entitlement enforcement, Retire/restore transitions, per-member score/required flags, legacy asset/import workflows, or administrative list/detail/delete endpoints. Category remains a separately discoverable catalog aggregate and is not a Question relationship; Question classification continues through Placement.
## Verification evidence (2026-07-31)
- `mvn -pl yudao-module-education test` with `EDU_TEST_POSTGRES_*` pointed at the local disposable PostgreSQL: **554 tests passed**, including 27 Flyway migration tests and the Category/Practice Blueprint PostgreSQL lifecycle tests. V4120 was actually executed in fresh disposable schemas.
- Focused Category/Practice Blueprint suite: **20 tests passed**, covering independent RBAC, fail-before-mapper provider guards, tenant ownership, stale CAS, student draft/active/archive visibility, target/count derivation, and append-only audit enforcement.
- `mvn -pl yudao-server -am -DskipTests clean compile`: **22 reactor modules passed**.
- `git diff --check`: passed.
- `target/classes/db/migration/education/V4120__add_category_and_practice_blueprint_authoring.sql`: present after compilation.
- Earlier lifecycle evidence remains valid: `mvn -pl yudao-module-education clean test` with the five `EDU_TEST_POSTGRES_*` variables pointed at the local Docker PostgreSQL: **502 tests passed**, including 20 Flyway migration tests and nine lifecycle/placement PostgreSQL integration tests. V4090 was actually executed in disposable PostgreSQL schemas.
- The lifecycle integration tests prove concurrent double-publish has one success and one lifecycle conflict with exactly one publish audit, cross-tenant and PUBLIC management fail closed, and `draft → publish → archive` matches student visibility.
- The real Spring Method Security contract tests prove each of author/classify/publish/archive requires its own permission and rejected calls do not reach the lifecycle service.
- `mvn -pl yudao-server -am -DskipTests clean compile`: **22 reactor modules passed**.
- `git diff --check`: passed.
- `target/classes/db/migration/education/V4080__add_question_publication_lifecycle.sql` and `V4090__add_question_placement.sql`: present after the module build.
## Risk and rollback
- **Risk:** High content-integrity and authorization risk.
- **Rollback:** Before rolling the application back behind V4090, disable native authoring by switching away from `JAVA_READ` (or disable Education when no alternate read authority is configured). Preserve V4090 data and schema, then correct forward. A V4080 application left writable in `JAVA_READ` is intentionally rejected when it attempts to publish an unplaced draft.

View File

@@ -0,0 +1,51 @@
# EDU-011 — Bounded content import assets, jobs, and export policy
- **Status:** bounded capability delivered
- **Type:** implementation program
- **Phase:** 3 / 6
- **Delivered migration:** V4130 only
## Delivered tenant-admin outcome
Education owns tenant-scoped import asset metadata and durable import jobs while reusing only public Infra APIs for private-file operations and other platform primitives. The delivered job lifecycle has exactly five states: `PREVIEW_PENDING`, `PREVIEW_READY`, `EXECUTE_PENDING`, `COMPLETED`, and `FAILED`.
The bounded capability provides:
- durable atomic claim with lease token, heartbeat, expired-lease recovery, bounded attempts, and terminal failure;
- tenant-scoped duplicate safety for preview requests and execution, so at-least-once delivery does not create a second logical job or repeat completed effects;
- fail-closed scanning: the default scanner result is `UNAVAILABLE`, and unavailable, infected, or errored scans are never executable;
- CSV/XLSX preview metadata when no parser is available; this reports file/type metadata only and does not claim row parsing or content validation;
- execution only after the scan is clean and the preview explicitly reports executable parsed content;
- an export-request redaction policy that excludes answers and private fields from authorized export requests.
## Ownership and reuse boundary
- Education owns `education_content_import_asset`, `education_content_import_job`, their business state, duplicate keys, lease/recovery semantics, preview policy, and execution orchestration.
- Infra continues to own generic file storage and platform facilities. Education integrates through public Infra APIs only; it does not depend on Infra DOs, mappers, `ServiceImpl` classes, or private implementation packages.
- V4130 is the only EDU-011 schema migration in this slice. No additional migration, generic scanner platform, generic scheduler, or Infra-internal extension is delivered.
## Deferred scope
- Production preview requests reference an admitted tenant asset ID; object keys, filenames, media types, and sizes are derived server-side from `education_content_import_asset` and are never accepted from the request body.
- The executable question-import service uses the V4130 `education_content_import_job` aggregate directly; the obsolete parallel `education_question_import_job` path has been removed.
- Per-job claims, heartbeat/finish fencing, expired lease recovery, bounded attempts, and terminal exhaustion failure use the production PostgreSQL mapper contract.
- No generated export file, downloadable export artifact, export worker, or export-job persistence is delivered. Only the request-time redaction policy is established.
- No production malware-scanner integration is delivered; the default remains fail-closed `UNAVAILABLE` until an external scanner adapter is configured.
- No full CSV/XLSX parser is promised by the fallback. Without an available parser, preview remains metadata-only and execution is blocked.
- File retention/deletion automation, dead-letter tooling, operator UI, partial-row import reporting, and legacy asset migration/re-scan remain deferred.
## Acceptance record
- [x] Education-owned import assets and durable jobs are represented by V4130.
- [x] Jobs use the five-state lifecycle `PREVIEW_PENDING`, `PREVIEW_READY`, `EXECUTE_PENDING`, `COMPLETED`, `FAILED`.
- [x] Claim, lease, heartbeat, expired-lease recovery, retry bounds, and duplicate safety are defined.
- [x] Scanning fails closed, with default `UNAVAILABLE`.
- [x] CSV/XLSX can return metadata-only preview when the parser is unavailable.
- [x] Execute is blocked unless scanning is clean and preview content is executable.
- [x] Export requests apply answer/private-field redaction policy.
- [x] Generated export files, retention automation, production parser/scanner adapters, operator UI, and legacy re-scan are explicitly outside this bounded capability; their absence is exposed through blockers and fail-closed behavior rather than represented as available.
## Risk and rollback
- **Risk:** Import processing remains security-sensitive; scanner or parser absence intentionally removes executability rather than degrading silently.
- **Rollback:** Disable import handlers while preserving Education asset/job state for inspection and forward recovery. Do not bypass scan or executable-preview gates.

View File

@@ -0,0 +1,55 @@
# EDU-012 — Classes and education relationships
- **Status:** implemented
- **Type:** bounded vertical capability
- **Phase:** 4
- **Decision:** Education owns tenant-local classes, student/teacher relationships, learning-risk rules, and follow-up tasks. System RBAC, AdminUser, departments, and data-permission policy remain authoritative; Member owns student accounts. Class roles never become System roles.
## Tenant-admin outcome
Tenant administrators manage classes, student education relationships, invitations, and supervision within explicit tenant and row-level scopes while Member/System remain the owners of generic users and roles.
## Scope
- Education class entity and membership relationships.
- Student/teacher/class domain roles without duplicating System RBAC.
- Invitations and duplicate-safe acceptance.
- Education profile extensions.
- Supervision relationships and data scopes if retained.
- Audit and operation logging.
## Delivered contract
- V4140 creates `education_class`, `education_class_member`, `education_class_invitation`, and append-only invitation audit.
- Both students and teachers are existing tenant-scoped Member users validated through `MemberUserApi`; Education stores only IDs and domain roles.
- Management permissions are independent: `education:class:create`, `education:class:query`, `education:class-member:query`, and `education:class-invitation:create`. Permissions are provisioned separately by System RBAC and are not represented in class relationships.
- All aggregate tenant IDs come from `TenantContextHolder`; tenant-qualified mapper predicates and PostgreSQL triggers reject cross-tenant relationships.
- Invitation creation is idempotent per tenant, actor, and key with request-hash conflict detection. Acceptance locks the invitation, checks invitee and expiry, and writes relationship plus audit in one transaction.
- This capability intentionally excludes account creation, password handling, platform tenant-ignore operations, and education profile duplication.
- V4270 adds supervision without changing account ownership: risk evidence is aggregated from existing Education practice, wrong-question, session, and vocabulary tables; Member and System data are API projections only.
- Classes, rules, and follow-ups carry only `dept_id`/`owner_user_id` authorization projections and are registered with RuoYi `DeptDataPermissionRule` for department/self row scope.
- Risk preview, rule authoring, idempotent task generation, and optimistic task handling have independent System permissions. `(tenant_id, batch_key, student_user_id)` is the retry-safe generation key; atomic PostgreSQL conflict-ignore and a subsequent scoped read return the same committed task to concurrent callers without recovering from a failed transaction.
## Role and permission semantics
- `STUDENT` and `TEACHER` are Education relationship labels only; they do not grant System RBAC permissions or authorize administrative endpoints.
- Tenant administrators act through explicit System permissions (`education:class:*` and `education:class-invitation:*`).
- A Member principal may only accept an invitation addressed to their own member user ID; accepting a `STUDENT` or `TEACHER` invitation creates that relationship but no additional API authority in this bounded slice.
- Future teacher actions require a separate permission matrix and endpoints; no implicit role-based elevation is implemented.
## Acceptance criteria
- [x] Generic account, password, token, tenant, and role tables are not duplicated.
- [x] Student/teacher/class permission matrix is documented and tested.
- [x] Cross-class and cross-tenant access is denied.
- [x] Invitation acceptance is idempotent and auditable.
- [x] Platform-admin tenant-ignore operations are explicit and permission guarded (none are exposed by this bounded capability).
- [x] Database changes use `flyway-postgresql`.
- [x] Risk preview and follow-up reads respect tenant plus System department/self data scope, including an unfiltered-class CTE aggregation path verified against a real `LoginUser`/`DeptDataPermissionRespDTO` PostgreSQL context.
- [x] Supervision assignees are validated by `AdminUserApi`, and students are enriched by `MemberUserApi`.
- [x] Follow-up generation and handling are duplicate-safe and stale-write-safe.
## Risk and rollback
- **Risk:** High authorization and relationship-integrity risk.
- **Rollback:** Disable management endpoints and correct relationships through audited forward operations.

View File

@@ -0,0 +1,62 @@
# EDU-013 — Education commercialization binding
- **Status:** in-progress
- **Type:** bounded implementation
- **Phase:** 5
- **Blockers:** Mall order-item paid/refunded public event, CRM referral/commission public contract
## Delivered bounded slice
Education now owns only:
- `QUESTION_COLLECTION` to Mall SPU bindings;
- tenant/member/resource entitlement aggregate;
- duplicate-safe grant, revoke, refund, and time-based expiry decisions;
- admin fulfillment endpoints and an internal public Java API;
- fail-closed collection question and new-practice access checks.
`education_question_collection.access_mode` is authoritative (`FREE`, `PRIVATE`, `PAID`). Existing `access_rules` remains descriptive metadata and is never evaluated for authorization. Financial orders, payment status, refund status, amounts, and ledgers remain outside Education.
The Product public API can validate SPUs when a Product adapter is installed, but the current reactor does not enable Mall. Current Trade public DTOs omit order items/SKU data and Pay callbacks cannot fan out, so no adapter pretends to infer fulfillment from insufficient signatures. Mall-owned automatic fulfillment remains blocked on a public paid/refunded order-item event.
## Public/admin interfaces
- `EducationEntitlementApi`: idempotent grant/revoke/refund and access check for trusted module adapters.
- `POST /admin-api/education/commercialization/bindings`
- `PUT /admin-api/education/commercialization/bindings/{resourceType}/{resourceId}/deactivate`
- `POST /admin-api/education/commercialization/entitlement-events`
Permissions: `education:commercialization:binding`, `education:commercialization:entitlement`.
## Outcome
Education products and access rights are connected to Mall, Pay, Member, and CRM without creating a parallel product, order, payment, refund, membership, or financial ledger in Education.
## Education ownership
Education may own only domain bindings and fulfillment orchestration, such as:
- education product to course/exam/content binding;
- entitlement scope and education-resource association;
- duplicate-safe fulfillment event state where no platform facility exists.
## Deferred automatic integrations
Automatic fulfillment from Mall paid/refunded order-item events and CRM referral/commission processing remains blocked because those public events/contracts are not available in the current reactor. The delivered admin endpoint and `EducationEntitlementApi` are trusted ingestion seams; they do not imply that Pay callbacks or Mall orders currently fan out automatically.
## Acceptance criteria
- [x] Mall/Pay/Member/CRM public contracts are mapped before implementation.
- [x] Payment callbacks and refunds remain in Pay; no automatic fan-out is claimed.
- [x] Generic products/orders remain in Mall where applicable.
- [x] Entitlement issuance, revocation, expiry, and refund effects are explicit and idempotent through the trusted ingestion seams.
- [x] Paid/private practice remains inaccessible until entitlement checks are complete.
- [x] Reconciliation and commission/referral ownership is explicit; automatic CRM integration remains deferred.
- [x] Financial and authorization tests cover duplicate trusted events and cross-tenant access.
- [x] Automatic Mall paid/refunded order-item fulfillment is explicitly unsupported until a Mall-owned public event exists; paid access remains fail-closed.
- [x] Automatic CRM referral/commission processing is explicitly unsupported until a CRM-owned public contract exists; no financial or referral behavior is simulated in Education.
## Risk and rollback
- **Risk:** Very high financial and access-control risk.
- **Rollback:** Disable fulfillment handlers and paid access; preserve financial ledgers in their owning modules.

View File

@@ -0,0 +1,55 @@
# EDU-014 — Extended student and secondary learning waves
- **Status:** partial implementation; bounded representative wave delivered
- **Type:** family disposition plus executable vertical slices
- **Phase:** 5
- **Migration:** `V4160__add_bounded_learning_wave.sql`
## Delivered bounded wave
| Family | Target owner / reused public capability | Data and API disposition | Priority | Verification disposition |
|---|---|---|---|---|
| Auth compatibility and phone/OAuth binding | Member + System auth APIs | **Replaced/reused.** Education does not copy credentials, sessions, phone binding, or OAuth state. Existing Member login remains the student entry point. Legacy auth data requires a separate identity migration, outside V4160. | Reuse now | Existing auth tests; no EDU-014 schema |
| Profile and education profile extensions | Member owns generic profile; Education owns learning projections | **Partially migrated.** Learning summary is Education-owned and exposes aggregate counts only. New generic profile fields are deferred. | P1 bounded | Summary service/API tests and tenant isolation |
| Vocabulary learning/review | Education | **Migrated for new writes.** Tenant/student/key progress and deterministic review scheduling are delivered. Legacy vocabulary history is retained at source pending an explicit import mapping; no silent import. | P1 | State, due-review, validation, tenant isolation |
| Scoreline and admissions content | Education catalog, when selected | **Deferred.** No safe authoritative dataset or freshness contract is established. Existing legacy data is retained read-only; no endpoint compatibility is claimed. | P2 | Retirement/defer compatibility contract only |
| Video entitlement and progress | Entitlement owner unresolved; media delivery outside Education | **Explicitly retired from this wave.** No video endpoint, token, progress write, or metadata-based access bypass is added. Legacy video/progress data is retained until an entitlement-led child slice defines import and deletion policy. | P3 blocked | Capability manifest must expose no video interface |
| Recommendation and AI generation | AI public services plus Education authorization | **Explicitly deferred.** No student profile or learning history is sent to AI, and no AI recommendation endpoint is exposed. Legacy recommendation data remains retained but non-authoritative. | P3 blocked | Capability manifest must expose no AI interface |
| Notifications and reminders | Education schedule + System `NotifyMessageSendApi` | **Migrated for exam reminders.** Education owns claim/retry state; System owns message rendering/storage. A stable `education_exam_reminder` template is conditionally seeded. | P1 | Due claim, retry, tenant context, notify-port tests |
| Points, badges, check-ins, feedback, exam countdowns | Education orchestration + Member `MemberPointApi`; feedback/reminders/badge rules Education; System Notify | **Partially migrated.** Fixed server-side learning awards, configurable tenant badge definitions, automatic practice/vocabulary/feedback rules, lifetime-once manual grants, tenant-admin handling/audit, bounded resolved-feedback rewards, and exam reminder/countdown data are delivered. Generic check-in/task exchange and badge triggers that depend on not-yet-migrated check-in/mock-exam/activity domains remain deferred. | P1 bounded | Duplicate award/grant, invalid rule, optimistic conflict, notify failure, feedback ownership |
| Learning analytics, leaderboard, trends, reports | Education projections over immutable reports/vocabulary/awards | **Partially migrated.** Own summary and bounded tenant leaderboard are delivered. Leaderboard is anonymized and contains no user IDs or report details. Trend/export surfaces are deferred. | P1 bounded | Tenant isolation, deterministic ranking, redaction |
## Executable APIs
Student identity and tenant are always derived from the authenticated context; no API accepts `userId` or `tenantId`.
- Vocabulary progress/review: `/app-api/education/vocabulary/*`
- Exam reminder create/list/cancel: `/app-api/education/exam-reminders`
- Student feedback: `/app-api/education/learning/feedback`
- Learning award orchestration: `/app-api/education/learning/awards`
- Own learning summary: `/app-api/education/learning/summary`
- Anonymous tenant leaderboard: `/app-api/education/learning/leaderboard`
- Tenant-admin learning operations: `/admin-api/education/learning-operations/*`
- Student badge projection: `/app-api/education/engagement/badges`
- Tenant-admin badge definitions and grants: `/admin-api/education/badge/*`
- Scheduled dispatch bean: `examReminderSendJob` using System Notify public API
## Security and reversibility
- All Education state is tenant-owned and uses explicit tenant/user predicates in addition to framework interception.
- Client requests cannot choose point values, badge codes, notify templates, delivery users, or leaderboard tenant.
- Exam reminder delivery uses a token-fenced, expiring database claim. An interrupted `SENDING` row is reclaimed after lease expiry, attempts are bounded, and exhausted claims become observable `FAILED` rows through V4200.
- Award rows and reminder rows are durable retry authorities; cross-module tables are never written directly by Education application code.
- Education point calls use stable Member ledger business keys backed by V4250 uniqueness. Feedback rewards require `RESOLVED` state, a separate reward permission, and a bounded server-validated value.
- Badge definitions and automatic rules are tenant-owned. Badge grants reuse the Education award ledger, validate manual targets through Member, project administrators through System, and use a partial unique key for lifetime-once delivery. A failed System notification is recorded as `FAILED` without undoing the grant.
- Automatic badge rules can subscribe only to implemented Education events (`PRACTICE_SUBMIT`, `VOCABULARY_REVIEW`, `FEEDBACK_RESOLVED`); absent legacy domains are not represented as fake triggers.
- Leaderboard output uses deterministic tenant-local aliases and aggregate score only. No phone, profile, member ID, answer, explanation, feedback content, or report detail is exported.
- Rollback disables/removes executable application paths while retaining V4160 data. Destructive rollback is not provided; later correction uses a higher forward migration.
- Video and AI remain fail-closed: this wave exposes no executable interface and establishes no entitlement through access metadata.
## Acceptance evidence required before production rollout
- Focused unit/controller tests for each delivered family.
- Real PostgreSQL Flyway migration and tenant-isolation tests.
- Compile and migration packaging verification.
- Operational creation of the Quartz schedule for `examReminderSendJob`; V4160 provides state/template, not an environment-specific cron row.

View File

@@ -0,0 +1,53 @@
# EDU-015 — Operational independence and legacy exit
- **Status:** implemented-contracts
- **Type:** integration and deployment program
- **Phase:** 6
- **Blockers:** production deployment evidence and implementation of each future EDU-011 worker/scanner workload
## Outcome
The target backend runs its selected education capabilities without depending on the old NestJS API, worker, Supabase auth/storage, or asset-scanner deployment, except for explicitly time-bounded adapters with owners and exit dates.
## Scope
- Replace selected Worker jobs with Infra Job/MQ and owning-domain handlers.
- Complete retries, dead letters, audit, notifications, and observability.
- Complete file/scanner deployment or approved alternative.
- Remove or disable temporary Scalar/legacy adapters according to provider strategy.
- Reconcile migrated data and operational runbooks.
- Prove deployment, startup, Flyway, and core user flows.
## Delivered contracts
- `/admin-api/education/operations/health` exposes the runtime dependency registry plus durable Worker/Scanner and open dead-letter summaries.
- `JAVA_READ` performs a target-only PostgreSQL schema readiness proof; Scalar is explicitly `NOT_SELECTED` in that mode.
- `SCALAR_READ` is marked legacy, required only when selected, uses `FAIL_CLOSED`, and records success/failure telemetry without exposing URL or credentials.
- V4170 owns durable `education_operational_component` and tenant-scoped `education_dead_letter` contracts without retaining business payloads.
- `EducationTenantContextPropagation` re-establishes and restores tenant context for executor tasks.
- `tools/education-target-smoke/java-read-readiness.sh` asserts target-only provider selection against a running target deployment.
Production migration and deployment evidence remain release activities and are not claimed by this change.
## Acceptance criteria
- [x] Every configured temporary legacy dependency exposes owner, telemetry, failure policy, and exit date; invalid selected Scalar configuration reports DOWN.
- [x] Implemented at-least-once import and reminder consumers use duplicate-safe keys or token-fenced leases with bounded recovery.
- [x] Job retries/dead letters and scanner/component health have durable persistence and an admin health projection; undeployed components are not fabricated as UP.
- [x] Module-owned migrations V4010V4200 execute and validate in isolated disposable PostgreSQL test schemas; shared Pilot/production execution remains a release-evidence activity.
- [x] The fixture-based Student harness passes target API contract/browser flows; a real deployed Student Web/H5 application remains outside this repository and is explicitly not claimed.
- [x] Runbooks contain no active MySQL/manual-SQL or obsolete NestJS startup requirement.
- [x] Rollback and incident procedures are documented in the Pilot acceptance runbook.
## Verification
- Application startup and health.
- Flyway history and migration execution.
- Worker/job deployment smoke tests.
- Playwright student harness and selected admin flows.
- Logs, metrics, traces, retry/dead-letter, and scanner health checks.
## Risk and rollback
- **Risk:** High deployment and production reliability risk.
- **Rollback:** Per capability using application/configuration rollback while preserving forward database history.

View File

@@ -0,0 +1,64 @@
# EDU-016 — Move PostgreSQL-specific Education persistence tests off H2
- **Status:** done — focused PostgreSQL persistence suite passes against the reachable `postgresdb` seam
- **Type:** test infrastructure / PostgreSQL integration
- **Phase:** 0 / database prerequisite
- **Blockers:** EDU-002, EDU-005 for final schema ownership
## Confirmed defect
Education unit tests use H2 with `MODE=MYSQL`, while production Mapper SQL intentionally uses PostgreSQL `ON CONFLICT`. H2 rejects the annotated SQL for favorites, wrong-question idempotency, unified answer/submit idempotency, and review-session conflict handling. Switching H2 to PostgreSQL mode does not solve this: H2 still rejects `ON CONFLICT` and also exposes Boolean/integer compatibility differences.
This prevents the full Practice/Wrong/Favorite regression suite from exercising production persistence semantics.
## Outcome
PostgreSQL-specific persistence behavior runs against real ephemeral PostgreSQL in the test suite, while fast database-independent tests may remain on H2 where their SQL is portable.
## Scope
- Select the repository-standard PostgreSQL integration-test mechanism, preferably Testcontainers or an existing project fixture.
- Move tests that execute `ON CONFLICT`, PostgreSQL JSON/JSONB, identity, Boolean, or concurrency semantics onto PostgreSQL.
- Keep controller and pure domain tests database-independent.
- Align test schema with module-owned Flyway after EDU-005/EDU-006; avoid maintaining a divergent hand-written full schema long term.
- Cover at least:
- `IdempotencyStoreMapper.insertIgnore`;
- wrong-question idempotency insert;
- favorite upsert;
- review-session insert-ignore;
- concurrent answer and submit claims.
## Acceptance criteria
- [x] No test relies on H2 to validate PostgreSQL `ON CONFLICT` semantics.
- [x] PostgreSQL tests execute the same Mapper SQL as production.
- [x] Test database is isolated and disposable.
- [x] Schema setup uses Flyway or an explicitly temporary bridge with an exit ticket.
- [x] Concurrency tests are stable and prove unique-constraint behavior.
- [x] CI prerequisites and local commands are documented.
## Verification
Completed against the existing reachable `postgresdb` service with credentials supplied only through `EDU_TEST_POSTGRES_*` environment variables:
```bash
mvn -pl yudao-module-education \
-Dtest=PracticeSessionMapperTest,FavoriteServiceImplTest,PracticeAnswerServiceImplTest,PracticeSubmitServiceImplTest,PracticeSubmitProjectionIntegrationTest,WrongQuestionServiceImplTest test
```
Result: 140 tests passed, 0 failures, 0 errors. The same 140-test suite also passed with JUnit class parallelism explicitly enabled, proving the shared schema resource lock prevents class-level collisions. The suite executes production Mapper SQL on PostgreSQL, including `ON CONFLICT` and JSONB behavior. A JUnit resource lock serializes PostgreSQL test classes that share one random JVM-scoped schema; each class closes its Spring context before the inherited lifecycle drops and recreates that schema, preventing cached contexts from reusing a dropped schema.
The temporary schema bridge was removed by EDU-006. PostgreSQL persistence tests now create their disposable random schema through the module-owned Flyway chain (`V4010`, `V4020`, `V4030`) and retain only `clean.sql` for per-test data isolation.
Local/CI prerequisites:
- a reachable disposable PostgreSQL database;
- PostgreSQL JDBC connectivity from the Maven process;
- non-blank `EDU_TEST_POSTGRES_HOST`, `EDU_TEST_POSTGRES_PORT`, `EDU_TEST_POSTGRES_DB`, `EDU_TEST_POSTGRES_USER`, and `EDU_TEST_POSTGRES_PASSWORD` values.
No Testcontainers or other external dependency was added. No PostgreSQL Flyway migration was executed or authorized by this ticket.
## Risk and rollback
- **Risk:** Medium CI/runtime cost; high value for persistence confidence.
- **Rollback:** Keep prior fast tests temporarily, but do not restore false H2 coverage claims for PostgreSQL SQL.

View File

@@ -0,0 +1,38 @@
# EDU-017 — Tenant appearance, public settings, and theme lifecycle
- **Status:** done — bounded appearance/theme slice implemented and verified
- **Type:** tenant administration / public presentation configuration
- **Phase:** 4 / tenant operations
- **Blockers:** EDU-003, System Tenant authority, Vben admin foundation
## Decision
System Tenant remains authoritative for tenant identity, display name, lifecycle, and bound websites. Education owns only presentation-specific configuration: branding extensions, student/admin feature flags, public runtime configuration, and draft/published theme state. The public tenant locator response remains the minimal EDU-003 contract; appearance is exposed separately under the normal request tenant context.
Platform theme templates are global trusted configuration and are explicitly excluded from MyBatis tenant injection. Tenant appearance rows extend `TenantBaseDO`, retain one live row per tenant, and use optimistic versions for every update. RuoYi System RBAC, tenant validation, admin projection, API access/operation logging, and the normal `tenant-id` security filter remain authoritative.
## Delivered contract
- Admin read, branding write, settings write, theme preview, and theme publish endpoints with independent permissions.
- `GET /education/tenant-appearance/public` is anonymous but not tenant-ignored; it requires the normal validated tenant header/context and returns no admin flags or draft data.
- System tenant name is the lazy-row default and fallback; no duplicate Education tenant identity or domain authority is created.
- Three legacy-compatible templates are seeded: `classic`, `focus`, and `high-contrast`.
- Theme preview is a shallow template/override merge. Publication revalidates the persisted draft and clears it atomically.
- Recursively rejects secret/password/token/private-key/API-key-like public keys except `secretRef`; theme tokens, CSS variables, icons, assets, colors, radii, modes, and density are allowlisted and unsafe renderable strings fail closed.
- Vben page `education/tenant-appearance/index` manages branding, JSON settings, templates, draft preview, and explicit publication with client-side preflight checks.
## Verification
- Policy unit tests cover recursive secret rejection, closed theme/asset key sets, colors/radii/CSS, URLs, and shallow merge.
- Service unit tests cover System-name fallback, optimistic conflict, sanitized draft persistence, and public projection.
- Method-security contract tests prove query, branding, settings, and theme permissions are independent.
- Real PostgreSQL service tests prove lazy defaults, shared templates, settings, draft/publish, public projection, stale-version rejection, and cross-tenant isolation through the production MyBatis interceptor.
- Flyway tests prove the V4290 schema, exact template seeds, menu shape, unique tenant row, and draft/publish database state.
## Explicitly open
- Domains stay in System Tenant `websites`; no Education domain CRUD authority is planned.
- Payment-account configuration must compose Pay rather than copy the legacy table.
- Authentication-provider configuration must reuse System/Member authentication seams.
- Tenant secret storage/rotation needs a dedicated encrypted private-storage decision and must never be added to the public appearance table.
- Activation codes and coupons need a Mall Promotion/Member entitlement ownership and idempotent redemption decision.

View File

@@ -0,0 +1,33 @@
# EDU-018 — Reuse native payment and social-provider administration
- **Status:** done — bounded native-administration reuse implemented and verified
- **Type:** tenant administration / module reuse
- **Phase:** 4 / tenant operations
- **Blockers:** EDU-017, Pay module, System social-client module, Vben admin foundation
## Decision
Legacy `tenant_payment_accounts` and supported OAuth-provider administration must not become Education-owned shadow tables. Payment applications/channels remain authoritative in Pay; tenant third-party login clients remain authoritative in System. V4300 places their existing Vben pages under the Education menu and grants only their original granular permissions.
The duplicated menu locations use unique route names, so roles may receive Education-scoped navigation without changing the original Pay/System routes. All requests still reach the native controllers and services. No credential is copied into Education and no compatibility facade invents a second status model.
## Delivered contract
- Education menu entry for native `pay/app/index` with Pay App and Pay Channel query/create/update/delete permissions.
- Education menu entry for native `system/social/client/index.vue` with Social Client query/create/update/delete permissions.
- Fail-closed Flyway menu-shape validation and collision detection.
- Existing Pay/System Vben forms, controllers, tenant interception, and validation are reused. EDU-020/V4320 subsequently activates the Pay runtime and supplies the missing tenant-scoped App/Channel PostgreSQL contract.
## Verification
- Real PostgreSQL Flyway test verifies both route components, unique route names, all twelve native permissions, and V4300 history.
- A conflicting pre-existing menu ID causes V4300 to fail rather than silently binding the wrong permission.
- The existing Vben production build already compiles both reused native pages; V4300 adds no frontend source or dependency.
## Explicitly open
- EDU-021 now maps bounded `tenant_collect` WeChat/Alipay accounts into native Pay with a redacted audit. Platform/service-provider modes, XPay/Xunhu replacement, and production bulk export/runbook work remain open.
- `system_sms_channel` is global and `@TenantIgnore`; it is not legacy tenant-level auth-provider equivalence.
- Aliyun PNVS has no proven native target provider and remains a separate auth/SMS slice.
- Generic tenant secret storage/rotation remains separate. Native Pay/System credentials stay owned by those modules.
- Activation codes and coupons remain a Mall Promotion/Member entitlement and idempotent-redemption slice.

View File

@@ -0,0 +1,34 @@
# EDU-019 — Secure learning activation codes
## Status
Done for the bounded V4310 contract. Legacy activation-code import and coupons remain separate.
## Decision
Activation codes are Education learning-access credentials, not Mall Promotion coupons. They reference a Mall-owned SPU through the existing tenant-scoped `education_resource_product_binding`, authenticate redemption with the existing Member principal, and grant access through the existing idempotent `EducationEntitlementService` event pipeline. No product, member, coupon, or second entitlement ledger is introduced.
## Delivered contract
- Tenant-owned activation-code batches and codes with database constraints, tenant-composite foreign keys, optimistic versions, and separate query/manage/generate permissions.
- Admin batch page/create/update/generate and masked code page/disable endpoints.
- Member-only app check/redeem endpoints; administrator and anonymous principals fail closed.
- Cryptographically random codes normalized for redemption. Plaintext is returned only by the successful generation response; persistence stores SHA-256 digest and a mask.
- `SELECT ... FOR UPDATE` serializes redemption. A successful grant writes the existing entitlement event/aggregate, marks the code redeemed, and increments the batch count in one transaction.
- Same-member retry returns the existing entitlement idempotently; another member receives an already-used conflict.
- Batch SPU, duration, and prefix become immutable after generation. Disabled batches, codes, or resource bindings cannot be redeemed. `durationDays=0` intentionally means no expiry.
- Vben page `education/activation-code/index` provides batch and masked-code tables, create/edit/generate flows, copy/download, unsaved-plaintext dismissal warning, and disable confirmation. Closing the generation modal clears plaintext.
## Verification
- PostgreSQL Flyway verifies both tables, constraints, cross-tenant batch references, four menu rows, and migration history through V4310.
- Controller contracts verify independent admin permissions and Member-only app access.
- Unit tests verify digest/mask persistence, entitlement composition, replay/conflict, target validation, and generated-batch immutability.
- Real PostgreSQL service tests verify digest-only persistence, tenant isolation, entitlement/event creation, same/different-member behavior, disabled dependencies, and one winner under concurrent redemption.
- Vben formatting, lint, Vue typecheck, and production build cover the fifteenth custom Education page.
## Explicit non-goals
- Importing or preserving plaintext from legacy activation-code rows.
- Coupon templates, claims, discounts, stacking, or redemption; those remain Mall Promotion-owned.
- Payment/order/refund fulfillment, legacy payment mode/provider mapping, tenant PNVS, or generic encrypted secret rotation.

View File

@@ -0,0 +1,36 @@
# EDU-020 — Activate tenant-scoped native Pay administration
- **Status:** done — bounded Pay App/Channel runtime activation implemented and verified
- **Type:** platform reuse / tenant security / database takeover
- **Phase:** 4 / tenant operations
- **Blockers:** EDU-018, native Pay module, PostgreSQL Flyway
## Problem
V4300 deliberately reused the native Pay App/Channel controllers, permissions, and Vben page, but the repository reactor and `yudao-server` still excluded `yudao-module-pay`, and the active PostgreSQL baseline had no `pay_app` or `pay_channel` tables. The menu was therefore only navigational evidence, not an operational payment-configuration backend.
Stock/global Pay tables are also unsafe to adopt silently in a multi-tenant education deployment. A channel supplied with an arbitrary `appId` must not bind to an application outside the current tenant.
## Delivered contract
- `yudao-module-pay` is included in the reactor and server runtime.
- V4320 creates Pay-owned `pay_app` and `pay_channel` tables with tenant columns, logical-delete audit fields, active-row uniqueness, status checks, and tenant-first indexes.
- Existing Pay tables without `tenant_id` fail migration with an explicit mapping error; V4320 never assigns legacy credentials to tenant `0` or another guessed tenant.
- `PayAppDO` explicitly extends `TenantBaseDO`, so framework MyBatis tenant interception is a declared contract rather than an implicit table convention.
- Channel create/update verifies that the referenced application is visible to the current tenant before persisting the channel.
- `/pay/app/list` now uses the actual `pay:app:query` permission already granted by V4300 instead of the obsolete `pay:merchant:query` permission.
- The existing `pay/app/index` Vben page remains authoritative; no Education payment form or credential table is duplicated.
## Verification
- 17 `PayChannelServiceTest` checks pass, including missing/cross-tenant-parent rejection seams.
- Two Pay tenant/permission contract checks pass.
- All 45 current PostgreSQL Flyway tests pass through V4380, including same `app_key` across tenants, duplicate rejection within a tenant, channel uniqueness, V4320 history, and fail-closed adoption of a global `pay_app` table.
- `mvn -pl yudao-server -am -DskipTests compile` includes and compiles the native Pay module.
## Explicitly open
- EDU-021 now provides an explicit, audited single-account import for `tenant_collect` WeChat/Alipay manifests. Platform/service-provider modes, unsupported providers, and production bulk export/runbook work remain open.
- EDU-022 now activates tenant-scoped native Pay order, refund, and notification persistence/UI; EDU-023 adds bounded terminal legacy transaction import; EDU-024 activates native Transfer/Wallet persistence and UI without inventing opening balances.
- Payment credentials remain Pay-owned. Generic tenant secret encryption/rotation and PNVS remain separate slices.
- EDU-025 delivers native Mall Product activation. Explicit legacy product import, Promotion/Trade, coupon redemption, purchase fulfillment, refund-to-entitlement revocation, and reconciliation remain separate commercialization slices.

View File

@@ -0,0 +1,51 @@
# EDU-021 — Import legacy tenant payment accounts into native Pay
- **Status:** done — bounded single-account import, audit, and native Pay UI entry implemented and verified
- **Type:** legacy data bridge / payment security / tenant isolation
- **Phase:** 4 / tenant operations
- **Blockers:** EDU-020, native Pay App/Channel runtime, PostgreSQL Flyway
## Problem
The legacy `tenant_payment_accounts` and `app_private.tenant_secrets` records cannot be copied directly into native Pay. Provider aliases, collection modes, channel variants, callback ownership, credential shapes, and status values are not one-to-one. Guessing any of them can route money or callbacks to the wrong party.
The migration also needs durable evidence without creating an Education payment shadow model or persisting a second plaintext credential copy.
## Delivered contract
- The bridge is Pay-owned and creates native `pay_app` and `pay_channel` rows through `PayAppService` and `PayChannelService`; Education owns neither a payment account nor a credential table.
- `POST /pay/legacy-account-import/import` imports exactly one explicitly reviewed manifest. It requires both `pay:app:create` and `pay:channel:create`.
- `GET /pay/legacy-account-import/page` exposes tenant-filtered audit history and requires both Pay App and Channel query permissions.
- The existing `pay/app/index` Vben page adds a **迁移旧支付账号** action and JSON manifest modal. The button uses explicit AND permission visibility, matching the controller.
- Import request-body logging is disabled for database access logs, non-production request logs, and unexpected-error logs so the manifest does not become a plaintext logging side channel.
- Only `tenant_collect` is accepted. `platform_collect` and `service_provider` fail closed because their settlement and merchant ownership semantics are not equivalent.
- Historical WeChat and Alipay aliases normalize to `wechat_pay` or `alipay`. Non-equivalent providers such as XPay/Xunhu fail closed.
- Operators must explicitly select a native channel such as `wx_lite`, `wx_pub`, or an Alipay variant. Provider family and channel family must match; the importer never guesses a WeChat client type.
- WeChat V3 and Alipay public-key configurations map into native Pay configuration objects. Multiple rotating WeChat platform keys require an explicit choice. Alipay accepts only the native production or sandbox official gateway.
- Old provider callbacks are not reused. The manifest must provide new business order/refund callbacks and may provide a transfer callback.
- `active` maps to enabled. `disabled` and `pending` map to disabled with an audit note.
- Within the current target tenant, `sourceAccountId` is the idempotency key. A replay with the same source SHA-256 returns the existing mapping; a different checksum is rejected rather than overwriting it.
## Audit and database contract
V4330 creates tenant-scoped `pay_legacy_account_import` with source identifiers/checksum, normalized provider/config digest, target App/Channel IDs, mapping notes, operator, and timestamp. It deliberately has no `config_public`, `secret_json`, raw config, or secret-value column.
Composite foreign keys `(tenant_id,target_app_id)` and `(tenant_id,target_channel_id)` prevent an audit row from pointing across tenants. A target tenant may import the same legacy UUID independently, while duplicate active source IDs inside one tenant are rejected. An existing global audit table without `tenant_id` causes migration failure and requires explicit disposition.
Credentials still enter native `pay_channel.config` using Pay's existing configuration storage. EDU-021 prevents an extra audit copy; it does not introduce generic encryption or key rotation for native Pay credentials.
## Verification
- Nine focused importer tests pass for WeChat/Alipay mapping, aliases, disabled-state mapping, replay, checksum conflict, unsupported modes/providers, channel mismatch, unsafe endpoints/rotating keys, and audit redaction. A controller contract verifies dual write permission and request-body logging suppression.
- The combined Pay selection passes 29 tests, including the prior tenant App/Channel contracts.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4330 coverage verifies tenant-independent legacy UUID reuse, no raw credential columns, cross-tenant composite-FK rejection, mode constraints, migration history, and fail-closed global-table adoption.
- The Vben `@vben/web-antd` typecheck passes with the import API, modal, and explicit dual-permission button.
## Explicitly open
- Audit history currently has a backend/Vben API contract but no dedicated history table in the account-import modal. EDU-023 provides its own recent transaction-import history table on the native order page.
- A controlled export job from the legacy database and operator runbook are still required before production bulk migration. The UI template contains placeholders and must never be submitted unchanged.
- Concurrent first imports of the same source account rely on the database unique constraint and transaction rollback; a friendly concurrent-replay response is not claimed.
- Platform/service-provider settlement, XPay/Xunhu replacement, generic credential encryption/rotation, and tenant PNVS require separate decisions.
- EDU-022 activates tenant-scoped native Pay order, refund, and notification ledgers; EDU-023 adds bounded terminal legacy transaction import; EDU-024 activates empty native Transfer/Wallet ledgers. Production bulk tooling and reviewed opening-balance migration remain open.
- EDU-025 delivers native Mall Product activation. Explicit legacy product import, Promotion/Trade, coupon import/redemption, purchase fulfillment, refunds-to-entitlement revocation, and reconciliation remain separate commercialization work.

View File

@@ -0,0 +1,45 @@
# EDU-022 — Activate tenant-scoped native Pay transactions
- **Status:** done — bounded order/refund/notification takeover and native administration UI verified
- **Type:** platform reuse / financial isolation / database takeover
- **Phase:** 4 / commercialization foundation
- **Blockers:** EDU-020, EDU-021, native Pay runtime, PostgreSQL Flyway
## Problem
EDU-020 made native Pay application/channel configuration operational, but the active PostgreSQL baseline still had no order, order-extension, refund, notification-task, or notification-log tables. The existing Pay controllers and Vben pages therefore could not administer real transaction state.
The stock transaction data objects were also inconsistent: notification tasks were tenant-aware, while orders, order extensions, refunds, and notification logs inherited only `BaseDO`. Silently creating global financial tables or assigning existing rows to a guessed tenant would make callbacks, exports, and background retries cross tenant boundaries.
## Delivered contract
- V4340 creates Pay-owned `pay_order`, `pay_order_extension`, `pay_refund`, `pay_notify_task`, and `pay_notify_log` tables for PostgreSQL. Education does not create a parallel order, refund, or webhook ledger.
- `PayOrderDO`, `PayOrderExtensionDO`, `PayRefundDO`, `PayNotifyTaskDO`, and `PayNotifyLogDO` all inherit `TenantBaseDO`, so normal MyBatis tenant interception scopes native admin queries and mutations.
- Composite tenant foreign keys bind orders to Pay applications/channels, extensions to orders/channels, refunds to their application/channel/order, and logs to their notification task. Cross-tenant references fail in PostgreSQL even if application code is bypassed.
- Active merchant order/refund identifiers are unique inside a tenant application but may be reused by another tenant. Native Pay numbers and extension numbers are tenant-scoped.
- One active notification task is allowed for each `(tenant_id,type,data_id)`. This closes duplicate terminal-callback races; a deliberately soft-deleted task may be recreated.
- Existing transaction tables without `tenant_id` fail V4340. No global financial row is assigned to tenant `0` or inferred from an application ID.
- The native order/refund callback entry points continue to resolve the channel first and execute the business update inside `TenantUtils.execute(channel.tenantId, ...)`. The existing notification retry job continues to use `@TenantJob`.
- Existing Pay controllers remain authoritative: `/pay/order` and `/pay/refund` provide tenant-filtered query/export contracts, while `/pay/notify` provides tenant-filtered task/detail reads and the provider callback entry points.
- V4340 exposes the existing `pay/order/index`, `pay/refund/index`, and `pay/notify/index` Vben pages below Education with the original `pay:order:*`, `pay:refund:*`, and `pay:notify:query` permissions. No custom Education transaction page was added.
## Database safety
The circular order/extension relationship is created in two steps. The final `fk_pay_order_extension` installation is guarded through `pg_constraint`: an equivalent named composite tenant foreign key is accepted, a conflicting named constraint fails closed, and a missing constraint is installed. V4340 also verifies the complete required column shape after table creation.
The migration intentionally creates empty native transaction ledgers. It does **not** import legacy `orders`, `payments`, `payment_events`, or `commerce_refund_requests`; importing those records requires an explicit, reconciled mapping with amount/status/identifier/callback ownership rules.
## Verification
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4340 coverage proves same merchant order ID across tenants, composite-FK cross-tenant rejection, active notification-task uniqueness and soft-delete recreation, native menu/permission shape, successful migration history, and fail-closed adoption of a global `pay_order` table.
- The combined native Pay transaction regression passes 86 tests: 46 order, 28 refund, 11 notification, and one tenant-inheritance contract.
- The previously disabled `PayNotifyServiceTest` is active; its asynchronous scheduling assertions and retry-count fixtures now match the production contract.
- `mvn -pl yudao-server -am -DskipTests compile` and the Vben `@vben/web-antd` typecheck are the closing reactor/UI gates for this slice.
## Explicitly open
- EDU-023 now supplies a bounded, terminal-only reconciled import. Production export tooling, reviewed Member-ID mapping, dry-run/runbook evidence, and operator sign-off remain required; direct table copying is still forbidden.
- EDU-024 now activates tenant-aware native Pay Transfer and Wallet persistence plus the existing administration pages. Historical opening balances remain deliberately unpopulated pending a reviewed source artifact.
- EDU-025 delivers native Mall Product activation and EDU-026 delivers native Promotion Coupon activation. Explicit legacy product/code-coupon import, Trade/other Promotion activation, and automatic purchase-to-entitlement fulfillment are not delivered.
- Refund completion does not yet revoke or shorten Education entitlements; commerce reconciliation must define partial-refund and replay semantics first.
- Coupons, commissions, referrals, dunning, settlement/reconciliation, generic Pay credential encryption/rotation, tenant PNVS, production deployment, and browser/API integration evidence remain separate work.

View File

@@ -0,0 +1,41 @@
# EDU-023 — Import reconciled terminal legacy Pay transactions
- **Status:** done — bounded terminal aggregate import, redacted audit, and native Pay order-page UI verified
- **Type:** legacy data bridge / financial reconciliation / privacy
- **Phase:** 4 / commercialization foundation
- **Blockers:** EDU-021, EDU-022, PostgreSQL Flyway
## Problem
The legacy `orders`, `payments`, `payment_events`, and `commerce_refund_requests` rows cannot be copied into native Pay independently. Their statuses, identifiers, and totals form one aggregate; importing a live or inconsistent aggregate could make native jobs, callbacks, or operators charge or refund it again. Raw provider payloads and error bodies also contain data that does not belong in a second audit store.
## Delivered contract
- `POST /pay/legacy-transaction-import/import` imports one explicitly reviewed aggregate and requires `pay:legacy-transaction:import`. `GET /pay/legacy-transaction-import/page` exposes tenant-filtered audit history under `pay:legacy-transaction:query`.
- Only terminal order, payment, and refund states are accepted. Amounts are already denominated in cents and must reconcile exactly: one verifiable successful payment at most, payment amount equals order price, successful refund sum equals `refundedPrice`, and order/refund status agrees with that sum.
- Every aggregate must reference an EDU-021 `pay_legacy_account_import` from the same source tenant. All payment and refund provider families must match that reviewed mapping.
- Imported data is written into Pay-owned `pay_order`, `pay_order_extension`, and `pay_refund`; Education does not gain a financial shadow ledger.
- Import does not call a provider SDK, enqueue notification tasks, invoke business callbacks, or copy provider credentials. `raw_payload`, event payloads, channel notification bodies, and error originals are excluded. Payment-event evidence is reduced to a count and SHA-256 digest.
- A target tenant uses source order UUID as its idempotency key. The same checksum replays the audit result without rewriting native ledgers; a different checksum is rejected. Native merchant order, payment extension, and refund-number collisions fail with a reconciliation error before writes.
- The legacy model has no reliable client IP or channel fee. Native rows therefore use `0.0.0.0` and zero fee, and the audit records that limitation.
- `targetUserId` is an explicit optional native Member ID. The importer does not infer a UUID-to-Member mapping and Pay does not depend directly on Member, avoiding a module cycle. Export tooling or the operator owns that reviewed mapping.
- Request-body access logging is disabled. The existing native `pay/order/index` page exposes a permission-aware JSON manifest modal and the most recent 50 audit rows; no raw provider payload is rendered.
## Database contract
V4350 adds tenant-scoped `pay_legacy_transaction_import`, `pay_legacy_transaction_payment_import`, and `pay_legacy_transaction_refund_import`. Composite tenant foreign keys bind audit rows to the reviewed account mapping and native App, Channel, Order, Extension, and Refund targets. Existing global tables fail closed. The same legacy source UUID may be imported independently by distinct target tenants.
The schema enforces terminal status sets, non-negative counts/totals, event-count/digest consistency, lowercase SHA-256 shapes, tenant-scoped source uniqueness, and exact expected table shape. None of the three tables contains a raw payload, credential, notification body, or error-original column.
## Verification
- Nine focused service/controller tests pass, covering reconciled full-refund import, redaction, replay, checksum conflict, provider mismatch, refund/status mismatch, event digest mismatch, refund-without-payment rejection, native-number collision, permission, and request-log suppression.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4350 coverage proves composite cross-tenant rejection, per-target-tenant source reuse, event digest consistency, sensitive-column absence, exact menu/permission shape, migration history, and fail-closed global-table adoption.
- Vben lint, formatting, and `@vben/web-antd` typecheck pass for the API, import/history modal, and native order-page integration.
## Explicitly open
- A controlled legacy export tool, Member-ID mapping artifact, operator runbook, dry-run report, backup, and production reconciliation sign-off are required before bulk import. The UI template contains placeholders and is not an unattended bulk migrator.
- Concurrent first imports and native-number races still rely on database uniqueness and transaction rollback; a friendlier conflict replay is not claimed.
- Failed/cancelled historical attempts are preserved as closed native extensions/refunds only inside a reconciled terminal aggregate. No live state is resumed.
- EDU-024 activates native Pay Transfer/Wallet with empty tenant-owned ledgers, and EDU-025 activates an empty native Product catalog. Reviewed opening balances, explicit legacy product import, Promotion/Trade, automatic purchase fulfillment, refund-to-entitlement revocation, settlement reconciliation, coupons, commissions, referrals, dunning, generic credential encryption/rotation, tenant PNVS, and production browser/API evidence remain separate slices.

View File

@@ -0,0 +1,41 @@
# EDU-024 — Activate native Pay Transfer and Wallet
- **Status:** done — tenant-owned native ledgers, security hardening, existing Pay APIs/UI, and PostgreSQL evidence delivered
- **Type:** native module takeover / financial ledger / tenant isolation
- **Phase:** 4 / commercialization foundation
- **Blockers:** EDU-020, EDU-022, PostgreSQL Flyway
## Problem
RuoYi already provides Transfer and Wallet services, callbacks, jobs, permissions, and Vben pages, but their persistence was not activated by the Education PostgreSQL migration line and the corresponding data objects were not tenant-aware. Reimplementing those capabilities inside Education would create a second financial ledger. Inferring a wallet balance from the legacy backend would be unsafe because the legacy schema has no equivalent authoritative balance aggregate.
## Delivered contract
- Existing native Pay contracts remain authoritative: `/pay/transfer`, `/pay/wallet`, `/pay/wallet-transaction`, `/pay/wallet-recharge`, and `/pay/wallet-recharge-package`. No Education transfer, balance, recharge, or transaction controller was added.
- `PayTransferDO`, `PayWalletDO`, `PayWalletTransactionDO`, `PayWalletRechargeDO`, and `PayWalletRechargePackageDO` now extend `TenantBaseDO`. Transfer synchronization keeps `@TenantJob`, and framework tenant injection scopes all normal mapper access.
- Wallet locks now use `pay_wallet:lock:{tenantId}:{walletOrUserId}` so the same native identifier in two tenants cannot serialize against or interfere with the other tenant's operation.
- Amount-changing service methods reject null, zero, and negative amounts. Administrator balance reductions use the conditional subtract path and cannot create a negative balance or incorrectly increase the member's lifetime expense total. `Integer.MIN_VALUE` cannot be negated through the admin request contract.
- Wallet recharge refund completion uses the persisted recharge `walletId`; it does not dereference a separately loaded wallet. The refund action has its own `pay:wallet-recharge:refund` permission.
- Wallet-provider transfer status lookup uses the native transfer number, matching the business key used when the wallet transaction was created.
- Request DTOs validate positive user/package IDs, valid wallet business types, positive add amounts, non-zero admin adjustments, package name/amount/status shape, and non-negative bonus amounts.
- The existing Vben `pay/transfer/index`, `pay/wallet/balance/index`, and `pay/wallet/rechargePackage/index` pages and APIs are reused. Member administration continues to provide the permission-aware balance-adjustment action.
## Database contract
V4360 creates empty tenant-scoped `pay_transfer`, `pay_wallet`, `pay_wallet_transaction`, `pay_wallet_recharge`, and `pay_wallet_recharge_package` ledgers. It never imports or invents a historical wallet balance.
Composite tenant foreign keys bind transfers to native Pay App/Channel and wallet rows to their Wallet, Package, Order, and Refund owners. Balances, frozen amounts, and cumulative totals cannot be negative. A tenant can have only one active wallet per `(userId,userType)`, while the same identifiers remain valid in another tenant. Non-administrator transaction business keys are tenant-idempotent. Existing global tables fail closed instead of being silently adopted.
V4360 also seeds the existing Vben routes and granular Transfer query/export, Wallet query/update, Recharge Package CRUD, and Recharge refund permissions.
## Verification
- Twelve focused Pay tests pass across transfer service behavior, tenant-aware DO/job/permission contracts, tenant-qualified Redis locking, safe positive/negative admin adjustments, non-positive amount rejection, recharge-refund wallet identity, and wallet-provider transfer lookup.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4360 coverage proves same identifiers across tenants, same-tenant wallet uniqueness, cross-tenant Wallet Transaction and Transfer foreign-key rejection, database rejection of negative balances, exact menu/permission shape, migration history, and fail-closed global-wallet adoption.
- The existing Vben Transfer/Wallet/Recharge Package APIs and pages pass formatting, lint, and `@vben/web-antd` typecheck as part of the combined UI verification.
## Explicitly open
- Production opening balances require a separately reviewed, reconciled source artifact and operator runbook. This slice deliberately creates empty wallet ledgers because the legacy system has no equivalent balance authority.
- Transfer initiation, wallet recharge, channel callbacks, and refunds still require target-environment credentials, App/Channel setup, explicit role grants, and browser/API integration evidence.
- EDU-025 now activates native Mall Product persistence and existing administration without importing legacy display rows. Promotion/Trade, explicit legacy product import, automatic purchase fulfillment, refund-driven entitlement revocation, settlement reconciliation, commissions, referrals, dunning, generic credential encryption/rotation, tenant PNVS, legacy activation-code import, coupons, and production bulk financial migration remain separate slices.

View File

@@ -0,0 +1,58 @@
# EDU-025 — Activate native Mall Product
- **Status:** done — tenant-owned native Product persistence, existing Product APIs/UI, RBAC, and PostgreSQL evidence delivered
- **Type:** native module takeover / product catalog / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-013, PostgreSQL Flyway
## Problem
RuoYi Vue Pro already provides Product controllers, services, mappers, permissions, and Vben pages for brands, categories, properties, SPUs, SKUs, comments, favorites, and browse history. The Mall reactor and Product server dependency were disabled, however, and the Product data objects were not tenant-aware. Rebuilding those capabilities inside Education would create a second product catalog.
The legacy `public.products` object is only a tenant/region presentation projection with `title`, a display `price_label`, links, tags, cover/iframe/detail images, ordering, and lifecycle status. It has no authoritative SKU, integer price, stock, brand, property, delivery, commission, or sales model. An automatic conversion would invent commercial facts.
## Delivered contract
- The root build now includes `yudao-module-mall`, while `yudao-server` activates only `yudao-module-product`. Promotion, Trade, and Statistics runtime dependencies remain off until their own tenant-ledger slices are verified.
- Existing native `/product/brand`, `/product/category`, `/product/property`, `/product/property/value`, `/product/spu`, `/product/comment`, `/product/favorite`, and `/product/browse-history` controllers remain authoritative. No Education product controller or duplicate catalog service was added.
- `ProductBrandDO`, `ProductCategoryDO`, `ProductPropertyDO`, `ProductPropertyValueDO`, `ProductSpuDO`, `ProductSkuDO`, `ProductCommentDO`, `ProductFavoriteDO`, and `ProductBrowseHistoryDO` now extend `TenantBaseDO`, so normal MyBatis access participates in framework tenant injection.
- Existing Vben pages are reused for SPU/SKU authoring, category trees, brands, property/value management, and comment moderation. V4370 seeds their original permissions below an Education `商品中心` route with unique component names.
- The Product Vben scope passes typecheck, oxlint, and oxfmt. The formatter normalized one pre-existing multiline expression in the SPU form without changing behavior.
## Database contract
V4370 creates tenant-scoped PostgreSQL tables:
- `product_brand`
- `product_category`
- `product_property`
- `product_property_value`
- `product_spu`
- `product_sku`
- `product_comment`
- `product_favorite`
- `product_browse_history`
Every table uses `(tenant_id,id)` as its primary ownership key, allowing an explicit native identifier to be reused in another tenant while keeping every reference tenant-qualified. Composite foreign keys enforce Property Value → Property, SPU → Category/Brand, SKU → SPU, Comment → SPU/SKU, and Favorite/Browse History → SPU. Category root `parent_id=0` remains a sentinel; a trigger requires non-root parents to exist in the same tenant and limits the native category model to two levels.
Database checks reject negative prices, stock, sales, integral, commission, weight, volume, and browse counts, and restrict comment scores to 15. Partial unique indexes protect active brand/property/value names, one active favorite per member/SPU, one active browse-history row per member/SPU, and one active comment per member/order item. Existing global Product tables fail closed instead of being silently assigned to a tenant.
V4370 seeds menu IDs 69206944 for the Product root, five native pages, and the exact SPU, Category, Brand, Property, and Comment controller permissions.
## Legacy data decision
V4370 creates an empty native catalog and does not read or transform legacy `public.products`. A later import, if required, must provide an explicit reviewed mapping for tenant UUIDs, region semantics, integer prices, SPU/SKU structure, stock authority, brand/category/property ownership, delivery mode, media admission, and Education resource bindings. A display price label or URL is not sufficient evidence for any of those fields.
## Verification
- The focused Product tenant contract passes and proves all nine native Product records inherit `TenantBaseDO`.
- The Product reactor test run succeeds; the repository's 33 pre-existing Product service tests remain disabled by their existing test configuration, while the new tenant contract executes successfully.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4370 coverage proves cross-tenant ID reuse, same-tenant uniqueness, composite foreign-key rejection, category parent isolation/two-level enforcement, amount/stock and score checks, exact menu shape, migration history, and fail-closed global-table adoption.
- `mvn -pl yudao-server -am -DskipTests compile` succeeds with Product enabled.
- `@vben/web-antd` typecheck and scoped Product oxlint/oxfmt checks pass.
## Explicitly open
- Native Product pages require target-environment role grants, browser/API smoke evidence, media/file configuration, and deliberate catalog population.
- Promotion coupons are delivered by EDU-026; Promotion discounts and other activity families, Trade cart/order/after-sale/delivery, Statistics, automatic purchase fulfillment, refund-driven entitlement revocation, and Education product-binding workflows remain separate slices.
- Legacy `products` import remains blocked on an explicit semantic mapping and reconciled source artifact; no SKU, stock, price, or category is inferred.

View File

@@ -0,0 +1,51 @@
# EDU-026 — Activate native Mall Promotion coupons
- **Status:** done — tenant-owned native coupon templates/instances, existing APIs/UI, RBAC, and PostgreSQL evidence delivered
- **Type:** native module takeover / coupon lifecycle / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-025, PostgreSQL Flyway
## Problem
RuoYi Vue Pro already provides coupon-template and issued-coupon controllers, services, Product-scope validation, Member lookup, registration issuance, expiry processing, permissions, and Vben pages. The Promotion server dependency was disabled and its two coupon records inherited only `BaseDO`, so reimplementing coupons inside Education would create a second marketing ledger without fixing tenant ownership.
Legacy `public.coupons` are code-based campaign rules with plan/region restrictions, first-order rules, usage counters, and `coupon_redemptions`. Native Promotion coupons are templates that issue member-owned coupon instances before order use. They are not losslessly interchangeable.
## Delivered contract
- `yudao-server` now activates `yudao-module-promotion` in addition to Product; Trade and Statistics remain disabled until their own ledger slices are verified.
- Existing native `/promotion/coupon-template`, `/promotion/coupon`, `/app-api/promotion/coupon-template`, and `/app-api/promotion/coupon` contracts remain authoritative. Education adds no coupon controller or duplicate service.
- `CouponTemplateDO` and `CouponDO` extend `TenantBaseDO`, so native mapper/service access participates in the framework tenant interceptor.
- Native template creation reuses Product SPU/category validation. Native coupon administration reuses Member lookup, direct/admin/registration issuance, expiry processing, use/return, and soft-delete recovery rules.
- Promotion's unrelated bargain/combination beans require `TradeOrderApi`. EDU-027 now activates the native Trade implementation and removes the temporary fail-closed adapter that was used while Trade was absent.
- V4380 mounts the existing Vben template and issued-coupon pages below an Education `优惠券中心` using the original seven controller permissions and unique component names.
## Database contract
V4380 creates tenant-scoped PostgreSQL tables:
- `promotion_coupon_template`
- `promotion_coupon`
Both use `(tenant_id,id)` ownership keys and explicitly named identity sequences. Coupon → Template is a tenant-qualified composite foreign key, so an identifier valid in another tenant cannot be referenced. Checks enforce native status/take/scope/validity/discount enums, fixed-date or relative-term validity, non-negative thresholds and discounts, issue/use counter consistency, positive members/orders, and complete used-coupon state. Indexes support template discovery, member/status lookup, template issuance lookup, and expiry jobs. Existing global coupon tables fail closed instead of being silently assigned to a tenant.
V4380 seeds menu IDs 69506959 for the coupon root, template page, issued-coupon page, and exact query/create/update/delete/send permissions.
## Legacy data decision
V4380 starts the native coupon ledger empty. It does not reinterpret a legacy code campaign as a pre-issued member coupon, invent template/instance IDs, discard plan/region/first-order semantics, or attach historical redemptions to unverified native orders and members. A later compatibility/import slice must explicitly decide whether to preserve code redemption as a separate adapter or transform reviewed campaigns and redemption history.
## Verification
- The Promotion coupon tenant contract proves both native coupon records inherit `TenantBaseDO`; EDU-027 separately verifies the native Trade dependency that replaced the temporary fallback.
- The focused V4380 Flyway scenario passes and proves same IDs can exist across tenants, cross-tenant template references fail, invalid counters/discounts/used state fail, explicit sequences exist, exact menus/permissions are installed, and global-table adoption fails closed.
- All 45 PostgreSQL Flyway integration tests pass.
- `mvn -pl yudao-server -am -DskipTests compile` succeeds with Product and Promotion enabled.
- `@vben/web-antd` typecheck and scoped Coupon oxlint/oxfmt checks pass.
## Explicitly open
- Legacy code-campaign and redemption compatibility/import remains a separate mapping slice.
- EDU-027 activates the tenant-scoped normal-order core. Coupon-to-special-order and refund composition still depend on deferred Promotion and Trade after-sale slices.
- Discount/reward/seckill/combination/bargain/point/Diy/KeFu Promotion families are not activated at the database/menu level by this slice.
- Target-environment role grants, browser/API smoke evidence, and deliberate template population remain operational work.

View File

@@ -0,0 +1,53 @@
# EDU-027 — Activate native Mall Trade order core
- **Status:** done — tenant-owned order core, native APIs/UI, RBAC, and PostgreSQL evidence delivered
- **Type:** native module takeover / order lifecycle / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-020, EDU-024, EDU-025, EDU-026, PostgreSQL Flyway
## Problem
RuoYi Vue Pro already provides the normalized Trade order aggregate, cart and price orchestration, Pay/Product/Promotion/Member composition, administrator order/config controllers, jobs, permissions, and Vben pages. The Server dependency was disabled and its core data objects inherited only `BaseDO`. Rebuilding orders inside Education would create a second commerce ledger and duplicate native payment, member, catalog, and coupon boundaries.
Legacy orders cannot be copied safely from terminal payment aggregates or display-only product rows. Native Trade requires verified Member identities, SPU/SKU line items, price allocation, delivery mode, payment linkage, status history, and promotion state that those projections do not contain.
## Delivered contract
- `yudao-server` activates `yudao-module-trade`; native `TradeOrderApiImpl` is now authoritative and the temporary Promotion fallback from EDU-026 is removed.
- Existing `/trade/order`, `/trade/config`, `/app-api/trade/order`, and `/app-api/trade/cart` contracts remain owned by Trade. Education adds no shadow order or cart API. This slice proves the administrator order/config interface and core persistence; it does not yet claim successful app checkout.
- `TradeOrderDO`, `TradeOrderItemDO`, `TradeOrderLogDO`, `TradeConfigDO`, and `CartDO` extend `TenantBaseDO`, so their native mapper/service access participates in framework tenant injection.
- V4390 mounts the existing order and trade-config Vben pages below an Education `交易中心` with the exact native query/update/pick-up/config permissions.
- Deferred after-sale, delivery-master-data, and brokerage tables are not silently activated. V4390 rejects any pre-existing version of those tables until a dedicated tenant-safe slice owns their schema and UI.
## Database contract
V4390 creates tenant-scoped PostgreSQL tables:
- `trade_config`
- `trade_cart`
- `trade_order`
- `trade_order_item`
- `trade_order_log`
All use `(tenant_id,id)` ownership keys and explicitly named identity sequences. Order Item → Order/Cart/Product and Order Log → Order references are tenant-qualified. Cart → Product, Order → Pay Order, and Order → Promotion Coupon references are also tenant-qualified. Checks enforce native order/type/terminal/delivery/refund/cancel/log enums, amount/count bounds, payment/cancel/delivery shapes, after-sale reference shape, and one active config per tenant. Indexes match administrator paging, member history, cart selection, auto-cancel/receive jobs, order details, and log history.
V4390 seeds menu IDs 69606967 for the Trade root, order page, config page, and exact five controller permissions.
## Legacy data decision
The native Trade ledger starts empty. V4390 does not fabricate line items from Pay totals, guess tenant/member/SPU/SKU links, translate terminal statuses into a richer order lifecycle, or attach historical coupons and refunds without reviewed source identities. A later import slice requires an explicit reconciliation contract and quarantine path.
## Verification
- The focused Trade tenant contract proves all five activated records inherit `TenantBaseDO`.
- The focused V4390 Flyway scenario proves same IDs can exist across tenants, cross-tenant Cart/Order Item references fail, invalid cancel/config states fail, five explicit sequences exist, exact menus/permissions are installed, core global-table adoption fails closed, and deferred Trade tables fail closed.
- The full PostgreSQL Flyway suite passes with 46 scenarios through V4390.
- `mvn -pl yudao-server -am -DskipTests compile` succeeds with Product, Promotion, and Trade enabled.
- The existing Vben order/config pages pass the web app typecheck and scoped lint/format checks.
## Explicitly open
- Native after-sale, delivery express/template/pick-up-store, and brokerage persistence/UI require later tenant-safe slices.
- Normal checkout still invokes native Promotion discount and reward lookups whose tables are deferred, and special orders require additional Promotion families. V4390 therefore does not claim end-to-end purchase creation, seckill, bargain, combination, point, discount, or reward orders.
- Legacy order import, automatic education-entitlement fulfillment, refund-driven revocation, and production reconciliation/runbooks remain separate work.
- Target-environment role grants, scheduled-job deployment, and browser/API smoke evidence remain operational work.

View File

@@ -0,0 +1,36 @@
# EDU-028 — Activate native checkout Promotion activities
- **Status:** done — tenant-owned discount/reward persistence, native APIs/UI, RBAC, and PostgreSQL evidence delivered
- **Type:** native module takeover / checkout dependency / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-025, EDU-026, EDU-027, PostgreSQL Flyway
## Problem
Every normal RuoYi Trade price calculation invokes the native limited-time discount and reward calculators. Those calculators call `DiscountActivityApi` and `RewardActivityApi` even when no activity is configured, so V4390's order core still failed with missing Promotion tables before it could return an empty promotion result. Replacing these APIs inside Education would duplicate Promotion ownership and bypass the native administration UI.
## Delivered contract
- `DiscountActivityDO`, `DiscountProductDO`, and `RewardActivityDO` extend `TenantBaseDO`; their existing services, mappers, controllers, and public APIs remain authoritative.
- V4400 creates tenant-scoped `promotion_discount_activity`, `promotion_discount_product`, and `promotion_reward_activity` tables with explicit PostgreSQL sequences and `(tenant_id,id)` ownership keys.
- Discount Product → Activity/SPU/SKU references are tenant-qualified. Active SKU uniqueness, status/time, discount type/value, reward condition/scope, and JSON reward-rule checks fail closed in PostgreSQL.
- The native cross-database `MyBatisUtils.findInSetWithParamIndex` already renders PostgreSQL `POSITION(...)` against the comma-separated `LongListTypeHandler` value; no mapper fork or Education shadow interface is introduced.
- Menu IDs 69706982 mount the existing `mall/promotion/discountActivity/index` and `mall/promotion/rewardActivity/index` Vben pages with the exact ten native controller permissions.
## Legacy data decision
V4400 starts both activity families empty. Legacy coupon/code campaigns are not equivalent to time-boxed SKU discounts or structured full-reduction/gift rules, and no activity, product scope, time range, or reward rule is inferred.
## Verification
- The Promotion tenant contract proves all three activated records participate in framework tenant isolation.
- The focused V4400 scenario proves cross-tenant identifier reuse, composite reference rejection, active-SKU uniqueness, PostgreSQL scope membership semantics, reward-rule/scope rejection, three explicit sequences, exact routes/permissions, and fail-closed global-table adoption.
- A Spring/MyBatis integration test invokes the real `DiscountActivityApiImpl` and `RewardActivityApiImpl` against migrated PostgreSQL tables and proves empty normal-checkout lookups return empty lists rather than missing-table errors.
- The full PostgreSQL Flyway suite passes 47 scenarios through V4400.
## Explicitly open
- This slice removes the normal price pipeline's discount/reward missing-table blocker; it does not claim end-to-end order creation. Delivery express/template/pick-up persistence and configured Pay App/Channel runtime data remain prerequisites for applicable checkout modes.
- Seckill, bargain, combination, point, and other special-order Promotion tables remain separate tenant-safe activations.
- Legacy Product/campaign/order import, purchase-driven Education entitlement fulfillment, refund-driven revocation, and production API/browser smoke evidence remain separate work.

View File

@@ -0,0 +1,34 @@
# EDU-029 — Activate native Trade delivery
- **Status:** done — tenant-owned delivery persistence, native checkout calculation, RBAC, UI, and PostgreSQL evidence delivered
- **Type:** native module takeover / checkout dependency / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-025, EDU-027, EDU-028, PostgreSQL Flyway
## Problem
Normal native Trade checkout supports express delivery and store pickup. Express pricing reads Product SPU delivery-template IDs, Member addresses, per-tenant Trade configuration, template charge/free rules, and delivery areas. Pickup validates an enabled store. The source backend has no equivalent physical-shipping aggregate, so implementing an Education-owned delivery API would duplicate RuoYi Trade and leave its existing administration UI disconnected.
## Delivered contract
- `DeliveryExpressDO`, `DeliveryExpressTemplateDO`, `DeliveryExpressTemplateChargeDO`, `DeliveryExpressTemplateFreeDO`, and `DeliveryPickUpStoreDO` extend `TenantBaseDO`; the native services, mappers, controllers, calculators, and Vben pages remain authoritative.
- V4410 creates all five tables with explicit sequences, `(tenant_id,id)` ownership, tenant-qualified template-child references, and a charge-mode-qualified reference that prevents child/template mode drift.
- Comma-separated area/user IDs are validated in the database using the formats consumed by `IntegerListTypeHandler` and `LongListTypeHandler`. Amounts, counts, charge modes, status, address, business hours, and coordinates fail closed.
- Product SPU → delivery template and Trade Order → pickup store references are tenant-qualified. Pickup orders require a store; relevant lookup indexes are installed.
- Menu IDs 69907006 mount the existing Express, Express Template, and Pickup Store pages with the exact thirteen permissions exposed by the native controllers.
## Legacy data decision
V4410 starts delivery configuration empty. The source backend contains no authoritative express company, freight rule, delivery area, store, coordinates, hours, or verifier mapping. No product is silently assigned a template and no store is fabricated.
## Verification
- The delivery tenant contract proves all five activated records participate in framework tenant isolation.
- The focused V4410 Flyway scenario proves cross-tenant identifier reuse, Product/template and Order/store ownership, charge-mode consistency, area/location checks, pickup-order shape, five explicit sequences, exact routes/permissions, and fail-closed global-table adoption.
- A Spring/MyBatis PostgreSQL integration test uses the production tenant SQL interceptor, creates same-named native templates in two tenants, proves isolated mapper results, reads real charge/free rules, and drives `TradeDeliveryPriceCalculator` through the persisted express template to a 700-cent delivery fee.
- The full Flyway suite passes 48 scenarios through V4410; server and Vben verification are recorded in the migration UI report.
## Explicitly open
- Delivery persistence and calculation are active, but configured Pay App/Channel data and a target-environment order/payment smoke remain prerequisites for an end-to-end paid checkout claim.
- Trade after-sale and brokerage, special-order Promotion families, explicit legacy Product/coupon/order import, purchase-driven Education entitlement fulfillment, and refund-driven revocation remain separate slices.

View File

@@ -0,0 +1,36 @@
# EDU-030 — Activate native Trade after-sale
- **Status:** done — tenant-owned after-sale persistence, native state machine, Pay Refund bridge, RBAC, corrected Vben UI, and PostgreSQL evidence delivered
- **Type:** native module takeover / refund lifecycle / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-022, EDU-025, EDU-027, EDU-029, PostgreSQL Flyway
## Problem
The source backend stores order-level UUID rows in `public.commerce_refund_requests` and append-only `public.commerce_refund_events`, with statuses `requested`, `approved`, `processing`, `succeeded`, `failed`, `rejected`, and `cancelled`. RuoYi already owns a richer line-item after-sale state machine: Member application, administrator agree/disagree, return logistics, receipt/refusal, Pay Refund creation/callback, order-item state updates, immutable operation logs, exact RBAC, and an existing Vben list/detail page. Reimplementing that lifecycle in Education would create a second refund ledger and bypass native Trade/Pay coordination.
## Delivered contract
- `AfterSaleDO` and `AfterSaleLogDO` extend `TenantBaseDO`; existing app/admin controllers, `AfterSaleServiceImpl`, `AfterSaleLogServiceImpl`, order updates, and `PayRefundApi` remain authoritative.
- V4420 creates `trade_after_sale` and `trade_after_sale_log` with explicit sequences and `(tenant_id,id)` ownership. Order, order item, SPU, SKU, Pay Refund, delivery express, log, and order-item back-reference constraints are tenant-qualified.
- Database checks enforce native status/type/way values, non-negative amounts, required audit/return/refund facts, JSON arrays, valid log actor/operation/status values, unique tenant numbers, and at most one active after-sale per order item.
- Menu IDs 70107015 reuse `mall/trade/afterSale/index` with exactly `trade:after-sale:query`, `agree`, `disagree`, `receive`, and `refund` permissions.
- The Vben page sends the backend's `auditReason` field, requires `refuseMemo` in a locked/validated refusal modal, displays the actual application `createTime`, and guards every action with its exact permission. Backend request validation now rejects blank refusal notes and the detail response exposes `createTime`.
## Legacy data decision
V4420 starts the native after-sale ledger empty. A source request identifies an aggregate UUID order but does not identify a verified native Member, `trade_order_item`, SPU/SKU allocation, return quantity, delivery company, or Pay Refund row. Its status history also does not prove the native line-item and return-logistics transitions. Automatic import would invent ownership and lifecycle facts, so explicit legacy refund import is deferred until legacy Product, Order, Order Item, and Member mappings are reviewed.
## Verification
- The after-sale tenant contract proves both native records participate in framework tenant isolation.
- The focused V4420 Flyway scenario proves cross-tenant identifier reuse, tenant-qualified ownership/back-references, two explicit sequences, audit/log rejection, exact route/permissions, and fail-closed adoption of a pre-existing global table.
- A Spring/MyBatis PostgreSQL integration test imports the real services and production tenant SQL interceptor, creates same-ID Product/Order/Order Item fixtures in tenants 10 and 20, and proves isolated create, page, detail, and log reads plus order-item update calls.
- The full PostgreSQL Flyway suite passes 49 scenarios through V4420. The server reactor compiles, and the corrected Vben page passes typecheck plus scoped oxlint/oxfmt checks.
## Explicitly open
- Trade brokerage and special-order Promotion families remain separate native activation slices.
- Legacy Product/coupon/order/refund import requires explicit UUID-to-native mappings and reconciliation artifacts; V4420 does not reinterpret source refunds.
- Configured target Pay App/Channel data and a deployed order/payment/refund smoke are still required for end-to-end production evidence.
- Purchase-driven Education entitlement fulfillment and refund-driven entitlement revocation remain separate orchestration work.

View File

@@ -0,0 +1,43 @@
# EDU-031 — Activate native Trade brokerage
- **Status:** done — native tenant-owned relationships, commission records, withdrawal/Pay Transfer bridge, exact RBAC, corrected Vben UI, and PostgreSQL evidence delivered
- **Type:** native module takeover / two-level commission / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-024, EDU-027, PostgreSQL Flyway
## Problem
The source backend has referral codes, leads, team edges, tracks, QR codes, CRM assignment, tenant commission settings, settlement aggregates/items, and proof/export events. RuoYi already owns a different but substantial Trade brokerage lifecycle: Member-backed promoter relationships, first/second-level order commissions, freeze/unfreeze, cancellation, withdrawals, Pay Transfer composition, app/admin APIs, scheduled settlement work, exact permissions, and three Vben administration pages. Rebuilding those overlapping behaviors in Education would create a second commission ledger and bypass native Member/Trade/Pay coordination.
Primary source evidence remains in:
- `/Users/tiku1/code/tiku-backend/supabase/migrations/202606210006_growth_referral_crm.sql`
- `/Users/tiku1/code/tiku-backend/supabase/migrations/202606290009_commission_settlements.sql`
- `/Users/tiku1/code/tiku-backend/supabase/migrations/202606290027_commission_settlement_proofs.sql`
## Delivered contract
- `BrokerageUserDO`, `BrokerageRecordDO`, and `BrokerageWithdrawDO` extend `TenantBaseDO`; native app/admin controllers, services, mappers, jobs, Member lookups, Trade order integration, and Pay Transfer API remain authoritative.
- V4430 creates `trade_brokerage_user`, `trade_brokerage_record`, and `trade_brokerage_withdraw` with `(tenant_id,id)` ownership, explicit record/withdraw sequences, tenant-qualified relationship/source/withdrawal/Pay Transfer/Trade Order references, commission idempotency, amount/state/account/audit/transfer checks, and a 099 withdrawal-fee range.
- Menu IDs 70207031 reuse the existing user, record, and withdrawal pages with exactly eight controller permissions: user query/create/update-bind/clear-bind/update-enable, record query, and withdrawal query/audit.
- The Vben user page uses those real permissions for both drill-downs, disables the eligibility switch without update permission, exposes responsive forms/tables, and its API types now match the backend responses. Withdrawal rejection trims and requires a non-blank reason on both sides.
- Immediate-settlement commission records now persist a settlement time, so the native time-range summary and ranking SQL includes zero-freeze commissions.
## Legacy data decision
V4430 activates the native ledger empty. Source referral/settlement rows use UUID identities and carry CRM lead assignment, referral-code/track attribution, settlement-batch state, and proof/export semantics that have no verified one-to-one mapping to native Member IDs, `trade_order` rows, two-level promoter relations, Pay Transfer rows, or immutable evidence artifacts. Importing them now would invent identity and settlement facts. Explicit referral CRM and commission-settlement import stays deferred until Member, lead, Order, payment, and proof mappings are reviewed; those source-only semantics are not declared retired.
## Verification
- The focused V4430 scenario proves table/sequence/check/index shape, same-number cross-tenant identities, tenant-qualified references, exact routes/permissions, and fail-closed adoption of a pre-existing global table.
- The tenant contract proves all three native records participate in framework tenant isolation and withdrawal inputs reject missing type, non-positive price, and blank audit reason.
- A Spring/MyBatis PostgreSQL integration test imports the real user/record services and production tenant SQL interceptor. Tenants 10 and 20 create the same Member IDs and business ID with different commission percentages; relationship/page reads, balances, MyBatis-Join summaries, and native annotated summary/ranking SQL remain isolated.
- The corrected Vben pages pass workspace typecheck and scoped oxlint/oxfmt checks.
- The full PostgreSQL Flyway suite passes 50 scenarios through V4430, and the server clean reactor compiles all 28 modules.
## Explicitly open
- Source referral code/lead/team-edge/track/QR/CRM-assignment semantics and settlement proof/export aggregates require dedicated mapping/import slices.
- Special-order Promotion families and explicit legacy Product/coupon/order/refund import remain separate.
- Purchase-driven Education entitlement fulfillment and refund-driven entitlement revocation remain separate orchestration work.
- Configured Pay runtime, scheduled unfreeze/transfer operation, and deployed order-to-commission-to-withdrawal smoke evidence remain required for production acceptance.

View File

@@ -0,0 +1,39 @@
# EDU-032 — Activate native Promotion seckill
- **Status:** done — native tenant-owned seckill configuration, activities, SKU stock, Trade Order bridge, exact RBAC, corrected Vben UI, and PostgreSQL evidence delivered
- **Type:** native module takeover / special-order promotion / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-025, EDU-027, PostgreSQL Flyway
## Problem
The source backend has no seckill table, endpoint, job, or administration surface. RuoYi already owns a complete Promotion seckill lifecycle: time configurations, SPU/SKU activity authoring, atomic stock changes, Trade Order integration, app/admin APIs, exact permissions, and two Vben administration pages. Rebuilding those behaviors in Education would create a second promotion inventory and bypass native Product, Promotion, Trade, Member, and tenant/RBAC coordination.
Repository-wide source inventory under `/Users/tiku1/code/tiku-backend` found no seckill or 秒杀 capability. That absence is a data decision: the target capability starts empty and no legacy records are invented.
## Delivered contract
- `SeckillConfigDO`, `SeckillActivityDO`, and `SeckillProductDO` extend `TenantBaseDO`; the native controllers, services, mappers, Product APIs, Trade Order fields, and Vben pages remain authoritative.
- V4440 creates `promotion_seckill_config`, `promotion_seckill_activity`, and `promotion_seckill_product` with `(tenant_id,id)` ownership, explicit PostgreSQL sequences, tenant-qualified Product SPU/SKU and activity references, JSON/time/price/stock/limit/state constraints, and fail-closed adoption of pre-existing global tables.
- Tenant-aware triggers validate every configured time slot, keep product activity snapshots aligned, and reject deletion of a slot still used by an activity. `trade_order` gains a tenant-qualified seckill-activity reference and an order-type/activity consistency constraint.
- Menu IDs 70407051 reuse the existing activity and time-config pages with exactly nine controller permissions: five activity permissions and four configuration permissions.
- Service validation rejects duplicate SKUs, seckill prices above native SKU prices, stock above native SKU stock, invalid limit relationships, and unsafe stock restoration. Closing an activity transactionally disables its product snapshots; a used time configuration cannot be deleted.
- The Vben pages use the real backend field shapes and permissions, validate time/limit/product relationships, handle empty products and prices safely, use the correct `0=enabled` switch direction, and provide responsive forms.
## Legacy data decision
V4440 activates all three native tables empty. There is no source seckill state to import, reconcile, or retire. This ticket does not reinterpret ordinary products, coupons, referral campaigns, or aggregate orders as seckill activities.
## Verification
- All 51 PostgreSQL Flyway scenarios pass through V4440, including the focused table/sequence/constraint/trigger/menu shape and fail-closed adoption cases.
- A Spring/MyBatis PostgreSQL integration test imports the real native seckill services and production tenant SQL interceptor. Tenants 10 and 20 create the same numeric config/activity/product IDs; page and detail reads remain isolated; two concurrent attempts for the final unit produce exactly one success; restoration, close-state propagation, and used-slot deletion protection are verified.
- Promotion contract tests prove the three records participate in framework tenant isolation and activity/config/product requests reject invalid time, limit, SKU, price, stock, status, and slider-image inputs.
- The scoped Vben seckill files pass oxlint and oxfmt checks, and the complete `@vben/web-antd` typecheck passes.
- The non-clean Maven reactor install through Education succeeds across 26 required modules without stopping the running server.
## Explicitly open
- Combination, Bargain, Point, and any other special-order Promotion families require separate tenant-safe activation slices.
- Explicit legacy Product/coupon/order/refund/referral/settlement-proof import remains separate; no source seckill import is needed.
- Purchase-driven Education entitlement fulfillment, refund-driven entitlement revocation, configured Pay runtime, and deployed end-to-end seckill checkout evidence remain required for production acceptance.

View File

@@ -0,0 +1,39 @@
# EDU-033 — Activate native Promotion combination
- **Status:** done — native tenant-owned combination activities, SKU pricing, group records, Trade Order bridge, exact RBAC, corrected Vben UI, and PostgreSQL concurrency evidence delivered
- **Type:** native module takeover / special-order promotion / tenant isolation
- **Phase:** 5 / commercialization foundation
- **Blockers:** EDU-025, EDU-027, PostgreSQL Flyway
## Problem
RuoYi already owns the complete group-buying lifecycle: SPU/SKU activity authoring, head/member records, group capacity and expiry, Trade Order integration, app/admin APIs, Member and Product lookups, permissions, and Vben administration. Rebuilding it in Education would create a second promotion/order aggregate and bypass the native Product, Promotion, Trade, Member, tenant, and RBAC boundaries.
The source backend has no group-buying capability. Its only `combination` value appears in `docs/pb_schema.json` and `apps/api/src/features/tenant-content/imports.ts`, where it means an education combination-question type. It must not be interpreted as a promotion activity, product, order, or group record.
## Delivered contract
- `CombinationActivityDO`, `CombinationProductDO`, and `CombinationRecordDO` extend `TenantBaseDO`; native controllers, services, mappers, Product/Member/Trade APIs, jobs, app endpoints, and Vben pages remain authoritative.
- V4450 creates `promotion_combination_activity`, `promotion_combination_product`, and `promotion_combination_record` with `(tenant_id,id)` ownership, PostgreSQL sequences, tenant-qualified Product SPU/SKU/activity/order/head references, snapshot/time/price/limit/state constraints, and fail-closed adoption of pre-existing global tables.
- Product triggers require activity/SPU/SKU/status/time consistency and prevent a combination price above the native SKU price. Record and Trade triggers lock the head row, enforce capacity and activity/order/user/head consistency, allow the order-before-record bridge state, and reject deletion of an activity with group records.
- Menu IDs 70607067 reuse the existing activity and record pages with exactly six controller permissions: five activity permissions and one record query permission.
- Request and service validation reject blank/oversized names, invalid time and limit relationships, groups smaller than two, duplicate or mismatched SKUs, non-positive identifiers/prices/counts, prices above the native SKU price, and cross-activity parent groups. Closing an activity transactionally disables product snapshots; activity updates retain and refresh snapshot status/time.
- Head creation returns the persisted head-record ID rather than the `0` sentinel. Joining locks the head before validation/insertion, preventing concurrent over-capacity membership.
- The Vben activity and record pages use the backend VO shapes and combination-record dictionary, validate positive/time/limit/product inputs, handle missing prices/products, pass the correct head ID to the member dialog, expose exact permission guards, and use viewport-bounded dialogs and keyboard-operable showcase controls.
## Legacy data decision
V4450 activates the native tables empty. No legacy rows are imported or invented. Education combination questions remain Education content and are not mapped to Mall group buying; ordinary products and aggregate orders are likewise not reinterpreted.
## Verification
- All 52 PostgreSQL Flyway scenarios pass through V4450, including focused table/sequence/constraint/trigger/menu shape, order bridge, cross-tenant same-ID, and fail-closed adoption cases.
- A Spring/MyBatis PostgreSQL integration test imports the real native activity and record services plus the production tenant SQL interceptor. Tenants 10 and 20 create the same numeric activity/product IDs; page, record, and summary reads remain isolated; activity edits and closure propagate product snapshots; a head returns its real record ID; two concurrent members competing for the final place produce exactly one success; cross-activity joins and deletion with records fail closed; Trade Order/record/head references remain consistent.
- Promotion contract tests prove all three records participate in framework tenant isolation, request DTOs carry the required validation annotations and relationship checks, and the head response conversion never returns the sentinel.
- Scoped Vben combination files pass oxlint and oxfmt checks, and the complete `@vben/web-antd` typecheck passes.
## Explicitly open
- Bargain, Point, and any other special-order Promotion families require separate tenant-safe activation slices.
- Explicit legacy Product/coupon/order/refund/referral/settlement-proof import remains separate; no source combination import is needed.
- Purchase-driven Education entitlement fulfillment, refund-driven entitlement revocation, configured Pay runtime, virtual-group expiry job operations, and deployed end-to-end combination checkout evidence remain required for production acceptance.

View File

@@ -0,0 +1,82 @@
# Education Migration Tickets
This directory turns [`GOAL.md`](../GOAL.md) into executable, blocker-aware vertical slices.
## Workflow
1. Work only tickets whose blockers are complete.
2. Start each implementation ticket in a fresh context after reading `GOAL.md`, the ticket, relevant decisions, and current Git status.
3. Preserve the dirty working tree. Implementation is serial unless an isolated worktree and integration plan are explicit.
4. Apply TDD at the ticket's declared seams.
5. Use `flyway-postgresql` for every schema, index, constraint, seed, baseline, backfill, or Flyway configuration change.
6. Close with focused tests, `git diff --check`, and `mvn -pl yudao-server -am -DskipTests clean compile`.
7. Report exact commands and results. Never report PostgreSQL migration success without an actual successful PostgreSQL run.
## Status vocabulary
- `done`: implemented and verified to the ticket's current acceptance criteria.
- `in-progress`: currently being implemented.
- `ready`: all blockers complete and no unresolved decision prevents work.
- `blocked`: depends on another ticket or product decision.
- `decision`: produces a recorded decision rather than production behavior.
## Ticket graph
```text
EDU-000 Phase 0 artifacts done
├── EDU-001 Safe question content done, follow-up coverage remains
├── EDU-002 Practice regression baseline done
│ ├── EDU-016 PostgreSQL persistence tests done; temporary bridge blocked on EDU-005/EDU-006
│ └── EDU-006 Practice schema Flyway done
│ ├── EDU-007 Create/restore practice done
│ ├── EDU-008 Idempotent answer save done
│ └── EDU-009 Atomic submit/report done
├── EDU-003 Tenant resolution decision done
│ └── EDU-004 Tenant/identity security done; ingress/IP-only probing throttle remains operational blocker
└── EDU-005 PostgreSQL/Flyway takeover decision done
EDU-009 + provider/content decisions
└── EDU-010 Tenant content publication done (bounded JAVA_READ publication scope delivered)
└── EDU-011 Import/export/assets/scanning bounded capability delivered; production adapters/export artifacts deferred
EDU-004
└── EDU-012 Classes and education relationships implemented (bounded class/invitation capability)
Commerce ownership decisions
└── EDU-013 Education commercialization bounded entitlement/binding capability implemented; platform events deferred
All owner/contract decisions
└── EDU-014 Extended learning waves bounded representative wave delivered; blocked families explicitly deferred
└── EDU-015 Operational independence contracts implemented; release/deployment evidence remains
EDU-003 + tenant-admin inventory
└── EDU-017 Tenant appearance/theme bounded appearance/settings/theme lifecycle delivered; integrations/secrets/codes deferred
└── EDU-018 Native payment/social admin Pay/System controllers, RBAC, and Vben pages reused
├── EDU-019 Learning activation codes bounded generation/redemption, entitlement composition, and Vben page delivered
└── EDU-020 Native Pay activation Pay module plus tenant App/Channel schema and parent isolation delivered
└── EDU-021 Legacy Pay import explicit tenant-account mapping into native Pay with redacted audit delivered
└── EDU-022 Native Pay transactions tenant order/refund/notify ledgers and native UI delivered
└── EDU-023 Legacy Pay transactions reconciled terminal import and redacted audit delivered
└── EDU-024 Native Pay Transfer/Wallet tenant ledgers and native UI delivered
└── EDU-025 Native Mall Product tenant catalog and native UI delivered
└── EDU-026 Native Mall Promotion coupon templates/instances and native UI delivered
└── EDU-027 Native Mall Trade order core and native UI delivered
└── EDU-028 Native checkout discount/reward activities and native UI delivered
└── EDU-029 Native Trade delivery and native UI delivered
└── EDU-030 Native Trade after-sale and corrected native UI delivered
└── EDU-031 Native Trade brokerage and corrected native UI delivered
└── EDU-032 Native Promotion Seckill and corrected native UI delivered
└── EDU-033 Native Promotion Combination and corrected native UI delivered
```
## Recommended execution order
1. Complete production release evidence for **EDU-015** using the Pilot runbook and target-only `JAVA_READ` deployment.
2. Add deferred EDU-011 scanner/parser/export adapters only when their owning platform contracts and deployment are available.
3. Add automatic EDU-013 fulfillment only after Mall/Pay/CRM expose the recorded public events; keep manual trusted fulfillment and access fail-closed meanwhile.
4. EDU-025 activates native Product, EDU-026 activates coupons, EDU-027 activates the normal Trade order core, EDU-028 activates discount/reward, EDU-029 activates delivery, EDU-030 activates after-sale/Pay Refund, EDU-031 activates native brokerage, EDU-032 activates native Seckill, and EDU-033 activates native Combination activities, group records, atomic capacity, exact RBAC, and corrected administration pages. Continue with Bargain/Point and other Promotion families, source referral CRM/settlement-proof import, explicit legacy product/coupon/order/refund import, automatic Mall/Pay fulfillment, refund-driven entitlement revocation, production export/runbook evidence, legacy activation-code import, tenant PNVS, and encrypted generic secret storage as separate slices.
5. Select the next EDU-014 family only after its entitlement, privacy, and owner decisions are recorded.
## Phase 0 completion caveat
Phase 0 artifacts exist, but several architecture and product decisions remain open. `EDU-000` is considered complete as an inventory deliverable, not as resolution of every decision it discovered.

View File

@@ -0,0 +1,140 @@
# Education Pilot 验收与回滚手册
## 1. 范围
本文覆盖学生核心学习闭环后端的 Pilot 发布、验证、监控和应用回滚。完整 Student Web/H5 源码当前不在本工作区,因此浏览器 E2E、桌面/H5 截图和前端构建验收仍是明确阻塞项,不能以 HTTP 或单元测试替代。
## 2. Pilot 配置
```yaml
yudao:
education:
enabled: true
catalog-read-enabled: true
practice-write-enabled: true
pilot-tenant-ids: [<pilot-tenant-id>]
catalog-mode: JAVA_READ
scalar:
enabled: false
owner: <required-only-when-scalar-read>
exit-date: <yyyy-MM-dd>
```
要求:
- `pilot-tenant-ids` 在 Pilot 环境必须显式配置,不能使用空列表。
- Pilot 默认以 `JAVA_READ` 启动,只依赖目标 PostgreSQL不得启动旧 NestJS API、旧 Worker、Supabase 或旧资产扫描服务作为前置条件。
- 仅在有明确负责人、告警、故障策略和退出日期的兼容窗口内切换 `SCALAR_READ`。Scalar token 只能通过密钥管理或环境变量注入。
- 发布前调用管理端 `/admin-api/education/capability``/admin-api/education/operations/health`;后者必须显示 `java-read-postgresql=UP``scalar-catalog=NOT_SELECTED`
## 3. 发布步骤
1. 备份 Education 相关表,并记录应用版本与 `flyway_schema_history`
2. 由数据库管理员预置彼此独立的 Flyway owner LOGIN role 与 runtime LOGIN role。Flyway role 拥有目标 schemaruntime role 只获得业务表所需权限,且不得拥有、继承所有者角色或写入 `education_question_lifecycle_transition_token``education_content_node_lifecycle_transition_token``education_question_collection_lifecycle_transition_token``education_question_collection_membership_token`。角色/密码不由 migration 创建。
3. 显式注入 `FLYWAY_USER``FLYWAY_PASSWORD`(不得回退到 master datasource 账号),使用 Server 配置的 PostgreSQL Flyway 执行 migrate 和 validate检查版本、脚本、checksum、success 以及四张 token 表 owner 均为 Flyway role。
4. 以 runtime datasource 账号验证四张 token 表均无 INSERT/UPDATE/DELETE/TRUNCATE随后启动应用同角色、继承 owner 或可写授权会导致 Education 启动检查 fail closed。
5. 先以 `catalog-read-enabled=false``practice-write-enabled=false` 部署应用。
6. 验证 System、Infra、Member 基础 smoke。
7. 仅对 Pilot 租户开启题库读取,完成 JAVA_READ 只读 smoke并运行 `tools/education-target-smoke/java-read-readiness.sh`
8. 对 Pilot 租户开启练习写入,完成会话、答案、交卷、报告、错题和收藏 smoke。
9. 若部署 Worker 或 Scanner先确认其持续写入 `education_operational_component`,且 `/admin-api/education/operations/health``DOWN` 组件和未处理死信。
10. 观察错误率、延迟和数据库写入后再扩大租户列表。
## 4. Smoke 清单
### 基础与身份
- [ ] 非 Pilot 租户访问题库和练习写入被拒绝。
- [ ] Pilot 租户可完成 tenant resolve、Member 登录、refresh、logout 和 Education context。
- [ ] 错误 `tenant-id` 被租户安全过滤器拒绝。
### 核心闭环
- [ ] 目录及题目只经 RuoYi API 返回,响应不含答案或解析。
- [ ] 创建练习后刷新可恢复相同会话、题序和已保存答案。
- [ ] 相同答案幂等键重试返回首次结果;旧版本和旧序号被拒绝。
- [ ] 交卷只生成一个报告,交卷后答案不可修改。
- [ ] 错题投影、错题复习和收藏操作仅对当前学生可见。
### 隔离
- [ ] tenant A / student A 不能读取或修改 tenant A / student B 的记录。
- [ ] tenant A 不能读取或修改 tenant B 的记录,即使资源 ID 被猜中。
- [ ] 对 session、report、wrong question、favorite 分别留存拒绝结果证据。
## 5. 故障与回滚
### JAVA_READ / PostgreSQL 故障
1. 设置 `catalog-read-enabled=false`,停止新的目录和题目读取;不得静默切回 Scalar。
2. 保持 `enabled=true`,使已有会话、报告、错题和收藏仍可访问。
3. 检查 `/admin-api/education/operations/health``java-read-postgresql` 结果和 Flyway 历史。
4. 如需冻结新写入,再设置 `practice-write-enabled=false`
5. 通过应用回滚或更高版本 Flyway 前滚修复,不执行 `flyway clean` 或手工回滚 SQL。
### Scalar 兼容窗口故障
仅当部署明确选择 `SCALAR_READ` 时适用:
1. 设置 `catalog-read-enabled=false`,停止新的 Scalar 读取。
2. 保持 `enabled=true`,使已有会话、报告、错题和收藏仍可访问。
3. 如需冻结新写入,再设置 `practice-write-enabled=false`
4. 验证 Education PostgreSQL 表行数和历史查询均未减少。
### 练习写入熔断
设置 `practice-write-enabled=false` 后:
- 新建练习、保存答案和交卷必须被拒绝;
- 当前会话恢复、指定会话读取、报告和报告历史仍应可读;
- 不执行清理、归档或 rollback SQL。
### 应用回滚
1. 将应用回滚到上一已验证版本。
2. 保留所有 Education 表和数据,不执行 `flyway clean`、手工删除或任何 `*-rollback.sql`
3. 若旧版本与新 schema 不兼容,保持功能关闭并通过更高版本 Flyway migration 前滚修复;不得通过删表恢复服务。
4. 重新验证 Member 登录、System 租户和 Infra 日志功能。
> 历史 `*-rollback.sql` 是数据销毁工具且不属于当前交付机制,不是常规应用版本回滚步骤。
## 6. 可观测性
发布窗口至少观察:
- `/admin-api/education/operations/health` 的必需依赖、Worker/Scanner 心跳和 open dead-letter 数;
- `education_operational_component``last_heartbeat_at`、最后成功/失败和 backlog
- `education_dead_letter` 仅保留负载指纹与脱敏错误分类,不得保存业务 payload、凭据或个人数据
- Scalar 兼容模式请求成功率、4xx/5xx/timeout、P95/P99 延迟;
- 练习创建成功/冲突数;
- 答案保存成功、幂等重放、版本冲突和旧序号拒绝数;
- 交卷成功、并发冲突和事务失败数;
- Pilot 租户拒绝数;
- JVM、数据库连接池、HTTP 错误率和接口延迟。
Scalar 日志只能记录脱敏路径、tenant ID、上游 request ID、状态、耗时和错误分类不得记录 Authorization、Scalar token、学生答案、正确答案或完整响应体。RuoYi access/error log 中的 trace ID 用于关联入口请求;验收时需保存一条从入口日志到 Scalar request ID 的关联证据。
### Worker、Scanner 与死信处置
1. Worker/Scanner 每次心跳使用固定 `component_key` upsert部署实例变化写入 `instance_id`
2. 心跳状态只能为 `STARTING/UP/DEGRADED/DOWN``detail` 必须脱敏且有界。
3. 重试耗尽后写入 `education_dead_letter`同一租户、组件、workload 只允许一个 OPEN 记录。
4. 排障后先把原 OPEN 记录标记为 `REQUEUED` 并填写 `resolved_at`/`resolution_note`,再通过所属业务服务重入队;禁止直接修改业务结果或把原 payload 写入死信表。
5. 未部署对应 Worker/Scanner 时不得伪造 UP 心跳;能力清单应保持未交付状态。
## 7. 验证命令
```bash
mvn -pl yudao-module-education -am test
mvn -pl yudao-server -am package -DskipTests
```
前端源码归位后还必须执行其 lint、类型检查、测试、生产构建及浏览器 E2E。
## 8. 已知限制
- 当前工作区缺少完整 Student Web/H5 前端源码。
- 尚不能在本仓库完成浏览器 Network 无直连 Scalar 断言。
- 尚不能完成桌面和 H5 视觉截图对比。
- 真实 Scalar smoke 依赖部署环境、固定上游版本和有效只读凭据。
- Pilot 租户列表属于部署配置,修改后需要按配置刷新机制重新加载或重启应用。

View File

@@ -0,0 +1,225 @@
# Scalar (tiku-backend) 接口契约 —— 从源码提取
日期2026-07-28
来源:`/Users/tiku1/code/tiku-backend` 仓库 NestJS Controller、DTO 和装饰器
状态:源码级契约冻结,真实 endpoint 验证需启动完整运行时Supabase + API server
## 运行时信息
- 框架NestJS + Fastify
- OpenAPI 路径:`/openapi.json`(仅非生产环境)
- Scalar 文档:`/docs`(仅非生产环境)
- 认证Bearer TokenJWT+ `x-tenant-id` header
- 响应 envelope`{ items/item, meta: { requestId } }`
## RuoYi Education 实际调用的路径
以下为 RuoYi `ScalarCatalogProvider` 使用的只读 GET 路径:
### 1. GET /api/catalog/regions
- **安全方案**: `x-tenant-id` header (`@ApiSecurity('tenant-id')`, `@TenantAccess()`)
- **查询参数**: `regionId?` (UUID, 可选)
- **响应**: `{ items: CatalogRegionResponseDto[], meta: { requestId } }`
- **CatalogRegionResponseDto 字段**:
- `id` (uuid, 必填)
- `legacyId` (string, nullable)
- `name` (string, 必填)
- `code` (string, nullable)
- `shortName` (string, nullable)
- `fullName` (string, nullable)
- `icon` (string, nullable)
- `pinyin` (string, nullable)
- `isHot` (boolean, 必填)
- `isActive` (boolean, 必填)
- `order` (number, 必填)
### 2. GET /api/catalog/region-modules
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?` (UUID)
- **响应**: `{ items: CatalogRegionModuleResponseDto[], meta }`
- **字段**: id, legacyId, regionId, name, type, icon, color, textColor, description, route, isPrimarySchoolModule, isActive, order
### 3. GET /api/catalog/module-nodes
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `moduleId?` (UUID)
- `parentId?` (string, "root" 表示根节点)
- **响应**: `{ items: CatalogEntityDto[], meta }` — 通用实体列表
### 4. GET /api/catalog/schools
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`
- **响应**: `{ items: CatalogSchoolResponseDto[], meta }`
- **字段**: id, legacyId, regionId, moduleId, name, professionalExamDate, metadata, createdAt, updatedAt
### 5. GET /api/catalog/majors
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`, `majorId?`, `moduleId?`, `type?`
- **响应**: `{ items: CatalogMajorResponseDto[], meta }`
### 6. GET /api/catalog/subjects
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`, `majorId?` (UUID), `moduleId?` (UUID), `type?` (string)
- **响应**: `{ items: CatalogEntityDto[], meta }`
### 7. GET /api/catalog/categories
- **安全方案**: `x-tenant-id`
- **查询参数**: `subjectId?` (UUID), `nodeId?` (UUID, 旧导航节点)
- **响应**: `{ items: CatalogEntityDto[], meta }`
### 8. GET /api/catalog/questions
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `subjectId?` (UUID)
- `categoryId?` (UUID)
- `nodeId?` (UUID, 旧导航节点)
- `entryId?` (UUID)
- `contentNodeId?` (UUID)
- `collectionId?` (UUID)
- `questionIds?` (string | string[], 逗号分隔或重复传参)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionResponseDto[], meta }`
- **QuestionResponseDto 字段** (extends CatalogEntityDto):
- `id` (uuid)
- `type` (string, 必填 — 题型)
- `typeLabel` (string, nullable)
- `difficulty` (number, nullable)
- `content` (unknown, 题干)
- `options` (array, 选项列表)
- `explanation` (string, nullable — **敏感字段**)
- `hasVideoExplanation` (boolean)
- 继承字段: legacyId, name, title, regionId, order, isActive, description, metadata
- **注意**: `options` 中包含正确选项标记、`explanation` 包含答案解析。RuoYi 在返回学生端 DTO 前必须剥离这些字段。
### 9. GET /api/catalog/content-entries
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `entryType?` (string)
- `includeHidden?` (boolean, default false)
- **响应**: `{ items: ContentEntryResponseDto[], meta }`
### 10. GET /api/catalog/content-nodes
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `entryId` (UUID, **必填**)
- `parentId?` (string, "root" 表示根节点)
- `mode?` ('children' | 'flat', default 'children')
- `includeInactive?` (boolean, default false)
- `markerType?` (string)
- **响应**: `{ items: ContentNodeResponseDto[], meta }`
### 11. GET /api/catalog/question-collections
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `entryId?` (UUID)
- `nodeId?` (UUID)
- `collectionType?` (string)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionCollectionResponseDto[], meta }`
### 12. GET /api/catalog/question-collections/questions
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `collectionId` (UUID, **必填**)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionResponseDto[], meta }`
### 13. GET /api/catalog/practice-blueprints
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `entryId?` (UUID)
- `nodeId?` (UUID)
- `collectionId?` (UUID)
- `mode?` (string)
- `limit?` (int, 1-2000)
- **响应**: `{ items: PracticeBlueprintResponseDto[], meta }`
- **字段**: id, mode, entryId, nodeId, collectionId, questionLimit, durationMinutes + CatalogEntityDto 继承字段
## 认证机制
### 租户识别
- 所有 catalog 路径使用 `@TenantAccess()` 装饰器 → `AccessPolicy { kind: 'tenant' }`
- 租户 ID 从 `x-tenant-id` 请求头提取CORS 白名单包含此头)
- `Principal` 装饰器从请求上下文提取 `principal.tenant.tenantId`
### Bearer Token
- catalog 的大多数端点不需要 Bearer只读、租户级访问
- `assets``assets/download``assets/preview` 需要 `@ApiBearerAuth()`
- 学习写入路径 (`/api/learning/*`) 需要 `@ApiBearerAuth()` + `@TenantUserAccess()`
## 响应格式
### 成功
```json
{
"items": [...],
"meta": { "requestId": "uuid" }
}
```
```json
{
"item": {...},
"meta": { "requestId": "uuid" }
}
```
### 错误
```json
{
"error": "面向调用方的错误信息",
"code": "REQUIRED_FIELD",
"requestId": "uuid",
"meta": { "requestId": "uuid" }
}
```
## 与 RuoYi adapter 的差异
| 项目 | RuoYi (Java) 假设 | Scalar (tiku-backend) 实际 |
|------|-------------------|---------------------------|
| 基础路径 | 配置的 `base-url` | `/api/catalog/*` |
| 认证头 | `Authorization: Bearer <token>` | 大多数 catalog 端点只需 `x-tenant-id`,不需要 Bearer |
| 租户头 | `x-tenant-id` | `x-tenant-id` ✅ 一致 |
| 分页 | `page` + `pageSize` | `limit` (1-2000),无 page 参数! |
| 题目过滤 | `published=true&hidden=false` 由 RuoYi 追加 | Scalar 端已有 `isActive` 过滤,但无 `published`/`hidden` query 参数 |
| 响应 envelope | 预期 `items` + 可能的 `total` | `items` + `meta.requestId`,无 `total` 字段! |
| 正确答案 | RuoYi 在返回学生端前剥离 | `QuestionResponseDto.options` 包含正确选项标记 |
## ⚠️ 关键差异
1. **分页**: RuoYi `ScalarCatalogProvider` 使用 `page` + `pageSize` query 参数,但 Scalar 只接受 `limit`。Java 端第 526-540 行固定追加 `page``pageSize` —— 这些参数在 Scalar controller 中不存在,会被忽略。
2. **total 字段**: RuoYi adapter 期望服务端返回 `total` 用于分页,但 Scalar 响应没有此字段。如果 RuoYi 依赖 `total` 做前端分页计算,需要确认 adapter 如何处理。
3. **published/hidden**: RuoYi 端固定追加 `published=true&hidden=false`,但这些参数在 Scalar controller DTO 中未定义。Scalar 的过滤逻辑在 repository 层而非 query 参数层。
## OpenAPI 生成方式
```bash
# 需要 Docker + Supabase 运行
cd tiku-backend
npm run supabase:start
npm run dev:api
curl http://127.0.0.1:8787/openapi.json > /tmp/tiku-openapi.json
# 或通过测试套件(也会启动真实服务器)
BACKEND_TEST_SKIP_DATABASE=true npm run test:backend:migration
# 生成文件:/tmp/tiku-openapi.json
```
当前环境不具备 Supabase/Docker无法生成运行时 OpenAPI JSON。源码级契约已在此文档冻结。

View File

@@ -0,0 +1,216 @@
# 恭学教育学生核心学习闭环 PRD
## Problem Statement
当前恭学教育系统基于 RuoYi-Vue-Pro已经具备成熟的租户、后台用户、会员、鉴权、支付、文件、短信、邮件、站内信、权限、字典、定时任务和审计基础设施但仓库内尚无生产级教育/题库/学习模块,完整前端源码也尚未纳入当前工作区。
另一个已经运行的 Scalar API 提供了题库、练习、资料、视频、会员和运营等大量教育接口;用户同时提供了学生学习中心、租户运营后台、平台管理后台的功能原型和效果图。若前端直接接入 Scalar或在 RuoYi 中再次独立实现身份、会员、支付等基础能力,会形成双鉴权、双租户、双订单和双数据源,造成权限不一致、数据难迁移、跨租户风险以及长期维护成本。
用户首先需要一个能够真实上线和验证的学生学习核心闭环:学生在正确租户下使用现有账号登录,浏览题库,创建练习,稳定保存答案,提交试卷,查看报告,并继续使用错题本和收藏夹。该闭环需要以 RuoYi 为统一入口和最终数据权威,同时允许尚未迁移的只读题库内容暂时经后端适配层来自 Scalar。实现还必须为后续会员支付、私有资料、视频、租户运营后台和平台治理留出清晰边界但不能让这些后续范围阻塞第一阶段交付。
## Solution
在 RuoYi-Vue-Pro 中新增独立的 Education 业务模块,以 RuoYi 作为所有前端请求、身份、租户、个人学习数据和未来支付权益的统一边界。学生 Web/H5 和 Vue 3 管理后台只能调用 RuoYi API不得直接访问 Scalar。
第一阶段交付以下纵向学习闭环:
1. 根据访问域名或受控租户参数识别租户。
2. 复用现有 Member 登录、短信登录、令牌刷新和退出能力。
3. 通过 Education 内部目录/题目接口读取题库;尚未迁移的数据由服务器端 Scalar 防腐适配层转换。
4. 在 RuoYi/MySQL 中创建归属于当前学生和租户的练习会话,并固定题目顺序与版本。
5. 使用幂等键、客户端序号和服务端版本安全地自动保存答案,支持刷新、断网和请求重试恢复。
6. 以原子状态转换提交试卷,保存稳定的评分结果和历史快照。
7. 生成练习报告、错题记录、收藏和基础学习进度。
8. 通过一个最高层的学生核心闭环 E2E 接缝验收整体行为,并使用较低层测试补足租户隔离、所有权、幂等、并发和 Scalar 契约等不可完全由单条 E2E 覆盖的风险。
后续阶段在同一模块边界内扩展个人中心、词汇、手册、分数线、AI 推荐、资料、视频、会员支付、权益、租户运营和平台治理,并逐项把 Scalar 内容迁移到 Java/MySQL。
## User Stories
1. As a student, I want the application to identify the correct school or tenant from my entry point, so that I enter the right branded learning environment.
2. As a student, I want a clear error when no valid tenant can be resolved, so that I do not accidentally sign in to the wrong organization.
3. As a student, I want to be blocked when a tenant is disabled, so that the platform does not expose inactive tenant data.
4. As a student, I want to sign in with my existing mobile number and password, so that I do not need a separate education account.
5. As a student, I want to sign in with an SMS verification code, so that I can recover access without remembering a password.
6. As a student, I want supported social or WeChat login methods to keep working, so that education does not replace the platforms existing authentication options.
7. As a student, I want my session to refresh securely, so that a long learning session is not lost when an access token expires.
8. As a student, I want to log out from the education application, so that another person using the device cannot access my learning data.
9. As a student, I want to return to the page I originally requested after login, so that authentication does not interrupt my intended task.
10. As a student, I want the application to display my existing nickname and avatar, so that my education profile is consistent with my member account.
11. As a student, I want the application to preserve the tenant context after login, so that subsequent requests cannot drift into another tenant.
12. As a student, I want to see a learning home page with a clear entry into the question bank, so that I can start studying quickly.
13. As a student, I want to resume an unfinished practice session from the learning home page, so that a refresh or temporary interruption does not discard my work.
14. As a student, I want to browse question banks by subject, category, region, major, or other supported catalog dimensions, so that I can find relevant material.
15. As a student, I want catalog filters to preserve their selected state while I navigate, so that I can compare and refine content efficiently.
16. As a student, I want clear loading, empty, unavailable, and permission-denied states in the catalog, so that I understand why content is not displayed.
17. As a student, I want only published and permitted question banks to appear, so that I do not see draft or unauthorized content.
18. As a student, I want question counts and practice configuration to be accurate, so that I understand what will be included before starting.
19. As a student, I want to create a practice session from selected criteria, so that the server prepares a stable set of questions for me.
20. As a student, I want the question order to remain stable throughout a practice session, so that refreshing does not reorder my work.
21. As a student, I want historical practice to preserve the version of each question I answered, so that later question edits do not change my old result.
22. As a student, I want question content to render correctly on desktop and mobile widths, so that I can learn on either device.
23. As a student, I want formulas and rich question content to render correctly, so that mathematical and technical questions remain understandable.
24. As a student, I want answer options to be easy to select using touch or mouse, so that answering is efficient and accessible.
25. As a student, I want to move to the previous or next question, so that I can navigate the practice naturally.
26. As a student, I want an answer-card overview, so that I can see answered, unanswered, and current questions.
27. As a student, I want my answer to save automatically, so that I do not lose progress if I leave the page unexpectedly.
28. As a student, I want to see whether an answer is saving, saved, retrying, or failed, so that I know whether my progress is safe.
29. As a student, I want a failed autosave to retry safely, so that network instability does not create duplicate or corrupted answers.
30. As a student, I want an older delayed save request to be rejected rather than overwrite my newer answer, so that request reordering cannot corrupt progress.
31. As a student, I want refreshing the page to restore the latest server-accepted answers, so that the server remains the durable source of truth.
32. As a student, I want duplicate clicks or requests to have one effective result, so that accidental repetition does not change my practice incorrectly.
33. As a student, I want to be prevented from answering a submitted, expired, cancelled, or foreign session, so that session state remains trustworthy.
34. As a student, I want correct answers and explanations hidden before submission, so that the practice cannot be cheated through API inspection.
35. As a student, I want a confirmation before final submission when unanswered questions remain, so that I can choose whether to review them.
36. As a student, I want submitting a practice session to be atomic, so that I never receive a partially scored report.
37. As a student, I want repeated submission after a timeout to return the original result, so that I do not create duplicate reports.
38. As a student, I want a clear score, correct count, incorrect count, and completion summary after submission, so that I understand my performance.
39. As a student, I want question-level result details after submission, so that I can learn from mistakes.
40. As a student, I want permitted explanations to appear after submission, so that I can understand the correct reasoning.
41. As a student, I want my practice history ordered and paginated, so that I can revisit previous work.
42. As a student, I want a report to remain stable even if an administrator later edits a question, so that historical records are auditable.
43. As a student, I want incorrectly answered questions added to my wrong-question book, so that I can focus future review.
44. As a student, I want repeated mistakes on the same question to increase its error count rather than create duplicate rows, so that the wrong-question book remains useful.
45. As a student, I want to mark a wrong question as mastered without deleting its history, so that progress remains visible.
46. As a student, I want to create a review practice from wrong questions, so that I can close knowledge gaps.
47. As a student, I want to favorite a question, so that I can return to important material later.
48. As a student, I want favoriting the same question repeatedly to remain idempotent, so that duplicate actions do not create duplicate records.
49. As a student, I want to remove a favorite, so that my collection remains relevant.
50. As a student, I want wrong questions and favorites to be paginated and filterable, so that large collections remain manageable.
51. As a student, I want another student to be unable to read or mutate my sessions, reports, wrong questions, or favorites, so that my learning data remains private.
52. As a student, I want another tenant to be unable to access my tenants private question banks or learning records, so that organizations remain isolated.
53. As a student, I want a traceable support reference when an upstream content service fails, so that support can investigate without exposing sensitive details.
54. As a tenant operator, I want student authentication to reuse the platforms member system, so that I do not manage duplicate accounts.
55. As a tenant operator, I want education data automatically scoped to my tenant, so that I cannot accidentally view another tenants students or content.
56. As a tenant operator, I want permission-controlled access to future education management screens, so that roles can be assigned through the existing menu and role system.
57. As a tenant operator, I want student learning reports to be based on immutable practice snapshots, so that supervision data remains trustworthy.
58. As a tenant operator, I want education actions to appear in existing access, error, and operation logs, so that incidents can be investigated centrally.
59. As a platform operator, I want public and tenant-owned content represented explicitly, so that public sharing does not require disabling tenant isolation globally.
60. As a platform operator, I want Scalar-backed capabilities to be visible through configuration and metrics, so that migration progress and dependency risk are measurable.
61. As a platform operator, I want to enable the new learning flow for pilot tenants first, so that production risk is contained.
62. As a platform operator, I want independent feature switches for catalog reads, practice creation, payments, private media, and imports, so that failures can be isolated.
63. As a platform operator, I want rollback to preserve practice history and idempotency records, so that deployment rollback does not lose student work.
64. As a support engineer, I want requests correlated by request or trace ID across RuoYi and Scalar, so that cross-system failures are diagnosable.
65. As a support engineer, I want logs to exclude tokens, phone numbers, correct answers, payment secrets, and signed URLs, so that observability does not create a data leak.
66. As a developer, I want one internal education contract independent of Scalar DTOs, so that the external provider can be changed or retired safely.
67. As a developer, I want Scalar errors translated consistently rather than converted to successful empty data, so that frontend and monitoring behavior is honest.
68. As a developer, I want contract tests for the Scalar envelope and errors, so that upstream changes fail before deployment.
69. As a developer, I want all personal learning writes to go directly to Java/MySQL, so that there is no dual-write reconciliation problem.
70. As a developer, I want existing member, tenant, permission, file, notification, and later payment APIs reused, so that the education module remains focused on education behavior.
71. As a developer, I want the education module to expose narrow module APIs, so that other modules do not import its mappers or data objects.
72. As a developer, I want schema changes delivered as ordered, reversible or explicitly non-reversible scripts, so that database releases can be operated safely.
73. As a QA engineer, I want one high-level E2E scenario to cover the entire student core loop, so that the released experience is tested from the users perspective.
74. As a QA engineer, I want targeted integration tests for tenant isolation, ownership, idempotency, concurrency, and adapter behavior, so that security and consistency failures are exercised deterministically.
75. As a product owner, I want the first release limited to the student core learning loop, so that value can be validated before building every prototype screen.
76. As a product owner, I want later membership, payment, private media, tenant operations, and platform governance to fit the same architecture, so that the first release does not become a dead end.
77. As a product owner, I want visual acceptance against the supplied concept images on desktop and H5 widths, so that functional completion also meets the intended experience.
78. As a product owner, I want incomplete future features clearly labeled rather than represented with mock data, so that release status is transparent.
## Implementation Decisions
- RuoYi is the unified application boundary and final source of truth. Frontends will not call Scalar directly.
- A new Education business module will own education-specific behavior and data. It will follow the repositorys controller, service, conversion, data-object, mapper, enum, job, and module-API conventions.
- The existing Member module will own student credentials, login, token refresh, logout, mobile number, nickname, avatar, level, points, tags, and other generic member data. Education-specific profile data will reference the member ID instead of duplicating account fields.
- The existing System module will own tenant administration, admin users, roles, menus, permissions, dictionaries, configuration, notifications, email, SMS, and audit facilities.
- The existing Infra module will own file records and storage. Education will own the authorization decision for paid or private resources.
- The Pay module will remain disabled during the first student-core release and will be activated in a later payment slice. Education orders and entitlements will be projections linked to Pay orders rather than an independent payment engine.
- The first release will activate Member and Education in the Maven reactor and server. Unrelated modules will remain disabled to limit build and runtime scope.
- The Scalar integration will be a server-side anti-corruption layer. External DTOs, enum values, pagination, errors, timestamps, identifiers, and metadata will be converted to internal education contracts before reaching services or controllers.
- Scalar will initially provide only explicitly approved read-only content capabilities. Student practice sessions, answers, reports, wrong questions, favorites, progress, future orders, entitlements, and private-resource decisions will never be written to Scalar.
- Each capability will have an explicit source state such as `SCALAR_READ`, `JAVA_NATIVE`, or `MIGRATED`. The system will not silently fall back between providers.
- Scalar failures will be mapped to explicit domain errors. An unavailable upstream must not appear as an empty successful catalog.
- Scalar requests will receive tenant context derived from the authenticated server context. The frontend cannot override authorization, tenant, user, or platform identity headers.
- Scalar authentication will use an approved server credential or token-exchange mechanism. Forwarding a frontend token is not permitted unless the frozen contract explicitly requires it and it passes security review.
- The public student API will use the repositorys existing app API conventions, standard success envelope, and page representation. A compatibility facade may preserve `/api` paths if the restored frontend requires them, but it will delegate to the same services rather than duplicate logic.
- Student IDs and tenant IDs for protected resources will be derived from the security context. Request-body user or tenant IDs will not be trusted.
- Tenant-scoped education data will use the platforms tenant-aware base object and database interceptor by default.
- Public content will use an explicit ownership scope or public marker. It will not be implemented by broadly disabling the tenant interceptor.
- Any tenant bypass will be isolated to a narrow platform service, documented, permission-protected, and covered by cross-tenant tests.
- The initial content model will include question banks, hierarchical catalog nodes, questions, options, source identifiers, publication state, content versions, and appropriate tenant-aware indexes.
- Correct answers and explanations will be treated as protected fields. Pre-submission student DTOs will not contain them.
- A practice session will belong to one tenant and one member. It will include a client-generated session identifier, lifecycle state, content selection, question count, score, timestamps, and a concurrency version.
- Starting a practice will freeze the question sequence and version. The system will retain enough snapshot data to keep historical reports stable after content changes.
- Answer autosave will require an idempotency key, a client command sequence, and the latest known server session version.
- Replaying the same idempotency key with the same request will return the original result. Reusing it for a different payload will return a conflict.
- Stale sequence or version updates will be rejected instead of overwriting newer accepted answers.
- Session submission will be an atomic, one-way state transition. Retrying a successfully committed submission will return the original result.
- Session ownership and active state will be checked in the service layer even when a controller is authenticated.
- Wrong questions will use one record per tenant, student, and question, with accumulated error count and mastery state. Marking as mastered will not erase history.
- Favorites will use one record per tenant, student, target type, and target ID and will support idempotent add/remove behavior.
- Basic learning progress will be stored as reliable aggregates. Expensive trends and summaries may later be calculated asynchronously through the existing job system.
- External resource mappings will preserve provider, external resource type, external ID, local ID, source version, synchronization state, and last synchronization time.
- Import and synchronization operations will use durable jobs and issue records rather than executing large migrations in a web request.
- Database changes will be delivered as ordered education SQL scripts with preconditions, verification queries, rollback SQL where safe, explicit rollback limitations, and lock-impact notes. The project will not pretend that Flyway or Liquibase exists when it does not.
- Permission names will follow the established `education:<resource>:<action>` pattern and will be seeded with menus and dictionaries rather than hardcoded only in the frontend.
- Stable business state machines will use Java enums and centralized transition validation. Dictionaries will provide configurable display values.
- Existing notification templates, mail accounts, SMS services, and in-app notification services will be reused. Education services will provide template codes and parameters rather than implementing a second delivery engine.
- Existing API access logs, API error logs, operation logs, login logs, and job logs will be reused. Education will add domain records only where business history must survive general log retention.
- Private media will not rely on the generic public and tenant-ignored file download route. A future Education access endpoint will authenticate the caller, validate tenant and resource state, check entitlement or operator permission, issue a short-lived URL, and audit the decision.
- The administration frontend will use the restored Vue 3 and Element Plus codebase and its existing request, route, store, permission, layout, form, table, pagination, upload, and theme conventions.
- The student frontend will be a responsive Web/H5 experience using the restored production frontend baseline. The static prototype is an acceptance reference, not a replacement architecture.
- The first release will cover tenant resolution, authentication shell, question-bank browsing, practice creation, answer autosave and recovery, submission, report, history, wrong questions, and favorites.
- Vocabulary, handbook, scorelines, AI recommendations, resources, videos, messages, growth, membership, payments, entitlements, tenant operations, platform governance, and full Scalar retirement will be implemented as later vertical slices.
- Feature flags will independently control Scalar catalog reads, Java content reads, practice creation, future payments, private media, imports, and frontend route exposure.
- Initial production rollout will use a pilot tenant. The release sequence will expand schema first, deploy disabled code, verify existing modules, enable read paths, then enable learning writes.
- Rollback will preserve practice history, reports, idempotency records, future orders, and entitlements. User-specific data will never roll back to Scalar.
- Observability will include request/trace ID, tenant, actor, use case, provider, upstream request ID, endpoint, latency, result, practice session, future order/import job, and authorization decision. Sensitive values will be redacted.
- The complete frontend sources, exact commits, machine-readable Scalar OpenAPI contract, production database version, Scalar availability expectations, and stable external identifier semantics are prerequisites to implementation.
## Testing Decisions
- Tests will assert externally observable behavior rather than private method calls, mapper invocation counts, or implementation-specific object construction.
- The primary acceptance seam will be one browser-level student core-loop E2E: resolve tenant, authenticate, browse a question bank, create a practice, save answers, refresh and recover, retry one simulated failed save, submit, inspect the report, and visit wrong questions and favorites.
- The E2E will also assert that browser network traffic contains no direct request to Scalar.
- The E2E will run at both representative desktop and H5 viewport sizes and capture key screenshots for comparison with the supplied concepts.
- Authentication tests will reuse the highest existing authentication seams: login endpoints, refresh, logout, and current-member behavior. Education will not unit-test the internals of Member authentication.
- Tenant isolation tests will create at least two tenants and overlapping-looking resource identifiers. They will assert that cross-tenant catalog, session, report, wrong-question, favorite, and future media access is denied.
- Ownership tests will create at least two students in one tenant and assert that one student cannot read, update, submit, or replay another students practice.
- Scalar adapter contract tests will cover single-item and paginated envelopes, request metadata, missing optional fields, additional fields, malformed required fields, 400, 401, 403, 404, 409, 429, timeout, and 5xx behavior.
- Scalar adapter tests will assert that failures are not converted to empty successes and that sensitive headers are not accepted from callers.
- Practice creation tests will assert stable question order, content version retention, ownership, tenant scope, and idempotent handling of a repeated client session identifier.
- Autosave tests will assert normal save, identical replay, payload mismatch conflict, stale sequence rejection, stale server-version rejection, delayed request ordering, refresh recovery, inactive-session rejection, and cross-user rejection.
- Submission tests will assert atomic scoring, unanswered questions, repeated submission, a timeout after commit, content edits after session creation, and stable historical reports.
- Wrong-question tests will assert unique upsert behavior, accumulated error count, mastery without history deletion, and review selection.
- Favorite tests will assert idempotent add, idempotent remove, tenant and owner filtering, and pagination.
- Response-security tests will assert that pre-submission DTOs and error logs do not contain correct answers or explanations.
- Logging tests will focus on the observable presence of correlation fields and absence of secrets, not exact log-line formatting.
- Database tests will follow the projects existing Spring and database test foundations and test real constraints for unique tenant/source mappings, sessions, answers, wrong questions, favorites, and idempotency records.
- Build verification will include the Education module with dependencies, the server package with activated Member/Education modules, and the restored frontends actual lint, type-check, test, and production build commands.
- Smoke tests against the real Scalar deployment will be read-only and version-pinned. They will run before enabling an adapter-backed feature in a target environment.
- Release verification will check existing System, Infra, and Member behavior for regressions before enabling any Education feature flag.
- Future payment tests will cover duplicate provider callbacks, status polling, browser return URLs that disagree with server state, refund replay, entitlement projection, and refund-access semantics.
- Future private-media tests will cover unauthenticated requests, wrong tenant, wrong student, expired entitlement, unpublished asset, short-lived URL generation, and audit records.
- Test fixtures will not contain production tokens, real student personal data, provider secrets, or licensed content not approved for test storage.
## Out of Scope
- Implementing all 18 student screens in the first release.
- Implementing all 34 tenant operations pages in the first release.
- Implementing platform tenant lifecycle, plans, subscriptions, public-bank governance, alerts, dunning, invoices, usage, and platform permissions in the first release.
- Activating payment, refunds, wallet checkout, membership entitlements, coupons, or activation codes in the first release.
- Implementing private paid-resource delivery or protected video playback in the first release.
- Implementing vocabulary study, knowledge handbook, historical scorelines, AI school recommendations, downloadable resources, messages, badges, check-in, tasks, or growth features in the first release.
- Implementing generic course and lesson management. The supplied product is initially modeled as an exam-prep catalog, question-bank, and practice system.
- Replacing the existing Member, System, Pay, Infra, notification, email, SMS, dictionary, role, menu, job, or audit infrastructure.
- Direct frontend integration with Scalar or persistence of Scalar/Supabase credentials in browser storage.
- Dual-writing personal learning data to RuoYi and Scalar.
- Treating the static prototypes CSS, state management, or mock data as production source code.
- Building the production frontend before the complete frontend repository and exact revision are provided.
- Claiming DRM, anti-download, watermarking, or advanced video protection without a separately approved media-security design.
- Introducing a new migration framework as part of the first Education slice. Database scripts will follow an explicit ordered-script process unless a separate migration decision is approved.
- Supporting every database vendor present in the repository in the first release. MySQL is the working assumption pending production confirmation.
- Migrating all Scalar content or decommissioning Scalar in the first release.
- Sending private student profile data to an AI provider.
- Building new email administration APIs unless a later frontend requirement demonstrates that the existing template and account capabilities are insufficient.
## Further Notes
- The currently checked-out frontend directories are incomplete. Implementation must pause at the frontend boundary until the production Vue 3 admin and student Web/H5 sources, branches, and exact commits are available.
- The Scalar share page is usable for discovery, but a machine-readable OpenAPI JSON or YAML export must be frozen before adapter implementation.
- The Scalar contract currently models education mainly as catalog nodes, content entries, question collections, practice blueprints, and questions rather than generic courses and lessons. The domain language in implementation should follow the exam-prep product unless product requirements change.
- Known Scalar uncertainties include management question list/detail reads, platform login, payment return and polling semantics, entitlement-resource relationships, answer autosave idempotency, and asynchronous media/import job states.
- The existing generic file download route is public and tenant-ignored. It must not be reused as the authorization boundary for paid education content.
- The root build currently leaves Member and Pay disabled. Member is required for the first release; Pay should be activated only when the payment slice starts.
- The desired execution order for an implementation agent is: module activation, tenant/auth shell, Scalar catalog adapter and contract tests, question read facade, practice creation, autosave and recovery, atomic submission and report, wrong questions/favorites, then full E2E and visual acceptance.
- Each implementation change set should contain schema, seed data, domain implementation, tests, API documentation, one complete frontend slice, and verified commands. Mock data or an uncalled endpoint must not be reported as complete.
- The issue tracker is the projects self-hosted Gitea instance. This spec should be labeled `ready-for-agent` once published.

View File

@@ -15,12 +15,13 @@
<!-- 各种 module 拓展 -->
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<!-- <module>yudao-module-member</module>-->
<module>yudao-module-member</module>
<module>yudao-module-education</module>
<!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>-->
<!-- <module>yudao-module-pay</module>-->
<!-- <module>yudao-module-mall</module>-->
<module>yudao-module-pay</module>
<module>yudao-module-mall</module>
<!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>-->
<!-- <module>yudao-module-iot</module>-->
@@ -32,7 +33,7 @@
</modules>
<name>恭学教育</name>
<description>恭学教育 - 让教育更简单。基于 Spring Boot + MyBatis Plus + Vue & Element 的后台管理系统。</description>
<description>恭学教育 - 让教育更简单。基于 Spring Boot + MyBatis Plus + Vue &amp; Element 的后台管理系统。</description>
<url>https://www.gongxue.com</url>
<properties>

View File

@@ -45,5 +45,5 @@ docker compose --env-file docker.env up -d
- admin ui: http://localhost:8080
- api server: http://localhost:48080
- mysql: root/123456, port: 3306
- postgresql: root/123456, port: 5432
- redis: port: 6379

View File

@@ -1,21 +1,19 @@
version: "3.4"
name: yudao-system
services:
mysql:
container_name: yudao-mysql
image: mysql:8
postgres:
container_name: yudao-postgres
image: postgres:17-alpine
restart: unless-stopped
tty: true
ports:
- "3306:3306"
- "5432:5432"
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-ruoyi-vue-pro}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
POSTGRES_DB: ${POSTGRES_DB:-ruoyi-vue-pro}
POSTGRES_USER: ${POSTGRES_USER:-root}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
volumes:
- mysql:/var/lib/mysql/
- ./sql/mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/ruoyi-vue-pro.sql:ro
- postgres:/var/lib/postgresql/data/
- ../../sql/postgresql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/000-ruoyi-vue-pro.sql:ro
redis:
container_name: yudao-redis
@@ -44,15 +42,15 @@ services:
-Djava.security.egd=file:/dev/./urandom
}
ARGS:
--spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:mysql://yudao-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
--spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:postgresql://yudao-postgres:5432/ruoyi-vue-pro}
--spring.datasource.dynamic.datasource.master.username=${MASTER_DATASOURCE_USERNAME:-root}
--spring.datasource.dynamic.datasource.master.password=${MASTER_DATASOURCE_PASSWORD:-123456}
--spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:mysql://yudao-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
--spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:postgresql://yudao-postgres:5432/ruoyi-vue-pro}
--spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root}
--spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456}
--spring.data.redis.host=${REDIS_HOST:-yudao-redis}
depends_on:
- mysql
- postgres
- redis
admin:
@@ -78,7 +76,7 @@ services:
- server
volumes:
mysql:
postgres:
driver: local
redis:
driver: local

View File

@@ -1,13 +1,14 @@
## mysql
MYSQL_DATABASE=ruoyi-vue-pro
MYSQL_ROOT_PASSWORD=123456
## postgresql
POSTGRES_DB=ruoyi-vue-pro
POSTGRES_USER=root
POSTGRES_PASSWORD=123456
## server
JAVA_OPTS=-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom
MASTER_DATASOURCE_URL=jdbc:mysql://yudao-mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
MASTER_DATASOURCE_USERNAME=root
MASTER_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD}
MASTER_DATASOURCE_URL=jdbc:postgresql://yudao-postgres:5432/${POSTGRES_DB}
MASTER_DATASOURCE_USERNAME=${POSTGRES_USER}
MASTER_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
SLAVE_DATASOURCE_URL=${MASTER_DATASOURCE_URL}
SLAVE_DATASOURCE_USERNAME=${MASTER_DATASOURCE_USERNAME}
SLAVE_DATASOURCE_PASSWORD=${MASTER_DATASOURCE_PASSWORD}

View File

@@ -0,0 +1,25 @@
CREATE ROLE yudao_flyway LOGIN NOINHERIT PASSWORD '123456';
CREATE ROLE yudao_runtime LOGIN NOINHERIT PASSWORD '123456';
ALTER SCHEMA public OWNER TO yudao_flyway;
DO $$
DECLARE
object RECORD;
BEGIN
FOR object IN
SELECT format('%I.%I', schemaname, tablename) AS name
FROM pg_tables
WHERE schemaname = 'public'
LOOP
EXECUTE 'ALTER TABLE ' || object.name || ' OWNER TO yudao_flyway';
END LOOP;
FOR object IN
SELECT format('%I.%I', sequence_schema, sequence_name) AS name
FROM information_schema.sequences
WHERE sequence_schema = 'public'
LOOP
EXECUTE 'ALTER SEQUENCE ' || object.name || ' OWNER TO yudao_flyway';
END LOOP;
END
$$;
GRANT USAGE ON SCHEMA public TO yudao_runtime;

View File

@@ -0,0 +1,8 @@
-- =============================================
-- Education 模块种子数据回滚
-- =============================================
DELETE FROM `system_menu`
WHERE `id` = 6801 AND `permission` = 'education:capability' AND `parent_id` = 6800;
DELETE FROM `system_menu`
WHERE `id` = 6800 AND `path` = '/education' AND `name` = '教育管理';

View File

@@ -0,0 +1,5 @@
-- =============================================
-- Education 模块 DDL
-- 当前为应用外壳阶段,无业务表;后续票据在此追加 CREATE TABLE 语句。
-- =============================================
-- 占位education 模块当前无业务表

View File

@@ -0,0 +1,15 @@
-- =============================================
-- Education 模块种子数据
-- 菜单 ID 范围6800-6899
-- 权限标识前缀education:
-- 可重复执行;角色授权由管理员按租户完成
-- =============================================
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 6800, '教育管理', '', 1, 50, 0, '/education', 'ep:school', NULL, NULL, 0, b'1', b'1', b'1', 'admin', NOW(), 'admin', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 6800);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 6801, '能力查询', 'education:capability', 3, 1, 6800, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', NOW(), 'admin', NOW(), b'0'
WHERE EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 6800 AND `path` = '/education' AND `deleted` = b'0')
AND NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 6801);

View File

@@ -0,0 +1 @@
-- Ticket #3 creates no database records, so rollback is intentionally a no-op.

View File

@@ -0,0 +1,3 @@
-- Ticket #3 adds no administrator permission.
-- Tenant resolution is @PermitAll and current education context only requires an authenticated Member session.
-- Therefore no system_menu rows are required for this vertical slice.

View File

@@ -0,0 +1,28 @@
-- =============================================
-- Education 模块 — 练习会话与题目快照回滚
-- Ticket #6 / Migration 002
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback (DROP TABLE) requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on these tables:
-- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
-- FROM information_schema.KEY_COLUMN_USAGE
-- WHERE REFERENCED_TABLE_NAME IN ('education_practice_session', 'education_practice_question')
-- AND TABLE_SCHEMA = DATABASE();
-- Result MUST be empty before proceeding.
--
-- 2. Verify the tables contain only data from this migration:
-- SELECT COUNT(*) AS session_count FROM education_practice_session;
-- SELECT COUNT(*) AS question_count FROM education_practice_question;
-- Operator must confirm these counts are acceptable to destroy.
--
-- 3. After verification, execute:
-- DROP TABLE IF EXISTS education_practice_question;
-- DROP TABLE IF EXISTS education_practice_session;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_practice_question;
-- -- DROP TABLE IF EXISTS education_practice_session;

View File

@@ -0,0 +1,101 @@
-- =============================================
-- Education 模块 — 练习会话与题目快照 DDL
-- Ticket #6: 练习会话创建、题目快照、恢复与状态机
-- Migration: 002
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- This migration MUST fail if either table already exists (no IF NOT EXISTS).
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 练习会话表
-- =============================================
-- Indexes:
-- uk_tenant_client_session — per-tenant uniqueness for clientSessionId idempotency.
-- Used by: selectByTenantAndClientSessionId (idempotent create check),
-- DuplicateKeyException catch for concurrent-create race resolution.
-- idx_tenant_user_status — covers getCurrentSession (latest ACTIVE by tenant+user)
-- and ownership queries. Column order: (tenant_id, user_id, status) so the
-- index supports both filtering by tenant+user and tenant+user+status.
-- Lock impact: INSERT acquires next-key lock on uk_tenant_client_session unique key;
-- concurrent inserts with same (tenant_id, client_session_id) serialize naturally.
-- No additional table-level locks required.
CREATE TABLE `education_practice_session` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '会话主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT 'Member 用户编号',
`client_session_id` VARCHAR(36) NOT NULL COMMENT '客户端生成的会话标识UUID用于幂等创建',
`status` VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
COMMENT '会话状态ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消',
`question_count` INT NOT NULL DEFAULT 0 COMMENT '题目总数',
`collection_id` VARCHAR(64) DEFAULT NULL COMMENT '源题集 ID',
`node_id` VARCHAR(64) DEFAULT NULL COMMENT '源目录节点 ID',
`type` VARCHAR(32) DEFAULT NULL COMMENT '筛选题型',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '筛选难度',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_client_session` (`tenant_id`, `client_session_id`),
KEY `idx_tenant_user_status` (`tenant_id`, `user_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习会话';
-- =============================================
-- 练习会话题目快照表
-- =============================================
-- Indexes:
-- uk_session_sequence — per-session uniqueness for question sequence numbers.
-- Used by: insertBatch to ensure no duplicate sequences within a session.
-- idx_session_id — covers selectBySessionIdOrderBySequence (load all questions
-- for a session, ordered by sequence). Also used by cascade delete lookups.
-- Lock impact: INSERT acquires gap locks within session_id range on uk_session_sequence;
-- concurrent inserts into different sessions are independent.
-- Options column: JSON data type stores only label, content, order — never isCorrect.
-- Application layer (optionsToSafeJson) strips correctness before storage.
CREATE TABLE `education_practice_question` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`session_id` BIGINT NOT NULL COMMENT '会话 ID',
`sequence` INT NOT NULL COMMENT '题目序号1-based服务端固定',
`question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID',
`content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '快照时的题目内容版本',
`stem` TEXT NOT NULL COMMENT '题干快照',
`type` VARCHAR(32) NOT NULL COMMENT '题型快照',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照',
`options` JSON NOT NULL COMMENT '选项快照 JSON不含 isCorrect',
`selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案',
`is_answered` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否已作答',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_session_sequence` (`session_id`, `sequence`),
KEY `idx_session_id` (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习会话题目快照';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify tables exist with correct structure:
-- SHOW CREATE TABLE education_practice_session;
-- SHOW CREATE TABLE education_practice_question;
-- Verify unique keys are enforced:
-- SHOW INDEX FROM education_practice_session WHERE Key_name = 'uk_tenant_client_session';
-- SHOW INDEX FROM education_practice_question WHERE Key_name = 'uk_session_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_practice_session;
-- SELECT COUNT(*) FROM education_practice_question;

View File

@@ -0,0 +1,30 @@
-- =============================================
-- Education 模块 — 答案保存幂等性回滚
-- Ticket #7 / Migration 003
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on education_answer_idempotency:
-- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
-- FROM information_schema.KEY_COLUMN_USAGE
-- WHERE REFERENCED_TABLE_NAME = 'education_answer_idempotency'
-- AND TABLE_SCHEMA = DATABASE();
-- Result MUST be empty before proceeding.
--
-- 2. Verify the table contains only data from this migration:
-- SELECT COUNT(*) AS idempotency_count FROM education_answer_idempotency;
-- Operator must confirm this count is acceptable to destroy.
--
-- 3. Verify no application code depends on client_sequence column:
-- Search codebase for 'clientSequence' / 'client_sequence' references.
--
-- 4. After verification, execute:
-- DROP TABLE IF EXISTS education_answer_idempotency;
-- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_answer_idempotency;
-- -- ALTER TABLE education_practice_question DROP COLUMN client_sequence;

View File

@@ -0,0 +1,97 @@
-- =============================================
-- Education 模块 — 答案保存幂等性 DDL
-- Ticket #7: 答案命令幂等、乐观锁并发控制、答案恢复
-- Migration: 003
-- Prerequisites: 002-education-practice-session.sql (session + question snapshots)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name = 'education_answer_idempotency';
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be 2.
-- =============================================
-- 答案命令幂等表
-- =============================================
-- Purpose: Provide durable idempotency for answer save commands.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
-- Same key + different request_hash → conflict.
-- Concurrent same-key inserts are resolved by unique constraint race handling.
--
-- Indexes:
-- uk_answer_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- INSERT during answer save. DuplicateKeyException catch for concurrent-create race resolution.
-- idx_tenant_session — covers lookup by session for audit/debug.
--
-- response_json: Stores the serialized answer response for replay after network timeout/retry.
-- request_hash: SHA-256 of canonical payload (sorted JSON fields) for content-based dedup.
CREATE TABLE `education_answer_idempotency` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '答题用户编号',
`operation` VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_ANSWER'
COMMENT '操作类型SUBMIT_ANSWER',
`idempotency_key` VARCHAR(64) NOT NULL COMMENT '客户端幂等键UUID',
`request_hash` VARCHAR(64) NOT NULL COMMENT '请求载荷 SHA-256 哈希',
`session_id` BIGINT NOT NULL COMMENT '会话 ID',
`question_id` VARCHAR(64) NOT NULL COMMENT '题目 ID',
`selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案',
`status` VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED'
COMMENT '状态ACCEPTED-已接受, CONFLICT-冲突',
`response_json` TEXT NOT NULL COMMENT '首次成功响应 JSON用于重试重放',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_answer_idempotency` (`tenant_id`, `user_id`, `operation`, `idempotency_key`),
KEY `idx_tenant_session` (`tenant_id`, `session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-答案命令幂等记录';
-- =============================================
-- PracticeQuestionDO: add client_sequence column
-- =============================================
-- Purpose: Track the last accepted client command sequence per question.
-- Rejects stale clientSequence: only sequences strictly greater than the
-- stored value are accepted (monotonic forward progression).
-- NULL means no answer has been accepted yet.
ALTER TABLE `education_practice_question`
ADD COLUMN `client_sequence` INT DEFAULT NULL COMMENT '最后接受的客户端命令序号',
ADD INDEX `idx_client_sequence` (`client_sequence`);
-- =============================================
-- PracticeSessionDO: add last_client_sequence column
-- =============================================
-- Purpose: Session-wide monotonic counter for client commands.
-- Rejects stale clientSequence across questions (not just per-question).
-- CAS incrementVersion now updates this column alongside version.
-- NULL means no answer has been accepted yet for this session.
ALTER TABLE `education_practice_session`
ADD COLUMN `last_client_sequence` INT DEFAULT NULL COMMENT '会话级最后接受的客户端命令序号(跨题目)';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new table exists:
-- SHOW CREATE TABLE education_answer_idempotency;
-- Verify unique key is enforced:
-- SHOW INDEX FROM education_answer_idempotency WHERE Key_name = 'uk_answer_idempotency';
-- Verify column added to question table:
-- SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT
-- FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND TABLE_NAME = 'education_practice_question'
-- AND COLUMN_NAME = 'client_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_answer_idempotency;

View File

@@ -0,0 +1,42 @@
-- =============================================
-- Education 模块 — Ticket #8 迁移回滚
-- 004-education-submit-report-rollback.sql
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on these tables:
-- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
-- FROM information_schema.KEY_COLUMN_USAGE
-- WHERE REFERENCED_TABLE_NAME IN ('education_submit_idempotency',
-- 'education_practice_report', 'education_practice_report_detail')
-- AND TABLE_SCHEMA = DATABASE();
-- Result MUST be empty before proceeding.
--
-- 2. Verify columns are not referenced by application code:
-- Search codebase for 'correct_answer' / 'explanation' references in
-- education_practice_question to confirm no other consumers.
--
-- 3. Verify the tables contain only data from this migration:
-- SELECT COUNT(*) AS idempotency_count FROM education_submit_idempotency;
-- SELECT COUNT(*) AS report_count FROM education_practice_report;
-- SELECT COUNT(*) AS detail_count FROM education_practice_report_detail;
-- Operator must confirm these counts are acceptable to destroy.
--
-- 4. After verification, execute:
-- DROP TABLE IF EXISTS education_practice_report_detail;
-- DROP TABLE IF EXISTS education_practice_report;
-- DROP TABLE IF EXISTS education_submit_idempotency;
-- ALTER TABLE education_practice_question
-- DROP COLUMN correct_answer,
-- DROP COLUMN explanation;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_practice_report_detail;
-- -- DROP TABLE IF EXISTS education_practice_report;
-- -- DROP TABLE IF EXISTS education_submit_idempotency;
-- -- ALTER TABLE education_practice_question
-- -- DROP COLUMN correct_answer,
-- -- DROP COLUMN explanation;

View File

@@ -0,0 +1,164 @@
-- =============================================
-- Education 模块 — 交卷提交与成绩报告 DDL
-- Ticket #8: 交卷 CAS、保护性答案快照、评分与报告
-- Migration: 004
-- Prerequisites: 003-education-answer-idempotency.sql
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_submit_idempotency',
-- 'education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_practice_session',
-- 'education_practice_question',
-- 'education_answer_idempotency');
-- Result MUST be 3.
-- =============================================
-- PracticeQuestionDO: add protected answer snapshot columns
-- =============================================
-- Purpose: At session creation, snapshot correct_answer and explanation
-- from the full CatalogQuestionDTO. These fields are NEVER exposed
-- before submission (enforced by SafeQuestionRespVO allow-list and
-- PracticeQuestionRespVO which does not include them).
ALTER TABLE `education_practice_question`
ADD COLUMN `correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照(不可在交卷前暴露)',
ADD COLUMN `explanation` TEXT DEFAULT NULL COMMENT '解析快照(不可在交卷前暴露)';
-- =============================================
-- 交卷幂等表
-- =============================================
-- Purpose: Provide durable idempotency for submit-session commands.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original report.
-- Same key + different request_hash → conflict.
-- Concurrent same-key inserts resolved by unique constraint race handling.
--
-- Indexes:
-- uk_submit_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- INSERT during submit. DuplicateKeyException catch for concurrent-create race resolution.
-- idx_submit_session — covers lookup by session for audit/debug.
CREATE TABLE `education_submit_idempotency` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '交卷用户编号',
`operation` VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_SESSION'
COMMENT '操作类型SUBMIT_SESSION',
`idempotency_key` VARCHAR(64) NOT NULL COMMENT '客户端幂等键UUID',
`request_hash` VARCHAR(64) NOT NULL COMMENT '请求载荷 SHA-256 哈希',
`session_id` BIGINT NOT NULL COMMENT '会话 ID',
`report_id` BIGINT DEFAULT NULL COMMENT '关联的报告 ID成功时有值',
`status` VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED'
COMMENT '状态ACCEPTED-已接受, CONFLICT-冲突',
`response_json` TEXT NOT NULL COMMENT '首次成功响应 JSON用于重试重放',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_submit_idempotency` (`tenant_id`, `user_id`, `operation`, `idempotency_key`),
KEY `idx_submit_session` (`tenant_id`, `session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-交卷幂等记录';
-- =============================================
-- 练习报告表(会话级)
-- =============================================
-- Purpose: Store the computed scoring result for a submitted session.
-- One report per session. Immutable after creation.
-- Question snapshots (stem, selectedAnswer, correctAnswer, explanation)
-- are stored in report_details so source question edits don't affect history.
--
-- Indexes:
-- uk_report_session — one report per session (unique).
-- idx_report_tenant_user — covers paginated history queries for current tenant+user.
-- idx_report_create_time — covers time-sorted listing.
CREATE TABLE `education_practice_report` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '用户编号',
`session_id` BIGINT NOT NULL COMMENT '会话 ID',
`question_count` INT NOT NULL COMMENT '题目总数',
`answered_count` INT NOT NULL DEFAULT 0 COMMENT '已答题数',
`unanswered_count` INT NOT NULL DEFAULT 0 COMMENT '未答题数',
`correct_count` INT NOT NULL DEFAULT 0 COMMENT '正确题数',
`incorrect_count` INT NOT NULL DEFAULT 0 COMMENT '错误题数',
`score` INT NOT NULL DEFAULT 0 COMMENT '得分(整数,满分 100 为基准)',
`status` VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED'
COMMENT '报告状态SUBMITTED',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_report_session` (`session_id`),
KEY `idx_report_tenant_user` (`tenant_id`, `user_id`),
KEY `idx_report_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习报告';
-- =============================================
-- 练习报告明细表(逐题结果)
-- =============================================
-- Purpose: Store per-question scoring results at submission time.
-- Includes snapshot of stem, selectedAnswer, correctAnswer, and explanation
-- so that history is stable even if source questions are later edited.
--
-- Indexes:
-- uk_report_sequence — per-report uniqueness for question sequence.
-- idx_detail_session — covers lookup by session for report assembly.
CREATE TABLE `education_practice_report_detail` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '用户编号',
`report_id` BIGINT NOT NULL COMMENT '报告 ID',
`session_id` BIGINT NOT NULL COMMENT '会话 ID',
`question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID',
`sequence` INT NOT NULL COMMENT '题目序号1-based',
`stem` TEXT NOT NULL COMMENT '题干快照',
`type` VARCHAR(32) NOT NULL COMMENT '题型快照',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照',
`selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案',
`correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照',
`is_correct` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否正确',
`explanation` TEXT DEFAULT NULL COMMENT '解析快照',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_report_sequence` (`report_id`, `sequence`),
KEY `idx_detail_session` (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习报告明细';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- SHOW CREATE TABLE education_submit_idempotency;
-- SHOW CREATE TABLE education_practice_report;
-- SHOW CREATE TABLE education_practice_report_detail;
-- Verify columns added to question table:
-- SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT
-- FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND TABLE_NAME = 'education_practice_question'
-- AND COLUMN_NAME IN ('correct_answer', 'explanation');
-- Verify unique keys are enforced:
-- SHOW INDEX FROM education_submit_idempotency WHERE Key_name = 'uk_submit_idempotency';
-- SHOW INDEX FROM education_practice_report WHERE Key_name = 'uk_report_session';
-- SHOW INDEX FROM education_practice_report_detail WHERE Key_name = 'uk_report_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_submit_idempotency;
-- SELECT COUNT(*) FROM education_practice_report;
-- SELECT COUNT(*) FROM education_practice_report_detail;

View File

@@ -0,0 +1,25 @@
-- =============================================
-- Education 模块 — 错题本 DDL Rollback
-- Migration: 005
-- =============================================
-- IMPORTANT: This is a documentation-only rollback.
-- No DROP/ALTER/DELETE statements are executed. The wrong_question
-- table is provenance-safe: it only accumulates data and mastering
-- is a status flag. Dropping these tables would lose student error
-- history with no recovery path.
--
-- What this migration created:
-- - education_wrong_question (new table)
-- - education_wrong_question_idempotency (new table)
-- - education_practice_report_detail.options (new column)
-- - education_practice_session.review_fingerprint (new column)
--
-- Manual rollback requires:
-- 1. Verified database backup before rollback
-- 2. Operator approval (DBA sign-off)
-- 3. Provenance of all wrong-question records preserved (exported)
-- 4. Soft-delete via deleted = b'1' before any hard drop
--
-- These tables are NOT deleted by this script. Wrong history is
-- retained; if deletion is required by external policy, consult
-- the DBA for a verified rollback procedure.

View File

@@ -0,0 +1,154 @@
-- =============================================
-- Education 模块 — 错题本 DDL
-- Ticket #9: 错题自动收集、复习练习创建
-- Migration: 005
-- Prerequisites: 004-education-submit-report.sql (report + detail tables)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_wrong_question',
-- 'education_wrong_question_idempotency');
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name IN ('education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 2.
-- =============================================
-- PracticeReportDetailDO: add options snapshot column
-- =============================================
-- PracticeReportDetailDO: add content_version + options snapshot columns
-- =============================================
-- Purpose: At submit time, snapshot question content version and options
-- (without isCorrect) so wrong-question book and review sessions have
-- stable display data. The options are already stripped of isCorrect
-- by the submit flow.
ALTER TABLE `education_practice_report_detail`
ADD COLUMN `content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '题目内容版本快照',
ADD COLUMN `options` TEXT DEFAULT NULL COMMENT '选项快照 JSON不含 isCorrect';
-- =============================================
-- PracticeSessionDO: add review fingerprint column
-- =============================================
-- Purpose: Persist the canonical fingerprint of wrong-question IDs used
-- to create a review session. On idempotent replay, the fingerprint
-- is compared: same tenant+clientSessionId+sameUser+sortedIDs match
-- returns the existing session; different ID set returns
-- SESSION_IDEMPOTENCY_MISMATCH.
ALTER TABLE `education_practice_session`
ADD COLUMN `review_fingerprint` VARCHAR(64) DEFAULT NULL COMMENT '复习会话题目指纹SHA-256 of sorted unique wrongQuestionIds';
-- 错题表
-- =============================================
-- Purpose: Persistent wrong-question book per student.
-- Each (tenant, user, question) is a unique entry.
-- Repeated wrong answers on the SAME question increment wrong_count
-- and update last_wrong_time. The idempotency guard table ensures
-- each (tenant, user, question, report) can upsert at most once.
--
-- master_status values: 'PENDING' (default) | 'MASTERED'
-- Marking mastered retains the full history and count; it does NOT
-- delete or archive the record. Students can optionally un-master.
--
-- Snapshot fields (stem, type, difficulty, options, content_version):
-- populated from the latest report detail that touched this question.
-- These are for listing/detail display without joining report details.
--
-- latest_correct_answer, latest_explanation:
-- also from the latest report detail; available for detail display
-- post-submit (not exposed in review session creation pre-submit).
--
-- Indexes:
-- uk_tenant_user_question — per-tenant, per-user, per-question uniqueness.
-- INSERT ... ON DUPLICATE KEY UPDATE is the primary write path.
-- idx_tenant_user_status — covers filtered list queries (page with status filter).
-- idx_tenant_user_last_wrong — covers time-sorted listing.
CREATE TABLE `education_wrong_question` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '学生用户编号',
`question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID',
-- snapshot fields for listing / detail (from latest report detail)
`stem` TEXT NOT NULL COMMENT '题干快照(最新)',
`type` VARCHAR(32) NOT NULL COMMENT '题型快照',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照',
`options` JSON NOT NULL COMMENT '选项快照 JSON不含 isCorrect',
`content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '题目内容版本',
`latest_correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照(最新,供详情展示)',
`latest_explanation` TEXT DEFAULT NULL COMMENT '解析快照(最新,供详情展示)',
-- timing & count
`first_wrong_time` DATETIME NOT NULL COMMENT '首次错误时间',
`last_wrong_time` DATETIME NOT NULL COMMENT '最近错误时间',
`wrong_count` INT NOT NULL DEFAULT 1 COMMENT '累计错误次数',
-- mastery
`master_status` VARCHAR(20) NOT NULL DEFAULT 'PENDING'
COMMENT '掌握状态PENDING-待掌握, MASTERED-已掌握',
`mastered_time` DATETIME DEFAULT NULL COMMENT '标记掌握时间',
-- provenance
`last_report_id` BIGINT DEFAULT NULL COMMENT '最近关联的报告 ID',
`last_session_id` BIGINT DEFAULT NULL COMMENT '最近关联的会话 ID',
-- audit
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_user_question` (`tenant_id`, `user_id`, `question_id`),
KEY `idx_tenant_user_status` (`tenant_id`, `user_id`, `master_status`),
KEY `idx_tenant_user_last_wrong` (`tenant_id`, `user_id`, `last_wrong_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-错题本';
-- =============================================
-- 错题流水幂等表
-- =============================================
-- Purpose: Ensure each (tenant, user, question, report) upserts the
-- wrong-question book exactly once. The submitSession transaction
-- INSERT IGNOREs into this table BEFORE the wrong question upsert;
-- a duplicate means this report already contributed to the count.
-- This guards against:
-- - Replayed submit (idempotent resubmit) double-counting
-- - Concurrent submit races where both threads evaluate the
-- same report details
--
-- Indexes:
-- uk_tenant_user_question_report — per (tenant, user, question, report) uniqueness.
-- INSERT IGNORE provides the idempotency guard BEFORE upserting.
-- wrong_question_id is filled after upsert for audit purposes.
-- idx_report — fast lookup by report for audit/debug.
CREATE TABLE `education_wrong_question_idempotency` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '学生用户编号',
`wrong_question_id` BIGINT DEFAULT NULL COMMENT '错题记录 IDupsert 后填充)',
`report_id` BIGINT NOT NULL COMMENT '报告 ID',
`question_id` VARCHAR(64) NOT NULL COMMENT '题目 ID',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_user_question_report` (`tenant_id`, `user_id`, `question_id`, `report_id`),
KEY `idx_report` (`report_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-错题流水幂等';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- SHOW CREATE TABLE education_wrong_question;
-- SHOW CREATE TABLE education_wrong_question_idempotency;
-- Verify unique keys are enforced:
-- SHOW INDEX FROM education_wrong_question WHERE Key_name = 'uk_tenant_user_question';
-- SHOW INDEX FROM education_wrong_question_idempotency WHERE Key_name = 'uk_tenant_user_question_report';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_wrong_question;
-- SELECT COUNT(*) FROM education_wrong_question_idempotency;

View File

@@ -0,0 +1,22 @@
-- =============================================
-- Education 模块 — 收藏夹 DDL Rollback
-- Migration: 007
-- =============================================
-- IMPORTANT: This is a documentation-only rollback.
-- No DROP/ALTER/DELETE statements are executed. The favorite
-- table is provenance-safe: it only accumulates user preference
-- data. Dropping this table would lose student favorites with
-- no recovery path.
--
-- What this migration created:
-- - education_favorite (new table)
--
-- Manual rollback requires:
-- 1. Verified database backup before rollback
-- 2. Operator approval (DBA sign-off)
-- 3. Provenance of all favorite records preserved (exported)
-- 4. Soft-delete via deleted = b'1' before any hard drop
--
-- These tables are NOT deleted by this script. Favorite history is
-- retained; if deletion is required by external policy, consult
-- the DBA for a verified rollback procedure.

View File

@@ -0,0 +1,77 @@
-- =============================================
-- Education 模块 — 收藏夹 DDL
-- Ticket #10: 学生收藏题目
-- Migration: 007
-- Prerequisites: 000-education-schema.sql (base tables)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND table_name = 'education_favorite';
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 收藏表
-- =============================================
-- Purpose: Student favorites for questions with safe snapshots.
-- Each (tenant, user, target_type, target_id) is a unique entry.
-- Logical deletion: setting deleted=1 marks as unfavorited.
-- Re-adding after deletion reactivates the row via ON DUPLICATE KEY UPDATE.
--
-- target_type values: 'QUESTION' (extensible enum)
--
-- Snapshot fields (stem, type, difficulty, options, content_version):
-- populated at creation time from the visible question's safe fields.
-- These snapshots preserve the question state as it appeared when favorited,
-- and remain stable even if the source question later changes or becomes unavailable.
--
-- available flag:
-- FALSE when the source question becomes hidden/unpublished after being
-- favorited. Existing favorites with available=FALSE remain listable but
-- display an "unavailable" indicator. New favorites cannot be created for
-- unavailable resources.
--
-- Indexes:
-- uk_tenant_user_target — per (tenant, user, target_type, target_id) uniqueness.
-- INSERT ... ON DUPLICATE KEY UPDATE is the primary reactivation path.
-- idx_tenant_user — covers listing queries filtered by current tenant+user.
-- idx_tenant_user_target_type — covers target-type-filtered listing.
CREATE TABLE `education_favorite` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`user_id` BIGINT NOT NULL COMMENT '学生用户编号',
`target_type` VARCHAR(32) NOT NULL COMMENT '目标类型QUESTION',
`target_id` VARCHAR(64) NOT NULL COMMENT '目标 ID题目 ID',
-- safe snapshot fields
`stem` TEXT DEFAULT NULL COMMENT '题干快照',
`type` VARCHAR(32) DEFAULT NULL COMMENT '题型快照',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照',
`options` JSON DEFAULT NULL COMMENT '选项快照 JSON不含 isCorrect',
`content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '题目内容版本',
-- availability
`available` BIT(1) NOT NULL DEFAULT b'1' COMMENT '源资源是否可用',
-- audit
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_user_target` (`tenant_id`, `user_id`, `target_type`, `target_id`),
KEY `idx_tenant_user` (`tenant_id`, `user_id`),
KEY `idx_tenant_user_target_type` (`tenant_id`, `user_id`, `target_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-收藏夹';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new table exists:
-- SHOW CREATE TABLE education_favorite;
-- Verify unique key is enforced:
-- SHOW INDEX FROM education_favorite WHERE Key_name = 'uk_tenant_user_target';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_favorite;

View File

@@ -0,0 +1,5 @@
artifacts/
*.har
*.log
.DS_Store
node_modules/

View File

@@ -0,0 +1,22 @@
# Browser acceptance harness
This directory contains a local-only student client and an acceptance suite. It has no lockfile or vendored browser binaries: do not install from the network during normal repository checks.
## Commands
From this directory:
```bash
npm run smoke # dependency-free route smoke test
npm run contract # dependency-free HTTP and adapter tests
npm run browser:if-available # runs Playwright only when it is already resolvable
npm run browser # explicit Playwright command, requires an existing install
```
The browser suite starts `server.js` itself, uses Chromium headlessly, and writes screenshots/traces to `artifacts/` (gitignored). It is intentionally not reported as passing when Playwright or its browser binary is unavailable.
To run against a real application instead of the deterministic local server, set `BASE_URL`; the server is then not started and the supplied token must be accepted by that application.
Required coverage includes desktop and H5 viewport core loops, timeout-after-commit with same-key retry, reload/current recovery, submit/report/wrong/favorite, logout, tenant/student isolation, and a request guard installed before navigation. The guard aborts every non-loopback request and any URL containing Scalar, Supabase, or provider-token patterns.
The dependency-free smoke route uses only Node built-ins and starts the local harness on loopback. It is the minimum check for environments without Playwright.

View File

@@ -0,0 +1,143 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const token = (name) => name;
const LOOPBACK = /^https?:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?(?:\/|$)/i;
const FORBIDDEN = /(scalar|supabase|(?:sk|pk|anon|service)[_-]?key|api[_-]?key|access[_-]?token|provider[_-]?token|anthropic|openai|gemini|deepseek)/i;
function installRequestGuard(page) {
const blocked = [];
const allowedViolations = [];
page.route('**/*', async (route) => {
const url = route.request().url();
if (!LOOPBACK.test(url) || FORBIDDEN.test(url)) {
blocked.push(`${route.request().method()} ${url}`);
await route.abort('blockedbyclient');
return;
}
await route.continue();
});
return (expectedBlocked = 0) => {
expect(allowedViolations, `unexpected request guard violations: ${allowedViolations.join(', ')}`).toEqual([]);
expect(blocked.length, `expected ${expectedBlocked} blocked requests, saw ${blocked.length}`).toBe(expectedBlocked);
};
}
async function connect(page, student) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.getByLabel('Local access token').fill(token(student));
await page.getByTestId('connect').click();
await expect(page.getByTestId('status')).toContainText(/Catalog ready|Session recovered|No active session/);
await expect(page.getByTestId('identity-chip')).toContainText(student.includes('tenant-a') ? 'Student A1' : 'Student B1');
}
async function start(page) {
await page.getByRole('button', { name: 'Start practice' }).click();
await expect(page.getByTestId('practice')).toContainText('Q1');
}
test.describe('education student core loop', () => {
test('request guard aborts external and provider-token URLs', async ({ page }) => {
const checkGuard = installRequestGuard(page);
await page.goto('/');
await page.waitForLoadState('networkidle');
const blocked = await page.evaluate(async () => {
const urls = ['https://example.invalid/scalar', 'https://provider.invalid/api?access_token=redacted'];
return Promise.all(urls.map(async (url) => {
try { await fetch(url); return false; } catch (_) { return true; }
}));
});
checkGuard(2);
});
test('desktop recovery, submit, wrong questions, and favorites', async ({ page }) => {
const checkGuard = installRequestGuard(page);
await connect(page, 'tenant-a-student-1');
await start(page);
const requests = [];
page.on('request', (request) => {
if (request.url().includes('/practice-session/answer')) requests.push(request);
});
await page.getByLabel('Database').check();
await expect(page.getByTestId('practice')).toContainText(/Saved|Ready/);
expect(requests.length).toBeGreaterThan(0);
await page.waitForLoadState('networkidle');
await page.getByLabel('Local access token').fill('tenant-a-student-1');
await page.getByTestId('connect').click();
await expect(page.getByTestId('status')).toContainText(/Session recovered|Catalog ready/);
await expect(page.getByTestId('practice')).toContainText('Database');
await page.getByLabel('Random delay').check();
await page.getByLabel('Version check').check();
await page.getByRole('button', { name: 'Submit practice' }).click();
await expect(page.getByTestId('status')).toContainText(/Submitted|Wrong questions loaded/);
await page.getByTestId('load-wrong').click();
await expect(page.getByTestId('wrong')).toBeVisible();
const favorite = await page.evaluate(async () => (await fetch('/app-api/education/favorite/create', { method: 'POST', headers: { Authorization: 'Bearer tenant-a-student-1', 'Content-Type': 'application/json' }, body: JSON.stringify({ targetType: 'QUESTION', targetId: 'q-a-1' }) })).json());
expect(favorite.code).toBe(0);
await page.getByTestId('load-favorites').click();
await expect(page.getByTestId('favorites-list')).toBeVisible();
await expect(page.getByTestId('favorites-list')).not.toContainText('No favorites yet.');
await page.screenshot({ path: 'artifacts/desktop-core-loop.png', fullPage: true });
checkGuard();
});
test('H5 viewport core loop', async ({ page }) => {
const checkGuard = installRequestGuard(page);
await page.setViewportSize({ width: 390, height: 844 });
await connect(page, 'tenant-a-student-1');
await start(page);
await page.getByLabel('Controller').check();
await expect(page.getByTestId('practice')).toContainText(/Saved|Ready/);
await page.screenshot({ path: 'artifacts/h5-core-loop.png', fullPage: true });
checkGuard();
});
test('timeout after commit retries with the same idempotency key', async ({ page }) => {
const checkGuard = installRequestGuard(page);
await connect(page, 'tenant-a-student-1');
const result = await page.evaluate(async () => {
const create = await fetch('/app-api/education/practice-session/create', {
method: 'POST', headers: { Authorization: 'Bearer tenant-a-student-1', 'Content-Type': 'application/json' },
body: JSON.stringify({ clientSessionId: `browser-timeout-${crypto.randomUUID()}`, collectionId: 'col-a-core', questionCount: 1 }),
});
const session = (await create.json()).data;
const body = JSON.stringify({ sessionId: session.id, questionSequence: 1, selectedAnswer: 'A', idempotencyKey: 'same-key', clientSequence: 1, expectedSessionVersion: 0 });
const first = await fetch('/app-api/education/practice-session/answer?fault=answer-timeout-after-commit', { method: 'PUT', headers: { Authorization: 'Bearer tenant-a-student-1', 'Content-Type': 'application/json' }, body });
const retry = await fetch('/app-api/education/practice-session/answer', { method: 'PUT', headers: { Authorization: 'Bearer tenant-a-student-1', 'Content-Type': 'application/json' }, body });
return { first: first.status, retry: retry.status, retryBody: await retry.json() };
});
expect(result.first).toBe(504);
expect(result.retry).toBe(200);
expect(result.retryBody.data.selectedAnswer).toBe('A');
checkGuard();
});
test('logout clears the in-memory student session', async ({ page }) => {
const checkGuard = installRequestGuard(page);
await connect(page, 'tenant-a-student-1');
await page.getByTestId('logout').click();
await expect(page.getByTestId('identity-chip')).toHaveText('Offline');
await expect(page.getByTestId('status')).toHaveText('Logged out');
checkGuard();
});
test('two tenants and two students cannot see each other resources', async ({ page, request }) => {
const checkGuard = installRequestGuard(page);
const a = await request.get('/app-api/education/context', { headers: { Authorization: 'Bearer tenant-a-student-1' } });
const b = await request.get('/app-api/education/context', { headers: { Authorization: 'Bearer tenant-b-student-1' } });
expect((await a.json()).data.userId).toBe('student-a1');
expect((await b.json()).data.userId).toBe('student-b1');
const create = await request.post('/app-api/education/practice-session/create', { headers: { Authorization: 'Bearer tenant-a-student-1' }, data: { clientSessionId: 'isolation', collectionId: 'col-a-core', questionCount: 1 } });
const session = (await create.json()).data;
const stolen = await request.get(`/app-api/education/practice-session/get?id=${session.id}`, { headers: { Authorization: 'Bearer tenant-a-student-2' } });
expect(stolen.status()).toBe(404);
const otherTenant = await request.get('/app-api/education/questions/page?collectionId=col-a-core', { headers: { Authorization: 'Bearer tenant-b-student-1' } });
expect((await otherTenant.json()).data.list).toHaveLength(0);
checkGuard();
});
});

View File

@@ -0,0 +1,34 @@
'use strict';
const API_PREFIX = '/app-api';
function buildRequest(path, options = {}, accessToken = '') {
const headers = {
Accept: 'application/json',
...(options.body ? { 'Content-Type': 'application/json' } : {}),
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
};
return { url: `${API_PREFIX}${path}`, options: { ...options, headers } };
}
function answerCommand(session, questionSequence, selectedAnswer, idempotencyKey, clientSequence) {
return {
sessionId: session.id,
questionSequence,
selectedAnswer,
idempotencyKey,
clientSequence,
expectedSessionVersion: session.sessionVersion,
};
}
function applyAnswerResult(session, result) {
return {
...session,
sessionVersion: result.sessionVersion,
serverVersion: result.serverVersion ?? result.sessionVersion,
acceptedSequence: Math.max(session.acceptedSequence || 0, result.acceptedSequence || 0),
};
}
module.exports = { buildRequest, answerCommand, applyAnswerResult };

View File

@@ -0,0 +1,19 @@
'use strict';
const assert = require('assert');
const { buildRequest, answerCommand, applyAnswerResult } = require('./adapter');
const request = buildRequest('/education/context', { method: 'GET' }, 'memory-token');
assert.equal(request.url, '/app-api/education/context');
assert.equal(request.options.headers.Authorization, 'Bearer memory-token');
assert.equal(request.options.headers.Accept, 'application/json');
const session = { id: 's-1', sessionVersion: 4, acceptedSequence: 2 };
const command = answerCommand(session, 3, 'B', 'answer-key-1', 3);
assert.deepEqual(command, { sessionId: 's-1', questionSequence: 3, selectedAnswer: 'B', idempotencyKey: 'answer-key-1', clientSequence: 3, expectedSessionVersion: 4 });
const advanced = applyAnswerResult(session, { sessionVersion: 5, acceptedSequence: 3 });
assert.equal(advanced.sessionVersion, 5);
assert.equal(advanced.acceptedSequence, 3);
assert.equal(applyAnswerResult(advanced, { sessionVersion: 6, acceptedSequence: 2 }).acceptedSequence, 3);
process.stdout.write('education student adapter unit tests passed\n');

View File

@@ -0,0 +1,45 @@
const API_PREFIX = '/app-api';
let accessToken = '';
let tenant = null;
let currentSession = null;
let clientSequence = 0;
let expectedSessionVersion = 0;
let saveState = 'idle';
const $ = (id) => document.getElementById(id);
const tokenFor = () => accessToken;
export function buildRequest(path, options = {}, token = accessToken) {
const headers = { Accept: 'application/json', ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}) };
return { url: `${API_PREFIX}${path}`, options: { ...options, headers } };
}
export function nextAnswerCommand(session, questionSequence, selectedAnswer, key = crypto.randomUUID()) {
return { sessionId: session.id, questionSequence, selectedAnswer, idempotencyKey: key, clientSequence: (session.acceptedSequence || 0) + 1, expectedSessionVersion: session.sessionVersion ?? session.serverVersion ?? 0 };
}
export function applyAnswerState(session, result) { return { ...session, sessionVersion: result.sessionVersion, serverVersion: result.serverVersion ?? result.sessionVersion, acceptedSequence: Math.max(session.acceptedSequence || 0, result.acceptedSequence || 0) }; }
function setStatus(text, tone = 'neutral') { $('status').textContent = text; $('status').dataset.tone = tone; $('connection-dot').dataset.tone = tone; }
function setSaveState(state, text) { saveState = state; const node = $('save-state'); if (node) { node.textContent = text; node.dataset.state = state; } }
function requestId(response) { const id = response.headers.get('x-request-id') || response.headers.get('x-trace-id'); if (id) $('request-id').textContent = `req ${id}`; }
function query(params = {}) { const value = new URLSearchParams(); Object.entries(params).forEach(([key, item]) => { if (item !== undefined && item !== null && item !== '') value.set(key, item); }); const result = value.toString(); return result ? `?${result}` : ''; }
async function api(path, options = {}) { const request = buildRequest(path, options); const response = await fetch(request.url, request.options); requestId(response); const payload = await response.json().catch(() => ({})); if (!response.ok || (payload.code !== undefined && payload.code !== 0)) { const error = new Error(payload.msg || `Request failed (${response.status})`); error.status = response.status; error.data = payload.data; throw error; } return payload.data; }
function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c])); }
function button(label, handler, className = 'button button-outline') { const b = document.createElement('button'); b.type = 'button'; b.textContent = label; b.className = className; b.addEventListener('click', handler); return b; }
function renderList(target, list, emptyText, render) { const node = $(target); node.innerHTML = ''; if (!list?.length) { node.innerHTML = `<p class="empty-state">${escapeHtml(emptyText)}</p>`; return; } list.forEach((item) => node.appendChild(render(item))); }
function item(title, detail, action) { const node = document.createElement('article'); node.className = 'list-item'; node.innerHTML = `<div><strong>${escapeHtml(title)}</strong><span>${escapeHtml(detail || '')}</span></div>`; if (action) node.append(action); return node; }
function renderContext(data) { tenant = data; $('identity-chip').textContent = `${data.tenantName || data.tenantId} · ${data.displayName || data.userId}`; $('context').innerHTML = `<div><dt>Tenant</dt><dd>${escapeHtml(data.tenantName || data.tenantId)}</dd></div><div><dt>Student</dt><dd>${escapeHtml(data.displayName || data.userId)}</dd></div>`; }
async function resolveTenant() { return api('/education/tenant/resolve'); }
async function connect(event) { event?.preventDefault(); accessToken = $('token').value.trim(); if (!accessToken) { const mobile = $('mobile').value.trim(); const password = $('password').value; if (!mobile || !password) { setStatus('Enter member credentials or a local token', 'bad'); return; } try { setStatus('Logging in…'); const login = await api('/member/auth/login', { method: 'POST', body: JSON.stringify({ mobile, password }) }, ''); accessToken = login?.accessToken || login?.token || ''; } catch (error) { setStatus(error.message, 'bad'); return; } } try { setStatus('Resolving tenant…'); await resolveTenant(); const context = await api('/education/context'); renderContext(context); setStatus('Connected', 'good'); await loadCatalog(); await loadCurrent(); } catch (error) { setStatus(error.message, 'bad'); } }
async function loginWithCredentials() { return null; }
async function logout() { try { if (accessToken) await api('/member/auth/logout', { method: 'POST' }); } catch (_) { /* local memory is still cleared */ } accessToken = ''; tenant = null; currentSession = null; $('identity-chip').textContent = 'Offline'; $('context').innerHTML = '<div><dt>Tenant</dt><dd>Not resolved</dd></div><div><dt>Student</dt><dd>Not authenticated</dd></div>'; renderPractice(); setStatus('Logged out', 'neutral'); }
async function loadCatalog() { try { setStatus('Loading catalog…'); const [collections, subjects] = await Promise.all([api('/education/catalog/question-collections?limit=20'), api('/education/catalog/subjects')]); const select = $('subject-filter'); select.innerHTML = '<option value="">All subjects</option>' + (subjects || []).map((x) => `<option value="${escapeHtml(x.id)}">${escapeHtml(x.name || x.title || x.id)}</option>`).join(''); renderList('collections', collections, 'No permitted collections returned.', collectionCard); setStatus('Catalog ready', 'good'); } catch (error) { setStatus(error.message, 'bad'); } }
function collectionCard(collection) { const article = document.createElement('article'); article.className = 'collection-card'; article.innerHTML = `<div class="collection-index">SET</div><h3>${escapeHtml(collection.name || collection.title || collection.id)}</h3><p>${escapeHtml(collection.description || 'A focused set for your next study pass.')}</p><div class="collection-meta"><span>${collection.questionCount ?? '?'} questions</span><span>${escapeHtml(collection.status || 'available')}</span></div>`; article.append(button('Start practice', () => createPractice(collection), 'button button-dark')); return article; }
async function createPractice(collection) { try { setStatus('Creating practice…'); currentSession = await api('/education/practice-session/create', { method: 'POST', body: JSON.stringify({ clientSessionId: crypto.randomUUID(), collectionId: collection.id, questionCount: Math.min(collection.questionCount || 5, 5) }) }); clientSequence = currentSession.acceptedSequence || 0; expectedSessionVersion = currentSession.sessionVersion || 0; renderPractice(); setStatus('Practice active', 'good'); location.hash = 'practice'; } catch (error) { setStatus(error.message, 'bad'); } }
async function loadCurrent() { try { setStatus('Checking your session…'); currentSession = await api('/education/practice-session/current'); if (currentSession) { clientSequence = currentSession.acceptedSequence || 0; expectedSessionVersion = currentSession.sessionVersion || 0; } renderPractice(); setStatus(currentSession ? 'Session recovered' : 'No active session', currentSession ? 'good' : 'neutral'); } catch (error) { setStatus(error.message, 'bad'); } }
function renderPractice() { const host = $('practice'); host.innerHTML = ''; $('state-readout').textContent = currentSession ? `${currentSession.status} · v${currentSession.sessionVersion ?? 0}` : 'No session'; if (!currentSession) { host.innerHTML = '<p class="empty-state">No active session. Start one above.</p>'; return; } const heading = document.createElement('div'); heading.className = 'practice-head'; heading.innerHTML = `<div><span class="session-badge">${escapeHtml(currentSession.status)}</span><strong>${currentSession.questionCount || currentSession.questions?.length || 0} questions</strong></div><span id="save-state" class="save-state" data-state="idle">Ready</span>`; host.append(heading); (currentSession.questions || []).forEach((question) => { const field = document.createElement('fieldset'); field.className = 'question'; field.innerHTML = `<legend><span>Q${question.sequence}</span>${escapeHtml(question.stem || question.questionId)}</legend><div class="options">${(question.options || []).map((option, index) => { const value = String.fromCharCode(65 + index); return `<label class="option"><input type="radio" name="q-${question.sequence}" value="${value}" ${question.selectedAnswer === value ? 'checked' : ''}><span><b>${value}</b>${escapeHtml(option)}</span></label>`; }).join('')}</div>`; field.querySelectorAll('input').forEach((input) => input.addEventListener('change', () => saveAnswer(question, input.value))); host.append(field); }); if (currentSession.status === 'ACTIVE') { const actions = document.createElement('div'); actions.className = 'practice-actions'; actions.append(button('Submit practice', submitPractice, 'button button-dark')); host.append(actions); } }
async function saveAnswer(question, answer) { const command = nextAnswerCommand({ ...currentSession, acceptedSequence: clientSequence, sessionVersion: expectedSessionVersion }, question.sequence, answer, question.pendingKey || crypto.randomUUID()); question.pendingKey = command.idempotencyKey; setSaveState('saving', 'Saving…'); try { const result = await api('/education/practice-session/answer', { method: 'PUT', body: JSON.stringify(command) }); currentSession = applyAnswerState(currentSession, result); clientSequence = currentSession.acceptedSequence; expectedSessionVersion = currentSession.sessionVersion; question.selectedAnswer = answer; setSaveState('saved', 'Saved'); $('state-readout').textContent = `${currentSession.status} · v${expectedSessionVersion}`; } catch (error) { if (error.status === 504 || error.status >= 500) { setSaveState('retrying', 'Retrying…'); try { const result = await api('/education/practice-session/answer', { method: 'PUT', body: JSON.stringify(command) }); currentSession = applyAnswerState(currentSession, result); clientSequence = currentSession.acceptedSequence; expectedSessionVersion = currentSession.sessionVersion; question.selectedAnswer = answer; setSaveState('saved', 'Saved after retry'); return; } catch (_) {} } setSaveState('failed', 'Save failed — retry by changing this answer'); setStatus(error.message, 'bad'); } }
async function submitPractice() { if (!currentSession) return; try { setStatus('Submitting…'); const result = await api('/education/practice-session/submit', { method: 'POST', body: JSON.stringify({ sessionId: currentSession.id, idempotencyKey: crypto.randomUUID(), expectedSessionVersion }) }); currentSession.status = 'SUBMITTED'; currentSession.sessionVersion = result.sessionVersion || expectedSessionVersion + 1; expectedSessionVersion = currentSession.sessionVersion; renderPractice(); setStatus(`Submitted · ${result.score ?? '—'} correct`, 'good'); await loadWrong(); } catch (error) { setStatus(error.message, 'bad'); } }
async function loadWrong() { try { const data = await api('/education/wrong-question/page?pageNo=1&pageSize=20'); renderList('wrong', data?.list, 'No wrong questions yet.', (x) => item(x.questionStem || x.stem || x.questionId || x.id, `${x.errorCount ?? 0} ${x.errorCount === 1 ? 'miss' : 'misses'} · ${x.masterStatus || 'unmastered'}`, x.masterStatus !== 'MASTERED' ? button('Mark mastered', () => masterWrong(x), 'button button-small') : null)); setStatus('Wrong questions loaded', 'good'); } catch (error) { setStatus(error.message, 'bad'); } }
async function masterWrong(wrong) { try { await api('/education/wrong-question/master', { method: 'PUT', body: JSON.stringify({ id: wrong.id }) }); await loadWrong(); } catch (error) { setStatus(error.message, 'bad'); } }
async function loadFavorites() { try { const data = await api('/education/favorite/page?pageNo=1&pageSize=20'); renderList('favorites-list', data?.list, 'No favorites yet.', (x) => item(x.questionStem || x.stem || x.targetId, x.targetType || 'QUESTION', button('Remove', () => removeFavorite(x), 'button button-small'))); setStatus('Favorites loaded', 'good'); } catch (error) { setStatus(error.message, 'bad'); } }
async function removeFavorite(favorite) { try { await api('/education/favorite/delete', { method: 'DELETE', body: JSON.stringify({ id: favorite.id, targetId: favorite.targetId }) }); await loadFavorites(); } catch (error) { setStatus(error.message, 'bad'); } }
$('auth-form').addEventListener('submit', connect); $('logout').addEventListener('click', logout); $('load-catalog').addEventListener('click', loadCatalog); $('load-current').addEventListener('click', loadCurrent); $('load-wrong').addEventListener('click', loadWrong); $('load-favorites').addEventListener('click', loadFavorites);

View File

@@ -0,0 +1,26 @@
# Education student harness verification
Date: 2026-07-28
## Results
- PASS — `npm run smoke` (dependency-free loopback route smoke test).
- PASS — `npm run contract` (dependency-free HTTP/adapter tests).
- PASS — Node syntax checks for all harness JavaScript, including `acceptance.spec.js`.
- PASS — `npm run browser:if-available`; Playwright Chromium was installed locally and all six acceptance tests passed.
- PASS — `git diff --check` for harness and workflow documentation paths.
## Security boundary review
- PASS — harness server binds to `127.0.0.1`; browser guard blocks non-loopback URLs and Scalar/provider-token patterns.
- PASS — no downloaded code, vendored binaries, copied prototype assets/classes, or external runtime requests found.
- PASS — no Scalar URL/token or provider secret found; screenshots/logs/trace artifacts are gitignored.
- PASS — identity and tenant are derived from bearer-token server context; resource ownership checks cover tenant and student.
- PASS — pre-submit question responses omit answer and explanation; submitted reports expose them only after submission.
- PASS — production backend files were not changed by this harness workflow (existing unrelated production changes remain outside this review scope).
## Remaining limitations
- Local deterministic harness browser acceptance is complete; it is not a substitute for the real Student Web/H5 application.
- Real Student Web/H5 lint, type checking, tests, production build, and browser E2E remain blocked because those sources are not in this workspace.
- Real Scalar read-only smoke, Pilot deployment configuration, production database migration, rollback, and trace-to-upstream observability evidence require a deployment environment and approved credentials.

View File

@@ -0,0 +1,36 @@
# Endpoint matrix
All paths below are browser-relative `/app-api` routes. The server derives authenticated user and tenant context; the harness never sends those as business fields.
| Capability | Method | Relative route | Request/query used by harness | Expected data shape | Notes |
|---|---|---|---|---|---|
| Tenant resolution | GET | `/education/tenant/resolve` | deployment-specific resolver query; not called automatically | tenant resolution object | Use server entry-point/domain policy; do not accept a client tenant override. |
| Education context | GET | `/education/context` | none | `{ userId, tenantId, tenantName, displayName }` | Authenticated; verifies active tenant. |
| Regions | GET | `/education/catalog/regions` | none | array of region objects | Catalog read gate applies. |
| Categories | GET | `/education/catalog/categories` | `subjectId`, optional `nodeId` | array | Catalog read gate applies. |
| Subjects | GET | `/education/catalog/subjects` | optional `regionId`, `schoolId`, `majorId`, `moduleId`, `type` | array | Catalog read gate applies. |
| Question collections | GET | `/education/catalog/question-collections` | optional `regionId`, `entryId`, `nodeId`, `collectionType`, `limit` | array of collections | Harness uses this as the practice start list. |
| Safe question page | GET | `/education/questions/page` | `collectionId`, `pageNo`, `pageSize` | page result `{ list, total }` | Must not include answers or explanations. |
| Practice preview | GET | `/education/practice-config/preview` | request VO query fields | preview object | Validates criteria without creating a session. |
| Create practice | POST | `/education/practice-session/create` | `{ clientSessionId, collectionId, nodeId?, type?, difficulty?, questionCount }` | practice session | Idempotent by client session ID. |
| Current practice | GET | `/education/practice-session/current` | none | session or `null` | Used for refresh recovery. |
| Practice by ID | GET | `/education/practice-session/get` | `id` | session | Ownership and tenant checks are server-side. |
| Save answer | PUT | `/education/practice-session/answer` | `{ sessionId, questionSequence, selectedAnswer, idempotencyKey, clientSequence, expectedSessionVersion }` | answer save result with version | Idempotent and stale-write resistant. |
| Submit practice | POST | `/education/practice-session/submit` | `{ sessionId, idempotencyKey, expectedSessionVersion }` | submit/report result | Atomic one-way transition; safe retry. |
| Report | GET | `/education/practice-session/report` | `sessionId` | report with details | Correct answers/explanations only after submit. |
| Report history | GET | `/education/practice-session/reports` | `pageNo`, `pageSize` | page result | Current student only. |
| Wrong questions | GET | `/education/wrong-question/page` | `pageNo`, `pageSize`, optional `masterStatus` | page result | Current student only. |
| Wrong question detail | GET | `/education/wrong-question/get` | `id` | detail | Includes answer/explanation after failure is recorded. |
| Mark mastered | PUT | `/education/wrong-question/master` | `id` | boolean | Idempotent. |
| Unmark mastered | PUT | `/education/wrong-question/unmaster` | `id` | boolean | Idempotent. |
| Wrong-question review | POST | `/education/wrong-question/review-session` | `{ clientSessionId, wrongQuestionIds[] }` | practice session | Server validates ownership. |
| Favorites | GET | `/education/favorite/page` | `pageNo`, `pageSize`, optional `targetType` | page result | Current student only. |
| Favorite create | POST | `/education/favorite/create` | `{ targetType: 'QUESTION', targetId }` | favorite item | Idempotent. |
| Favorite delete | DELETE | `/education/favorite/delete` | `{ id? or targetType, targetId? }` | boolean | Logical/idempotent removal. |
| Favorite status | POST | `/education/favorite/status` | `{ questionIds[] }` | `{ questionIds }` | Batch status probe. |
## Envelope and failures
The project convention is a common result envelope. Successful payloads are expected under `data`; page payloads generally contain `list` and `total`. Errors should remain errors rather than becoming empty success data. Capture the server-provided request/trace ID for local investigation, but never record authorization headers or full sensitive response bodies.
The route prefix is intentionally `/app-api`, not a direct Scalar URL. If the local server uses another deployment prefix, adapt the reverse proxy rather than changing the harness to call Scalar.

View File

@@ -0,0 +1,60 @@
# Fixture schemas
Fixtures are synthetic documentation examples, not default application data and not copies of prototype data. They model the stable fields the harness reads.
## `context.json`
```json
{
"userId": 1001,
"tenantId": 2001,
"tenantName": "Local Pilot School",
"displayName": "Local Pilot School"
}
```
## `question-collection.json`
```json
{
"id": "collection-local-001",
"name": "Synthetic practice collection",
"collectionType": "QUESTION_BANK",
"questionCount": 3,
"status": "PUBLISHED"
}
```
## `practice-session.json`
```json
{
"id": 9001,
"clientSessionId": "local-session-001",
"status": "ACTIVE",
"questionCount": 3,
"sessionVersion": 1,
"questions": [
{
"sequence": 1,
"questionId": "question-local-001",
"contentVersion": "v1",
"stem": "Synthetic question content",
"type": "choice",
"options": [{ "label": "A", "content": "Synthetic option" }],
"selectedAnswer": null
}
]
}
```
## `page.json`
```json
{
"list": [],
"total": 0
}
```
Do not add `correctAnswer`, `explanation`, access tokens, phone numbers, real names, provider identifiers, or licensed question text to pre-submission fixtures. Post-submission report examples may include answer/explanation fields only when explicitly needed to document the permitted post-submit response boundary.

View File

@@ -0,0 +1,6 @@
{
"userId": 1001,
"tenantId": 2001,
"tenantName": "Local Pilot School",
"displayName": "Local Pilot School"
}

View File

@@ -0,0 +1,4 @@
{
"list": [],
"total": 0
}

View File

@@ -0,0 +1,18 @@
{
"id": 9001,
"clientSessionId": "local-session-001",
"status": "ACTIVE",
"questionCount": 3,
"sessionVersion": 1,
"questions": [
{
"sequence": 1,
"questionId": "question-local-001",
"contentVersion": "v1",
"stem": "Synthetic question content",
"type": "choice",
"options": [{ "label": "A", "content": "Synthetic option" }],
"selectedAnswer": null
}
]
}

View File

@@ -0,0 +1,7 @@
{
"id": "collection-local-001",
"name": "Synthetic practice collection",
"collectionType": "QUESTION_BANK",
"questionCount": 3,
"status": "PUBLISHED"
}

View File

@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Local-only student learning loop browser harness">
<title>Study loop / education harness</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="topbar">
<a class="wordmark" href="./" aria-label="Study loop home"><span class="wordmark-mark" aria-hidden="true"></span><span>study loop</span></a>
<div class="topbar-actions"><span id="identity-chip" class="identity-chip" data-testid="identity-chip">Offline</span><button id="logout" class="quiet-button" type="button" data-testid="logout">Log out</button></div>
</header>
<main class="page-shell">
<section class="intro" aria-labelledby="page-title">
<div><p class="kicker">STUDENT / CORE LOOP</p><h1 id="page-title">Make one good<br><em>pass through.</em></h1><p class="intro-copy">A small, honest browser seam for finding a set, practising, and learning from the misses.</p></div>
<div class="connection-card" aria-live="polite"><span class="connection-dot" id="connection-dot"></span><span id="status" data-testid="status">Not connected</span><span id="request-id" class="request-id"></span></div>
</section>
<div class="safety-note" role="note"><span aria-hidden="true"></span><span><strong>Local harness.</strong> Calls stay on relative <code>/app-api</code> routes. Your token lives in memory and your tenant is always server-derived.</span></div>
<section class="auth-panel" id="auth-panel" aria-labelledby="auth-title">
<div class="section-label"><span>01</span><span>Entry</span></div>
<div class="auth-main"><div><h2 id="auth-title">Connect your study space</h2><p>Resolve the tenant, then use an existing member account.</p></div><form id="auth-form"><label for="mobile">Member login</label><div class="login-fields"><input id="mobile" type="tel" autocomplete="username" placeholder="Mobile number"><input id="password" type="password" autocomplete="current-password" placeholder="Password"><button class="button button-dark" type="submit" data-testid="connect">Log in</button></div><p class="field-help">Local stub accepts <code>tenant-a-student-1</code> as a token below, or use the server's member credentials.</p><label class="token-label" for="token">Local access token <span>(memory only, test fallback)</span></label><input id="token" type="password" autocomplete="off" placeholder="tenant-a-student-1"></form></div>
<dl class="identity-grid" id="context" data-testid="context"><div><dt>Tenant</dt><dd>Not resolved</dd></div><div><dt>Student</dt><dd>Not authenticated</dd></div></dl>
</section>
<div class="workspace">
<nav class="side-nav" aria-label="Learning loop sections"><p class="nav-title">Your loop</p><a href="#discover" class="nav-link active"><span>01</span>Find a set</a><a href="#practice" class="nav-link"><span>02</span>Practice</a><a href="#review" class="nav-link"><span>03</span>Review</a><a href="#favorites" class="nav-link"><span>04</span>Keep close</a><p class="nav-foot">Server truth<br><span id="state-readout">No session</span></p></nav>
<div class="content-column">
<section class="content-section" id="discover" aria-labelledby="discover-title"><div class="section-label"><span>02</span><span>Discover</span></div><div class="section-heading"><div><h2 id="discover-title">Choose a question set</h2><p>Only published collections permitted for your space appear here.</p></div><button class="button button-outline" id="load-catalog" type="button" data-testid="load-catalog">Load catalog</button></div><fieldset class="filters"><legend class="sr-only">Catalog filters</legend><label>Subject<select id="subject-filter" data-testid="subject-filter"><option value="">All subjects</option></select></label><label>Category<select id="category-filter"><option value="">All categories</option></select></label></fieldset><div id="collections" class="collection-grid" data-testid="collections"><p class="empty-state">Connect first, then load your permitted sets.</p></div></section>
<section class="content-section practice-section" id="active-practice" aria-labelledby="practice-title"><div class="section-label"><span>03</span><span>Active work</span></div><div class="section-heading"><div><h2 id="practice-title">Practice, without losing your place</h2><p id="practice-subtitle">Your latest accepted answer is the durable one.</p></div><button class="button button-outline" id="load-current" type="button" data-testid="reload-current">Reload current</button></div><div id="practice" class="practice-card" data-testid="practice"><p class="empty-state">No active session. Start one above.</p></div></section>
<section class="content-section result-grid" id="review"><div class="result-panel"><div class="section-label"><span>04</span><span>Review</span></div><div class="section-heading"><div><h2>Wrong questions</h2><p>Turn a miss into the next pass.</p></div><button class="button button-outline" id="load-wrong" type="button" data-testid="load-wrong">Load</button></div><div id="wrong" class="item-list" data-testid="wrong"><p class="empty-state">Not loaded.</p></div></div><div class="result-panel" id="favorites"><div class="section-label"><span>05</span><span>Keep close</span></div><div class="section-heading"><div><h2>Favorites</h2><p>A short list worth returning to.</p></div><button class="button button-outline" id="load-favorites" type="button" data-testid="load-favorites">Load</button></div><div id="favorites-list" class="item-list" data-testid="favorites-list"><p class="empty-state">Not loaded.</p></div></div></section>
</div>
</div>
</main>
<footer><span>Education student harness</span><a href="endpoint-matrix.md">Endpoint matrix</a><a href="fixtures/README.md">Fixture schemas</a></footer>
<script type="module" src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
{
"name": "education-student-harness",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "education-student-harness",
"devDependencies": {
"@playwright/test": "^1.52.0"
}
},
"node_modules/@playwright/test": {
"version": "1.62.0",
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.62.0.tgz",
"integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.62.0",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.62.0.tgz",
"integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.0",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.62.0.tgz",
"integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "education-student-harness",
"private": true,
"description": "Offline-safe browser acceptance harness for the education student core loop",
"scripts": {
"smoke": "node smoke-route.test.js",
"contract": "node test.js && node adapter.test.js",
"test": "npm run smoke && npm run contract && npm run browser:if-available",
"browser": "playwright test",
"browser:if-available": "node run-playwright-if-available.js"
},
"devDependencies": {
"@playwright/test": "^1.52.0"
}
}

View File

@@ -0,0 +1,25 @@
// @ts-check
const { defineConfig } = require('@playwright/test');
const port = process.env.PW_PORT || '4197';
module.exports = defineConfig({
testDir: '.',
testMatch: /acceptance\.spec\.js$/,
timeout: 30_000,
fullyParallel: false,
reporter: [['list'], ['json', { outputFile: 'artifacts/playwright-results.json' }]],
use: {
baseURL: process.env.BASE_URL || `http://127.0.0.1:${port}`,
headless: true,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'off',
},
webServer: process.env.BASE_URL ? undefined : {
command: `PORT=${port} node server.js`,
url: `http://127.0.0.1:${port}`,
reuseExistingServer: false,
timeout: 10_000,
},
});

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env node
'use strict';
const { spawnSync } = require('child_process');
const path = require('path');
const cwd = __dirname;
const result = spawnSync(process.execPath, ['-e', "try { require.resolve('@playwright/test'); require.resolve('playwright'); } catch (_) { process.exit(2); }"], { cwd, stdio: 'inherit' });
if (result.status === 2) {
process.stdout.write('Playwright unavailable; dependency-free smoke/contract checks remain available.\n');
process.exit(0);
}
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const run = spawnSync(command, ['playwright', 'test'], { cwd, stdio: 'inherit' });
process.exit(run.status == null ? 1 : run.status);

Some files were not shown because too many files have changed in this diff Show More