✨ 完善客户业务员关联和同步

This commit is contained in:
caojia
2026-10-28 22:19:47 +08:00
parent 1688cddac7
commit 5f082d905d
14 changed files with 344 additions and 66 deletions
@@ -1,10 +1,4 @@
-- 客户归属销售员由"工号"改存本系统用户ID(数据权限按 user_id 控制,工号后续修改不影响归属)
-- 同步链路已在 CustomerSyncService.resolveSalesUserId 做同样转换,本脚本只处理存量数据。
-- 仅转换"含非数字字符"的工号值(工号形如 WS0052/WSC0184/wsadmin);
-- 已是数字(user_id)的行不触碰,脚本可重复执行。
UPDATE ctr_customer c
JOIN sys_user u ON u.del_flag = '0' AND u.user_name = c.sales_user_id
SET c.sales_user_id = CONVERT(u.user_id, CHAR)
WHERE c.del_flag = '0'
AND c.sales_user_id IS NOT NULL
AND c.sales_user_id REGEXP '[^0-9]';
-- DEPRECATED: 不要执行本文件。
-- 旧版脚本仅按非数字判断工号并直接 UPDATE,无法安全处理重复工号和数字工号/user_id 冲突。
-- 请先执行 customer+supplier/20261028_customer_sales_user_classification.sql 查看清单,
-- 再按上线顺序执行 customer+supplier/20261028_customer_sales_user_migration.sql。
+2 -1
View File
@@ -356,7 +356,8 @@ CREATE TABLE `ctr_customer` (
`credit_code` varchar(18) DEFAULT '' COMMENT '统一社会信用代码',
`customer_type` char(1) DEFAULT '0' COMMENT '业务类型(0直客 1同行 2集运)',
`is_enable` char(1) DEFAULT '0' COMMENT '启用状态(0正常 1停用)',
`sales_user_id` varchar(64) DEFAULT '' COMMENT '业务员用户ID',
`sales_user_id` varchar(64) DEFAULT NULL COMMENT '业务员系统用户ID(sys_user.user_id)',
`source_sales_code` varchar(255) DEFAULT NULL COMMENT '客户同步源业务员工号(内部字段)',
`sales_user` varchar(50) DEFAULT '' COMMENT '业务员姓名',
`ed` decimal(15,2) DEFAULT '0.00' COMMENT '资金额度(元)',
`zq` int(11) DEFAULT '0' COMMENT '回款账期(天)',
@@ -0,0 +1,75 @@
-- MySQL 5.7
-- 客户业务员存量迁移前分类清单(只读,不修改数据)。
-- 请先执行本文件,检查 ID/工号冲突,再执行 20261028_customer_sales_user_migration.sql。
-- 分类依据:精确匹配 sys_user.user_id;或按区分大小写的 sys_user.user_name 唯一匹配。
-- 分类汇总:VALID_USER_ID 保留;UNIQUE_USERNAME 可安全转换;其余分类写入冲突/未匹配清单。
SELECT classified.classification, COUNT(*) AS customer_count
FROM (
SELECT c.customer_id,
CASE
WHEN c.sales_user_id IS NULL OR TRIM(c.sales_user_id) = '' THEN 'EMPTY'
WHEN id_match.match_count > 0 AND name_match.match_count > 0 THEN 'ID_USERNAME_COLLISION'
WHEN id_match.match_count > 0 THEN 'VALID_USER_ID'
WHEN name_match.match_count = 1 THEN 'UNIQUE_USERNAME'
WHEN name_match.match_count > 1 THEN 'AMBIGUOUS_USERNAME'
ELSE 'NO_MATCH'
END AS classification
FROM ctr_customer c
LEFT JOIN (
SELECT CAST(user_id AS CHAR) AS lookup_value, COUNT(*) AS match_count
FROM sys_user
WHERE del_flag = '0'
GROUP BY CAST(user_id AS CHAR)
) id_match ON BINARY id_match.lookup_value = BINARY TRIM(c.sales_user_id)
LEFT JOIN (
SELECT BINARY TRIM(user_name) AS lookup_value, COUNT(*) AS match_count
FROM sys_user
WHERE del_flag = '0'
AND user_name IS NOT NULL
AND user_name != ''
GROUP BY BINARY TRIM(user_name)
) name_match ON name_match.lookup_value = BINARY TRIM(c.sales_user_id)
WHERE c.del_flag = '0'
) classified
GROUP BY classified.classification
ORDER BY classified.classification;
-- 逐条分类。冲突项会列出原值、user_id 候选和 user_name 候选,供管理员人工核实。
SELECT c.customer_id,
c.customer_code,
c.customer_name,
c.sales_user_id AS original_sales_user_id,
CASE
WHEN c.sales_user_id IS NULL OR TRIM(c.sales_user_id) = '' THEN 'EMPTY'
WHEN id_match.match_count > 0 AND name_match.match_count > 0 THEN 'ID_USERNAME_COLLISION'
WHEN id_match.match_count > 0 THEN 'VALID_USER_ID'
WHEN name_match.match_count = 1 THEN 'UNIQUE_USERNAME'
WHEN name_match.match_count > 1 THEN 'AMBIGUOUS_USERNAME'
ELSE 'NO_MATCH'
END AS classification,
COALESCE(id_match.match_count, 0) AS user_id_match_count,
COALESCE(name_match.match_count, 0) AS user_name_match_count,
id_match.candidate_user_ids AS user_id_candidates,
name_match.candidate_user_ids AS user_name_candidates
FROM ctr_customer c
LEFT JOIN (
SELECT CAST(user_id AS CHAR) AS lookup_value,
COUNT(*) AS match_count,
GROUP_CONCAT(CAST(user_id AS CHAR) ORDER BY user_id SEPARATOR ',') AS candidate_user_ids
FROM sys_user
WHERE del_flag = '0'
GROUP BY CAST(user_id AS CHAR)
) id_match ON BINARY id_match.lookup_value = BINARY TRIM(c.sales_user_id)
LEFT JOIN (
SELECT BINARY TRIM(user_name) AS lookup_value,
COUNT(*) AS match_count,
GROUP_CONCAT(CAST(user_id AS CHAR) ORDER BY user_id SEPARATOR ',') AS candidate_user_ids
FROM sys_user
WHERE del_flag = '0'
AND user_name IS NOT NULL
AND user_name != ''
GROUP BY BINARY TRIM(user_name)
) name_match ON name_match.lookup_value = BINARY TRIM(c.sales_user_id)
WHERE c.del_flag = '0'
ORDER BY classification, c.customer_id;
@@ -0,0 +1,142 @@
-- MySQL 5.7
-- 客户业务员工号 -> 系统用户ID 存量迁移。
-- 上线前先执行 20261028_customer_sales_user_classification.sql 并核对 ID_USERNAME_COLLISION。
-- 自动转换仅处理唯一 user_name 匹配;未匹配、重复匹配和 ID/工号冲突会备份原值、清空 sales_user_id,留给管理员在客户编辑页处理。
-- 本脚本不连接数据库执行;可重复运行。完成标记写入后,重复运行不会再次处理存量或改写备份。
-- 新增内部源工号列(兼容 MySQL 5.7,不依赖 ADD COLUMN IF NOT EXISTS)。
SET @add_source_sales_code_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE ctr_customer ADD COLUMN source_sales_code varchar(255) DEFAULT NULL COMMENT ''客户同步源业务员工号(内部字段)'' AFTER sales_user_id',
'SELECT 1')
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'ctr_customer'
AND column_name = 'source_sales_code'
);
PREPARE add_source_sales_code_stmt FROM @add_source_sales_code_sql;
EXECUTE add_source_sales_code_stmt;
DEALLOCATE PREPARE add_source_sales_code_stmt;
-- 未匹配时使用 NULL,避免空字符串伪装成用户ID。
ALTER TABLE ctr_customer
MODIFY COLUMN sales_user_id varchar(64) DEFAULT NULL COMMENT '业务员系统用户ID(sys_user.user_id)';
-- 备份表同时保留分类结果和候选ID;PRIMARY KEY + INSERT IGNORE 保证原始值不会被后续重跑覆盖。
CREATE TABLE IF NOT EXISTS ctr_customer_sales_user_migration_backup (
customer_id bigint(20) NOT NULL COMMENT '客户ID',
customer_code varchar(50) NOT NULL COMMENT '客户编码',
original_sales_user_id varchar(64) DEFAULT NULL COMMENT '迁移前 sales_user_id 原值',
classification varchar(40) NOT NULL COMMENT '迁移前分类',
user_id_match_count int NOT NULL DEFAULT 0 COMMENT '精确 user_id 匹配数',
user_name_match_count int NOT NULL DEFAULT 0 COMMENT '精确 user_name 匹配数',
user_id_candidates text COMMENT 'user_id 匹配候选',
user_name_candidates text COMMENT 'user_name 匹配候选',
backup_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '备份时间',
PRIMARY KEY (customer_id),
KEY idx_customer_sales_user_backup_classification (classification)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户业务员ID迁移原值及分类备份';
-- 防止成功执行后重跑时把后来新建/人工维护的客户重新当作存量数据处理。
CREATE TABLE IF NOT EXISTS ctr_customer_sales_user_migration_control (
migration_key varchar(64) NOT NULL,
status varchar(16) NOT NULL DEFAULT 'PENDING',
completed_at datetime DEFAULT NULL,
PRIMARY KEY (migration_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户业务员ID迁移执行状态';
INSERT IGNORE INTO ctr_customer_sales_user_migration_control (migration_key, status)
VALUES ('20261028_customer_sales_user', 'PENDING');
START TRANSACTION;
-- 首次运行时先保留旧值和候选;后续事务失败可安全重跑,成功后由 control 表阻止再次处理。
INSERT IGNORE INTO ctr_customer_sales_user_migration_backup (
customer_id, customer_code, original_sales_user_id, classification,
user_id_match_count, user_name_match_count, user_id_candidates, user_name_candidates
)
SELECT c.customer_id,
c.customer_code,
c.sales_user_id,
CASE
WHEN c.sales_user_id IS NULL OR TRIM(c.sales_user_id) = '' THEN 'EMPTY'
WHEN id_match.match_count > 0 AND name_match.match_count > 0 THEN 'ID_USERNAME_COLLISION'
WHEN id_match.match_count > 0 THEN 'VALID_USER_ID'
WHEN name_match.match_count = 1 THEN 'UNIQUE_USERNAME'
WHEN name_match.match_count > 1 THEN 'AMBIGUOUS_USERNAME'
ELSE 'NO_MATCH'
END,
COALESCE(id_match.match_count, 0),
COALESCE(name_match.match_count, 0),
id_match.candidate_user_ids,
name_match.candidate_user_ids
FROM ctr_customer c
JOIN ctr_customer_sales_user_migration_control migration
ON migration.migration_key = '20261028_customer_sales_user'
AND migration.status = 'PENDING'
LEFT JOIN (
SELECT CAST(user_id AS CHAR) AS lookup_value,
COUNT(*) AS match_count,
GROUP_CONCAT(CAST(user_id AS CHAR) ORDER BY user_id SEPARATOR ',') AS candidate_user_ids
FROM sys_user
WHERE del_flag = '0'
GROUP BY CAST(user_id AS CHAR)
) id_match ON BINARY id_match.lookup_value = BINARY TRIM(c.sales_user_id)
LEFT JOIN (
SELECT BINARY TRIM(user_name) AS lookup_value,
COUNT(*) AS match_count,
GROUP_CONCAT(CAST(user_id AS CHAR) ORDER BY user_id SEPARATOR ',') AS candidate_user_ids
FROM sys_user
WHERE del_flag = '0'
AND user_name IS NOT NULL
AND user_name != ''
GROUP BY BINARY TRIM(user_name)
) name_match ON name_match.lookup_value = BINARY TRIM(c.sales_user_id)
WHERE c.del_flag = '0'
AND c.source_sales_code IS NULL;
-- 唯一工号映射到系统用户ID;不唯一、不匹配或存在ID/工号冲突的项不猜测,清空关联并保留源值。
UPDATE ctr_customer c
JOIN ctr_customer_sales_user_migration_backup b
ON b.customer_id = c.customer_id
JOIN ctr_customer_sales_user_migration_control migration
ON migration.migration_key = '20261028_customer_sales_user'
AND migration.status = 'PENDING'
SET c.source_sales_code = b.original_sales_user_id,
c.sales_user_id = CASE
WHEN b.classification = 'UNIQUE_USERNAME'
THEN b.user_name_candidates
ELSE NULL
END
WHERE c.del_flag = '0'
AND c.source_sales_code IS NULL
AND b.classification IN (
'UNIQUE_USERNAME',
'AMBIGUOUS_USERNAME',
'NO_MATCH',
'ID_USERNAME_COLLISION'
);
-- 将旧空字符串归一为 NULL,已确认是系统用户ID的行保持不变。
UPDATE ctr_customer c
JOIN ctr_customer_sales_user_migration_control migration
ON migration.migration_key = '20261028_customer_sales_user'
AND migration.status = 'PENDING'
SET c.sales_user_id = NULL
WHERE c.del_flag = '0'
AND c.sales_user_id IS NOT NULL
AND TRIM(c.sales_user_id) = '';
UPDATE ctr_customer_sales_user_migration_control
SET status = 'DONE', completed_at = NOW()
WHERE migration_key = '20261028_customer_sales_user'
AND status = 'PENDING';
COMMIT;
-- 执行后核对人工处理清单;原值和候选均保存在备份表中。
SELECT customer_id, customer_code, original_sales_user_id, classification,
user_id_candidates, user_name_candidates
FROM ctr_customer_sales_user_migration_backup
WHERE classification IN ('ID_USERNAME_COLLISION', 'AMBIGUOUS_USERNAME', 'NO_MATCH')
ORDER BY classification, customer_id;