From 3741ef5fe42dad2e06941364c4ea47ad433380ae Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 31 Mar 2026 16:12:45 +0800 Subject: [PATCH 01/79] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E6=A8=A1=E5=9E=8B=E6=89=93=E6=A0=87=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-03-31-abnormal-account-bank-tag-design.md | 382 ++++++++++++++++++ ...abnormal-account-bank-tag-design-record.md | 38 ++ 2 files changed, 420 insertions(+) create mode 100644 docs/design/2026-03-31-abnormal-account-bank-tag-design.md create mode 100644 docs/reports/implementation/2026-03-31-abnormal-account-bank-tag-design-record.md diff --git a/docs/design/2026-03-31-abnormal-account-bank-tag-design.md b/docs/design/2026-03-31-abnormal-account-bank-tag-design.md new file mode 100644 index 00000000..07b84cc9 --- /dev/null +++ b/docs/design/2026-03-31-abnormal-account-bank-tag-design.md @@ -0,0 +1,382 @@ +# 异常账户模型接入银行流水打标设计文档 + +**模块**: 银行流水打标 +**日期**: 2026-03-31 + +## 一、背景 + +当前银行流水打标主链路已经具备以下基础能力: + +- 规则元数据管理与启用控制 +- `CcdiBankTagServiceImpl` 统一执行入口 +- `CcdiBankTagAnalysisMapper.xml` 承载真实规则 SQL +- `ccdi_bank_statement_tag_result` 统一承载 `STATEMENT / OBJECT` 命中结果 +- 项目风险总览按对象型结果聚合员工风险情况 + +根据 [异常账户.xlsx](/Users/wkc/Desktop/ccdi/ccdi/assets/异常账户.xlsx) 与 [员工账户.xlsx](/Users/wkc/Desktop/ccdi/ccdi/assets/员工账户.xlsx),本次需要新增独立模型“异常账户”,并正式接入以下两条规则: + +- `SUDDEN_ACCOUNT_CLOSURE`:突然销户 +- `DORMANT_ACCOUNT_LARGE_ACTIVATION`:休眠账户大额启用 + +这两条规则均依赖新增账户信息表 `ccdi_account_info`,且风险筛查对象明确为“员工本人”。本次目标是在不改造现有打标架构的前提下,将两条规则纳入现有项目打标主链路,并补充能够稳定命中的测试数据与验证手段。 + +## 二、目标 + +本次设计目标如下: + +1. 新增账户信息表 `ccdi_account_info`,支撑异常账户规则计算。 +2. 新增独立模型 `ABNORMAL_ACCOUNT`,并接入 2 条对象型规则。 +3. 将两条规则接入现有 `executeObjectRule(...)` 打标链路,不新增平行处理模块。 +4. 补充最小可命中的测试数据 SQL,并覆盖正样本与反样本。 +5. 保留 Java 自动化测试,同时在验证阶段使用 MySQL MCP 执行真实 SQL,确认命中结果符合业务口径。 +6. 在设计确认后,分别产出后端与前端实施计划文档。 + +## 三、范围 + +### 3.1 本次范围 + +- 新增 `ccdi_account_info` 建表 SQL +- 新增模型 `ABNORMAL_ACCOUNT` +- 新增规则元数据 `SUDDEN_ACCOUNT_CLOSURE`、`DORMANT_ACCOUNT_LARGE_ACTIVATION` +- `CcdiBankTagServiceImpl` 新增对象型规则分发 +- `CcdiBankTagAnalysisMapper.java/.xml` 新增 2 条对象型查询 +- 新增测试数据 SQL +- 新增 Java 自动化测试 +- 新增基于 MySQL MCP 的真实 SQL 验证步骤 +- 新增设计文档、后端实施计划、前端实施计划 + +### 3.2 不在本次范围 + +- 不开发“异常账户人员信息”独立查询、分页、详情、导出真实数据链路 +- 不改前端页面展示逻辑 +- 不扩展到关系人或外部账户 +- 不新增动态规则引擎、DSL 或兼容性补丁方案 +- 不改造 `lsfx-mock-server` +- 不将固定阈值改造成项目可配置参数 + +## 四、现状分析 + +### 4.1 当前主链路 + +当前项目级银行流水打标流程为: + +1. `CcdiBankTagServiceImpl.rebuildProject(...)` 加载启用规则。 +2. 规则按 `rule_code` 分发到 `executeStatementRule(...)` 或 `executeObjectRule(...)`。 +3. `CcdiBankTagAnalysisMapper.xml` 执行真实 SQL,返回流水型或对象型命中结果。 +4. Service 将命中结果组装为 `CcdiBankTagResult` 并写入 `ccdi_bank_statement_tag_result`。 +5. 项目结果总览再按对象维度聚合风险人数和命中规则快照。 + +### 4.2 当前缺口 + +当前仓库中“异常账户人员信息”仍为占位展示,且主打标规则中尚无“异常账户”模型与对应规则编码。也就是说,本次缺口主要是: + +- 缺少账户信息基础表 +- 缺少异常账户模型与规则元数据 +- 缺少两条规则的对象型 SQL +- 缺少最小可命中的测试样本与真实 SQL 验证 + +## 五、方案对比 + +### 5.1 方案一:最小闭环接入现有对象型打标链路 + +做法: + +- 新增独立模型 `ABNORMAL_ACCOUNT` +- 两条规则均按 `OBJECT` 结果类型落到员工维度 +- 通过 `CcdiBankTagAnalysisMapper.xml` 计算命中结果 +- 结果继续写入 `ccdi_bank_statement_tag_result` + +优点: + +- 改动最小 +- 完全复用现有打标主链路 +- 能直接进入现有员工风险总览聚合 + +缺点: + +- 本轮不打通“异常账户人员信息”独立详情链路 + +### 5.2 方案二:在方案一基础上同时打通异常账户独立结果链路 + +优点: + +- 风险详情中的“异常账户人员信息”可展示真实数据 + +缺点: + +- 改动范围明显扩大 +- 超出本次需求 +- 不符合最短路径实现要求 + +### 5.3 方案三:仅补 SQL 验证,不接入主系统打标链路 + +优点: + +- 开发最省 + +缺点: + +- 无法满足“正式接入主系统打标链路”的需求 + +### 5.4 结论 + +采用方案一: + +- 新增独立模型 `ABNORMAL_ACCOUNT` +- 两条规则均按对象型规则接入现有打标链路 +- 结果沉淀到现有结果表 +- 后续如需开发异常账户独立查询能力,再以此为基础扩展 + +## 六、总体设计 + +### 6.1 模型与规则设计 + +本次新增如下模型与规则: + +- 模型编码:`ABNORMAL_ACCOUNT` +- 模型名称:`异常账户` +- 规则一:`SUDDEN_ACCOUNT_CLOSURE` / `突然销户` +- 规则二:`DORMANT_ACCOUNT_LARGE_ACTIVATION` / `休眠账户大额启用` + +两条规则统一定义为: + +- `result_type = OBJECT` +- `object_type = STAFF_ID_CARD` +- `object_key = 员工身份证号` + +### 6.2 结果落库 + +两条规则命中后继续写入现有结果表 `ccdi_bank_statement_tag_result`,不新增单独结果表。 + +结果字段约束如下: + +- `model_code = ABNORMAL_ACCOUNT` +- `rule_code` 使用全大写风格 +- `result_type = OBJECT` +- `bank_statement_id = null` +- `object_type = STAFF_ID_CARD` +- `object_key = 员工身份证号` +- `reason_detail` 存储账户号、异常日期与统计快照 + +### 6.3 数据流 + +数据流保持为: + +1. 项目级打标入口加载启用规则。 +2. 当规则编码为 `SUDDEN_ACCOUNT_CLOSURE` 或 `DORMANT_ACCOUNT_LARGE_ACTIVATION` 时,进入 `executeObjectRule(...)`。 +3. Mapper SQL 在项目范围内将 `ccdi_bank_statement` 与 `ccdi_account_info`、`ccdi_base_staff` 关联。 +4. SQL 返回员工身份证号维度的对象型命中结果。 +5. Service 将命中结果写入 `ccdi_bank_statement_tag_result`。 +6. 员工风险聚合继续从该结果表汇总,无需新建平行链路。 + +## 七、表结构设计 + +### 7.1 新增表 `ccdi_account_info` + +以 [员工账户.xlsx](/Users/wkc/Desktop/ccdi/ccdi/assets/员工账户.xlsx) 为准,新增表 `ccdi_account_info`,核心字段如下: + +- `account_id` +- `account_no` +- `account_type` +- `account_name` +- `owner_type` +- `owner_id` +- `bank` +- `bank_code` +- `currency` +- `is_self_account` +- `monthly_avg_trans_count` +- `monthly_avg_trans_amount` +- `trans_freq_type` +- `dr_max_single_amount` +- `cr_max_single_amount` +- `dr_max_daily_amount` +- `cr_max_daily_amount` +- `trans_risk_level` +- `status` +- `effective_date` +- `invalid_date` +- `created_by` +- `updated_by` +- `create_time` +- `update_time` + +### 7.2 关联约束 + +本次规则只识别员工本人账户,关联口径固定为: + +- `ccdi_account_info.owner_type = 'EMPLOYEE'` +- `ccdi_account_info.owner_id = ccdi_base_staff.id_card` +- `ccdi_account_info.account_no = ccdi_bank_statement.LE_ACCOUNT_NO` + +说明: + +- 仓库中当前未见单独的账号加解密或标准化链路,因此本次设计要求建表脚本、测试数据与流水数据直接使用一致账号值 +- 本次不将关系人账户纳入规则范围 + +## 八、规则 SQL 口径 + +### 8.1 `SUDDEN_ACCOUNT_CLOSURE` + +业务口径: + +- 员工本人账户已销户 +- 销户日前 30 天内仍存在交易记录 + +SQL 设计约束: + +- 仅统计项目内流水 +- 统计窗口限定为 `[invalid_date - 30天, invalid_date)` +- 按“员工身份证号 + 账号”粒度聚合,再映射回员工对象 + +命中条件: + +- `status = 2` +- `invalid_date is not null` +- 窗口内存在至少 1 笔交易 + +返回结果: + +- `objectType = STAFF_ID_CARD` +- `objectKey = 员工身份证号` + +`reasonDetail` 结构: + +- `账户{account_no}于{invalid_date}销户,销户前30天内最后交易日{last_tx_date},累计交易金额{window_total_amount}元,单笔最大金额{window_max_single_amount}元` + +### 8.2 `DORMANT_ACCOUNT_LARGE_ACTIVATION` + +业务口径: + +- 员工本人账户状态正常 +- 开户后长期未使用 +- 首次启用后出现大额资金流动 + +SQL 设计约束: + +- 仅统计项目内流水 +- 以该账户在项目内的首次流水日期作为“启用时间” +- “沉睡时长”按开户日期到首次交易日期计算 + +命中条件: + +- `status = 1` +- `effective_date is not null` +- `first_tx_date >= effective_date + 6个月` +- 且满足以下任一: + - 启用后累计交易总额 `>= 500000` + - 启用后单笔最大交易金额 `>= 100000` + +返回结果: + +- `objectType = STAFF_ID_CARD` +- `objectKey = 员工身份证号` + +`reasonDetail` 结构: + +- `账户{account_no}开户于{effective_date},首次交易日期{first_tx_date},沉睡时长{dormant_months}个月,启用后累计交易金额{total_amount}元,单笔最大金额{max_single_amount}元` + +### 8.3 公共规则约束 + +- 仅识别员工本人账户,不识别关系人和外部账户 +- 仅按项目内流水计算,不跨项目拼接历史流水 +- 累计金额使用 `amount_dr + amount_cr` +- 单笔最大金额使用 `greatest(amount_dr, amount_cr)` +- 同一员工多个账户分别判断,允许同一规则写入多条结果,避免强行合并后丢失账户级快照 + +## 九、测试数据设计 + +### 9.1 测试数据组织原则 + +新增一份独立增量 SQL,放在 `sql/migration/`,仅构造本次规则所需最小样本。 + +### 9.2 样本设计 + +建议最少包含以下样本: + +- 员工 A:命中 `SUDDEN_ACCOUNT_CLOSURE` + - 账户已销户 + - 销户前 30 天内有 2 到 3 笔项目流水 +- 员工 B:命中 `DORMANT_ACCOUNT_LARGE_ACTIVATION` + - 开户日期早于首次交易至少 6 个月 + - 启用后累计金额超过 50 万 +- 员工 C:休眠不足 6 个月,不命中 +- 员工 D:已销户,但销户前 30 天无流水,不命中 + +### 9.3 数据一致性要求 + +- `ccdi_account_info.account_no` 与 `ccdi_bank_statement.LE_ACCOUNT_NO` 必须一致 +- `owner_id` 与员工身份证号一致 +- 正样本与反样本必须处于同一项目验证口径下,避免跨项目误差 + +## 十、测试与验证设计 + +### 10.1 Java 自动化测试 + +保留两层自动化测试: + +1. Service 分发测试 + - 新规则能进入 `executeObjectRule(...)` +2. Mapper / SQL 结构测试 + - 新 Mapper 方法存在 + - XML 中存在对应 ` + + + + select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '账户', t.accountNo, + '开户于', DATE_FORMAT(t.effectiveDate, '%Y-%m-%d'), + ',首次交易日期', DATE_FORMAT(t.firstTxDate, '%Y-%m-%d'), + ',沉睡时长', CAST(t.dormantMonths AS CHAR), + '个月,启用后累计交易金额', CAST(t.windowTotalAmount AS CHAR), + '元,单笔最大金额', CAST(t.windowMaxSingleAmount AS CHAR), + '元' + ) AS reasonDetail + from ( + select + staff.id_card AS objectKey, + ai.account_no AS accountNo, + ai.effective_date AS effectiveDate, + min(tx.txDate) AS firstTxDate, + timestampdiff(MONTH, ai.effective_date, min(tx.txDate)) AS dormantMonths, + round(sum(tx.tradeTotalAmount), 2) AS windowTotalAmount, + round(max(tx.tradeMaxSingleAmount), 2) AS windowMaxSingleAmount + from ccdi_account_info ai + inner join ccdi_base_staff staff + on staff.id_card = ai.owner_id + inner join ( + select + trim(bs.LE_ACCOUNT_NO) AS accountNo, + COALESCE( + STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'), + STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') + ) AS txDate, + IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0) AS tradeTotalAmount, + GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS tradeMaxSingleAmount + from ccdi_bank_statement bs + where bs.project_id = #{projectId} + and trim(IFNULL(bs.LE_ACCOUNT_NO, '')) != '' + ) tx + on tx.accountNo = trim(ai.account_no) + where ai.owner_type = 'EMPLOYEE' + and ai.status = 1 + and ai.effective_date is not null + group by staff.id_card, ai.account_no, ai.effective_date + having min(tx.txDate) >= DATE_ADD(ai.effective_date, INTERVAL 6 MONTH) + ) t + where t.windowTotalAmount >= 500000 + or t.windowMaxSingleAmount >= 100000 + + select + account.account_no as accountNo, + account.account_no as account_no, + coalesce(nullif(account.account_name, ''), staff.name) as accountName, + account.bank as bankName, + tr.rule_name as abnormalType, + tr.rule_code as rule_code, + case + when tr.rule_code = 'SUDDEN_ACCOUNT_CLOSURE' then date_format(account.invalid_date, '%Y-%m-%d') + when tr.rule_code = 'DORMANT_ACCOUNT_LARGE_ACTIVATION' then substring( + substring_index( + substring_index(tr.reason_detail, ',', 2), + '首次交易日期', + -1 + ), + 1, + 10 + ) + else null + end as abnormal_time, + case + when account.status = 1 then '正常' + when account.status = 2 then '已销户' + else cast(account.status as char) + end as status + from ccdi_bank_statement_tag_result tr + inner join ccdi_account_info account + on account.owner_type = 'EMPLOYEE' + and account.owner_id = tr.object_key + and instr(tr.reason_detail, account.account_no) > 0 + left join ccdi_base_staff staff + on staff.id_card = tr.object_key + + + + + + Date: Tue, 31 Mar 2026 21:02:00 +0800 Subject: [PATCH 22/79] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E4=BA=BA=E5=91=98=E6=9C=8D=E5=8A=A1=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ICcdiProjectOverviewService.java | 11 ++ .../impl/CcdiProjectOverviewServiceImpl.java | 48 ++++++ ...ectOverviewServiceAbnormalAccountTest.java | 148 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java index 11c0aef4..854906b4 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; @@ -158,6 +159,16 @@ public interface ICcdiProjectOverviewService { return new CcdiProjectAbnormalAccountPageVO(); } + /** + * 导出异常账户人员信息 + * + * @param projectId 项目ID + * @return 导出列表 + */ + default List exportAbnormalAccountPeople(Long projectId) { + return List.of(); + } + /** * 重算结果总览员工结果并同步项目风险人数 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index 9e9e10b4..26d9ae03 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -2,15 +2,19 @@ package com.ruoyi.ccdi.project.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO; @@ -258,6 +262,31 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return result; } + @Override + public CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) { + ensureProjectExists(queryDTO.getProjectId()); + + Page page = new Page<>( + defaultAbnormalAccountPageNum(queryDTO.getPageNum()), + defaultAbnormalAccountPageSize(queryDTO.getPageSize()) + ); + Page resultPage = overviewMapper.selectAbnormalAccountPage(page, queryDTO); + + CcdiProjectAbnormalAccountPageVO result = new CcdiProjectAbnormalAccountPageVO(); + result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords())); + result.setTotal(resultPage == null ? 0L : resultPage.getTotal()); + return result; + } + + @Override + public List exportAbnormalAccountPeople(Long projectId) { + ensureProjectExists(projectId); + + return defaultList(overviewMapper.selectAbnormalAccountList(projectId)).stream() + .map(this::buildAbnormalAccountExcelRow) + .toList(); + } + @Override public void exportRiskDetails(HttpServletResponse response, Long projectId) { CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO(); @@ -420,6 +449,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue(); } + private long defaultAbnormalAccountPageNum(Integer pageNum) { + return pageNum == null || pageNum <= 0 ? 1L : pageNum.longValue(); + } + + private long defaultAbnormalAccountPageSize(Integer pageSize) { + return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue(); + } + private long defaultPageNum(Integer pageNum) { return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue(); } @@ -462,6 +499,17 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return row; } + private CcdiProjectAbnormalAccountExcel buildAbnormalAccountExcelRow(CcdiProjectAbnormalAccountItemVO item) { + CcdiProjectAbnormalAccountExcel row = new CcdiProjectAbnormalAccountExcel(); + row.setAccountNo(item.getAccountNo()); + row.setAccountName(item.getAccountName()); + row.setBankName(item.getBankName()); + row.setAbnormalType(item.getAbnormalType()); + row.setAbnormalTime(item.getAbnormalTime()); + row.setStatus(item.getStatus()); + return row; + } + private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) { if (relatedStaffName == null || relatedStaffName.isBlank()) { return null; diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java new file mode 100644 index 00000000..b06fc1c2 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java @@ -0,0 +1,148 @@ +package com.ruoyi.ccdi.project.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO; +import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; +import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; +import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; +import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper; +import com.ruoyi.common.exception.ServiceException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiProjectOverviewServiceAbnormalAccountTest { + + @InjectMocks + private CcdiProjectOverviewServiceImpl service; + + @Mock + private CcdiProjectOverviewMapper overviewMapper; + + @Mock + private CcdiProjectMapper projectMapper; + + @Mock + private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper; + + @Mock + private CcdiBankTagResultMapper bankTagResultMapper; + + @Mock + private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder; + + @Mock + private CcdiProjectRiskDetailWorkbookExporter workbookExporter; + + @Test + void shouldMapAbnormalAccountPageRowsAndTotal() { + mockProjectExists(40L); + + CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); + queryDTO.setProjectId(40L); + queryDTO.setPageNum(1); + queryDTO.setPageSize(5); + + CcdiProjectAbnormalAccountItemVO item = new CcdiProjectAbnormalAccountItemVO(); + item.setAccountNo("6222000000000001"); + item.setAccountName("李四"); + item.setBankName("中国农业银行"); + item.setAbnormalType("突然销户"); + item.setAbnormalTime("2026-03-20"); + item.setStatus("已销户"); + + Page resultPage = new Page<>(1, 5); + resultPage.setRecords(List.of(item)); + resultPage.setTotal(1L); + when(overviewMapper.selectAbnormalAccountPage(any(Page.class), any(CcdiProjectAbnormalAccountQueryDTO.class))) + .thenReturn(resultPage); + + CcdiProjectAbnormalAccountPageVO result = service.getAbnormalAccountPeople(queryDTO); + + assertEquals(1, result.getRows().size()); + assertEquals(1L, result.getTotal()); + assertEquals("6222000000000001", result.getRows().getFirst().getAccountNo()); + assertEquals("突然销户", result.getRows().getFirst().getAbnormalType()); + verify(overviewMapper).selectAbnormalAccountPage( + argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L), + argThat(query -> query.getProjectId().equals(40L)) + ); + } + + @Test + void shouldDefaultAbnormalAccountPageNumAndPageSizeToOneAndFive() { + mockProjectExists(40L); + + Page emptyPage = new Page<>(1, 5); + emptyPage.setRecords(List.of()); + emptyPage.setTotal(0L); + when(overviewMapper.selectAbnormalAccountPage(any(Page.class), any(CcdiProjectAbnormalAccountQueryDTO.class))) + .thenReturn(emptyPage); + + CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); + queryDTO.setProjectId(40L); + service.getAbnormalAccountPeople(queryDTO); + + verify(overviewMapper).selectAbnormalAccountPage( + argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L), + any(CcdiProjectAbnormalAccountQueryDTO.class) + ); + } + + @Test + void shouldExportAbnormalAccountPeopleRows() { + mockProjectExists(40L); + + CcdiProjectAbnormalAccountItemVO item = new CcdiProjectAbnormalAccountItemVO(); + item.setAccountNo("6222000000000002"); + item.setAccountName("王五"); + item.setBankName("中国银行"); + item.setAbnormalType("休眠账户大额启用"); + item.setAbnormalTime("2025-08-01"); + item.setStatus("正常"); + when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(item)); + + List rows = service.exportAbnormalAccountPeople(40L); + + assertEquals(1, rows.size()); + assertEquals("6222000000000002", rows.getFirst().getAccountNo()); + assertEquals("王五", rows.getFirst().getAccountName()); + assertEquals("中国银行", rows.getFirst().getBankName()); + assertEquals("休眠账户大额启用", rows.getFirst().getAbnormalType()); + assertEquals("2025-08-01", rows.getFirst().getAbnormalTime()); + assertEquals("正常", rows.getFirst().getStatus()); + verify(overviewMapper).selectAbnormalAccountList(40L); + } + + @Test + void shouldThrowWhenProjectDoesNotExistForAbnormalAccountQueries() { + when(projectMapper.selectById(99L)).thenReturn(null); + + CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); + queryDTO.setProjectId(99L); + + assertThrows(ServiceException.class, () -> service.getAbnormalAccountPeople(queryDTO)); + assertThrows(ServiceException.class, () -> service.exportAbnormalAccountPeople(99L)); + } + + private void mockProjectExists(Long projectId) { + CcdiProject project = new CcdiProject(); + project.setProjectId(projectId); + when(projectMapper.selectById(projectId)).thenReturn(project); + } +} From 1e0813a84c6c742dfd4d229acfb52c31709ed65c Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 31 Mar 2026 21:03:13 +0800 Subject: [PATCH 23/79] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=A3=8E=E9=99=A9?= =?UTF-8?q?=E6=98=8E=E7=BB=86=E5=BC=82=E5=B8=B8=E8=B4=A6=E6=88=B7=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/CcdiProjectOverviewServiceImpl.java | 3 ++- ...CcdiProjectRiskDetailWorkbookExporter.java | 19 ++++++++++++--- .../CcdiProjectOverviewServiceImplTest.java | 14 +++++++++++ ...ProjectRiskDetailWorkbookExporterTest.java | 23 +++++++++++++++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index 26d9ae03..ab6570f8 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -295,8 +295,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi List suspiciousRows = exportSuspiciousTransactions(queryDTO); List creditRows = exportEmployeeCreditNegative(projectId); + List abnormalRows = exportAbnormalAccountPeople(projectId); try { - workbookExporter.export(response, projectId, suspiciousRows, creditRows); + workbookExporter.export(response, projectId, suspiciousRows, creditRows, abnormalRows); } catch (IOException e) { throw new ServiceException("导出风险明细失败"); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java index 215c722e..4cc08acb 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java @@ -1,5 +1,6 @@ package com.ruoyi.ccdi.project.service.impl; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.common.utils.file.FileUtils; @@ -27,7 +28,8 @@ public class CcdiProjectRiskDetailWorkbookExporter { HttpServletResponse response, Long projectId, List suspiciousRows, - List creditRows + List creditRows, + List abnormalRows ) throws IOException { response.setContentType(CONTENT_TYPE); FileUtils.setAttachmentResponseHeader(response, "风险明细_" + projectId + ".xlsx"); @@ -35,7 +37,7 @@ public class CcdiProjectRiskDetailWorkbookExporter { try (Workbook workbook = new XSSFWorkbook()) { writeSuspiciousSheet(workbook.createSheet("涉疑交易明细"), suspiciousRows); writeCreditSheet(workbook.createSheet("员工负面征信信息"), creditRows); - writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息")); + writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息"), abnormalRows); workbook.write(response.getOutputStream()); } } @@ -88,10 +90,21 @@ public class CcdiProjectRiskDetailWorkbookExporter { } } - private void writeAbnormalAccountSheet(Sheet sheet) { + private void writeAbnormalAccountSheet(Sheet sheet, List rows) { Row header = sheet.createRow(0); String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态" }; writeHeader(header, headers); + + for (int i = 0; i < rows.size(); i++) { + CcdiProjectAbnormalAccountExcel item = rows.get(i); + Row row = sheet.createRow(i + 1); + row.createCell(0).setCellValue(safeText(item.getAccountNo())); + row.createCell(1).setCellValue(safeText(item.getAccountName())); + row.createCell(2).setCellValue(safeText(item.getBankName())); + row.createCell(3).setCellValue(safeText(item.getAbnormalType())); + row.createCell(4).setCellValue(safeText(item.getAbnormalTime())); + row.createCell(5).setCellValue(safeText(item.getStatus())); + } } private void writeHeader(Row row, String[] headers) { diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java index 774ae742..c1e5d2a7 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java @@ -5,10 +5,12 @@ import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; @@ -268,6 +270,15 @@ class CcdiProjectOverviewServiceImplTest { creditItem.setCivilLmt(new BigDecimal("20000.00")); when(overviewMapper.selectEmployeeCreditNegativeList(40L)).thenReturn(List.of(creditItem)); + CcdiProjectAbnormalAccountItemVO abnormalItem = new CcdiProjectAbnormalAccountItemVO(); + abnormalItem.setAccountNo("6222000000000001"); + abnormalItem.setAccountName("李四"); + abnormalItem.setBankName("中国农业银行"); + abnormalItem.setAbnormalType("突然销户"); + abnormalItem.setAbnormalTime("2026-03-20"); + abnormalItem.setStatus("已销户"); + when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem)); + MockHttpServletResponse response = new MockHttpServletResponse(); service.exportRiskDetails(response, 40L); @@ -282,6 +293,9 @@ class CcdiProjectOverviewServiceImplTest { ), argThat((List rows) -> rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName()) + ), + argThat((List rows) -> + rows.size() == 1 && "6222000000000001".equals(rows.getFirst().getAccountNo()) ) ); } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java index 7d2da1ef..c65e061b 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java @@ -1,5 +1,6 @@ package com.ruoyi.ccdi.project.service.impl; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import org.apache.poi.ss.usermodel.WorkbookFactory; @@ -36,7 +37,15 @@ class CcdiProjectRiskDetailWorkbookExporterTest { creditRow.setCivilCnt(1); creditRow.setCivilLmt(new BigDecimal("20000.00")); - exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow)); + CcdiProjectAbnormalAccountExcel abnormalRow = new CcdiProjectAbnormalAccountExcel(); + abnormalRow.setAccountNo("6222000000000001"); + abnormalRow.setAccountName("李四"); + abnormalRow.setBankName("中国农业银行"); + abnormalRow.setAbnormalType("突然销户"); + abnormalRow.setAbnormalTime("2026-03-20"); + abnormalRow.setStatus("已销户"); + + exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow), List.of(abnormalRow)); assertTrue(response.getContentType().startsWith( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @@ -48,8 +57,18 @@ class CcdiProjectRiskDetailWorkbookExporterTest { assertEquals("员工负面征信信息", workbook.getSheetAt(1).getSheetName()); assertEquals("异常账户人员信息", workbook.getSheetAt(2).getSheetName()); assertEquals("账号", workbook.getSheetAt(2).getRow(0).getCell(0).getStringCellValue()); + assertEquals("开户人", workbook.getSheetAt(2).getRow(0).getCell(1).getStringCellValue()); + assertEquals("银行", workbook.getSheetAt(2).getRow(0).getCell(2).getStringCellValue()); + assertEquals("异常类型", workbook.getSheetAt(2).getRow(0).getCell(3).getStringCellValue()); + assertEquals("异常发生时间", workbook.getSheetAt(2).getRow(0).getCell(4).getStringCellValue()); assertEquals("状态", workbook.getSheetAt(2).getRow(0).getCell(5).getStringCellValue()); - assertEquals(1, workbook.getSheetAt(2).getPhysicalNumberOfRows()); + assertEquals("6222000000000001", workbook.getSheetAt(2).getRow(1).getCell(0).getStringCellValue()); + assertEquals("李四", workbook.getSheetAt(2).getRow(1).getCell(1).getStringCellValue()); + assertEquals("中国农业银行", workbook.getSheetAt(2).getRow(1).getCell(2).getStringCellValue()); + assertEquals("突然销户", workbook.getSheetAt(2).getRow(1).getCell(3).getStringCellValue()); + assertEquals("2026-03-20", workbook.getSheetAt(2).getRow(1).getCell(4).getStringCellValue()); + assertEquals("已销户", workbook.getSheetAt(2).getRow(1).getCell(5).getStringCellValue()); + assertEquals(2, workbook.getSheetAt(2).getPhysicalNumberOfRows()); } } } From 4b5ac7388cc8e139f1eb98b5484a1b1d799f5a8b Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 31 Mar 2026 21:04:06 +0800 Subject: [PATCH 24/79] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E4=BA=BA=E5=91=98=E4=BF=A1=E6=81=AF=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=AE=9E=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...abnormal-account-backend-implementation.md | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md diff --git a/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md b/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md new file mode 100644 index 00000000..825b10d9 --- /dev/null +++ b/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md @@ -0,0 +1,136 @@ +# 项目详情风险明细异常账户人员信息后端实施记录 + +## 1. 实施概述 + +- 实施日期:2026-03-31 +- 实施目标:为项目详情风险明细补齐“异常账户人员信息”的真实后端分页查询与统一导出能力 +- 实施范围:`ccdi-project` 模块结果总览控制器、服务层、Mapper SQL、统一工作簿导出器及对应测试 + +## 2. 新增接口与对象 + +### 2.1 新增接口 + +- `GET /ccdi/project/overview/abnormal-account-people` + - 入参:`projectId`、`pageNum`、`pageSize` + - 返回:`rows`、`total` + - 权限:`ccdi:project:query` + +### 2.2 新增 DTO / VO / Excel 对象 + +- `CcdiProjectAbnormalAccountQueryDTO` + - 承载异常账户分页查询入参 +- `CcdiProjectAbnormalAccountItemVO` + - 承载单条异常账户明细 +- `CcdiProjectAbnormalAccountPageVO` + - 承载分页查询结果 `rows/total` +- `CcdiProjectAbnormalAccountExcel` + - 承载统一导出第 3 个 sheet 的行数据 + +## 3. Mapper SQL 口径 + +异常账户分页与导出统一复用同一套基础查询口径: + +- 仅查询当前项目:`tr.project_id = projectId` +- 仅查询异常账户模型:`tr.model_code = 'ABNORMAL_ACCOUNT'` +- 仅查询对象型结果:`tr.bank_statement_id is null` +- 仅查询员工本人账户:`account.owner_type = 'EMPLOYEE'` 且 `account.owner_id = tr.object_key` +- 仅在 `reason_detail` 中命中具体账号时返回:`instr(tr.reason_detail, account.account_no) > 0` +- 排序统一为:`异常发生时间 desc -> 账号 asc -> 规则编码 asc` + +字段映射如下: + +- `accountNo`:`ccdi_account_info.account_no` +- `accountName`:优先 `ccdi_account_info.account_name`,为空回退 `ccdi_base_staff.name` +- `bankName`:`ccdi_account_info.bank` +- `abnormalType`:`ccdi_bank_statement_tag_result.rule_name` +- `abnormalTime` + - `SUDDEN_ACCOUNT_CLOSURE` 取 `invalid_date` + - `DORMANT_ACCOUNT_LARGE_ACTIVATION` 从 `reason_detail` 提取首次交易日期 +- `status` + - `1 -> 正常` + - `2 -> 已销户` + +## 4. 服务层与统一导出改动 + +### 4.1 服务层 + +- 在 `ICcdiProjectOverviewService` 中新增: + - `getAbnormalAccountPeople(queryDTO)` + - `exportAbnormalAccountPeople(projectId)` +- 在 `CcdiProjectOverviewServiceImpl` 中实现: + - 项目存在性校验 + - 分页默认值 `pageNum=1`、`pageSize=5` + - 分页结果直接映射为 `CcdiProjectAbnormalAccountPageVO` + - 导出结果映射为 `CcdiProjectAbnormalAccountExcel` + +### 4.2 统一导出 + +- `exportRiskDetails(...)` 现在会同时查询: + - 涉疑交易明细 + - 员工负面征信信息 + - 异常账户人员信息 +- `CcdiProjectRiskDetailWorkbookExporter.export(...)` 方法签名扩展为接收异常账户列表 +- 第 3 个 sheet `异常账户人员信息` 从“仅表头”改为“表头 + 真实数据行” +- 第 3 个 sheet 列顺序固定为: + - `账号` + - `开户人` + - `银行` + - `异常类型` + - `异常发生时间` + - `状态` + +## 5. 自动化验证 + +### 5.1 基线验证 + +执行命令: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +验证结果: + +- 40 个相关既有测试通过 + +### 5.2 任务内 TDD 验证 + +按计划分别执行并通过: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewMapperSqlTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +### 5.3 最终回归 + +执行命令: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceAbnormalAccountTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +结果: + +- 47 个测试全部通过 +- `BUILD SUCCESS` + +## 6. 手工联调与进程处理 + +- 本次未执行手工联调 +- 未启动新的后端 `java -jar ruoyi-admin.jar` 进程 +- 因未启动额外前后端进程,无额外进程需要关闭 + +## 7. 结果结论 + +- 异常账户人员信息分页接口已具备真实查询能力 +- 页面查询与统一导出第 3 个 sheet 已复用同一套异常账户明细口径 +- 返回字段已覆盖: + - `accountNo` + - `accountName` + - `bankName` + - `abnormalType` + - `abnormalTime` + - `status` From 4dca2b2b634b8802423232a82db71b6db23ed6cd Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 31 Mar 2026 21:07:24 +0800 Subject: [PATCH 25/79] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E4=BA=BA=E5=91=98=E5=89=8D=E7=AB=AF=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/api/ccdi/projectOverview.js | 12 +++++ .../components/detail/RiskDetailSection.vue | 47 +++++++++++++++++++ ...detail-abnormal-account-pagination.test.js | 22 +++++++++ 3 files changed, 81 insertions(+) create mode 100644 ruoyi-ui/tests/unit/risk-detail-abnormal-account-pagination.test.js diff --git a/ruoyi-ui/src/api/ccdi/projectOverview.js b/ruoyi-ui/src/api/ccdi/projectOverview.js index 6874d822..1beddebd 100644 --- a/ruoyi-ui/src/api/ccdi/projectOverview.js +++ b/ruoyi-ui/src/api/ccdi/projectOverview.js @@ -79,3 +79,15 @@ export function getOverviewEmployeeCreditNegative(params) { } }) } + +export function getOverviewAbnormalAccountPeople(params) { + return request({ + url: '/ccdi/project/overview/abnormal-account-people', + method: 'get', + params: { + projectId: params.projectId, + pageNum: params.pageNum, + pageSize: params.pageSize + } + }) +} diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue index 64006fb5..24400d59 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue @@ -314,6 +314,7 @@ + + diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/EnterpriseRelationEditDialog.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/EnterpriseRelationEditDialog.vue new file mode 100644 index 00000000..856128cc --- /dev/null +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/EnterpriseRelationEditDialog.vue @@ -0,0 +1,87 @@ + + + diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue new file mode 100644 index 00000000..f4a82cd9 --- /dev/null +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue @@ -0,0 +1,192 @@ + + + diff --git a/sql/migration/2026-04-17-add-enterprise-base-info-menu.sql b/sql/migration/2026-04-17-add-enterprise-base-info-menu.sql new file mode 100644 index 00000000..1520a6f4 --- /dev/null +++ b/sql/migration/2026-04-17-add-enterprise-base-info-menu.sql @@ -0,0 +1,199 @@ +-- 实体库管理菜单 +-- 挂载到“信息维护”目录下,可重复执行 + +SET @parent_menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE menu_name = '信息维护' + AND parent_id = 0 + LIMIT 1 +); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + update_by, + update_time, + remark +) +SELECT + '实体库管理', + @parent_menu_id, + 12, + 'enterpriseBaseInfo', + 'ccdiEnterpriseBaseInfo/index', + 1, + 0, + 'C', + '0', + '0', + 'ccdi:enterpriseBaseInfo:list', + 'documentation', + 'admin', + NOW(), + '', + NULL, + '实体库管理菜单' +FROM dual +WHERE @parent_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @parent_menu_id + AND path = 'enterpriseBaseInfo' + ); + +SET @menu_id = ( + SELECT menu_id + FROM sys_menu + WHERE parent_id = @parent_menu_id + AND path = 'enterpriseBaseInfo' + LIMIT 1 +); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + remark +) +SELECT '实体库查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:enterpriseBaseInfo:query', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:enterpriseBaseInfo:query' + ); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + remark +) +SELECT '实体库新增', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:enterpriseBaseInfo:add', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:enterpriseBaseInfo:add' + ); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + remark +) +SELECT '实体库修改', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:enterpriseBaseInfo:edit', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:enterpriseBaseInfo:edit' + ); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + remark +) +SELECT '实体库删除', @menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'ccdi:enterpriseBaseInfo:remove', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:enterpriseBaseInfo:remove' + ); + +INSERT INTO sys_menu ( + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + create_by, + create_time, + remark +) +SELECT '实体库导入', @menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'ccdi:enterpriseBaseInfo:import', '#', 'admin', NOW(), '' +FROM dual +WHERE @menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM sys_menu + WHERE parent_id = @menu_id + AND perms = 'ccdi:enterpriseBaseInfo:import' + ); diff --git a/sql/migration/2026-04-17-create-intermediary-enterprise-relation.sql b/sql/migration/2026-04-17-create-intermediary-enterprise-relation.sql new file mode 100644 index 00000000..d6678cff --- /dev/null +++ b/sql/migration/2026-04-17-create-intermediary-enterprise-relation.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `ccdi_intermediary_enterprise_relation` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `intermediary_biz_id` VARCHAR(64) NOT NULL COMMENT '所属中介biz_id', + `social_credit_code` VARCHAR(18) NOT NULL COMMENT '统一社会信用代码', + `relation_person_post` VARCHAR(100) DEFAULT NULL COMMENT '关联角色/职务', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_intermediary_enterprise` (`intermediary_biz_id`, `social_credit_code`), + KEY `idx_intermediary_biz_id` (`intermediary_biz_id`), + KEY `idx_social_credit_code` (`social_credit_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中介关联机构关系表'; diff --git a/sql/migration/2026-04-17-fix-intermediary-person-sub-type-dict.sql b/sql/migration/2026-04-17-fix-intermediary-person-sub-type-dict.sql new file mode 100644 index 00000000..d3d2b68a --- /dev/null +++ b/sql/migration/2026-04-17-fix-intermediary-person-sub-type-dict.sql @@ -0,0 +1,4 @@ +-- 如当前环境通过数据字典维护 person_sub_type,请补齐以下固定值: +-- 本人、配偶、子女、父母、兄弟姐妹、其他 +-- +-- 具体 dict_type 请按现网实际配置填写后执行。 diff --git a/tongweb_62318.properties b/tongweb_62318.properties new file mode 100644 index 00000000..153ab7f7 --- /dev/null +++ b/tongweb_62318.properties @@ -0,0 +1,15 @@ +#TongTech License properties +#Fri Apr 17 16:39:03 CST 2026 +application.location=/Users/wkc/Desktop/ccdi/ccdi +license.create.date=2024-12-10 +license.customer.name=\u6D59\u6C5F\u519C\u6751\u5546\u4E1A\u8054\u5408\u94F6\u884C\u80A1\u4EFD\u6709\u9650\u516C\u53F8 +license.end.date=-1 +license.extern.properties.name=validateType,order_number,license_info +license.extern.properties.value=file,2024-2121,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\n +license.file.content=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\n +license.file.path=classpath\:license.dat +license.max.number=-1 +license.project.name=\u6D59\u6C5F\u519C\u6751\u5546\u4E1A\u8054\u5408\u94F6\u884C\u80A1\u4EFD\u6709\u9650\u516C\u53F8\u5173\u4E8E\u56FD\u4EA7\u5316\u5E94\u7528\u670D\u52A1\u5668\u4E2D\u95F4\u4EF6\u91C7\u8D2D +license.type=release +license.validate.type=file +server.number=7.0.E.7 From 2b321a8621963628ab0679d9211123369734ceb3 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Mon, 20 Apr 2026 11:24:18 +0800 Subject: [PATCH 43/79] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=AD=E4=BB=8B?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 18436 -> 18436 bytes AGENTS.md | 3 + bin/restart_java_backend.sh | 11 +- .../CcdiIntermediaryController.java | 107 ++ .../domain/dto/CcdiIntermediaryQueryDTO.java | 11 +- .../domain/vo/CcdiIntermediaryVO.java | 29 +- .../service/ICcdiIntermediaryService.java | 84 ++ .../impl/CcdiIntermediaryServiceImpl.java | 182 +++- .../collection/CcdiIntermediaryMapper.xml | 99 +- .../service/impl/CcdiBankTagServiceImpl.java | 44 +- .../project/CcdiBankTagAnalysisMapper.xml | 12 +- .../CcdiBankTagAnalysisMapperXmlTest.java | 28 + .../impl/CcdiBankTagServiceImplTest.java | 19 + ...library-refactor-backend-implementation.md | 16 +- ...r-bank-tag-stuck-backend-implementation.md | 65 ++ ...ibrary-refactor-frontend-implementation.md | 9 + ruoyi-ui/src/api/ccdiIntermediary.js | 93 +- .../ccdiIntermediary/components/DataTable.vue | 29 +- .../components/DetailDialog.vue | 152 +-- .../components/EditDialog.vue | 719 +++----------- .../EnterpriseRelationEditDialog.vue | 38 +- .../components/RelativeEditDialog.vue | 33 +- .../components/SearchForm.vue | 27 +- ruoyi-ui/src/views/ccdiIntermediary/index.vue | 931 +++++------------- sql/2026-03-16-bank-tagging.sql | 2 +- sql/dpc_employee.sql | 4 +- sql/dpc_intermediary_blacklist.sql | 2 +- sql/fix_charset.sql | 6 +- ...reate-intermediary-enterprise-relation.sql | 2 +- ...-table-collation-to-utf8mb4-general-ci.sql | 60 ++ ...x-bank-tag-task-error-message-longtext.sql | 9 + 31 files changed, 1360 insertions(+), 1466 deletions(-) create mode 100644 docs/plans/backend/2026-04-20-docker-bank-tag-stuck-backend-implementation.md create mode 100644 sql/migration/2026-04-17-unify-all-table-collation-to-utf8mb4-general-ci.sql create mode 100644 sql/migration/2026-04-20-fix-bank-tag-task-error-message-longtext.sql diff --git a/.DS_Store b/.DS_Store index fcc8bf48884eec05986dae76568797a3c3567241..8eda584e6a739008612c06e1a73855159bfb72d0 100644 GIT binary patch delta 1151 zcmdT?-Afcv9Gz3@-eCyu3dZ<>W2}gx*7}v1A858cNP!+uDU&Dk`XNP*sEr}TQYx_>QIxU>>9+W~u*%*xEr?S&xlAE@ z4ec0nwmgJ_?L1En?~tZ%XoaF;!c(O^Aor;O+fFFk#i;s@F*R&xA$^0atRgICerJCu z7-YeX-$&OH(qkRnARCe9VO9SV%CoPR5W3i949a1(d0bb+8xqfcLi{u_##<_nqD4Y% z)4IAOHWA{p{Av}2StU7XVoR$i`YF})F)boTSfV!e88zq|V%BE(rk-)HxVPL2_kmmE zK678VZ`=>WAs!A8IFX7pq$2})C_yQ9Vh`%kfJQXKi^J%FA2J3I!Z1c)U;?Lb8q+v~ zOSp_Hn8j7xz&+f@Jf30^&+!5;@eV6k#e00kI)0kd*;QsRy%5sET6O|>Awz0s?W#x3 zk4|xlI;_&jvY3*ZwoPzJG@g}h!)zf(q6w^M8|Fy`65YfGY(uwHB+$)ldVEYBCvh+-1QaRMiC9v5&iR>UmkSP|E76Sr_1cW@Vv@EA|b?%YCix3siCGQV%B X{F|(7kfDE()BQi>e7)ePS$F&bPiY^4 delta 992 zcmd5(-Ahwp96rxc**Uz~-p!Qtx|%dGbq;iCvnDzdp-@}PMHXe9d5&~yZnh88K9Itz z$}rxG3JDsNbrpr(R#$;H1<@$F6T0foK1e8pUU&Q>ynLSL_j}*xd44l(mz~U<)6z!dQqo8(bjcD93Z8Fmt8$BI{T{|ntgm`q9!ngB*u}(6Nago%lmUnJ; PkIkFi`,确保会话字符集为 `utf8mb4`,避免导入或写入乱码 +- 数据库字符集与排序规则统一要求:所有业务表、系统表新增或修改时,必须显式使用 `utf8mb4` 字符集与 `utf8mb4_general_ci` 排序规则;禁止引入 `utf8mb4_0900_ai_ci`、`utf8mb4_unicode_ci` 或其他混用排序规则 - 银行流水打标相关规则与参数编码需要统一使用全大写;新增或修改 `rule_code`、`indicator_code`、`param_code` 时,禁止混用大小写风格 --- @@ -165,6 +167,7 @@ return AjaxResult.success(result); - 前端表单不要暴露通用审计字段 - 新增菜单、字典、初始化数据时,同步补充 SQL 脚本 - 执行数据库脚本或导入数据库前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新、导入时默认使用 `bin/mysql_utf8_exec.sh` +- 所有系统表和业务表的表级、字符字段级排序规则统一为 `utf8mb4_general_ci`;新增建表 SQL、字段追加 SQL、表结构修复 SQL 必须显式声明,避免因默认排序规则漂移导致联表或条件查询报错 ### 前端规范 diff --git a/bin/restart_java_backend.sh b/bin/restart_java_backend.sh index eebe6aeb..bc8f1ae3 100755 --- a/bin/restart_java_backend.sh +++ b/bin/restart_java_backend.sh @@ -83,7 +83,7 @@ collect_pids() { fi fi - marker_pids=$(pgrep -f "$APP_MARKER" 2>/dev/null || true) + marker_pids=$(pgrep -f -- "$APP_MARKER" 2>/dev/null || true) if [ -n "${marker_pids:-}" ]; then for pid in $marker_pids; do if is_managed_backend_pid "$pid"; then @@ -92,6 +92,15 @@ collect_pids() { done fi + port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true) + if [ -n "${port_pids:-}" ]; then + for pid in $port_pids; do + if is_managed_backend_pid "$pid"; then + all_pids="$all_pids $pid" + fi + done + fi + unique_pids="" for pid in $all_pids; do case " $unique_pids " in diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java index 070ff06d..b1c5692f 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java @@ -72,6 +72,26 @@ public class CcdiIntermediaryController extends BaseController { return success(vo); } + /** + * 查询中介亲属列表 + */ + @Operation(summary = "查询中介亲属列表") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:query')") + @GetMapping("/{bizId}/relatives") + public AjaxResult getRelativeList(@PathVariable String bizId) { + return success(intermediaryService.selectIntermediaryRelativeList(bizId)); + } + + /** + * 查询中介亲属详情 + */ + @Operation(summary = "查询中介亲属详情") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:query')") + @GetMapping("/relative/{relativeBizId}") + public AjaxResult getRelativeInfo(@PathVariable String relativeBizId) { + return success(intermediaryService.selectIntermediaryRelativeDetail(relativeBizId)); + } + /** * 查询实体中介详情 */ @@ -105,6 +125,28 @@ public class CcdiIntermediaryController extends BaseController { return toAjax(intermediaryService.updateIntermediaryPerson(editDTO)); } + /** + * 新增中介亲属 + */ + @Operation(summary = "新增中介亲属") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:add')") + @Log(title = "中介亲属", businessType = BusinessType.INSERT) + @PostMapping("/{bizId}/relative") + public AjaxResult addRelative(@PathVariable String bizId, @Validated @RequestBody CcdiIntermediaryRelativeAddDTO addDTO) { + return toAjax(intermediaryService.insertIntermediaryRelative(bizId, addDTO)); + } + + /** + * 修改中介亲属 + */ + @Operation(summary = "修改中介亲属") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:edit')") + @Log(title = "中介亲属", businessType = BusinessType.UPDATE) + @PutMapping("/relative") + public AjaxResult editRelative(@Validated @RequestBody CcdiIntermediaryRelativeEditDTO editDTO) { + return toAjax(intermediaryService.updateIntermediaryRelative(editDTO)); + } + /** * 新增实体中介 */ @@ -127,6 +169,49 @@ public class CcdiIntermediaryController extends BaseController { return toAjax(intermediaryService.updateIntermediaryEntity(editDTO)); } + /** + * 查询中介关联机构列表 + */ + @Operation(summary = "查询中介关联机构列表") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:query')") + @GetMapping("/{bizId}/enterprise-relations") + public AjaxResult getEnterpriseRelationList(@PathVariable String bizId) { + return success(intermediaryService.selectIntermediaryEnterpriseRelationList(bizId)); + } + + /** + * 查询中介关联机构详情 + */ + @Operation(summary = "查询中介关联机构详情") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:query')") + @GetMapping("/enterprise-relation/{id}") + public AjaxResult getEnterpriseRelationInfo(@PathVariable Long id) { + return success(intermediaryService.selectIntermediaryEnterpriseRelationDetail(id)); + } + + /** + * 新增中介关联机构 + */ + @Operation(summary = "新增中介关联机构") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:add')") + @Log(title = "中介关联机构", businessType = BusinessType.INSERT) + @PostMapping("/{bizId}/enterprise-relation") + public AjaxResult addEnterpriseRelation(@PathVariable String bizId, + @Validated @RequestBody CcdiIntermediaryEnterpriseRelationAddDTO addDTO) { + return toAjax(intermediaryService.insertIntermediaryEnterpriseRelation(bizId, addDTO)); + } + + /** + * 修改中介关联机构 + */ + @Operation(summary = "修改中介关联机构") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:edit')") + @Log(title = "中介关联机构", businessType = BusinessType.UPDATE) + @PutMapping("/enterprise-relation") + public AjaxResult editEnterpriseRelation(@Validated @RequestBody CcdiIntermediaryEnterpriseRelationEditDTO editDTO) { + return toAjax(intermediaryService.updateIntermediaryEnterpriseRelation(editDTO)); + } + /** * 删除中介 */ @@ -138,6 +223,28 @@ public class CcdiIntermediaryController extends BaseController { return toAjax(intermediaryService.deleteIntermediaryByIds(ids)); } + /** + * 删除中介亲属 + */ + @Operation(summary = "删除中介亲属") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:remove')") + @Log(title = "中介亲属", businessType = BusinessType.DELETE) + @DeleteMapping("/relative/{relativeBizId}") + public AjaxResult removeRelative(@PathVariable String relativeBizId) { + return toAjax(intermediaryService.deleteIntermediaryRelative(relativeBizId)); + } + + /** + * 删除中介关联机构 + */ + @Operation(summary = "删除中介关联机构") + @PreAuthorize("@ss.hasPermi('ccdi:intermediary:remove')") + @Log(title = "中介关联机构", businessType = BusinessType.DELETE) + @DeleteMapping("/enterprise-relation/{id}") + public AjaxResult removeEnterpriseRelation(@PathVariable Long id) { + return toAjax(intermediaryService.deleteIntermediaryEnterpriseRelation(id)); + } + /** * 校验人员ID唯一性 */ diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryQueryDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryQueryDTO.java index abf26407..fbf1e889 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryQueryDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryQueryDTO.java @@ -19,12 +19,15 @@ public class CcdiIntermediaryQueryDTO implements Serializable { @Serial private static final long serialVersionUID = 1L; - @Schema(description = "姓名/机构名称") + @Schema(description = "名称") private String name; - @Schema(description = "证件号/统一社会信用代码") + @Schema(description = "证件号") private String certificateNo; - @Schema(description = "中介类型(1=个人, 2=实体)") - private String intermediaryType; + @Schema(description = "记录类型(INTERMEDIARY/RELATIVE/ENTERPRISE_RELATION)") + private String recordType; + + @Schema(description = "关联中介信息(姓名或证件号)") + private String relatedIntermediaryKeyword; } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryVO.java index a875cdea..2f6aae6d 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryVO.java @@ -21,32 +21,25 @@ public class CcdiIntermediaryVO implements Serializable { @Serial private static final long serialVersionUID = 1L; - @Schema(description = "ID") - private String id; + @Schema(description = "记录类型") + private String recordType; - @Schema(description = "姓名/机构名称") + @Schema(description = "记录ID") + private String recordId; + + @Schema(description = "名称") private String name; - @Schema(description = "证件号/统一社会信用代码") + @Schema(description = "证件号") private String certificateNo; - @Schema(description = "中介类型(1=个人, 2=实体)") - private String intermediaryType; + @Schema(description = "关联中介姓名") + private String relatedIntermediaryName; - @Schema(description = "人员类型") - private String personType; - - @Schema(description = "公司") - private String company; - - @Schema(description = "数据来源") - private String dataSource; + @Schema(description = "关联关系") + private String relationText; @Schema(description = "创建时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; - - @Schema(description = "修改时间") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private Date updateTime; } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java index 07eeb727..195633a0 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java @@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.dto.*; import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel; import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel; +import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEntityDetailVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryPersonDetailVO; +import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryRelativeVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryVO; import java.util.List; @@ -35,6 +37,22 @@ public interface ICcdiIntermediaryService { */ CcdiIntermediaryPersonDetailVO selectIntermediaryPersonDetail(String bizId); + /** + * 查询中介亲属列表 + * + * @param bizId 中介本人ID + * @return 亲属列表 + */ + List selectIntermediaryRelativeList(String bizId); + + /** + * 查询中介亲属详情 + * + * @param relativeBizId 亲属ID + * @return 亲属详情 + */ + CcdiIntermediaryRelativeVO selectIntermediaryRelativeDetail(String relativeBizId); + /** * 查询实体中介详情 * @@ -59,6 +77,31 @@ public interface ICcdiIntermediaryService { */ int updateIntermediaryPerson(CcdiIntermediaryPersonEditDTO editDTO); + /** + * 新增中介亲属 + * + * @param bizId 中介本人ID + * @param addDTO 新增DTO + * @return 结果 + */ + int insertIntermediaryRelative(String bizId, CcdiIntermediaryRelativeAddDTO addDTO); + + /** + * 修改中介亲属 + * + * @param editDTO 编辑DTO + * @return 结果 + */ + int updateIntermediaryRelative(CcdiIntermediaryRelativeEditDTO editDTO); + + /** + * 删除中介亲属 + * + * @param relativeBizId 亲属ID + * @return 结果 + */ + int deleteIntermediaryRelative(String relativeBizId); + /** * 新增实体中介 * @@ -75,6 +118,47 @@ public interface ICcdiIntermediaryService { */ int updateIntermediaryEntity(CcdiIntermediaryEntityEditDTO editDTO); + /** + * 查询中介关联机构列表 + * + * @param bizId 中介本人ID + * @return 关联机构列表 + */ + List selectIntermediaryEnterpriseRelationList(String bizId); + + /** + * 查询中介关联机构详情 + * + * @param id 主键ID + * @return 关联机构详情 + */ + CcdiIntermediaryEnterpriseRelationVO selectIntermediaryEnterpriseRelationDetail(Long id); + + /** + * 新增中介关联机构 + * + * @param bizId 中介本人ID + * @param addDTO 新增DTO + * @return 结果 + */ + int insertIntermediaryEnterpriseRelation(String bizId, CcdiIntermediaryEnterpriseRelationAddDTO addDTO); + + /** + * 修改中介关联机构 + * + * @param editDTO 编辑DTO + * @return 结果 + */ + int updateIntermediaryEnterpriseRelation(CcdiIntermediaryEnterpriseRelationEditDTO editDTO); + + /** + * 删除中介关联机构 + * + * @param id 主键ID + * @return 结果 + */ + int deleteIntermediaryEnterpriseRelation(Long id); + /** * 批量删除中介 * diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java index 8ae5dcf2..ddeb8a7d 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java @@ -4,14 +4,18 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.CcdiBizIntermediary; import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo; +import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation; import com.ruoyi.info.collection.domain.dto.*; import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel; import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel; +import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEntityDetailVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryPersonDetailVO; +import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryRelativeVO; import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryVO; import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper; import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper; +import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper; import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper; import com.ruoyi.info.collection.service.ICcdiIntermediaryEntityImportService; import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService; @@ -48,6 +52,9 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { @Resource private CcdiIntermediaryMapper intermediaryMapper; + @Resource + private CcdiIntermediaryEnterpriseRelationMapper enterpriseRelationMapper; + @Resource private ICcdiIntermediaryPersonImportService personImportService; @@ -81,7 +88,7 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { @Override public CcdiIntermediaryPersonDetailVO selectIntermediaryPersonDetail(String bizId) { CcdiBizIntermediary person = bizIntermediaryMapper.selectById(bizId); - if (person == null) { + if (person == null || !isIntermediaryPerson(person)) { return null; } @@ -92,6 +99,24 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { return vo; } + @Override + public List selectIntermediaryRelativeList(String bizId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CcdiBizIntermediary::getRelatedNumId, bizId) + .ne(CcdiBizIntermediary::getPersonSubType, "本人") + .orderByDesc(CcdiBizIntermediary::getCreateTime); + return bizIntermediaryMapper.selectList(wrapper).stream().map(this::buildRelativeVo).toList(); + } + + @Override + public CcdiIntermediaryRelativeVO selectIntermediaryRelativeDetail(String relativeBizId) { + CcdiBizIntermediary relative = bizIntermediaryMapper.selectById(relativeBizId); + if (relative == null || isIntermediaryPerson(relative)) { + return null; + } + return buildRelativeVo(relative); + } + /** * 查询实体中介详情 * @@ -130,6 +155,8 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { CcdiBizIntermediary person = new CcdiBizIntermediary(); BeanUtils.copyProperties(addDTO, person); + person.setPersonSubType("本人"); + person.setRelatedNumId(null); person.setDataSource("MANUAL"); return bizIntermediaryMapper.insert(person); @@ -151,12 +178,64 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { } } + CcdiBizIntermediary existing = bizIntermediaryMapper.selectById(editDTO.getBizId()); + if (existing == null || !isIntermediaryPerson(existing)) { + throw new RuntimeException("中介本人不存在"); + } + CcdiBizIntermediary person = new CcdiBizIntermediary(); BeanUtils.copyProperties(editDTO, person); + person.setPersonSubType("本人"); + person.setRelatedNumId(null); return bizIntermediaryMapper.updateById(person); } + @Override + @Transactional + public int insertIntermediaryRelative(String bizId, CcdiIntermediaryRelativeAddDTO addDTO) { + CcdiBizIntermediary owner = requireIntermediaryPerson(bizId); + validateRelativePersonSubType(addDTO.getPersonSubType()); + if (!checkPersonIdUnique(addDTO.getPersonId(), null)) { + throw new RuntimeException("该证件号已存在"); + } + + CcdiBizIntermediary relative = new CcdiBizIntermediary(); + BeanUtils.copyProperties(addDTO, relative); + relative.setRelatedNumId(owner.getBizId()); + relative.setDataSource("MANUAL"); + return bizIntermediaryMapper.insert(relative); + } + + @Override + @Transactional + public int updateIntermediaryRelative(CcdiIntermediaryRelativeEditDTO editDTO) { + CcdiBizIntermediary existing = bizIntermediaryMapper.selectById(editDTO.getBizId()); + if (existing == null || isIntermediaryPerson(existing)) { + throw new RuntimeException("中介亲属不存在"); + } + validateRelativePersonSubType(editDTO.getPersonSubType()); + if (StringUtils.isNotEmpty(editDTO.getPersonId()) + && !checkPersonIdUnique(editDTO.getPersonId(), editDTO.getBizId())) { + throw new RuntimeException("该证件号已存在"); + } + + CcdiBizIntermediary relative = new CcdiBizIntermediary(); + BeanUtils.copyProperties(editDTO, relative); + relative.setRelatedNumId(existing.getRelatedNumId()); + return bizIntermediaryMapper.updateById(relative); + } + + @Override + @Transactional + public int deleteIntermediaryRelative(String relativeBizId) { + CcdiBizIntermediary existing = bizIntermediaryMapper.selectById(relativeBizId); + if (existing == null || isIntermediaryPerson(existing)) { + throw new RuntimeException("中介亲属不存在"); + } + return bizIntermediaryMapper.deleteById(relativeBizId); + } + /** * 新增实体中介 * @@ -197,6 +276,49 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { return enterpriseBaseInfoMapper.updateById(entity); } + @Override + public List selectIntermediaryEnterpriseRelationList(String bizId) { + return enterpriseRelationMapper.selectByIntermediaryBizId(bizId); + } + + @Override + public CcdiIntermediaryEnterpriseRelationVO selectIntermediaryEnterpriseRelationDetail(Long id) { + return enterpriseRelationMapper.selectDetailById(id); + } + + @Override + @Transactional + public int insertIntermediaryEnterpriseRelation(String bizId, CcdiIntermediaryEnterpriseRelationAddDTO addDTO) { + CcdiBizIntermediary owner = requireIntermediaryPerson(bizId); + validateEnterpriseRelation(owner.getBizId(), addDTO.getSocialCreditCode(), null); + + CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation(); + BeanUtils.copyProperties(addDTO, relation); + relation.setIntermediaryBizId(owner.getBizId()); + return enterpriseRelationMapper.insert(relation); + } + + @Override + @Transactional + public int updateIntermediaryEnterpriseRelation(CcdiIntermediaryEnterpriseRelationEditDTO editDTO) { + CcdiIntermediaryEnterpriseRelation existing = enterpriseRelationMapper.selectById(editDTO.getId()); + if (existing == null) { + throw new RuntimeException("中介关联机构不存在"); + } + validateEnterpriseRelation(existing.getIntermediaryBizId(), editDTO.getSocialCreditCode(), existing.getId()); + + CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation(); + BeanUtils.copyProperties(editDTO, relation); + relation.setIntermediaryBizId(existing.getIntermediaryBizId()); + return enterpriseRelationMapper.updateById(relation); + } + + @Override + @Transactional + public int deleteIntermediaryEnterpriseRelation(Long id) { + return enterpriseRelationMapper.deleteById(id); + } + /** * 批量删除中介 * @@ -208,12 +330,19 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { public int deleteIntermediaryByIds(String[] ids) { int count = 0; for (String id : ids) { - // 判断是个人还是实体(个人ID长度较长,实体统一社会信用代码18位) - if (id.length() > 18) { - // 个人中介 + CcdiBizIntermediary intermediary = bizIntermediaryMapper.selectById(id); + if (intermediary != null) { + if (isIntermediaryPerson(intermediary)) { + bizIntermediaryMapper.delete(new LambdaQueryWrapper() + .eq(CcdiBizIntermediary::getRelatedNumId, id)); + enterpriseRelationMapper.delete(new LambdaQueryWrapper() + .eq(CcdiIntermediaryEnterpriseRelation::getIntermediaryBizId, id)); + } count += bizIntermediaryMapper.deleteById(id); - } else { - // 实体中介 + continue; + } + + if (enterpriseBaseInfoMapper.selectById(id) != null) { count += enterpriseBaseInfoMapper.deleteById(id); } } @@ -325,4 +454,45 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { return taskId; } + + private boolean isIntermediaryPerson(CcdiBizIntermediary person) { + return "本人".equals(person.getPersonSubType()); + } + + private CcdiBizIntermediary requireIntermediaryPerson(String bizId) { + CcdiBizIntermediary owner = bizIntermediaryMapper.selectById(bizId); + if (owner == null || !isIntermediaryPerson(owner)) { + throw new RuntimeException("中介本人不存在"); + } + return owner; + } + + private void validateRelativePersonSubType(String personSubType) { + if ("本人".equals(personSubType)) { + throw new RuntimeException("亲属关系不能为本人"); + } + } + + private void validateEnterpriseRelation(String bizId, String socialCreditCode, Long excludeId) { + requireIntermediaryPerson(bizId); + if (enterpriseBaseInfoMapper.selectById(socialCreditCode) == null) { + throw new RuntimeException("关联机构不存在"); + } + boolean exists = enterpriseRelationMapper.existsByIntermediaryBizIdAndSocialCreditCode(bizId, socialCreditCode); + if (exists) { + if (excludeId == null) { + throw new RuntimeException("该中介已关联此机构"); + } + CcdiIntermediaryEnterpriseRelation existing = enterpriseRelationMapper.selectById(excludeId); + if (existing == null || !socialCreditCode.equals(existing.getSocialCreditCode())) { + throw new RuntimeException("该中介已关联此机构"); + } + } + } + + private CcdiIntermediaryRelativeVO buildRelativeVo(CcdiBizIntermediary relative) { + CcdiIntermediaryRelativeVO vo = new CcdiIntermediaryRelativeVO(); + BeanUtils.copyProperties(relative, vo); + return vo; + } } diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml index 3601d0bd..3aa07391 100644 --- a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml @@ -4,57 +4,86 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - + diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java index 75d879b1..1665f81f 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java @@ -39,6 +39,7 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { private static final String STATUS_RUNNING = "RUNNING"; private static final String STATUS_SUCCESS = "SUCCESS"; private static final String STATUS_FAILED = "FAILED"; + private static final String TASK_ERROR_MESSAGE_FALLBACK = "任务失败,详细异常请查看后端日志"; private static final String RESULT_TYPE_STATEMENT = "STATEMENT"; private static final String OBJECT_TYPE_STAFF_ID_CARD = "STAFF_ID_CARD"; @@ -147,12 +148,11 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { return task.getId(); } catch (Exception ex) { task.setStatus(STATUS_FAILED); - task.setErrorMessage(ex.getMessage()); task.setEndTime(new Date()); task.setNeedRerun(null); task.setUpdateBy(operator); task.setUpdateTime(new Date()); - taskMapper.updateTask(task); + updateFailedTaskSafely(task, ex); projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.PROCESSING, operator); log.error("【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}", task.getId(), projectId, modelCode, triggerType, ex.getMessage(), ex); @@ -359,4 +359,44 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { } return Integer.parseInt(value); } + + private void updateFailedTaskSafely(CcdiBankTagTask task, Exception ex) { + task.setErrorMessage(buildSafeTaskErrorMessage(ex)); + try { + taskMapper.updateTask(task); + } catch (Exception updateEx) { + log.error("【流水标签】写入任务失败状态异常: taskId={}, error={}", + task.getId(), updateEx.getMessage(), updateEx); + task.setErrorMessage(TASK_ERROR_MESSAGE_FALLBACK); + taskMapper.updateTask(task); + } + } + + private static String buildSafeTaskErrorMessage(Throwable throwable) { + if (throwable == null) { + return TASK_ERROR_MESSAGE_FALLBACK; + } + + StringBuilder builder = new StringBuilder(); + if (throwable.getMessage() != null && !throwable.getMessage().isBlank()) { + builder.append(throwable.getMessage().trim()); + } + + Throwable rootCause = throwable; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + + if (rootCause != throwable && rootCause.getMessage() != null && !rootCause.getMessage().isBlank()) { + String rootMessage = rootCause.getMessage().trim(); + if (!builder.toString().contains(rootMessage)) { + if (!builder.isEmpty()) { + builder.append(" | rootCause="); + } + builder.append(rootMessage); + } + } + + return builder.isEmpty() ? TASK_ERROR_MESSAGE_FALLBACK : builder.toString(); + } } diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml index 76a5a42d..4c0d8af8 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml @@ -924,7 +924,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.applicant_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci where IFNULL(pt.actual_amount, 0) > 100000 union select distinct @@ -935,7 +935,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.purchase_leader_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci where pt.purchase_leader_id is not null and IFNULL(pt.actual_amount, 0) > 100000 ) t @@ -975,7 +975,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.applicant_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci where IFNULL(pt.actual_amount, 0) > 0 and IFNULL(pt.supplier_name, '') <> '' @@ -989,7 +989,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.purchase_leader_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci where pt.purchase_leader_id is not null and IFNULL(pt.actual_amount, 0) > 0 and IFNULL(pt.supplier_name, '') <> '' @@ -1006,7 +1006,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.applicant_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci where IFNULL(pt.actual_amount, 0) > 0 union @@ -1018,7 +1018,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from ccdi_purchase_transaction pt inner join ( - ) project_staff on project_staff.staffId = pt.purchase_leader_id + ) project_staff on project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci where pt.purchase_leader_id is not null and IFNULL(pt.actual_amount, 0) > 0 ) source_total diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java index 745c1372..d92fecd5 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java @@ -148,6 +148,34 @@ class CcdiBankTagAnalysisMapperXmlTest { ); } + @Test + void largePurchaseTransactionRule_shouldAlignCollationForJoinFields() throws Exception { + String xml = readXml(RESOURCE); + String purchaseSelectSql = extractSelectSql(xml, "selectLargePurchaseTransactionStatements"); + String supplierSelectSql = extractSelectSql(xml, "selectSupplierConcentrationObjects"); + + assertTrue( + purchaseSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci") + ); + assertTrue( + purchaseSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci") + ); + assertTrue( + supplierSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.applicant_id COLLATE utf8mb4_general_ci") + ); + assertTrue( + supplierSelectSql.contains("project_staff.staffId COLLATE utf8mb4_general_ci = pt.purchase_leader_id COLLATE utf8mb4_general_ci") + ); + assertTrue( + !xml.contains("project_staff.staffId = pt.applicant_id"), + "采购交易相关 join 不应再使用未声明 COLLATE 的 applicant_id 比较" + ); + assertTrue( + !xml.contains("project_staff.staffId = pt.purchase_leader_id"), + "采购交易相关 join 不应再使用未声明 COLLATE 的 purchase_leader_id 比较" + ); + } + @Test void assetRegistrationMismatchRules_shouldUseRealSqlAndAssetTable() throws Exception { String xml = readXml(RESOURCE); diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java index eeb201a0..0ecf92f8 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java @@ -28,9 +28,11 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.math.BigDecimal; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -494,6 +496,23 @@ class CcdiBankTagServiceImplTest { ))); } + @Test + void buildSafeTaskErrorMessage_shouldKeepLongMessageForLongTextColumn() throws Exception { + Method method = CcdiBankTagServiceImpl.class.getDeclaredMethod( + "buildSafeTaskErrorMessage", Throwable.class + ); + method.setAccessible(true); + + String longMessage = "X".repeat(5000); + RuntimeException throwable = new RuntimeException("root-cause:" + longMessage); + + String result = (String) method.invoke(null, throwable); + + assertNotNull(result); + assertTrue(result.length() > 2000, "LONGTEXT 方案下不应继续把错误信息截断到 2000"); + assertTrue(result.contains("root-cause"), "错误信息应保留根因关键字"); + } + @Test void abnormalAccountMapperXml_shouldDeclareObjectSelects() throws Exception { String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml")); diff --git a/docs/plans/backend/2026-04-17-intermediary-library-refactor-backend-implementation.md b/docs/plans/backend/2026-04-17-intermediary-library-refactor-backend-implementation.md index f2ecfccb..613db21d 100644 --- a/docs/plans/backend/2026-04-17-intermediary-library-refactor-backend-implementation.md +++ b/docs/plans/backend/2026-04-17-intermediary-library-refactor-backend-implementation.md @@ -115,7 +115,7 @@ CREATE TABLE IF NOT EXISTS `ccdi_intermediary_enterprise_relation` ( UNIQUE KEY `uk_intermediary_enterprise` (`intermediary_biz_id`, `social_credit_code`), KEY `idx_intermediary_biz_id` (`intermediary_biz_id`), KEY `idx_social_credit_code` (`social_credit_code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中介关联机构关系表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='中介关联机构关系表'; ``` - [ ] **Step 3: 编写 `person_sub_type` 固定值脚本** @@ -529,6 +529,20 @@ mvn -pl ccdi-info-collection -am -DskipTests compile bin/mysql_utf8_exec.sh sql/migration/2026-04-17-create-intermediary-enterprise-relation.sql ``` +## 执行结果 + +- 实际测试命令:`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest test` +- 测试结果:`BUILD SUCCESS`,共执行 8 个测试,`Failures: 0, Errors: 0, Skipped: 0` +- 实际编译命令:`mvn -pl ccdi-info-collection -am -DskipTests compile` +- 编译结果:`BUILD SUCCESS` +- 实际数据库变更命令:`bin/mysql_utf8_exec.sh sql/migration/2026-04-17-create-intermediary-enterprise-relation.sql` +- 数据库变更结果:`ccdi_intermediary_enterprise_relation` 已创建成功 +- 实际全库排序规则修复命令:`bin/mysql_utf8_exec.sh sql/migration/2026-04-17-unify-all-table-collation-to-utf8mb4-general-ci.sql` +- 排序规则修复结果:业务表、系统表的表级与字符字段级排序规则已统一为 `utf8mb4_general_ci` +- 实际运行验证: + - 重新打包命令:`mvn -pl ruoyi-admin -am -DskipTests package` + - 接口验证:`POST /login/test` 获取 token 后,`GET /ccdi/intermediary/list?pageNum=1&pageSize=10` 返回 `code=200`,联合查询不再出现 `Illegal mix of collations` + ## 完成标准 - `ccdi_intermediary_enterprise_relation` 表与唯一约束创建完成 diff --git a/docs/plans/backend/2026-04-20-docker-bank-tag-stuck-backend-implementation.md b/docs/plans/backend/2026-04-20-docker-bank-tag-stuck-backend-implementation.md new file mode 100644 index 00000000..2032d22d --- /dev/null +++ b/docs/plans/backend/2026-04-20-docker-bank-tag-stuck-backend-implementation.md @@ -0,0 +1,65 @@ +# Docker 环境项目打标卡住后端实施记录 + +## 背景 + +- 现象:Docker 部署后的后端中,项目打标任务会长时间停留在“打标中”。 +- 样本项目:`90337` +- 排查目标:确认任务卡住的真实根因,并按最短路径修复。 + +## 已定位问题 + +### 1. 打标规则 SQL 在 MySQL 8 / Docker 环境触发排序规则冲突 + +- 规则:`LARGE_PURCHASE_TRANSACTION` +- 异常:`Illegal mix of collations (utf8mb4_0900_ai_ci) and (utf8mb4_general_ci)` +- 影响:任务在规则执行阶段直接失败。 + +### 2. 失败落库时错误信息过长,导致任务状态无法更新 + +- 表:`ccdi_bank_tag_task` +- 字段:`error_message` +- 异常:`Data too long for column 'error_message'` +- 影响: + - 任务本应更新为 `FAILED`,但更新再次失败 + - 项目状态没有从 `打标中` 回退 + - 前端看到的就是“打标一直卡住” + +## 本次改动范围 + +- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml` + - 为 `LARGE_PURCHASE_TRANSACTION` 相关 join 显式补齐统一排序规则。 +- `sql/migration/2026-04-20-fix-bank-tag-task-error-message-longtext.sql` + - 将 `ccdi_bank_tag_task` 表统一到 `utf8mb4_general_ci`,并把 `error_message` 调整为 `LONGTEXT`。 +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java` + - 调整任务失败错误信息拼装逻辑,保留完整根因,不再按 2000 截断。 +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java` + - 新增错误信息裁剪测试。 +- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java` + - 新增 SQL 排序规则约束测试。 + +## 验证 + +- 运行新增测试,先确认失败,再完成修复后确认通过。 +- 修复后检查项目 `90337` 的任务状态是否能正确进入失败态或完成态,不再停留在 `RUNNING`。 + +## 实际执行结果 + +- 已执行数据库脚本: + - `sql/migration/2026-04-20-fix-bank-tag-task-error-message-longtext.sql` +- 已重新打包并重启 Docker 后端容器。 +- 已手工复位历史卡死任务: + - `ccdi_bank_tag_task.id = 76` +- 已重新触发项目 `90337` 打标。 +- 最新验证结果: + - `ccdi_bank_tag_task.id = 79` + - `status = SUCCESS` + - `success_rule_count = 35` + - `hit_count = 132` + - 项目 `90337` 状态已回到 `1 = 已完成` + +## 结论 + +- “打标卡住”并非任务一直运行,而是: + 1. 采购相关规则 SQL 因排序规则冲突失败 + 2. 失败异常写入 `ccdi_bank_tag_task.error_message` 时又因字段长度不足再次失败 +- 本次已按数据库方案改为 `LONGTEXT`,并将采购链路所有相关 join 显式统一为 `utf8mb4_general_ci`。 diff --git a/docs/plans/frontend/2026-04-17-intermediary-library-refactor-frontend-implementation.md b/docs/plans/frontend/2026-04-17-intermediary-library-refactor-frontend-implementation.md index e342e47e..8dc8c221 100644 --- a/docs/plans/frontend/2026-04-17-intermediary-library-refactor-frontend-implementation.md +++ b/docs/plans/frontend/2026-04-17-intermediary-library-refactor-frontend-implementation.md @@ -459,6 +459,15 @@ source ~/.nvm/nvm.sh && nvm use 14.21.3 npm run build:prod ``` +## 执行结果 + +- 实际执行命令:`cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod` +- Node 版本:`v14.21.3` +- 构建结果:`DONE Build complete. The dist directory is ready to be deployed.` +- 备注:仅存在既有包体积告警(asset size / entrypoint size limit),无语法错误和模块解析错误 +- 补充修复:中介库首页“查看中介亲属”弹窗已改为只读模式,查看态不再允许编辑或提交 +- 补充修复:中介库首页“查看关联机构”弹窗已改为只读模式,查看态不再允许编辑或提交 + ## 完成标准 - 首页搜索字段调整为名称、证件号、记录类型、关联中介信息 diff --git a/ruoyi-ui/src/api/ccdiIntermediary.js b/ruoyi-ui/src/api/ccdiIntermediary.js index 7edb58c0..f6252390 100644 --- a/ruoyi-ui/src/api/ccdiIntermediary.js +++ b/ruoyi-ui/src/api/ccdiIntermediary.js @@ -43,15 +43,6 @@ export function addEntityIntermediary(data) { }) } -// 修改中介黑名单 -export function updateIntermediary(data) { - return request({ - url: '/ccdi/intermediary', - method: 'put', - data: data - }) -} - // 修改个人中介黑名单 export function updatePersonIntermediary(data) { return request({ @@ -78,6 +69,90 @@ export function delIntermediary(intermediaryIds) { }) } +// 查询中介亲属列表 +export function listIntermediaryRelatives(bizId) { + return request({ + url: '/ccdi/intermediary/' + bizId + '/relatives', + method: 'get' + }) +} + +// 查询中介亲属详情 +export function getIntermediaryRelative(relativeBizId) { + return request({ + url: '/ccdi/intermediary/relative/' + relativeBizId, + method: 'get' + }) +} + +// 新增中介亲属 +export function addIntermediaryRelative(bizId, data) { + return request({ + url: '/ccdi/intermediary/' + bizId + '/relative', + method: 'post', + data: data + }) +} + +// 修改中介亲属 +export function updateIntermediaryRelative(data) { + return request({ + url: '/ccdi/intermediary/relative', + method: 'put', + data: data + }) +} + +// 删除中介亲属 +export function delIntermediaryRelative(relativeBizId) { + return request({ + url: '/ccdi/intermediary/relative/' + relativeBizId, + method: 'delete' + }) +} + +// 查询中介关联机构列表 +export function listIntermediaryEnterpriseRelations(bizId) { + return request({ + url: '/ccdi/intermediary/' + bizId + '/enterprise-relations', + method: 'get' + }) +} + +// 查询中介关联机构详情 +export function getIntermediaryEnterpriseRelation(id) { + return request({ + url: '/ccdi/intermediary/enterprise-relation/' + id, + method: 'get' + }) +} + +// 新增中介关联机构 +export function addIntermediaryEnterpriseRelation(bizId, data) { + return request({ + url: '/ccdi/intermediary/' + bizId + '/enterprise-relation', + method: 'post', + data: data + }) +} + +// 修改中介关联机构 +export function updateIntermediaryEnterpriseRelation(data) { + return request({ + url: '/ccdi/intermediary/enterprise-relation', + method: 'put', + data: data + }) +} + +// 删除中介关联机构 +export function delIntermediaryEnterpriseRelation(id) { + return request({ + url: '/ccdi/intermediary/enterprise-relation/' + id, + method: 'delete' + }) +} + // 下载导入模板(已废弃,保留以兼容旧代码) export function importTemplate() { return request({ diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue index 2dd53758..360760b5 100644 --- a/ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue @@ -2,34 +2,29 @@
- - - - - + + + + - + @@ -80,12 +69,49 @@ export default { detailData: { type: Object, default: () => ({}) + }, + relativeList: { + type: Array, + default: () => [] + }, + enterpriseRelationList: { + type: Array, + default: () => [] } }, + data() { + return { + dialogVisible: false + }; + }, watch: { - visible(val) { + visible: { + immediate: true, + handler(val) { + this.dialogVisible = val; + } + }, + dialogVisible(val) { this.$emit("update:visible", val); } + }, + methods: { + formatGender(gender) { + if (gender === "M") return "男"; + if (gender === "F") return "女"; + if (gender === "O") return "其他"; + return gender || "-"; + } } }; + + diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue index df4a1a20..12c27f4d 100644 --- a/ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue @@ -1,314 +1,104 @@ @@ -41,6 +60,10 @@ export default { ownerName: { type: String, default: "" + }, + readonly: { + type: Boolean, + default: false } }, data() { @@ -72,6 +95,9 @@ export default { }, methods: { handleSubmit() { + if (this.readonly) { + return; + } this.$refs.formRef.validate(valid => { if (valid) { this.$emit("submit", { ...this.localForm }); diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue index f4a82cd9..1fac95c9 100644 --- a/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/RelativeEditDialog.vue @@ -9,7 +9,7 @@ - + - + - + - + - + - + - + - + - + - + - + @@ -124,6 +124,10 @@ export default { type: String, default: "" }, + readonly: { + type: Boolean, + default: false + }, relationTypeOptions: { type: Array, default: () => [] @@ -177,6 +181,9 @@ export default { }, methods: { handleSubmit() { + if (this.readonly) { + return; + } this.$refs.formRef.validate(valid => { if (valid) { this.$emit("submit", { ...this.localForm }); diff --git a/ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue b/ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue index 06a2765e..912758d3 100644 --- a/ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue +++ b/ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue @@ -1,11 +1,11 @@