素材库与考核调研此前各有一套人事数据,彼此没有外键,靠姓名、邮箱在运行时 逐层软匹配。本次把身份判定收敛到钉钉唯一值,并让组织架构与人员状态跟随 上游 HR 系统自动更新。 人员身份 - 新增账号与权威人员档案的正式绑定表,活跃绑定双向唯一 - users.dingtalkUserId 补上唯一约束,数据库层面杜绝一人多号 - 移除钉钉登录的同名自动合并,同名冲突一律转人工核对 - 看板删除特判由比对用户名改为比对钉钉唯一值,避免同名越权 组织架构 - 新增 OrganizationUnit,直接镜像上游 HR 部门树 - 不再从员工部门路径倒推,同名部门不会再被合并 - 上游已删除的部门下线而非物理删除 人员状态 - 新增规范化的 employmentState,取代对混杂原始值做关键词匹配 - 接入上游离职表,离职判定不再依赖中文关键词 - 离职联动:归档账号并把素材托管给所在部门,默认只处理离职满 30 天者 素材归属 - 归属变更纳入正式转移,记录最初上传者、转移流水与理由 - 新增素材托管部门,归属与署名分离:托管只改维护责任,不动署名 - 编辑素材的权限判定统一到 canManageMaterial - 修复任务-详情上传把邮箱误存为作者名的问题 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
122 lines
4.8 KiB
PL/PgSQL
122 lines
4.8 KiB
PL/PgSQL
-- 素材库账号 ↔ 权威人员档案 正式绑定关系
|
||
--
|
||
-- 背景:此前账号与花名册人员之间没有任何外键,仅在运行时按
|
||
-- 钉钉ID → 邮箱 → sourcePayload JSON 逐层软匹配,数据库层面
|
||
-- 并不知道某个素材库账号和某条花名册档案是同一个人。
|
||
--
|
||
-- 本迁移建立正式绑定表,并按钉钉唯一值回填存量绑定。
|
||
-- 严格遵守:只按钉钉唯一值自动绑定,姓名与邮箱不参与自动建立绑定。
|
||
--
|
||
-- 可重复执行。
|
||
|
||
BEGIN;
|
||
|
||
-- ========== 1. 前置校验 ==========
|
||
-- 一个钉钉身份对应多个素材库账号时,回填会在同一人员档案上产生
|
||
-- 多条 ACTIVE 绑定,违反下面的部分唯一索引。此处提前拦截并给出明确原因,
|
||
-- 而不是让索引创建阶段抛出难以定位的报错。
|
||
DO $$
|
||
DECLARE
|
||
conflict_count INTEGER;
|
||
BEGIN
|
||
SELECT COUNT(*) INTO conflict_count
|
||
FROM (
|
||
SELECT "dingtalkUserId"
|
||
FROM users
|
||
WHERE "dingtalkUserId" IS NOT NULL
|
||
GROUP BY "dingtalkUserId"
|
||
HAVING COUNT(*) > 1
|
||
) AS duplicated;
|
||
|
||
IF conflict_count > 0 THEN
|
||
RAISE EXCEPTION
|
||
'存在 % 个钉钉账号绑定了多个素材库账号,必须先人工处理后再执行本迁移。禁止按姓名自动合并。',
|
||
conflict_count;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- ========== 2. 钉钉唯一值:全项目唯一 ==========
|
||
-- 此前 users."dingtalkUserId" 没有任何唯一约束,数据库层面允许两个素材库账号
|
||
-- 填入同一个钉钉身份。既然钉钉唯一值是全项目唯一的人员判定依据,这条约束必须落到库上。
|
||
-- PostgreSQL 中多个 NULL 互不冲突,因此未绑定钉钉的账号不受影响。
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "users_dingtalkUserId_key"
|
||
ON users("dingtalkUserId");
|
||
|
||
-- ========== 3. 建表 ==========
|
||
CREATE TABLE IF NOT EXISTS user_employee_bindings (
|
||
id TEXT NOT NULL,
|
||
user_id TEXT NOT NULL,
|
||
employee_profile_id TEXT NOT NULL,
|
||
binding_source TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'ACTIVE',
|
||
verified_at TIMESTAMP(3),
|
||
note TEXT,
|
||
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP(3) NOT NULL,
|
||
CONSTRAINT user_employee_bindings_pkey PRIMARY KEY (id)
|
||
);
|
||
|
||
-- 外键:账号或人员档案被删除时,绑定关系一并清除
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_constraint WHERE conname = 'user_employee_bindings_user_id_fkey'
|
||
) THEN
|
||
ALTER TABLE user_employee_bindings
|
||
ADD CONSTRAINT user_employee_bindings_user_id_fkey
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_constraint WHERE conname = 'user_employee_bindings_profile_id_fkey'
|
||
) THEN
|
||
ALTER TABLE user_employee_bindings
|
||
ADD CONSTRAINT user_employee_bindings_profile_id_fkey
|
||
FOREIGN KEY (employee_profile_id) REFERENCES performance_survey_employee_profiles(id)
|
||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- ========== 4. 索引 ==========
|
||
CREATE INDEX IF NOT EXISTS user_employee_binding_user_idx ON user_employee_bindings(user_id);
|
||
CREATE INDEX IF NOT EXISTS user_employee_binding_profile_idx ON user_employee_bindings(employee_profile_id);
|
||
CREATE INDEX IF NOT EXISTS user_employee_binding_status_idx ON user_employee_bindings(status);
|
||
|
||
-- 活跃绑定的双向唯一:一个账号只能绑一名权威人员,一名权威人员只能绑一个活跃账号。
|
||
-- 使用部分唯一索引,从而允许保留历史(status <> 'ACTIVE')绑定记录。
|
||
CREATE UNIQUE INDEX IF NOT EXISTS user_employee_binding_active_user_key
|
||
ON user_employee_bindings(user_id) WHERE status = 'ACTIVE';
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS user_employee_binding_active_profile_key
|
||
ON user_employee_bindings(employee_profile_id) WHERE status = 'ACTIVE';
|
||
|
||
-- ========== 5. 回填存量绑定 ==========
|
||
-- 只按钉钉唯一值匹配。行 ID 由 user_id + profile_id 确定性生成,保证重复执行幂等。
|
||
INSERT INTO user_employee_bindings (
|
||
id, user_id, employee_profile_id, binding_source, status, verified_at, created_at, updated_at
|
||
)
|
||
SELECT
|
||
md5('user_employee_binding:' || u.id || ':' || p.id)::uuid::text,
|
||
u.id,
|
||
p.id,
|
||
'MIGRATION',
|
||
'ACTIVE',
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP
|
||
FROM users u
|
||
JOIN performance_survey_employee_profiles p
|
||
ON p.dingtalk_user_id = u."dingtalkUserId"
|
||
WHERE u."dingtalkUserId" IS NOT NULL
|
||
AND u."dingtalkUserId" <> ''
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM user_employee_bindings b
|
||
WHERE b.user_id = u.id AND b.status = 'ACTIVE'
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM user_employee_bindings b
|
||
WHERE b.employee_profile_id = p.id AND b.status = 'ACTIVE'
|
||
);
|
||
|
||
COMMIT;
|