# 小菜记账 增量架构设计文档 > 版本:v1.0 | 架构师:高见远(Gao) | 日期:2025-07 --- ## Part A: 系统设计 ### 1. 实现方案 + 框架选型 #### 1.1 分类拖拽排序(P0-1) **技术挑战**:微信小程序不支持 HTML5 Drag and Drop API,Uni-app 的 `movable-area`/`movable-view` 适合单元素拖拽而非列表重排。 **方案**:基于 Touch 事件的自定义拖拽排序组件 `DragSortList`。 - 监听 `@touchstart` / `@touchmove` / `@touchend` 三组事件 - 拖拽时通过 `transform: translateY()` 实现元素位移动画 - 拖拽过程中实时计算目标索引,交换数组元素位置 - 松手后调用已就绪的 `sortCategories(ids)` API 批量更新排序 - 支出/收入各自独立排序(复用现有 `tabType` 切换 + `currentCategories` computed) - **不引入第三方拖拽库**——Uni-app 小程序环境下无成熟可用方案,自研可控性更高 #### 1.2 服务端导出流式响应(P2-3) **方案**:Node.js 原生 Stream + `pipeline()` 管道。 - **< 1 万条**:使用 `Readable.from()` 创建内存流,逐行 push 数据,通过 `pipeline()` 管道传输到 Response - **≥ 1 万条**:先写入临时文件(`os.tmpdir()`),再 `fs.createReadStream()` 流式返回,完成后删除临时文件 - CSV 格式:手动拼装(无需额外依赖),添加 BOM 头确保中文兼容 - JSON 格式:流式写入 `[` + 逐条 `,` 分隔 + `]`,避免全量 JSON.stringify - Response Headers:`Content-Type: text/csv; charset=utf-8` / `application/json; charset=utf-8`,`Content-Disposition: attachment; filename=...` - 超时保护:流式响应不设 `express.json()` body 限制,但设 60s 超时 #### 1.3 标签系统数据库设计(P3-2~4) **设计原则**:标签不区分收支类型,跨分类使用,轻量关联表。 - `tags` 表:用户维度,每用户最多 20 个标签,8 色预设 - `transaction_tags` 表:多对多关联,每笔交易最多 5 个标签 - 标签与交易在同一事务中写入(`POST/PUT /transactions` 扩展 `tagIds` 字段) - 查询交易列表时 LEFT JOIN 聚合标签信息(避免 N+1 查询) - 统计按标签聚合使用 `GROUP BY tag_id` #### 1.4 数据导入(P2-2) **方案**:JSON 格式导入,服务端校验 + 批量写入 + 去重。 - 接收 JSON 数组,校验字段格式(必填:date, type, amount, category_name) - 按 `date + type + amount + note` 组合去重(查现有记录比对) - 使用 `INSERT ... VALUES (...), (...), ...` 批量写入(每批 100 条) - 返回结果:`{ total, imported, skipped, errors[] }` #### 1.5 统计年度/周视图(P2-1) **方案**:后端 `period` 参数扩展 + 前端维度切换器。 - 后端:`GET /stats/category` 和 `GET /stats/trend` 新增 `period=week|month|year` 参数 - `week`:基于当前日期计算本周一至本周日,趋势 X 轴为周一~周日 - `month`:保持现有行为(默认) - `year`:基于当前年份 1-12 月,趋势 X 轴为 1月~12月 - 年视图日期范围计算:`startDate = ${year}-01-01`, `endDate = ${year}-12-31` - 周视图日期范围计算:获取本周一和本周日 - 趋势数据 GROUP BY 逻辑:年视图按月 GROUP,周视图按日 GROUP --- ### 2. 文件列表及相对路径 #### 2.1 新建文件 | 文件路径 | 说明 | |---------|------| | `client/src/api/backup.ts` | 备份管理 API(下载备份) | | `client/src/api/tag.ts` | 标签 CRUD + 统计 API | | `client/src/api/export.ts` | 服务端导出 API | | `client/src/api/health.ts` | 健康检查 API | | `client/src/stores/tag.ts` | 标签 Pinia Store | | `client/src/components/DragSortList/DragSortList.vue` | 拖拽排序列表组件 | | `client/src/pages/backup-manage/index.vue` | 备份管理页面(仅管理员) | | `client/src/pages/data-import/index.vue` | 数据导入页面 | | `client/src/pages/tag-manage/index.vue` | 标签管理页面 | | `server/src/routes/tag.ts` | 标签 CRUD + 按标签统计路由 | | `server/src/routes/export.ts` | 服务端导出路由 | #### 2.2 修改文件 | 文件路径 | 主要变更 | |---------|---------| | `server/src/db/schema.sql` | 新增 tags、transaction_tags 表定义 | | `server/src/db/init.ts` | 新增 tags、transaction_tags 表迁移 | | `server/src/routes/feedback.ts` | 新增 `GET /mine` 端点(用户查看自己的反馈) | | `server/src/routes/backup.ts` | 新增 `GET /:id/download` 端点(临时签名 URL) | | `server/src/routes/stats.ts` | category/trend 端点增加 `period` 参数支持 | | `server/src/routes/transaction.ts` | POST/PUT 支持 `tagIds`;GET 支持 `tagId` 筛选;GET/:id 返回 tags | | `server/src/index.ts` | health 端点移到 authMiddleware 之前;注册 tag、export 路由 | | `client/src/api/feedback.ts` | 新增 `getMyFeedbacks()` API 函数 | | `client/src/api/stats.ts` | 修改 `getCategoryStats`、`getTrend` 支持 `period` 参数 | | `client/src/api/transaction.ts` | 新增 `importTransactions()`;类型扩展 `tagIds`、`tagId` | | `client/src/api/admin.ts` | 新增 `getHealth()` API 函数 | | `client/src/stores/category.ts` | 无需修改(sortCategories 已就绪) | | `client/src/stores/stats.ts` | fetchCategoryStats/fetchTrend 支持 period 参数 | | `client/src/pages/category-manage/index.vue` | 集成 DragSortList 拖拽排序 | | `client/src/pages/feedback/index.vue` | 新增"我的反馈"Tab | | `client/src/pages/stats/index.vue` | 添加维度切换器(周/月/年) | | `client/src/pages/profile/index.vue` | 导出面板增加"服务端导出"选项 | | `client/src/pages/add/index.vue` | 添加标签选择入口 | | `client/src/pages/bills/index.vue` | 添加标签筛选功能 | | `client/src/pages/admin/index.vue` | 添加服务状态卡片 | | `client/src/pages.json` | 注册新页面路由 | --- ### 3. 数据结构和接口(类图) ```mermaid classDiagram class Tag { +int id +int user_id +string name +string color +string created_at } class TransactionTag { +int transaction_id +int tag_id } class Transaction { +int id +int user_id +int amount +string type +int category_id +string note +string date +int group_id +int recurring_id +Tag[] tags } class Feedback { +int id +int user_id +string type +string content +string contact +string status +string admin_reply +string created_at } class Category { +int id +int user_id +string name +string icon +string color +string type +int sort_order +int is_custom } Transaction "1" --o "*" TransactionTag : has Tag "1" --o "*" TransactionTag : referenced_by Transaction ..> Category : belongs_to note for Tag "每用户最多 20 个标签\n8 色预设颜色选择器\n标签不区分收支类型" note for TransactionTag "复合主键 (transaction_id, tag_id)\n每笔交易最多 5 个标签" ``` #### 3.1 新增数据库表 **tags 表** ```sql CREATE TABLE IF NOT EXISTS tags ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, name VARCHAR(20) NOT NULL COMMENT '标签名称', color VARCHAR(20) NOT NULL DEFAULT '#FF8C69' COMMENT '标签颜色', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_name (user_id, name), INDEX idx_user (user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` **transaction_tags 表** ```sql CREATE TABLE IF NOT EXISTS transaction_tags ( transaction_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY (transaction_id, tag_id), INDEX idx_tag (tag_id), FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 3.2 新增/修改 API 端点 ##### 标签 CRUD | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | GET | `/api/tags` | — | `{ list: Tag[] }` | 获取当前用户所有标签 | | POST | `/api/tags` | `{ name, color }` | `{ id }` | 创建标签(限 20 个/用户) | | PUT | `/api/tags/:id` | `{ name?, color? }` | — | 更新标签 | | DELETE | `/api/tags/:id` | — | — | 删除标签(级联删除关联) | | GET | `/api/stats/by-tag` | `?month&period&type` | `TagStat[]` | 按标签统计 | **TagStat 类型**: ```ts { id: number; name: string; color: string; amount: number; count: number } ``` ##### 用户反馈列表 | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | GET | `/api/feedback/mine` | `?page&pageSize` | `{ list, total, page, pageSize }` | 当前用户的反馈列表 | ##### 备份下载 | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | GET | `/api/backup/:id/download` | — | 文件流 | 下载备份文件(临时签名 URL,1h 有效) | **实现方式**:在 `backup` 表中无需存储,使用 HMAC-SHA256 生成带过期时间的签名 token,下载时校验签名。签名格式:`backupId:timestamp:hmac-sha256(backupId:timestamp)`。将签名作为 `?token=xxx` 传给前端,下载端点校验 token 有效性(无需认证中间件)。 ##### 统计年度/周视图 | 方法 | 路径 | 请求参数 | 说明 | |------|------|---------|------| | GET | `/api/stats/category` | 新增 `period=week\|month\|year` | 周/月/年分类统计 | | GET | `/api/stats/trend` | 新增 `period=week\|month\|year` | 周/月/年趋势数据 | **period 参数逻辑**: - `week`(新增):`startDate` = 本周一,`endDate` = 本周日,趋势按日 GROUP - `month`(默认):保持现有行为,趋势按日 GROUP - `year`(新增):`startDate` = 当年1月1日,`endDate` = 当年12月31日,趋势按月 GROUP(`DATE_FORMAT(date, '%Y-%m')`) **年视图趋势响应**(新增 `label` 字段): ```ts { label: string; date: string; amount: number }[] // 年视图 label = "1月"~"12月" // 周视图 label = "周一"~"周日" // 月视图 label = "1日"~"31日"(保持兼容) ``` ##### 数据导入 | 方法 | 路径 | 请求 | 响应 | 说明 | |------|------|------|------|------| | POST | `/api/transactions/import` | `{ items: ImportItem[] }` | `{ total, imported, skipped, errors[] }` | 批量导入 | **ImportItem 类型**: ```ts { date: string // 必填,YYYY-MM-DD type: string // 必填,expense | income amount: number // 必填,单位:分 category_name: string // 可选,匹配不到则归入"未分类" note?: string tag_names?: string[] // 可选,按名称匹配标签 } ``` ##### 服务端导出 | 方法 | 路径 | 请求参数 | 响应 | 说明 | |------|------|---------|------|------| | GET | `/api/export` | `startDate, endDate, type, format=csv\|json` | 文件流 | 流式导出 | ##### 健康检查(改造) | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/health` | 移到 `authMiddleware` 之前,无需认证 | ##### 交易记录(改造) | 方法 | 路径 | 变更 | |------|------|------| | POST | `/api/transactions` | 请求新增 `tagIds: number[]`(可选,最多 5 个) | | PUT | `/api/transactions/:id` | 请求新增 `tagIds: number[]`(可选,最多 5 个,全量替换) | | GET | `/api/transactions` | 新增 `tagId` 查询参数筛选 | | GET | `/api/transactions/:id` | 响应新增 `tags: { id, name, color }[]` | --- ### 4. 程序调用流程(时序图) #### 4.1 分类拖拽排序 ```mermaid sequenceDiagram participant U as 用户 participant P as category-manage participant D as DragSortList participant S as categoryStore participant A as sortCategories API participant B as PUT /categories/sort U->>P: 长按分类项进入排序模式 P->>P: showSortMode = true P->>D: 渲染 DragSortList (items=currentCategories) U->>D: touchstart (记录起始位置) U->>D: touchmove (计算偏移, 交换元素位置) D->>D: 实时更新 items 数组顺序 U->>D: touchend (拖拽结束) D->>P: @change事件 (新顺序ids) P->>S: sortCategories(newIds) S->>A: sortCategories(ids) A->>B: PUT /categories/sort { ids } B-->>A: { code: 0 } A-->>S: 成功 S->>S: fetchCategories() 刷新 ``` #### 4.2 数据导入 ```mermaid sequenceDiagram participant U as 用户 participant P as data-import participant A as transaction API participant S as POST /transactions/import participant DB as MySQL U->>P: 选择 JSON 文件 P->>P: uni.chooseFile / 读取文件内容 P->>P: 解析 JSON,显示预览(条数、日期范围) U->>P: 确认导入 P->>A: importTransactions(items) A->>S: POST /transactions/import { items } S->>S: 校验每条记录格式 S->>DB: 查询已有记录 (去重比对) S->>DB: 批量 INSERT (每批100条) S->>DB: 写入 transaction_tags (如有 tag_names) S-->>A: { total, imported, skipped, errors } A-->>P: 导入结果 P->>U: 显示导入结果(成功X条, 跳过Y条) ``` #### 4.3 标签关联交易 ```mermaid sequenceDiagram participant U as 用户 participant P as add/index participant TS as tagStore participant TA as transaction API participant S as POST /transactions participant DB as MySQL U->>P: 点击"添加标签" P->>P: 显示标签选择面板(已有标签 + 新建入口) U->>P: 选择标签(最多5个) P->>P: selectedTagIds 更新 U->>P: 保存交易 P->>TA: createTransaction({...data, tagIds}) TA->>S: POST /transactions { amount, type, ..., tagIds } S->>S: 校验 tagIds (≤5, 属于当前用户) S->>DB: INSERT INTO transactions S->>DB: INSERT INTO transaction_tags (批量) S-->>TA: { id } TA-->>P: 成功 Note over P,U: 编辑交易时同理,PUT 全量替换 tagIds ``` #### 4.4 服务端导出 ```mermaid sequenceDiagram participant U as 用户 participant P as profile/index participant A as export API participant S as GET /api/export participant DB as MySQL U->>P: 点击"服务端导出" P->>A: serverExport(params) A->>S: GET /api/export?startDate=&endDate=&type=&format= S->>DB: SELECT COUNT(*) (判断数量) alt 记录 < 10000 S->>DB: 流式查询 (cursor) S->>S: Readable.from() 逐行生成 CSV/JSON S-->>P: 流式响应 (Transfer-Encoding: chunked) else 记录 ≥ 10000 S->>DB: 流式查询写入临时文件 S->>S: fs.createReadStream() S-->>P: 流式响应 S->>S: 删除临时文件 end P->>U: 保存文件到本地 ``` --- ### 5. 待明确事项 | # | 事项 | 当前假设 | |---|------|---------| | 1 | 备份下载的签名 URL 是通过新端点直接下载,还是返回签名 URL 让前端跳转? | 采用直接下载方式:`GET /api/backup/:id/download?token=xxx`,后端校验 token 后流式返回文件 | | 2 | 年视图/周视图的 `month` 参数如何处理? | 新增 `period` 参数,当 `period=year` 时 `month` 参数被忽略,使用当前年份;当 `period=week` 时使用当前周 | | 3 | 导入 JSON 的 `category_name` 匹配不到时如何处理? | 归入"未分类"(category_id = NULL),不自动创建分类 | | 4 | 标签预设 8 色具体色值? | 复用现有 `colorOptions`:`#FF8C69, #7BC67E, #5B9BD5, #FFD700, #FF69B4, #8B5CF6, #F97316, #06B6D4` | | 5 | 健康检查移到 authMiddleware 前是否影响安全? | `/api/health` 仅返回 DB 连接状态和服务器时间,不泄露敏感信息,安全无影响 | | 6 | 数据导入文件大小限制? | 前端限制 5MB,后端 `express.json({ limit: '5mb' })` 仅对导入端点放宽(当前全局 10kb) | --- ## Part B: 任务分解 ### 6. 需要新增的依赖包 #### 后端 (server) ``` 无新增依赖 ``` > 流式导出使用 Node.js 内置 `stream`、`fs`、`os` 模块,CSV 拼装手写无需 `json2csv`,签名 URL 使用已有 `crypto` 模块。 #### 前端 (client) ``` 无新增依赖 ``` > 拖拽排序使用 Touch 事件自研实现,不引入第三方拖拽库。 --- ### 7. 任务列表(按依赖顺序) #### T01: 后端基础设施 + 数据库迁移 **描述**:新增 tags/transaction_tags 表、修改 health 端点位置、注册新路由模块、调整 express.json 限制。 **涉及文件**: - `server/src/db/schema.sql` — 新增 tags、transaction_tags 表定义 - `server/src/db/init.ts` — 新增迁移逻辑(创建 tags、transaction_tags 表) - `server/src/index.ts` — health 移到 authMiddleware 前;注册 tag、export 路由;导入端点 body 限制调整 - `server/src/routes/tag.ts` — 新建:标签 CRUD 端点 + 按标签统计端点 - `server/src/routes/export.ts` — 新建:服务端流式导出端点 **依赖任务**:无 **优先级**:P0 --- #### T02: 后端 API 改造(现有路由扩展) **描述**:扩展现有路由以支持新功能——反馈 mine 端点、备份下载、交易 tagIds 支持、统计 period 参数。 **涉及文件**: - `server/src/routes/feedback.ts` — 新增 `GET /mine` 端点 - `server/src/routes/backup.ts` — 新增 `GET /:id/download` 端点(签名 URL 校验) - `server/src/routes/transaction.ts` — POST/PUT 支持 tagIds;GET 支持 tagId 筛选;GET/:id 返回 tags - `server/src/routes/stats.ts` — category/trend 增加 period 参数(week/year 视图) **依赖任务**:T01(tags 表需先存在) **优先级**:P0 --- #### T03: 前端 API 层 + Store 层 + 新页面 **描述**:新增所有前端 API 函数和 Store,创建新页面(标签管理、备份管理、数据导入),创建拖拽组件。 **涉及文件**: - `client/src/api/feedback.ts` — 新增 `getMyFeedbacks()` - `client/src/api/backup.ts` — 新建:备份列表 + 下载 API - `client/src/api/tag.ts` — 新建:标签 CRUD + 统计 API - `client/src/api/export.ts` — 新建:服务端导出 API - `client/src/api/health.ts` — 新建:健康检查 API - `client/src/api/stats.ts` — 修改:getCategoryStats/getTrend 支持 period - `client/src/api/transaction.ts` — 新增 `importTransactions()`;类型扩展 tagIds/tagId - `client/src/api/admin.ts` — 新增 `getHealth()` - `client/src/stores/tag.ts` — 新建:标签 Pinia Store - `client/src/stores/stats.ts` — 修改:fetchCategoryStats/fetchTrend 支持 period - `client/src/components/DragSortList/DragSortList.vue` — 新建:拖拽排序组件 - `client/src/pages/tag-manage/index.vue` — 新建:标签管理页面 - `client/src/pages/backup-manage/index.vue` — 新建:备份管理页面 - `client/src/pages/data-import/index.vue` — 新建:数据导入页面 - `client/src/pages.json` — 注册新页面路由 **依赖任务**:T01(需知 API 端点格式) **优先级**:P1 --- #### T04: 前端现有页面改造 **描述**:改造现有页面以集成新功能——分类拖拽、反馈列表、统计维度切换、标签入口、服务端导出、健康检查卡片。 **涉及文件**: - `client/src/pages/category-manage/index.vue` — 集成 DragSortList 拖拽排序 - `client/src/pages/feedback/index.vue` — 新增"我的反馈"Tab - `client/src/pages/stats/index.vue` — 添加维度切换器(周/月/年)+ 标签统计入口 - `client/src/pages/profile/index.vue` — 导出面板增加"服务端导出"选项 - `client/src/pages/add/index.vue` — 添加标签选择入口 - `client/src/pages/bills/index.vue` — 添加标签筛选功能 - `client/src/pages/admin/index.vue` — 添加服务状态卡片(30s 自动刷新) **依赖任务**:T03(依赖 API 层和组件) **优先级**:P1 --- #### T05: 集成联调 + 边界处理 **描述**:前后端联调、边界场景处理(导入去重、大量数据导出、拖拽边界)、最终测试修复。 **涉及文件**: - 可能涉及 T01~T04 中所有文件的微调 - 重点:`server/src/routes/export.ts`(大量数据流式导出稳定性) - 重点:`server/src/routes/transaction.ts`(导入去重逻辑) - 重点:`client/src/components/DragSortList/DragSortList.vue`(拖拽边界场景) **依赖任务**:T04 **优先级**:P2 --- ### 8. 共享知识(跨文件约定) ``` - API 响应格式统一:{ code: 0, data: T } 成功;{ code: 5位数字, message: string } 失败 - 错误码约定: - 40001: 参数无效 - 40002: 类型无效 - 40100: 未登录 - 40101: token过期 - 40300: 无权限 - 40400: 资源不存在 - 42900: 请求频繁 - 50000: 服务器内部错误 - 金额单位统一为"分"(INT),前端展示时 / 100 - 日期格式统一为 YYYY-MM-DD,月份为 YYYY-MM - 认证使用 HMAC-SHA256 Token,Bearer 格式,30天有效期 - 公开路径(无需认证):/api/auth/login, /api/auth/demo-login, /api/health, /api/backup/:id/download(token 校验) - 前端 request() 函数自动处理 401 重登录、错误 toast - Pinia Store 方法命名:fetch* (获取数据)、add/create (新增)、update (更新)、delete (删除) - 前端 API 函数命名:get* (GET), create*/submit* (POST), update*/sort* (PUT), delete* (DELETE) - SCSS 变量:$primary=#FF8C69, $success=#7BC67E, $danger=#FF6B6B, $text=#2D1B1B, $surface=#FFFFFF, $bg=#FFF8F0 - 组件样式使用 scoped + @import '@/styles/mixins.scss' - 标签预设 8 色:#FF8C69, #7BC67E, #5B9BD5, #FFD700, #FF69B4, #8B5CF6, #F97316, #06B6D4 - 每笔交易最多 5 个标签,每用户最多 20 个标签 - 导入文件限制 5MB,导出流式响应超时 60s - 分页参数:page (从1开始), pageSize (默认20, 最大100) ``` --- ### 9. 任务依赖图 ```mermaid graph LR T01[T01: 后端基础设施+DB迁移] --> T02[T02: 后端API改造] T01 --> T03[T03: 前端API+Store+新页面] T02 --> T04[T04: 前端现有页面改造] T03 --> T04 T04 --> T05[T05: 集成联调+边界处理] ```