feat(m0): 后端骨架 + 账号体系(M0 验收 36/36 通过)

技术栈(已在 4.1.1 上实测跑通,非纸面选型)
  Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PostgreSQL 16.15
  + Redis 7 + Flyway 12.4 + springdoc 3.1.1;前端定 Vue 3 + Element Plus + uni-app

交付内容
  M0-01 基础设施     deploy/docker-compose.yml(PG + Redis + MinIO,容器名与端口避让本机既有项目)
  M0-02 后端骨架     xt-common + xt-server;统一响应 / 全局异常 / 58 个错误码 /
                     多租户拦截 / 软删除 / 字段自动填充 / AES-GCM / HMAC 索引 / RSA / JWT
  M0-05 租户        开通租户(一次完成建租户 + 发 6 个内置角色 + 建首个管理员)、列表、详情
  M0-06 组织        CRUD + 组织树 + ancestor_path 自动维护 + 占用校验
  M0-07 PC 登录     RSA 解密 + 图形验证码 + 失败锁定 + 等时比较防账号枚举 + 审计日志
  M0-08 登出/当前用户  Token 黑名单(存 SHA-256,TTL 到自然过期)
  M0-01 数据库      Flyway 2 个迁移 → 26 张表(20 业务 + 5 标准 + 1 迁移记录)

安全姿态
  · 租户拦截器【默认拒绝】:匿名请求触及租户表直接抛 40101,不做「不加条件放行」
  · 权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表,杜绝前后端漂移
  · 匿名接口只靠 @PublicApi 放行,不做路径白名单,避免两套机制各说各话
  · 账号不存在时也跑一次 BCrypt(假哈希),拉平响应时间防枚举
  · 登录密码 RSA 传输,防止网关/APM 把明文密码记进日志

脚本
  scripts/sync-ddl.py          文档 DDL → Flyway 迁移,带 --check 防漂移
  scripts/e2e-m0.py            M0 验收 36 项,可进 CI
  scripts/verify-docs.py       文档一致性(接口数【逐节】核对、错误码、跨文档数字、表格列数)
  scripts/dev-login.py         RSA 加密登录助手
  scripts/dev-clean-e2e.py     清理 E2E 测试数据

文档修正(实测暴露,非人工审查)
  · DDL 缺陷:sys_tenant.contact_phone / sys_user.phone / collect_roster.phone
    原 varchar(20) 按明文长度建,装不下 AES-GCM 密文(约 52 字符),改 varchar(128)(PRD V2.4)
  · 错误码 40007b/40007c 带字母后缀与「code 为数字」的约定冲突,规范化为 40071/40072
  · 07-开发实施计划 新增 §9 版本兼容踩坑记录(10 条实录)与 §10 M0 完成情况
This commit is contained in:
2026-09-28 17:28:21 +08:00
parent 63aec55329
commit 9d7a8ca845
88 changed files with 7564 additions and 32 deletions
@@ -0,0 +1,724 @@
-- ═══════════════════════════════════════════════════════════════════
-- V1 · 业务表(20 张)
--
-- ⚠️ 本文件由脚本生成,请勿手改!
-- 来源 :docs/db/schema-postgresql.sql
-- 重新生成 :python scripts/sync-ddl.py
-- 校验同步 :python scripts/sync-ddl.py --check
--
-- 源文件已在 PostgreSQL 16.15 实测零错误:
-- 20 表 / 177 列注释 / 65 索引 / 20 表注释
-- ═══════════════════════════════════════════════════════════════════
-- ============ 1) 建表 ============
CREATE TABLE sys_tenant (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_code varchar(32) NOT NULL,
tenant_name varchar(128) NOT NULL,
tenant_type smallint NOT NULL DEFAULT 1,
contact_name varchar(64),
contact_phone varchar(128),
package_id bigint,
expire_time timestamp,
status smallint NOT NULL DEFAULT 1,
logo_url varchar(512),
config_json jsonb,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_tenant_code UNIQUE (tenant_code)
);
COMMENT ON TABLE sys_tenant IS '租户';
COMMENT ON COLUMN sys_tenant.id IS '租户ID';
COMMENT ON COLUMN sys_tenant.tenant_code IS '租户编码(短码,用于小程序扫码识别)';
COMMENT ON COLUMN sys_tenant.tenant_name IS '租户名称(学校/机构)';
COMMENT ON COLUMN sys_tenant.tenant_type IS '1学校 2认定机构 3报名点 4企业';
COMMENT ON COLUMN sys_tenant.contact_name IS '联系人';
COMMENT ON COLUMN sys_tenant.contact_phone IS '联系电话(AES加密存储;varchar(128)容纳密文,非明文长度)';
COMMENT ON COLUMN sys_tenant.package_id IS '套餐ID';
COMMENT ON COLUMN sys_tenant.expire_time IS '到期时间';
COMMENT ON COLUMN sys_tenant.status IS '0停用 1正常 2试用';
COMMENT ON COLUMN sys_tenant.logo_url IS 'Logo';
COMMENT ON COLUMN sys_tenant.config_json IS '租户配置(默认压缩参数/文件保留天数等)';
CREATE TABLE sys_org (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
parent_id bigint NOT NULL DEFAULT 0,
org_name varchar(128) NOT NULL,
org_type smallint NOT NULL DEFAULT 3,
org_code varchar(64),
ancestor_path varchar(512),
leader_user_id bigint,
sort_no integer NOT NULL DEFAULT 0,
status smallint NOT NULL DEFAULT 1,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX idx_tenant_parent ON sys_org (tenant_id, parent_id);
CREATE INDEX idx_tenant_name ON sys_org (tenant_id, org_name);
COMMENT ON TABLE sys_org IS '组织架构';
COMMENT ON COLUMN sys_org.parent_id IS '父级ID,0为根';
COMMENT ON COLUMN sys_org.org_name IS '组织名称,如 24中技数控1班';
COMMENT ON COLUMN sys_org.org_type IS '1学校 2院系/专业 3班级 4报名点';
COMMENT ON COLUMN sys_org.org_code IS '组织编码/班号';
COMMENT ON COLUMN sys_org.ancestor_path IS '祖先路径,如 0,1,5';
COMMENT ON COLUMN sys_org.leader_user_id IS '负责人(班主任)';
CREATE TABLE sys_occupation_ref (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
parent_id bigint NOT NULL DEFAULT 0,
name varchar(128) NOT NULL,
aliases varchar(512),
has_child smallint NOT NULL DEFAULT 0,
status smallint NOT NULL DEFAULT 1,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_parent ON sys_occupation_ref (parent_id);
CREATE INDEX idx_occref_name ON sys_occupation_ref (name);
COMMENT ON TABLE sys_occupation_ref IS '职业工种参考字典(仅拼写校验,非约束)';
COMMENT ON COLUMN sys_occupation_ref.parent_id IS '0=职业; 非0=该职业下的工种';
COMMENT ON COLUMN sys_occupation_ref.aliases IS '常见错写/别名,用于纠错提示';
COMMENT ON COLUMN sys_occupation_ref.has_child IS '该职业是否有工种';
CREATE TABLE sys_user (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL DEFAULT 0,
org_id bigint,
username varchar(64) NOT NULL,
password varchar(128),
real_name varchar(64) NOT NULL,
phone varchar(128),
phone_hash char(64),
open_id varchar(64),
union_id varchar(64),
id_card_no varchar(128),
id_card_hash char(64),
avatar_url varchar(512),
user_type smallint NOT NULL DEFAULT 1,
status smallint NOT NULL DEFAULT 1,
last_login_time timestamp,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_tenant_username UNIQUE (tenant_id, username)
);
CREATE INDEX idx_phone_hash ON sys_user (phone_hash);
CREATE INDEX idx_idcard_hash ON sys_user (id_card_hash);
CREATE INDEX idx_openid ON sys_user (open_id);
COMMENT ON TABLE sys_user IS '用户';
COMMENT ON COLUMN sys_user.tenant_id IS '0=平台运营';
COMMENT ON COLUMN sys_user.org_id IS '所属组织';
COMMENT ON COLUMN sys_user.username IS '登录名(教师)';
COMMENT ON COLUMN sys_user.password IS 'BCrypt';
COMMENT ON COLUMN sys_user.phone IS '手机号(AES加密存储;varchar(128)容纳密文,非明文长度)';
COMMENT ON COLUMN sys_user.phone_hash IS '手机号HMAC,用于唯一检索';
COMMENT ON COLUMN sys_user.open_id IS '微信openid';
COMMENT ON COLUMN sys_user.id_card_no IS '证件号(AES加密存储)';
COMMENT ON COLUMN sys_user.id_card_hash IS '证件号HMAC,用于比对与去重';
COMMENT ON COLUMN sys_user.user_type IS '1教师/管理员 2填报人(学生) 3社会人员';
COMMENT ON COLUMN sys_user.status IS '0停用 1正常 2待激活';
CREATE TABLE collect_task (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_code varchar(64) NOT NULL,
task_name varchar(200) NOT NULL,
collect_mode smallint NOT NULL DEFAULT 1,
batch_no varchar(64),
major_name varchar(64),
job_name varchar(64),
job_level varchar(32),
cert_type smallint NOT NULL DEFAULT 1,
start_time timestamp,
end_time timestamp,
audit_config jsonb,
naming_template varchar(256) NOT NULL DEFAULT '{姓名}-{证件号}-{材料名}',
compress_config jsonb,
roster_required smallint NOT NULL DEFAULT 1,
status smallint NOT NULL DEFAULT 0,
published_time timestamp,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_task_tenant_code UNIQUE (tenant_id, task_code)
);
CREATE INDEX idx_tenant_status ON collect_task (tenant_id, status);
CREATE INDEX idx_end_time ON collect_task (end_time);
COMMENT ON TABLE collect_task IS '收集任务';
COMMENT ON COLUMN collect_task.task_code IS '任务编码,用于文件路径';
COMMENT ON COLUMN collect_task.task_name IS '如 2026年9月机电专业认定材料收集';
COMMENT ON COLUMN collect_task.collect_mode IS '1学校收集 2社会收集';
COMMENT ON COLUMN collect_task.batch_no IS '批次号';
COMMENT ON COLUMN collect_task.major_name IS '专业';
COMMENT ON COLUMN collect_task.job_name IS '报考工种';
COMMENT ON COLUMN collect_task.job_level IS '报考等级';
COMMENT ON COLUMN collect_task.cert_type IS '1自主认定 2社会评价 3专项职业能力';
COMMENT ON COLUMN collect_task.end_time IS '截止时间';
COMMENT ON COLUMN collect_task.audit_config IS '审核流配置,见6.5';
COMMENT ON COLUMN collect_task.compress_config IS '任务级压缩默认参数';
COMMENT ON COLUMN collect_task.roster_required IS '是否需要名册: 固定为1,两种收集模式都必填(名单门禁)';
COMMENT ON COLUMN collect_task.status IS '0草稿 1收集中 2已截止 3已归档 4暂停';
CREATE TABLE collect_task_org (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
org_id bigint NOT NULL,
head_user_id bigint,
deputy_user_ids varchar(256),
expected_count integer NOT NULL DEFAULT 0,
status smallint NOT NULL DEFAULT 1,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT uk_task_org UNIQUE (task_id, org_id)
);
CREATE INDEX idx_tenant_head ON collect_task_org (tenant_id, head_user_id);
COMMENT ON TABLE collect_task_org IS '任务覆盖组织与审核人绑定';
COMMENT ON COLUMN collect_task_org.head_user_id IS '该班初审人(班主任),为空则取 sys_org.leader_user_id';
COMMENT ON COLUMN collect_task_org.deputy_user_ids IS '副班主任/协办人,逗号分隔';
COMMENT ON COLUMN collect_task_org.expected_count IS '应填人数(冗余,由名册汇总)';
COMMENT ON COLUMN collect_task_org.status IS '0已移除 1生效';
CREATE TABLE collect_form_schema (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
version integer NOT NULL DEFAULT 1,
schema_json jsonb NOT NULL,
is_current smallint NOT NULL DEFAULT 1,
create_by bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_task_version ON collect_form_schema (task_id, version);
COMMENT ON TABLE collect_form_schema IS '表单定义(版本化)';
COMMENT ON COLUMN collect_form_schema.version IS '版本号,已发布任务修改则+1';
COMMENT ON COLUMN collect_form_schema.schema_json IS '字段定义数组';
CREATE TABLE collect_material_def (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
material_code varchar(32) NOT NULL,
material_name varchar(64) NOT NULL,
aliases varchar(512),
name_pattern varchar(256),
required smallint NOT NULL DEFAULT 1,
visible_when varchar(256),
file_types varchar(128) NOT NULL DEFAULT 'jpg,jpeg,png',
max_size_kb integer NOT NULL DEFAULT 1024,
compress_profile varchar(8) NOT NULL DEFAULT 'AUTO',
min_long_side integer NOT NULL DEFAULT 1400,
min_quality smallint NOT NULL DEFAULT 70,
target_dpi integer,
fixed_size varchar(32),
output_format varchar(16) NOT NULL DEFAULT 'jpg',
max_count smallint NOT NULL DEFAULT 1,
merge_enabled smallint NOT NULL DEFAULT 0,
ocr_type varchar(32),
sample_url varchar(512),
tip_text varchar(512),
sort_no integer NOT NULL DEFAULT 0,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_task_code UNIQUE (task_id, material_code)
);
CREATE INDEX idx_matdef_tenant ON collect_material_def (tenant_id);
COMMENT ON TABLE collect_material_def IS '材料项定义';
COMMENT ON COLUMN collect_material_def.material_code IS '如 IDCARD/PHOTO1/SCHOOL_ROLL/WORK_PROOF';
COMMENT ON COLUMN collect_material_def.material_name IS '规范材料名,如 身份证';
COMMENT ON COLUMN collect_material_def.aliases IS '别名,逗号分隔,用于历史文件名识别';
COMMENT ON COLUMN collect_material_def.name_pattern IS '命名模板,空则继承任务级';
COMMENT ON COLUMN collect_material_def.visible_when IS '条件显示表达式';
COMMENT ON COLUMN collect_material_def.file_types IS '格式白名单';
COMMENT ON COLUMN collect_material_def.max_size_kb IS '体积上限KB';
COMMENT ON COLUMN collect_material_def.compress_profile IS '压缩档位: AUTO自动判定/P1证件照/P2证件翻拍/P3扫描件/P4截图/P5文档, 见6.3';
COMMENT ON COLUMN collect_material_def.min_long_side IS '分辨率下限(仅P2证件翻拍档生效)';
COMMENT ON COLUMN collect_material_def.min_quality IS '质量下限,低于65视为不可接受';
COMMENT ON COLUMN collect_material_def.target_dpi IS '目标DPI(仅P3扫描件档生效,建议150-300)';
COMMENT ON COLUMN collect_material_def.fixed_size IS '固定像素尺寸(仅P1证件照档,如295x413)';
COMMENT ON COLUMN collect_material_def.output_format IS 'P4截图档固定输出png';
COMMENT ON COLUMN collect_material_def.max_count IS '允许上传份数,身份证正反面可2';
COMMENT ON COLUMN collect_material_def.merge_enabled IS '多份是否合并为一张';
COMMENT ON COLUMN collect_material_def.ocr_type IS 'IDCARD/GENERAL/NONE';
COMMENT ON COLUMN collect_material_def.sample_url IS '示例图';
COMMENT ON COLUMN collect_material_def.tip_text IS '填报提示语';
CREATE TABLE collect_roster (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
org_id bigint,
seq_no integer,
student_no varchar(64),
real_name varchar(64) NOT NULL,
name_pinyin varchar(128),
id_card_no varchar(128) NOT NULL,
id_card_hash char(64) NOT NULL,
id_card_mask varchar(32),
phone varchar(128),
phone_hash char(64),
ext_json jsonb,
bind_user_id bigint,
bind_time timestamp,
bind_type smallint,
submit_status smallint NOT NULL DEFAULT 0,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_task_idcard UNIQUE (task_id, id_card_hash)
);
CREATE INDEX idx_task_org ON collect_roster (task_id, org_id);
CREATE INDEX idx_bind_user ON collect_roster (bind_user_id);
CREATE INDEX idx_roster_name ON collect_roster (task_id, real_name);
COMMENT ON TABLE collect_roster IS '名册';
COMMENT ON COLUMN collect_roster.org_id IS '班级';
COMMENT ON COLUMN collect_roster.seq_no IS '序号(来自Excel)';
COMMENT ON COLUMN collect_roster.student_no IS '学号(⚠️初稿遗漏,名册唯一性依据之一)';
COMMENT ON COLUMN collect_roster.name_pinyin IS '拼音,用于近音纠错';
COMMENT ON COLUMN collect_roster.id_card_no IS 'AES加密';
COMMENT ON COLUMN collect_roster.id_card_hash IS 'HMAC,唯一性判断';
COMMENT ON COLUMN collect_roster.id_card_mask IS '脱敏展示 3601**********1234';
COMMENT ON COLUMN collect_roster.ext_json IS '名册其余字段(学历/工作单位/报考职业等)';
COMMENT ON COLUMN collect_roster.bind_user_id IS '绑定的填报人';
COMMENT ON COLUMN collect_roster.bind_type IS '1门禁核验匹配 2管理员指定';
COMMENT ON COLUMN collect_roster.submit_status IS '0未提交 1草稿 2待审 3已通过 4已驳回';
CREATE TABLE collect_submission (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
roster_id bigint,
org_id bigint,
submitter_id bigint NOT NULL,
form_version integer NOT NULL DEFAULT 1,
form_data jsonb,
-- ⚠️ 与 collect_roster 对称的 18 个业务列:存学生「确认/修改后」的最终值
-- 需要与名单值做 diff(留痕"学生改了哪些字段"),也是统计"实际报考各工种人数"的依据
occupation varchar(64),
worktype varchar(64),
job_level varchar(32),
education varchar(32),
is_student smallint,
school varchar(128),
major varchar(128),
enroll_date date,
work_unit varchar(128),
is_retake smallint,
retake_theory varchar(64),
retake_practice varchar(64),
retake_overall varchar(64),
work_years smallint,
cert_occupation varchar(128),
cert_level varchar(32),
cert_no varchar(64),
extras_json jsonb DEFAULT '{}'::jsonb, -- 逃生舱
changed_fields jsonb, -- 学生相对名单修改过的字段(留痕)
status smallint NOT NULL DEFAULT 0,
reject_count integer NOT NULL DEFAULT 0,
last_reject_reason varchar(512),
red_count integer NOT NULL DEFAULT 0,
yellow_count integer NOT NULL DEFAULT 0,
submit_time timestamp,
class_audit_by bigint, class_audit_time timestamp,
admin_audit_by bigint, admin_audit_time timestamp,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_task_submitter UNIQUE (task_id, submitter_id)
);
CREATE INDEX idx_task_status ON collect_submission (task_id, status, org_id);
CREATE INDEX idx_submitter ON collect_submission (submitter_id);
COMMENT ON TABLE collect_submission IS '填报单';
COMMENT ON COLUMN collect_submission.roster_id IS '社会收集可为空';
COMMENT ON COLUMN collect_submission.submitter_id IS '填报人user_id';
COMMENT ON COLUMN collect_submission.form_data IS '表单数据快照(扩展字段;核心18列见上方独立列)';
COMMENT ON COLUMN collect_submission.occupation IS '最终报考职业(学生确认后,应与名单一致)';
COMMENT ON COLUMN collect_submission.worktype IS '最终报考工种';
COMMENT ON COLUMN collect_submission.job_level IS '最终报考等级';
COMMENT ON COLUMN collect_submission.is_student IS '是否在校生 0否 1是';
COMMENT ON COLUMN collect_submission.work_unit IS '工作单位(没有填"无")';
COMMENT ON COLUMN collect_submission.is_retake IS '是否补考 0否 1是';
COMMENT ON COLUMN collect_submission.extras_json IS '租户/机构额外追加字段(逃生舱)';
COMMENT ON COLUMN collect_submission.changed_fields IS '学生相对名单修改过的字段(留痕: {字段:{old,new,time}})';
COMMENT ON COLUMN collect_submission.status IS '0草稿 1待初审 2待终审 3已通过 4已驳回 5已归档';
COMMENT ON COLUMN collect_submission.red_count IS '红色问题数';
COMMENT ON COLUMN collect_submission.yellow_count IS '黄色问题数';
CREATE TABLE collect_file (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
submission_id bigint NOT NULL,
roster_id bigint,
material_code varchar(32) NOT NULL,
version integer NOT NULL DEFAULT 1,
is_current smallint NOT NULL DEFAULT 1,
original_name varchar(512) NOT NULL,
standard_name varchar(512),
file_ext varchar(16) NOT NULL,
mime_type varchar(64),
raw_size bigint NOT NULL,
final_size bigint NOT NULL,
file_hash char(64) NOT NULL,
raw_path varchar(512),
std_path varchar(512),
raw_expire_time timestamp,
width integer, height integer, quality smallint,
dpi integer,
ocr_status smallint NOT NULL DEFAULT 0,
ocr_result jsonb,
ocr_confidence numeric(4,3),
detected_material_code varchar(32),
detect_confidence numeric(4,3),
detect_source varchar(32),
detect_conflict smallint NOT NULL DEFAULT 0,
need_manual_place smallint NOT NULL DEFAULT 0,
vlm_called smallint NOT NULL DEFAULT 0,
validate_status smallint NOT NULL DEFAULT 0,
source smallint NOT NULL DEFAULT 1,
upload_ip varchar(64),
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX idx_file_submission ON collect_file (submission_id, material_code, is_current);
CREATE INDEX idx_task_material ON collect_file (task_id, material_code);
CREATE INDEX idx_hash ON collect_file (file_hash);
CREATE INDEX idx_raw_expire ON collect_file (raw_expire_time);
COMMENT ON TABLE collect_file IS '文件实体';
COMMENT ON COLUMN collect_file.is_current IS '是否最新有效版本';
COMMENT ON COLUMN collect_file.original_name IS '原始文件名';
COMMENT ON COLUMN collect_file.standard_name IS '规范文件名';
COMMENT ON COLUMN collect_file.raw_size IS '原始字节';
COMMENT ON COLUMN collect_file.final_size IS '处理后字节';
COMMENT ON COLUMN collect_file.file_hash IS 'SHA-256,用于查重';
COMMENT ON COLUMN collect_file.raw_path IS '对象存储key-原始';
COMMENT ON COLUMN collect_file.std_path IS '对象存储key-规范';
COMMENT ON COLUMN collect_file.raw_expire_time IS '原始文件清理时间';
COMMENT ON COLUMN collect_file.ocr_status IS '0未识别 1成功 2失败';
COMMENT ON COLUMN collect_file.ocr_result IS 'OCR结构化结果(不存明文证件号,存hash)';
COMMENT ON COLUMN collect_file.detected_material_code IS '材料识别结果(材料类型编码),见功能逻辑卷3.5-B';
COMMENT ON COLUMN collect_file.detect_confidence IS '材料识别置信度';
COMMENT ON COLUMN collect_file.detect_source IS '识别来源: FILENAME/形态KEYWORD/POSITION/VLM/MIXED';
COMMENT ON COLUMN collect_file.detect_conflict IS '是否与槽位冲突(1=冲突)';
COMMENT ON COLUMN collect_file.need_manual_place IS '是否需要人工归属(散图分拣用)';
COMMENT ON COLUMN collect_file.vlm_called IS '是否触发了VLM兜底(用于成本核算)';
COMMENT ON COLUMN collect_file.validate_status IS '0未校验 1通过 2警告 3错误';
COMMENT ON COLUMN collect_file.source IS '1本人上传 2班主任代传 3管理员代传 4整理器导入';
CREATE TABLE collect_validation_issue (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
submission_id bigint NOT NULL,
file_id bigint,
rule_code varchar(32) NOT NULL,
severity smallint NOT NULL,
field_key varchar(64),
title varchar(128) NOT NULL,
detail varchar(512),
expected_val varchar(256),
actual_val varchar(256),
auto_fixed smallint NOT NULL DEFAULT 0,
resolved smallint NOT NULL DEFAULT 0,
resolved_by bigint, resolved_time timestamp,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_issue_submission ON collect_validation_issue (submission_id, resolved);
CREATE INDEX idx_task_severity ON collect_validation_issue (task_id, severity, resolved);
COMMENT ON TABLE collect_validation_issue IS '校验问题';
COMMENT ON COLUMN collect_validation_issue.file_id IS '可为空(表单级问题)';
COMMENT ON COLUMN collect_validation_issue.rule_code IS 'VAL-01..VAL-12';
COMMENT ON COLUMN collect_validation_issue.severity IS '1信息 2警告 3错误';
COMMENT ON COLUMN collect_validation_issue.expected_val IS '期望值(如名册中的证件号)';
COMMENT ON COLUMN collect_validation_issue.actual_val IS '实际值';
COMMENT ON COLUMN collect_validation_issue.auto_fixed IS '是否已自动修复';
CREATE TABLE collect_audit_record (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
submission_id bigint NOT NULL,
audit_node varchar(32) NOT NULL,
auditor_id bigint NOT NULL,
action smallint NOT NULL,
reason_code varchar(32),
reason_text varchar(512),
material_codes varchar(256),
duration_sec integer,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_audit_submission ON collect_audit_record (submission_id, create_time);
CREATE INDEX idx_task_node ON collect_audit_record (task_id, audit_node, create_time);
COMMENT ON TABLE collect_audit_record IS '审核记录';
COMMENT ON COLUMN collect_audit_record.audit_node IS 'CLASS_AUDIT/ADMIN_AUDIT';
COMMENT ON COLUMN collect_audit_record.action IS '1通过 2驳回 3批量通过 4批量驳回 5转交 6代传';
COMMENT ON COLUMN collect_audit_record.reason_code IS '驳回原因编码';
COMMENT ON COLUMN collect_audit_record.material_codes IS '被驳回的材料项';
COMMENT ON COLUMN collect_audit_record.duration_sec IS '处理耗时';
CREATE TABLE collect_export_task (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
export_name varchar(256) NOT NULL,
scope_json jsonb,
split_size_mb integer NOT NULL DEFAULT 0,
desensitize smallint NOT NULL DEFAULT 0,
status smallint NOT NULL DEFAULT 0,
progress smallint NOT NULL DEFAULT 0,
file_count integer, total_size bigint,
result_files jsonb,
error_msg varchar(512),
expire_time timestamp,
create_by bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
finish_time timestamp,
PRIMARY KEY (id)
);
CREATE INDEX idx_task ON collect_export_task (task_id, create_time);
CREATE INDEX idx_status ON collect_export_task (status);
COMMENT ON TABLE collect_export_task IS '导出任务';
COMMENT ON COLUMN collect_export_task.scope_json IS '范围:orgIds/userIds/仅通过终审/是否含原图';
COMMENT ON COLUMN collect_export_task.split_size_mb IS '0不分卷,>0按MB分卷';
COMMENT ON COLUMN collect_export_task.desensitize IS '是否脱敏';
COMMENT ON COLUMN collect_export_task.status IS '0排队 1处理中 2完成 3失败';
COMMENT ON COLUMN collect_export_task.progress IS '0-100';
COMMENT ON COLUMN collect_export_task.result_files IS '生成的包列表[{name,path,size,part}]';
COMMENT ON COLUMN collect_export_task.expire_time IS '下载有效期';
CREATE TABLE collect_file_process_log (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
file_id bigint NOT NULL,
step varchar(32) NOT NULL,
before_val varchar(512),
after_val varchar(512),
extra_json jsonb,
cost_ms integer,
success smallint NOT NULL DEFAULT 1,
error_msg varchar(512),
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_file ON collect_file_process_log (file_id, step);
COMMENT ON TABLE collect_file_process_log IS '文件处理日志';
COMMENT ON COLUMN collect_file_process_log.step IS 'NORMALIZE_NAME/COMPRESS/CONVERT/OCR/EXIF_FIX';
COMMENT ON COLUMN collect_file_process_log.extra_json IS '如 {beforeSize,afterSize,attempts,quality,finalLongSide}';
CREATE TABLE collect_notify_record (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint,
channel varchar(16) NOT NULL, -- WECHAT_SUBSCRIBE / WECHAT_MP / SMS / INBOX
biz_type varchar(32) NOT NULL, -- TASK_PUBLISH / DEADLINE_REMIND / REJECT / APPROVE / EXPORT_DONE / AUDIT_TIMEOUT
receiver_id bigint NOT NULL, -- 接收人 sys_user.id
receiver_role varchar(16), -- 接收人角色(便于按角色统计)
template_code varchar(64),
content varchar(1024),
status smallint NOT NULL DEFAULT 0, -- 0待发 1成功 2失败 3额度不足跳过
fail_reason varchar(512),
send_time timestamp,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_notify_task ON collect_notify_record (task_id, biz_type, create_time);
CREATE INDEX idx_notify_recv ON collect_notify_record (receiver_id, create_time);
COMMENT ON TABLE collect_notify_record IS '通知记录';
COMMENT ON COLUMN collect_notify_record.channel IS '渠道: 微信订阅消息/服务号/短信/站内';
COMMENT ON COLUMN collect_notify_record.biz_type IS '业务类型';
COMMENT ON COLUMN collect_notify_record.status IS '0待发 1成功 2失败 3额度不足跳过';
COMMENT ON COLUMN collect_notify_record.fail_reason IS '失败原因;额度不足时记录(应对R12订阅消息次数限制)';
-- 订阅额度余额(微信一次性订阅消息,发一条消耗一条)
CREATE TABLE collect_subscribe_quota (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
template_code varchar(64) NOT NULL,
total integer NOT NULL DEFAULT 0, -- 累计授权次数
used integer NOT NULL DEFAULT 0, -- 已消耗次数
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT uk_quota_user_tpl UNIQUE (user_id, template_code)
);
COMMENT ON TABLE collect_subscribe_quota IS '微信订阅消息额度余额';
COMMENT ON COLUMN collect_subscribe_quota.total IS '累计授权次数';
COMMENT ON COLUMN collect_subscribe_quota.used IS '已消耗次数;余额=total-used,不足时自动降级到服务号或短信';
CREATE TABLE collect_rule_template (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
template_name varchar(128) NOT NULL,
cert_type smallint,
form_schema jsonb,
materials jsonb,
naming_template varchar(256),
is_public smallint NOT NULL DEFAULT 0,
use_count integer NOT NULL DEFAULT 0,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX idx_ruletpl_tenant ON collect_rule_template (tenant_id, is_public);
COMMENT ON TABLE collect_rule_template IS '规则模板';
COMMENT ON COLUMN collect_rule_template.tenant_id IS '0=平台公共模板';
COMMENT ON COLUMN collect_rule_template.template_name IS '如 自主认定标准模板';
COMMENT ON COLUMN collect_rule_template.cert_type IS '1自主认定 2社会评价 3专项职业能力';
COMMENT ON COLUMN collect_rule_template.form_schema IS '表单字段模板';
COMMENT ON COLUMN collect_rule_template.materials IS '材料清单模板';
CREATE TABLE collect_cleanup_job (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
job_name varchar(128) NOT NULL,
source_type smallint NOT NULL DEFAULT 1,
source_path varchar(512) NOT NULL,
target_task_id bigint,
naming_template varchar(256) NOT NULL DEFAULT '{姓名}-{证件号}-{材料名}',
compress_config jsonb,
status smallint NOT NULL DEFAULT 0,
progress smallint NOT NULL DEFAULT 0,
report_json jsonb,
total_files integer, fixed_files integer, failed_files integer,
result_path varchar(512),
create_by bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
finish_time timestamp,
PRIMARY KEY (id)
);
CREATE INDEX idx_cleanup_tenant ON collect_cleanup_job (tenant_id, create_time);
COMMENT ON TABLE collect_cleanup_job IS '历史数据整理任务';
COMMENT ON COLUMN collect_cleanup_job.source_type IS '1上传压缩包 2对象存储目录';
COMMENT ON COLUMN collect_cleanup_job.target_task_id IS '结果可导入某任务';
COMMENT ON COLUMN collect_cleanup_job.status IS '0排队 1解析中 2处理中 3完成 4失败';
COMMENT ON COLUMN collect_cleanup_job.report_json IS '体检报告:命名违规/证件号异常/体积超标/缺失/重复';
-- ============ 1b) 客户确认后新增(FR-509) ============
CREATE TABLE collect_join_request (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
task_id bigint NOT NULL,
real_name varchar(64) NOT NULL,
id_card_hash char(64) NOT NULL,
id_card_mask varchar(32) NOT NULL,
phone_hash char(64),
phone_mask varchar(32),
org_hint varchar(128),
reason varchar(500),
status smallint NOT NULL DEFAULT 0,
reject_reason varchar(500),
roster_id bigint,
audit_by bigint,
audit_time timestamp,
applicant_openid varchar(64) NOT NULL,
applicant_uid bigint,
source smallint NOT NULL DEFAULT 1,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint DEFAULT 0 NOT NULL,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX uk_join_task_idcard ON collect_join_request (task_id, id_card_hash)
WHERE deleted = 0 AND status IN (0, 1);
CREATE INDEX idx_join_task_status ON collect_join_request (task_id, status, create_time);
CREATE INDEX idx_join_openid ON collect_join_request (applicant_openid);
COMMENT ON TABLE collect_join_request IS '名单加入申请';
COMMENT ON COLUMN collect_join_request.real_name IS '申请人自述姓名(明文,业务必需)';
COMMENT ON COLUMN collect_join_request.id_card_hash IS 'HMAC,与门禁一致,不存明文';
COMMENT ON COLUMN collect_join_request.id_card_mask IS '脱敏展示';
COMMENT ON COLUMN collect_join_request.org_hint IS '申请人自述班级(选填,可由管理员修正)';
COMMENT ON COLUMN collect_join_request.reason IS '申请说明';
COMMENT ON COLUMN collect_join_request.status IS '0待审 1已通过 2已驳回 3已失效(任务截止)';
COMMENT ON COLUMN collect_join_request.reject_reason IS '驳回原因(驳回时必填)';
COMMENT ON COLUMN collect_join_request.roster_id IS '审批通过后生成的名册项';
COMMENT ON COLUMN collect_join_request.applicant_openid IS '用于限流(每任务<=2次)与审批通过后自动绑定';
COMMENT ON COLUMN collect_join_request.source IS '1门禁失败页申请 2管理员代录';
-- ============ 2) 补列 ALTER ============
ALTER TABLE collect_task ADD COLUMN plan_scope_json jsonb;
ALTER TABLE collect_task ADD COLUMN plan_scope_confirmed smallint NOT NULL DEFAULT 0;
COMMENT ON COLUMN collect_task.plan_scope_json IS '本期报考范围快照(从名单聚合): {occupations:[{name,worktypes:[{name,levels:[]}]}], confirmed:true}';
COMMENT ON COLUMN collect_task.plan_scope_confirmed IS '管理员是否已确认范围(未确认则追加名单不告警)';
-- 同学号的补齐:官方模板 21 字段中的非身份字段全部建为正式列
-- 姓名、身份证号已有独立列(real_name / id_card_no),此处补齐其余 18 个
ALTER TABLE collect_roster
ADD COLUMN occupation varchar(64), -- 报考职业 G
ADD COLUMN worktype varchar(64), -- 报考工种 H
ADD COLUMN job_level varchar(32), -- 报考等级 I
ADD COLUMN education varchar(32), -- 学历 D
ADD COLUMN is_student smallint, -- 是否在校生 N 0否 1是
ADD COLUMN school varchar(128), -- 在读学校 O
ADD COLUMN major varchar(128), -- 所学专业 P
ADD COLUMN enroll_date date, -- 入学时间 Q YYYY-MM-DD
ADD COLUMN work_unit varchar(128), -- 工作单位 F 无则填"无"
ADD COLUMN is_retake smallint, -- 是否补考 J 0否 1是
ADD COLUMN retake_theory varchar(64), -- 补考理论 K
ADD COLUMN retake_practice varchar(64), -- 补考实操 L
ADD COLUMN retake_overall varchar(64), -- 补考综合 M
ADD COLUMN work_years smallint, -- 本职业工作年限 R
ADD COLUMN cert_occupation varchar(128), -- 已获证书职业工种 S
ADD COLUMN cert_level varchar(32), -- 已获证书等级 T
ADD COLUMN cert_no varchar(64), -- 已获证书编号 U
ADD COLUMN extras_json jsonb DEFAULT '{}'::jsonb; -- 逃生舱:仅存租户/机构额外追加的字段
-- 报考范围聚合与按工种统计必需的索引
CREATE INDEX idx_roster_scope ON collect_roster (tenant_id, task_id, occupation, worktype, job_level);
CREATE INDEX idx_roster_org ON collect_roster (task_id, org_id, deleted);
COMMENT ON COLUMN collect_roster.occupation IS '报考职业(锁定,由老师确定)';
COMMENT ON COLUMN collect_roster.worktype IS '报考工种(锁定;该职业无工种时填"无";专项职业能力为空)';
COMMENT ON COLUMN collect_roster.job_level IS '报考等级(锁定;专项职业能力为空)';
COMMENT ON COLUMN collect_roster.education IS '学历(下拉11项)';
COMMENT ON COLUMN collect_roster.is_student IS '是否在校生 0否 1是';
COMMENT ON COLUMN collect_roster.school IS '在读学校(在校生=是时必填)';
COMMENT ON COLUMN collect_roster.major IS '所学专业(在校生=是时必填)';
COMMENT ON COLUMN collect_roster.enroll_date IS '入学时间(在校生=是时必填,格式YYYY-MM-DD)';
COMMENT ON COLUMN collect_roster.work_unit IS '工作单位(没有填"无")';
COMMENT ON COLUMN collect_roster.is_retake IS '是否补考 0否 1是';
COMMENT ON COLUMN collect_roster.retake_theory IS '补考理论(级联)';
COMMENT ON COLUMN collect_roster.retake_practice IS '补考实操(级联)';
COMMENT ON COLUMN collect_roster.retake_overall IS '补考综合(级联)';
COMMENT ON COLUMN collect_roster.work_years IS '本职业工作年限(仅阿拉伯数字)';
COMMENT ON COLUMN collect_roster.cert_occupation IS '已获证书职业工种(选填)';
COMMENT ON COLUMN collect_roster.cert_level IS '已获证书等级(选填)';
COMMENT ON COLUMN collect_roster.cert_no IS '已获证书编号(选填)';
COMMENT ON COLUMN collect_roster.extras_json IS '租户/机构额外追加的字段(逃生舱,不参与核心统计)';
@@ -0,0 +1,138 @@
-- ═══════════════════════════════════════════════════════════════════
-- V2 · 标准表(PRD 未给 DDL,注明「从略、沿用现有框架实现」)
--
-- 为什么单列一个迁移而不是塞进 V1:
-- docs/db/schema-postgresql.sql 是文档交付物,口径是「20 张业务表」,
-- 已在 PG 16.15 实测零错误并写进验收标准。标准表另计,不动那个口径。
--
-- 命名约定(沿用 V1):
-- · 主键 id / 租户 tenant_id / 公共字段 create_by,create_time,update_by,update_time,deleted
-- · ⚠️ PG 的索引名与约束名必须 schema 内【全局唯一】,一律加表缩写前缀
-- · 需要「删了能重建同名」的唯一性,一律用部分唯一索引 + WHERE deleted = 0
-- ═══════════════════════════════════════════════════════════════════
-- ── 1) sys_role 角色 ────────────────────────────────────────────────
-- 内置角色在「开通租户」时按代码里的定义写入该租户,所以角色是租户隔离的,
-- 多租户拦截器能统一处理,不用给角色表开特例。
CREATE TABLE sys_role (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
role_code varchar(64) NOT NULL,
role_name varchar(64) NOT NULL,
role_type smallint NOT NULL DEFAULT 1,
data_scope smallint NOT NULL DEFAULT 4,
sort_no integer NOT NULL DEFAULT 0,
remark varchar(255),
status smallint NOT NULL DEFAULT 1,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX uk_role_tenant_code ON sys_role (tenant_id, role_code) WHERE deleted = 0;
CREATE INDEX idx_role_tenant_status ON sys_role (tenant_id, status);
COMMENT ON TABLE sys_role IS '角色';
COMMENT ON COLUMN sys_role.role_code IS '角色码,如 SCHOOL_ADMIN;与代码常量 PermissionDef 对齐';
COMMENT ON COLUMN sys_role.role_type IS '1内置(不可删,权限由代码决定) 2自定义';
COMMENT ON COLUMN sys_role.data_scope IS '1全平台 2本租户 3本组织及下级 4仅本人';
COMMENT ON COLUMN sys_role.status IS '0停用 1正常';
-- ── 2) sys_user_role 用户与角色 ─────────────────────────────────────
CREATE TABLE sys_user_role (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
role_id bigint NOT NULL,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX uk_userrole_user_role ON sys_user_role (user_id, role_id) WHERE deleted = 0;
CREATE INDEX idx_userrole_role ON sys_user_role (role_id);
CREATE INDEX idx_userrole_tenant_user ON sys_user_role (tenant_id, user_id);
COMMENT ON TABLE sys_user_role IS '用户与角色的关联';
COMMENT ON COLUMN sys_user_role.user_id IS 'sys_user.id';
COMMENT ON COLUMN sys_user_role.role_id IS 'sys_role.id';
-- ── 3) sys_role_permission 角色授权 ─────────────────────────────────
-- 权限码本身(有哪些权限、叫什么、属于哪个菜单)定义在代码里,见 PermissionDef。
-- 表里只存「这个角色被授了哪些码」,所以不需要 sys_permission 字典表——
-- 权限定义不会漂移,前端菜单与后端校验永远一致。
CREATE TABLE sys_role_permission (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL,
role_id bigint NOT NULL,
perm_code varchar(128) NOT NULL,
create_by bigint,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_by bigint,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted smallint NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX uk_roleperm_role_code ON sys_role_permission (role_id, perm_code) WHERE deleted = 0;
CREATE INDEX idx_roleperm_tenant_role ON sys_role_permission (tenant_id, role_id);
COMMENT ON TABLE sys_role_permission IS '角色与权限码的关联(权限码定义在代码 PermissionDef)';
COMMENT ON COLUMN sys_role_permission.perm_code IS '权限码,如 task:create';
-- ── 4) sys_login_log 登录日志 ───────────────────────────────────────
-- PRD §10.3「审计留痕:登录、查看明文、下载、导出、代传、删除全部记录(含 IP、UA)」
-- 只增不改,所以没有 update_* 与 deleted 字段——日志表加软删除没有意义。
-- ⚠️ 会持续增长,需配定期归档(建议保留 12 个月,M7 上线前定策略)。
CREATE TABLE sys_login_log (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL DEFAULT 0,
user_id bigint,
username varchar(64),
login_type smallint NOT NULL DEFAULT 1,
success smallint NOT NULL DEFAULT 1,
fail_reason varchar(128),
ip varchar(64),
user_agent varchar(512),
trace_id varchar(64),
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_loginlog_user_time ON sys_login_log (user_id, create_time);
CREATE INDEX idx_loginlog_username_time ON sys_login_log (username, create_time);
CREATE INDEX idx_loginlog_time ON sys_login_log (create_time);
COMMENT ON TABLE sys_login_log IS '登录日志(审计留痕,只增不改)';
COMMENT ON COLUMN sys_login_log.login_type IS '1 PC 2 小程序 3 开放接口';
COMMENT ON COLUMN sys_login_log.success IS '0失败 1成功';
COMMENT ON COLUMN sys_login_log.fail_reason IS '失败原因;⚠️不记录密码或密码片段';
COMMENT ON COLUMN sys_login_log.username IS '冗余存登录名,方便未匹配到用户时也能审计';
-- ── 5) sys_operation_log 操作日志 ───────────────────────────────────
CREATE TABLE sys_operation_log (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
tenant_id bigint NOT NULL DEFAULT 0,
user_id bigint,
username varchar(64),
module varchar(64),
action varchar(64),
request_method varchar(16),
request_uri varchar(512),
request_param text,
biz_code integer,
duration_ms integer,
ip varchar(64),
user_agent varchar(512),
trace_id varchar(64),
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE INDEX idx_oplog_tenant_time ON sys_operation_log (tenant_id, create_time);
CREATE INDEX idx_oplog_user_time ON sys_operation_log (user_id, create_time);
CREATE INDEX idx_oplog_module_time ON sys_operation_log (module, create_time);
COMMENT ON TABLE sys_operation_log IS '操作日志(审计留痕,只增不改)';
COMMENT ON COLUMN sys_operation_log.module IS '业务模块,如 tenant/org/user/task';
COMMENT ON COLUMN sys_operation_log.action IS '动作,如 create/update/delete/export/approve';
COMMENT ON COLUMN sys_operation_log.request_param IS '⚠️入库前必须脱敏:去掉密码/证件号明文';
COMMENT ON COLUMN sys_operation_log.biz_code IS '业务返回码,用于区分成功与业务失败';
COMMENT ON COLUMN sys_operation_log.duration_ms IS '耗时毫秒,用于定位慢操作';
COMMENT ON COLUMN sys_operation_log.trace_id IS '全链路追踪 ID';