- 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
874 lines
38 KiB
Markdown
874 lines
38 KiB
Markdown
# 小菜记账 — 全量迭代 PRD v2
|
||
|
||
> 版本: v2.0 | 日期: 2026-06-10
|
||
> 类型: 全量迭代 PRD(安全性 · 代码质量 · 性能 · UX · 可测试性 · 新功能)
|
||
> 基线: 已完成三轮迭代(代码审查修复 · UI 优化 · 前后端功能对齐 8 项差距)
|
||
> 范围: 本次审查发现 35 项改进点,覆盖 6 个维度
|
||
|
||
---
|
||
|
||
## 一、产品目标
|
||
|
||
| # | 目标 | 说明 | 关联维度 |
|
||
|---|------|------|----------|
|
||
| G1 | 消除生产环境安全风险 | .env 泄露、SQL 注入、弱密钥、Token 暴露等 P0/P1 安全问题必须在上线前全部清零 | 安全性 |
|
||
| G2 | 建立可持续的代码质量基线 | 消除重复代码、补齐事务与异常处理、统一规范,使后续迭代不会因技术债减速 | 代码质量 |
|
||
| G3 | 核心路径性能感知提升 50% | 首页加载、分类/标签操作、统计查询等高频路径的响应时间减半 | 性能优化 |
|
||
| G4 | 交互体验达到主流记账 App 水准 | 删除撤销、群组只读、离线记账、下拉刷新等缺失体验补齐 | 用户体验 |
|
||
| G5 | 建立测试体系与核心功能扩展 | 从零测试覆盖到关键路径有保障,并新增财务报告与预算预警等高价值功能 | 可测试性 · 新功能 |
|
||
|
||
---
|
||
|
||
## 二、用户故事
|
||
|
||
### P0 — 安全紧急
|
||
|
||
> **S1** As a 产品负责人, I want .env 文件中的生产凭据从 Git 历史中彻底清除, so that 攻击者无法通过代码仓库获取数据库密码和微信密钥。
|
||
|
||
> **S2** As a 用户, I want 后端所有 SQL 查询使用参数化方式, so that 我的财务数据不会被 SQL 注入攻击窃取。
|
||
|
||
> **S3** As a 用户, I want 认证 Token 使用强密钥且命名统一, so that 伪造 Token 攻击无法成功。
|
||
|
||
### P1 — 安全高优
|
||
|
||
> **S4** As a 用户, I want 导出/备份下载链接使用一次性短期凭证, so that 我的财务数据不会被 URL 泄露导致未授权访问。
|
||
|
||
> **S5** As a 用户, I want 登录 Token 支持轮换机制, so that 即使 Token 泄露也能在短时间内失效。
|
||
|
||
> **S6** As a 管理员, I want 删除用户时使用软删除并需二次确认, so that 不会因误操作导致用户数据永久丢失。
|
||
|
||
> **S7** As a 运维人员, I want 数据库连接不硬编码默认凭据, so that 未配置环境变量时服务直接报错而非以弱凭据连接。
|
||
|
||
### P1 — 代码质量
|
||
|
||
> **C1** As a 开发者, I want 消除前后端重复的工具函数, so that 修改逻辑时不需要同步多处代码。
|
||
|
||
> **C2** As a 开发者, I want tag-manage 与 category-manage 共用可复用组件, so that 两个管理页面行为一致且维护成本低。
|
||
|
||
> **C4** As a 用户, I want 周期记账同步操作在数据库事务中执行, so that 部分失败不会产生脏数据。
|
||
|
||
> **C5** As a 开发者, I want Store 的异步操作都有 try/catch, so that 未捕获异常不会导致界面卡死。
|
||
|
||
> **C7** As a 开发者, I want 所有 API 地址从 config.ts 统一读取, so that 环境切换不会遗漏。
|
||
|
||
### P1 — 性能优化
|
||
|
||
> **Perf-1** As a 用户, I want 添加/编辑分类后列表立即更新而无需等待接口返回, so that 操作反馈无延迟感。
|
||
|
||
> **Perf-2** As a 用户, I want 统计页面的多个接口并行请求, so that 页面加载不会串行等待。
|
||
|
||
### P1 — 用户体验
|
||
|
||
> **UX-1** As a 用户, I want 删除记录后 3 秒内可以撤销, so that 误删不会导致数据永久丢失。
|
||
|
||
> **UX-2** As a 群组成员, I want 看到非本人记录时显示只读标记, so that 我不会误编辑他人记录。
|
||
|
||
> **UX-3** As a 用户, I want 预算设置支持群组维度, so that 群组场景下也能设置和跟踪预算。
|
||
|
||
### P1 — 可测试性
|
||
|
||
> **T-1** As a 开发者, I want 后端路由有集成测试覆盖, so that 接口变更不会静默破坏功能。
|
||
|
||
### P1 — 新功能
|
||
|
||
> **F-1** As a 用户, I want 每周/月收到财务报告推送, so that 我不用主动打开 App 也能了解收支状况。
|
||
|
||
> **F-2** As a 用户, I want 预算达到 80%/100%/120% 时收到预警通知, so that 我能及时控制支出。
|
||
|
||
---
|
||
|
||
## 三、需求池
|
||
|
||
### P0 — Must Have(安全紧急,阻塞上线)
|
||
|
||
| ID | 维度 | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|------|----------|----------|
|
||
| S1 | 安全 | .env 凭据泄露修复 | 全局 | `.env` 在 `.gitignore` 中(已有但需验证);Git 历史中不含任何凭据明文;所有已泄露密钥已轮换 |
|
||
| S2 | 安全 | logs.ts SQL 拼接修复 | `server/routes/logs.ts` | `LIMIT ${pSize} OFFSET ${offset}` 改为 `pool.query` + 参数占位符 `?`;或改为 `parseInt` + 范围校验后内插 |
|
||
| S3 | 安全 | TOKEN_SECRET 弱默认值 + 命名不一致 | `server/middleware/auth.ts`, `server/routes/auth.ts`, `server/routes/backup.ts` | 统一为 `TOKEN_SECRET`;移除所有 fallback 默认值,未配置时进程拒绝启动 |
|
||
|
||
### P1 — Should Have
|
||
|
||
#### 安全
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| S4 | Export/Backup URL 传认证 token → 短期一次性下载凭证 | `server/routes/export.ts`, `server/routes/backup.ts` | 下载凭证有效期 5 分钟,一次性使用后失效;不使用 query string 传长期 Token |
|
||
| S5 | Refresh Token 机制 | `server/routes/auth.ts`, `server/middleware/auth.ts`, `client/src/utils/request.ts` | Access Token 有效期缩短至 2h;Refresh Token 有效期 30d;Refresh Token 支持轮换(用后旧 Token 失效) |
|
||
| S6 | Admin 硬删除 → 软删除 + 二次确认 | `server/routes/admin.ts`, DB schema | `users` 表新增 `deleted_at` 字段;删除操作改为 `SET deleted_at = NOW()`;需前端二次确认弹窗;查询自动过滤 `deleted_at IS NULL` |
|
||
| S7 | DB 连接移除默认凭据 | `server/src/db/connection.ts` | 移除 `|| 'localhost'`、`|| 'xiaocai'`、`|| 'xiaocai123'` 等 fallback;未配置时 `createPool` 抛出明确错误 |
|
||
|
||
#### 代码质量
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| C1 | 重复工具函数抽取 | 前后端 `utils/` | 识别并合并功能相同的工具函数(如日期格式化、金额处理);抽取为共享模块或分别去重 |
|
||
| C2 | tag-manage / category-manage 重复代码 | 前端页面 | 抽取 `ManagePageLayout` 可复用组件(列表 + 新增 + 编辑 + 删除 + 拖拽排序);两页面基于该组件定制 |
|
||
| C4 | Recurring 同步缺事务 | `server/routes/recurring.ts` | `/sync` 接口使用 `pool.getConnection()` + `connection.beginTransaction()` 包裹;部分失败时 `rollback()` |
|
||
| C5 | Store 缺 try/catch | `client/src/stores/*.ts` | 所有 Store 中 `await api.xxx()` 调用包裹 try/catch;catch 中使用统一错误提示(toast);不吞错误 |
|
||
| C7 | 硬编码 URL | 前端全局 | 全局搜索 `http://` 和 `xiaocai.j35.site`,替换为 `config.ts` 导出的变量 |
|
||
|
||
#### 性能优化
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| Perf-1 | 分类/标签乐观更新 | `client/stores/category.ts`, `client/stores/tag.ts` | 新增/编辑/排序操作先更新本地 state 再调接口;失败时回滚并提示;删除仍等接口确认后移除 |
|
||
| Perf-2 | groupStore 并行加载 | `client/stores/group.ts` | `refreshAll()` 中多个 `fetchXxx()` 改为 `Promise.all()` 并行执行 |
|
||
| Perf-3 | Stats 聚合接口 | `server/routes/stats.ts` | 合并 `/stats/overview` + `/stats/category` + `/stats/trend` 为单一 `/stats/dashboard` 接口;减少 3 次请求为 1 次 |
|
||
| Perf-4 | Track 批量 INSERT | `server/routes/track.ts` | 多条埋点数据合并为单条 `INSERT INTO ... VALUES (?,?), (?)` 批量写入 |
|
||
| Perf-5 | Admin N+1 查询 | `server/routes/admin.ts` | 用户列表查询改为 JOIN 单次获取关联数据,消除循环内查询 |
|
||
| Perf-6 | 前端数据缓存 | `client/stores/*.ts` | 分类、标签等低频变更数据增加内存缓存标记,`onShow` 时判断是否需刷新 |
|
||
| Perf-7 | 未读计数缓存 | `client/stores/notification.ts` | 未读计数结果缓存 30s,避免每次 `onShow` 都请求 |
|
||
|
||
#### 用户体验
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| UX-1 | 删除撤销 | `client/pages/bills/`, `client/stores/transaction.ts` | 删除后底部弹出 Snackbar,3 秒内可点击"撤销"恢复;撤销成功数据不变;超时后真正删除 |
|
||
| UX-2 | 群组只读标记 | `client/pages/bills/`, `client/components/TransactionItem/` | 非本人记录显示小锁图标;点击进入查看模式而非编辑模式;查看模式下字段不可修改 |
|
||
| UX-3 | waitForReady 超时处理 | `client/utils/app-ready.ts` | 超时后 reject 并在页面级捕获,展示"网络异常,请检查网络后重试"提示;不再静默继续 |
|
||
| UX-4 | 预算 group_id | `server/routes/budget.ts`, `client/stores/budget.ts`, `client/pages/budget/` | 预算设置/查询支持 `group_id` 参数;群组视图下预算卡片显示群组总预算 + 我的份额 |
|
||
|
||
#### 可测试性
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| T-1 | 后端路由集成测试 | `server/` | 使用 Jest + supertest;覆盖 auth、transaction、category、budget 核心 CRUD 路由;断言状态码 + 返回格式 + 权限校验 |
|
||
|
||
#### 新功能
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| F-1 | 月度/周度财务报告推送 | 后端定时任务 + 微信订阅消息 | 每周一早 9 点推送上周收支摘要;每月 1 号推送上月收支报告;使用微信订阅消息模板;用户可在设置中开关 |
|
||
| F-2 | 预算超支预警 | 后端定时任务 + 通知系统 | 80% 时站内通知提醒;100% 时站内通知 + 微信订阅消息;120% 时站内紧急通知 + 微信订阅消息;预算检查在记账后实时触发 |
|
||
|
||
### P2 — Nice to Have
|
||
|
||
#### 代码质量
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| C3 | 上传重复逻辑抽取 | `server/routes/user.ts`, `server/routes/notification.ts` | 抽取 `handleUpload(dir, file)` 公共函数,统一 MIME 校验 + 大小限制 + 文件名生成 |
|
||
| C6 | Store 吞错误修复 | `client/stores/*.ts` | catch 块中至少 `console.error` + 用户可见 toast 提示;不再空 catch |
|
||
| C8 | Icon 组件 → iconfont | `client/components/Icon/` | 将 PNG 图标映射替换为 iconfont 字体图标;减小包体积;支持动态颜色 |
|
||
| C9 | 头像 URL 拼接重复 | 前端多处 | 抽取 `getAvatarUrl(filename)` 工具函数,统一拼接 `API_BASE + 路径 + 文件名` |
|
||
|
||
#### 性能优化
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| Perf-8 | Logs 流式读取 | `server/routes/logs.ts` | 大量日志数据使用 stream 分批返回,避免一次性加载到内存 |
|
||
| Perf-9 | ChartWrapper 延迟渲染 | `client/components/ChartWrapper/` | 图表组件进入可视区域后才开始渲染(IntersectionObserver);非可视区域显示占位 |
|
||
|
||
#### 用户体验
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| UX-5 | 下拉刷新统一 | 前端所有数据页面 | 所有列表页面启用 `onPullDownRefresh`;统一在 `pages.json` 配置 `enablePullDownRefresh` |
|
||
| UX-6 | 离线支持 | `client/` 全局 | 记账操作存入本地队列 + 网络状态监听;网络恢复后自动同步;离线期间操作标记为"待同步"状态 |
|
||
| UX-7 | Loading 态统一 | 前端全局 | 所有异步操作添加 loading 态(骨架屏 / spinner / 按钮禁用);操作期间禁止重复提交 |
|
||
|
||
#### 可测试性
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| T-2 | Composables 测试 | `client/` | 抽取可复用逻辑为 composables;使用 Vitest 编写单元测试 |
|
||
| T-3 | 依赖注入 | `client/` | API 调用通过 provide/inject 注入,便于测试时 mock |
|
||
|
||
#### 新功能
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| F-3 | 多设备同步 | 后端 + 前端 | 基于 `updated_at` 时间戳的增量同步;冲突策略:后写入优先;同步状态可视化 |
|
||
| F-4 | AA 分账 | 后端 + 前端 | 群组内选择多笔交易,按人均/自定义比例分摊;生成每人应付/应收金额;支持结算确认 |
|
||
| F-5 | 智能记账 | 后端 + 前端 | 备注关键词自动匹配分类(如"滴滴"→交通);历史金额学习建议;最近使用分类优先 |
|
||
|
||
### P3 — 远期规划
|
||
|
||
#### 代码质量
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| C10 | 路由中间件一致性 | `server/routes/*.ts` | 统一所有路由的中间件注册方式(统一在 `index.ts` 注册还是路由内自注册) |
|
||
|
||
#### 性能优化
|
||
|
||
| ID | 需求 | 影响范围 | 验收标准 |
|
||
|----|------|----------|----------|
|
||
| Perf-10 | 数据可视化增强 | `client/pages/stats/` | 支持更丰富的图表类型(饼图、折线图、柱状图);支持图表数据导出为图片 |
|
||
|
||
---
|
||
|
||
## 四、安全修复方案
|
||
|
||
### S1:.env 凭据泄露修复
|
||
|
||
**当前状态**:`.env` 已在 `.gitignore` 中,但文件内容已在历史提交中暴露,包含数据库地址 `115.120.243.74`、密码 `dnjwYbpMdmASCxfH`、微信 AppID/Secret。
|
||
|
||
**修复步骤**:
|
||
|
||
1. **立即轮换所有已泄露凭据**:
|
||
- MySQL: 修改 `xiaocai_test` 用户密码
|
||
- 微信: 在微信公众平台重置 AppSecret
|
||
- TOKEN_SECRET: 生成新的强随机密钥(`openssl rand -hex 32`)
|
||
2. **清除 Git 历史**:
|
||
```bash
|
||
# 使用 git-filter-repo(推荐,比 BFG 更安全)
|
||
pip install git-filter-repo
|
||
git filter-repo --path server/.env --invert-paths
|
||
git filter-repo --blob-callback 'blob.data = blob.data.replace(b"dnjwYbpMdmASCxfH", b"REDACTED")'
|
||
```
|
||
3. **强制推送并通知协作者**:
|
||
```bash
|
||
git push origin --force --all
|
||
```
|
||
4. **验证 `.gitignore` 生效**:
|
||
```bash
|
||
git check-ignore server/.env # 应输出 server/.env
|
||
```
|
||
5. **添加 pre-commit hook 防止再次提交**:
|
||
```bash
|
||
# .githooks/pre-commit
|
||
if git diff --cached --name-only | grep -q '\.env'; then
|
||
echo "ERROR: .env files must not be committed"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
**回滚方案**:Git 历史重写前创建完整备份 `git clone --mirror`。
|
||
|
||
### S2:logs.ts SQL 拼接修复
|
||
|
||
**当前状态**:`server/routes/logs.ts:160-168` 使用模板字符串拼接 LIMIT/OFFSET:
|
||
|
||
```typescript
|
||
// ❌ 当前代码
|
||
LIMIT ${pSize} OFFSET ${offset}
|
||
```
|
||
|
||
**修复方案**:
|
||
|
||
```typescript
|
||
// ✅ 方案 A:参数化查询(推荐)
|
||
const safeLimit = Math.min(Math.max(parseInt(String(pSize)) || 10, 1), 100)
|
||
const safeOffset = Math.max(parseInt(String(offset)) || 0, 0)
|
||
const [rows] = await pool.query(
|
||
`SELECT t.*, u.nickname
|
||
FROM track_events t
|
||
LEFT JOIN users u ON t.user_id = u.id
|
||
WHERE ${where}
|
||
ORDER BY t.created_at DESC
|
||
LIMIT ? OFFSET ?`,
|
||
[...params, safeLimit, safeOffset]
|
||
)
|
||
```
|
||
|
||
**关键点**:`LIMIT`/`OFFSET` 的值必须先 `parseInt` + 范围校验,防止非整数或超大值;`pool.query` 支持 `?` 占位符用于整数参数(与 `pool.execute` 的 prepared statement 不同)。
|
||
|
||
### S3:TOKEN_SECRET 弱默认值 + 命名不一致
|
||
|
||
**当前状态**:
|
||
- `auth.ts` / `middleware/auth.ts`:`process.env.TOKEN_SECRET || 'xiaocai-token-secret-change-in-production'`
|
||
- `backup.ts`:`process.env.JWT_SECRET || 'xiaocai-secret'`(命名不一致 + 弱默认值)
|
||
- `.env` 中:`TOKEN_SECRET=xiaocai-prod-secret-2026-change-me`(弱密钥)
|
||
|
||
**修复方案**:
|
||
|
||
1. **统一命名为 `TOKEN_SECRET`**:
|
||
- `backup.ts` 中 `JWT_SECRET` → `TOKEN_SECRET`
|
||
- 删除 `const JWT_SECRET = ...` 声明,改为从 `middleware/auth.ts` 导出或统一读取 `process.env.TOKEN_SECRET`
|
||
|
||
2. **移除所有 fallback 默认值,强制要求环境变量**:
|
||
```typescript
|
||
// server/src/config/token.ts(新增)
|
||
export const TOKEN_SECRET = process.env.TOKEN_SECRET
|
||
if (!TOKEN_SECRET || TOKEN_SECRET.length < 32) {
|
||
console.error('[Security] TOKEN_SECRET must be set and at least 32 characters')
|
||
process.exit(1)
|
||
}
|
||
```
|
||
|
||
3. **生成强密钥**:
|
||
```bash
|
||
# 在服务器上生成并写入 .env
|
||
echo "TOKEN_SECRET=$(openssl rand -hex 32)" >> server/.env
|
||
```
|
||
|
||
4. **所有引用处改为**:
|
||
```typescript
|
||
import { TOKEN_SECRET } from '../config/token'
|
||
```
|
||
|
||
### S4:Export/Backup URL 短期一次性下载凭证
|
||
|
||
**当前状态**:
|
||
- `backup.ts`:下载链接使用 HMAC 签名的 `backupId:timestamp:hmac` 格式 token,通过 query string 传递
|
||
- `export.ts`:导出接口支持 query string 传 token 兼容
|
||
|
||
**问题**:虽然已有 HMAC 签名,但无过期时间校验(backup 有 timestamp 但未校验时效),且 token 可重复使用。
|
||
|
||
**修复方案**:
|
||
|
||
1. **新增 `download_tokens` 表**:
|
||
```sql
|
||
CREATE TABLE download_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
token VARCHAR(64) NOT NULL UNIQUE,
|
||
user_id INT NOT NULL,
|
||
resource_type ENUM('backup', 'export') NOT NULL,
|
||
resource_id VARCHAR(100),
|
||
expires_at DATETIME NOT NULL,
|
||
used_at DATETIME DEFAULT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_token (token),
|
||
INDEX idx_expires (expires_at)
|
||
);
|
||
```
|
||
|
||
2. **生成下载凭证**:
|
||
```typescript
|
||
// 有效期 5 分钟
|
||
const token = randomBytes(32).toString('hex')
|
||
await pool.query(
|
||
'INSERT INTO download_tokens (token, user_id, resource_type, resource_id, expires_at) VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE))',
|
||
[token, userId, 'backup', backupId]
|
||
)
|
||
```
|
||
|
||
3. **验证下载凭证**:
|
||
```typescript
|
||
const [rows] = await pool.query(
|
||
'SELECT * FROM download_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW() AND used_at IS NULL',
|
||
[token, userId]
|
||
)
|
||
if (!rows.length) return res.status(403).json({ code: 40300, message: '下载凭证无效或已过期' })
|
||
// 标记为已使用
|
||
await pool.query('UPDATE download_tokens SET used_at = NOW() WHERE id = ?', [rows[0].id])
|
||
```
|
||
|
||
4. **定期清理过期 token**:在 health check 或定时任务中清理 `expires_at < NOW()` 的记录。
|
||
|
||
### S5:Refresh Token 机制
|
||
|
||
**当前状态**:单 Token 机制,HMAC-SHA256 签名,30 天过期,无轮换。
|
||
|
||
**修复方案**:
|
||
|
||
1. **双 Token 机制**:
|
||
- Access Token:有效期 2 小时,用于 API 调用
|
||
- Refresh Token:有效期 30 天,仅用于刷新 Access Token
|
||
|
||
2. **新增 `refresh_tokens` 表**:
|
||
```sql
|
||
CREATE TABLE refresh_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
token_hash VARCHAR(64) NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
revoked_at DATETIME DEFAULT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_user (user_id),
|
||
INDEX idx_hash (token_hash)
|
||
);
|
||
```
|
||
|
||
3. **认证流程**:
|
||
- 登录:返回 `accessToken` + `refreshToken`
|
||
- API 调用:Header 带 `Authorization: Bearer <accessToken>`
|
||
- Token 过期(401):前端用 `refreshToken` 调用 `/auth/refresh` 获取新 `accessToken` + 新 `refreshToken`(旧 refresh token 失效)
|
||
- Refresh Token 也过期:需重新登录
|
||
|
||
4. **前端适配**(`client/src/utils/request.ts`):
|
||
- 现有 401 重登录机制改为 401 → refresh → 重试
|
||
- refresh 也失败(401)→ 清除本地 token → 跳转登录
|
||
|
||
5. **安全措施**:
|
||
- Refresh Token 存储 hash 值(SHA-256),不存明文
|
||
- 每个 Refresh Token 仅使用一次(用后旧 token 失效)
|
||
- 用户修改密码时撤销所有 Refresh Token
|
||
|
||
### S6:Admin 硬删除 → 软删除 + 二次确认
|
||
|
||
**当前状态**:`admin.ts:170` 直接 `DELETE FROM users WHERE id = ?`,无二次确认。
|
||
|
||
**修复方案**:
|
||
|
||
1. **DB Schema 变更**:
|
||
```sql
|
||
ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL;
|
||
CREATE INDEX idx_users_deleted ON users(deleted_at);
|
||
```
|
||
|
||
2. **后端修改**:
|
||
```typescript
|
||
// 软删除
|
||
router.delete('/users/:id', async (req, res) => {
|
||
await pool.query('UPDATE users SET deleted_at = NOW() WHERE id = ?', [req.params.id])
|
||
res.json({ code: 0, data: { message: '用户已禁用' } })
|
||
})
|
||
|
||
// 恢复
|
||
router.post('/users/:id/restore', async (req, res) => {
|
||
await pool.query('UPDATE users SET deleted_at = NULL WHERE id = ?', [req.params.id])
|
||
res.json({ code: 0, data: { message: '用户已恢复' } })
|
||
})
|
||
```
|
||
|
||
3. **全局查询过滤**:在 `auth.ts` 登录时检查 `deleted_at IS NULL`;其他查询同理。
|
||
|
||
4. **前端二次确认**:
|
||
```
|
||
管理员点击"删除用户" → 弹出确认弹窗:
|
||
"确定要禁用用户「{nickname}」吗?该操作将冻结该用户的所有数据,但不会删除记录。"
|
||
[取消] [确认禁用]
|
||
```
|
||
|
||
### S7:DB 连接移除默认凭据
|
||
|
||
**当前状态**:`connection.ts` 中 `password: process.env.DB_PASSWORD || 'xiaocai123'` 等硬编码默认值。
|
||
|
||
**修复方案**:
|
||
|
||
```typescript
|
||
// ✅ 修复后
|
||
const required = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'] as const
|
||
for (const key of required) {
|
||
if (!process.env[key]) {
|
||
console.error(`[DB] Missing required environment variable: ${key}`)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
const pool = mysql.createPool({
|
||
host: process.env.DB_HOST!,
|
||
user: process.env.DB_USER!,
|
||
password: process.env.DB_PASSWORD!,
|
||
database: process.env.DB_NAME!,
|
||
waitForConnections: true,
|
||
connectionLimit: 10,
|
||
queueLimit: 0,
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 五、UI 交互说明
|
||
|
||
### UX-1:删除撤销
|
||
|
||
```
|
||
交互流程:
|
||
|
||
1. 用户在账单列表左滑删除某条记录
|
||
2. 记录从列表中移除(动画:向左滑出)
|
||
3. 底部弹出 Snackbar:
|
||
┌───────────────────────────────────────────┐
|
||
│ 🗑 已删除 1 条记录 [撤销] │
|
||
└───────────────────────────────────────────┘
|
||
- 背景色: $surface (#FFFFFF)
|
||
- 文字色: $text-sec (#8B7E7E)
|
||
- "撤销"按钮: $primary (#FF8C69) + 500 字重
|
||
- 自动消失时间: 3 秒
|
||
- 从底部上滑动画: 200ms ease-out
|
||
4. 3 秒内点击"撤销":
|
||
- 记录重新插入列表原位置(动画:从左侧滑入)
|
||
- 不调用后端删除接口
|
||
- Snackbar 消失
|
||
5. 3 秒超时未操作:
|
||
- Snackbar 下滑消失
|
||
- 调用后端 DELETE /transactions/:id
|
||
- 如果接口失败,记录恢复到列表 + 错误提示
|
||
|
||
实现要点:
|
||
- 删除操作先只移除前端列表项,不立即调接口
|
||
- 使用 pendingDelete Map 存储待删除记录(id → 原始数据 + 原位置索引)
|
||
- 多次快速删除时 Snackbar 累加计数:"已删除 N 条记录"
|
||
- 撤销时按 LIFO 顺序恢复
|
||
```
|
||
|
||
### UX-2:群组只读标记
|
||
|
||
```
|
||
交互设计:
|
||
|
||
1. TransactionItem 组件变更:
|
||
- 新增 props: `isReadOnly: boolean`
|
||
- 非本人记录(transaction.user_id !== currentUserId):
|
||
├── 右上角显示小锁图标(Lucide Lock, 16rpx, $text-sec 颜色)
|
||
├── 左滑不显示删除按钮
|
||
└── 整体透明度降为 0.85(区分视觉层级)
|
||
|
||
2. 点击交互:
|
||
- 本人记录:点击 → 编辑页面(现有行为)
|
||
- 非本人记录:点击 → 查看页面(只读模式)
|
||
├── 页面布局与编辑页相同
|
||
├── 所有输入框/选择器为 disabled 态
|
||
├── 金额/分类/标签仅展示不可修改
|
||
├── 底部无"保存"按钮
|
||
└── 顶部标题显示"查看记录"
|
||
|
||
3. 群组视图下的记账按钮:
|
||
- 仍然可以新增自己的记录
|
||
- 新增记录自动归属当前用户
|
||
```
|
||
|
||
### UX-3:waitForReady 超时处理
|
||
|
||
```
|
||
交互流程:
|
||
|
||
1. 页面 onMounted 调用 await waitForReady()
|
||
2. 如果 8 秒内未就绪:
|
||
- waitForReady() reject(而非当前 resolve)
|
||
- 页面 catch 中显示全屏错误状态:
|
||
┌─────────────────────────┐
|
||
│ │
|
||
│ ⚠️ 网络连接异常 │
|
||
│ 请检查网络后点击重试 │
|
||
│ │
|
||
│ [重新加载] │
|
||
│ │
|
||
└─────────────────────────┘
|
||
- "重新加载"按钮点击后重新执行 waitForReady() + loadData()
|
||
- 不再静默以未登录状态加载数据
|
||
```
|
||
|
||
### UX-4:预算 group_id 支持
|
||
|
||
```
|
||
交互设计:
|
||
|
||
1. 预算页面(budget/index.vue)变更:
|
||
- 个人视图:显示"我的月度预算"(现有行为不变)
|
||
- 群组视图:显示两个卡片:
|
||
├── 群组总预算卡片:所有成员预算之和 + 群组总支出
|
||
│ └── "群组总预算 ¥ 8,000.00"
|
||
└── 我的预算卡片:当前用户个人预算 + 个人支出
|
||
└── "我的预算 ¥ 3,000.00"
|
||
|
||
2. 设置预算弹窗变更:
|
||
- 个人视图:设置个人月度预算
|
||
- 群组视图:设置"我在群组中的月度预算"
|
||
- 弹窗标题根据视图切换
|
||
|
||
3. 预算进度条变更:
|
||
- 个人视图:支出/预算(现有)
|
||
- 群组视图:
|
||
├── 总进度条:群组总支出 / 群组总预算
|
||
└── 我的进度条:我的支出 / 我的预算(子进度条样式)
|
||
```
|
||
|
||
### UX-5:下拉刷新统一
|
||
|
||
```
|
||
实现规范:
|
||
|
||
1. pages.json 全局配置:
|
||
"globalStyle": {
|
||
"enablePullDownRefresh": true
|
||
}
|
||
|
||
2. 所有数据页面统一模式:
|
||
onPullDownRefresh(async () => {
|
||
await loadData()
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
|
||
3. 需要启用下拉刷新的页面:
|
||
- 首页 index
|
||
- 账单页 bills
|
||
- 统计页 stats
|
||
- 通知中心 notifications
|
||
- 群组管理 group-manage
|
||
- 标签管理 tag-manage
|
||
|
||
4. 不启用下拉刷新的页面:
|
||
- 表单页(add, profile-edit, budget)
|
||
- 设置页
|
||
```
|
||
|
||
### UX-6:离线支持
|
||
|
||
```
|
||
架构设计:
|
||
|
||
1. 操作队列(localStorage):
|
||
- Key: `xc:pendingOps`
|
||
- 数据结构:
|
||
[
|
||
{ id: 'uuid', type: 'create', data: {...}, createdAt: timestamp, status: 'pending' },
|
||
{ id: 'uuid', type: 'update', data: {...}, createdAt: timestamp, status: 'pending' }
|
||
]
|
||
|
||
2. 网络状态监听:
|
||
onMounted(() => {
|
||
uni.onNetworkStatusChange(({ isConnected }) => {
|
||
if (isConnected) processPendingOps()
|
||
})
|
||
})
|
||
|
||
3. 操作执行流程:
|
||
- 有网络:正常调接口
|
||
- 无网络:
|
||
├── 写入操作队列
|
||
├── 本地 state 立即更新(乐观更新)
|
||
├── 记录标记为"待同步"状态(右下角小图标)
|
||
└── Toast:"已保存,将在网络恢复后同步"
|
||
|
||
4. 同步流程(processPendingOps):
|
||
- 按时间顺序逐条执行
|
||
- 全部成功:清除队列 + toast "同步完成"
|
||
- 部分失败:标记失败项 + toast "N 条同步失败" + 保留在队列中重试
|
||
|
||
5. 限制范围:
|
||
- MVP 阶段仅支持"新增记账"离线操作
|
||
- 编辑/删除等操作离线时禁用(提示"请连接网络后操作")
|
||
```
|
||
|
||
### UX-7:Loading 态统一
|
||
|
||
```
|
||
规范:
|
||
|
||
1. 页面级加载:使用 Skeleton 骨架屏组件
|
||
- 首次加载 onMounted 时显示
|
||
- 下拉刷新不显示骨架屏(数据已存在)
|
||
|
||
2. 操作级加载:按钮/操作区域 loading 态
|
||
- 按钮点击后 disabled + 显示 spinner
|
||
- 防止重复提交(debounce 300ms 或 loading ref 守卫)
|
||
|
||
3. 全局 loading:
|
||
- 页面切换时的导航栏 loading(uni.showNavigationBarLoading)
|
||
- 长时间操作(导出/备份)使用全屏 loading + 进度提示
|
||
|
||
4. 空状态:
|
||
- 数据为空时显示空状态插画 + 引导文案
|
||
- 与加载中状态明确区分(不是空白页)
|
||
```
|
||
|
||
---
|
||
|
||
## 六、性能优化方案
|
||
|
||
### Perf-1:分类/标签乐观更新
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 新增/编辑/排序分类后,等待接口返回才更新列表,用户感知延迟 200-500ms |
|
||
| 优化策略 | 先更新本地 Pinia state,再异步调接口;接口失败时回滚 state + toast 提示 |
|
||
| 实现方式 | Store 方法内部:先 `state.items.push(newItem)` → `await api.create()` → 失败则 `state.items.pop()` + `toast('操作失败')` |
|
||
| 预期效果 | 操作感知延迟从 200-500ms 降至 < 16ms(一帧) |
|
||
| 适用范围 | 新增分类/标签、编辑分类/标签、拖拽排序;删除操作仍等接口确认(防误删) |
|
||
|
||
### Perf-2:groupStore 并行加载
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | `refreshAll()` 中 `fetchCategories()` → `fetchTags()` → `fetchTransactions()` 串行执行,总耗时 = 3 次请求之和 |
|
||
| 优化策略 | 无依赖的请求改为 `Promise.all()` 并行执行 |
|
||
| 实现方式 | `await Promise.all([fetchCategories(), fetchTags()]); await fetchTransactions()`(transactions 依赖 categories 的映射) |
|
||
| 预期效果 | 并行部分耗时 = max(单次请求),总体减少 40-60% |
|
||
|
||
### Perf-3:Stats 聚合接口
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 统计页加载时串行调用 `/stats/overview` + `/stats/category` + `/stats/trend`,3 次 HTTP 往返 |
|
||
| 优化策略 | 合并为单一 `/stats/dashboard` 接口,后端并行查询后一次性返回 |
|
||
| 实现方式 | 后端新路由中 `Promise.all([queryOverview, queryCategory, queryTrend])` → 返回 `{ overview, category, trend }` |
|
||
| 预期效果 | 3 次 HTTP 往返 → 1 次;减少约 200ms 网络开销 |
|
||
|
||
### Perf-4:Track 批量 INSERT
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 前端一次页面访问可能触发多条埋点,每条单独 INSERT,N 条 = N 次数据库写入 |
|
||
| 优化策略 | 前端批量发送,后端使用 `INSERT INTO ... VALUES (?), (?), (?)` 批量写入 |
|
||
| 实现方式 | 前端 `tracker.ts` 攒批(500ms 或 10 条),批量 POST;后端 `track.ts` 接收数组后构建批量 INSERT |
|
||
| 预期效果 | 10 条埋点从 10 次 INSERT 降至 1 次,减少数据库连接开销 90% |
|
||
|
||
### Perf-5:Admin N+1 查询
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 用户列表查询后,循环每个用户查询其交易数/群组数等信息,N 个用户 = 1 + N 次查询 |
|
||
| 优化策略 | 使用 LEFT JOIN + GROUP BY 在一次查询中获取所有关联数据 |
|
||
| 实现方式 | `SELECT u.*, COUNT(DISTINCT t.id) as tx_count, COUNT(DISTINCT gm.group_id) as group_count FROM users u LEFT JOIN transactions t ON ... LEFT JOIN group_members gm ON ... GROUP BY u.id` |
|
||
| 预期效果 | 1+N 次查询 → 1 次查询;列表加载从 O(n) 降至 O(1) |
|
||
|
||
### Perf-6:前端数据缓存
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 分类、标签等低频变更数据每次 `onShow` 都重新请求 |
|
||
| 优化策略 | 增加 `lastFetchTime` 标记,5 分钟内 `onShow` 跳过请求 |
|
||
| 实现方式 | Store 中增加 `lastFetchTime: Ref<number>`,`fetch()` 前判断 `Date.now() - lastFetchTime.value < 5 * 60 * 1000` 则跳过 |
|
||
| 预期效果 | 页面切换时减少 60-80% 的冗余请求 |
|
||
|
||
### Perf-7:未读计数缓存
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 每次进入"我的"页面或 `onShow` 都请求未读通知计数 |
|
||
| 优化策略 | 计数结果缓存 30 秒;读取通知后主动清零缓存 |
|
||
| 实现方式 | `unreadCount` 请求后记录 `lastFetchTime`,30s 内直接返回缓存值;`markAllRead()` 后 `unreadCount = 0` + 清缓存 |
|
||
| 预期效果 | 频繁切换页面时减少通知计数请求 80%+ |
|
||
|
||
### Perf-8:Logs 流式读取(P2)
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 管理后台查看日志时全量加载到内存 |
|
||
| 优化策略 | 使用 Node.js stream 分批返回 |
|
||
| 实现方式 | `fs.createReadStream(logPath).pipe(res)` 或数据库 `queryStream()` |
|
||
| 预期效果 | 内存占用从 O(n) 降至 O(buffer_size) |
|
||
|
||
### Perf-9:ChartWrapper 延迟渲染(P2)
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 当前问题 | 统计页所有图表同时渲染,低端设备卡顿 |
|
||
| 优化策略 | 使用 IntersectionObserver,图表进入可视区域后才渲染 |
|
||
| 实现方式 | ChartWrapper 组件内监听可视状态,不可见时显示占位(固定高度 div) |
|
||
| 预期效果 | 首屏渲染时间减少 30-50% |
|
||
|
||
---
|
||
|
||
## 七、测试体系建设计划
|
||
|
||
> 目标:从零覆盖到核心路径有保障,分 4 阶段逐步建设
|
||
|
||
### 阶段一:后端路由集成测试(P1,预计 2 周)
|
||
|
||
**范围**:核心 CRUD 路由的集成测试
|
||
|
||
**技术栈**:Jest + supertest + 测试数据库
|
||
|
||
**覆盖清单**:
|
||
|
||
| 模块 | 测试用例 | 优先级 |
|
||
|------|----------|--------|
|
||
| auth | 登录成功/失败、Token 生成与验证、无效 Token 拒绝 | P0 |
|
||
| transaction | CRUD、group_id 过滤、分页、金额精度 | P0 |
|
||
| category | CRUD、默认/自定义分类隔离、排序、迁移 | P1 |
|
||
| budget | CRUD、月度预算、群组视图 | P1 |
|
||
| stats | overview/category/trend 查询、period 参数校验 | P2 |
|
||
|
||
**实现步骤**:
|
||
|
||
1. 安装依赖:`npm i -D jest ts-jest supertest @types/supertest`
|
||
2. 创建测试数据库配置:`.env.test` 指向独立测试库
|
||
3. 编写 `tests/setup.ts`:每个测试套件前清空测试库 + seed
|
||
4. 编写路由测试:使用 `supertest(app)` 发送请求,断言状态码 + 返回格式
|
||
5. 配置 `npm test` 命令
|
||
|
||
**验收标准**:核心路由测试覆盖率 ≥ 80%,`npm test` 全绿。
|
||
|
||
### 阶段二:前端 Store 单元测试(P1,预计 1.5 周)
|
||
|
||
**范围**:Pinia Store 的核心逻辑测试
|
||
|
||
**技术栈**:Vitest + @pinia/testing
|
||
|
||
**覆盖清单**:
|
||
|
||
| Store | 测试用例 | 优先级 |
|
||
|-------|----------|--------|
|
||
| transaction | fetchList/fetchDetail/create/update/delete、乐观更新、删除撤销 | P0 |
|
||
| category | fetchAll/create/update/sort/乐观更新 | P1 |
|
||
| budget | fetch/set/群组视图 | P1 |
|
||
| group | fetchGroups/switchToPersonal/leaveGroup(BUG-03 回归) | P0 |
|
||
| user | login/logout/token 管理 | P1 |
|
||
|
||
**实现步骤**:
|
||
|
||
1. 安装依赖:`npm i -D vitest @pinia/testing @vue/test-utils`
|
||
2. 创建 `vitest.config.ts`
|
||
3. Mock API 调用:使用 `vi.mock('@/api/*')`
|
||
4. 编写 Store 测试:使用 `createTestingPinia()`
|
||
5. 配置 `npm run test:unit` 命令
|
||
|
||
**验收标准**:Store 核心方法测试覆盖率 ≥ 70%。
|
||
|
||
### 阶段三:API 契约测试(P2,预计 1 周)
|
||
|
||
**范围**:确保前后端 API 接口格式一致
|
||
|
||
**技术栈**:Jest + JSON Schema 验证
|
||
|
||
**实现方式**:
|
||
|
||
1. 定义 API 响应 Schema(`tests/schemas/`):
|
||
```typescript
|
||
// transaction-response.schema.ts
|
||
export const transactionListSchema = {
|
||
type: 'object',
|
||
required: ['code', 'data'],
|
||
properties: {
|
||
code: { const: 0 },
|
||
data: {
|
||
type: 'object',
|
||
required: ['list', 'total'],
|
||
properties: {
|
||
list: { type: 'array' },
|
||
total: { type: 'number' }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
2. 集成测试中同时验证响应格式符合 Schema
|
||
3. 前端 `request.ts` 添加开发模式响应校验
|
||
|
||
**验收标准**:所有已定义 Schema 的接口响应 100% 符合契约。
|
||
|
||
### 阶段四:工具函数单元测试(P2,预计 1 周)
|
||
|
||
**范围**:前端 `utils/` 和后端 `utils/` 的纯函数测试
|
||
|
||
**覆盖清单**:
|
||
|
||
| 模块 | 测试用例 |
|
||
|------|----------|
|
||
| `format.ts` | `formatAmount`、`formatDate`(含跨年 BUG-02 回归)、`formatAmountRaw` |
|
||
| `request.ts` | 401 重试队列、超时处理、请求参数格式 |
|
||
| `app-ready.ts` | waitForReady 正常/超时场景 |
|
||
| `backup.ts`(后端) | 备份文件生成、签名校验 |
|
||
| `date.ts`(后端) | `getCurrentMonth`、`getMonthRange` |
|
||
|
||
**验收标准**:工具函数测试覆盖率 ≥ 90%。
|
||
|
||
---
|
||
|
||
## 八、待确认问题
|
||
|
||
| # | 问题 | 影响范围 | 建议方案 | 风险 |
|
||
|---|------|----------|----------|------|
|
||
| Q1 | Refresh Token 存储方式:数据库 vs Redis | S5 | 当前技术栈无 Redis,建议存 MySQL `refresh_tokens` 表;如果后续并发量增长,迁移到 Redis | MySQL 写入频率低,短中期可行 |
|
||
| Q2 | 软删除的级联范围:是否需要同时软删除该用户的所有交易记录? | S6 | 建议仅软删除用户账号(`deleted_at`),交易记录保留但通过 `deleted_at IS NULL` 过滤;历史数据完整性更重要 | 保留交易可能影响群组统计,需在群组视图中标注"已注销用户" |
|
||
| Q3 | 删除撤销的批量操作上限:最多支持同时撤销几条? | UX-1 | 建议上限 5 条(Snackbar 最多显示"已删除 5 条记录");超过 5 条时直接删除不走撤销流程 | 5 条覆盖绝大多数场景,过多会增加本地状态管理复杂度 |
|
||
| Q4 | 离线记账的冲突策略:如果离线期间同一条记录被其他设备修改了怎么办? | UX-6 | MVP 阶段仅支持离线新增(不涉及修改/删除),新增记录使用服务端生成的 ID,不存在主键冲突 | 如需支持离线编辑,需引入版本号或时间戳对比机制 |
|
||
| Q5 | 财务报告推送的微信订阅消息模板审核:微信对金融类模板审核严格,是否需要用户额外授权? | F-1 | 建议先实现站内通知(现有通知系统),微信订阅消息作为增强项;需提前申请微信模板消息审核 | 微信审核可能被拒,需准备备选方案(如短信或仅站内推送) |
|
||
| Q6 | 预算预警的检查时机:仅在记账后检查 vs 定时全量扫描? | F-2 | 建议双重策略:记账后实时检查(精准、低开销)+ 每日凌晨全量扫描(覆盖非记账场景如周期记账自动生成) | 仅实时检查可能遗漏周期记账触发的超支 |
|
||
| Q7 | 乐观更新的回滚粒度:分类排序失败时,是回滚全部排序还是仅回滚失败项? | Perf-1 | 建议回滚全部:排序是全量操作(传递完整 ID 数组),部分回滚语义不清晰;失败后恢复排序前快照 + toast | 全量回滚用户感知更一致,但可能丢失用户已做的其他排序操作 |
|
||
| Q8 | 测试环境数据隔离:集成测试使用独立数据库还是 Docker 容器? | T-1 | 建议独立测试数据库(`.env.test`),不用 Docker(减少 CI 复杂度);CI 环境中用 GitHub Actions service container 启动 MySQL | 本地开发需额外配置测试数据库,但比 Docker 方案简单 |
|
||
|
||
---
|
||
|
||
## 九、迭代排期建议
|
||
|
||
| 阶段 | 时间 | 内容 | 交付物 |
|
||
|------|------|------|--------|
|
||
| **Phase 0** | 第 1 周 | P0 安全修复(S1/S2/S3) | 凭据轮换完成、SQL 参数化、TOKEN_SECRET 强制 |
|
||
| **Phase 1** | 第 2-3 周 | P1 安全(S4-S7)+ P1 代码质量(C1/C2/C4/C5/C7) | 双 Token 机制、软删除、代码去重 |
|
||
| **Phase 2** | 第 4-5 周 | P1 性能(Perf-1~7)+ P1 UX(UX-1~4) | 乐观更新、聚合接口、删除撤销、群组只读 |
|
||
| **Phase 3** | 第 6-7 周 | P1 测试(T-1)+ P1 新功能(F-1/F-2) | 集成测试覆盖、财务报告推送、预算预警 |
|
||
| **Phase 4** | 第 8-10 周 | P2 需求(C3/C6/C8/C9 + Perf-8~9 + UX-5~7 + T-2~3 + F-3~5) | 离线支持、AA 分账、智能记账 |
|
||
| **Phase 5** | 远期 | P3 需求(C10 + Perf-10) | 路由中间件统一、数据可视化增强 |
|
||
|
||
---
|
||
|
||
*文档版本: v2.0 | 最后更新: 2026-06-10*
|