From 7a317204f7f0dba02ef1a72f66bede2386014e47 Mon Sep 17 00:00:00 2001
From: wjj <2069666735@qq.com>
Date: Mon, 13 Jul 2026 13:05:35 +0800
Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=B5=81=E6=B0=B4=E6=89=93?=
=?UTF-8?q?=E6=A0=87=E8=A7=84=E5=88=99=E5=8F=A3=E5=BE=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../project/CcdiBankTagAnalysisMapper.xml | 68 +++++++++++++++----
.../CcdiBankTagAnalysisMapperXmlTest.java | 43 ++++++++++++
...ank-tag-rule-caliber-adjustment-backend.md | 39 +++++++++++
...ng-sensitive-keyword-min-amount-backend.md | 35 ++++++++++
4 files changed, 171 insertions(+), 14 deletions(-)
create mode 100644 docs/reports/implementation/2026-07-13-bank-tag-rule-caliber-adjustment-backend.md
create mode 100644 docs/reports/implementation/2026-07-13-gambling-sensitive-keyword-min-amount-backend.md
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 3a0e5532..47724c9f 100644
--- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml
+++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml
@@ -388,11 +388,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
staff.id_card AS idCard,
SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) AS annualAmount
from ccdi_bank_statement bs
+ inner join (
+ select
+ max(COALESCE(
+ STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
+ STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 10), '%Y-%m-%d')
+ )) AS anchorDate
+ from ccdi_bank_statement anchor_bs
+ where anchor_bs.project_id = #{projectId}
+ ) project_anchor on project_anchor.anchorDate is not null
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
where bs.project_id = #{projectId}
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
and
- and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
+ and 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')
+ ) >= DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)
group by staff.id_card
having SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
) t
@@ -541,11 +553,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS annualAmount
from ccdi_bank_statement bs
+ inner join (
+ select
+ max(COALESCE(
+ STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
+ STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 10), '%Y-%m-%d')
+ )) AS anchorDate
+ from ccdi_bank_statement anchor_bs
+ where anchor_bs.project_id = #{projectId}
+ ) project_anchor on project_anchor.anchorDate is not null
where bs.project_id = #{projectId}
and
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and
- and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
+ and 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')
+ ) >= DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)
group by bs.cret_no
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
) t
@@ -775,12 +799,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
CONCAT(
'摘要/对手命中赌博敏感词,摘要“', IFNULL(bs.USER_MEMO, ''),
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
- '”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR), ' 元'
+ '”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR),
+ ' 元,达到敏感交易最低金额 200 元'
) AS reasonDetail
from ccdi_bank_statement bs
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
where bs.project_id = #{projectId}
- and IFNULL(bs.AMOUNT_DR, 0) > 0
+ and IFNULL(bs.AMOUNT_DR, 0) >= 200
and (
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
@@ -796,12 +821,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
'”摘要/对手方命中疑似赌博关键词,摘要“', IFNULL(bs.USER_MEMO, ''),
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
- '”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
+ '”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
+ ' 元,达到敏感交易最低金额 200 元'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and
- and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
+ and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) >= 200
and (
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
@@ -1152,7 +1178,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
CONCAT(
'物业缴费金额 ', CAST(trade.amountDr AS CHAR),
' 元,对手方“', IFNULL(trade.customerAccountName, ''),
- '”,证件号 ', trade.personId, ' 名下无房产登记'
+ '”,证件号 ', trade.personId, ' 名下无匹配房产登记'
) AS reasonDetail
from (
select
@@ -1161,14 +1187,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bs.group_id AS groupId,
bs.batch_id AS logId,
IFNULL(bs.AMOUNT_DR, 0) AS amountDr,
- bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName
+ bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
+ bs.USER_MEMO AS userMemo
from ccdi_bank_statement bs
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_DR, 0) > 0
and (
- IFNULL(bs.USER_MEMO, '') REGEXP '物业|物业费|管理费|物业服务|综合服务'
- or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区'
+ IFNULL(bs.USER_MEMO, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
+ or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
)
union all
@@ -1179,26 +1206,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bs.group_id AS groupId,
bs.batch_id AS logId,
IFNULL(bs.AMOUNT_DR, 0) AS amountDr,
- bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName
+ bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
+ bs.USER_MEMO AS userMemo
from ccdi_bank_statement bs
inner join ccdi_staff_fmy_relation relation on relation.relation_cert_no = bs.cret_no
where bs.project_id = #{projectId}
and relation.status = 1
and IFNULL(bs.AMOUNT_DR, 0) > 0
and (
- IFNULL(bs.USER_MEMO, '') REGEXP '物业|物业费|管理费|物业服务|综合服务'
- or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区'
+ IFNULL(bs.USER_MEMO, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
+ or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
)
) trade
left join (
select distinct
- asset.person_id AS personId
+ asset.person_id AS personId,
+ asset.asset_name AS assetName
from ccdi_asset_info asset
where asset.asset_main_type = '房产'
and asset.asset_sub_type = '住宅'
and asset.asset_status = '正常'
+ and trim(IFNULL(asset.asset_name, '')) != ''
) asset
on asset.personId = trade.personId
+ and (
+ (
+ trim(IFNULL(trade.customerAccountName, '')) != ''
+ and IFNULL(trade.customerAccountName, '') LIKE concat('%', asset.assetName, '%')
+ )
+ or (
+ trim(IFNULL(trade.userMemo, '')) != ''
+ and IFNULL(trade.userMemo, '') LIKE concat('%', asset.assetName, '%')
+ )
+ )
where asset.personId is null
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 a228e2d4..87ec3822 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
@@ -152,6 +152,49 @@ class CcdiBankTagAnalysisMapperXmlTest {
assertTrue(!selectSql.contains("social_credit_code = bs"));
}
+ @Test
+ void gamblingSensitiveKeywordRules_shouldIgnoreSmallAmountTransactions() throws Exception {
+ String xml = readXml(RESOURCE);
+ String staffSelectSql = extractSelectSql(xml, "selectGamblingSensitiveKeywordStatements");
+ String externalSelectSql = extractSelectSql(xml, "selectExternalGamblingMemoStatements");
+
+ assertTrue(staffSelectSql.contains("IFNULL(bs.AMOUNT_DR, 0) >= 200"));
+ assertTrue(staffSelectSql.contains("达到敏感交易最低金额 200 元"));
+ assertTrue(externalSelectSql.contains(
+ "GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) >= 200"
+ ));
+ assertTrue(externalSelectSql.contains("达到敏感交易最低金额 200 元"));
+ }
+
+ @Test
+ void annualTurnoverRules_shouldUseProjectStatementDateAsOneYearAnchor() throws Exception {
+ String xml = readXml(RESOURCE);
+ String staffSelectSql = extractSelectSql(xml, "selectAnnualTurnoverObjects");
+ String externalSelectSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");
+
+ assertTrue(staffSelectSql.contains("project_anchor.anchorDate"));
+ assertTrue(staffSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
+ assertFalse(staffSelectSql.contains("CURDATE()"));
+ assertTrue(externalSelectSql.contains("project_anchor.anchorDate"));
+ assertTrue(externalSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
+ assertFalse(externalSelectSql.contains("CURDATE()"));
+ }
+
+ @Test
+ void propertyFeeMismatchRule_shouldUseReasonablePropertyKeywordsAndAssetNameMatching() throws Exception {
+ String xml = readXml(RESOURCE);
+ String selectSql = extractSelectSql(xml, "selectPropertyFeeRegistrationMismatchStatements");
+
+ assertTrue(selectSql.contains("物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金"));
+ assertFalse(selectSql.contains("中心"));
+ assertFalse(selectSql.contains("社区"));
+ assertFalse(selectSql.contains("大厦"));
+ assertTrue(selectSql.contains("asset.asset_name AS assetName"));
+ assertTrue(selectSql.contains("trade.customerAccountName"));
+ assertTrue(selectSql.contains("trade.userMemo"));
+ assertTrue(selectSql.contains("名下无匹配房产登记"));
+ }
+
@Test
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
String xml = readXml(RESOURCE);
diff --git a/docs/reports/implementation/2026-07-13-bank-tag-rule-caliber-adjustment-backend.md b/docs/reports/implementation/2026-07-13-bank-tag-rule-caliber-adjustment-backend.md
new file mode 100644
index 00000000..6069b9c9
--- /dev/null
+++ b/docs/reports/implementation/2026-07-13-bank-tag-rule-caliber-adjustment-backend.md
@@ -0,0 +1,39 @@
+# 流水打标规则口径调整后端实施记录
+
+## 修改背景
+
+对现有流水打标规则进行审查后,确认需要优先处理两个后端口径问题:
+
+1. 年流水交易额使用系统当前日期 `CURDATE()` 作为近一年锚点,历史流水项目重复跑模型时结果会随运行日期漂移。
+2. 物业缴费与房产登记不匹配规则仅判断是否存在任意房产登记,且关键词口径要兼顾召回和误报控制。
+
+外部人员夜间交易属于早期残留规则,不属于当前最终外部人员四类口径,本次不调整。
+
+## 修改内容
+
+1. 年流水交易额口径
+ - `ANNUAL_TURNOVER` 改为以当前项目内最大交易日期作为近一年锚点。
+ - `EXTERNAL_ANNUAL_TURNOVER` 同步改为以当前项目内最大交易日期作为近一年锚点。
+ - 不再依赖系统当前日期 `CURDATE()`。
+
+2. 物业缴费与房产登记不匹配口径
+ - 物业候选关键词收敛为 `物业、物管、业委会、业主委员会、维修基金、住宅专项维修资金、房屋维修资金`。
+ - 去掉 `中心、社区、大厦` 等泛词,降低普通转账或机构名称误命中的概率。
+ - 使用 `ccdi_asset_info.asset_name` 与流水对手方名称、摘要做名称匹配。
+ - 若已登记正常住宅资产名称能匹配流水对手方或摘要,则不再判定为“物业缴费与房产登记不匹配”。
+ - 命中原因由“名下无房产登记”调整为“名下无匹配房产登记”。
+
+## 影响范围
+
+- 影响员工年流水交易额超限规则。
+- 影响外部人员年流水交易额超限规则。
+- 影响物业缴费与房产登记不匹配规则。
+- 不新增模型参数,不改数据库结构,不改数据库脚本。
+- 不调整外部人员夜间交易残留规则。
+
+## 验证情况
+
+- 已补充 Mapper XML 单测断言,覆盖近一年锚点和物业资产名称匹配。
+- `mvn -pl ccdi-project -DskipTests compile` 执行通过。
+- 已执行 Mapper 静态校验,确认年流水规则不再包含 `CURDATE()`,物业规则使用收敛关键词并保留资产名称匹配,外部夜间交易规则未被调整。
+- 尝试执行 `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest" test`,但在 `testCompile` 阶段被既有无关测试编译错误阻断:`CcdiFileUploadServiceImplTest` 仍使用旧版 `LsfxAnalysisClient.uploadFile` 参数签名,`CcdiBankStatementTest` 仍调用当前 `BankStatementItem` 不存在的旧 setter。
diff --git a/docs/reports/implementation/2026-07-13-gambling-sensitive-keyword-min-amount-backend.md b/docs/reports/implementation/2026-07-13-gambling-sensitive-keyword-min-amount-backend.md
new file mode 100644
index 00000000..6db049f1
--- /dev/null
+++ b/docs/reports/implementation/2026-07-13-gambling-sensitive-keyword-min-amount-backend.md
@@ -0,0 +1,35 @@
+# 疑似敏感交易最低金额门槛后端实施记录
+
+## 修改背景
+
+疑似敏感交易原先只要支出流水摘要或对手方命中赌博、游戏、彩票等敏感关键词,且支出金额大于 0 即会命中,导致 20 元、50 元等小额支出也进入疑似敏感交易明细。
+
+参考本地知识库中 LSFX 的个人敏感支出口径,敏感类规则通常会结合金额条件控制误报。本次按业务确认,不新增模型参数,不改数据库脚本,直接在后端规则查询中固定最低金额门槛。
+
+## 修改内容
+
+1. 员工疑似敏感交易 `GAMBLING_SENSITIVE_KEYWORD`
+ - 命中条件由 `AMOUNT_DR > 0` 调整为 `AMOUNT_DR >= 200`。
+ - 命中原因追加“达到敏感交易最低金额 200 元”。
+
+2. 外部人员疑似赌博摘要 `EXTERNAL_GAMBLING_MEMO`
+ - 命中条件由交易金额大于 0 调整为交易金额 `>= 200`。
+ - 交易金额仍按收入、支出两侧最大值判断。
+ - 命中原因追加“达到敏感交易最低金额 200 元”。
+
+3. 后端单测
+ - 增加 Mapper XML 断言,确保两类敏感关键词规则都保留 200 元最低金额门槛。
+
+## 影响范围
+
+- 影响员工疑似赌博模型下的“疑似敏感交易”流水命中。
+- 影响外部人员可疑赌博模型下的“外部人员疑似赌博摘要”流水命中。
+- 不新增模型参数,不改 `ccdi_model_param`,不需要执行数据库脚本。
+- 低于 200 元的敏感词流水不再命中上述两类规则。
+
+## 验证情况
+
+- 已补充后端 Mapper XML 单测断言。
+- `mvn -pl ccdi-project -DskipTests compile` 执行通过。
+- 已执行 Mapper 静态校验,确认员工疑似敏感交易和外部人员疑似赌博摘要均包含 `>= 200` 最低金额条件。
+- 尝试执行 `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest,BankTagRuleConfigResolverTest,CcdiBankTagServiceImplTest" test`,但在 `testCompile` 阶段被既有无关测试编译错误阻断:`CcdiFileUploadServiceImplTest` 仍使用旧版 `LsfxAnalysisClient.uploadFile` 参数签名,`CcdiBankStatementTest` 仍调用当前 `BankStatementItem` 不存在的旧 setter。