Files
xiaocai/docs/arch-iteration-v2.md
wangxiaogang 61f9b33f8c feat(iter-v2): T01 partial - TOKEN_SECRET check, SQL param, export credential, docs
- S2: logs.ts LIMIT/OFFSET SQL parameterization (? placeholders)
- S3: checkTokenSecret() startup validation, JWT_SECRET→TOKEN_SECRET in backup.ts
- S3: Access Token expiry shortened to 2h (from 30d)
- S4: Export download uses HMAC signed short-term token (/prepare endpoint)
- S4: Frontend export.ts adapted for prepare-then-download flow
- .env.example reorganized with security notes
- Added PRD and architecture docs for iteration v2
2026-06-11 08:58:24 +08:00

41 KiB
Raw Permalink Blame History

小菜记账 — 全量迭代架构设计 v2

版本: v2.0 | 日期: 2026-06-10 架构师: 高见远Gao 基线: PRD v2.035 项改进点6 个维度) 技术栈: Uni-app + Vue 3 + Pinia + SCSS (前端) | Node.js + Express + MySQL 8.0 (后端)


一、实现方案 + 技术选型

1.1 安全修复方案

S1.env 凭据泄露修复

项目 方案
核心思路 凭据轮换 + Git 历史重写 + pre-commit 防护
工具 git-filter-repoPython比 BFG 更安全,支持 blob 回调替换)
防护 .githooks/pre-commit 拦截 .env 文件提交
回滚 操作前 git clone --mirror 完整备份
执行顺序 ① 轮换所有凭据 → ② 重写 Git 历史 → ③ 强制推送 → ④ 验证 .gitignore

S2logs.ts SQL 拼接修复

项目 方案
当前问题 LIMIT ${pSize} OFFSET ${offset} 模板字符串直接拼接
修复方案 parseInt + 范围校验后使用 pool.query + ? 占位符
关键点 pool.query 支持 ? 占位符用于整数参数;pool.execute 的 prepared statement 对 LIMIT/OFFSET 类型要求严格,故选用 pool.query

S3TOKEN_SECRET 弱默认值 + 命名不一致

项目 方案
核心思路 新增 server/src/config/token.ts 集中管理,移除所有 fallback
命名统一 backup.tsJWT_SECRETTOKEN_SECRET;所有引用改为 import { TOKEN_SECRET } from '../config/token'
启动校验 TOKEN_SECRET 未设置或长度 < 32 时 process.exit(1)
密钥生成 openssl rand -hex 32 生成 64 字符强密钥

S4Export/Backup 短期一次性下载凭证

项目 方案
核心思路 新增 download_tokens 表,生成随机凭证(crypto.randomBytes(32)
有效期 5 分钟,一次性使用后标记 used_at
存储 MySQL 表(写入频率极低,无需 Redis
清理 健康检查时附带清理 expires_at < NOW() 的记录
改动 backup.tsexport.ts 的下载逻辑改为先获取凭证再用凭证下载

S5Refresh Token 机制

项目 方案
架构 双 Token 机制Access Token2h+ Refresh Token30d
存储 refresh_tokens 表,仅存储 SHA-256 hash不存明文
轮换 每次刷新后旧 Refresh Token 立即失效(revoked_at),返回新 Token
前端适配 request.ts 401 → 尝试 refresh → 成功则重试原请求 → refresh 也失败则清 token 跳登录
安全 修改密码时撤销所有 Refresh Token单用户最多 5 个有效 Refresh Token
优势 保持现有 HMAC-SHA256 签名方式不变,仅缩短 Access Token 有效期

S6Admin 硬删除 → 软删除 + 二次确认

项目 方案
DB 变更 users 表新增 deleted_at DATETIME DEFAULT NULL + 索引
删除操作 UPDATE users SET deleted_at = NOW() WHERE id = ?
恢复接口 POST /api/admin/users/:id/restoreSET deleted_at = NULL
全局过滤 所有涉及 users 表的查询增加 WHERE deleted_at IS NULL
前端确认 管理页点击删除 → 弹窗二次确认 → 调用软删除接口

S7DB 连接移除默认凭据

项目 方案
修复 移除所有 `
启动校验 遍历必需环境变量,缺失任一即 process.exit(1)

1.2 通用组件抽取方案

ManageList 组件

client/src/components/ManageList/ManageList.vue

Props:
  - items: T[]               // 列表数据
  - displayField: string      // 显示字段名
  - colorField?: string       // 颜色字段名(标签用)
  - iconField?: string        // 图标字段名(分类用)
  - showDrag?: boolean        // 是否显示拖拽排序
  - showEdit?: boolean        // 是否显示编辑按钮
  - showDelete?: boolean      // 是否显示删除按钮
  - emptyText?: string        // 空状态文案

Emits:
  - add()
  - edit(item: T)
  - delete(item: T)
  - sort(ids: number[])

Slots:
  - item-icon="{ item }"      // 自定义图标区域(分类用 CategoryIcon
  - item-badge="{ item }"     // 自定义标签区域

ColorPicker 组件

client/src/components/ColorPicker/ColorPicker.vue

Props:
  - modelValue: string        // 当前选中颜色
  - colors?: string[]         // 可选颜色列表(默认 8 色)

Emits:
  - update:modelValue(color: string)

EditModal 组件

client/src/components/EditModal/EditModal.vue

Props:
  - visible: boolean
  - title: string
  - fields: EditField[]       // 动态字段配置
  - modelValue: Record<string, any>

Emits:
  - update:visible(val: boolean)
  - confirm(data: Record<string, any>)

interface EditField {
  key: string
  label: string
  type: 'text' | 'color'
  placeholder?: string
  maxlength?: number
}

1.3 乐观更新实现模式

模式:先写本地 state再异步调接口失败则回滚

async function addCategory(data) {
  // 1. 生成临时 ID负数避免与真实 ID 冲突)
  const tempId = -Date.now()
  const optimisticItem = { id: tempId, ...data }

  // 2. 立即更新本地 state
  categories.value.push(optimisticItem)

  try {
    // 3. 调用 API
    const result = await api.createCategory(data)

    // 4. 用真实 ID 替换临时 ID
    const idx = categories.value.findIndex(c => c.id === tempId)
    if (idx !== -1) categories.value[idx].id = result.id
  } catch (err) {
    // 5. 回滚:移除临时项
    categories.value = categories.value.filter(c => c.id !== tempId)
    uni.showToast({ title: '操作失败', icon: 'none' })
  }
}

适用范围:
  ✅ 新增分类/标签、编辑分类/标签、拖拽排序
  ❌ 删除操作仍等接口确认(防误删)

1.4 Refresh Token 实现方案

┌──────────┐     POST /auth/login      ┌──────────┐
│  Client  │ ──────────────────────────>│  Server  │
│          │<──────────────────────────│          │
│          │  { accessToken, refreshToken }       │
│          │                            │          │
│          │  GET /api/xxx              │          │
│          │  Header: Bearer <accessToken>       │
│          │ ──────────────────────────>│          │
│          │<──────────────────────────│          │
│          │  200 OK                    │          │
│          │                            │          │
│          │  GET /api/xxx (token 过期)  │          │
│          │ ──────────────────────────>│          │
│          │<──────────────────────────│          │
│          │  401 { code: 40101 }       │          │
│          │                            │          │
│          │  POST /auth/refresh        │          │
│          │  { refreshToken }          │          │
│          │ ──────────────────────────>│          │
│          │<──────────────────────────│          │
│          │  { accessToken, refreshToken }       │
│          │                            │          │
│          │  重试原请求                 │          │
│          │ ──────────────────────────>│          │
└──────────┘                            └──────────┘

后端关键实现

  • auth.ts 登录成功时:生成 Access Token2h+ Refresh TokenrandomBytes(32)),存 hash 到 refresh_tokens
  • 新增 POST /auth/refresh:验证 Refresh Token hash → 撤销旧 Token → 生成新双 Token
  • middleware/auth.tsAccess Token 过期返回 40101(区别于无效 Token 的 40100

前端关键实现

  • request.ts:收到 40101 → 调用 /auth/refresh → 成功则重试 → 失败则清 token 跳登录
  • 存储:xc:accessToken + xc:refreshToken 分开存储
  • 并发请求时只触发一次 refresh其他请求排队等待

1.5 软删除方案

影响范围:
  - users 表:新增 deleted_at 字段
  - 所有 JOIN users 的查询:增加 deleted_at IS NULL 过滤
  - admin.tsDELETE → UPDATE SET deleted_at
  - 新增恢复接口
  - 登录验证:检查 deleted_at IS NULL

具体改动位置:
  1. schema.sql / migrate.sqlALTER TABLE
  2. auth.ts 登录WHERE openid = ? AND deleted_at IS NULL
  3. admin.ts 用户列表WHERE deleted_at IS NULL或增加"已禁用"筛选)
  4. transaction.ts 列表查询JOIN users 时增加 u.deleted_at IS NULL
  5. 群组统计/成员列表:增加 deleted_at 过滤

1.6 iconfont 迁移方案

当前状态Icon.vue 使用 PNG 图片映射
迁移方案:
  1. 在 iconfont.cn 创建项目,上传 SVG 图标
  2. 生成字体文件ttf放入 client/src/static/iconfont/
  3. 新建 client/src/styles/iconfont.scss@font-face 声明
  4. Icon.vue 改为 <text class="iconfont icon-xxx" /> 方式渲染
  5. 保留 name prop 接口不变,内部映射改为 class 名
  6. 额外图标(如 lock直接在 iconfont 项目中添加

收益:
  - 包体积减小(字体 < 20KB vs 多个 PNG
  - 支持动态颜色color prop 直接生效)
  - 新增图标只需上传 SVG无需切图

1.7 离线队列方案

架构设计:

1. 离线队列管理器client/src/utils/offline.ts
   - pendingOps: 存储于 uni.setStorageSync('xc:pendingOps')
   - 数据结构: Array<{ id: string, type: 'create', data: object, createdAt: number, status: 'pending' | 'failed' }>
   - MVP 仅支持 create新增记账

2. 网络状态监听
   - App.vue onLaunch 中注册 uni.onNetworkStatusChange
   - 网络恢复时自动调用 processPendingOps()

3. 操作流程
   - 有网络:正常调接口
   - 无网络:
     ├── 写入 pendingOps
     ├── 本地 state 立即更新(乐观更新 + "待同步"标记)
     └── Toast "已保存,将在网络恢复后同步"

4. 同步流程
   - 按时间顺序逐条执行
   - 全部成功:清除队列 + toast "同步完成"
   - 部分失败:标记失败项 + 保留在队列 + toast "N 条同步失败"

5. 网络检测
   - uni.getNetworkType() 获取当前网络状态
   - 封装 isOnline(): boolean 工具函数

1.8 财务报告推送方案

架构设计:

1. 定时任务server/src/services/scheduler.ts
   - 使用 node-cron 实现
   - 每周一 09:00生成上周收支摘要 → 推送
   - 每月 1 号 09:00生成上月收支报告 → 推送

2. 报告生成server/src/services/report.ts
   - 查询指定时间范围的收支数据
   - 生成摘要文本(总支出/收入、Top 分类、日均消费等)

3. 微信订阅消息server/src/services/wechat-subscribe.ts
   - 调用微信 subscribeMessage.send 接口
   - 模板 ID 需在微信公众平台申请
   - 用户需先授权订阅(一次性授权,每次推送需用户主动触发授权)

4. 降级策略
   - 优先站内通知(现有 notifications 系统)
   - 微信订阅消息作为增强项
   - 如果模板审核不通过,仅保留站内通知

5. 用户设置
   - user_settings 表存储推送开关
   - 客户端设置页增加"财务报告推送"开关

1.9 预算预警方案

架构设计:

1. 双重检查策略
   - 实时检查:记账后立即检查预算(精准、低开销)
   - 每日全量扫描:凌晨定时扫描所有用户预算(覆盖周期记账等非手动场景)

2. 预警等级
   - 80%站内通知type: 'personal', is_urgent: false
   - 100%:站内通知 + 微信订阅消息
   - 120%站内紧急通知is_urgent: true+ 微信订阅消息

3. 去重机制
   - 同一用户同一月同一等级只推送一次
   - budget_alerts 表记录已推送的预警user_id + month + level → UNIQUE

4. 触发点
   - transaction.ts POST 创建记账后 → 调用 checkBudgetAlert(userId, month)
   - scheduler.ts 每日扫描 → 批量 checkBudgetAlert

1.10 测试框架选型

层级 框架 理由
后端集成测试 Jest + supertest Express 生态标配supertest 无需启动真实服务器;与 ts-jest 配合良好
前端 Store 测试 Vitest + @pinia/testing Vitest 与 Vite 原生集成Uni-app 基于 Vite@pinia/testing 提供 createTestingPinia()
工具函数测试 Jest后端+ Vitest前端 纯函数测试最简单,适合作为测试练手
API 契约测试 Jest + ajvJSON Schema 轻量级方案,不需要引入完整的契约测试框架

二、文件列表及相对路径

2.1 新建文件

文件路径 用途
server/src/config/token.ts TOKEN_SECRET 集中配置与启动校验
server/src/services/scheduler.ts 定时任务调度node-cron
server/src/services/report.ts 财务报告生成逻辑
server/src/services/budget-alert.ts 预算预警检查与推送逻辑
server/src/services/wechat-subscribe.ts 微信订阅消息发送封装
server/src/utils/upload.ts 公共上传处理函数C3 抽取)
server/src/routes/report.ts 财务报告 API 路由
server/tests/setup.ts 测试数据库初始化与清理
server/tests/auth.test.ts 认证路由集成测试
server/tests/transaction.test.ts 交易路由集成测试
server/tests/category.test.ts 分类路由集成测试
server/tests/budget.test.ts 预算路由集成测试
server/tests/stats.test.ts 统计路由集成测试
server/jest.config.js Jest 配置
server/.env.test 测试数据库环境变量
client/src/components/ManageList/ManageList.vue 通用管理列表组件
client/src/components/ColorPicker/ColorPicker.vue 颜色选择器组件
client/src/components/EditModal/EditModal.vue 通用编辑弹窗组件
client/src/components/Snackbar/Snackbar.vue 删除撤销 Snackbar 组件
client/src/utils/offline.ts 离线队列管理器
client/src/utils/avatar.ts 头像 URL 拼接工具函数C9
client/src/static/iconfont/ iconfont 字体文件目录
client/src/styles/iconfont.scss iconfont 样式声明
client/tests/stores/transaction.test.ts 交易 Store 单元测试
client/tests/stores/category.test.ts 分类 Store 单元测试
client/tests/utils/format.test.ts 格式化工具函数测试
client/tests/utils/app-ready.test.ts app-ready 工具函数测试
client/vitest.config.ts Vitest 配置
.githooks/pre-commit 防止 .env 提交的 Git 钩子

2.2 修改文件

文件路径 主要变更
后端 - 安全
server/src/db/connection.ts 移除默认凭据 fallback增加启动校验
server/src/middleware/auth.ts 引用 config/token.tsAccess Token 有效期缩短为 2h区分 40100/40101 错误码
server/src/routes/auth.ts 引用 config/token.ts实现双 Token 机制;新增 POST /refresh 接口
server/src/routes/backup.ts JWT_SECRET → TOKEN_SECRET下载改用 download_tokens 凭证
server/src/routes/admin.ts 硬删除 → 软删除新增恢复接口N+1 优化
server/src/routes/logs.ts SQL 拼接改为参数化查询
server/src/routes/export.ts 下载改用 download_tokens 凭证
后端 - 代码质量
server/src/routes/recurring.ts sync 接口增加事务包裹
server/src/routes/user.ts 上传逻辑抽取使用公共函数
server/src/routes/notification.ts 上传逻辑抽取使用公共函数
后端 - 性能
server/src/routes/stats.ts 新增 GET /dashboard 聚合接口
server/src/routes/track.ts 逐条 INSERT → 批量 INSERT
server/src/routes/budget.ts 支持 group_id 参数;记账后触发预算预警
server/src/routes/transaction.ts 创建记账后触发预算预警检查
后端 - DB
server/src/db/schema.sql 新增 download_tokens、refresh_tokens、budget_alerts、user_settings 表users 增加 deleted_at
server/src/db/migrate.sql 对应迁移脚本
后端 - 入口
server/src/index.ts 移除 TOKEN_SECRET 警告;注册 report 路由;初始化 scheduler
前端 - 代码质量
client/src/stores/category.ts 乐观更新try/catch缓存标记 lastFetchTime
client/src/stores/tag.ts 乐观更新try/catch缓存标记 lastFetchTime
client/src/stores/transaction.ts 删除撤销try/catch
client/src/stores/budget.ts try/catch支持 group_id
client/src/stores/notification.ts 未读计数缓存 30s
client/src/stores/group.ts refreshAll 并行加载
client/src/stores/stats.ts 使用 dashboard 聚合接口
client/src/stores/user.ts Refresh Token 适配
client/src/utils/format.ts 去重getCurrentMonth 已在后端存在)
client/src/utils/request.ts Refresh Token 重试逻辑
client/src/utils/app-ready.ts 超时 reject非静默 resolve
client/src/utils/tracker.ts 攒批发送500ms 或 10 条)
client/src/config.ts 已集中化,搜索残留硬编码 URL
client/src/components/Icon/Icon.vue PNG → iconfont 字体图标
client/src/components/TransactionItem/TransactionItem.vue 群组只读标记
client/src/components/ChartWrapper/ 延迟渲染IntersectionObserver
前端 - 页面
client/src/pages/category-manage/index.vue 使用 ManageList/ColorPicker/EditModal 组件
client/src/pages/tag-manage/index.vue 使用 ManageList/ColorPicker/EditModal 组件
client/src/pages/bills/index.vue 删除撤销 Snackbar
client/src/pages/budget/index.vue 群组预算视图
client/src/pages/stats/index.vue 使用 dashboard 接口
client/src/pages/add/index.vue 离线记账支持
client/src/pages.json 全局 enablePullDownRefresh
client/src/App.vue 注册网络状态监听
配置
server/package.json 新增 jest、ts-jest、supertest、node-cron 依赖
client/package.json 新增 vitest、@pinia/testing、@vue/test-utils 依赖

三、数据结构和接口

3.1 新增/修改的数据库表结构

新增表

-- 一次性下载凭证表S4
CREATE TABLE IF NOT EXISTS download_tokens (
  id INT AUTO_INCREMENT PRIMARY KEY,
  token VARCHAR(64) NOT NULL UNIQUE COMMENT '随机凭证',
  user_id INT NOT NULL,
  resource_type ENUM('backup', 'export') NOT NULL COMMENT '资源类型',
  resource_id VARCHAR(100) DEFAULT '' COMMENT '资源标识(备份名等)',
  expires_at DATETIME NOT NULL COMMENT '过期时间',
  used_at DATETIME DEFAULT NULL COMMENT '使用时间NULL=未使用)',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_token (token),
  INDEX idx_expires (expires_at),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Refresh Token 表S5
CREATE TABLE IF NOT EXISTS refresh_tokens (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  token_hash VARCHAR(64) NOT NULL COMMENT 'SHA-256(token)',
  expires_at DATETIME NOT NULL COMMENT '过期时间',
  revoked_at DATETIME DEFAULT NULL COMMENT '撤销时间NULL=有效)',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user (user_id),
  INDEX idx_hash (token_hash),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 预算预警记录表F-2去重用
CREATE TABLE IF NOT EXISTS budget_alerts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  month VARCHAR(7) NOT NULL COMMENT '格式2026-05',
  level ENUM('80', '100', '120') NOT NULL COMMENT '预警等级',
  group_id INT DEFAULT NULL COMMENT '群组IDNULL=个人)',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_user_month_level (user_id, month, level, group_id),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户设置表F-1 报告推送开关等)
CREATE TABLE IF NOT EXISTS user_settings (
  user_id INT PRIMARY KEY,
  report_push_enabled TINYINT(1) DEFAULT 1 COMMENT '是否开启财务报告推送',
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

修改表

-- users 表新增软删除字段S6
ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间NULL=正常)';
CREATE INDEX idx_users_deleted ON users(deleted_at);

-- budgets 表:新增 group_id 支持UX-4
ALTER TABLE budgets DROP INDEX uk_user_month;
ALTER TABLE budgets ADD COLUMN group_id INT DEFAULT NULL COMMENT '群组IDNULL=个人预算)';
ALTER TABLE budgets ADD UNIQUE KEY uk_user_group_month (user_id, group_id, month);
ALTER TABLE budgets ADD INDEX idx_group_month (group_id, month);

3.2 新增/修改的 API 端点定义

新增端点

方法 路径 说明 请求体 / 参数 响应
POST /api/auth/refresh 刷新 Access Token { refreshToken: string } { code: 0, data: { accessToken, refreshToken, expiresIn } }
GET /api/stats/dashboard 聚合统计(一次返回 overview + category + trend ?month=2026-06&type=expense&period=month&group_id=null { code: 0, data: { overview, category, trend } }
POST /api/admin/users/:id/restore 恢复软删除用户 { code: 0 }
POST /api/download-tokens 生成下载凭证 `{ resourceType: 'backup' 'export', resourceId: string }`
GET /api/download/:token 使用凭证下载文件 :token 路径参数 文件流或 403
GET /api/reports/weekly 获取上周报告 { code: 0, data: { startDate, endDate, expense, income, topCategories, dailyAvg } }
GET /api/reports/monthly 获取上月报告 { code: 0, data: { month, expense, income, topCategories, dailyAvg, monthOverMonth } }
GET /api/user/settings 获取用户设置 { code: 0, data: { reportPushEnabled, ... } }
PUT /api/user/settings 更新用户设置 { reportPushEnabled?: boolean } { code: 0 }

修改端点

方法 路径 变更说明
POST /api/auth/login 返回值新增 refreshTokenexpiresIn 字段
POST /api/auth/demo-login 同上
DELETE /api/admin/users/:id 改为软删除(SET deleted_at = NOW()
GET /api/budget 支持 group_id 参数;群组视图返回 groupTotal + myAmount
POST /api/budget 支持 group_id 参数
POST /api/transactions 创建后触发预算预警检查
POST /api/track 支持批量 events[] 数组,后端批量 INSERT
GET /api/backup/:id/download 改用 download_tokens 凭证
GET /api/export 改用 download_tokens 凭证GET 带 token 参数改为先获取凭证再下载)

四、程序调用流图

4.1 Refresh Token 刷新流程

sequenceDiagram
    participant C as Client (request.ts)
    participant S as Server (auth.ts)
    participant M as Server (auth middleware)
    participant DB as MySQL (refresh_tokens)

    Note over C,S: 1. 正常登录
    C->>S: POST /auth/login { code }
    S->>DB: INSERT refresh_tokens (user_id, token_hash, expires_at)
    S-->>C: { accessToken(2h), refreshToken(30d), userId }

    Note over C,S: 2. API 调用Access Token 有效)
    C->>M: GET /api/xxx Authorization: Bearer <accessToken>
    M->>M: 验证 HMAC 签名 + 有效期
    M-->>C: 200 OK { data }

    Note over C,S: 3. Access Token 过期
    C->>M: GET /api/xxx Authorization: Bearer <expiredToken>
    M->>M: 签名有效但已过期
    M-->>C: 401 { code: 40101, message: 'token已过期' }

    Note over C,S: 4. 自动刷新
    C->>S: POST /auth/refresh { refreshToken }
    S->>DB: SELECT WHERE token_hash=SHA256(refreshToken) AND revoked_at IS NULL AND expires_at>NOW()
    DB-->>S: 找到记录
    S->>DB: UPDATE refresh_tokens SET revoked_at=NOW() WHERE id=?
    S->>DB: INSERT refresh_tokens (新 token_hash)
    S-->>C: { accessToken, refreshToken, expiresIn }
    C->>C: 存储新 Token重试原请求

4.2 删除撤销流程

sequenceDiagram
    participant U as User
    participant P as BillsPage
    participant S as TransactionStore
    participant SN as Snackbar
    participant API as Server API

    U->>P: 左滑删除记录
    P->>S: pendingDelete(id, item, index)
    S->>S: 从 transactions 列表移除
    S->>SN: 显示 Snackbar "已删除 1 条记录 [撤销]"
    SN-->>P: 3秒倒计时

    alt 3秒内点击撤销
        U->>SN: 点击 [撤销]
        SN->>S: cancelDelete(id)
        S->>S: 恢复到列表原位置
        SN->>SN: 消失
    else 3秒超时
        SN->>S: confirmDelete(id)
        S->>API: DELETE /api/transactions/:id
        alt 删除成功
            API-->>S: 200 OK
            S->>S: 清除 pendingDelete 记录
        else 删除失败
            API-->>S: 500 Error
            S->>S: 恢复到列表原位置
            S->>U: toast "删除失败"
        end
        SN->>SN: 消失
    end

4.3 预算预警触发流程

sequenceDiagram
    participant C as Client
    participant T as Transaction Route
    participant BA as BudgetAlert Service
    participant DB as MySQL
    participant NS as Notification System
    participant WX as WeChat API

    C->>T: POST /api/transactions { amount, type, date }
    T->>DB: INSERT INTO transactions
    T->>BA: checkBudgetAlert(userId, month)
    BA->>DB: SELECT budget FROM budgets WHERE user_id=? AND month=?
    BA->>DB: SELECT SUM(amount) FROM transactions WHERE user_id=? AND month=? AND type='expense'

    alt 支出/预算 >= 80% 且未推送过
        BA->>DB: INSERT budget_alerts (level='80')
        BA->>NS: 创建站内通知
        NS->>DB: INSERT INTO notifications
    else 支出/预算 >= 100%
        BA->>DB: INSERT budget_alerts (level='100')
        BA->>NS: 创建站内通知
        BA->>WX: 发送微信订阅消息
    else 支出/预算 >= 120%
        BA->>DB: INSERT budget_alerts (level='120')
        BA->>NS: 创建紧急站内通知 (is_urgent=true)
        BA->>WX: 发送微信订阅消息
    end

    T-->>C: { code: 0, data: { id } }

五、任务列表

按批次分组,每批次包含 1 个任务,共 5 个任务 优先级P0阻塞上线> P1必须有> P2锦上添花

T01: 项目基础设施与安全修复

字段 内容
任务 ID T01
优先级 P0
描述 搭建项目基础设施(依赖安装、配置文件、入口文件、数据库迁移),并完成所有 P0/P1 安全修复S1-S7
依赖
涉及文件 新建server/src/config/token.ts, .githooks/pre-commit, server/.env.test, server/jest.config.js, client/vitest.config.ts
修改server/package.json, client/package.json, server/src/db/schema.sql, server/src/db/migrate.sql, server/src/db/connection.ts, server/src/middleware/auth.ts, server/src/routes/auth.ts, server/src/routes/backup.ts, server/src/routes/admin.ts, server/src/routes/logs.ts, server/src/routes/export.ts, server/src/index.ts, client/src/pages.json

详细子项

  1. S1 .env 凭据泄露修复:安装 git-filter-repo、重写历史、添加 pre-commit 钩子
  2. S2 SQL 参数化logs.ts:167 LIMIT/OFFSET 改为 ? 占位符 + parseInt 校验
  3. S3 TOKEN_SECRET 统一:新建 config/token.ts,统一命名 + 强制启动校验 + 生成强密钥
  4. S4 下载凭证:新建 download_tokens 表,改造 backup.ts / export.ts 下载逻辑
  5. S5 Refresh Token:新建 refresh_tokens 表,双 Token 机制Access 2h + Refresh 30d前端 request.ts 401 自动刷新
  6. S6 软删除users 表新增 deleted_atadmin.ts DELETE → UPDATE新增恢复接口全局查询过滤
  7. S7 DB 连接校验connection.ts 移除 fallback启动时校验必需环境变量
  8. 基础设施package.json 新增依赖jest, supertest, node-cron, vitest 等jest/vitest 配置,.env.test

T02: 代码质量重构

字段 内容
任务 ID T02
优先级 P1
描述 抽取通用组件消除重复代码ManageList/ColorPicker/EditModal补齐事务与异常处理统一工具函数与硬编码 URL
依赖 T01
涉及文件 新建client/src/components/ManageList/ManageList.vue, client/src/components/ColorPicker/ColorPicker.vue, client/src/components/EditModal/EditModal.vue, server/src/utils/upload.ts, client/src/utils/avatar.ts, client/src/static/iconfont/*, client/src/styles/iconfont.scss
修改client/src/pages/category-manage/index.vue, client/src/pages/tag-manage/index.vue, client/src/components/Icon/Icon.vue, server/src/routes/recurring.ts, server/src/routes/user.ts, server/src/routes/notification.ts, client/src/utils/format.ts, client/src/config.ts

详细子项

  1. C2 ManageList 抽取:新建 ManageList 组件category-manage 和 tag-manage 改为使用该组件,统一行为
  2. C2 ColorPicker 抽取:从两个管理页面的弹窗中抽取颜色选择器
  3. C2 EditModal 抽取:从两个管理页面的弹窗中抽取编辑弹窗
  4. C4 Recurring 事务recurring.ts /sync 接口用 getConnection() + beginTransaction() 包裹
  5. C3 上传逻辑抽取user.tsnotification.ts 的 multer 配置抽取为 utils/upload.ts
  6. C1 工具函数去重:前端 format.tsgetCurrentMonth 与后端 date.ts 重复,前端保留并标注来源
  7. C7 硬编码 URL:全局搜索 http://xiaocai.j35.site,统一从 config.ts 读取
  8. C8 Icon → iconfont生成字体图标Icon.vue 改为字体渲染
  9. C9 头像 URL 拼接:抽取 getAvatarUrl(filename) 工具函数

T03: 性能优化与 UX 增强

字段 内容
任务 ID T03
优先级 P1
描述 实现乐观更新、并行加载、聚合接口、删除撤销、群组只读、离线记账、下拉刷新等性能与体验提升
依赖 T01
涉及文件 新建client/src/components/Snackbar/Snackbar.vue, client/src/utils/offline.ts
修改client/src/stores/category.ts, client/src/stores/tag.ts, client/src/stores/group.ts, client/src/stores/stats.ts, client/src/stores/notification.ts, client/src/stores/transaction.ts, client/src/stores/budget.ts, client/src/utils/request.ts, client/src/utils/app-ready.ts, client/src/utils/tracker.ts, client/src/components/TransactionItem/TransactionItem.vue, client/src/components/ChartWrapper/, client/src/pages/bills/index.vue, client/src/pages/budget/index.vue, client/src/pages/stats/index.vue, client/src/pages/add/index.vue, client/src/App.vue, server/src/routes/stats.ts, server/src/routes/track.ts, server/src/routes/budget.ts

详细子项

  1. Perf-1 乐观更新category/tag Store 新增/编辑/排序操作先更新本地 state 再调接口,失败回滚
  2. Perf-2 并行加载groupStore refreshAll() 中无依赖的 fetchCategories() + fetchTags() 改为 Promise.all()
  3. Perf-3 聚合接口:后端 GET /stats/dashboardPromise.all 并行查询 overview + category + trend
  4. Perf-4 批量 INSERT:前端 tracker.ts 攒批500ms/10条后端 track.ts 接收数组批量 INSERT
  5. Perf-5 Admin N+1admin.ts 用户列表改为 LEFT JOIN 单次查询
  6. Perf-6 前端缓存category/tag Store 增加 lastFetchTime5 分钟内 onShow 跳过请求
  7. Perf-7 未读计数缓存notification Store 缓存 30s
  8. UX-1 删除撤销transaction Store 实现 pendingDelete Map + Snackbar 组件 + 3 秒超时
  9. UX-2 群组只读TransactionItem 新增 isReadOnly prop非本人记录显示锁图标
  10. UX-3 waitForReady 超时app-ready.ts 超时后 reject页面级 catch 显示错误提示
  11. UX-4 预算 group_idbudget Store/页面/路由 支持 group_id 参数
  12. UX-5 下拉刷新pages.json 全局启用,各页面实现 onPullDownRefresh
  13. UX-6 离线记账offline.ts 队列管理 + App.vue 网络监听 + add 页面适配
  14. UX-7 Loading 态:异步操作添加 loading ref 守卫 + 按钮禁用

T04: 测试体系建设

字段 内容
任务 ID T04
优先级 P1
描述 搭建前后端测试框架,编写核心路由集成测试与 Store 单元测试,建立持续测试基线
依赖 T01
涉及文件 新建server/tests/setup.ts, server/tests/auth.test.ts, server/tests/transaction.test.ts, server/tests/category.test.ts, server/tests/budget.test.ts, server/tests/stats.test.ts, client/tests/stores/transaction.test.ts, client/tests/stores/category.test.ts, client/tests/utils/format.test.ts, client/tests/utils/app-ready.test.ts

详细子项

  1. 后端测试框架搭建jest.config.js + setup.ts测试数据库初始化与清理
  2. auth 测试:登录成功/失败、Token 生成与验证、Refresh Token 刷新、无效 Token 拒绝
  3. transaction 测试CRUD、group_id 过滤、分页、金额精度、权限校验
  4. category 测试CRUD、默认/自定义分类隔离、排序、迁移
  5. budget 测试CRUD、月度预算、群组视图
  6. stats 测试overview/category/trend/dashboard 查询、period 参数校验
  7. 前端 Store 测试transaction/category Store 核心方法测试
  8. 工具函数测试format.tsformatAmount、formatDate、getCurrentMonth、app-ready.ts

T05: 新功能(财务报告 + 预算预警)

字段 内容
任务 ID T05
优先级 P1
描述 实现周/月财务报告推送、预算超支预警、微信订阅消息集成
依赖 T01
涉及文件 新建server/src/services/scheduler.ts, server/src/services/report.ts, server/src/services/budget-alert.ts, server/src/services/wechat-subscribe.ts, server/src/routes/report.ts, client/src/api/report.ts
修改server/src/index.ts, server/src/routes/transaction.ts, server/src/routes/budget.ts

详细子项

  1. scheduler.ts:使用 node-cron 注册定时任务(每周一 09:00 推送周报、每月 1 号 09:00 推送月报、每日凌晨全量预算扫描)
  2. report.ts:查询指定时间范围的收支数据,生成摘要文本
  3. budget-alert.ts:实现 checkBudgetAlert 逻辑80%/100%/120% 分级预警 + 去重
  4. wechat-subscribe.ts:封装微信订阅消息发送接口
  5. report 路由GET /reports/weekly、GET /reports/monthly
  6. 用户设置GET/PUT /user/settingsreport_push_enabled 开关
  7. 触发集成transaction.ts 创建记账后调用 budgetAlert.check()budget.ts 设置预算后调用 budgetAlert.check()

六、任务依赖图

graph TD
    T01["T01: 项目基础设施与安全修复<br/>(P0 · S1-S7 + 依赖安装 + DB迁移)"]
    T02["T02: 代码质量重构<br/>(P1 · C1-C9 + 通用组件)"]
    T03["T03: 性能优化与 UX 增强<br/>(P1 · Perf-1~9 + UX-1~7)"]
    T04["T04: 测试体系建设<br/>(P1 · T-1 + T-2 + T-4)"]
    T05["T05: 新功能<br/>(P1 · F-1 + F-2)"]

    T01 --> T02
    T01 --> T03
    T01 --> T04
    T01 --> T05

    style T01 fill:#FF6B6B,color:#fff
    style T02 fill:#FFD700,color:#333
    style T03 fill:#7BC67E,color:#fff
    style T04 fill:#5B9BD5,color:#fff
    style T05 fill:#8B5CF6,color:#fff

并行策略T01 完成后T02/T03/T04/T05 可并行推进。其中 T03 与 T02 有轻微文件冲突stores/),建议 T02 先行或约定合并策略。


七、依赖包列表

后端新增

- node-cron@^3.0.3          # 定时任务调度F-1 财务报告、F-2 预算扫描)
- jest@^29.7.0               # 后端测试框架T-1
- ts-jest@^29.1.1            # Jest TypeScript 支持
- supertest@^6.3.3           # HTTP 集成测试T-1
- @types/supertest@^6.0.2   # supertest 类型定义

前端新增

- vitest@^1.2.0              # 前端测试框架T-2
- @pinia/testing@^0.1.3     # Pinia 测试工具
- @vue/test-utils@^2.4.3    # Vue 组件测试工具

八、共享知识(跨文件约定)

8.1 API 响应格式

// 所有 API 统一响应格式
{ code: 0, data: T, message?: string }       // 成功
{ code: 40100, message: '未登录' }            // Token 无效
{ code: 40101, message: 'token已过期' }       // Token 过期(可刷新)
{ code: 40300, message: '无权访问' }          // 权限不足
{ code: 40400, message: '资源不存在' }        // 找不到
{ code: 42900, message: '请求过于频繁' }      // 限流
{ code: 50000, message: '服务器错误' }        // 服务端异常

8.2 认证相关

- Access Token 有效期2 小时HMAC-SHA256 签名格式base64(userId:timestamp:signature)
- Refresh Token 有效期30 天(随机 32 字节,存 SHA-256 hash
- 前端存储 Keyxc:accessToken, xc:refreshToken
- Authorization HeaderBearer <accessToken>
- 40101 表示 Token 过期可刷新40100 表示 Token 无效需重新登录

8.3 金额处理

- 所有金额以「分」为单位存储INT 类型)
- 前端展示使用 formatAmount() 转为 "¥ 1,234.56" 格式
- 输入时以「元」为单位,提交前乘以 100 转为分
- 金额校验1 <= amount <= 999999999

8.4 日期处理

- 数据库存储 DATE 类型YYYY-MM-DD
- 月份参数格式YYYY-MM如 "2026-06"
- 时区统一:服务器 + 客户端均按 UTC+8 处理
- getCurrentMonth() 前后端逻辑一致

8.5 软删除约定

- users 表使用 deleted_at 字段标记软删除
- 所有查询 users 表的 SQL 必须增加 WHERE deleted_at IS NULL
- 登录时检查 deleted_at IS NULL
- 群组成员查询需过滤已删除用户
- 已删除用户在群组统计中标注"已注销用户"

8.6 乐观更新约定

- 新增/编辑操作:先更新本地 state → 再调 API → 失败回滚 + toast
- 删除操作:仍等 API 确认后再移除(防误删)
- 排序操作:全量回滚(排序是完整 ID 数组操作)
- 临时 ID使用负数时间戳-Date.now()API 成功后替换为真实 ID

8.7 前端缓存约定

- 低频变更数据(分类/标签5 分钟缓存onShow 判断是否需要刷新
- 未读计数30 秒缓存
- 缓存失效:手动操作(新增/编辑/删除)后立即清除对应缓存
- 下拉刷新:强制清除缓存并重新请求

8.8 组件命名约定

- 组件目录client/src/components/ComponentName/ComponentName.vue
- Store 命名use{Name}Store
- API 模块client/src/api/{name}.ts
- 页面目录client/src/pages/{name}/index.vue

九、待明确事项

# 问题 影响范围 当前假设 风险
Q1 Refresh Token 存储方式 S5 存 MySQL refresh_tokens 表;短中期可行,高并发时迁移 Redis 当前写入频率极低MySQL 足够
Q2 软删除级联范围 S6 仅软删除用户账号,交易记录保留但通过 deleted_at IS NULL 过滤 保留交易可能影响群组统计,需标注"已注销用户"
Q3 删除撤销批量上限 UX-1 上限 5 条Snackbar 最多显示"已删除 5 条记录"),超过直接删除 5 条覆盖绝大多数场景
Q4 离线记账冲突策略 UX-6 MVP 仅支持离线新增(不涉及修改/删除),新增使用服务端生成 ID不存在主键冲突 如需离线编辑需引入版本号机制
Q5 微信订阅消息模板审核 F-1 优先站内通知,微信订阅消息作为增强项;需提前申请模板审核 微信审核可能被拒,需准备备选方案
Q6 预算预警检查时机 F-2 双重策略:记账后实时检查 + 每日凌晨全量扫描 仅实时检查可能遗漏周期记账触发的超支
Q7 乐观更新回滚粒度 Perf-1 排序操作全量回滚(语义清晰),新增/编辑单条回滚 全量回滚可能丢失已做的其他排序操作
Q8 测试环境数据隔离 T-1 独立测试数据库(.env.testCI 用 GitHub Actions service container 本地需额外配置测试数据库
Q9 budgets 表 group_id 迁移 UX-4 现有数据 group_id 默认 NULL个人预算UNIQUE KEY 从 (user_id, month) 改为 (user_id, group_id, month) 迁移需确保不破坏现有预算数据
Q10 iconfont 图标清单 C8 需从现有 Icon.vue 的 PNG 映射中提取完整图标列表,并在 iconfont.cn 重新制作 部分图标可能无现成 SVG需设计师配合

文档版本: v2.0 | 最后更新: 2026-06-10