修复lsfx删除文件接口logIds解析异常

This commit is contained in:
wkc
2026-03-16 16:25:01 +08:00
parent 7ae3fde8ef
commit e1ee681550
5 changed files with 1034 additions and 4 deletions

214
assets/大额交易.csv Normal file
View File

@@ -0,0 +1,214 @@
序号,模型名称,核心异常点(展示在前端页面),业务口径,相关指标,指标英文名,风险筛查对象,技术口径,代码,限制阈值指标,可疑结果返回,风险等级
1.1,大额交易,房车消费支出交易,备注或对交易对手是房产公司、二手房、车辆销售公司、物业公司等。,购买车房支出金额,prop_exp_amt,员工本人及亲属,关联员工及其亲属所有账户ccdi_account_info 关联 ccdi_fmy_relation_person在 ccdi_bank_statement 中筛选 amount_dr>0 且对手方/摘要含房产/车产关键词,"sql<br>---员工及其亲属购买车房支出流水id
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 (user_memo rlike '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷'
or customer_account_name rlike '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局')
and amount_dr > 0
union all
select 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 (user_memo rlike '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷'
or customer_account_name rlike '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局')
and amount_dr > 0
;",/,流水明细,一般
1.2,,税务支出交易,有税务支出记录,税务支出金额,tax_exp_amt,员工本人及亲属,员工及其亲属账户中,筛选 amount_dr>0 且摘要含税务关键词,"sql<br>----员工及其亲属税务支出流水id
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 (user_memo rlike '税务|缴税|税款'
or customer_account_name rlike '税务|税务局|国库|国家金库|财政')
and amount_dr > 0
union all
select 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 (user_memo rlike '税务|缴税|税款'
or customer_account_name rlike '税务|税务局|国库|国家金库|财政')
and amount_dr > 0
;",/,流水明细,一般
1.3,,大额单笔收入,同一交易对手(除本人、家庭成员外、本单位代发工资)单笔超过设置限额超过设置限额的资金流入;,大额流入金额(单笔),SINGLE_TRANSACTION_AMOUNT,员工本人,员工账户中,筛选 amount_cr>0对手方名称不在该员工的家庭关系内排除工资代发按员工和对手方汇总金额判断单笔是否超限,"sql<br>--员工与同一交易对手非亲属的最大一笔收入交易流水id
select t1.bank_statement_id
from
(
select t1.id_card
,t2.bank_statement_id
,t2.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 t2.le_account_name <> t2.customer_account_name
and not (customer_account_name = '浙江兰溪农村商业银行股份有限公司'
and (user_memo like '%代发%'
or user_memo like '%工资%'
or user_memo like '%奖金%'
or user_memo like '%薪酬%'
or user_memo like '%薪金%'
or user_memo like '%补贴%'
or user_memo like '%薪%'
or user_memo like '%年终奖%'
or user_memo like '%年金%'
or user_memo like '%加班费%'
or user_memo like '%劳务费%'
or user_memo like '%劳务外包%'
or user_memo like '%提成%'
or user_memo like '%劳务派遣%'
or user_memo like '%绩效%'
or user_memo like '%酬劳%'
or user_memo like '%PAYROLL%'
or user_memo like '%SALA%'
or user_memo like '%CPF%'
or user_memo like '%directors%fees%'
or user_memo like '%批量代付%'
or cash_type like '%代发%'
or cash_type like '%工资%'
or cash_type like '%劳务费%' ))
and amount_cr > 0
) t1
left join ccdi_staff_fmy_relation t2
on t1.id_card = t2.person_id
and t1.customer_account_name = t2.relation_name
where t2.person_id is null;",大额流入金额,流水明细,一般
新增,,累计收入超限,同一交易对手(除本人、家庭成员外、本单位代发工资)累计交易金额超过设置限额的资金流入;,累计流入金额(所有累计),CUMULATIVE_TRANSACTION_AMOUNT,员工本人,员工账户中,筛选 amount_cr>0对手方名称不在该员工的家庭关系内排除工资代发按员工和对手方汇总金额判断累计是否超限,"sql<br>--员工与同一交易对手(非亲属)的累计收入交易金额
select
t1.id_card
,t1.customer_account_name
,t1.sum_amount_cr
from
(
select t1.id_card
,customer_account_name
,sum(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 like '%代发%'
or user_memo like '%工资%'
or user_memo like '%奖金%'
or user_memo like '%薪酬%'
or user_memo like '%薪金%'
or user_memo like '%补贴%'
or user_memo like '%薪%'
or user_memo like '%年终奖%'
or user_memo like '%年金%'
or user_memo like '%加班费%'
or user_memo like '%劳务费%'
or user_memo like '%劳务外包%'
or user_memo like '%提成%'
or user_memo like '%劳务派遣%'
or user_memo like '%绩效%'
or user_memo like '%酬劳%'
or user_memo like '%PAYROLL%'
or user_memo like '%SALA%'
or user_memo like '%CPF%'
or user_memo like '%directors%fees%'
or user_memo like '%批量代付%'
or cash_type like '%代发%'
or cash_type like '%工资%'
or cash_type like '%劳务费%' ))
group by id_card,customer_account_name
having sum(amount_cr)>0
) t1
left join ccdi_staff_fmy_relation t2
on t1.id_card = t2.person_id
and t1.customer_account_name = t2.relation_name
where t2.person_id is null;
",累计流入金额,个人、累积金额,一般
1.4,,年流水交易额超限,年流水交易额超过设置限额,年交易金额,annual_turnover,员工本人,员工账户中,排除本人及亲属名称,统计一年内 amount_cr+amount_dr 总额,"sql<br>--员工年交易金额
select t1.id_card
,sum(trans_amount) as annual_trans_amount
from
(
select t1.id_card
,amount_dr + amount_cr as trans_amount
from
ccdi_base_staff t1
inner join
ccdi_bank_statement t2
on t1.id_card = t2.cret_no
where project_id = PROJECT_ID
and left(TRX_DATE,10) >= add_months(current_date(),-12) --近一年
and t2.le_account_name <> t2.customer_account_name --排除同名交易
) t1
left join ccdi_staff_fmy_relation t2
on t1.id_card = t2.person_id
and t1.customer_account_name = t2.relation_name
where t2.person_id is NULL
group by t1.id_card;",年交易金额,个人、累积金额,一般
1.5,,大额存现交易,大额存现,单笔超过设置限额;,大额存现金额(单笔),LARGE_CASH_DEPOSIT,员工本人,员工及其亲属账户中,筛选 现金存入,且单笔 amount_cr 超阈值,按员工汇总,"sql<br>---员工大额存现单流水id
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_cr> $$$$ ---大额存现阈值参数
and (
(((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or
((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%')
)
;
",大额存现金额,流水明细,一般
,,短时间多次存现,短时间多次存现,次数超过设置限额,单日存现总次数,FREQUENT_CASH_DEPOSIT,员工本人,员工及其亲属账户中,按日统计现金存入次数超阈值,"sql<br>--员工及其亲属 大额现金存入次数
select id_card,count(1)
from
(
select t1.id_card
,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 amount_cr> $$$$$$
and (
(((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or
((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%')
)
union all
select t1.person_id
,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 amount_cr> $$$$$$$
and (
(((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or
((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%')
)
)group by id_card
;
",单日存现总次数,个人、日期、次数,一般
1.6,,大额转账交易,大额转账单笔超过设置限额,大额转账金额(单笔),large_tfr_cnt,员工本人,员工及其亲属账户中,筛选单笔 amount_dr 超金额阈值的数据,,大额转账金额,流水明细,一般
1 序号 模型名称 核心异常点(展示在前端页面) 业务口径 相关指标 指标英文名 风险筛查对象 技术口径 代码 限制阈值指标 可疑结果返回 风险等级
2 1.1 大额交易 房车消费支出交易 备注或对交易对手是房产公司、二手房、车辆销售公司、物业公司等。 购买车房支出金额 prop_exp_amt 员工本人及亲属 关联员工及其亲属所有账户(ccdi_account_info 关联 ccdi_fmy_relation_person),在 ccdi_bank_statement 中筛选 amount_dr>0 且对手方/摘要含房产/车产关键词 sql<br>---员工及其亲属购买车房支出流水id 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 (user_memo rlike '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷' or customer_account_name rlike '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局') and amount_dr > 0 union all select 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 (user_memo rlike '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷' or customer_account_name rlike '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局') and amount_dr > 0 ; / 流水明细 一般
3 1.2 税务支出交易 有税务支出记录 税务支出金额 tax_exp_amt 员工本人及亲属 员工及其亲属账户中,筛选 amount_dr>0 且摘要含税务关键词 sql<br>----员工及其亲属税务支出流水id 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 (user_memo rlike '税务|缴税|税款' or customer_account_name rlike '税务|税务局|国库|国家金库|财政') and amount_dr > 0 union all select 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 (user_memo rlike '税务|缴税|税款' or customer_account_name rlike '税务|税务局|国库|国家金库|财政') and amount_dr > 0 ; / 流水明细 一般
4 1.3 大额单笔收入 同一交易对手(除本人、家庭成员外、本单位代发工资)单笔超过设置限额超过设置限额的资金流入; 大额流入金额(单笔) SINGLE_TRANSACTION_AMOUNT 员工本人 员工账户中,筛选 amount_cr>0,对手方名称不在该员工的家庭关系内,排除工资代发,按员工和对手方汇总金额,判断单笔是否超限 sql<br>--员工与同一交易对手(非亲属)的最大一笔收入交易流水id select t1.bank_statement_id from ( select t1.id_card ,t2.bank_statement_id ,t2.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 t2.le_account_name <> t2.customer_account_name and not (customer_account_name = '浙江兰溪农村商业银行股份有限公司' and (user_memo like '%代发%' or user_memo like '%工资%' or user_memo like '%奖金%' or user_memo like '%薪酬%' or user_memo like '%薪金%' or user_memo like '%补贴%' or user_memo like '%薪%' or user_memo like '%年终奖%' or user_memo like '%年金%' or user_memo like '%加班费%' or user_memo like '%劳务费%' or user_memo like '%劳务外包%' or user_memo like '%提成%' or user_memo like '%劳务派遣%' or user_memo like '%绩效%' or user_memo like '%酬劳%' or user_memo like '%PAYROLL%' or user_memo like '%SALA%' or user_memo like '%CPF%' or user_memo like '%directors%fees%' or user_memo like '%批量代付%' or cash_type like '%代发%' or cash_type like '%工资%' or cash_type like '%劳务费%' )) and amount_cr > 0 ) t1 left join ccdi_staff_fmy_relation t2 on t1.id_card = t2.person_id and t1.customer_account_name = t2.relation_name where t2.person_id is null; 大额流入金额 流水明细 一般
5 新增 累计收入超限 同一交易对手(除本人、家庭成员外、本单位代发工资)累计交易金额超过设置限额的资金流入; 累计流入金额(所有累计) CUMULATIVE_TRANSACTION_AMOUNT 员工本人 员工账户中,筛选 amount_cr>0,对手方名称不在该员工的家庭关系内,排除工资代发,按员工和对手方汇总金额,判断累计是否超限 sql<br>--员工与同一交易对手(非亲属)的累计收入交易金额 select t1.id_card ,t1.customer_account_name ,t1.sum_amount_cr from ( select t1.id_card ,customer_account_name ,sum(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 like '%代发%' or user_memo like '%工资%' or user_memo like '%奖金%' or user_memo like '%薪酬%' or user_memo like '%薪金%' or user_memo like '%补贴%' or user_memo like '%薪%' or user_memo like '%年终奖%' or user_memo like '%年金%' or user_memo like '%加班费%' or user_memo like '%劳务费%' or user_memo like '%劳务外包%' or user_memo like '%提成%' or user_memo like '%劳务派遣%' or user_memo like '%绩效%' or user_memo like '%酬劳%' or user_memo like '%PAYROLL%' or user_memo like '%SALA%' or user_memo like '%CPF%' or user_memo like '%directors%fees%' or user_memo like '%批量代付%' or cash_type like '%代发%' or cash_type like '%工资%' or cash_type like '%劳务费%' )) group by id_card,customer_account_name having sum(amount_cr)>0 ) t1 left join ccdi_staff_fmy_relation t2 on t1.id_card = t2.person_id and t1.customer_account_name = t2.relation_name where t2.person_id is null; 累计流入金额 个人、累积金额 一般
6 1.4 年流水交易额超限 年流水交易额超过设置限额 年交易金额 annual_turnover 员工本人 员工账户中,排除本人及亲属名称,统计一年内 amount_cr+amount_dr 总额 sql<br>--员工年交易金额 select t1.id_card ,sum(trans_amount) as annual_trans_amount from ( select t1.id_card ,amount_dr + amount_cr as trans_amount from ccdi_base_staff t1 inner join ccdi_bank_statement t2 on t1.id_card = t2.cret_no where project_id = PROJECT_ID and left(TRX_DATE,10) >= add_months(current_date(),-12) --近一年 and t2.le_account_name <> t2.customer_account_name --排除同名交易 ) t1 left join ccdi_staff_fmy_relation t2 on t1.id_card = t2.person_id and t1.customer_account_name = t2.relation_name where t2.person_id is NULL group by t1.id_card; 年交易金额 个人、累积金额 一般
7 1.5 大额存现交易 大额存现,单笔超过设置限额; 大额存现金额(单笔) LARGE_CASH_DEPOSIT 员工本人 员工及其亲属账户中,筛选 现金存入,且单笔 amount_cr 超阈值,按员工汇总 sql<br>---员工大额存现单流水id 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_cr> $$$$ ---大额存现阈值参数 and ( (((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or ((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%') ) ; 大额存现金额 流水明细 一般
8 短时间多次存现 短时间多次存现,次数超过设置限额 单日存现总次数 FREQUENT_CASH_DEPOSIT 员工本人 员工及其亲属账户中,按日统计现金存入次数超阈值 sql<br>--员工及其亲属 大额现金存入次数 select id_card,count(1) from ( select t1.id_card ,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 amount_cr> $$$$$$ and ( (((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or ((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%') ) union all select t1.person_id ,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 amount_cr> $$$$$$$ and ( (((user_memo like '%现金%' and user_memo not like '%金管理%' and user_memo not like '%金添利%' and user_memo not like '%现金利%' and user_memo not like '%现金宝%' and user_memo not like '%金分析%' ) or user_memo like '%存现%' or user_memo like '%现存%' or cash_type like '%现金%' or cash_type like '%存现%' or cash_type like '%现存%' or cash_type like '%金存入%' or user_memo like '%金存入%' or (user_memo like '%ATM%' and (user_memo like '%存款%' or user_memo like '%转入%')) or (cash_type like '%ATM%' and (cash_type like '%存款%' or cash_type like '%转入%'))) and (customer_account_name = '' or customer_account_name = '无' or customer_account_name like '%存现%') or user_memo like '%DEPOSIT%') or ((customer_account_name = '库存现金' or ((user_memo like '%现金存款%' or user_memo like '%自助存款%' or user_memo like '%CRS存款%' or cash_type like '%现金存款%' or cash_type like '%自助存款%' or cash_type like '%本行CRS存款%' or cash_type like '%柜面%' or user_memo like '%柜面%') and customer_account_name = '' )) or (customer_account_name = '现金' and user_memo not like '%借款%') or user_memo like '%本行ATM%') ) )group by id_card ; 单日存现总次数 个人、日期、次数 一般
9 1.6 大额转账交易 大额转账单笔超过设置限额 大额转账金额(单笔) large_tfr_cnt 员工本人 员工及其亲属账户中,筛选单笔 amount_dr 超金额阈值的数据 大额转账金额 流水明细 一般

View File

@@ -0,0 +1,410 @@
# Project Upload File Delete Backend Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 为项目上传文件列表新增后端删除能力,支持删除已解析成功的文件、清理本地流水,并把上传记录状态更新为 `deleted`
**Architecture:**`CcdiFileUploadController` 新增按记录 ID 删除接口,由 Controller 获取当前登录用户 ID 并传给 `ICcdiFileUploadService`。Service 负责查询记录、校验状态、调用 `LsfxAnalysisClient.deleteFiles`、删除 `ccdi_bank_statement` 中对应 `logId` 的流水,并更新 `ccdi_file_upload_record.file_status``deleted`;统计 VO 同步扩展 `deleted` 状态。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito, Maven
---
### Task 1: 补齐删除接口控制器契约
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java`
**Step 1: Write the failing test**
`CcdiFileUploadControllerTest` 中新增测试,验证删除接口会读取当前登录用户 ID 并调用 Service
```java
@Test
void deleteFile_shouldUseCurrentLoginUserId() {
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
.thenReturn("删除成功");
AjaxResult result = controller.deleteFile(123L);
assertEquals(200, result.get("code"));
assertEquals("删除成功", result.get("msg"));
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
}
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId
```
Expected:
- `FAIL`
- 原因是 `CcdiFileUploadController` 中还没有 `deleteFile` 方法或 `ICcdiFileUploadService` 中还没有 `deleteFileUploadRecord` 方法
**Step 3: Write minimal implementation**
在接口和控制器中补最小实现:
```java
String deleteFileUploadRecord(Long id, Long operatorUserId);
```
```java
@DeleteMapping("/{id}")
@Operation(summary = "删除上传文件", description = "按上传记录ID删除文件并清理流水")
public AjaxResult deleteFile(@PathVariable Long id) {
Long userId = SecurityUtils.getUserId();
String message = fileUploadService.deleteFileUploadRecord(id, userId);
return AjaxResult.success(message);
}
```
**Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java
git commit -m "test: 补充上传文件删除接口控制器契约"
```
### Task 2: 实现删除成功主链路
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Write the failing test**
`CcdiFileUploadServiceImplTest` 中新增成功链路测试:
```java
@Test
void deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted() {
CcdiFileUploadRecord record = buildRecord();
record.setProjectId(PROJECT_ID);
record.setLsfxProjectId(LSFX_PROJECT_ID);
record.setLogId(LOG_ID);
record.setFileStatus("parsed_success");
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
when(lsfxClient.deleteFiles(any())).thenReturn(buildDeleteFilesResponse());
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
assertEquals("删除成功", result);
verify(lsfxClient).deleteFiles(argThat(request ->
request.getGroupId().equals(LSFX_PROJECT_ID)
&& request.getUserId().equals(9527)
&& request.getLogIds().length == 1
&& request.getLogIds()[0].equals(LOG_ID)
));
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
verify(recordMapper).updateById(argThat(item ->
RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus())
));
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted
```
Expected:
- `FAIL`
- 原因是 `CcdiFileUploadServiceImpl` 中还没有 `deleteFileUploadRecord` 实现
**Step 3: Write minimal implementation**
`CcdiFileUploadServiceImpl` 中实现删除主链路:
```java
@Override
@Transactional
public String deleteFileUploadRecord(Long id, Long operatorUserId) {
CcdiFileUploadRecord record = recordMapper.selectById(id);
validateDeleteRecord(record);
DeleteFilesRequest request = new DeleteFilesRequest();
request.setGroupId(record.getLsfxProjectId());
request.setLogIds(new Integer[]{record.getLogId()});
request.setUserId(toUploadUserId(operatorUserId));
DeleteFilesResponse response = lsfxClient.deleteFiles(request);
if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) {
throw new RuntimeException("流水分析平台删除文件失败");
}
bankStatementMapper.deleteByProjectIdAndBatchId(record.getProjectId(), record.getLogId());
CcdiFileUploadRecord update = new CcdiFileUploadRecord();
update.setId(record.getId());
update.setFileStatus("deleted");
recordMapper.updateById(update);
return "删除成功";
}
```
同时补一个私有校验方法,仅先满足成功路径所需字段。
**Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "feat: 打通上传文件删除成功主链路"
```
### Task 3: 补齐删除前置校验与失败保护
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Write the failing test**
为关键边界新增失败测试,至少覆盖“状态不允许删除”和“平台删除失败时不应更新本地状态”:
```java
@Test
void deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus() {
CcdiFileUploadRecord record = buildRecord();
record.setFileStatus("parsed_failed");
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
assertTrue(exception.getMessage().contains("仅支持删除解析成功文件"));
}
@Test
void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() {
CcdiFileUploadRecord record = buildRecord();
record.setFileStatus("parsed_success");
record.setLogId(LOG_ID);
record.setLsfxProjectId(LSFX_PROJECT_ID);
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
when(lsfxClient.deleteFiles(any())).thenThrow(new RuntimeException("lsfx delete failed"));
assertThrows(RuntimeException.class, () -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
verify(bankStatementMapper, org.mockito.Mockito.never()).deleteByProjectIdAndBatchId(any(), any());
verify(recordMapper, org.mockito.Mockito.never()).updateById(argThat(item ->
"deleted".equals(item.getFileStatus())
));
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails
```
Expected:
- `FAIL`
- 原因是现有实现还没有完整的前置校验和失败保护
**Step 3: Write minimal implementation**
补全 Service 校验和异常处理:
```java
private void validateDeleteRecord(CcdiFileUploadRecord record) {
if (record == null) {
throw new RuntimeException("上传记录不存在");
}
if (!"parsed_success".equals(record.getFileStatus())) {
if ("deleted".equals(record.getFileStatus())) {
throw new RuntimeException("文件已删除,请勿重复操作");
}
throw new RuntimeException("仅支持删除解析成功文件");
}
if (record.getLsfxProjectId() == null) {
throw new RuntimeException("缺少流水分析项目ID");
}
if (record.getLogId() == null) {
throw new RuntimeException("缺少文件logId");
}
}
```
不要吞掉平台删除异常,让事务直接回滚。
**Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "test: 补齐上传文件删除校验与失败保护"
```
### Task 4: 扩展统计口径支持 `deleted`
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Write the failing test**
新增统计测试,验证 `deleted` 状态会被正确映射到 VO
```java
@Test
void countByStatus_shouldIncludeDeletedCount() {
when(recordMapper.countByStatus(PROJECT_ID)).thenReturn(List.of(
Map.of("status", "uploading", "count", 1),
Map.of("status", "deleted", "count", 2)
));
CcdiFileUploadStatisticsVO result = service.countByStatus(PROJECT_ID);
assertEquals(1L, result.getUploading());
assertEquals(2L, result.getDeleted());
assertEquals(3L, result.getTotal());
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount
```
Expected:
- `FAIL`
- 原因是 `CcdiFileUploadStatisticsVO` 还没有 `deleted` 字段,或 `countByStatus` 还没有映射该状态
**Step 3: Write minimal implementation**
在 VO 和 Service 中补 `deleted` 字段及映射:
```java
private Long deleted;
```
```java
vo.setDeleted(0L);
case "deleted" -> vo.setDeleted(count);
```
**Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "feat: 扩展上传文件统计支持已删除状态"
```
### Task 5: 跑后端回归验证
**Files:**
- Verify only: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
- Verify only: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Verify only: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java`
- Verify only: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Run focused tests**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest,CcdiFileUploadServiceImplTest
```
Expected:
- 全部 `PASS`
**Step 2: Run module compile**
Run:
```bash
mvn clean compile -pl ccdi-project -am
```
Expected:
- `BUILD SUCCESS`
**Step 3: Record manual API smoke checklist**
手工检查以下接口行为:
- `DELETE /ccdi/file-upload/{id}` 删除 `parsed_success` 记录返回成功
- 删除 `parsed_failed` 记录返回错误
- 重复删除 `deleted` 记录返回错误
**Step 4: Commit**
```bash
git add .
git commit -m "test: 完成上传文件删除后端回归验证"
```

View File

@@ -0,0 +1,362 @@
# Project Upload File Delete Frontend Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在项目详情-上传数据-上传文件列表中新增操作列,实现“查看错误原因”和“删除”交互,并让删除后的记录显示为“已删除”。
**Architecture:** 先把状态到“操作/标签”的判断抽取到一个轻量 helper通过 Node 可执行脚本做最小自动化校验;然后在 `ccdiProjectUpload.js` 新增删除接口,在 `UploadData.vue` 中接入操作列、删除确认框、删除后刷新列表与统计。保留现有轮询机制,不新增前端全局状态管理。
**Tech Stack:** Vue 2, Element UI, Axios request wrapper, Node script verification, npm build
---
### Task 1: 抽取状态与操作规则并先写失败测试
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/uploadFileActionRules.mjs`
- Create: `tests/frontend/upload-file-action-rules.test.mjs`
**Step 1: Write the failing test**
新建一个轻量 Node 规则测试脚本,先定义期望行为:
```javascript
import assert from "node:assert/strict";
import {
getUploadFileAction,
getUploadFileStatusText,
getUploadFileStatusType
} from "../../ruoyi-ui/src/views/ccdiProject/components/detail/uploadFileActionRules.mjs";
assert.deepEqual(getUploadFileAction("parsed_failed"), { key: "viewError", text: "查看错误原因" });
assert.deepEqual(getUploadFileAction("parsed_success"), { key: "delete", text: "删除" });
assert.equal(getUploadFileAction("deleted"), null);
assert.equal(getUploadFileStatusText("deleted"), "已删除");
assert.equal(getUploadFileStatusType("deleted"), "info");
```
**Step 2: Run test to verify it fails**
Run:
```bash
node tests/frontend/upload-file-action-rules.test.mjs
```
Expected:
- `FAIL`
- 原因是 helper 文件还不存在
**Step 3: Write minimal implementation**
创建 helper 文件:
```javascript
export function getUploadFileAction(status) {
const actionMap = {
parsed_failed: { key: "viewError", text: "查看错误原因" },
parsed_success: { key: "delete", text: "删除" }
};
return actionMap[status] || null;
}
export function getUploadFileStatusText(status) {
const map = {
uploading: "上传中",
parsing: "解析中",
parsed_success: "解析成功",
parsed_failed: "解析失败",
deleted: "已删除"
};
return map[status] || status;
}
export function getUploadFileStatusType(status) {
const map = {
uploading: "primary",
parsing: "warning",
parsed_success: "success",
parsed_failed: "danger",
deleted: "info"
};
return map[status] || "info";
}
```
**Step 4: Run test to verify it passes**
Run:
```bash
node tests/frontend/upload-file-action-rules.test.mjs
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/uploadFileActionRules.mjs tests/frontend/upload-file-action-rules.test.mjs
git commit -m "test: 补充上传文件操作规则测试"
```
### Task 2: 新增删除 API 契约
**Files:**
- Modify: `ruoyi-ui/src/api/ccdiProjectUpload.js`
**Step 1: Write the failing usage**
先在计划中约定组件调用的新 API 形式,后续组件实现前必须存在:
```javascript
export function deleteFileUploadRecord(id) {
return request({
url: `/ccdi/file-upload/${id}`,
method: "delete"
});
}
```
**Step 2: Run a quick import smoke check**
Run:
```bash
node -e "const fs=require('fs'); const text=fs.readFileSync('ruoyi-ui/src/api/ccdiProjectUpload.js','utf8'); if(!text.includes('deleteFileUploadRecord')) process.exit(1);"
```
Expected:
- `FAIL`
**Step 3: Write minimal implementation**
把旧的按 `projectId + uploadType` 删除接口保留不动,新加按记录 ID 删除的方法:
```javascript
export function deleteFileUploadRecord(id) {
return request({
url: `/ccdi/file-upload/${id}`,
method: "delete"
});
}
```
**Step 4: Run smoke check to verify it passes**
Run:
```bash
node -e "const fs=require('fs'); const text=fs.readFileSync('ruoyi-ui/src/api/ccdiProjectUpload.js','utf8'); if(!text.includes('deleteFileUploadRecord')) process.exit(1);"
```
Expected:
- 退出码 `0`
**Step 5: Commit**
```bash
git add ruoyi-ui/src/api/ccdiProjectUpload.js
git commit -m "feat: 新增上传文件按记录删除接口"
```
### Task 3: 在列表中渲染操作列和错误原因弹窗
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/uploadFileActionRules.mjs`
- Test: `tests/frontend/upload-file-action-rules.test.mjs`
**Step 1: Write the failing test**
先扩充规则测试,确保无操作状态不会返回按钮:
```javascript
assert.equal(getUploadFileAction("uploading"), null);
assert.equal(getUploadFileAction("parsing"), null);
```
**Step 2: Run test to verify it fails**
Run:
```bash
node tests/frontend/upload-file-action-rules.test.mjs
```
Expected:
- 如果 helper 暂未覆盖全部状态,则 `FAIL`
**Step 3: Write minimal implementation**
`UploadData.vue` 中:
- 引入 helper
- 给表格新增“操作”列
- 根据 `getUploadFileAction(scope.row.fileStatus)` 渲染按钮
- 增加 `handleViewError(row)`,直接读取 `row.errorMessage || "未知错误"` 并调用:
```javascript
this.$alert(row.errorMessage || "未知错误", "错误信息", {
confirmButtonText: "确定",
type: "error"
});
```
表格模板示例:
```vue
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="scope">
<el-button
v-if="getRowAction(scope.row)"
type="text"
@click="handleRowAction(scope.row)"
>
{{ getRowAction(scope.row).text }}
</el-button>
</template>
</el-table-column>
```
**Step 4: Run test to verify it passes**
Run:
```bash
node tests/frontend/upload-file-action-rules.test.mjs
```
Expected:
- `PASS`
**Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue ruoyi-ui/src/views/ccdiProject/components/detail/uploadFileActionRules.mjs tests/frontend/upload-file-action-rules.test.mjs
git commit -m "feat: 新增上传文件列表操作列与错误原因查看"
```
### Task 4: 接入删除确认与刷新逻辑
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- Modify: `ruoyi-ui/src/api/ccdiProjectUpload.js`
**Step 1: Write the failing manual checklist**
先记录需要被满足的行为:
- `parsed_success` 行点击“删除”必须弹出二次确认
- 确认后调用 `deleteFileUploadRecord(row.id)`
- 成功后提示“删除成功”
- 成功后刷新 `loadStatistics()``loadFileList()`
- 若仍存在 `uploading/parsing` 记录,则继续轮询;否则不重复启动轮询
**Step 2: Implement the minimal component code**
`UploadData.vue` 新增删除逻辑:
```javascript
async handleDeleteFile(row) {
await this.$confirm(
"删除后将同步删除流水分析平台中的文件,并清除本系统中该文件对应的所有银行流水数据,是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}
);
await deleteFileUploadRecord(row.id);
this.$message.success("删除成功");
await Promise.all([this.loadStatistics(), this.loadFileList()]);
if (this.statistics.uploading > 0 || this.statistics.parsing > 0) {
this.startPolling();
}
}
```
并在统一入口 `handleRowAction(row)` 中根据 action key 分流到:
- `viewError`
- `delete`
**Step 3: Run build to verify no syntax errors**
Run:
```bash
cd ruoyi-ui
npm run build:prod
```
Expected:
- `BUILD SUCCESS` 或等价的前端打包成功输出
**Step 4: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue ruoyi-ui/src/api/ccdiProjectUpload.js
git commit -m "feat: 接入上传文件删除确认与刷新逻辑"
```
### Task 5: 完成前端手工回归验证
**Files:**
- Verify only: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- Verify only: `ruoyi-ui/src/api/ccdiProjectUpload.js`
- Verify only: `tests/frontend/upload-file-action-rules.test.mjs`
**Step 1: Run rules test**
Run:
```bash
node tests/frontend/upload-file-action-rules.test.mjs
```
Expected:
- `PASS`
**Step 2: Run production build**
Run:
```bash
cd ruoyi-ui
npm run build:prod
```
Expected:
- 打包成功
**Step 3: Perform manual UI verification**
手工验证清单:
- `parsed_failed` 行显示“查看错误原因”
- 点击后能弹出错误信息
- `parsed_success` 行显示“删除”
- 点击删除会出现确认框
- 删除成功后状态显示为“已删除”
- `deleted` 行不再显示任何操作按钮
**Step 4: Commit**
```bash
git add .
git commit -m "test: 完成上传文件删除前端回归验证"
```

View File

@@ -1,4 +1,6 @@
from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form, Query
import json
from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form, HTTPException, Query
from services.token_service import TokenService
from services.file_service import FileService
from services.statement_service import StatementService
@@ -14,6 +16,32 @@ file_service = FileService()
statement_service = StatementService()
def _parse_log_ids(log_ids: str) -> List[int]:
"""兼容逗号分隔和 JSON 数组两种 logIds 传参格式。"""
raw_value = log_ids.strip()
if not raw_value:
raise HTTPException(status_code=422, detail="logIds 不能为空")
try:
if raw_value.startswith("["):
parsed = json.loads(raw_value)
if not isinstance(parsed, list):
raise ValueError
values = parsed
else:
values = [item.strip() for item in raw_value.split(",") if item.strip()]
if not values:
raise ValueError
return [int(item) for item in values]
except (TypeError, ValueError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=422,
detail="logIds 必须是逗号分隔的数字字符串或 JSON 数组"
) from exc
# ==================== 接口1获取Token ====================
@router.post("/account/common/getToken")
async def get_token(
@@ -144,15 +172,14 @@ async def get_upload_status(
@router.post("/watson/api/project/batchDeleteUploadFile")
async def delete_files(
groupId: int = Form(..., description="项目id"),
logIds: str = Form(..., description="文件id数组逗号分隔,如: 10001,10002"),
logIds: str = Form(..., description="文件id数组支持 10001,10002 或 [10001,10002]"),
userId: int = Form(..., description="用户柜员号"),
):
"""批量删除上传的文件
根据logIds列表删除对应的文件记录
"""
# 将逗号分隔的字符串转换为整数列表
log_id_list = [int(id.strip()) for id in logIds.split(",")]
log_id_list = _parse_log_ids(logIds)
return file_service.delete_files(groupId, log_id_list, userId)

View File

@@ -174,3 +174,20 @@ def test_field_completeness(client):
for field in required_fields:
assert field in log, f"缺少字段: {field}"
def test_delete_files_accepts_array_style_log_ids(client):
"""测试删除文件接口兼容数组风格的 logIds 入参"""
response = client.post(
"/watson/api/project/batchDeleteUploadFile",
data={
"groupId": 1000,
"logIds": "[50689]",
"userId": 902001,
}
)
assert response.status_code == 200
data = response.json()
assert data["code"] == "200 OK"
assert data["message"] == "delete.files.success"