forked from wangziqi/ruoyi-vue-pro
164 lines
18 KiB
Markdown
164 lines
18 KiB
Markdown
# 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 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.
|