From 09ca5b43fb26bfc35924f06c689ed5aff1adf223 Mon Sep 17 00:00:00 2001 From: rosercode Date: Mon, 21 Sep 2026 20:30:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=84=E5=88=99=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8E=E5=A4=9A=E9=80=89=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端新增 DELETE /rule/batch,按 userId 校验规则归属,先清理标签关联再批量删除并返回实际删除数;前端规则管理表格增加多选列与批量删除按钮,补充三语文案;同步更新 README 中 AI 渠道推荐与图片记账说明。 --- README.md | 3 +- frontend/src/api/rule.ts | 5 +++ frontend/src/locales/messages.ts | 4 +++ frontend/src/views/RuleManage.vue | 35 ++++++++++++++++++- .../finance/controller/RuleController.java | 14 ++++++++ .../finance/service/RuleService.java | 11 ++++++ .../finance/service/impl/RuleServiceImpl.java | 30 ++++++++++++++++ 7 files changed, 100 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f25fbcf..433b3c8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ ### 交易渠道 - 内置渠道:支付宝、微信、美团、淘宝、京东、招商银行、交通银行、现金 - 支持自定义渠道的新建、改名与删除(有账单使用时不可删除,改名不影响历史账单) +- 新增/编辑记录时可由 AI 推荐已有渠道或生成新渠道 ### 统计与图表 - 月度收支统计、分类占比、收支趋势、分组统计与概览汇总 @@ -35,7 +36,7 @@ ### AI 能力(DeepSeek) - 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳 - 自然语言查询解析与自然语言记账解析 -- 图片记账:上传账单 / 支付截图,由视觉模型识别并生成多条记录,逐条预览勾选后批量导入 +- 图片记账:一次上传最多 5 张账单 / 支付截图(支持粘贴),由视觉模型识别、SSE 实时推送进度,生成多条记录并标记疑似重复(按「类型+金额+时间」),提取原始字段,逐条预览勾选后批量导入 - AI 生成分类、标签,以及 Emoji / 颜色推荐 - AI 调用用量统计与明细查看 diff --git a/frontend/src/api/rule.ts b/frontend/src/api/rule.ts index 960b26a..fd24700 100644 --- a/frontend/src/api/rule.ts +++ b/frontend/src/api/rule.ts @@ -73,6 +73,11 @@ export function deleteRule(id: number) { return request.delete(`/rule/${id}`) } +/** 批量删除规则,返回实际删除数量 */ +export function batchDeleteRules(ids: number[]) { + return request.delete('/rule/batch', { data: { ids } }) +} + /** 启用/停用指定规则 */ export function toggleRuleEnabled(id: number, enabled: boolean) { return request.put(`/rule/${id}/enabled`, { enabled }) diff --git a/frontend/src/locales/messages.ts b/frontend/src/locales/messages.ts index cf22ef9..bbbe606 100644 --- a/frontend/src/locales/messages.ts +++ b/frontend/src/locales/messages.ts @@ -546,6 +546,10 @@ export default { status: { zh: '启用状态', en: 'Status', ja: '有効状態' }, statusOn: { zh: '已启用', en: 'Enabled', ja: '有効' }, statusOff: { zh: '已停用', en: 'Disabled', ja: '無効' }, + batchDelete: { zh: '批量删除', en: 'Batch Delete', ja: '一括削除' }, + batchDeleteNone: { zh: '请先选择要删除的规则', en: 'Please select rules to delete', ja: '削除するルールを選択してください' }, + batchDeleteConfirm: { zh: '确定删除选中的 {count} 条规则吗?', en: 'Delete the {count} selected rule(s)?', ja: '選択した {count} 件のルールを削除しますか?' }, + batchDeleted: { zh: '已删除 {count} 条规则', en: 'Deleted {count} rule(s)', ja: '{count} 件のルールを削除しました' }, }, // 账单导入 diff --git a/frontend/src/views/RuleManage.vue b/frontend/src/views/RuleManage.vue index 091c42a..a137805 100644 --- a/frontend/src/views/RuleManage.vue +++ b/frontend/src/views/RuleManage.vue @@ -7,7 +7,7 @@ import { NInput, NInputNumber, NSelect, NSwitch, NTag, NText, NCard, } from 'naive-ui' import type { DataTableColumns } from 'naive-ui' -import { getRulePage, createRule, updateRule, deleteRule, toggleRuleEnabled } from '../api/rule' +import { getRulePage, createRule, updateRule, deleteRule, batchDeleteRules, toggleRuleEnabled } from '../api/rule' import type { RuleItem } from '../api/rule' import { getCategoryList } from '../api/category' import { getTagList } from '../api/tag' @@ -52,6 +52,7 @@ const total = ref(0) const searchKeyword = ref('') const filterEnabled = ref(null) // 'ON' | 'OFF' | null const filterType = ref(null) +const checkedRuleIds = ref([]) // 表格多选选中的规则ID(用于批量删除) /** AI 规则建议行(带本地 key 供表格选择使用) */ type AiRuleRow = AiRuleSuggestion & { key: number } @@ -162,6 +163,8 @@ function conditionTags(row: RuleItem) { /** 创建表格列配置 */ const columns = computed>(() => [ + // 多选列:用于批量操作(如批量删除) + { type: 'selection' }, { title: t('common.name'), key: 'name', @@ -713,6 +716,31 @@ function handleDelete(id: number) { }) } +/** 批量删除选中的规则:确认后调用接口并刷新列表 */ +function handleBatchDelete() { + const count = checkedRuleIds.value.length + if (count === 0) { + message.warning(t('rule.batchDeleteNone')) + return + } + dialog.warning({ + title: t('common.confirmDelete'), + content: t('rule.batchDeleteConfirm', { count }), + positiveText: t('common.delete'), + negativeText: t('common.cancel'), + onPositiveClick: async () => { + try { + const res: any = await batchDeleteRules(checkedRuleIds.value) + message.success(t('rule.batchDeleted', { count: res.data })) + checkedRuleIds.value = [] + await fetchData() + } catch (e: any) { + message.error(e.message || t('common.deleteFailed')) + } + }, + }) +} + // 收支类型变化时,若已选分类类型不匹配则清空,避免保存非法组合 watch(() => form.value.type, (type) => { if (form.value.categoryId && type) { @@ -739,6 +767,9 @@ onMounted(() => {

{{ t('rule.title') }}

{{ t('ai.rule.entry') }} + + {{ t('rule.batchDelete') }} + {{ t('rule.add') }} @@ -773,11 +804,13 @@ onMounted(() => { batchDelete(@RequestBody @Valid BatchDeleteRequest body) { + Long userId = UserContext.getUserId(); + return Result.success(ruleService.batchDelete(userId, body.getIds())); + } + /** * 启用或停用规则 * diff --git a/src/main/java/cn/rosercode/finance/service/RuleService.java b/src/main/java/cn/rosercode/finance/service/RuleService.java index 26dbae8..22c4e06 100644 --- a/src/main/java/cn/rosercode/finance/service/RuleService.java +++ b/src/main/java/cn/rosercode/finance/service/RuleService.java @@ -56,6 +56,17 @@ public interface RuleService { */ void delete(Long userId, Long id); + /** + * 批量删除规则及其标签关联。 + * + *

仅删除归属当前用户的规则,先清理标签关联再批量删除,返回实际删除数。

+ * + * @param userId 当前用户ID + * @param ids 待删除的规则ID列表 + * @return 实际删除的规则数 + */ + int batchDelete(Long userId, List ids); + /** * 启用或停用规则。 * diff --git a/src/main/java/cn/rosercode/finance/service/impl/RuleServiceImpl.java b/src/main/java/cn/rosercode/finance/service/impl/RuleServiceImpl.java index 8ec96c8..d07399b 100644 --- a/src/main/java/cn/rosercode/finance/service/impl/RuleServiceImpl.java +++ b/src/main/java/cn/rosercode/finance/service/impl/RuleServiceImpl.java @@ -195,6 +195,36 @@ public class RuleServiceImpl implements RuleService { log.info("删除规则: userId={}, id={}", userId, id); } + /** + * {@inheritDoc} + * + * 仅删除归属当前用户的规则,先清理标签关联再批量删除,返回实际删除数。 + */ + @Override + @Transactional + public int batchDelete(Long userId, List ids) { + if (ids == null || ids.isEmpty()) { + return 0; + } + // 仅保留属于当前用户的规则ID + List ownedIds = ruleMapper.selectList(new LambdaQueryWrapper() + .select(Rule::getId) + .eq(Rule::getUserId, userId) + .in(Rule::getId, ids)) + .stream() + .map(Rule::getId) + .collect(Collectors.toList()); + if (ownedIds.isEmpty()) { + return 0; + } + // 先清理标签关联,再批量删除规则 + ruleTagMapper.delete(new LambdaQueryWrapper() + .in(RuleTag::getRuleId, ownedIds)); + int deleted = ruleMapper.deleteByIds(ownedIds); + log.info("批量删除规则: userId={}, 请求={}, 实际={}", userId, ids.size(), deleted); + return deleted; + } + /** * {@inheritDoc} */