diff --git a/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/dataobject/catalog/QuestionCollectionDO.java b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/dataobject/catalog/QuestionCollectionDO.java index 4484e4bd..343d6a19 100644 --- a/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/dataobject/catalog/QuestionCollectionDO.java +++ b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/dataobject/catalog/QuestionCollectionDO.java @@ -32,4 +32,5 @@ public class QuestionCollectionDO extends CatalogScopeDO { private String metadata; private String publicationStatus; private Integer authoringVersion; + private Integer membershipVersion; } diff --git a/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/QuestionCollectionMapper.java b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/QuestionCollectionMapper.java index 8ee3c9c7..a1a15c61 100644 --- a/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/QuestionCollectionMapper.java +++ b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/QuestionCollectionMapper.java @@ -65,6 +65,7 @@ public interface QuestionCollectionMapper extends BaseMapperX= 0), + ADD CONSTRAINT ck_education_question_collection_membership_version CHECK (membership_version >= 0), ADD CONSTRAINT ck_education_question_collection_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)); @@ -51,7 +53,7 @@ BEGIN IF NEW.scope='PUBLIC' THEN IF NEW.publication_status<>'ACTIVE' OR NOT NEW.is_active OR NEW.is_hidden OR NEW.authoring_version<>0 THEN RAISE EXCEPTION 'PUBLIC collection writes are not tenant authoring' USING ERRCODE='23514'; END IF; - ELSIF NEW.scope='TENANT_OWNED' AND (NEW.collection_type<>'MANUAL' OR NEW.publication_status<>'DRAFT' OR NEW.is_active OR NOT NEW.is_hidden OR NEW.authoring_version<>0 OR NEW.question_count<>0) THEN + ELSIF NEW.scope='TENANT_OWNED' AND (NEW.collection_type<>'MANUAL' OR NEW.publication_status<>'DRAFT' OR NEW.is_active OR NOT NEW.is_hidden OR NEW.authoring_version<>0 OR NEW.membership_version<>0 OR NEW.question_count<>0) THEN RAISE EXCEPTION 'tenant collection must start empty DRAFT' USING ERRCODE='23514'; END IF; RETURN NEW; END IF; @@ -62,13 +64,17 @@ BEGIN IF NEW.authoring_version<>OLD.authoring_version+1 THEN RAISE EXCEPTION 'collection authoring version must advance exactly once' USING ERRCODE='23514'; END IF; IF OLD.publication_status<>'DRAFT' AND (NEW.entry_id IS DISTINCT FROM OLD.entry_id OR NEW.node_id IS DISTINCT FROM OLD.node_id OR NEW.name IS DISTINCT FROM OLD.name OR NEW.title IS DISTINCT FROM OLD.title OR NEW.collection_type IS DISTINCT FROM OLD.collection_type OR - NEW.question_count IS DISTINCT FROM OLD.question_count OR NEW.duration_minutes IS DISTINCT FROM OLD.duration_minutes OR + NEW.question_count IS DISTINCT FROM OLD.question_count OR NEW.membership_version IS DISTINCT FROM OLD.membership_version OR NEW.duration_minutes IS DISTINCT FROM OLD.duration_minutes OR NEW.access_rules IS DISTINCT FROM OLD.access_rules OR NEW.sort_order IS DISTINCT FROM OLD.sort_order OR NEW.metadata IS DISTINCT FROM OLD.metadata) THEN RAISE EXCEPTION 'active collection content is immutable' USING ERRCODE='23514'; END IF; - IF NEW.publication_status IS NOT DISTINCT FROM OLD.publication_status AND OLD.publication_status='DRAFT' - AND NEW.question_count IS DISTINCT FROM OLD.question_count THEN + IF NEW.membership_version IS DISTINCT FROM OLD.membership_version THEN + IF OLD.publication_status<>'DRAFT' OR NEW.publication_status<>'DRAFT' OR NEW.membership_version<>OLD.membership_version+1 THEN + RAISE EXCEPTION 'collection membership version must advance exactly once in DRAFT' USING ERRCODE='23514'; + END IF; EXECUTE format('INSERT INTO %I.education_question_collection_membership_token VALUES ($1,$2,$3,$4)',TG_TABLE_SCHEMA) USING pg_current_xact_id()::text::BIGINT,NEW.tenant_id,NEW.id,NEW.authoring_version; + ELSIF NEW.question_count IS DISTINCT FROM OLD.question_count THEN + RAISE EXCEPTION 'collection question_count requires membership version advance' USING ERRCODE='23514'; END IF; IF NEW.publication_status IS DISTINCT FROM OLD.publication_status THEN PERFORM pg_advisory_xact_lock(NEW.id); @@ -174,7 +180,7 @@ CREATE TRIGGER trg_education_collection_membership_mutation BEFORE INSERT OR UPD CREATE TRIGGER trg_education_question_active_collection_archive BEFORE UPDATE OF status ON education_question FOR EACH ROW EXECUTE FUNCTION education_block_active_collection_question_archive(); CREATE TRIGGER trg_education_question_collection_audit_validate BEFORE INSERT ON education_question_collection_lifecycle_audit FOR EACH ROW EXECUTE FUNCTION education_validate_question_collection_audit(); CREATE TRIGGER trg_education_question_collection_audit_immutable BEFORE UPDATE OR DELETE ON education_question_collection_lifecycle_audit FOR EACH ROW EXECUTE FUNCTION education_prevent_question_collection_audit_mutation(); -CREATE CONSTRAINT TRIGGER trg_education_question_collection_membership_count AFTER UPDATE OF question_count ON education_question_collection DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION education_require_question_collection_membership_count(); +CREATE CONSTRAINT TRIGGER trg_education_question_collection_membership_count AFTER UPDATE OF membership_version ON education_question_collection DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION education_require_question_collection_membership_count(); CREATE CONSTRAINT TRIGGER trg_education_question_collection_audit_required AFTER UPDATE OF publication_status ON education_question_collection DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION education_require_question_collection_audit(); DO $$ DECLARE installed INTEGER; BEGIN diff --git a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/collection/authoring/QuestionCollectionAuthoringPostgreSqlIntegrationTest.java b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/collection/authoring/QuestionCollectionAuthoringPostgreSqlIntegrationTest.java index 1ae9e578..a7c2ac3a 100644 --- a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/collection/authoring/QuestionCollectionAuthoringPostgreSqlIntegrationTest.java +++ b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/collection/authoring/QuestionCollectionAuthoringPostgreSqlIntegrationTest.java @@ -62,6 +62,24 @@ class QuestionCollectionAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbI assertEquals(2L, auditMapper.selectCount()); } + @Test void sameSizeAndEmptyMembershipReplacementAdvanceSharedVersion() { + Long id = service.createDraft(command(null)); + assertEquals(1, service.replaceMembership(id, List.of(publishedQuestionIds.get(0)), 0)); + assertEquals(2, service.replaceMembership(id, List.of(publishedQuestionIds.get(1)), 1)); + assertEquals(List.of(publishedQuestionIds.get(1)), rawMembership(id)); + Long empty = service.createDraft(command(null)); + assertEquals(1, service.replaceMembership(empty, List.of(), 0)); + assertEquals(1, collectionMapper.selectTenantOwnedById(10L, empty).getMembershipVersion()); + } + + private List rawMembership(Long collectionId) { + return cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> membershipMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(QuestionCollectionQuestionDO::getCollectionId, collectionId) + .orderByAsc(QuestionCollectionQuestionDO::getSortOrder))).stream() + .map(QuestionCollectionQuestionDO::getQuestionId).toList(); + } + @Test void duplicateAndIneligibleMembershipFailClosedWithoutChangingVersion() { Long id = service.createDraft(command(null)); ServiceException duplicate = assertThrows(ServiceException.class, () -> service.replaceMembership(id, List.of(publishedQuestionIds.get(0), publishedQuestionIds.get(0)), 0));