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

920 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小菜记账 — 全量迭代架构设计 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-repo`Python比 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.ts``JWT_SECRET``TOKEN_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.ts``export.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/restore``SET deleted_at = NULL` |
| 全局过滤 | 所有涉及 `users` 表的查询增加 `WHERE deleted_at IS NULL` |
| 前端确认 | 管理页点击删除 → 弹窗二次确认 → 调用软删除接口 |
#### S7DB 连接移除默认凭据
| 项目 | 方案 |
|------|------|
| 修复 | 移除所有 `|| 'localhost'``|| 'xiaocai'``|| 'xiaocai123'` fallback |
| 启动校验 | 遍历必需环境变量,缺失任一即 `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.ts`Access 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 新增/修改的数据库表结构
#### 新增表
```sql
-- 一次性下载凭证表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;
```
#### 修改表
```sql
-- 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 }` | `{ code: 0, data: { token, downloadUrl } }` |
| 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` | 返回值新增 `refreshToken``expiresIn` 字段 |
| 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 刷新流程
```mermaid
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 删除撤销流程
```mermaid
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 预算预警触发流程
```mermaid
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_at`admin.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.ts``notification.ts` 的 multer 配置抽取为 `utils/upload.ts`
6. **C1 工具函数去重**:前端 `format.ts``getCurrentMonth` 与后端 `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/dashboard``Promise.all` 并行查询 overview + category + trend
4. **Perf-4 批量 INSERT**:前端 tracker.ts 攒批500ms/10条后端 track.ts 接收数组批量 INSERT
5. **Perf-5 Admin N+1**admin.ts 用户列表改为 LEFT JOIN 单次查询
6. **Perf-6 前端缓存**category/tag Store 增加 `lastFetchTime`5 分钟内 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_id**budget 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()
---
## 六、任务依赖图
```mermaid
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 响应格式
```typescript
// 所有 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*