Files
xiaocai/docs/arch-alignment.md
wangxiaogang 31f6487d61 feat: 前后端功能对齐 - 实现8个对齐差距
P0-1: 分类拖拽排序 - category-manage 添加拖拽 UI
P1-1: 反馈回复展示 - 新增 GET /feedback/mine + 前端我的反馈Tab
P1-2: 备份管理页面 - 新增下载端点 + backup-manage 页面
P2-1: 统计年度/周视图 - stats 支持 period 参数 + 前端维度切换器
P2-2: 数据导入 - 新增 POST /import 端点 + data-import 页面
P2-3: 数据导出服务端化 - 新增 GET /export 流式端点
P3-1: 健康检查展示 - health 移到 auth 前 + admin 状态卡片
P3-2~4: 交易标签系统 - tags CRUD + 交易关联 + 按标签筛选统计

后端: 新增 tag.ts/export.ts 路由, 改造 feedback/backup/transaction/stats
前端: 新增 DragSortList 组件, 3个新页面, 改造 7 个现有页面
QA 修复: 5个严重Bug + 4个潜在问题
2026-06-10 17:30:36 +08:00

22 KiB
Raw Permalink Blame History

小菜记账 增量架构设计文档

版本v1.0 | 架构师高见远Gao | 日期2025-07


Part A: 系统设计

1. 实现方案 + 框架选型

1.1 分类拖拽排序P0-1

技术挑战:微信小程序不支持 HTML5 Drag and Drop APIUni-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 HeadersContent-Type: text/csv; charset=utf-8 / application/json; charset=utf-8Content-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/categoryGET /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 支持 tagIdsGET 支持 tagId 筛选GET/:id 返回 tags
server/src/index.ts health 端点移到 authMiddleware 之前;注册 tag、export 路由
client/src/api/feedback.ts 新增 getMyFeedbacks() API 函数
client/src/api/stats.ts 修改 getCategoryStatsgetTrend 支持 period 参数
client/src/api/transaction.ts 新增 importTransactions();类型扩展 tagIdstagId
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. 数据结构和接口(类图)

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 表

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 表

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 类型

{ 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 文件流 下载备份文件(临时签名 URL1h 有效)

实现方式:在 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日趋势按月 GROUPDATE_FORMAT(date, '%Y-%m')

年视图趋势响应(新增 label 字段):

{ label: string; date: string; amount: number }[]
// 年视图 label = "1月"~"12月"
// 周视图 label = "周一"~"周日"
// 月视图 label = "1日"~"31日"(保持兼容)
数据导入
方法 路径 请求 响应 说明
POST /api/transactions/import { items: ImportItem[] } { total, imported, skipped, errors[] } 批量导入

ImportItem 类型

{
  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 分类拖拽排序

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 数据导入

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 标签关联交易

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 服务端导出

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=yearmonth 参数被忽略,使用当前年份;当 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 内置 streamfsos 模块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 支持 tagIdsGET 支持 tagId 筛选GET/:id 返回 tags
  • server/src/routes/stats.ts — category/trend 增加 period 参数week/year 视图)

依赖任务T01tags 表需先存在)

优先级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 TokenBearer 格式30天有效期
- 公开路径(无需认证):/api/auth/login, /api/auth/demo-login, /api/health, /api/backup/:id/downloadtoken 校验)
- 前端 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. 任务依赖图

graph LR
    T01[T01: 后端基础设施+DB迁移] --> T02[T02: 后端API改造]
    T01 --> T03[T03: 前端API+Store+新页面]
    T02 --> T04[T04: 前端现有页面改造]
    T03 --> T04
    T04 --> T05[T05: 集成联调+边界处理]