diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java index dc81be47..7985cd43 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java @@ -110,9 +110,13 @@ public interface CcdiBankTagAnalysisMapper { * 疑似赌博交易 * * @param projectId 项目ID + * @param amountMinThreshold 可疑金额下限 + * @param amountMaxThreshold 可疑金额上限 * @return 对象命中结果 */ - List selectMultiPartyGamblingTransferObjects(@Param("projectId") Long projectId); + List selectMultiPartyGamblingTransferObjects(@Param("projectId") Long projectId, + @Param("amountMinThreshold") BigDecimal amountMinThreshold, + @Param("amountMaxThreshold") BigDecimal amountMaxThreshold); /** * 疑似敏感交易 @@ -134,17 +138,23 @@ public interface CcdiBankTagAnalysisMapper { * 月度固定收入疑似兼职 * * @param projectId 项目ID + * @param threshold 月度固定收入阈值 * @return 对象命中结果 */ - List selectMonthlyFixedIncomeObjects(@Param("projectId") Long projectId); + List selectMonthlyFixedIncomeObjects(@Param("projectId") Long projectId, + @Param("threshold") BigDecimal threshold); /** * 固定交易对手转入疑似兼职 * * @param projectId 项目ID + * @param quarterMinThreshold 季度收入下限 + * @param quarterMaxThreshold 季度收入上限 * @return 对象命中结果 */ - List selectFixedCounterpartyTransferObjects(@Param("projectId") Long projectId); + List selectFixedCounterpartyTransferObjects(@Param("projectId") Long projectId, + @Param("quarterMinThreshold") BigDecimal quarterMinThreshold, + @Param("quarterMaxThreshold") BigDecimal quarterMaxThreshold); /** * 摘要收入疑似兼职 diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java index 0c1edf3b..e7128b04 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java @@ -35,7 +35,10 @@ public class BankTagRuleConfigResolver { Map.entry("FOREX_SELL_AMT", Set.of("SINGLE_SETTLEMENT_AMOUNT")), Map.entry("WITHDRAW_CNT", Set.of("WITHDRAW_CNT")), Map.entry("STOCK_TFR_LARGE", Set.of("STOCK_TFR_LARGE")), - Map.entry("LARGE_STOCK_TRADING", Set.of("STOCK_TFR_LARGE")) + Map.entry("LARGE_STOCK_TRADING", Set.of("STOCK_TFR_LARGE")), + Map.entry("MULTI_PARTY_GAMBLING_TRANSFER", Set.of("MULTI_PARTY_AMT_MIN", "MULTI_PARTY_AMT_MAX")), + Map.entry("MONTHLY_FIXED_INCOME", Set.of("MONTHLY_FIXED_INCOME")), + Map.entry("FIXED_COUNTERPARTY_TRANSFER", Set.of("FIXED_COUNTERPARTY_TRANSFER_MIN", "FIXED_COUNTERPARTY_TRANSFER_MAX")) ); @Resource diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java index d6cd9440..c5d6cceb 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java @@ -267,9 +267,19 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { toInteger(config.getThresholdValue("FREQUENT_CASH_DEPOSIT")) ); case "LOW_INCOME_RELATIVE_LARGE_TRANSACTION" -> analysisMapper.selectLowIncomeRelativeLargeTransactionObjects(projectId); - case "MULTI_PARTY_GAMBLING_TRANSFER" -> analysisMapper.selectMultiPartyGamblingTransferObjects(projectId); - case "MONTHLY_FIXED_INCOME" -> analysisMapper.selectMonthlyFixedIncomeObjects(projectId); - case "FIXED_COUNTERPARTY_TRANSFER" -> analysisMapper.selectFixedCounterpartyTransferObjects(projectId); + case "MULTI_PARTY_GAMBLING_TRANSFER" -> analysisMapper.selectMultiPartyGamblingTransferObjects( + projectId, + toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MIN")), + toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MAX")) + ); + case "MONTHLY_FIXED_INCOME" -> analysisMapper.selectMonthlyFixedIncomeObjects( + projectId, toBigDecimal(config.getThresholdValue("MONTHLY_FIXED_INCOME")) + ); + case "FIXED_COUNTERPARTY_TRANSFER" -> analysisMapper.selectFixedCounterpartyTransferObjects( + projectId, + toBigDecimal(config.getThresholdValue("FIXED_COUNTERPARTY_TRANSFER_MIN")), + toBigDecimal(config.getThresholdValue("FIXED_COUNTERPARTY_TRANSFER_MAX")) + ); case "INTEREST_PAYMENT_BY_OTHERS" -> analysisMapper.selectInterestPaymentByOthersObjects(projectId); case "SUPPLIER_CONCENTRATION" -> analysisMapper.selectSupplierConcentrationObjects(projectId); case "WITHDRAW_CNT" -> analysisMapper.selectWithdrawCntObjects( 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 58ee94ed..439ecd70 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml @@ -124,6 +124,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) + + bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司' + and ( + IFNULL(bs.USER_MEMO, '') REGEXP '代发|工资|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|PAYROLL|SALA|CPF|directors.*fees' + or IFNULL(bs.CASH_TYPE, '') REGEXP '代发|工资|劳务费' + ) + + + + ( + IFNULL(bs.USER_MEMO, '') REGEXP '代扣|个税|社保|公积金|水费|电费|燃气|话费|党费|医保' + or IFNULL(bs.CASH_TYPE, '') REGEXP '代扣|个税|社保|公积金' + or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '税务|社保|公积金' + ) + + select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '低收入关系人累计交易 ', CAST(t.totalAmount AS CHAR), + ' 元,命中关系人数 ', CAST(t.relationCount AS CHAR), ' 人' + ) AS reasonDetail + from ( + select + relation.person_id AS objectKey, + ROUND(SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)), 2) AS totalAmount, + COUNT(DISTINCT relation.relation_cert_no) AS relationCount + from ccdi_staff_fmy_relation relation + inner join ccdi_bank_statement bs on relation.relation_cert_no = bs.cret_no + where relation.status = 1 + and ( + relation.annual_income is null + or relation.annual_income = 0 + or relation.annual_income / 12 < 3000 + ) + and bs.project_id = #{projectId} + and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') + group by relation.person_id + having SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) > 100000 + ) t select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '近12个月有 ', CAST(t.monthCount AS CHAR), + ' 个月固定收入超过阈值,月均收入 ', CAST(t.avgAmount AS CHAR), ' 元' + ) AS reasonDetail + from ( + select + monthly_income.idCard AS objectKey, + COUNT(DISTINCT monthly_income.incomeMonth) AS monthCount, + ROUND(AVG(monthly_income.monthAmount), 2) AS avgAmount + from ( + select + staff.id_card AS idCard, + LEFT(TRIM(bs.TRX_DATE), 7) AS incomeMonth, + ROUND(SUM(IFNULL(bs.AMOUNT_CR, 0)), 2) AS monthAmount + 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_CR, 0) > 0 + and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') <> '' + and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') + and + 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(CURDATE(), INTERVAL 12 MONTH) + group by staff.id_card, LEFT(TRIM(bs.TRX_DATE), 7) + having SUM(IFNULL(bs.AMOUNT_CR, 0)) > #{threshold} + ) monthly_income + group by monthly_income.idCard + having COUNT(DISTINCT monthly_income.incomeMonth) >= 6 + and STDDEV(monthly_income.monthAmount) / NULLIF(AVG(monthly_income.monthAmount), 0) <= 0.3 + ) t select - bs.bank_statement_id AS bankStatementId, - bs.group_id AS groupId, - bs.batch_id AS logId, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + trade.bankStatementId AS bankStatementId, + trade.groupId AS groupId, + trade.logId AS logId, + CONCAT( + '购房交易金额 ', CAST(trade.amountDr AS CHAR), + ' 元,对手方“', IFNULL(trade.customerAccountName, ''), + '”,证件号 ', trade.personId, ' 名下无房产登记' + ) AS reasonDetail + from ( + select + staff.id_card AS personId, + bs.bank_statement_id AS bankStatementId, + bs.group_id AS groupId, + bs.batch_id AS logId, + IFNULL(bs.AMOUNT_DR, 0) AS amountDr, + bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName + 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 '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局' + ) + + union all + + select + relation.relation_cert_no AS personId, + bs.bank_statement_id AS bankStatementId, + bs.group_id AS groupId, + bs.batch_id AS logId, + IFNULL(bs.AMOUNT_DR, 0) AS amountDr, + bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName + 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 '房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局' + ) + ) trade + left join ( + select distinct + asset.person_id AS personId + from ccdi_asset_info asset + where asset.asset_main_type = '房产' + and asset.asset_sub_type = '住宅' + and asset.asset_status = '正常' + ) asset + on asset.personId = trade.personId + where asset.personId is null select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '供应商“', t.supplierName, + '”采购金额 ', CAST(t.supplierAmount AS CHAR), + ' 元,占总采购金额 ', CAST(t.supplierRatioPct AS CHAR), '%' + ) AS reasonDetail + from ( + select + supplier_hit.objectKey AS objectKey, + SUBSTRING_INDEX( + GROUP_CONCAT(supplier_hit.supplierName ORDER BY supplier_hit.supplierRatio DESC SEPARATOR ','), + ',', + 1 + ) AS supplierName, + MAX(supplier_hit.supplierAmount) AS supplierAmount, + ROUND(MAX(supplier_hit.supplierRatio) * 100, 2) AS supplierRatioPct + from ( + select + source.objectKey AS objectKey, + source.supplierName AS supplierName, + ROUND(SUM(source.actualAmount), 2) AS supplierAmount, + SUM(source.actualAmount) / NULLIF(total_amount.totalAmount, 0) AS supplierRatio + from ( + select distinct + staff.id_card AS objectKey, + pt.purchase_id AS purchaseId, + pt.supplier_name AS supplierName, + IFNULL(pt.actual_amount, 0) AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.applicant_id + where IFNULL(pt.actual_amount, 0) > 0 + and IFNULL(pt.supplier_name, '') <> '' + + union + + select distinct + staff.id_card AS objectKey, + pt.purchase_id AS purchaseId, + pt.supplier_name AS supplierName, + IFNULL(pt.actual_amount, 0) AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id + where pt.purchase_leader_id is not null + and IFNULL(pt.actual_amount, 0) > 0 + and IFNULL(pt.supplier_name, '') <> '' + ) source + inner join ( + select + source_total.objectKey AS objectKey, + ROUND(SUM(source_total.actualAmount), 2) AS totalAmount + from ( + select distinct + staff.id_card AS objectKey, + pt.purchase_id AS purchaseId, + IFNULL(pt.actual_amount, 0) AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.applicant_id + where IFNULL(pt.actual_amount, 0) > 0 + + union + + select distinct + staff.id_card AS objectKey, + pt.purchase_id AS purchaseId, + IFNULL(pt.actual_amount, 0) AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id + where pt.purchase_leader_id is not null + and IFNULL(pt.actual_amount, 0) > 0 + ) source_total + group by source_total.objectKey + ) total_amount + on total_amount.objectKey = source.objectKey + group by source.objectKey, source.supplierName, total_amount.totalAmount + having SUM(source.actualAmount) / NULLIF(total_amount.totalAmount, 0) > 0.7 + ) supplier_hit + group by supplier_hit.objectKey + ) t select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '工资入账 ', CAST(t.salaryAmount AS CHAR), + ' 元后24小时内转出 ', CAST(t.transferAmount AS CHAR), + ' 元,占比 ', CAST(t.transferRatioPct AS CHAR), '%' + ) AS reasonDetail + from ( + select + salary.objectKey AS objectKey, + MAX(salary.salaryAmount) AS salaryAmount, + MAX(out_trade.transferAmount) AS transferAmount, + ROUND(MAX(out_trade.transferAmount / NULLIF(salary.salaryAmount, 0)) * 100, 2) AS transferRatioPct + from ( + select + staff.id_card AS objectKey, + IFNULL(bs.AMOUNT_CR, 0) AS salaryAmount, + 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 salaryTime + 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_CR, 0) > 0 + and + ) salary + inner join ( + select + salary_source.objectKey AS objectKey, + salary_source.salaryTime AS salaryTime, + ROUND(SUM(IFNULL(out_bs.AMOUNT_DR, 0)), 2) AS transferAmount + from ( + select + staff.id_card AS objectKey, + 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 salaryTime + 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_CR, 0) > 0 + and + ) salary_source + inner join ccdi_bank_statement out_bs + on out_bs.project_id = #{projectId} + and out_bs.cret_no = salary_source.objectKey + and IFNULL(out_bs.AMOUNT_DR, 0) > 0 + and COALESCE( + STR_TO_DATE(LEFT(TRIM(out_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'), + STR_TO_DATE(LEFT(TRIM(out_bs.TRX_DATE), 10), '%Y-%m-%d') + ) > salary_source.salaryTime + and COALESCE( + STR_TO_DATE(LEFT(TRIM(out_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'), + STR_TO_DATE(LEFT(TRIM(out_bs.TRX_DATE), 10), '%Y-%m-%d') + ) <= DATE_ADD(salary_source.salaryTime, INTERVAL 24 HOUR) + group by salary_source.objectKey, salary_source.salaryTime + ) out_trade + on out_trade.objectKey = salary.objectKey + and out_trade.salaryTime = salary.salaryTime + where out_trade.transferAmount / NULLIF(salary.salaryAmount, 0) > 0.8 + group by salary.objectKey + ) t