feat(education): add tenant question placement

This commit is contained in:
2026-07-30 22:28:08 +08:00
parent 34bc1fe41e
commit 2a40cbd69e
21 changed files with 813 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
# EDU-010 — Tenant content publication and graph integrity
- **Status:** in progress — catalog graph-integrity and the verified JAVA_READ tenant-question lifecycle slice are delivered; broader classification/collection authoring remains
- **Status:** in progress — catalog graph-integrity, the verified JAVA_READ tenant-question lifecycle, and Question Placement slices are delivered; category, collection, and blueprint authoring remain
- **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)
@@ -26,7 +26,8 @@ Authorized tenant administrators can author, classify, publish, archive, and ret
- [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] Database graph-integrity changes use `flyway-postgresql` and forward migration V4070.
- [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.
## Delivery progress — graph-integrity slice (2026-07-30)
@@ -67,22 +68,35 @@ Delivered:
- 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; classification, collection, and platform-curator write paths remain outside this bounded slice.
- 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 and tenant-question lifecycle slice. EDU-010 remains in progress because its tenant-admin outcome also includes classification, collection, blueprint, retire, and platform-curator workflows that are not part of this bounded slice.
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, Content Node authoring, Collection/Blueprint authoring, PUBLIC curator workflows, and a distinct Retire state remain outside this slice. In the target domain, `education_category` is not connected to Question; the delivered classification seam is explicitly Question Placement through `question.node_id`.
## Verification evidence (2026-07-30)
- `mvn -pl yudao-module-education test` with the five `EDU_TEST_POSTGRES_*` variables pointed at the local Docker PostgreSQL: **485 tests passed**, including 18 Flyway migration tests and five lifecycle PostgreSQL integration tests. V4080 was actually executed in disposable PostgreSQL schemas.
- `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/publish/archive requires its own permission and rejected calls do not reach the lifecycle service.
- 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`: present after the module build.
- `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:** Disable authoring/publishing and roll back application; preserve data and correct forward.
- **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.