From 5f082d905ddd636c1ac6dacf17fde566d9f85d05 Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Wed, 28 Oct 2026 22:19:47 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=AE=8C=E5=96=84=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E4=B8=9A=E5=8A=A1=E5=91=98=E5=85=B3=E8=81=94=E5=92=8C?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customerinfo/domain/BmsCustomer.java | 16 +- .../mapper/BmsCustomerMapper.java | 3 + .../mapper/CustomerSyncMapper.java | 11 +- .../impl/BmsCustomerLogServiceImpl.java | 2 +- .../service/impl/BmsCustomerServiceImpl.java | 21 ++- .../sync/CustomerSyncService.java | 76 +++++++--- .../mapper/customer/BmsCustomerMapper.xml | 18 ++- .../mapper/customer/CustomerSyncMapper.xml | 22 ++- .../mapper/price/PrcPriceSetMapper.xml | 3 +- ...20261019_customer_sales_user_to_userid.sql | 14 +- bms-back/sql/bms_init.sql | 3 +- ...028_customer_sales_user_classification.sql | 75 +++++++++ ...20261028_customer_sales_user_migration.sql | 142 ++++++++++++++++++ .../customerinfo/index.vue | 4 +- 14 files changed, 344 insertions(+), 66 deletions(-) create mode 100644 bms-back/sql/customer+supplier/20261028_customer_sales_user_classification.sql create mode 100644 bms-back/sql/customer+supplier/20261028_customer_sales_user_migration.sql diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java index 7c1070c4..1d351d7e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java @@ -7,9 +7,9 @@ import java.util.Date; import java.util.List; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import lombok.ToString; import com.bms.common.annotation.Excel; import com.bms.common.core.domain.BaseEntity; @@ -52,16 +52,20 @@ public class BmsCustomer extends BaseEntity private String isEnable; /** 业务员用户ID */ - @Excel(name = "业务员用户ID") private String salesUserId; - /** 业务员姓名 */ - @Excel(name = "业务员姓名") + /** 业务员姓名(查询结果只回填已关联系统用户的姓名) */ private String salesUser; - /** 业务员昵称(join sys_user.nick_name,sales_user_id 存工号=sys_user.user_name) */ + /** 业务员昵称(关联 sys_user.nick_name) */ + @Excel(name = "业务员") private String salesUserNickName; + /** 客户同步源中的原始业务员工号,仅供内部同步和迁移使用。 */ + @JsonIgnore + @ToString.Exclude + private String sourceSalesCode; + /** 资金额度(元) */ @Excel(name = "资金额度", readConverterExp = "元=") private BigDecimal ed; diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/BmsCustomerMapper.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/BmsCustomerMapper.java index 2edfef01..540b5206 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/BmsCustomerMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/BmsCustomerMapper.java @@ -34,6 +34,9 @@ public interface BmsCustomerMapper */ public List selectBmsCustomerList(BmsCustomer bmsCustomer); + /** 校验业务员用户ID仍对应未删除的系统用户。 */ + public int countValidSalesUserId(@org.apache.ibatis.annotations.Param("salesUserId") String salesUserId); + /** * 新增客户主 * diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/CustomerSyncMapper.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/CustomerSyncMapper.java index db93569d..d04ffb55 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/CustomerSyncMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/mapper/CustomerSyncMapper.java @@ -11,16 +11,15 @@ import com.bms.customer.customerinfo.domain.BmsCustomer; */ public interface CustomerSyncMapper { - /** - * 库中未删除的全部客户编码,用于判定新增/更新 - */ + /** 库中未删除的全部客户编码,用于判定新增/更新。 */ List selectExistingCodes(); - /** - * 未删除用户的 工号(user_name)->用户ID(user_id) 映射,用于 TMS BELONG_SALER 工号转换 - */ + /** 未删除用户映射,用于唯一工号匹配。 */ List selectUserLoginMap(); + /** 读取存量客户的人工业务员选择和上次同步的源工号。 */ + List selectExistingSalesAssignments(); + /** * 新增客户(同步专用,字段集与老系统导入脚本一致) */ diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java index 01279b9a..128c1f9a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java @@ -587,7 +587,7 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService return s == null ? "" : s; } - /** 人员字段展示:姓名(工号),缺姓名时只显示工号 */ + /** 人员字段展示:姓名(系统用户ID),缺姓名时只显示系统用户ID。 */ private static String userDisplay(String code, String name) { String c = trimToNull(code); diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerServiceImpl.java index 0718dfe2..63ff757e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerServiceImpl.java @@ -82,7 +82,7 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService * @return 客户主 */ @Override - // sales_user_id 存的是员工工号(=sys_user.user_name),仅本人/本部门均通过 sys_user 别名 su 关联匹配 + // sales_user_id 只存系统 user_id;数据权限通过该用户的部门和用户范围判断。 @DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id") public List selectBmsCustomerList(BmsCustomer bmsCustomer) { @@ -275,6 +275,7 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService public int insertBmsCustomer(BmsCustomer bmsCustomer) { checkContractDate(bmsCustomer.getContractStartDate(), bmsCustomer.getContractEndDate()); + normalizeAndValidateSalesUserId(bmsCustomer); SysUser user = SecurityUtils.getLoginUser().getUser(); bmsCustomer.setCreateTime(DateUtils.getNowDate()); bmsCustomer.setCreateBy(user.getUserName()); @@ -303,6 +304,7 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService public int updateBmsCustomer(BmsCustomer bmsCustomer) { checkContractDate(bmsCustomer.getContractStartDate(), bmsCustomer.getContractEndDate()); + normalizeAndValidateSalesUserId(bmsCustomer); SysUser user = SecurityUtils.getLoginUser().getUser(); bmsCustomer.setUpdateTime(DateUtils.getNowDate()); bmsCustomer.setUpdateBy(user.getUserName()); @@ -518,4 +520,21 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService throw new ServiceException("合同结束时间不能早于开始时间"); } } + + /** 客户编辑仅允许写入真实存在的系统用户ID,不接受工号作为兼容值。 */ + private void normalizeAndValidateSalesUserId(BmsCustomer customer) + { + String salesUserId = customer.getSalesUserId(); + if (salesUserId == null || salesUserId.trim().isEmpty()) + { + customer.setSalesUserId(null); + return; + } + salesUserId = salesUserId.trim(); + if (bmsCustomerMapper.countValidSalesUserId(salesUserId) == 0) + { + throw new ServiceException("业务员不存在或已删除,请从业务员选择器中重新选择"); + } + customer.setSalesUserId(salesUserId); + } } diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java index 7f90ebf3..9b45cb26 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java @@ -9,6 +9,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +24,7 @@ import com.bms.customer.customerinfo.mapper.CustomerSyncMapper; * 老系统 Oracle TAB_CUSTOMER -> ctr_customer 每日同步。 * * 字段映射、字典转换与 D:\\生产客户数据\\scripts\\import_customers.py 完全一致: - * 按 customer_code 判定新增/更新;源为空的字段不写(走默认值/保留原值)。 + * 按 customer_code 判定新增/更新;普通资料的源空字段不写,业务员归属则按匹配结果明确写入ID或NULL。 * * @author bms */ @@ -59,7 +60,6 @@ public class CustomerSyncService private static final int LEN_CODE = 50; private static final int LEN_NAME = 100; private static final int LEN_CODE_DW = 50; - private static final int LEN_USER_ID = 64; private static final int LEN_USER = 50; private static final int LEN_REMARK = 500; /** 告警明细最多打印条数 */ @@ -86,8 +86,15 @@ public class CustomerSyncService Set existingCodes = new HashSet<>(customerSyncMapper.selectExistingCodes()); - // 业务员归属转本系统用户ID:数据权限按 user_id 控制,工号后续修改不影响归属 - Map userIdByLogin = loadUserIdByLogin(); + // 源工号只用于匹配;归属字段始终保存系统 user_id。 + Map> userIdsByLogin = loadUserIdsByLogin(); + Set validUserIds = new HashSet<>(); + userIdsByLogin.values().forEach(validUserIds::addAll); + Map existingSalesAssignments = new HashMap<>(); + for (BmsCustomer existing : customerSyncMapper.selectExistingSalesAssignments()) + { + existingSalesAssignments.put(existing.getCustomerCode(), existing); + } List inserts = new ArrayList<>(); List updates = new ArrayList<>(); @@ -123,7 +130,8 @@ public class CustomerSyncService continue; } - BmsCustomer c = mapRow(row, code, name, userIdByLogin, warnings); + BmsCustomer c = mapRow(row, code, name, userIdsByLogin, + validUserIds, existingSalesAssignments.get(code), warnings); if (existingCodes.contains(code)) { c.setUpdateBy(SYNC_MARKER); @@ -161,7 +169,8 @@ public class CustomerSyncService * 单行映射(逻辑对齐 import_customers.py 的 build_row) */ private BmsCustomer mapRow(Map row, String code, String name, - Map userIdByLogin, List warnings) + Map> userIdsByLogin, Set validUserIds, + BmsCustomer existingAssignment, List warnings) { BmsCustomer c = new BmsCustomer(); c.setCustomerCode(truncate(code, LEN_CODE, code, "customer_code", warnings)); @@ -169,7 +178,10 @@ public class CustomerSyncService c.setDelFlag("0"); c.setCustomerCodeDw(truncate(getStr(row, "CUSTOMER_OUTER_CODE"), LEN_CODE_DW, code, "customer_code_dw", warnings)); - c.setSalesUserId(resolveSalesUserId(clean(getStr(row, "BELONG_SALER")), userIdByLogin, code, warnings)); + String sourceSalesCode = clean(getStr(row, "BELONG_SALER")); + c.setSourceSalesCode(sourceSalesCode); + c.setSalesUserId(resolveSalesUserId(sourceSalesCode, userIdsByLogin, + validUserIds, existingAssignment, code, warnings)); c.setSalesUser(truncate(getStr(row, "BELONG_SALER_NAME"), LEN_USER, code, "sales_user", warnings)); c.setSettlementUser(truncate(getStr(row, "SETTLEMENT_MAN"), LEN_USER, code, "settlement_user", warnings)); c.setServiceUser(truncate(getStr(row, "SERVICE_MAN"), LEN_USER, code, "service_user", warnings)); @@ -231,40 +243,56 @@ public class CustomerSyncService return ("1".equals(s) || "1.0".equals(s)) ? "0" : "1"; } - /** 加载未删除用户的 工号(user_name)->用户ID(user_id) 映射 */ - private Map loadUserIdByLogin() + /** 加载未删除用户的工号映射;保留重复项以便将歧义匹配按未关联处理。 */ + private Map> loadUserIdsByLogin() { - Map map = new HashMap<>(); + Map> map = new HashMap<>(); for (SysUser u : customerSyncMapper.selectUserLoginMap()) { - if (u != null && u.getUserName() != null && u.getUserId() != null - && !map.containsKey(u.getUserName())) + if (u != null && clean(u.getUserName()) != null && u.getUserId() != null) { - map.put(u.getUserName(), String.valueOf(u.getUserId())); + map.computeIfAbsent(u.getUserName().trim(), key -> new ArrayList<>()) + .add(String.valueOf(u.getUserId())); } } return map; } /** - * 业务员工号 -> 本系统 sys_user.user_id 转换(数据权限按 user_id 控制,工号修改不影响归属)。 - * 工号在系统用户中不存在时保留原工号并告警(常见于该用户尚未同步),避免归属信息丢失。 + * 只在唯一匹配时关联系统用户。 + * 源工号不变时保留客户页面人工选择的有效业务员;工号为空时也保留有效业务员。 */ - private String resolveSalesUserId(String salerCode, Map userIdByLogin, + private String resolveSalesUserId(String salerCode, Map> userIdsByLogin, + Set validUserIds, BmsCustomer existingAssignment, String code, List warnings) { - String s = truncate(salerCode, LEN_USER_ID, code, "sales_user_id", warnings); - if (s == null) + String existingUserId = existingAssignment == null ? null : clean(existingAssignment.getSalesUserId()); + boolean existingUserValid = existingUserId != null && validUserIds.contains(existingUserId); + if (salerCode == null) { - return null; + return existingUserValid ? existingUserId : null; } - String userId = userIdByLogin.get(s); - if (userId != null) + + boolean sourceCodeUnchanged = existingAssignment != null + && Objects.equals(salerCode, clean(existingAssignment.getSourceSalesCode())); + boolean sourceCodeUnknown = existingAssignment != null + && clean(existingAssignment.getSourceSalesCode()) == null; + if (existingUserValid && (sourceCodeUnchanged || sourceCodeUnknown)) { - return userId; + // 历史有效用户ID可能尚无可恢复的源工号;首次同步建立源工号基线时不覆盖已有人工归属。 + return existingUserId; } - warnings.add(code + ": 业务员工号 " + s + " 在系统用户中不存在,sales_user_id 保留原工号"); - return s; + + List matchedUserIds = userIdsByLogin.get(salerCode); + if (matchedUserIds != null && matchedUserIds.size() == 1) + { + return matchedUserIds.get(0); + } + + warnings.add(code + (matchedUserIds == null || matchedUserIds.isEmpty() + ? ": 业务员工号未匹配到系统用户,客户保持未关联" + : ": 业务员工号匹配到多个系统用户,客户保持未关联")); + return null; } private static String clean(String v) diff --git a/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml b/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml index a490fdec..eed04e5e 100644 --- a/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml @@ -45,7 +45,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, - c.is_enable, c.sales_user_id, c.sales_user, c.ed, c.zq, c.settlement_user_id, c.settlement_user, + c.is_enable, + case when su.user_id is not null then cast(su.user_id as char) else null end as sales_user_id, + su.nick_name as sales_user, c.ed, c.zq, c.settlement_user_id, c.settlement_user, c.service_user_id, c.service_user, c.settlement_type, c.temp_ed, (select coalesce(sum(ca.balance), 0) from ctr_customer_account ca where ca.customer_id = c.customer_id and ca.currency = 'CNY' and ca.del_flag = '0') as account_balance, @@ -61,11 +63,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" su.nick_name as sales_user_nick_name, cps.price_set_id, ps.price_set_name from ctr_customer c - + left join sys_user su on su.del_flag = '0' - and (su.user_id = c.sales_user_id + 0 or su.user_name = c.sales_user_id) + and cast(su.user_id as char) = c.sales_user_id left join prc_customer_price_set cps on cps.customer_id = c.customer_id left join prc_price_set ps on ps.price_set_id = cps.price_set_id @@ -92,7 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and c.customer_type = #{customerType} and c.is_enable = #{isEnable} and c.sales_user_id = #{salesUserId} - and c.sales_user = #{salesUser} + and su.nick_name = #{salesUser} and c.ed = #{ed} and c.zq = #{zq} and c.settlement_user_id = #{settlementUserId} @@ -111,6 +112,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by c.customer_id desc + + - + - + + + INSERT INTO ctr_customer ( customer_code, customer_name, is_enable, - del_flag, create_by + del_flag, create_by, sales_user_id, source_sales_code , customer_code_dw , customer_type - , sales_user_id , sales_user , settlement_user , service_user @@ -37,10 +43,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" , create_time ) VALUES ( #{customerCode}, #{customerName}, #{isEnable}, - '0', #{createBy} + '0', #{createBy}, #{salesUserId}, #{sourceSalesCode} , #{customerCodeDw} , #{customerType} - , #{salesUserId} , #{salesUser} , #{settlementUser} , #{serviceUser} @@ -61,7 +66,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" , customer_name = #{customerName} , customer_code_dw = #{customerCodeDw} , customer_type = #{customerType} - , sales_user_id = #{salesUserId} + , sales_user_id = #{salesUserId} + , source_sales_code = #{sourceSalesCode} , sales_user = #{salesUser} , settlement_user = #{settlementUser} , service_user = #{serviceUser} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml index f250e02f..6dcdff31 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml @@ -93,8 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND ifnull(c.del_flag, '0') = '0' INNER JOIN sys_user su ON su.del_flag = '0' - AND (su.user_name = c.sales_user_id COLLATE utf8mb4_general_ci - OR su.user_id = c.sales_user_id + 0) + AND cast(su.user_id as char) = c.sales_user_id WHERE cps.price_set_id = s.price_set_id ${params.dataScope} ) diff --git a/bms-back/sql/20261019_customer_sales_user_to_userid.sql b/bms-back/sql/20261019_customer_sales_user_to_userid.sql index 9fce5381..30f9fe7b 100644 --- a/bms-back/sql/20261019_customer_sales_user_to_userid.sql +++ b/bms-back/sql/20261019_customer_sales_user_to_userid.sql @@ -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。 diff --git a/bms-back/sql/bms_init.sql b/bms-back/sql/bms_init.sql index 8cc6d792..dfabf801 100644 --- a/bms-back/sql/bms_init.sql +++ b/bms-back/sql/bms_init.sql @@ -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 '回款账期(天)', diff --git a/bms-back/sql/customer+supplier/20261028_customer_sales_user_classification.sql b/bms-back/sql/customer+supplier/20261028_customer_sales_user_classification.sql new file mode 100644 index 00000000..38b33d56 --- /dev/null +++ b/bms-back/sql/customer+supplier/20261028_customer_sales_user_classification.sql @@ -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; diff --git a/bms-back/sql/customer+supplier/20261028_customer_sales_user_migration.sql b/bms-back/sql/customer+supplier/20261028_customer_sales_user_migration.sql new file mode 100644 index 00000000..a611f8bd --- /dev/null +++ b/bms-back/sql/customer+supplier/20261028_customer_sales_user_migration.sql @@ -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; diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue index 9233e870..36373df0 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue @@ -148,7 +148,7 @@ - + @@ -393,7 +393,7 @@ const copyColumns = computed(() => [ { key: 'creditCode', label: '统一社会信用代码', get: r => r.creditCode || '' }, { key: 'customerType', label: '业务类型', get: r => proxy.selectDictLabel(customer_type.value, r.customerType) }, { key: 'isEnable', label: '启用状态', get: r => proxy.selectDictLabel(sys_normal_disable.value, r.isEnable) }, - { key: 'salesUserId', label: '业务员', get: r => r.salesUserNickName || r.salesUser || r.salesUserId || '' }, + { key: 'salesUserId', label: '业务员', get: r => r.salesUserNickName || '' }, { key: 'zq', label: '回款账期', get: r => (r.zq === null || r.zq === undefined ? '' : r.zq) }, { key: 'settlementUserId', label: '结算员', get: r => getUserName(r.settlementUserId) }, { key: 'serviceUserId', label: '客服', get: r => getUserName(r.serviceUserId) },