fix(education): control content node visibility

This commit is contained in:
2026-07-30 23:50:50 +08:00
parent e0a695978c
commit f84aecf8f5
9 changed files with 36 additions and 20 deletions

View File

@@ -51,7 +51,7 @@ public class ContentNodeAuthoringController {
private ContentNodeAuthoringCommand toCommand(ContentNodeDraftReqVO request, Integer expectedVersion) {
return new ContentNodeAuthoringCommand(request.getEntryId(), request.getParentId(), request.getName(),
request.getTitle(), request.getNodeType(), request.getMarkerType(), request.getLeaf(),
request.getSelectable(), request.getHidden(), request.getSortOrder(), request.getMetadata(), expectedVersion);
request.getTitle(), request.getNodeType(), request.getMarkerType(), request.getSelectable(),
request.getSortOrder(), request.getMetadata(), expectedVersion);
}
}

View File

@@ -15,9 +15,7 @@ public class ContentNodeDraftReqVO {
@Size(max = 200) private String title;
@NotBlank @Size(max = 50) private String nodeType;
@Size(max = 50) private String markerType;
@NotNull private Boolean leaf;
@NotNull private Boolean selectable;
@NotNull private Boolean hidden;
private Integer sortOrder;
private String metadata;
}

View File

@@ -65,6 +65,7 @@ public interface ContentNodeMapper extends BaseMapperX<ContentNodeDO> {
.eq(ContentNodeDO::getAuthoringVersion, expectedVersion)
.set(ContentNodeDO::getPublicationStatus, targetStatus)
.set(ContentNodeDO::getIsActive, active)
.set(ContentNodeDO::getIsHidden, !active)
.set(ContentNodeDO::getAuthoringVersion, expectedVersion + 1));
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.education.service.contentnode.authoring;
public record ContentNodeAuthoringCommand(Long entryId, Long parentId, String name, String title,
String nodeType, String markerType, Boolean leaf,
Boolean selectable, Boolean hidden, Integer sortOrder,
String metadata, Integer expectedAuthoringVersion) {
String nodeType, String markerType, Boolean selectable,
Integer sortOrder, String metadata,
Integer expectedAuthoringVersion) {
}

View File

@@ -38,7 +38,8 @@ public class ContentNodeAuthoringServiceImpl implements ContentNodeAuthoringServ
ContentNodeDO node = new ContentNodeDO();
apply(node, command);
node.setTenantId(tenantId); node.setScope(TENANT_OWNED);
node.setPublicationStatus("DRAFT"); node.setAuthoringVersion(0); node.setIsActive(false);
node.setPublicationStatus("DRAFT"); node.setAuthoringVersion(0);
node.setIsActive(false); node.setIsHidden(true); node.setIsLeaf(true);
nodeMapper.insert(node);
return node.getId();
}
@@ -105,7 +106,7 @@ public class ContentNodeAuthoringServiceImpl implements ContentNodeAuthoringServ
private void apply(ContentNodeDO node, ContentNodeAuthoringCommand command) {
node.setEntryId(command.entryId()); node.setParentId(command.parentId()); node.setName(command.name());
node.setTitle(command.title()); node.setNodeType(command.nodeType()); node.setMarkerType(command.markerType());
node.setIsLeaf(command.leaf()); node.setIsSelectable(command.selectable()); node.setIsHidden(command.hidden());
node.setIsSelectable(command.selectable());
node.setSortOrder(command.sortOrder() != null ? command.sortOrder() : 0); node.setMetadata(command.metadata());
}

View File

@@ -6,14 +6,18 @@ ALTER TABLE education_content_node
ADD COLUMN authoring_version INTEGER NOT NULL DEFAULT 0;
UPDATE education_content_node
SET publication_status = CASE WHEN is_active THEN 'ACTIVE' ELSE 'ARCHIVED' END;
SET publication_status = CASE WHEN is_active AND NOT is_hidden THEN 'ACTIVE' ELSE 'ARCHIVED' END,
is_active = is_active AND NOT is_hidden,
is_hidden = NOT (is_active AND NOT is_hidden);
ALTER TABLE education_content_node
ADD CONSTRAINT ck_education_content_node_publication_status
CHECK (publication_status IN ('DRAFT', 'ACTIVE', 'ARCHIVED')),
ADD CONSTRAINT ck_education_content_node_authoring_version CHECK (authoring_version >= 0),
ADD CONSTRAINT ck_education_content_node_lifecycle_consistent
CHECK (is_active = (publication_status = 'ACTIVE'));
ADD CONSTRAINT ck_education_content_node_lifecycle_consistent CHECK (
(publication_status = 'ACTIVE' AND is_active AND NOT is_hidden) OR
(publication_status IN ('DRAFT', 'ARCHIVED') AND NOT is_active AND is_hidden)
);
COMMENT ON COLUMN education_content_node.publication_status IS '内容节点发布状态DRAFT、ACTIVE、ARCHIVED';
COMMENT ON COLUMN education_content_node.authoring_version IS '内容节点创作和生命周期共用的乐观锁版本';
@@ -47,11 +51,13 @@ RETURNS TRIGGER LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.scope = 'PUBLIC' THEN
IF NEW.publication_status <> 'ACTIVE' OR NOT NEW.is_active OR NEW.authoring_version <> 0 THEN
IF NEW.publication_status <> 'ACTIVE' OR NOT NEW.is_active OR NEW.is_hidden
OR NEW.authoring_version <> 0 THEN
RAISE EXCEPTION 'PUBLIC content node writes are not managed by tenant authoring' USING ERRCODE = '23514';
END IF;
ELSIF NEW.scope = 'TENANT_OWNED' AND
(NEW.publication_status <> 'DRAFT' OR NEW.is_active OR NEW.authoring_version <> 0) THEN
(NEW.publication_status <> 'DRAFT' OR NEW.is_active OR NOT NEW.is_hidden
OR NEW.authoring_version <> 0) THEN
RAISE EXCEPTION 'tenant content nodes must start in DRAFT' USING ERRCODE = '23514';
END IF;
RETURN NEW;

View File

@@ -57,7 +57,7 @@ class ContentNodeAuthoringControllerContractTest {
security.permissions.add("education:content-node:author");
ContentNodeReviseReqVO request = new ContentNodeReviseReqVO();
request.setExpectedAuthoringVersion(2); request.setName("Revised"); request.setNodeType("category");
request.setEntryId(100L); request.setSelectable(true); request.setHidden(false); request.setLeaf(true);
request.setEntryId(100L); request.setSelectable(true);
assertEquals(3, controller.revise(101L, request).getData());
assertEquals(2, service.command.expectedAuthoringVersion());
}
@@ -65,7 +65,7 @@ class ContentNodeAuthoringControllerContractTest {
private ContentNodeDraftReqVO request() {
ContentNodeDraftReqVO request = new ContentNodeDraftReqVO();
request.setEntryId(100L); request.setName("Algebra"); request.setNodeType("category");
request.setSelectable(true); request.setHidden(false); request.setLeaf(true);
request.setSelectable(true);
return request;
}

View File

@@ -38,15 +38,25 @@ class ContentNodeAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbIntegrat
@Test void draftActivateArchiveControlsStudentDiscoveryAndPlacement() {
Long id = service.createDraft(command(null, null, "Algebra", null));
ContentNodeDO draft = nodeMapper.selectTenantOwnedById(10L, id);
assertFalse(draft.getIsActive());
assertTrue(draft.getIsHidden());
assertTrue(draft.getIsLeaf());
assertEquals(0, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
assertNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
assertEquals(1, service.reviseDraft(id, command(null, null, "Algebra revised", 0)));
assertEquals(2, service.activate(id, 1, 7L));
ContentNodeDO active = nodeMapper.selectTenantOwnedById(10L, id);
assertTrue(active.getIsActive());
assertFalse(active.getIsHidden());
assertEquals(1, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
assertNotNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
assertEquals(3, service.archive(id, 2, 7L));
ContentNodeDO archived = nodeMapper.selectTenantOwnedById(10L, id);
assertFalse(archived.getIsActive());
assertTrue(archived.getIsHidden());
assertEquals(0, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
assertNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
assertEquals(2L, auditMapper.selectCount());
@@ -74,6 +84,6 @@ class ContentNodeAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbIntegrat
private ContentNodeAuthoringCommand command(Long entryId, Long parentId, String name, Integer version) {
return new ContentNodeAuthoringCommand(entryId != null ? entryId : 100L, parentId, name, null,
"category", null, true, true, false, 0, null, version);
"category", null, true, 0, null, version);
}
}

View File

@@ -484,12 +484,12 @@ class EducationFlywayMigrationIntegrationTest {
(id, tenant_id, scope, entry_key, name, entry_type)
VALUES (100, 10, 'TENANT_OWNED', 'questions', 'Questions', 'question');
INSERT INTO education_content_node
(id, tenant_id, scope, entry_id, name, node_type, publication_status, is_active)
VALUES (200, 10, 'TENANT_OWNED', 100, 'Draft', 'category', 'DRAFT', false);
(id, tenant_id, scope, entry_id, name, node_type, publication_status, is_active, is_hidden)
VALUES (200, 10, 'TENANT_OWNED', 100, 'Draft', 'category', 'DRAFT', false, true);
""");
assertThatThrownBy(() -> execute(schema, """
UPDATE education_content_node
SET publication_status = 'ACTIVE', is_active = true, authoring_version = 1
SET publication_status = 'ACTIVE', is_active = true, is_hidden = false, authoring_version = 1
WHERE id = 200;
""")).hasMessageContaining("content node lifecycle audit must accompany its transition");
assertThatThrownBy(() -> execute(schema, """