forked from wangziqi/ruoyi-vue-pro
feat(education): add tenant question placement
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
-- EDU-010: make tenant Question Placement an explicit optimistic command and
|
||||
-- require a stable, available Content Node before native publication.
|
||||
|
||||
ALTER TABLE education_question
|
||||
ADD COLUMN placement_version INTEGER NOT NULL DEFAULT 0,
|
||||
ADD CONSTRAINT ck_education_question_placement_version
|
||||
CHECK (placement_version >= 0);
|
||||
|
||||
COMMENT ON COLUMN education_question.placement_version IS
|
||||
'题目目录放置的乐观锁版本;不属于不可变题目内容版本';
|
||||
|
||||
CREATE FUNCTION education_enforce_question_placement_mutation()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.node_id IS NOT DISTINCT FROM OLD.node_id
|
||||
AND NEW.placement_version IS NOT DISTINCT FROM OLD.placement_version THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF NEW.scope <> 'TENANT_OWNED' OR NEW.tenant_id <= 0 THEN
|
||||
RAISE EXCEPTION 'PUBLIC question placement is not managed by tenant authoring'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF OLD.status <> 'DRAFT' OR NEW.status <> 'DRAFT' THEN
|
||||
RAISE EXCEPTION 'published or archived question placement is immutable'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.node_id IS NOT DISTINCT FROM OLD.node_id THEN
|
||||
RAISE EXCEPTION 'question placement version cannot change without placement'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.placement_version IS NULL
|
||||
OR NEW.placement_version <> OLD.placement_version + 1 THEN
|
||||
RAISE EXCEPTION 'question placement version must advance exactly once'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION education_enforce_question_lifecycle_transition()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
placement_node_id BIGINT;
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF NEW.status <> 'DRAFT' OR NEW.is_published THEN
|
||||
RAISE EXCEPTION 'new questions must start in DRAFT'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF NEW.status IS DISTINCT FROM OLD.status
|
||||
AND (NEW.scope <> 'TENANT_OWNED' OR NEW.tenant_id <= 0) THEN
|
||||
RAISE EXCEPTION 'PUBLIC question lifecycle is not managed by tenant authoring'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.status IS DISTINCT FROM OLD.status AND NOT (
|
||||
(OLD.status = 'DRAFT' AND NEW.status = 'PUBLISHED') OR
|
||||
(OLD.status = 'PUBLISHED' AND NEW.status = 'ARCHIVED')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'invalid question lifecycle transition from % to %', OLD.status, NEW.status
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF OLD.status = 'DRAFT' AND NEW.status = 'PUBLISHED' THEN
|
||||
IF NEW.node_id IS NULL THEN
|
||||
RAISE EXCEPTION 'question must be placed before publication'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
EXECUTE format(
|
||||
'SELECT id FROM %I.education_content_node' ||
|
||||
' WHERE id = $1 AND deleted = false AND is_active = true' ||
|
||||
' AND is_hidden = false AND is_selectable = true FOR SHARE',
|
||||
TG_TABLE_SCHEMA)
|
||||
INTO placement_node_id
|
||||
USING NEW.node_id;
|
||||
IF placement_node_id IS NULL THEN
|
||||
RAISE EXCEPTION 'question placement target is unavailable'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
END IF;
|
||||
IF NEW.status IS DISTINCT FROM OLD.status THEN
|
||||
EXECUTE format(
|
||||
'INSERT INTO %I.education_question_lifecycle_transition_token' ||
|
||||
' (transaction_id, tenant_id, question_id, content_version, from_status, to_status)' ||
|
||||
' VALUES ($1, $2, $3, $4, $5, $6)',
|
||||
TG_TABLE_SCHEMA)
|
||||
USING pg_current_xact_id()::text::BIGINT, NEW.tenant_id, NEW.id,
|
||||
NEW.content_version, OLD.status, NEW.status;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION education_enforce_question_placement_mutation() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_enforce_question_lifecycle_transition() FROM PUBLIC;
|
||||
|
||||
CREATE TRIGGER trg_education_question_placement_mutation
|
||||
BEFORE UPDATE OF node_id, placement_version ON education_question
|
||||
FOR EACH ROW EXECUTE FUNCTION education_enforce_question_placement_mutation();
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
installed_permission_count INTEGER;
|
||||
BEGIN
|
||||
IF to_regclass('system_menu') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id, path, icon, component,
|
||||
component_name, status, visible, keep_alive, always_show, creator, updater
|
||||
) VALUES
|
||||
(6805, '题目归类', 'education:question:classify', 3, 5, 6800, '', '', NULL,
|
||||
NULL, 0, false, true, true, 'education-flyway', 'education-flyway')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
SELECT count(*)
|
||||
INTO installed_permission_count
|
||||
FROM system_menu
|
||||
WHERE id = 6805
|
||||
AND permission = 'education:question:classify'
|
||||
AND type = 3
|
||||
AND parent_id = 6800
|
||||
AND status = 0
|
||||
AND deleted = 0;
|
||||
IF installed_permission_count <> 1 THEN
|
||||
RAISE EXCEPTION 'Education Question Placement RBAC seed ID conflicts with existing system_menu row'
|
||||
USING ERRCODE = '23505';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user