实现突然销户打标规则
This commit is contained in:
@@ -1211,6 +1211,60 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
) t
|
) t
|
||||||
</select>
|
</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 id="selectLargeStockTradingStatements" resultMap="BankTagStatementHitResultMap">
|
||||||
select
|
select
|
||||||
bs.bank_statement_id AS bankStatementId,
|
bs.bank_statement_id AS bankStatementId,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.math.BigDecimal;
|
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.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -438,6 +440,68 @@ class CcdiBankTagServiceImplTest {
|
|||||||
verify(analysisMapper).selectDormantAccountLargeActivationObjects(40L);
|
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) {
|
private CcdiBankTagRule buildRule(String modelCode, String modelName, String ruleCode, String ruleName, String resultType) {
|
||||||
CcdiBankTagRule rule = new CcdiBankTagRule();
|
CcdiBankTagRule rule = new CcdiBankTagRule();
|
||||||
rule.setModelCode(modelCode);
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
class CcdiProjectOverviewEmployeeResultBuilderTest {
|
class CcdiProjectOverviewEmployeeResultBuilderTest {
|
||||||
|
|
||||||
@@ -38,7 +39,11 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
|
|||||||
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
|
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
|
||||||
"SUSPICIOUS_PART_TIME", "可疑兼职", "MONTHLY_FIXED_INCOME", "疑似兼职", "MEDIUM"),
|
"SUSPICIOUS_PART_TIME", "可疑兼职", "MONTHLY_FIXED_INCOME", "疑似兼职", "MEDIUM"),
|
||||||
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
|
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 =
|
List<CcdiProjectOverviewEmployeeResult> results =
|
||||||
@@ -52,20 +57,22 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
|
|||||||
assertEquals("E1001", result.getStaffCode());
|
assertEquals("E1001", result.getStaffCode());
|
||||||
assertEquals(12L, result.getDeptId());
|
assertEquals(12L, result.getDeptId());
|
||||||
assertEquals("信息二部", result.getDeptName());
|
assertEquals("信息二部", result.getDeptName());
|
||||||
assertEquals(5, result.getRuleCount());
|
assertEquals(7, result.getRuleCount());
|
||||||
assertEquals(4, result.getModelCount());
|
assertEquals(5, result.getModelCount());
|
||||||
assertEquals(7, result.getHitCount());
|
assertEquals(9, result.getHitCount());
|
||||||
assertEquals("HIGH", result.getRiskLevelCode());
|
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());
|
result.getModelCodesCsv());
|
||||||
assertNotNull(result.getRiskPoint());
|
assertNotNull(result.getRiskPoint());
|
||||||
|
|
||||||
JSONArray modelNames = JSON.parseArray(result.getModelNamesJson());
|
JSONArray modelNames = JSON.parseArray(result.getModelNamesJson());
|
||||||
assertEquals(List.of("异常交易", "大额交易", "可疑兼职", "可疑财产"),
|
assertEquals(List.of("异常账户", "异常交易", "大额交易", "可疑兼职", "可疑财产"),
|
||||||
modelNames.toList(String.class));
|
modelNames.toList(String.class));
|
||||||
|
|
||||||
JSONArray hitRules = JSON.parseArray(result.getHitRulesJson());
|
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);
|
JSONObject firstRule = hitRules.getJSONObject(0);
|
||||||
assertEquals("ABNORMAL_CUSTOMER_TRANSACTION", firstRule.getString("ruleCode"));
|
assertEquals("ABNORMAL_CUSTOMER_TRANSACTION", firstRule.getString("ruleCode"));
|
||||||
assertEquals("异常客户交易", firstRule.getString("ruleName"));
|
assertEquals("异常客户交易", firstRule.getString("ruleName"));
|
||||||
@@ -78,6 +85,7 @@ class CcdiProjectOverviewEmployeeResultBuilderTest {
|
|||||||
item -> item.getString("modelCode"),
|
item -> item.getString("modelCode"),
|
||||||
item -> item.getIntValue("warningCount")
|
item -> item.getIntValue("warningCount")
|
||||||
));
|
));
|
||||||
|
assertEquals(2, warningCountByModel.get("ABNORMAL_ACCOUNT"));
|
||||||
assertEquals(2, warningCountByModel.get("ABNORMAL_TRANSACTION"));
|
assertEquals(2, warningCountByModel.get("ABNORMAL_TRANSACTION"));
|
||||||
assertEquals(3, warningCountByModel.get("LARGE_TRANSACTION"));
|
assertEquals(3, warningCountByModel.get("LARGE_TRANSACTION"));
|
||||||
assertEquals(1, warningCountByModel.get("SUSPICIOUS_PROPERTY"));
|
assertEquals(1, warningCountByModel.get("SUSPICIOUS_PROPERTY"));
|
||||||
|
|||||||
Reference in New Issue
Block a user