- 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
920 lines
41 KiB
Markdown
920 lines
41 KiB
Markdown
# 小菜记账 — 全量迭代架构设计 v2
|
||
|
||
> 版本: v2.0 | 日期: 2026-06-10
|
||
> 架构师: 高见远(Gao)
|
||
> 基线: PRD v2.0(35 项改进点,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 |
|
||
|
||
#### S2:logs.ts SQL 拼接修复
|
||
|
||
| 项目 | 方案 |
|
||
|------|------|
|
||
| 当前问题 | `LIMIT ${pSize} OFFSET ${offset}` 模板字符串直接拼接 |
|
||
| 修复方案 | `parseInt` + 范围校验后使用 `pool.query` + `?` 占位符 |
|
||
| 关键点 | `pool.query` 支持 `?` 占位符用于整数参数;`pool.execute` 的 prepared statement 对 LIMIT/OFFSET 类型要求严格,故选用 `pool.query` |
|
||
|
||
#### S3:TOKEN_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 字符强密钥 |
|
||
|
||
#### S4:Export/Backup 短期一次性下载凭证
|
||
|
||
| 项目 | 方案 |
|
||
|------|------|
|
||
| 核心思路 | 新增 `download_tokens` 表,生成随机凭证(`crypto.randomBytes(32)`) |
|
||
| 有效期 | 5 分钟,一次性使用后标记 `used_at` |
|
||
| 存储 | MySQL 表(写入频率极低,无需 Redis) |
|
||
| 清理 | 健康检查时附带清理 `expires_at < NOW()` 的记录 |
|
||
| 改动 | `backup.ts` 和 `export.ts` 的下载逻辑改为先获取凭证再用凭证下载 |
|
||
|
||
#### S5:Refresh Token 机制
|
||
|
||
| 项目 | 方案 |
|
||
|------|------|
|
||
| 架构 | 双 Token 机制:Access Token(2h)+ Refresh Token(30d) |
|
||
| 存储 | `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 有效期 |
|
||
|
||
#### S6:Admin 硬删除 → 软删除 + 二次确认
|
||
|
||
| 项目 | 方案 |
|
||
|------|------|
|
||
| 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` |
|
||
| 前端确认 | 管理页点击删除 → 弹窗二次确认 → 调用软删除接口 |
|
||
|
||
#### S7:DB 连接移除默认凭据
|
||
|
||
| 项目 | 方案 |
|
||
|------|------|
|
||
| 修复 | 移除所有 `|| '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 Token(2h)+ Refresh Token(randomBytes(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.ts:DELETE → UPDATE SET deleted_at
|
||
- 新增恢复接口
|
||
- 登录验证:检查 deleted_at IS NULL
|
||
|
||
具体改动位置:
|
||
1. schema.sql / migrate.sql:ALTER 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 + ajv(JSON 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.ts;Access 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 '群组ID(NULL=个人)',
|
||
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 '群组ID(NULL=个人预算)';
|
||
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.ts(formatAmount、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/settings,report_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)
|
||
- 前端存储 Key:xc:accessToken, xc:refreshToken
|
||
- Authorization Header:Bearer <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.test),CI 用 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*
|