Files
ccdi/docs/reports/implementation/2026-03-17-model-sql-check-and-rewrite.md

39 KiB
Raw Blame History

模型 SQL 与当前库表结构核对及改写报告

执行概况

  • 执行时间2026-03-17
  • 源文件:assets/模型信息.csv
  • 数据库配置来源: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_DRAMOUNT_CR
  • ccdi_bank_statement 的摘要字段为 USER_MEMO
  • ccdi_bank_statement 的对手方字段为 CUSTOMER_ACCOUNT_NAMECUSTOMER_ACCOUNT_NO

本次改写原则

  • 保持原始业务意图,不随意增加新的业务规则
  • 优先适配 MySQL 5.7,去除 WITHadd_months() 等不兼容写法
  • PROJECT_ID$$$$$$$ 改为 MySQL 会话变量
  • 对于当前库无法独立支持的外部依赖,给出“部分可执行”的改写版,并明确缺失项
  • 对于原 SQL 与业务口径不完全一致的地方,在说明中单独标注

CSV 原始内容与逐条改写

CSV 第 1 行

  • 序号:2.1
  • 模型名称:异常交易
  • 核心异常点(展示在前端页面):与客户之间非正常资金往来
  • 业务口径:员工及关系人与客户及关系人之间有超过1000元以上的资金往来客户指信贷类客户包括贷款户、担保人中介库人员包括中介注册的主体及主体关系人。
  • 相关指标:与特定客户交易总额
  • 指标英文名:
  • 风险筛查对象:员工本人及亲属、贷款客户、担保人、中介
  • 技术口径:
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:高风险
  • 原始代码:
--员工及其亲属与贷款客户、担保户、中介有异常交易
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)
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 万元,按员工汇总
  • 限制阈值指标:/
  • 可疑结果返回:个人、累积金额
  • 风险等级:一般
  • 原始代码:
--员工亲属低收入但交易金额高
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

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
  • 限制阈值指标:单日频繁转账金额下限
  • 可疑结果返回:个人、日期、累计金额
  • 风险等级:高风险
  • 原始代码:
--员工 疑似赌博
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

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
  • 模型名称:
  • 核心异常点(展示在前端页面):疑似敏感交易
  • 业务口径:备注或交易摘要、对手有“游戏、抖币、体彩、福彩”等字眼。
  • 相关指标:赌博关键词支出
  • 指标英文名:
  • 风险筛查对象:员工本人
  • 技术口径:员工本人账户中,筛选备注或交易摘要、对手方名称有“游戏、抖币、体彩、福彩”等字眼关键词,汇总金额
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:高风险
  • 原始代码:
--员工 网络赌博、体彩
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

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且对手方名称非配偶/子女,统计笔数
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:
  • 原始代码:
--员工 可疑关系
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

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除本行工资收入外每月有固定收入固定收入金额自行设置。
  • 限制阈值指标:非本行工资收入金额
  • 可疑结果返回:个人、累积金额
  • 风险等级:
  • 原始代码:
--员工 可疑兼职
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

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
  • 风险筛查对象:员工本人
  • 技术口径:员工本人账户中,按季/年统计同一对手方累计流入金额在设定区间内
  • 限制阈值指标:稳定季度收入金额
  • 可疑结果返回:个人、累积金额
  • 风险等级:
  • 原始代码:
--员工 可疑固定收入
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

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 行

  • 序号:
  • 模型名称:
  • 核心异常点(展示在前端页面):疑似兼职
  • 业务口径:转入资金摘要有“工资”、“分红”、“红利”、“利息(非银行结息)”等收入
  • 相关指标:/
  • 指标英文名:
  • 风险筛查对象:员工本人
  • 技术口径:
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:高风险
  • 原始代码:
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

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
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:
  • 原始代码:
--员工及其亲属购买房产但无资产登记
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_timeupdated_at
  • 原 SQL 使用了未加引号的中文常量
  • 原技术口径提到 ccdi_family_liability,但当前库实际只有 ccdi_asset_info
  • 资产表中房产相关枚举值需按现网实际码值替换

改写 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_ESTATEHOUSESHOPNORMAL 只是占位值,必须替换成 ccdi_asset_info 的真实码值
  • 原业务口径还提到“有新增登记购房,但无相关购房交易记录”,该反向校验不在原 SQL 中,本次未擅自补充为双向模型

CSV 第 11 行

  • 序号:6.2
  • 模型名称:可疑财产
  • 核心异常点(展示在前端页面):物业缴费与房产登记不匹配
  • 业务口径:员工及关系人有物业缴费记录,但名下房产无新增登记。
  • 相关指标:/
  • 指标英文名:
  • 风险筛查对象:员工本人及亲属
  • 技术口径:员工及其亲属账户中存在物业费支出且在家庭负债表中无新增房产登记则标记1
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:
  • 原始代码:
有物业缴费记录但无房产登记
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

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
  • 限制阈值指标:/
  • 可疑结果返回:流水明细
  • 风险等级:
  • 原始代码:
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

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<br> 前缀 SQL
    • 1 行带原始 SQL 但未写 sql<br> 前缀
    • 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_accttmp_model_21_assure_cust_accttmp_model_21_intermediary_acct 三张引用表,当前库尚未创建

建议后续动作

  1. 2.1 建立客户账号引用清单表,或者把外部客户库同步到 ccdi
  2. 为资产模型补齐 asset_main_typeasset_sub_typeasset_status 的字典码值说明
  3. PROJECT_ID、上下限金额等参数沉淀到模型参数表,而不是保留在文本 SQL 中
  4. 如果后续要在代码里执行这些 SQL建议统一封装成 MyBatis XML并配套 DTO/VO 与单元测试