实现突然销户打标规则

This commit is contained in:
wkc
2026-03-31 16:31:58 +08:00
parent 127a59bf78
commit a3f49dc176
3 changed files with 133 additions and 7 deletions

View File

@@ -1211,6 +1211,60 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
) t
</select>
<select id="selectSuddenAccountClosureObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
t.objectKey AS objectKey,
CONCAT(
'账户', t.accountNo,
'于', DATE_FORMAT(t.invalidDate, '%Y-%m-%d'),
'销户销户前30天内最后交易日', DATE_FORMAT(t.lastTxDate, '%Y-%m-%d'),
',累计交易金额', 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.invalid_date AS invalidDate,
max(tx.txDate) AS lastTxDate,
round(sum(tx.tradeAmount), 2) AS windowTotalAmount,
round(max(tx.tradeAmount), 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,
GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS tradeAmount
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 = 2
and ai.invalid_date is not null
and tx.txDate >= DATE_SUB(ai.invalid_date, INTERVAL 30 DAY)
and tx.txDate &lt; ai.invalid_date
group by staff.id_card, ai.account_no, ai.invalid_date
) t
</select>
<select id="selectDormantAccountLargeActivationObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectLargeStockTradingStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,

View File

@@ -28,6 +28,8 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -438,6 +440,68 @@ class CcdiBankTagServiceImplTest {
verify(analysisMapper).selectDormantAccountLargeActivationObjects(40L);
}
@Test
void rebuildProject_shouldInsertSuddenAccountClosureObjectResults() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
"SUDDEN_ACCOUNT_CLOSURE", "突然销户", "OBJECT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
BankTagObjectHitVO hit = new BankTagObjectHitVO();
hit.setObjectType("STAFF_ID_CARD");
hit.setObjectKey("330101199001011234");
hit.setReasonDetail("账户62220001于2026-03-15销户销户前30天内最后交易日2026-03-10累计交易金额120000元单笔最大金额80000元");
when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectSuddenAccountClosureObjects(40L)).thenReturn(List.of(hit));
service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
"ABNORMAL_ACCOUNT".equals(item.getModelCode())
&& "SUDDEN_ACCOUNT_CLOSURE".equals(item.getRuleCode())
&& "OBJECT".equals(item.getResultType())
&& "STAFF_ID_CARD".equals(item.getObjectType())
)));
}
@Test
void rebuildProject_shouldInsertDormantAccountLargeActivationObjectResults() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("ABNORMAL_ACCOUNT", "异常账户",
"DORMANT_ACCOUNT_LARGE_ACTIVATION", "休眠账户大额启用", "OBJECT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
BankTagObjectHitVO hit = new BankTagObjectHitVO();
hit.setObjectType("STAFF_ID_CARD");
hit.setObjectKey("330101199001011235");
hit.setReasonDetail("账户62220002开户于2025-01-01首次交易日期2025-08-01沉睡时长7个月启用后累计交易金额500000元单笔最大金额120000元");
when(ruleMapper.selectEnabledRules("ABNORMAL_ACCOUNT")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectDormantAccountLargeActivationObjects(40L)).thenReturn(List.of(hit));
service.rebuildProject(40L, "ABNORMAL_ACCOUNT", "admin", TriggerType.MANUAL);
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
"ABNORMAL_ACCOUNT".equals(item.getModelCode())
&& "DORMANT_ACCOUNT_LARGE_ACTIVATION".equals(item.getRuleCode())
&& "OBJECT".equals(item.getResultType())
&& "STAFF_ID_CARD".equals(item.getObjectType())
)));
}
@Test
void abnormalAccountMapperXml_shouldDeclareObjectSelects() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml"));
assertTrue(xml.contains("select id=\"selectSuddenAccountClosureObjects\""));
assertTrue(xml.contains("select id=\"selectDormantAccountLargeActivationObjects\""));
}
private CcdiBankTagRule buildRule(String modelCode, String modelName, String ruleCode, String ruleName, String resultType) {
CcdiBankTagRule rule = new CcdiBankTagRule();
rule.setModelCode(modelCode);

View File

@@ -12,6 +12,7 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectOverviewEmployeeResultBuilderTest {
@@ -38,7 +39,11 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"SUSPICIOUS_PART_TIME", "可疑兼职", "MONTHLY_FIXED_INCOME", "疑似兼职", "MEDIUM"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"SUSPICIOUS_PROPERTY", "可疑财产", "HOUSE_REGISTRATION_MISMATCH", "房产登记不匹配", "LOW")
"SUSPICIOUS_PROPERTY", "可疑财产", "HOUSE_REGISTRATION_MISMATCH", "房产登记不匹配", "LOW"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"ABNORMAL_ACCOUNT", "异常账户", "SUDDEN_ACCOUNT_CLOSURE", "突然销户", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"ABNORMAL_ACCOUNT", "异常账户", "DORMANT_ACCOUNT_LARGE_ACTIVATION", "休眠账户大额启用", "HIGH")
);
List<CcdiProjectOverviewEmployeeResult> results =
@@ -52,20 +57,22 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
assertEquals("E1001", result.getStaffCode());
assertEquals(12L, result.getDeptId());
assertEquals("信息二部", result.getDeptName());
assertEquals(5, result.getRuleCount());
assertEquals(4, result.getModelCount());
assertEquals(7, result.getHitCount());
assertEquals(7, result.getRuleCount());
assertEquals(5, result.getModelCount());
assertEquals(9, result.getHitCount());
assertEquals("HIGH", result.getRiskLevelCode());
assertEquals("ABNORMAL_TRANSACTION,LARGE_TRANSACTION,SUSPICIOUS_PART_TIME,SUSPICIOUS_PROPERTY",
assertEquals("ABNORMAL_ACCOUNT,ABNORMAL_TRANSACTION,LARGE_TRANSACTION,SUSPICIOUS_PART_TIME,SUSPICIOUS_PROPERTY",
result.getModelCodesCsv());
assertNotNull(result.getRiskPoint());
JSONArray modelNames = JSON.parseArray(result.getModelNamesJson());
assertEquals(List.of("异常交易", "大额交易", "可疑兼职", "可疑财产"),
assertEquals(List.of("异常账户", "异常交易", "大额交易", "可疑兼职", "可疑财产"),
modelNames.toList(String.class));
JSONArray hitRules = JSON.parseArray(result.getHitRulesJson());
assertEquals(5, hitRules.size());
assertEquals(7, hitRules.size());
assertTrue(result.getHitRulesJson().contains("SUDDEN_ACCOUNT_CLOSURE"));
assertTrue(result.getHitRulesJson().contains("DORMANT_ACCOUNT_LARGE_ACTIVATION"));
JSONObject firstRule = hitRules.getJSONObject(0);
assertEquals("ABNORMAL_CUSTOMER_TRANSACTION", firstRule.getString("ruleCode"));
assertEquals("异常客户交易", firstRule.getString("ruleName"));
@@ -78,6 +85,7 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
item -> item.getString("modelCode"),
item -> item.getIntValue("warningCount")
));
assertEquals(2, warningCountByModel.get("ABNORMAL_ACCOUNT"));
assertEquals(2, warningCountByModel.get("ABNORMAL_TRANSACTION"));
assertEquals(3, warningCountByModel.get("LARGE_TRANSACTION"));
assertEquals(1, warningCountByModel.get("SUSPICIOUS_PROPERTY"));