feat(credit): 添加被执行人企业关联匹配功能

- 在 CreditJudgmentDebtorController 中新增 refreshCompanyIdByCompanyName 接口
- 实现根据企业名称自动匹配并更新 companyId 的批量处理逻辑
- 支持按租户维度进行企业名称匹配,避免跨租户误匹配
- 添加企业名称标准化处理和模糊匹配机制
- 实现批量更新和事务管理,提升处理效率
- 优化关键词搜索条件,精确匹配企业名称而非模糊匹配
- 添加公司名称规范化方法 normalizeCompanyName
- 修复竞品表字段别名从 mainCompanyName 改为 companyName

feat(shop): 优化分销商推荐关系绑定机制

- 修改 ShopDealerRefereeController 的 save 方法为幂等绑定
- 新增 bindFirstLevel 方法实现一级推荐关系的幂等绑定
- 添加用户身份验证和安全校验机制
- 增加 source 和 scene 字段支持来源追踪
- 实现重复绑定防护和业务异常处理
- 添加经销商有效性校验机制
This commit is contained in:
2026-01-20 17:02:59 +08:00
parent 1898d3ac9b
commit 7487236ac6
10 changed files with 318 additions and 22 deletions

View File

@@ -6,9 +6,11 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditJudgmentDebtor;
import com.gxwebsoft.credit.param.CreditJudgmentDebtorImportParam;
import com.gxwebsoft.credit.param.CreditJudgmentDebtorParam;
import com.gxwebsoft.credit.service.CreditCompanyService;
import com.gxwebsoft.credit.service.CreditJudgmentDebtorService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -51,6 +53,9 @@ public class CreditJudgmentDebtorController extends BaseController {
@Resource
private BatchImportSupport batchImportSupport;
@Resource
private CreditCompanyService creditCompanyService;
@Operation(summary = "分页查询被执行人")
@GetMapping("/page")
public ApiResult<PageResult<CreditJudgmentDebtor>> page(CreditJudgmentDebtorParam param) {
@@ -143,6 +148,171 @@ public class CreditJudgmentDebtorController extends BaseController {
return fail("删除失败");
}
/**
* 根据企业名称匹配企业并更新 companyId匹配 CreditCompany.name / CreditCompany.matchName
*
* <p>默认仅更新 companyId 为空的记录;如需覆盖更新,传 onlyNull=false。</p>
*/
@PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:update')")
@OperationLog
@Operation(summary = "根据企业名称匹配并更新companyId")
@PostMapping("/company-id/refresh")
public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName(
@RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull,
@RequestParam(value = "limit", required = false) Integer limit
) {
User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
// 1) 读取待处理数据(仅取必要字段,避免一次性拉全表字段)
var debtorQuery = creditJudgmentDebtorService.lambdaQuery()
.select(CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getName, CreditJudgmentDebtor::getCompanyId, CreditJudgmentDebtor::getTenantId)
.eq(currentTenantId != null, CreditJudgmentDebtor::getTenantId, currentTenantId)
.isNotNull(CreditJudgmentDebtor::getName);
if (Boolean.TRUE.equals(onlyNull)) {
debtorQuery.isNull(CreditJudgmentDebtor::getCompanyId);
}
if (limit != null && limit > 0) {
debtorQuery.last("limit " + Math.min(limit, 200000));
}
List<CreditJudgmentDebtor> debtors = debtorQuery.list();
if (CollectionUtils.isEmpty(debtors)) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("updated", 0);
result.put("matched", 0);
result.put("notFound", 0);
result.put("ambiguous", 0);
return success("无可更新数据", result);
}
// 2) 按租户维度匹配(避免管理员/跨租户场景误匹配)
Map<Integer, List<CreditJudgmentDebtor>> debtorsByTenant = new LinkedHashMap<>();
for (CreditJudgmentDebtor d : debtors) {
if (d == null) {
continue;
}
Integer tenantId = currentTenantId != null ? currentTenantId : d.getTenantId();
if (tenantId == null) {
// 未知租户下不做跨租户匹配,避免误更新
continue;
}
debtorsByTenant.computeIfAbsent(tenantId, k -> new ArrayList<>()).add(d);
}
// 3) 批量更新 companyId
int updated = 0;
int matched = 0;
int notFound = 0;
int ambiguous = 0;
final int batchSize = 500;
List<CreditJudgmentDebtor> updates = new ArrayList<>(batchSize);
final int inChunkSize = 900;
for (Map.Entry<Integer, List<CreditJudgmentDebtor>> entry : debtorsByTenant.entrySet()) {
Integer tenantId = entry.getKey();
List<CreditJudgmentDebtor> tenantDebtors = entry.getValue();
if (tenantId == null || CollectionUtils.isEmpty(tenantDebtors)) {
continue;
}
// 3.1) 查询当前租户下的 companyId 映射
LinkedHashMap<String, Integer> companyIdByName = new LinkedHashMap<>();
LinkedHashMap<String, Integer> ambiguousByName = new LinkedHashMap<>();
java.util.LinkedHashSet<String> nameSet = new java.util.LinkedHashSet<>();
for (CreditJudgmentDebtor d : tenantDebtors) {
String name = normalizeCompanyName(d != null ? d.getName() : null);
if (name != null) {
nameSet.add(name);
}
}
List<String> allNames = new ArrayList<>(nameSet);
for (int i = 0; i < allNames.size(); i += inChunkSize) {
List<String> chunk = allNames.subList(i, Math.min(allNames.size(), i + inChunkSize));
if (CollectionUtils.isEmpty(chunk)) {
continue;
}
List<CreditCompany> companies = creditCompanyService.lambdaQuery()
.select(CreditCompany::getId, CreditCompany::getName, CreditCompany::getMatchName, CreditCompany::getTenantId)
.eq(CreditCompany::getTenantId, tenantId)
.and(w -> w.in(CreditCompany::getName, chunk).or().in(CreditCompany::getMatchName, chunk))
.list();
for (CreditCompany c : companies) {
if (c == null || c.getId() == null) {
continue;
}
addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getName()), c.getId());
addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getMatchName()), c.getId());
}
}
// 3.2) 更新当前租户下的被执行人 companyId
for (CreditJudgmentDebtor d : tenantDebtors) {
String key = normalizeCompanyName(d != null ? d.getName() : null);
if (key == null) {
continue;
}
Integer amb = ambiguousByName.get(key);
if (amb != null && amb > 0) {
ambiguous++;
continue;
}
Integer companyId = companyIdByName.get(key);
if (companyId == null) {
notFound++;
continue;
}
matched++;
boolean needUpdate = d.getCompanyId() == null || !companyId.equals(d.getCompanyId());
if (Boolean.TRUE.equals(onlyNull)) {
needUpdate = d.getCompanyId() == null;
}
if (!needUpdate) {
continue;
}
CreditJudgmentDebtor patch = new CreditJudgmentDebtor();
patch.setId(d.getId());
patch.setCompanyId(companyId);
updates.add(patch);
if (updates.size() >= batchSize) {
updated += batchImportSupport.runInNewTx(() -> {
boolean ok = creditJudgmentDebtorService.updateBatchById(updates, batchSize);
return ok ? updates.size() : 0;
});
updates.clear();
}
}
}
// currentTenantId 为空时,租户缺失的数据不做匹配更新,避免误更新
if (currentTenantId == null) {
for (CreditJudgmentDebtor d : debtors) {
if (d != null && d.getTenantId() == null) {
notFound++;
}
}
}
if (!updates.isEmpty()) {
updated += batchImportSupport.runInNewTx(() -> {
boolean ok = creditJudgmentDebtorService.updateBatchById(updates, batchSize);
return ok ? updates.size() : 0;
});
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("updated", updated);
result.put("matched", matched);
result.put("notFound", notFound);
result.put("ambiguous", ambiguous);
return success("更新完成,更新" + updated + "", result);
}
/**
* 批量导入被执行人
*/
@@ -244,10 +414,32 @@ public class CreditJudgmentDebtorController extends BaseController {
if (isImportHeaderRow(param)) {
return true;
}
return ImportHelper.isBlank(param.getCaseNumber())
&& ImportHelper.isBlank(param.getName())
&& ImportHelper.isBlank(param.getName1())
&& ImportHelper.isBlank(param.getCode());
return ImportHelper.isBlank(param.getCaseNumber());
}
private static String normalizeCompanyName(String name) {
if (name == null) {
return null;
}
String v = name.replace(' ', ' ').trim();
return v.isEmpty() ? null : v;
}
private static void addCompanyNameMapping(Map<String, Integer> idByName,
Map<String, Integer> ambiguousByName,
String key,
Integer companyId) {
if (key == null || companyId == null) {
return;
}
Integer existing = idByName.get(key);
if (existing == null) {
idByName.put(key, companyId);
return;
}
if (!existing.equals(companyId)) {
ambiguousByName.put(key, 1);
}
}
private boolean isImportHeaderRow(CreditJudgmentDebtorImportParam param) {

View File

@@ -146,8 +146,10 @@ public class CreditJudicialDocumentController extends BaseController {
int successCount = 0;
try {
// 支持按选项卡名称导入默认读取“裁判文书”sheet不存在则回退到第 0 个sheet
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "裁判文书", 0);
ExcelImportSupport.ImportResult<CreditJudicialDocumentImportParam> importResult = ExcelImportSupport.read(
file, CreditJudicialDocumentImportParam.class, this::isEmptyImportRow);
file, CreditJudicialDocumentImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditJudicialDocumentImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
@@ -544,8 +546,7 @@ public class CreditJudicialDocumentController extends BaseController {
if (param == null) {
return true;
}
return ImportHelper.isBlank(param.getCaseNumber())
&& ImportHelper.isBlank(param.getCauseOfAction());
return ImportHelper.isBlank(param.getCaseNumber());
}
private CreditJudicialDocument convertImportParamToEntity(CreditJudicialDocumentImportParam param) {

View File

@@ -228,10 +228,10 @@ public class CreditJudiciaryController extends BaseController {
}
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
// 验证必填字段
if (item.getName() == null || item.getName().trim().isEmpty()) {
errorMessages.add("" + excelRowNumber + "行:项目名称不能为空");
continue;
}
// if (item.getName() == null || item.getName().trim().isEmpty()) {
// errorMessages.add("第" + excelRowNumber + "行:项目名称不能为空");
// continue;
// }
if (item.getCode() == null || item.getCode().trim().isEmpty()) {
errorMessages.add("" + excelRowNumber + "行:唯一标识不能为空");
continue;