完善异常账户预警口径和账户查询
This commit is contained in:
@@ -39,6 +39,10 @@ public class CcdiAccountInfoQueryDTO implements Serializable {
|
||||
@Schema(description = "账户姓名")
|
||||
private String accountName;
|
||||
|
||||
/** 账户号码 */
|
||||
@Schema(description = "账户号码")
|
||||
private String accountNo;
|
||||
|
||||
/** 账户类型 */
|
||||
@Schema(description = "账户类型")
|
||||
private String accountType;
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
<if test="query.accountName != null and query.accountName != ''">
|
||||
AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%')
|
||||
</if>
|
||||
<if test="query.accountNo != null and query.accountNo != ''">
|
||||
AND ai.account_no LIKE CONCAT('%', #{query.accountNo}, '%')
|
||||
</if>
|
||||
<if test="query.accountType != null and query.accountType != ''">
|
||||
AND ai.account_type = #{query.accountType}
|
||||
</if>
|
||||
|
||||
@@ -38,6 +38,18 @@ class CcdiAccountInfoMapperTest {
|
||||
assertTrue(sql.contains("ai.owner_type <> 'credit_customer'"), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAccountInfoPage_shouldFilterByAccountNo() throws Exception {
|
||||
MappedStatement mappedStatement = loadMappedStatement(
|
||||
"com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper.selectAccountInfoPage");
|
||||
CcdiAccountInfoQueryDTO queryDTO = new CcdiAccountInfoQueryDTO();
|
||||
queryDTO.setAccountNo("6222");
|
||||
|
||||
String sql = renderSql(mappedStatement, Map.of("query", queryDTO)).toLowerCase();
|
||||
|
||||
assertTrue(sql.contains("ai.account_no like concat('%', ?, '%')"), sql);
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.domain.excel;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -26,4 +27,10 @@ public class CcdiProjectAbnormalAccountExcel {
|
||||
|
||||
@Excel(name = "状态")
|
||||
private String status;
|
||||
|
||||
@Excel(name = "命中原因")
|
||||
private String reasonDetail;
|
||||
|
||||
@Excel(name = "涉及金额")
|
||||
private BigDecimal involvedAmount;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -19,4 +20,8 @@ public class CcdiProjectAbnormalAccountItemVO {
|
||||
private String abnormalTime;
|
||||
|
||||
private String status;
|
||||
|
||||
private String reasonDetail;
|
||||
|
||||
private BigDecimal involvedAmount;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
|
||||
writer.subsection("3. 异常账户信息表(共" + report.getAbnormalAccounts().size() + "条)");
|
||||
writer.table(
|
||||
List.of("账号", "开户人", "银行", "异常类型", "异常发生时间", "状态"),
|
||||
List.of("账号", "开户人", "银行", "异常类型", "异常发生时间", "状态", "命中原因", "涉及金额"),
|
||||
report.getAbnormalAccounts().stream()
|
||||
.map(item -> List.of(
|
||||
maskAccount(item.getAccountNo()),
|
||||
@@ -238,10 +238,12 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
safeText(item.getBankName()),
|
||||
safeText(item.getAbnormalType()),
|
||||
safeText(item.getAbnormalTime()),
|
||||
safeText(item.getStatus())
|
||||
safeText(item.getStatus()),
|
||||
safeText(item.getReasonDetail()),
|
||||
formatMoney(item.getInvolvedAmount())
|
||||
))
|
||||
.collect(Collectors.toList()),
|
||||
new float[] { 0.18F, 0.13F, 0.2F, 0.23F, 0.14F, 0.12F },
|
||||
new float[] { 0.12F, 0.1F, 0.14F, 0.14F, 0.11F, 0.08F, 0.22F, 0.09F },
|
||||
"暂无异常账户信息"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -905,6 +905,8 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
row.setAbnormalType(item.getAbnormalType());
|
||||
row.setAbnormalTime(item.getAbnormalTime());
|
||||
row.setStatus(item.getStatus());
|
||||
row.setReasonDetail(item.getReasonDetail());
|
||||
row.setInvolvedAmount(item.getInvolvedAmount());
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ public class CcdiProjectRiskDetailWorkbookExporter {
|
||||
|
||||
private void writeAbnormalAccountSheet(Sheet sheet, List<CcdiProjectAbnormalAccountExcel> rows) {
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态" };
|
||||
String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态", "命中原因", "涉及金额" };
|
||||
writeHeader(header, headers);
|
||||
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
@@ -118,6 +118,8 @@ public class CcdiProjectRiskDetailWorkbookExporter {
|
||||
row.createCell(3).setCellValue(safeText(item.getAbnormalType()));
|
||||
row.createCell(4).setCellValue(safeText(item.getAbnormalTime()));
|
||||
row.createCell(5).setCellValue(safeText(item.getStatus()));
|
||||
row.createCell(6).setCellValue(safeText(item.getReasonDetail()));
|
||||
row.createCell(7).setCellValue(safeNumber(item.getInvolvedAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1730,6 +1730,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and tx.txDate < ai.invalid_date
|
||||
group by staff.id_card, ai.account_no, ai.invalid_date
|
||||
) t
|
||||
where t.windowTotalAmount >= 500000
|
||||
or t.windowMaxSingleAmount >= 100000
|
||||
</select>
|
||||
|
||||
<select id="selectDormantAccountLargeActivationObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -1750,6 +1752,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
staff.id_card AS objectKey,
|
||||
ai.account_no AS accountNo,
|
||||
ai.effective_date AS effectiveDate,
|
||||
project_window.projectStartDate AS projectStartDate,
|
||||
min(tx.txDate) AS firstTxDate,
|
||||
timestampdiff(MONTH, ai.effective_date, min(tx.txDate)) AS dormantMonths,
|
||||
round(sum(tx.tradeTotalAmount), 2) AS windowTotalAmount,
|
||||
@@ -1757,6 +1760,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_account_info ai
|
||||
inner join ccdi_base_staff staff
|
||||
on staff.id_card = ai.owner_id
|
||||
inner join (
|
||||
select
|
||||
min(COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(project_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(project_bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
)) AS projectStartDate
|
||||
from ccdi_bank_statement project_bs
|
||||
where project_bs.project_id = #{projectId}
|
||||
) project_window
|
||||
inner join (
|
||||
select
|
||||
trim(bs.LE_ACCOUNT_NO) AS accountNo,
|
||||
@@ -1774,8 +1786,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
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
|
||||
and project_window.projectStartDate is not null
|
||||
group by staff.id_card, ai.account_no, ai.effective_date, project_window.projectStartDate
|
||||
having min(tx.txDate) >= DATE_ADD(ai.effective_date, INTERVAL 6 MONTH)
|
||||
and project_window.projectStartDate <= DATE_SUB(min(tx.txDate), INTERVAL 6 MONTH)
|
||||
) t
|
||||
where t.windowTotalAmount >= 500000
|
||||
or t.windowMaxSingleAmount >= 100000
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
<result property="abnormalType" column="abnormalType"/>
|
||||
<result property="abnormalTime" column="abnormal_time"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="reasonDetail" column="reasonDetail"/>
|
||||
<result property="involvedAmount" column="involved_amount"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="ReportUploadSubjectResultMap"
|
||||
@@ -1473,7 +1475,12 @@
|
||||
when account.status = 1 then '正常'
|
||||
when account.status = 2 then '已销户'
|
||||
else cast(account.status as char)
|
||||
end as status
|
||||
end as status,
|
||||
tr.reason_detail as reasonDetail,
|
||||
greatest(
|
||||
ifnull(cast(substring_index(substring_index(tr.reason_detail, '累计交易金额', -1), '元', 1) as decimal(18, 2)), 0),
|
||||
ifnull(cast(substring_index(substring_index(tr.reason_detail, '单笔最大金额', -1), '元', 1) as decimal(18, 2)), 0)
|
||||
) as involved_amount
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
inner join ccdi_account_info account
|
||||
on account.owner_type = 'EMPLOYEE'
|
||||
@@ -1500,7 +1507,9 @@
|
||||
abnormal.bankName,
|
||||
abnormal.abnormalType,
|
||||
abnormal.abnormal_time,
|
||||
abnormal.status
|
||||
abnormal.status,
|
||||
abnormal.reasonDetail,
|
||||
abnormal.involved_amount
|
||||
from (
|
||||
<include refid="abnormalAccountBaseSql"/>
|
||||
where tr.project_id = #{query.projectId}
|
||||
@@ -1523,7 +1532,9 @@
|
||||
abnormal.bankName,
|
||||
abnormal.abnormalType,
|
||||
abnormal.abnormal_time,
|
||||
abnormal.status
|
||||
abnormal.status,
|
||||
abnormal.reasonDetail,
|
||||
abnormal.involved_amount
|
||||
from (
|
||||
<include refid="abnormalAccountBaseSql"/>
|
||||
where tr.project_id = #{projectId}
|
||||
|
||||
@@ -72,6 +72,26 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
assertTrue(xml.contains("AS logId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dormantAccountLargeActivation_shouldRequireCompleteObservationWindow() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectDormantAccountLargeActivationObjects");
|
||||
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate"), selectSql);
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate is not null"), selectSql);
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate <= DATE_SUB(min(tx.txDate), INTERVAL 6 MONTH)"), selectSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void suddenAccountClosure_shouldRequireLargeTransactionInClosureWindow() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectSuddenAccountClosureObjects");
|
||||
|
||||
assertTrue(selectSql.contains("tx.txDate >= DATE_SUB(ai.invalid_date, INTERVAL 30 DAY)"), selectSql);
|
||||
assertTrue(selectSql.contains("where t.windowTotalAmount >= 500000"), selectSql);
|
||||
assertTrue(selectSql.contains("or t.windowMaxSingleAmount >= 100000"), selectSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void houseOrCarExpenseRule_shouldJoinBankStatementAndReturnStatementHitFields() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
|
||||
@@ -211,6 +211,8 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(abnormalPageSql.contains("tr.bank_statement_id is null"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("account.owner_type = 'EMPLOYEE'"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("tr.reason_detail"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("abnormal.reasonDetail"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("abnormal.involved_amount"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("instr(tr.reason_detail, account.account_no) > 0"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("when account.status = 1 then '正常'"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("when account.status = 2 then '已销户'"), abnormalPageSql);
|
||||
@@ -225,10 +227,15 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(abnormalExportSql.contains("tr.bank_statement_id is null"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("account.owner_type = 'EMPLOYEE'"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("tr.reason_detail"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("abnormal.reasonDetail"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("abnormal.involved_amount"), abnormalExportSql);
|
||||
assertTrue(
|
||||
abnormalExportSql.contains("order by abnormal_time desc, account.account_no asc, tr.rule_code asc"),
|
||||
abnormalExportSql
|
||||
);
|
||||
|
||||
assertTrue(xml.contains("tr.reason_detail as reasonDetail"), xml);
|
||||
assertTrue(xml.contains("as involved_amount"), xml);
|
||||
}
|
||||
|
||||
private String extractSelect(String xml, String selectId) {
|
||||
|
||||
@@ -163,6 +163,9 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
|
||||
private String resolveTestFontPath() {
|
||||
List<String> candidates = List.of(
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc",
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||
@@ -325,6 +328,8 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
row.setAbnormalType("突然销户");
|
||||
row.setAbnormalTime("2026-03-20");
|
||||
row.setStatus("已销户");
|
||||
row.setReasonDetail("账户6222000000000003销户前存在异常交易");
|
||||
row.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -65,6 +66,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
item.setAbnormalType("突然销户");
|
||||
item.setAbnormalTime("2026-03-20");
|
||||
item.setStatus("已销户");
|
||||
item.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
item.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
|
||||
Page<CcdiProjectAbnormalAccountItemVO> resultPage = new Page<>(1, 5);
|
||||
resultPage.setRecords(List.of(item));
|
||||
@@ -78,6 +81,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
assertEquals(1L, result.getTotal());
|
||||
assertEquals("6222000000000001", result.getRows().getFirst().getAccountNo());
|
||||
assertEquals("突然销户", result.getRows().getFirst().getAbnormalType());
|
||||
assertEquals("账户6222000000000001销户前存在异常交易", result.getRows().getFirst().getReasonDetail());
|
||||
assertEquals(new BigDecimal("120000.00"), result.getRows().getFirst().getInvolvedAmount());
|
||||
verify(overviewMapper).selectAbnormalAccountPage(
|
||||
argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
|
||||
argThat(query -> query.getProjectId().equals(40L))
|
||||
@@ -115,6 +120,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
item.setAbnormalType("休眠账户大额启用");
|
||||
item.setAbnormalTime("2025-08-01");
|
||||
item.setStatus("正常");
|
||||
item.setReasonDetail("账户6222000000000002休眠后大额启用");
|
||||
item.setInvolvedAmount(new BigDecimal("500000.00"));
|
||||
when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(item));
|
||||
|
||||
List<CcdiProjectAbnormalAccountExcel> rows = service.exportAbnormalAccountPeople(40L);
|
||||
@@ -126,6 +133,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
assertEquals("休眠账户大额启用", rows.getFirst().getAbnormalType());
|
||||
assertEquals("2025-08-01", rows.getFirst().getAbnormalTime());
|
||||
assertEquals("正常", rows.getFirst().getStatus());
|
||||
assertEquals("账户6222000000000002休眠后大额启用", rows.getFirst().getReasonDetail());
|
||||
assertEquals(new BigDecimal("500000.00"), rows.getFirst().getInvolvedAmount());
|
||||
verify(overviewMapper).selectAbnormalAccountList(40L);
|
||||
}
|
||||
|
||||
|
||||
@@ -295,6 +295,8 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
abnormalItem.setAbnormalType("突然销户");
|
||||
abnormalItem.setAbnormalTime("2026-03-20");
|
||||
abnormalItem.setStatus("已销户");
|
||||
abnormalItem.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
abnormalItem.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
@@ -316,7 +318,10 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName())
|
||||
),
|
||||
argThat((List<CcdiProjectAbnormalAccountExcel> rows) ->
|
||||
rows.size() == 1 && "6222000000000001".equals(rows.getFirst().getAccountNo())
|
||||
rows.size() == 1
|
||||
&& "6222000000000001".equals(rows.getFirst().getAccountNo())
|
||||
&& "账户6222000000000001销户前存在异常交易".equals(rows.getFirst().getReasonDetail())
|
||||
&& new BigDecimal("120000.00").equals(rows.getFirst().getInvolvedAmount())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
|
||||
abnormalRow.setAbnormalType("突然销户");
|
||||
abnormalRow.setAbnormalTime("2026-03-20");
|
||||
abnormalRow.setStatus("已销户");
|
||||
abnormalRow.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
abnormalRow.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
|
||||
exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow), List.of(abnormalRow));
|
||||
|
||||
@@ -78,12 +80,16 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
|
||||
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("命中原因", workbook.getSheetAt(2).getRow(0).getCell(6).getStringCellValue());
|
||||
assertEquals("涉及金额", workbook.getSheetAt(2).getRow(0).getCell(7).getStringCellValue());
|
||||
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("账户6222000000000001销户前存在异常交易", workbook.getSheetAt(2).getRow(1).getCell(6).getStringCellValue());
|
||||
assertEquals(120000D, workbook.getSheetAt(2).getRow(1).getCell(7).getNumericCellValue());
|
||||
assertEquals(2, workbook.getSheetAt(2).getPhysicalNumberOfRows());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ class CcdiBankTagRuleSqlMetadataTest {
|
||||
"2026-03-31-create-ccdi-account-info-and-abnormal-account-rules.sql");
|
||||
|
||||
assertAll(
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户已销户,且销户日前30天内仍存在交易记录。"),
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户已销户,且销户日前30天内存在大额资金流动。"),
|
||||
"SUDDEN_ACCOUNT_CLOSURE 应使用设计文档中的业务口径"),
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户开户后长期未使用,首次启用后出现大额资金流动。"),
|
||||
"DORMANT_ACCOUNT_LARGE_ACTIVATION 应使用设计文档中的业务口径"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别员工本人账户销户前30天内仍有交易的员工对象"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别员工本人账户销户前30天内存在大额资金流动的员工对象"),
|
||||
"SUDDEN_ACCOUNT_CLOSURE 应同步真实规则说明"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别长期休眠后首次启用即出现大额资金流动的员工对象"),
|
||||
"DORMANT_ACCOUNT_LARGE_ACTIVATION 应同步真实规则说明")
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 结果总览异常账户口径调整后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
修正“休眠账户大额启用”在项目流水不完整时的误报,并在结果总览异常账户数据中透出命中原因和涉及金额。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 调整 `DORMANT_ACCOUNT_LARGE_ACTIVATION` SQL:项目流水最早日期必须覆盖启用日前 6 个月观察窗口。
|
||||
- 调整 `SUDDEN_ACCOUNT_CLOSURE` SQL:销户前 30 天窗口内需存在大额资金流动,避免普通销户前交易被判定为异常账户线索。
|
||||
- 异常账户分页和导出查询增加 `reasonDetail`、`involvedAmount`。
|
||||
- 风险明细工作簿和 PDF 报告的异常账户表增加“命中原因”“涉及金额”。
|
||||
- 新增规则元数据增量 SQL,同步“突然销户”业务口径。
|
||||
- 更新后端单元测试断言。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不开发非实控账户规则。
|
||||
- 不新增“关联员工”列。
|
||||
- 不新增“销户后交易”校验类场景。
|
||||
|
||||
## 验证
|
||||
|
||||
- 执行异常账户规则、结果总览 Mapper、导出器相关测试。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 账户库账户号码筛选后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在账户库管理列表和导出查询中支持按账户号码筛选,方便业务按账号直接定位账户主档。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- `CcdiAccountInfoQueryDTO` 增加 `accountNo` 查询字段。
|
||||
- `CcdiAccountInfoMapper.xml` 的账户库查询条件增加 `ai.account_no LIKE` 过滤。
|
||||
- 页面列表和导出共用同一查询条件,保持筛选口径一致。
|
||||
- 补充 Mapper SQL 渲染测试,确认账号条件被拼入列表 SQL。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不调整账户主档字段结构。
|
||||
- 不修改账户导入模板。
|
||||
- 不混入项目风险结果或异常账户预警规则。
|
||||
|
||||
## 验证
|
||||
|
||||
- 执行账户库 Mapper 测试,确认 SQL 渲染包含账户号码过滤条件。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 结果总览异常账户展示调整前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在结果总览“异常账户人员信息”表格中展示命中原因和涉及金额,降低业务查看预警时的理解成本。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 异常账户表格增加“命中原因”列。
|
||||
- 异常账户表格增加“涉及金额”列,并复用现有金额格式化方法。
|
||||
- 更新前端结构测试。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不新增异常账户筛选栏。
|
||||
- 不新增详情弹窗。
|
||||
- 不展示“关联员工”列。
|
||||
|
||||
## 验证
|
||||
|
||||
- 执行前端结构测试和生产构建。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 账户库账户号码筛选前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在账户库管理查询栏新增“账户号码”筛选项,支持输入部分账号后查询。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 在账户库查询表单中新增“账户号码”输入框。
|
||||
- 查询参数 `queryParams` 增加 `accountNo`。
|
||||
- 重置查询时同步清空 `accountNo`。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不调整列表列结构。
|
||||
- 不新增高级搜索或独立详情入口。
|
||||
- 不改变新增、编辑、导入的账户号码校验逻辑。
|
||||
|
||||
## 验证
|
||||
|
||||
- 通过前端源码检查确认查询项、请求参数和重置逻辑一致。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 结果总览异常账户口径与展示实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修正 `休眠账户大额启用`:项目流水必须覆盖启用日前完整 6 个月观察窗口,避免仅导入某日起流水时误判为首次启用。
|
||||
- 收紧 `突然销户`:销户前 30 天窗口内需满足累计交易金额 50 万以上或单笔最大金额 10 万以上,避免普通销户前交易被误判为异常账户线索。
|
||||
- 异常账户结果查询新增 `命中原因` 和 `涉及金额`。
|
||||
- 结果总览异常账户表格新增“命中原因”“涉及金额”两列。
|
||||
- 风险明细工作簿和 PDF 报告的异常账户表同步新增两列。
|
||||
- 新增 `sql/migration/2026-07-17-update-abnormal-account-rule-caliber.sql` 同步规则元数据。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响异常账户模型规则 `DORMANT_ACCOUNT_LARGE_ACTIVATION` 的命中口径。
|
||||
- 影响异常账户模型规则 `SUDDEN_ACCOUNT_CLOSURE` 的命中口径。
|
||||
- 影响结果总览异常账户分页接口、统一风险明细导出和 PDF 报告。
|
||||
- 不涉及非实控账户规则,不增加关联员工展示。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已执行:`mvn -pl ccdi-project -am test "-Dtest=CcdiProjectOverviewServiceAbnormalAccountTest,CcdiProjectOverviewMapperSqlTest,CcdiBankTagAnalysisMapperXmlTest,CcdiProjectRiskDetailWorkbookExporterTest,CcdiProjectOverviewReportPdfExporterTest,CcdiProjectOverviewServiceImplTest,CcdiBankTagRuleSqlMetadataTest" "-Dsurefire.failIfNoSpecifiedTests=false"`。
|
||||
- 结果:通过,相关后端测试共 58 个用例成功。
|
||||
- 已执行:`node tests\unit\risk-detail-abnormal-account-layout.test.js`。
|
||||
- 结果:通过。
|
||||
- 已执行:`npm run build:prod`。
|
||||
- 结果:构建通过,仅存在既有资源体积警告;当前工作区未跟踪的 `0uS5znL.docx` 被构建统计为大资源,未纳入本次处理范围。
|
||||
- 已执行:使用 Chrome DevTools 打开本机真实前端 `http://127.0.0.1:8088/`。
|
||||
- 结果:页面停留登录页,`/dev-api/captchaImage` 返回 500;同时 `http://127.0.0.1:62318/swagger-ui.html` 无法连接,确认真实后端不可用,因此未进入结果总览业务页做端到端验收。本次未使用 mock 后端替代真实后端。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 账户库账户号码筛选实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端账户库查询 DTO 新增 `accountNo` 字段。
|
||||
- 账户库列表和导出 SQL 增加 `ai.account_no LIKE` 查询条件。
|
||||
- 前端账户库管理查询栏新增“账户号码”输入框,并在重置查询时清空。
|
||||
- 新增 Mapper SQL 渲染测试,覆盖账户号码筛选条件。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响账户库管理列表查询接口 `/ccdi/accountInfo/list`。
|
||||
- 影响账户库导出接口复用的查询条件。
|
||||
- 不影响账户新增、编辑、删除、导入和异常账户预警规则。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已执行:`mvn -pl ccdi-info-collection -am test "-Dtest=CcdiAccountInfoMapperTest" "-Dsurefire.failIfNoSpecifiedTests=false"`。
|
||||
- 结果:通过,`CcdiAccountInfoMapperTest` 共 2 个用例成功。
|
||||
- 已执行:前端环境验证。`.nvmrc` 指向的 Node 14.21.3 在本机切换后 `node`、`npm` 不可用,按仓库约定改用 Node 22.22.3 + npm 10.9.8。
|
||||
- 已执行:`npm run build:prod`,构建通过,仅存在既有资源体积警告;当前工作区未跟踪的 `0uS5znL.docx` 被构建统计为大资源,未纳入本次处理范围。
|
||||
- 已执行:使用 Chrome DevTools 打开临时静态服务页面,登录页可渲染;因静态服务没有 `/prod-api` 后端代理,无法进入需鉴权的账户库业务页,临时静态服务已关闭。
|
||||
@@ -44,6 +44,11 @@
|
||||
<el-input v-model="queryParams.accountName" placeholder="请输入账户姓名" clearable style="width: 100%" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||
<el-form-item label="账户号码">
|
||||
<el-input v-model="queryParams.accountNo" placeholder="请输入账户号码" clearable style="width: 100%" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||
<el-form-item label="账户类型">
|
||||
<el-select v-model="queryParams.accountType" placeholder="请选择账户类型" clearable style="width: 100%">
|
||||
@@ -532,6 +537,7 @@ export default {
|
||||
bankScope: '',
|
||||
relationType: '',
|
||||
accountName: '',
|
||||
accountNo: '',
|
||||
accountType: '',
|
||||
isActualControl: undefined,
|
||||
riskLevel: '',
|
||||
@@ -662,6 +668,7 @@ export default {
|
||||
bankScope: '',
|
||||
relationType: '',
|
||||
accountName: '',
|
||||
accountNo: '',
|
||||
accountType: '',
|
||||
isActualControl: undefined,
|
||||
riskLevel: '',
|
||||
|
||||
@@ -224,6 +224,10 @@
|
||||
<el-table-column prop="abnormalType" label="异常类型" min-width="160" />
|
||||
<el-table-column prop="abnormalTime" label="异常发生时间" min-width="160" />
|
||||
<el-table-column prop="status" label="状态" min-width="120" />
|
||||
<el-table-column prop="reasonDetail" label="命中原因" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column prop="involvedAmount" label="涉及金额" min-width="130" align="right">
|
||||
<template slot-scope="scope">{{ formatAmount(scope.row.involvedAmount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
|
||||
@@ -18,5 +18,7 @@ const source = fs.readFileSync(
|
||||
"异常类型",
|
||||
"异常发生时间",
|
||||
"状态",
|
||||
"命中原因",
|
||||
"涉及金额",
|
||||
"当前项目暂无异常账户人员信息",
|
||||
].forEach((token) => assert(source.includes(token), token));
|
||||
|
||||
@@ -72,7 +72,7 @@ INSERT INTO ccdi_bank_statement (
|
||||
meta_json, no_balance, begin_balance, end_balance, group_id, override_bs_id, payment_method, cret_no
|
||||
) VALUES
|
||||
(90331, 0, 90331011, '测试员工A', '6222000000000001', 20260305, '2026-03-05', '2026-03-05', 'CNY',
|
||||
0.00, 70000.00, 180000.00, '转账', -1, '交易对手A1', NULL, '兰溪农商银行', NULL,
|
||||
0.00, 120000.00, 230000.00, '转账', -1, '交易对手A1', NULL, '兰溪农商银行', NULL,
|
||||
'异常账户测试A-1', NULL, 'A0001', 'LANXI', '0', 0, '', 0, 1, 1, NOW(), 1, NULL, 0, 0, 0, 90331, 0, NULL, '330101199001010001'),
|
||||
(90331, 0, 90331011, '测试员工A', '6222000000000001', 20260310, '2026-03-10', '2026-03-10', 'CNY',
|
||||
50000.00, 0.00, 130000.00, '转账', -1, '交易对手A2', NULL, '兰溪农商银行', NULL,
|
||||
|
||||
@@ -46,8 +46,8 @@ INSERT INTO ccdi_bank_tag_rule (
|
||||
remark
|
||||
) VALUES
|
||||
('ABNORMAL_ACCOUNT', '异常账户', 'SUDDEN_ACCOUNT_CLOSURE', '突然销户', NULL, 'OBJECT', 'HIGH',
|
||||
'员工本人账户已销户,且销户日前30天内仍存在交易记录。', 1, 10, 'system',
|
||||
'真实规则:识别员工本人账户销户前30天内仍有交易的员工对象'),
|
||||
'员工本人账户已销户,且销户日前30天内存在大额资金流动。', 1, 10, 'system',
|
||||
'真实规则:识别员工本人账户销户前30天内存在大额资金流动的员工对象'),
|
||||
('ABNORMAL_ACCOUNT', '异常账户', 'DORMANT_ACCOUNT_LARGE_ACTIVATION', '休眠账户大额启用', NULL, 'OBJECT', 'HIGH',
|
||||
'员工本人账户开户后长期未使用,首次启用后出现大额资金流动。', 1, 20, 'system',
|
||||
'真实规则:识别长期休眠后首次启用即出现大额资金流动的员工对象')
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
UPDATE ccdi_bank_tag_rule
|
||||
SET business_caliber = '员工本人账户已销户,且销户日前30天内存在大额资金流动。',
|
||||
remark = '真实规则:识别员工本人账户销户前30天内存在大额资金流动的员工对象',
|
||||
update_by = 'system',
|
||||
update_time = NOW()
|
||||
WHERE model_code = 'ABNORMAL_ACCOUNT'
|
||||
AND rule_code = 'SUDDEN_ACCOUNT_CLOSURE';
|
||||
Reference in New Issue
Block a user