✨ 完善客户业务员关联和同步
This commit is contained in:
+10
-6
@@ -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;
|
||||
|
||||
+3
@@ -34,6 +34,9 @@ public interface BmsCustomerMapper
|
||||
*/
|
||||
public List<BmsCustomer> selectBmsCustomerList(BmsCustomer bmsCustomer);
|
||||
|
||||
/** 校验业务员用户ID仍对应未删除的系统用户。 */
|
||||
public int countValidSalesUserId(@org.apache.ibatis.annotations.Param("salesUserId") String salesUserId);
|
||||
|
||||
/**
|
||||
* 新增客户主
|
||||
*
|
||||
|
||||
+5
-6
@@ -11,16 +11,15 @@ import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
*/
|
||||
public interface CustomerSyncMapper
|
||||
{
|
||||
/**
|
||||
* 库中未删除的全部客户编码,用于判定新增/更新
|
||||
*/
|
||||
/** 库中未删除的全部客户编码,用于判定新增/更新。 */
|
||||
List<String> selectExistingCodes();
|
||||
|
||||
/**
|
||||
* 未删除用户的 工号(user_name)->用户ID(user_id) 映射,用于 TMS BELONG_SALER 工号转换
|
||||
*/
|
||||
/** 未删除用户映射,用于唯一工号匹配。 */
|
||||
List<SysUser> selectUserLoginMap();
|
||||
|
||||
/** 读取存量客户的人工业务员选择和上次同步的源工号。 */
|
||||
List<BmsCustomer> selectExistingSalesAssignments();
|
||||
|
||||
/**
|
||||
* 新增客户(同步专用,字段集与老系统导入脚本一致)
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+20
-1
@@ -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<BmsCustomer> 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);
|
||||
}
|
||||
}
|
||||
|
||||
+52
-24
@@ -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<String> existingCodes = new HashSet<>(customerSyncMapper.selectExistingCodes());
|
||||
|
||||
// 业务员归属转本系统用户ID:数据权限按 user_id 控制,工号后续修改不影响归属
|
||||
Map<String, String> userIdByLogin = loadUserIdByLogin();
|
||||
// 源工号只用于匹配;归属字段始终保存系统 user_id。
|
||||
Map<String, List<String>> userIdsByLogin = loadUserIdsByLogin();
|
||||
Set<String> validUserIds = new HashSet<>();
|
||||
userIdsByLogin.values().forEach(validUserIds::addAll);
|
||||
Map<String, BmsCustomer> existingSalesAssignments = new HashMap<>();
|
||||
for (BmsCustomer existing : customerSyncMapper.selectExistingSalesAssignments())
|
||||
{
|
||||
existingSalesAssignments.put(existing.getCustomerCode(), existing);
|
||||
}
|
||||
|
||||
List<BmsCustomer> inserts = new ArrayList<>();
|
||||
List<BmsCustomer> 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<String, Object> row, String code, String name,
|
||||
Map<String, String> userIdByLogin, List<String> warnings)
|
||||
Map<String, List<String>> userIdsByLogin, Set<String> validUserIds,
|
||||
BmsCustomer existingAssignment, List<String> 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<String, String> loadUserIdByLogin()
|
||||
/** 加载未删除用户的工号映射;保留重复项以便将歧义匹配按未关联处理。 */
|
||||
private Map<String, List<String>> loadUserIdsByLogin()
|
||||
{
|
||||
Map<String, String> map = new HashMap<>();
|
||||
Map<String, List<String>> 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<String, String> userIdByLogin,
|
||||
private String resolveSalesUserId(String salerCode, Map<String, List<String>> userIdsByLogin,
|
||||
Set<String> validUserIds, BmsCustomer existingAssignment,
|
||||
String code, List<String> 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<String> 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)
|
||||
|
||||
@@ -45,7 +45,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<!-- 信用额度=总额度+额外额度-已用额度-->
|
||||
<sql id="selectBmsCustomerVo">
|
||||
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
|
||||
<!-- sales_user_id 统一存本系统 user_id(同步时由工号转换、手工录入选人即存ID),
|
||||
数据权限按 su.user_id / su.dept_id 控制;保留 user_name 匹配仅作历史未转换工号兜底 -->
|
||||
<!-- sales_user_id 只保存 sys_user.user_id;源工号不参与客户查询和权限匹配。 -->
|
||||
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
|
||||
<!-- 客户套餐绑定:uk_customer_one_set 保证一客户至多一条,不会放大行数 -->
|
||||
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"
|
||||
<if test="customerType != null and customerType != ''"> and c.customer_type = #{customerType}</if>
|
||||
<if test="isEnable != null and isEnable != ''"> and c.is_enable = #{isEnable}</if>
|
||||
<if test="salesUserId != null and salesUserId != ''"> and c.sales_user_id = #{salesUserId}</if>
|
||||
<if test="salesUser != null and salesUser != ''"> and c.sales_user = #{salesUser}</if>
|
||||
<if test="salesUser != null and salesUser != ''"> and su.nick_name = #{salesUser}</if>
|
||||
<if test="ed != null "> and c.ed = #{ed}</if>
|
||||
<if test="zq != null "> and c.zq = #{zq}</if>
|
||||
<if test="settlementUserId != null and settlementUserId != ''"> and c.settlement_user_id = #{settlementUserId}</if>
|
||||
@@ -111,6 +112,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by c.customer_id desc
|
||||
</select>
|
||||
|
||||
<select id="countValidSalesUserId" resultType="int">
|
||||
select count(1)
|
||||
from sys_user
|
||||
where user_id = #{salesUserId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectBmsCustomerByCustomerId" parameterType="Long" resultMap="BmsCustomerResult">
|
||||
<include refid="selectBmsCustomerVo"/>
|
||||
where c.customer_id = #{customerId} and c.del_flag = '0'
|
||||
|
||||
@@ -8,22 +8,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
SELECT customer_code FROM ctr_customer WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 工号->用户ID 映射(列别名显式映射,项目未开下划线转驼峰) -->
|
||||
<!-- 工号->用户ID 候选;Java 层检测重复工号,不任意取一条。 -->
|
||||
<select id="selectUserLoginMap" resultType="com.bms.common.core.domain.entity.SysUser">
|
||||
SELECT user_id AS userId, user_name AS userName
|
||||
FROM sys_user
|
||||
WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 字段集与 scripts/import_customers.py 的 MAP_COLUMNS 保持一致;
|
||||
源行为空的字段不写,走数据库默认值 -->
|
||||
<select id="selectExistingSalesAssignments" resultType="com.bms.customer.customerinfo.domain.BmsCustomer">
|
||||
SELECT customer_code AS customerCode,
|
||||
sales_user_id AS salesUserId,
|
||||
source_sales_code AS sourceSalesCode
|
||||
FROM ctr_customer
|
||||
WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 普通客户资料沿用源空值不覆盖;业务员ID和源工号始终按本次匹配结果写入,允许明确清空。 -->
|
||||
<insert id="insertSyncCustomer" parameterType="com.bms.customer.customerinfo.domain.BmsCustomer">
|
||||
INSERT INTO ctr_customer (
|
||||
customer_code, customer_name, is_enable,
|
||||
del_flag, create_by
|
||||
del_flag, create_by, sales_user_id, source_sales_code
|
||||
<if test="customerCodeDw != null">, customer_code_dw</if>
|
||||
<if test="customerType != null">, customer_type</if>
|
||||
<if test="salesUserId != null">, sales_user_id</if>
|
||||
<if test="salesUser != null">, sales_user</if>
|
||||
<if test="settlementUser != null">, settlement_user</if>
|
||||
<if test="serviceUser != null">, service_user</if>
|
||||
@@ -37,10 +43,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="createTime != null">, create_time</if>
|
||||
) VALUES (
|
||||
#{customerCode}, #{customerName}, #{isEnable},
|
||||
'0', #{createBy}
|
||||
'0', #{createBy}, #{salesUserId}, #{sourceSalesCode}
|
||||
<if test="customerCodeDw != null">, #{customerCodeDw}</if>
|
||||
<if test="customerType != null">, #{customerType}</if>
|
||||
<if test="salesUserId != null">, #{salesUserId}</if>
|
||||
<if test="salesUser != null">, #{salesUser}</if>
|
||||
<if test="settlementUser != null">, #{settlementUser}</if>
|
||||
<if test="serviceUser != null">, #{serviceUser}</if>
|
||||
@@ -61,7 +66,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerName != null">, customer_name = #{customerName}</if>
|
||||
<if test="customerCodeDw != null">, customer_code_dw = #{customerCodeDw}</if>
|
||||
<if test="customerType != null">, customer_type = #{customerType}</if>
|
||||
<if test="salesUserId != null">, sales_user_id = #{salesUserId}</if>
|
||||
, sales_user_id = #{salesUserId}
|
||||
, source_sales_code = #{sourceSalesCode}
|
||||
<if test="salesUser != null">, sales_user = #{salesUser}</if>
|
||||
<if test="settlementUser != null">, settlement_user = #{settlementUser}</if>
|
||||
<if test="serviceUser != null">, service_user = #{serviceUser}</if>
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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;
|
||||
@@ -148,7 +148,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColVisible('salesUserId')" label="业务员" align="center" prop="salesUserId">
|
||||
<template #default="scope">{{ scope.row.salesUserNickName || scope.row.salesUser || scope.row.salesUserId }}</template>
|
||||
<template #default="scope">{{ scope.row.salesUserNickName || '' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column v-if="isColVisible('ed')" label="资金额度" align="center" prop="ed" />-->
|
||||
@@ -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) },
|
||||
|
||||
Reference in New Issue
Block a user