feat: 完成CRM-ERP客户同步管理模块
主要功能:
- 新增同步管理后台界面,支持全局管理(不限租户)
- 实现租户-ERP公司映射配置,支持多公司绑定
- 实现定时同步任务,可配置Cron表达式
- 实现手动同步功能,从ERP拉取客户数据更新CRM
- 支持新增经销商(ERP客户在CRM不存在时)
- 新增同步日志和预警管理
技术实现:
- 新增 crm_sync_log、crm_sync_alert、sys_tenant_company 表
- 使用 TenantHelper.ignore() 实现全局查询(忽略租户过滤)
- ERP动态API支持 ${param} 字符串替换用于IN条件
- 新增公司列表动态API配置
同步流程:
1. 获取租户-公司映射配置
2. 从ERP获取各公司客户列表
3. 匹配CRM已有经销商并更新
4. 未匹配且ERP未停用的客户新增为经销商
5. 生成差异预警(CRM维护字段)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,4 +79,17 @@ public class CustomerController extends BaseController {
|
||||
public R<List<CustomerVO>> brands() {
|
||||
return R.ok(customerService.getBrands());
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户选择列表(用于CRM选择器)
|
||||
* 支持按公司过滤(companyIds逗号分隔)
|
||||
*/
|
||||
@GetMapping("/select")
|
||||
public TableDataInfo<CustomerVO> select(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "20") int pageSize,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String companyIds) {
|
||||
return customerService.queryCustomerSelectList(pageNum, pageSize, keyword, companyIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,4 +143,60 @@ public interface CustomerMapper extends BaseMapper<CustomerGeneral> {
|
||||
"WHERE BRAND IS NOT NULL AND BRANDNAME IS NOT NULL " +
|
||||
"ORDER BY BRAND")
|
||||
List<CustomerVO> selectBrands();
|
||||
|
||||
/**
|
||||
* 客户选择列表(用于CRM选择器,支持多公司过滤)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT TOP ${pageSize} * FROM (" +
|
||||
" SELECT ROW_NUMBER() OVER (ORDER BY CLTCODE) AS rn, " +
|
||||
" CLTCODE AS customerCode, " +
|
||||
" CLTNAME AS customerName, " +
|
||||
" COMPANY_ID AS companyCode, " +
|
||||
" COMPANY_NAME AS companyName, " +
|
||||
" LINKMAN AS contactName, " +
|
||||
" TEL1 AS phone, " +
|
||||
" province, city, ISSTOP AS isStop " +
|
||||
" FROM SCLTGENERAL " +
|
||||
" WHERE 1=1 " +
|
||||
" <if test='keyword != null and keyword != \"\"'>" +
|
||||
" AND (CLTCODE LIKE '%' + #{keyword} + '%' " +
|
||||
" OR CLTNAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR LINKMAN LIKE '%' + #{keyword} + '%') " +
|
||||
" </if>" +
|
||||
" <if test='companyIds != null and companyIds != \"\"'>" +
|
||||
" AND COMPANY_ID IN " +
|
||||
" <foreach collection='companyIdList' item='id' open='(' separator=',' close=')'>" +
|
||||
" #{id}" +
|
||||
" </foreach>" +
|
||||
" </if>" +
|
||||
") t WHERE rn > (${pageNum} - 1) * ${pageSize} ORDER BY rn" +
|
||||
"</script>")
|
||||
List<CustomerVO> selectCustomerSelectPage(@Param("pageNum") int pageNum,
|
||||
@Param("pageSize") int pageSize,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("companyIds") String companyIds,
|
||||
@Param("companyIdList") List<String> companyIdList);
|
||||
|
||||
/**
|
||||
* 客户选择总数(用于CRM选择器,支持多公司过滤)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT COUNT(*) FROM SCLTGENERAL " +
|
||||
"WHERE 1=1 " +
|
||||
"<if test='keyword != null and keyword != \"\"'>" +
|
||||
" AND (CLTCODE LIKE '%' + #{keyword} + '%' " +
|
||||
" OR CLTNAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR LINKMAN LIKE '%' + #{keyword} + '%') " +
|
||||
"</if>" +
|
||||
"<if test='companyIds != null and companyIds != \"\"'>" +
|
||||
" AND COMPANY_ID IN " +
|
||||
" <foreach collection='companyIdList' item='id' open='(' separator=',' close=')'>" +
|
||||
" #{id}" +
|
||||
" </foreach>" +
|
||||
"</if>" +
|
||||
"</script>")
|
||||
long selectCustomerSelectCount(@Param("keyword") String keyword,
|
||||
@Param("companyIds") String companyIds,
|
||||
@Param("companyIdList") List<String> companyIdList);
|
||||
}
|
||||
|
||||
@@ -30,4 +30,9 @@ public interface ICustomerService {
|
||||
* 获取所有品牌列表
|
||||
*/
|
||||
List<CustomerVO> getBrands();
|
||||
|
||||
/**
|
||||
* 客户选择列表(用于CRM选择器,支持多公司过滤)
|
||||
*/
|
||||
TableDataInfo<CustomerVO> queryCustomerSelectList(int pageNum, int pageSize, String keyword, String companyIds);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.hzhub.erp.mapper.SalesOrganizationMapper;
|
||||
import org.hzhub.erp.service.ICustomerService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -43,4 +45,17 @@ public class CustomerServiceImpl implements ICustomerService {
|
||||
public List<CustomerVO> getBrands() {
|
||||
return customerMapper.selectBrands();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<CustomerVO> queryCustomerSelectList(int pageNum, int pageSize, String keyword, String companyIds) {
|
||||
// 解析companyIds为列表
|
||||
List<String> companyIdList = Collections.emptyList();
|
||||
if (companyIds != null && !companyIds.isBlank()) {
|
||||
companyIdList = Arrays.asList(companyIds.split(","));
|
||||
}
|
||||
|
||||
long total = customerMapper.selectCustomerSelectCount(keyword, companyIds, companyIdList);
|
||||
List<CustomerVO> list = customerMapper.selectCustomerSelectPage(pageNum, pageSize, keyword, companyIds, companyIdList);
|
||||
return new TableDataInfo<>(list, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,16 +127,47 @@ public class DynamicApiExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SQL模板(将 #{param} 转换为 ? 占位符,并提取参数值)
|
||||
* 处理SQL模板(将 #{param} 转换为 ? 占位符,${param} 直接替换为参数值)
|
||||
*
|
||||
* @param sqlTemplate SQL模板
|
||||
* @param params 参数Map
|
||||
* @return 处理后的SQL和参数值列表
|
||||
*/
|
||||
private ProcessedSql processSqlTemplate(String sqlTemplate, Map<String, Object> params) {
|
||||
// 正则表达式匹配 #{paramName}
|
||||
log.info("处理SQL模板: params={}", params);
|
||||
|
||||
// 1. 先处理 ${param} 字符串替换(用于IN条件等)
|
||||
Pattern stringPattern = Pattern.compile("\\$\\{(\\w+)\\}");
|
||||
Matcher stringMatcher = stringPattern.matcher(sqlTemplate);
|
||||
|
||||
StringBuffer intermediateSql = new StringBuffer();
|
||||
while (stringMatcher.find()) {
|
||||
String paramName = stringMatcher.group(1);
|
||||
Object paramValue = params.get(paramName);
|
||||
|
||||
log.info("处理${}参数: paramName={}, paramValue={}", paramName, paramValue);
|
||||
|
||||
if (paramValue != null) {
|
||||
// 直接替换参数值(字符串拼接)
|
||||
// 安全检查:只允许包含数字、字母、逗号、单引号的值
|
||||
String valueStr = paramValue.toString();
|
||||
if (!isValidStringValue(valueStr)) {
|
||||
throw new SecurityException("参数值包含非法字符: " + paramName);
|
||||
}
|
||||
stringMatcher.appendReplacement(intermediateSql, valueStr);
|
||||
} else {
|
||||
// 参数为空时,替换为空字符串(可能导致SQL语法问题,需注意)
|
||||
log.warn("参数{}为空,替换为空字符串", paramName);
|
||||
stringMatcher.appendReplacement(intermediateSql, "");
|
||||
}
|
||||
}
|
||||
stringMatcher.appendTail(intermediateSql);
|
||||
|
||||
log.info("字符串替换后SQL: {}", intermediateSql);
|
||||
|
||||
// 2. 再处理 #{param} 预编译参数
|
||||
Pattern pattern = Pattern.compile("#\\{(\\w+)\\}");
|
||||
Matcher matcher = pattern.matcher(sqlTemplate);
|
||||
Matcher matcher = pattern.matcher(intermediateSql.toString());
|
||||
|
||||
List<Object> paramValues = new ArrayList<>();
|
||||
StringBuffer processedSql = new StringBuffer();
|
||||
@@ -160,6 +191,14 @@ public class DynamicApiExecutor {
|
||||
return new ProcessedSql(finalSql, paramValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字符串值是否安全(只允许数字、字母、逗号、单引号)
|
||||
*/
|
||||
private boolean isValidStringValue(String value) {
|
||||
// 只允许:数字、字母、逗号、单引号、空格
|
||||
return value.matches("^[0-9a-zA-Z,\\'\\s]+$");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WHERE 条件中的动态逻辑(IS NOT NULL THEN ...)
|
||||
* 例如:WHERE #{customerCode} IS NOT NULL THEN customer_code = #{customerCode}
|
||||
|
||||
Reference in New Issue
Block a user