实现突然销户打标规则
This commit is contained in:
@@ -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 < 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user