diff --git a/docs/reports/implementation/2026-03-17-model-sql-check-and-rewrite.md b/docs/reports/implementation/2026-03-17-model-sql-check-and-rewrite.md new file mode 100644 index 00000000..b2b04f45 --- /dev/null +++ b/docs/reports/implementation/2026-03-17-model-sql-check-and-rewrite.md @@ -0,0 +1,1260 @@ +# 模型 SQL 与当前库表结构核对及改写报告 + +## 执行概况 + +- 执行时间:2026-03-17 +- 源文件:[assets/模型信息.csv](/Users/wkc/Desktop/ccdi/ccdi/assets/模型信息.csv) +- 数据库配置来源:[ruoyi-admin/src/main/resources/application-dev.yml](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-admin/src/main/resources/application-dev.yml) +- 目标数据库:`MySQL 5.7.44`,库名 `ccdi` +- 报告目标: + - 保留 CSV 原始内容并转存为 Markdown + - 检查模型 SQL 与当前数据库表结构是否匹配 + - 检查 SQL 在当前 MySQL 方言下的语法兼容性 + - 将可落地 SQL 改写为适配当前库结构的版本 + +## 核对基线 + +### 当前库存在的核心表 + +- `ccdi_base_staff` +- `ccdi_bank_statement` +- `ccdi_staff_fmy_relation` +- `ccdi_asset_info` +- `ccdi_biz_intermediary` +- `ccdi_cust_fmy_relation` +- `ccdi_cust_enterprise_relation` + +### 当前库不存在的外部对象 + +- `odsdb.blfmconf` +- `sjfx_pro.bdfmhqaa_orc` +- `xdzx.assure_infomation` +- `中介名单` + +### 当前库关键字段实际情况 + +- `ccdi_bank_statement` 存在 `TRX_DATE`,不存在 `trx_time` +- `ccdi_asset_info` 存在 `update_time`,不存在 `updated_at` +- `ccdi_bank_statement` 的金额字段为 `AMOUNT_DR`、`AMOUNT_CR` +- `ccdi_bank_statement` 的摘要字段为 `USER_MEMO` +- `ccdi_bank_statement` 的对手方字段为 `CUSTOMER_ACCOUNT_NAME`、`CUSTOMER_ACCOUNT_NO` + +### 本次改写原则 + +- 保持原始业务意图,不随意增加新的业务规则 +- 优先适配 `MySQL 5.7`,去除 `WITH`、`add_months()` 等不兼容写法 +- 将 `PROJECT_ID`、`$$$`、`$$$$` 改为 MySQL 会话变量 +- 对于当前库无法独立支持的外部依赖,给出“部分可执行”的改写版,并明确缺失项 +- 对于原 SQL 与业务口径不完全一致的地方,在说明中单独标注 + +--- + +## CSV 原始内容与逐条改写 + +### CSV 第 1 行 + +- 序号:`2.1` +- 模型名称:`异常交易` +- 核心异常点(展示在前端页面):`与客户之间非正常资金往来` +- 业务口径:`员工及关系人与客户及关系人之间有超过1000元以上的资金往来;客户指信贷类客户包括贷款户、担保人,中介库人员,包括中介注册的主体及主体关系人。` +- 相关指标:`与特定客户交易总额` +- 指标英文名:`空` +- 风险筛查对象:`员工本人及亲属、贷款客户、担保人、中介` +- 技术口径:`空` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`高风险` +- 原始代码: + +```sql +--员工及其亲属与贷款客户、担保户、中介有异常交易 +with loan_cust_acct as ( +select t2.aa01ac15 +from +( +select substr(nfabcsid,4) as nfabcsid + from odsdb.blfmconf --贷款合同文件 + where nfaacost in ('3','5','7') --合同状态 + and substr(nfaabrno,1,3) = '902' --机构 + and del_f = '0' + group by substr(nfabcsid,4) +) t1 +inner join +( +select + aa01ac15 --账号 +,aa62cfno +from sjfx_pro.bdfmhqaa_orc +where del_f = 0 + and substr(trim(aa47brno),1,3) = '902' --机构号修改 +and rcstrs1b <>'9' + AND aa15zhzt ='1' -- 账户状态 1-正常 2-销户 3-新开户 4-结清 +group by aa01ac15 ,aa62cfno +) t2 +on t1.nfabcsid = t2.aa62cfno +) , +assure_cust_acct as ( +select t2.aa01ac15 +from +( +select asseure_sign +from xdzx.assure_infomation +where del_f= '0' +and assure_state <> '2' +and substr(create_org,1,3)='902' +group by asseure_sign +) t1 +inner join +( +select + aa01ac15 --账号 +,aa03csno +from sjfx_pro.bdfmhqaa_orc +where del_f = 0 + and substr(trim(aa47brno),1,3) = '902' --机构号修改 +and rcstrs1b <>'9' + AND aa15zhzt ='1' -- 账户状态 1-正常 2-销户 3-新开户 4-结清 +group by aa01ac15 ,aa03csno +) t2 +on t1.asseure_sign = t2.aa03csno +) +select a.bank_statement_id +from +( +select t1.id_card +,t2.bank_statement_id +,customer_account_no +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and (amount_dr>1000 or amount_cr>1000) +and customer_account_no is not null +union all +select t1.person_id +,t2.bank_statement_id +,customer_account_no +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +and (amount_dr>1000 or amount_cr>1000) +and customer_account_no is not null +) a +where exists (select 1 from loan_cust_acct b on a.customer_account_no = b.aa01ac15) +or exists (select 1 from assure_cust_acct c on a.customer_account_no = c.aa01ac15) +or exists (select 1 from 中介名单 d on a.customer_account_no = d.中介账号); +``` + +**核对结论** + +- 原 SQL 依赖 3 个当前库不存在的外部 schema 和 1 个不存在的中介表 +- 原 SQL 使用 `WITH`,当前 `MySQL 5.7` 不支持 +- 原 SQL 中 `exists (select 1 from ... on ...)` 是明确语法错误 +- 当前 `ccdi` 库没有贷款客户账号、担保人账号、中介账号的现成对照表,无法完全在现库独立落地 + +**改写 SQL** + +以下改写版可在 `MySQL 5.7` 执行,但需要先准备 3 张引用表: + +- `tmp_model_21_loan_cust_acct(account_no)` +- `tmp_model_21_assure_cust_acct(account_no)` +- `tmp_model_21_intermediary_acct(account_no)` + +```sql +SET @project_id := 0; +SET @min_trade_amount := 1000; + +SELECT DISTINCT a.bank_statement_id +FROM ( + SELECT + t1.id_card AS person_id, + t2.bank_statement_id, + t2.CUSTOMER_ACCOUNT_NO AS customer_account_no + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND (t2.AMOUNT_DR > @min_trade_amount OR t2.AMOUNT_CR > @min_trade_amount) + AND t2.CUSTOMER_ACCOUNT_NO IS NOT NULL + + UNION ALL + + SELECT + t1.person_id, + t2.bank_statement_id, + t2.CUSTOMER_ACCOUNT_NO AS customer_account_no + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + AND (t2.AMOUNT_DR > @min_trade_amount OR t2.AMOUNT_CR > @min_trade_amount) + AND t2.CUSTOMER_ACCOUNT_NO IS NOT NULL +) a +WHERE EXISTS ( + SELECT 1 + FROM tmp_model_21_loan_cust_acct b + WHERE b.account_no = a.customer_account_no +) + OR EXISTS ( + SELECT 1 + FROM tmp_model_21_assure_cust_acct c + WHERE c.account_no = a.customer_account_no +) + OR EXISTS ( + SELECT 1 + FROM tmp_model_21_intermediary_acct d + WHERE d.account_no = a.customer_account_no +); +``` + +**未解决依赖** + +- 当前库没有贷款客户账号、担保人账号、中介账号映射表 +- 如果后续需要完全落地,应先把外部客户账号清单同步到 `ccdi` 或建立中间表 + +--- + +### CSV 第 2 行 + +- 序号:`2.2` +- 模型名称:`空` +- 核心异常点(展示在前端页面):`低收入亲属大额交易` +- 业务口径:`关系人中没有收入或月收入低于3000元的人员,累计交易金额超过10万元。` +- 相关指标:`低收入亲属的累计交易额` +- 指标英文名:`空` +- 风险筛查对象:`员工亲属` +- 技术口径:`关联员工及其亲属,筛选平均月收入<3000的亲属的账户,在流水中单笔或累计 >10 万元,按员工汇总` +- 限制阈值指标:`/` +- 可疑结果返回:`个人、累积金额` +- 风险等级:`一般` +- 原始代码: + +```sql +--员工亲属低收入但交易金额高 +select t1.person_id +,t2.trans_amount +from +( +select person_id +,relation_cert_no +,avg(amount_cr) as avg_amount_cr +from +( +select t1.person_id +,t1.relation_cert_no +,substr(trx_time,1,7) +,sum(amount_cr) as amount_cr--收入金额 +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +and t2.le_account_name <> t2.customer_account_name --排除同名交易 +group by t1.person_id,t1.relation_cert_no,substr(trx_time,1,7) +) +group by person_id,relation_cert_no +having avg(amount_cr)<=3000 ---月均收入不超过3000 +) t1 +left join +( +select t1.relation_cert_no +,sum(amount_cr + amount_dr) as trans_amount +,max(amount_cr) as max_amount_cr +,max(amount_dr) as max_amount_dr +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +group by t1.relation_cert_no +having sum(amount_cr + amount_dr) >= 100000 +) t2 +on t1.relation_cert_no = t2.relation_cert_no +where t2.relation_cert_no is not null +; +``` + +**核对结论** + +- 原 SQL 引用了不存在的 `trx_time` +- 原 SQL 的内层派生表缺少别名 +- 业务意图可完全映射到当前库 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @monthly_income_ceiling := 3000; +SET @total_trade_floor := 100000; + +SELECT + low_income.person_id, + low_income.relation_cert_no, + risky.trans_amount, + risky.max_amount_cr, + risky.max_amount_dr +FROM ( + SELECT + monthly.person_id, + monthly.relation_cert_no, + AVG(monthly.month_income) AS avg_amount_cr + FROM ( + SELECT + t1.person_id, + t1.relation_cert_no, + LEFT(t2.TRX_DATE, 7) AS income_month, + SUM(t2.AMOUNT_CR) AS month_income + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + AND t2.LE_ACCOUNT_NAME <> t2.CUSTOMER_ACCOUNT_NAME + GROUP BY t1.person_id, t1.relation_cert_no, LEFT(t2.TRX_DATE, 7) + ) monthly + GROUP BY monthly.person_id, monthly.relation_cert_no + HAVING AVG(monthly.month_income) <= @monthly_income_ceiling +) low_income +INNER JOIN ( + SELECT + t1.relation_cert_no, + SUM(IFNULL(t2.AMOUNT_CR, 0) + IFNULL(t2.AMOUNT_DR, 0)) AS trans_amount, + MAX(t2.AMOUNT_CR) AS max_amount_cr, + MAX(t2.AMOUNT_DR) AS max_amount_dr + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + GROUP BY t1.relation_cert_no + HAVING SUM(IFNULL(t2.AMOUNT_CR, 0) + IFNULL(t2.AMOUNT_DR, 0)) >= @total_trade_floor +) risky + ON low_income.relation_cert_no = risky.relation_cert_no; +``` + +--- + +### CSV 第 3 行 + +- 序号:`3.1` +- 模型名称:`疑似赌博` +- 核心异常点(展示在前端页面):`疑似赌博交易` +- 业务口径:`多人(2人及以上)、多次(2次以上)、相近时间(同一天)内,有转账、微信转账、支付宝转账发生,且额度在可疑区间。金额区间可在排查设置页面进行设置` +- 相关指标:`单日疑似赌博转账金额下限` +- 指标英文名:`空` +- 风险筛查对象:`员工本人` +- 技术口径:`员工本人账户中,24h内统计向不同对手方转账(cash_type='转账')且金额在可疑区间,笔数≥2且对手方≥2` +- 限制阈值指标:`单日频繁转账金额下限` +- 可疑结果返回:`个人、日期、累计金额` +- 风险等级:`高风险` +- 原始代码: + +```sql +--员工 疑似赌博 +select id_card,trans_date +from +( +select t1.id_card,left(trx_time,10) as trans_date,customer_account_name +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and ((amount_dr>= $$$ and amount_dr<=$$$) or (amount_cr>= $$$ and amount_cr<=$$$)) -----转入转出金额区间 +and customer_account_name is not null +and (user_memo rlike '转帐|转账|红包|网转|转入' or cash_type rlike '转帐|转账|红包|网转|转入') +and user_memo not like '%款%' +and t2.le_account_name <> t2.customer_account_name --排除同名交易 +group by t1.id_card,left(trx_time,10),customer_account_name +having count(1)>2 +) +group by id_card,trans_date +having count(distinct customer_account_name)>=2; +``` + +**核对结论** + +- 原 SQL 引用了不存在的 `trx_time` +- 外层 `from (...)` 缺少别名 +- 原技术口径写“24h 内”,原 SQL 实际按自然日统计 +- 原 SQL 内层 `having count(1)>2` 与“2次以上”的自然语言描述并不完全一致 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @amount_min := 1000; +SET @amount_max := 5000; + +SELECT + daily.id_card, + daily.trans_date, + COUNT(DISTINCT daily.customer_account_name) AS counterparty_count, + SUM(daily.trade_count) AS trade_count +FROM ( + SELECT + t1.id_card, + LEFT(t2.TRX_DATE, 10) AS trans_date, + t2.CUSTOMER_ACCOUNT_NAME AS customer_account_name, + COUNT(*) AS trade_count + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND ( + (t2.AMOUNT_DR BETWEEN @amount_min AND @amount_max) + OR (t2.AMOUNT_CR BETWEEN @amount_min AND @amount_max) + ) + AND t2.CUSTOMER_ACCOUNT_NAME IS NOT NULL + AND ( + t2.USER_MEMO REGEXP '转帐|转账|红包|网转|转入' + OR t2.CASH_TYPE REGEXP '转帐|转账|红包|网转|转入' + ) + AND (t2.USER_MEMO IS NULL OR t2.USER_MEMO NOT LIKE '%款%') + AND t2.LE_ACCOUNT_NAME <> t2.CUSTOMER_ACCOUNT_NAME + GROUP BY t1.id_card, LEFT(t2.TRX_DATE, 10), t2.CUSTOMER_ACCOUNT_NAME + HAVING COUNT(*) >= 2 +) daily +GROUP BY daily.id_card, daily.trans_date +HAVING COUNT(DISTINCT daily.customer_account_name) >= 2; +``` + +**说明** + +- 改写版将“多次”按 `>= 2` 处理,更贴近中文业务口径 +- 如果后续必须按原 SQL 的严格条件执行,可将 `HAVING COUNT(*) >= 2` 改回 `> 2` + +--- + +### CSV 第 4 行 + +- 序号:`空` +- 模型名称:`空` +- 核心异常点(展示在前端页面):`空` +- 业务口径:`空` +- 相关指标:`单日疑似赌博转账金额上限` +- 指标英文名:`空` +- 风险筛查对象:`员工本人` +- 技术口径:`空` +- 代码:`空` +- 限制阈值指标:`单日频繁转账金额上限` +- 可疑结果返回:`空` +- 风险等级:`空` + +**核对结论** + +- 该行不是独立模型 SQL,而是对 `3.1` 的上限参数补充 +- 建议在模型参数表中为 `3.1` 同时维护下限和上限两个阈值字段,不应单独落成 SQL + +--- + +### CSV 第 5 行 + +- 序号:`3.2` +- 模型名称:`空` +- 核心异常点(展示在前端页面):`疑似敏感交易` +- 业务口径:`备注或交易摘要、对手有“游戏、抖币、体彩、福彩”等字眼。` +- 相关指标:`赌博关键词支出` +- 指标英文名:`空` +- 风险筛查对象:`员工本人` +- 技术口径:`员工本人账户中,筛选备注或交易摘要、对手方名称有“游戏、抖币、体彩、福彩”等字眼关键词,汇总金额` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`高风险` +- 原始代码: + +```sql +--员工 网络赌博、体彩 +select t2.bank_statement_id +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and amount_dr> 0 +and (user_memo rlike '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注' +or customer_account_name rlike '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注') +; +``` + +**核对结论** + +- 主表和字段与当前库匹配 +- 语法可兼容 `MySQL 5.7` +- 仍需用参数替换 `PROJECT_ID` + +**改写 SQL** + +```sql +SET @project_id := 0; + +SELECT + t2.bank_statement_id +FROM ccdi_base_staff t1 +INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no +WHERE t2.project_id = @project_id + AND t2.AMOUNT_DR > 0 + AND ( + t2.USER_MEMO REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注' + ); +``` + +--- + +### CSV 第 6 行 + +- 序号:`4` +- 模型名称:`可疑关系` +- 核心异常点(展示在前端页面):`特殊金额交易` +- 业务口径:`除与配偶、子女外,发生特殊金额交易,如1314元、520元等具有特殊含义的金额。` +- 相关指标:`特殊金额交易` +- 指标英文名:`空` +- 风险筛查对象:`员工本人` +- 技术口径:`员工本人账户中,筛选交易金额匹配特殊值(如1314),且对手方名称非配偶/子女,统计笔数` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`空` +- 原始代码: + +```sql +--员工 可疑关系 +select distinct t2.bank_statement_id +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +left join +ccdi_staff_fmy_relation t3 +on t1.id_card = t3.person_id +and t2.customer_account_name = t3.relation_name +where t2.project_id = PROJECT_ID +and t2.le_account_name <> t2.customer_account_name --排除同名交易 +and t3.relation_type not in (配偶,子女) --关系类型按实际数据的码值确定 +and (amount_dr in (520,1314) or amount_cr in (520,1314)) +; +``` + +**核对结论** + +- 原 SQL 中 `not in (配偶,子女)` 缺少引号,且使用了全角逗号,语法错误 +- 使用 `left join + not in` 会把未匹配到亲属关系的记录误伤为 `null not in (...)` +- 当前库的 `relation_type` 实际可能存码值,不能直接假定一定是中文 + +**改写 SQL** + +```sql +SET @project_id := 0; + +SELECT DISTINCT + t2.bank_statement_id +FROM ccdi_base_staff t1 +INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no +WHERE t2.project_id = @project_id + AND t2.LE_ACCOUNT_NAME <> t2.CUSTOMER_ACCOUNT_NAME + AND (t2.AMOUNT_DR IN (520, 1314) OR t2.AMOUNT_CR IN (520, 1314)) + AND NOT EXISTS ( + SELECT 1 + FROM ccdi_staff_fmy_relation t3 + WHERE t3.person_id = t1.id_card + AND t3.status = 1 + AND t3.relation_name = t2.CUSTOMER_ACCOUNT_NAME + AND t3.relation_type IN ('配偶', '子女') + ); +``` + +**说明** + +- 如果 `relation_type` 在现网存的是码值,需要把 `'配偶'`、`'子女'` 替换成真实码值 + +--- + +### CSV 第 7 行 + +- 序号:`5.1` +- 模型名称:`可疑兼职` +- 核心异常点(展示在前端页面):`疑似兼职` +- 业务口径:`除本行工资收入外,每月有固定收入,固定收入金额自行设置。` +- 相关指标:`月度非本行工资收入金额` +- 指标英文名:`MONTHLY_FIXED_INCOME` +- 风险筛查对象:`员工本人` +- 技术口径:`员工本人账户中,筛选 amount_cr>0,除本行工资收入外,每月有固定收入,固定收入金额自行设置。` +- 限制阈值指标:`非本行工资收入金额` +- 可疑结果返回:`个人、累积金额` +- 风险等级:`空` +- 原始代码: + +```sql +--员工 可疑兼职 +select id_card +FROM +( +select t1.id_card +,left(TRX_DATE,7) as incm_mnth +,sum(t2.amount_cr) as sum_amount_cr +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and t2.le_account_name <> t2.customer_account_name --排除同名交易 +and not (customer_account_name = '浙江兰溪农村商业银行股份有限公司' +and (user_memo rlike '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' +or cash_type rlike '代发|工资|劳务费' )) +and customer_account_name is not null +and left(TRX_DATE,10) >= add_months(current_date(),-12) --近一年 +and amount_cr>0 +group by t1.id_card,left(t2.TRX_DATE,7) +) +where sum_amount_cr > $$$$ --每月收入参数 +group by id_card +having count(distinct incm_mnth)>=6 -----一年内6个月满足收入门槛 +and STDDEV(sum_amount_cr)/AVG(sum_amount_cr) <= 0.3 --标准差与平均值的比值小,波动小 +; +``` + +**核对结论** + +- 原 SQL 外层派生表缺少别名 +- 原 SQL 使用 `add_months()`,当前 `MySQL 5.7` 不支持 +- `TRX_DATE` 为字符串,必须显式转日期后再做时间比较 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @monthly_income_floor := 5000; +SET @stable_month_count := 6; +SET @volatility_ratio_limit := 0.3; + +SELECT + monthly_income.id_card +FROM ( + SELECT + t1.id_card, + LEFT(t2.TRX_DATE, 7) AS incm_mnth, + SUM(t2.AMOUNT_CR) AS sum_amount_cr + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND t2.LE_ACCOUNT_NAME <> t2.CUSTOMER_ACCOUNT_NAME + AND NOT ( + t2.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司' + AND ( + t2.USER_MEMO REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' + OR t2.CASH_TYPE REGEXP '代发|工资|劳务费' + ) + ) + AND t2.CUSTOMER_ACCOUNT_NAME IS NOT NULL + AND STR_TO_DATE(LEFT(t2.TRX_DATE, 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) + AND t2.AMOUNT_CR > 0 + GROUP BY t1.id_card, LEFT(t2.TRX_DATE, 7) +) monthly_income +WHERE monthly_income.sum_amount_cr > @monthly_income_floor +GROUP BY monthly_income.id_card +HAVING COUNT(DISTINCT monthly_income.incm_mnth) >= @stable_month_count + AND STDDEV(monthly_income.sum_amount_cr) / NULLIF(AVG(monthly_income.sum_amount_cr), 0) <= @volatility_ratio_limit; +``` + +--- + +### CSV 第 8 行 + +- 序号:`5.2` +- 模型名称:`空` +- 核心异常点(展示在前端页面):`疑似兼职` +- 业务口径:`每季或每年从固定交易对手转入金额,金额可设区间值,如5000-10000。` +- 相关指标:`季度稳定收入金额` +- 指标英文名:`FIXED_COUNTERPARTY_TRANSFER` +- 风险筛查对象:`员工本人` +- 技术口径:`员工本人账户中,按季/年统计同一对手方累计流入金额在设定区间内` +- 限制阈值指标:`稳定季度收入金额` +- 可疑结果返回:`个人、累积金额` +- 风险等级:`空` +- 原始代码: + +```sql +--员工 可疑固定收入 +select id_card +from +( +select +id_card +,customer_account_name +,trans_qrt +,sum(amount_cr) as amount_total +from +( +select t1.id_card +,customer_account_name +,amount_cr +,concat(year(trx_time),'-Q',quarter(trx_time)) as trans_qrt --每季度的固定收入 +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and not (customer_account_name = '浙江兰溪农村商业银行股份有限公司' +and (user_memo rlike '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' +or cash_type rlike '代发|工资|劳务费' )) +and customer_account_name is not null +and left(TRX_DATE,10) >= add_months(current_date(),-12) --近一年 +and amount_cr>0 --每笔收入金额 +and t2.le_account_name <> t2.customer_account_name --排除同名交易 +) +group by id_card,customer_account_name,trans_qrt +having sum(amount_cr) between $$$$ and $$$$ ---该季度总收入,区间自行设置 +) +group by id_card +having count(distinct customer_account_name) < 3 +; +``` + +**核对结论** + +- 原 SQL 引用了不存在的 `trx_time` +- 两层派生表均缺少别名 +- 原 SQL 使用 `add_months()`,当前 `MySQL 5.7` 不支持 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @quarter_income_min := 5000; +SET @quarter_income_max := 10000; + +SELECT + quarter_income.id_card +FROM ( + SELECT + src.id_card, + src.customer_account_name, + CONCAT(YEAR(src.trx_date), '-Q', QUARTER(src.trx_date)) AS trans_qrt, + SUM(src.amount_cr) AS amount_total + FROM ( + SELECT + t1.id_card, + t2.CUSTOMER_ACCOUNT_NAME AS customer_account_name, + t2.AMOUNT_CR AS amount_cr, + STR_TO_DATE(LEFT(t2.TRX_DATE, 10), '%Y-%m-%d') AS trx_date + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND NOT ( + t2.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司' + AND ( + t2.USER_MEMO REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' + OR t2.CASH_TYPE REGEXP '代发|工资|劳务费' + ) + ) + AND t2.CUSTOMER_ACCOUNT_NAME IS NOT NULL + AND STR_TO_DATE(LEFT(t2.TRX_DATE, 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) + AND t2.AMOUNT_CR > 0 + AND t2.LE_ACCOUNT_NAME <> t2.CUSTOMER_ACCOUNT_NAME + ) src + GROUP BY src.id_card, src.customer_account_name, CONCAT(YEAR(src.trx_date), '-Q', QUARTER(src.trx_date)) + HAVING SUM(src.amount_cr) BETWEEN @quarter_income_min AND @quarter_income_max +) quarter_income +GROUP BY quarter_income.id_card +HAVING COUNT(DISTINCT quarter_income.customer_account_name) < 3; +``` + +--- + +### CSV 第 9 行 + +- 序号:`空` +- 模型名称:`空` +- 核心异常点(展示在前端页面):`疑似兼职` +- 业务口径:`转入资金摘要有“工资”、“分红”、“红利”、“利息(非银行结息)”等收入` +- 相关指标:`/` +- 指标英文名:`空` +- 风险筛查对象:`员工本人` +- 技术口径:`空` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`高风险` +- 原始代码: + +```sql +select distinct t2.bank_statement_id +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where t2.project_id = PROJECT_ID +and t2.customer_account_name <> '浙江兰溪农村商业银行股份有限公司' +and (t2.user_memo rlike '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' +or cash_type rlike '代发|工资|劳务费' ) +; +``` + +**核对结论** + +- 原 SQL 主表、字段、语法均基本匹配当前库 +- 结合业务口径,建议增加 `AMOUNT_CR > 0`,避免把支出误判成收入类线索 + +**改写 SQL** + +```sql +SET @project_id := 0; + +SELECT DISTINCT + t2.bank_statement_id +FROM ccdi_base_staff t1 +INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no +WHERE t2.project_id = @project_id + AND t2.AMOUNT_CR > 0 + AND t2.CUSTOMER_ACCOUNT_NAME <> '浙江兰溪农村商业银行股份有限公司' + AND ( + t2.USER_MEMO REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' + OR t2.CASH_TYPE REGEXP '代发|工资|劳务费' + ); +``` + +--- + +### CSV 第 10 行 + +- 序号:`6.1` +- 模型名称:`可疑财产` +- 核心异常点(展示在前端页面):`购房交易与房产登记不匹配` +- 业务口径:`员工及关系人有购房交易,但名下房产无新增登记;有新增登记购房,但无相关购房交易记录。` +- 相关指标:`/` +- 指标英文名:`空` +- 风险筛查对象:`员工本人及亲属` +- 技术口径:`员工及其亲属账户中,存在购房支出,且在家庭负债表(ccdi_family_liability)中该员工名下无新增房产登记,则标记1` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`空` +- 原始代码: + +```sql +--员工及其亲属购买房产但无资产登记 +select distinct bank_statement_id +from +( +select t1.id_card +,bank_statement_id +,trx_time +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and (user_memo rlike '(购|买).*房|房款|首付' +or customer_account_name rlike '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局') +and amount_dr > 0 +union all +select t1.person_id +,bank_statement_id +,trx_time +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +and (user_memo rlike '(购|买).*房|房款|首付' +or customer_account_name rlike '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局') +and amount_dr > 0 +) t1 +left join +( +select person_id +,max(updated_at) as updated_at +from ccdi_asset_info +where asset_main_type = 不动产 --根据具体数据确定码值 +and asset_sub_type in (住宅,商铺) --根据具体数据确定码值 +and asset_status = 正常 --根据具体数据确定码值 +group by person_id +) t2 +on t1.id_card = t2.person_id +where t1.trx_time > t2.updated_at --购买时间大于最近一次资产更新时间 +or t2.person_id is null; --未登记房产 +``` + +**核对结论** + +- 原 SQL 引用了不存在的 `trx_time` 和 `updated_at` +- 原 SQL 使用了未加引号的中文常量 +- 原技术口径提到 `ccdi_family_liability`,但当前库实际只有 `ccdi_asset_info` +- 资产表中房产相关枚举值需按现网实际码值替换 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @asset_main_type_house := 'REAL_ESTATE'; +SET @asset_sub_type_house_1 := 'HOUSE'; +SET @asset_sub_type_house_2 := 'SHOP'; +SET @asset_status_normal := 'NORMAL'; + +SELECT DISTINCT + house_trade.bank_statement_id +FROM ( + SELECT + t1.id_card AS person_id, + t2.bank_statement_id, + STR_TO_DATE(LEFT(t2.TRX_DATE, 10), '%Y-%m-%d') AS trx_date + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '(购|买).*房|房款|首付' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局' + ) + AND t2.AMOUNT_DR > 0 + + UNION ALL + + SELECT + t1.person_id, + t2.bank_statement_id, + STR_TO_DATE(LEFT(t2.TRX_DATE, 10), '%Y-%m-%d') AS trx_date + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '(购|买).*房|房款|首付' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局' + ) + AND t2.AMOUNT_DR > 0 +) house_trade +LEFT JOIN ( + SELECT + person_id, + MAX(update_time) AS latest_asset_update_time + FROM ccdi_asset_info + WHERE asset_main_type = @asset_main_type_house + AND asset_sub_type IN (@asset_sub_type_house_1, @asset_sub_type_house_2) + AND asset_status = @asset_status_normal + GROUP BY person_id +) asset + ON house_trade.person_id = asset.person_id +WHERE asset.person_id IS NULL + OR house_trade.trx_date > DATE(asset.latest_asset_update_time); +``` + +**说明** + +- `REAL_ESTATE`、`HOUSE`、`SHOP`、`NORMAL` 只是占位值,必须替换成 `ccdi_asset_info` 的真实码值 +- 原业务口径还提到“有新增登记购房,但无相关购房交易记录”,该反向校验不在原 SQL 中,本次未擅自补充为双向模型 + +--- + +### CSV 第 11 行 + +- 序号:`6.2` +- 模型名称:`可疑财产` +- 核心异常点(展示在前端页面):`物业缴费与房产登记不匹配` +- 业务口径:`员工及关系人有物业缴费记录,但名下房产无新增登记。` +- 相关指标:`/` +- 指标英文名:`空` +- 风险筛查对象:`员工本人及亲属` +- 技术口径:`员工及其亲属账户中,存在物业费支出,且在家庭负债表中无新增房产登记,则标记1` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`空` +- 原始代码: + +```sql +有物业缴费记录但无房产登记 +select distinct bank_statement_id +FROM +( +select t1.id_card +,bank_statement_id +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and (user_memo rlike '物业|物业费|管理费|物业服务|综合服务' +or customer_account_name rlike '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区') +and amount_dr > 0 +union all +select t1.person_id +,bank_statement_id +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +and (user_memo rlike '物业|物业费|管理费|物业服务|综合服务' +or customer_account_name rlike '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区') +and amount_dr > 0 +) t1 +left join +( +select person_id +,max(updated_at) as updated_at +from ccdi_asset_info +where asset_main_type = 不动产 +and asset_sub_type in (住宅,商铺) +and asset_status = 正常 +group by person_id +) t2 +on t1.id_card = t2.person_id +where t2.person_id is null; +``` + +**核对结论** + +- 原 SQL 引用了不存在的 `updated_at` +- 原 SQL 的资产枚举值写法不符合当前库 +- 可完全改写为当前结构可执行版本 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @asset_main_type_house := 'REAL_ESTATE'; +SET @asset_sub_type_house_1 := 'HOUSE'; +SET @asset_sub_type_house_2 := 'SHOP'; +SET @asset_status_normal := 'NORMAL'; + +SELECT DISTINCT + property_fee.bank_statement_id +FROM ( + SELECT + t1.id_card AS person_id, + t2.bank_statement_id + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '物业|物业费|管理费|物业服务|综合服务' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区' + ) + AND t2.AMOUNT_DR > 0 + + UNION ALL + + SELECT + t1.person_id, + t2.bank_statement_id + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '物业|物业费|管理费|物业服务|综合服务' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区' + ) + AND t2.AMOUNT_DR > 0 +) property_fee +LEFT JOIN ( + SELECT + person_id + FROM ccdi_asset_info + WHERE asset_main_type = @asset_main_type_house + AND asset_sub_type IN (@asset_sub_type_house_1, @asset_sub_type_house_2) + AND asset_status = @asset_status_normal + GROUP BY person_id +) asset + ON property_fee.person_id = asset.person_id +WHERE asset.person_id IS NULL; +``` + +--- + +### CSV 第 12 行 + +- 序号:`6.3` +- 模型名称:`可疑财产` +- 核心异常点(展示在前端页面):`大额纳税与资产登记不匹配` +- 业务口径:`员工及关系人有5000元以上的纳税记录,但名下无房产车产新增登记。` +- 相关指标:`/` +- 指标英文名:`空` +- 风险筛查对象:`员工本人及亲属` +- 技术口径:`员工及其亲属账户中,存在单笔纳税 >5000 元,且在家庭负债表中无新增房产/车产登记,则标记1` +- 限制阈值指标:`/` +- 可疑结果返回:`流水明细` +- 风险等级:`空` +- 原始代码: + +```sql +有5000元以上的纳税记录但无房产登记 +select distinct bank_statement_id +FROM +( +select t1.id_card +,bank_statement_id +from +ccdi_base_staff t1 +inner join +ccdi_bank_statement t2 +on t1.id_card = t2.cret_no +where project_id = PROJECT_ID +and (user_memo rlike '税务|缴税|税款' +or customer_account_name rlike '税务|税务局|国库|国家金库|财政') +and amount_dr >= 5000 +union all +select t1.person_id +,bank_statement_id +from +ccdi_staff_fmy_relation t1 +inner join +ccdi_bank_statement t2 +on t1.relation_cert_no = t2.cret_no +where t1.status = 1 +and t2.project_id = PROJECT_ID +and (user_memo rlike '税务|缴税|税款' +or customer_account_name rlike '税务|税务局|国库|国家金库|财政') +and amount_dr >= 5000 +) t1 +left join +( +select person_id +,max(updated_at) as updated_at +from ccdi_asset_info +where asset_main_type = 不动产 +and asset_sub_type in (住宅,商铺) +and asset_status = 正常 +group by person_id +) t2 +on t1.id_card = t2.person_id +where t2.person_id is null; +``` + +**核对结论** + +- 原 SQL 仍然只校验“无房产登记”,与业务口径中的“无房产车产新增登记”不一致 +- 原 SQL 引用了不存在的 `updated_at` +- 资产类型应按真实码值配置 + +**改写 SQL** + +```sql +SET @project_id := 0; +SET @tax_amount_floor := 5000; +SET @asset_main_type_house := 'REAL_ESTATE'; +SET @asset_main_type_car := 'VEHICLE'; +SET @asset_status_normal := 'NORMAL'; + +SELECT DISTINCT + tax_trade.bank_statement_id +FROM ( + SELECT + t1.id_card AS person_id, + t2.bank_statement_id + FROM ccdi_base_staff t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.id_card = t2.cret_no + WHERE t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '税务|缴税|税款' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '税务|税务局|国库|国家金库|财政' + ) + AND t2.AMOUNT_DR >= @tax_amount_floor + + UNION ALL + + SELECT + t1.person_id, + t2.bank_statement_id + FROM ccdi_staff_fmy_relation t1 + INNER JOIN ccdi_bank_statement t2 + ON t1.relation_cert_no = t2.cret_no + WHERE t1.status = 1 + AND t2.project_id = @project_id + AND ( + t2.USER_MEMO REGEXP '税务|缴税|税款' + OR t2.CUSTOMER_ACCOUNT_NAME REGEXP '税务|税务局|国库|国家金库|财政' + ) + AND t2.AMOUNT_DR >= @tax_amount_floor +) tax_trade +LEFT JOIN ( + SELECT + person_id + FROM ccdi_asset_info + WHERE asset_status = @asset_status_normal + AND asset_main_type IN (@asset_main_type_house, @asset_main_type_car) + GROUP BY person_id +) asset + ON tax_trade.person_id = asset.person_id +WHERE asset.person_id IS NULL; +``` + +**说明** + +- 该改写版按业务口径补充了“车产”主类判断,但主类码值仍需按现网真实枚举替换 + +--- + +## 汇总结论 + +- CSV 共 `12` 行,其中: + - `10` 行带 `sql
` 前缀 SQL + - `1` 行带原始 SQL 但未写 `sql
` 前缀 + - `1` 行仅用于补充阈值参数,没有独立 SQL +- 当前库能直接承接并改写的模型: + - `2.2` + - `3.1` + - `3.2` + - `4` + - `5.1` + - `5.2` + - `CSV 第 9 行补充规则` + - `6.1` + - `6.2` + - `6.3` +- 当前库不能完全独立落地的模型: + - `2.1` + +## 语法校验结果 + +- 已使用 `EXPLAIN` 在当前 `MySQL 5.7.44 / ccdi` 上校验通过的改写 SQL: + - `2.2` + - `3.1` + - `3.2` + - `4` + - `5.1` + - `5.2` + - `CSV 第 9 行补充规则` + - `6.1` + - `6.2` + - `6.3` +- 未纳入自动语法校验的改写 SQL: + - `2.1` + - 原因:改写版依赖 `tmp_model_21_loan_cust_acct`、`tmp_model_21_assure_cust_acct`、`tmp_model_21_intermediary_acct` 三张引用表,当前库尚未创建 + +## 建议后续动作 + +1. 为 `2.1` 建立客户账号引用清单表,或者把外部客户库同步到 `ccdi` +2. 为资产模型补齐 `asset_main_type`、`asset_sub_type`、`asset_status` 的字典码值说明 +3. 将 `PROJECT_ID`、上下限金额等参数沉淀到模型参数表,而不是保留在文本 SQL 中 +4. 如果后续要在代码里执行这些 SQL,建议统一封装成 MyBatis XML,并配套 DTO/VO 与单元测试