forked from wangziqi/ruoyi-vue-pro
feat(education): add native question publication lifecycle
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
-- EDU-010: make native question authoring draft-first and keep lifecycle audit
|
||||
-- in the same PostgreSQL transaction as publication state changes.
|
||||
|
||||
CREATE FUNCTION education_question_answer_key_is_valid(
|
||||
question_type VARCHAR,
|
||||
answer_key VARCHAR,
|
||||
question_options JSONB
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
normalized_type VARCHAR;
|
||||
answer_labels JSONB;
|
||||
option_count INTEGER;
|
||||
distinct_label_count INTEGER;
|
||||
ordered_option_count INTEGER;
|
||||
distinct_order_count INTEGER;
|
||||
BEGIN
|
||||
normalized_type := lower(trim(COALESCE(question_type, '')));
|
||||
IF answer_key IS NULL OR btrim(answer_key) = ''
|
||||
OR jsonb_typeof(question_options) <> 'array' THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
IF normalized_type IN (
|
||||
'fill', 'text', 'terms', 'short_answer', 'composition', 'discuss',
|
||||
'translation', 'case_analysis', 'brief_analysis', 'calculation',
|
||||
'analysis_design', 'combination', 'solution'
|
||||
) THEN
|
||||
RETURN jsonb_array_length(question_options) = 0;
|
||||
END IF;
|
||||
IF normalized_type NOT IN ('choice', 'multi', 'multi_choice', 'judge', 'image')
|
||||
OR jsonb_array_length(question_options) < 2 THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT count(*),
|
||||
count(DISTINCT btrim(item ->> 'label')),
|
||||
count(*) FILTER (WHERE jsonb_typeof(item -> 'order') = 'number'),
|
||||
count(DISTINCT (item ->> 'order')::NUMERIC)
|
||||
FILTER (WHERE jsonb_typeof(item -> 'order') = 'number')
|
||||
INTO option_count, distinct_label_count, ordered_option_count, distinct_order_count
|
||||
FROM jsonb_array_elements(question_options) item
|
||||
WHERE jsonb_typeof(item) = 'object'
|
||||
AND (SELECT count(*) FROM jsonb_object_keys(item)) = 3
|
||||
AND item ?& ARRAY['label', 'content', 'order']
|
||||
AND jsonb_typeof(item -> 'label') = 'string'
|
||||
AND btrim(item ->> 'label') <> ''
|
||||
AND jsonb_typeof(item -> 'content') = 'string'
|
||||
AND btrim(item ->> 'content') <> ''
|
||||
AND jsonb_typeof(item -> 'order') IN ('number', 'null');
|
||||
IF option_count <> jsonb_array_length(question_options)
|
||||
OR distinct_label_count <> option_count
|
||||
OR distinct_order_count <> ordered_option_count THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
IF normalized_type IN ('multi', 'multi_choice') THEN
|
||||
BEGIN
|
||||
answer_labels := answer_key::JSONB;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN false;
|
||||
END;
|
||||
IF jsonb_typeof(answer_labels) <> 'array'
|
||||
OR jsonb_array_length(answer_labels) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(answer_labels) answer
|
||||
WHERE jsonb_typeof(answer) <> 'string'
|
||||
OR btrim(answer #>> '{}') = ''
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(question_options) item
|
||||
WHERE btrim(item ->> 'label') = btrim(answer #>> '{}')
|
||||
)
|
||||
)
|
||||
OR (SELECT count(*) FROM jsonb_array_elements_text(answer_labels)) <>
|
||||
(SELECT count(DISTINCT btrim(value))
|
||||
FROM jsonb_array_elements_text(answer_labels) value) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
RETURN true;
|
||||
END IF;
|
||||
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(question_options) item
|
||||
WHERE btrim(item ->> 'label') = btrim(answer_key)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION education_question_answer_key_is_valid(VARCHAR, VARCHAR, JSONB) FROM PUBLIC;
|
||||
|
||||
-- Hold back concurrent inserts and updates until the historical safety check,
|
||||
-- normalization, and lifecycle write guards all commit together.
|
||||
LOCK TABLE education_question IN SHARE ROW EXCLUSIVE MODE;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
invalid_question_id BIGINT;
|
||||
BEGIN
|
||||
SELECT id
|
||||
INTO invalid_question_id
|
||||
FROM education_question
|
||||
WHERE status = 'PUBLISHED'
|
||||
AND is_published = true
|
||||
AND (
|
||||
stem IS NULL OR btrim(stem) = '' OR
|
||||
NOT education_question_answer_key_is_valid(type, correct_answer, options)
|
||||
)
|
||||
ORDER BY id
|
||||
LIMIT 1;
|
||||
IF invalid_question_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'unsafe historical published question % blocks V4080', invalid_question_id
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE education_question DROP CONSTRAINT ck_education_question_status;
|
||||
|
||||
UPDATE education_question
|
||||
SET status = CASE
|
||||
WHEN status = 'DRAFT' THEN 'DRAFT'
|
||||
WHEN status = 'PUBLISHED' AND is_published = true THEN 'PUBLISHED'
|
||||
ELSE 'ARCHIVED'
|
||||
END,
|
||||
is_published = CASE
|
||||
WHEN status = 'PUBLISHED' AND is_published = true THEN true
|
||||
ELSE false
|
||||
END;
|
||||
|
||||
ALTER TABLE education_question ALTER COLUMN status SET DEFAULT 'DRAFT';
|
||||
ALTER TABLE education_question ALTER COLUMN is_published SET DEFAULT false;
|
||||
ALTER TABLE education_question
|
||||
ADD CONSTRAINT ck_education_question_status
|
||||
CHECK (status IN ('DRAFT', 'PUBLISHED', 'ARCHIVED')),
|
||||
ADD CONSTRAINT ck_education_question_publication_consistent
|
||||
CHECK (is_published = (status = 'PUBLISHED'));
|
||||
|
||||
CREATE TABLE education_question_version (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL,
|
||||
question_id BIGINT NOT NULL,
|
||||
version_number INTEGER NOT NULL,
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
type_label VARCHAR(50),
|
||||
difficulty VARCHAR(32),
|
||||
question_content JSONB,
|
||||
options JSONB NOT NULL,
|
||||
correct_answer VARCHAR(500),
|
||||
explanation TEXT,
|
||||
analysis TEXT,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT ck_education_question_version_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT ck_education_question_version_number CHECK (version_number > 0),
|
||||
CONSTRAINT fk_education_question_version_question
|
||||
FOREIGN KEY (question_id) REFERENCES education_question (id),
|
||||
CONSTRAINT uk_education_question_version_question_number
|
||||
UNIQUE (question_id, version_number),
|
||||
CONSTRAINT uk_education_question_version_tenant_question_number
|
||||
UNIQUE (tenant_id, question_id, version_number)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_question_version IS
|
||||
'教育-题目不可变内容版本;当前版本号由 education_question.content_version 指向';
|
||||
COMMENT ON COLUMN education_question_version.correct_answer IS
|
||||
'服务端答案;不得进入学生端安全投影';
|
||||
|
||||
INSERT INTO education_question_version (
|
||||
tenant_id, scope, question_id, version_number, stem, type, type_label,
|
||||
difficulty, question_content, options, correct_answer, explanation, analysis,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
)
|
||||
SELECT tenant_id, scope, id, content_version, stem, type, type_label,
|
||||
difficulty, question_content, options, correct_answer, explanation, analysis,
|
||||
creator, create_time, updater, update_time, false
|
||||
FROM education_question;
|
||||
|
||||
ALTER TABLE education_question
|
||||
ADD CONSTRAINT fk_education_question_current_version
|
||||
FOREIGN KEY (id, content_version)
|
||||
REFERENCES education_question_version (question_id, version_number)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
CREATE TABLE education_question_lifecycle_audit (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
question_id BIGINT NOT NULL,
|
||||
content_version INTEGER NOT NULL,
|
||||
actor_id BIGINT NOT NULL,
|
||||
from_status VARCHAR(20) NOT NULL,
|
||||
to_status VARCHAR(20) NOT NULL,
|
||||
occurred_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT ck_education_question_lifecycle_audit_transition CHECK (
|
||||
(from_status = 'DRAFT' AND to_status = 'PUBLISHED') OR
|
||||
(from_status = 'PUBLISHED' AND to_status = 'ARCHIVED')
|
||||
),
|
||||
CONSTRAINT fk_education_question_lifecycle_audit_version
|
||||
FOREIGN KEY (tenant_id, question_id, content_version)
|
||||
REFERENCES education_question_version (tenant_id, question_id, version_number),
|
||||
CONSTRAINT uk_education_question_lifecycle_audit_transition
|
||||
UNIQUE (tenant_id, question_id, from_status, to_status)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_question_lifecycle_audit IS
|
||||
'教育-题目发布生命周期追加式领域审计,与状态转换同事务提交';
|
||||
CREATE INDEX idx_education_question_lifecycle_audit_tenant_question
|
||||
ON education_question_lifecycle_audit (tenant_id, question_id, occurred_at, id);
|
||||
|
||||
CREATE TABLE education_question_lifecycle_transition_token (
|
||||
transaction_id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
question_id BIGINT NOT NULL,
|
||||
content_version INTEGER NOT NULL,
|
||||
from_status VARCHAR(20) NOT NULL,
|
||||
to_status VARCHAR(20) NOT NULL,
|
||||
PRIMARY KEY (transaction_id, tenant_id, question_id, from_status, to_status)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_question_lifecycle_transition_token IS
|
||||
'题目状态触发器与审计触发器之间的事务内临时凭据;成功审计后即消费';
|
||||
REVOKE ALL ON TABLE education_question_lifecycle_transition_token FROM PUBLIC;
|
||||
|
||||
CREATE FUNCTION education_prevent_question_version_mutation()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'question versions are immutable'
|
||||
USING ERRCODE = '23514';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_enforce_question_lifecycle_transition()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
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 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;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_prevent_question_lifecycle_audit_mutation()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'question lifecycle audit is append-only'
|
||||
USING ERRCODE = '23514';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_prevent_question_content_projection_mutation()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'question content must change through immutable versions'
|
||||
USING ERRCODE = '23514';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_require_question_lifecycle_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
audit_exists BOOLEAN;
|
||||
BEGIN
|
||||
IF NEW.status IS NOT DISTINCT FROM OLD.status THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
EXECUTE format(
|
||||
'SELECT EXISTS (SELECT 1 FROM %I.education_question_lifecycle_audit' ||
|
||||
' WHERE tenant_id = $1 AND question_id = $2 AND content_version = $3' ||
|
||||
' AND from_status = $4 AND to_status = $5 AND deleted = false)',
|
||||
TG_TABLE_SCHEMA)
|
||||
INTO audit_exists
|
||||
USING NEW.tenant_id, NEW.id, NEW.content_version, OLD.status, NEW.status;
|
||||
IF NOT audit_exists THEN
|
||||
RAISE EXCEPTION 'question lifecycle transition requires transactional audit'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_check_question_version_ownership()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
question_tenant_id BIGINT;
|
||||
question_scope VARCHAR(20);
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'SELECT tenant_id, scope FROM %I.education_question WHERE id = $1 FOR SHARE',
|
||||
TG_TABLE_SCHEMA)
|
||||
INTO question_tenant_id, question_scope
|
||||
USING NEW.question_id;
|
||||
IF question_tenant_id IS NULL OR question_scope IS NULL THEN
|
||||
RAISE EXCEPTION 'question version points to missing question %', NEW.question_id
|
||||
USING ERRCODE = '23503';
|
||||
END IF;
|
||||
IF NEW.tenant_id IS DISTINCT FROM question_tenant_id
|
||||
OR NEW.scope IS DISTINCT FROM question_scope THEN
|
||||
RAISE EXCEPTION 'question version ownership must equal question ownership'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION education_validate_question_lifecycle_audit_insert()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
question_status VARCHAR(20);
|
||||
question_version INTEGER;
|
||||
transition_token_count INTEGER;
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'SELECT status, content_version FROM %I.education_question' ||
|
||||
' WHERE tenant_id = $1 AND id = $2 FOR SHARE',
|
||||
TG_TABLE_SCHEMA)
|
||||
INTO question_status, question_version
|
||||
USING NEW.tenant_id, NEW.question_id;
|
||||
IF question_status IS NULL OR question_version IS NULL
|
||||
OR question_status IS DISTINCT FROM NEW.to_status
|
||||
OR question_version IS DISTINCT FROM NEW.content_version THEN
|
||||
RAISE EXCEPTION 'question lifecycle audit must accompany its state transition'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
EXECUTE format(
|
||||
'DELETE FROM %I.education_question_lifecycle_transition_token' ||
|
||||
' WHERE transaction_id = $1 AND tenant_id = $2 AND question_id = $3' ||
|
||||
' AND content_version = $4 AND from_status = $5 AND to_status = $6',
|
||||
TG_TABLE_SCHEMA)
|
||||
USING pg_current_xact_id()::text::BIGINT, NEW.tenant_id, NEW.question_id,
|
||||
NEW.content_version, NEW.from_status, NEW.to_status;
|
||||
GET DIAGNOSTICS transition_token_count = ROW_COUNT;
|
||||
IF transition_token_count <> 1 THEN
|
||||
RAISE EXCEPTION 'question lifecycle audit must accompany its state transition'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION education_prevent_question_version_mutation() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_enforce_question_lifecycle_transition() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_prevent_question_lifecycle_audit_mutation() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_prevent_question_content_projection_mutation() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_require_question_lifecycle_audit() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_check_question_version_ownership() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION education_validate_question_lifecycle_audit_insert() FROM PUBLIC;
|
||||
|
||||
CREATE TRIGGER trg_education_question_version_reference_scope
|
||||
BEFORE INSERT OR UPDATE OF tenant_id, scope, question_id ON education_question_version
|
||||
FOR EACH ROW EXECUTE FUNCTION education_check_reference_scope('question_id', 'education_question');
|
||||
CREATE TRIGGER trg_education_question_version_ownership_equal
|
||||
BEFORE INSERT OR UPDATE OF tenant_id, scope, question_id ON education_question_version
|
||||
FOR EACH ROW EXECUTE FUNCTION education_check_question_version_ownership();
|
||||
CREATE TRIGGER trg_education_question_version_scope_immutable
|
||||
BEFORE UPDATE OF tenant_id, scope ON education_question_version
|
||||
FOR EACH ROW EXECUTE FUNCTION education_prevent_catalog_scope_change();
|
||||
CREATE TRIGGER trg_education_question_version_immutable
|
||||
BEFORE UPDATE OR DELETE ON education_question_version
|
||||
FOR EACH ROW EXECUTE FUNCTION education_prevent_question_version_mutation();
|
||||
CREATE TRIGGER trg_education_question_lifecycle_transition
|
||||
BEFORE INSERT OR UPDATE OF status ON education_question
|
||||
FOR EACH ROW EXECUTE FUNCTION education_enforce_question_lifecycle_transition();
|
||||
CREATE TRIGGER trg_education_question_lifecycle_audit_immutable
|
||||
BEFORE UPDATE OR DELETE ON education_question_lifecycle_audit
|
||||
FOR EACH ROW EXECUTE FUNCTION education_prevent_question_lifecycle_audit_mutation();
|
||||
CREATE TRIGGER trg_education_question_lifecycle_audit_insert_valid
|
||||
BEFORE INSERT ON education_question_lifecycle_audit
|
||||
FOR EACH ROW EXECUTE FUNCTION education_validate_question_lifecycle_audit_insert();
|
||||
CREATE TRIGGER trg_education_question_content_projection_immutable
|
||||
BEFORE UPDATE OF content_version, stem, type, type_label, difficulty,
|
||||
question_content, options, correct_answer, explanation, analysis
|
||||
ON education_question
|
||||
FOR EACH ROW EXECUTE FUNCTION education_prevent_question_content_projection_mutation();
|
||||
CREATE CONSTRAINT TRIGGER trg_education_question_lifecycle_audit_required
|
||||
AFTER UPDATE ON education_question
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION education_require_question_lifecycle_audit();
|
||||
|
||||
-- The platform schema is adopted before Education Flyway in deployed environments.
|
||||
-- Isolated Education migration tests intentionally have no System tables.
|
||||
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
|
||||
(6800, '教育管理', '', 1, 50, 0, '/education', 'ep:school', NULL,
|
||||
NULL, 0, false, true, true, 'education-flyway', 'education-flyway'),
|
||||
(6801, '能力查询', 'education:capability', 3, 1, 6800, '', '', NULL,
|
||||
NULL, 0, false, true, true, 'education-flyway', 'education-flyway'),
|
||||
(6802, '题目创作', 'education:question:author', 3, 2, 6800, '', '', NULL,
|
||||
NULL, 0, false, true, true, 'education-flyway', 'education-flyway'),
|
||||
(6803, '题目发布', 'education:question:publish', 3, 3, 6800, '', '', NULL,
|
||||
NULL, 0, false, true, true, 'education-flyway', 'education-flyway'),
|
||||
(6804, '题目归档', 'education:question:archive', 3, 4, 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 status = 0
|
||||
AND deleted = 0
|
||||
AND (
|
||||
(id = 6800 AND permission = '' AND type = 1 AND parent_id = 0 AND path = '/education')
|
||||
OR (id = 6801 AND permission = 'education:capability' AND type = 3 AND parent_id = 6800)
|
||||
OR (id = 6802 AND permission = 'education:question:author' AND type = 3 AND parent_id = 6800)
|
||||
OR (id = 6803 AND permission = 'education:question:publish' AND type = 3 AND parent_id = 6800)
|
||||
OR (id = 6804 AND permission = 'education:question:archive' AND type = 3 AND parent_id = 6800)
|
||||
);
|
||||
IF installed_permission_count <> 5 THEN
|
||||
RAISE EXCEPTION 'Education System RBAC seed IDs conflict with existing system_menu rows'
|
||||
USING ERRCODE = '23505';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user