Files
OnboardXAdmin/database/migrations/20260827_onboarding_template_contract_scope_migration.sql
T
2026-08-27 11:09:20 +08:00

344 lines
13 KiB
SQL

-- OnboardX 公司、部门、岗位、合同模板及入职模板层级升级脚本
-- 适用数据库:MySQL 8.0+
-- 说明:
-- 1. 可重复执行,不会覆盖已经人工维护的归属数据。
-- 2. 仅对能够从现有岗位/入职模板关系中唯一确定的数据执行安全回填。
-- 3. 脚本末尾会输出仍需人工处理的数据,请先处理完再启用相关模板。
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS `ob_scope_add_column_if_missing`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_add_column_if_missing`(
IN p_table_name VARCHAR(128),
IN p_column_name VARCHAR(128),
IN p_column_definition VARCHAR(1000)
)
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table_name
AND COLUMN_NAME = p_column_name
) THEN
SET @ddl = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD COLUMN `',
p_column_name, '` ', p_column_definition
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
DROP PROCEDURE IF EXISTS `ob_scope_add_index_if_missing`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_add_index_if_missing`(
IN p_table_name VARCHAR(128),
IN p_index_name VARCHAR(128),
IN p_index_definition VARCHAR(1000)
)
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table_name
AND INDEX_NAME = p_index_name
) THEN
SET @ddl = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD ', p_index_definition
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
DROP PROCEDURE IF EXISTS `ob_scope_add_unique_index_if_clean`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_add_unique_index_if_clean`(
IN p_table_name VARCHAR(128),
IN p_column_name VARCHAR(128),
IN p_index_name VARCHAR(128)
)
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table_name
AND INDEX_NAME = p_index_name
) THEN
SET @duplicate_count = 0;
SET @check_sql = CONCAT(
'SELECT COUNT(*) INTO @duplicate_count FROM (',
'SELECT `', p_column_name, '` FROM `', p_table_name, '` ',
'WHERE `', p_column_name, '` IS NOT NULL ',
'AND LENGTH(TRIM(`', p_column_name, '`)) > 0 ',
'GROUP BY `', p_column_name, '` HAVING COUNT(*) > 1',
') duplicated_values'
);
PREPARE stmt FROM @check_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
IF @duplicate_count = 0 THEN
SET @ddl = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD UNIQUE INDEX `',
p_index_name, '` (`', p_column_name, '`)'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END IF;
END$$
DELIMITER ;
-- 入职模板增加所属部门。
CALL `ob_scope_add_column_if_missing`(
'ob_onboarding_template',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 合同模板增加所属公司和部门。暂时允许 NULL,以兼容待人工归类的历史模板。
CALL `ob_scope_add_column_if_missing`(
'ob_contract_template',
'CompanyId',
'BIGINT NULL COMMENT ''所属公司Id'' AFTER `Id`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_contract_template',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 兼容尚未执行上一版部门迁移的数据库,重复调用不会新增重复字段。
CALL `ob_scope_add_column_if_missing`(
'ob_position',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_onboarding_task',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_employee',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 合同模板与岗位为多对多关系,一份通用合同可同时适用于多个岗位。
CREATE TABLE IF NOT EXISTS `ob_contract_template_position` (
`Id` BIGINT NOT NULL COMMENT '主键',
`ContractTemplateId` BIGINT NOT NULL COMMENT '合同模板Id',
`PositionId` BIGINT NOT NULL COMMENT '适用岗位Id',
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_ob_contract_template_position` (`ContractTemplateId`, `PositionId`),
KEY `idx_ob_contract_template_position_position` (`PositionId`, `ContractTemplateId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同模板适用岗位';
CALL `ob_scope_add_index_if_missing`(
'ob_position',
'idx_ob_position_department_status',
'INDEX `idx_ob_position_department_status` (`DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_onboarding_template',
'idx_ob_onboarding_template_scope',
'INDEX `idx_ob_onboarding_template_scope` (`CompanyId`, `DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_contract_template',
'idx_ob_contract_template_scope',
'INDEX `idx_ob_contract_template_scope` (`CompanyId`, `DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_onboarding_task',
'idx_ob_onboarding_task_department',
'INDEX `idx_ob_onboarding_task_department` (`DepartmentId`, `TaskStatus`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_employee',
'idx_ob_employee_department',
'INDEX `idx_ob_employee_department` (`DepartmentId`, `IsDelete`)'
);
-- 身份证号和手机号是正式员工防重复的数据库最终防线。
-- 若历史数据已经重复,本次会安全跳过建索引,并由脚本末尾查询列出冲突数据。
CALL `ob_scope_add_unique_index_if_clean`(
'ob_employee',
'IdCardNumber',
'uk_ob_employee_id_card'
);
CALL `ob_scope_add_unique_index_if_clean`(
'ob_employee',
'Phone',
'uk_ob_employee_phone'
);
-- 安全回填入职模板部门:只有全部适用岗位均属于同一个非空部门时才回填。
UPDATE `ob_onboarding_template` template
INNER JOIN (
SELECT
relation.`OnboardingTemplateId`,
MIN(position.`DepartmentId`) AS `DepartmentId`
FROM `ob_onboarding_template_position` relation
INNER JOIN `ob_position` position ON position.`Id` = relation.`PositionId`
WHERE COALESCE(relation.`IsDelete`, 0) = 0
AND COALESCE(position.`IsDelete`, 0) = 0
GROUP BY relation.`OnboardingTemplateId`
HAVING COUNT(*) > 0
AND COUNT(DISTINCT position.`DepartmentId`) = 1
AND SUM(position.`DepartmentId` IS NULL) = 0
) source ON source.`OnboardingTemplateId` = template.`Id`
SET template.`DepartmentId` = source.`DepartmentId`
WHERE template.`DepartmentId` IS NULL;
-- 安全回填合同模板公司、部门:只有历史引用的入职模板归属完全一致时才回填。
UPDATE `ob_contract_template` contract_template
INNER JOIN (
SELECT
relation.`ContractTemplateId`,
MIN(template.`CompanyId`) AS `CompanyId`,
MIN(template.`DepartmentId`) AS `DepartmentId`
FROM `ob_onboarding_template_contract` relation
INNER JOIN `ob_onboarding_template` template
ON template.`Id` = relation.`OnboardingTemplateId`
WHERE COALESCE(relation.`IsDelete`, 0) = 0
AND COALESCE(template.`IsDelete`, 0) = 0
GROUP BY relation.`ContractTemplateId`
HAVING COUNT(DISTINCT template.`CompanyId`) = 1
AND COUNT(DISTINCT template.`DepartmentId`) = 1
AND SUM(template.`DepartmentId` IS NULL) = 0
) source ON source.`ContractTemplateId` = contract_template.`Id`
SET contract_template.`CompanyId` = COALESCE(contract_template.`CompanyId`, source.`CompanyId`),
contract_template.`DepartmentId` = COALESCE(contract_template.`DepartmentId`, source.`DepartmentId`)
WHERE contract_template.`CompanyId` IS NULL
OR contract_template.`DepartmentId` IS NULL;
-- 根据历史入职模板关系,回填合同模板已经实际使用过的岗位范围。
INSERT IGNORE INTO `ob_contract_template_position` (
`Id`,
`ContractTemplateId`,
`PositionId`,
`Status`,
`CreateTime`,
`IsDelete`
)
SELECT DISTINCT
CAST(CONV(SUBSTRING(MD5(CONCAT(
'obctp:', contract_relation.`ContractTemplateId`, ':', position_relation.`PositionId`
)), 1, 15), 16, 10) AS UNSIGNED) AS `Id`,
contract_relation.`ContractTemplateId`,
position_relation.`PositionId`,
'enable',
NOW(3),
0
FROM `ob_onboarding_template_contract` contract_relation
INNER JOIN `ob_onboarding_template_position` position_relation
ON position_relation.`OnboardingTemplateId` = contract_relation.`OnboardingTemplateId`
INNER JOIN `ob_contract_template` contract_template
ON contract_template.`Id` = contract_relation.`ContractTemplateId`
INNER JOIN `ob_position` position
ON position.`Id` = position_relation.`PositionId`
WHERE COALESCE(contract_relation.`IsDelete`, 0) = 0
AND COALESCE(position_relation.`IsDelete`, 0) = 0
AND contract_template.`CompanyId` = position.`CompanyId`
AND contract_template.`DepartmentId` = position.`DepartmentId`;
DROP PROCEDURE IF EXISTS `ob_scope_add_unique_index_if_clean`;
DROP PROCEDURE IF EXISTS `ob_scope_add_index_if_missing`;
DROP PROCEDURE IF EXISTS `ob_scope_add_column_if_missing`;
-- 结果 1:仍未明确部门归属的岗位。必须在岗位管理中补齐。
SELECT
position.`Id`,
position.`PositionCode`,
position.`PositionName`,
position.`CompanyId`
FROM `ob_position` position
WHERE position.`DepartmentId` IS NULL
AND COALESCE(position.`IsDelete`, 0) = 0;
-- 结果 2:仍未明确部门归属的入职模板。必须编辑并重新保存。
SELECT
template.`Id`,
template.`TemplateCode`,
template.`TemplateName`,
template.`CompanyId`
FROM `ob_onboarding_template` template
WHERE template.`DepartmentId` IS NULL
AND COALESCE(template.`IsDelete`, 0) = 0;
-- 结果 3:仍未明确公司/部门或没有适用岗位的合同模板。必须编辑并重新保存。
SELECT
contract_template.`Id`,
contract_template.`TemplateCode`,
contract_template.`TemplateName`,
contract_template.`CompanyId`,
contract_template.`DepartmentId`,
COUNT(position_relation.`Id`) AS `PositionCount`
FROM `ob_contract_template` contract_template
LEFT JOIN `ob_contract_template_position` position_relation
ON position_relation.`ContractTemplateId` = contract_template.`Id`
AND COALESCE(position_relation.`IsDelete`, 0) = 0
WHERE COALESCE(contract_template.`IsDelete`, 0) = 0
GROUP BY
contract_template.`Id`,
contract_template.`TemplateCode`,
contract_template.`TemplateName`,
contract_template.`CompanyId`,
contract_template.`DepartmentId`
HAVING contract_template.`CompanyId` IS NULL
OR contract_template.`DepartmentId` IS NULL
OR COUNT(position_relation.`Id`) = 0;
-- 结果 4:检查层级不一致的数据,正常情况下应返回 0 行。
SELECT
contract_template.`Id` AS `ContractTemplateId`,
contract_template.`TemplateName`,
position.`Id` AS `PositionId`,
position.`PositionName`
FROM `ob_contract_template` contract_template
INNER JOIN `ob_contract_template_position` relation
ON relation.`ContractTemplateId` = contract_template.`Id`
INNER JOIN `ob_position` position ON position.`Id` = relation.`PositionId`
WHERE contract_template.`CompanyId` <> position.`CompanyId`
OR contract_template.`DepartmentId` <> position.`DepartmentId`;
-- 结果 5:正式员工身份证号或手机号重复时,唯一索引会被安全跳过;请清理后重新运行脚本。
SELECT
'IdCardNumber' AS `ConflictField`,
employee.`IdCardNumber` AS `ConflictValue`,
COUNT(*) AS `ConflictCount`
FROM `ob_employee` employee
WHERE NULLIF(TRIM(employee.`IdCardNumber`), '') IS NOT NULL
GROUP BY employee.`IdCardNumber`
HAVING COUNT(*) > 1
UNION ALL
SELECT
'Phone' AS `ConflictField`,
employee.`Phone` AS `ConflictValue`,
COUNT(*) AS `ConflictCount`
FROM `ob_employee` employee
WHERE NULLIF(TRIM(employee.`Phone`), '') IS NOT NULL
GROUP BY employee.`Phone`
HAVING COUNT(*) > 1;