Files
ruoyi-vue-pro/docs/education/migration/issues/EDU-010-content-publication.md

19 KiB

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

  • Admin permission and data-scope matrix is explicit.
  • Cross-tenant graph relationships cannot be persisted in the current V4020 catalog graph.
  • PUBLIC and tenant-owned reference rules are enforced across the current 19 reference edges, with exact trigger-mapping and focused behavior tests.
  • V4080 extends the graph guard to the new Question Version → Question edge and makes the version table the twelfth ownership-protected catalog table.
  • Unpublished/archived content is never student-visible.
  • Publication is transactional and auditable.
  • Tenant Question Placement is permission-separated, tenant-safe, optimistic, and frozen after publication.
  • Database graph-integrity changes use flyway-postgresql and forward migrations V4070/V4090.
  • The next Manual Question Collection bounded contract, exclusions, pre-provisioning constraint, and readiness gates are explicit before implementation.
  • A current-tenant TENANT_OWNED Manual Question Collection can be authored only in JAVA_READ on an existing ACTIVE, visible Content Node.
  • Collection lifecycle is DRAFT → ACTIVE → ARCHIVED with one optimistic authoring version; ACTIVE content/membership is immutable and archive is terminal.
  • DRAFT membership is ordered replace-all, accepts only current-tenant TENANT_OWNED PUBLISHED Questions, and rejects duplicates.
  • 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/draftseducation:question:author;
    • PUT /admin-api/education/questions/{id}/publisheducation:question:publish;
    • PUT /admin-api/education/questions/{id}/archiveeducation: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

  • Current target evidence identifies education_question_collection_question as the sole collection-membership fact, with tenant-scoped duplicate prevention and deterministic membership order.
  • 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.
  • Provider-neutral option safety is already resolved by EDU-001 and QuestionContentSafety; it is not an EDU-010 blocker.
  • 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.
  • Before implementation, allocate V4110 through flyway-postgresql and deliver forward adoption from the boolean collection availability fields.
  • Collection listing and collection-question reads validate ACTIVE collection lifecycle and Content Node availability at the route boundary.
  • Separate collection author/publish/archive permissions, tenant-bound commands, transactional lifecycle audit, CAS conflicts, server-derived question_count, and transaction boundaries are implemented.
  • 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.