Transactions 模块 API 文档
基础信息
- Base URL:
/api/transactions - 认证方式: JWT Bearer Token
数据库表
transactions 表
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | text (PK) | 必填 | 10位随机ID |
| bill_id | text (FK→bills) | 必填 | 所属账单 |
| category_id | text (FK→categories) | 选填 | 分类ID(通常为 NULL) |
| uploaded_by | text (FK→users) | 选填 | 上传者用户ID |
| tx_id | text | 选填 | 原始CSV中的ID |
| tx_time | datetime | 必填 | 交易时间(原始格式,不转换) |
| tx_year | integer | 选填 | 年份(生成字段) |
| tx_month | integer | 选填 | 月份(生成字段) |
| amount | integer | 必填 | 金额(单位:分,整数存储) |
| fee | integer | 必填 | 手续费(默认0) |
| currency | text | 必填 | 币种,默认 CNY |
| tx_type | text | 选填 | 交易类型(非固定枚举:支出/收入/退款等) |
| account1 | text | 选填 | 账户1 |
| account2 | text | 选填 | 账户2 |
| account1_id | text (FK→accounts) | 选填 | 账户1 ID |
| account2_id | text (FK→accounts) | 选填 | 账户2 ID |
| target_amount | integer | 选填 | 目标金额(转账用) |
| note | text | 选填 | 备注 |
| bill_tag | text | 选填 | 账单标记 |
| tag | text | 选填 | 标签 |
| bill_image | text | 选填 | 账单图片路径 |
| created_at | datetime | 必填 | 创建时间 |
| updated_at | datetime | 必填 | 更新时间 |
| category | text | 选填 | 分类名称(迁移后新增,与 category_id 并存) |
| sub_category | text | 选填 | 二级分类名称(迁移后新增) |
| reimbursed | text | 选填 | 已报销标记(迁移后新增) |
| coupon | text | 选填 | 优惠券金额(迁移后新增,存为text) |
| bookkeeper | text | 选填 | 记账者(迁移后新增) |
| related_tx | text | 选填 | 关联单笔ID(迁移后新增) |
(bill_id, tx_id) — 同一账单内 tx_id 唯一
Schema 关键问题:
1.
amount 存的是整数(单位:分),前端展示需 /100
2. category_id 外键在 CSV 导入后始终为 NULL(导入代码写的是 category 字符串字段),依赖展示时请 JOIN categories 表
3. CSV 导入新增的字段(category/sub_category/reimbursed/coupon/bookkeeper/related_tx)没有对应的外键约束,数据冗余存储bill_csv_uploads 表
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | text (PK) | 必填 | 10位随机ID |
| bill_id | text (FK→bills) | 必填 | 所属账单 |
| file_name | text | 必填 | 上传文件名 |
| checksum | text | 选填 | 未使用 |
| rows_imported | integer | 选填 | 未使用(返回用 count) |
| status | text | 选填 | 未使用(默认 pending) |
| uploaded_by | text (FK→users) | 选填 | 上传者 |
| created_at | datetime | 必填 | 创建时间 |
| updated_at | datetime | 必填 | 更新时间(作为实际上传时间) |
接口列表
GET/api/transactions/:billId
1. 获取账单的所有交易记录
触发时机: 切换到某个账单时,由fetchBillDetails 触发
selectBill(billId) → fetchBillDetails(billId) → Promise.all([
GET /bills/:id, ← 返回 bill + members + categories(含在本接口)
GET /transactions/:id ← 本接口
])
注意:members 和 categories 字段来自 /bills/:id 的响应,不是单独请求 /bills/:id/categories。前端 fetchBillDetails 只并行请求2个接口。
后端处理:
1. 校验用户是账单成员
2. 查询 transactions.bill_id = :billId,按 tx_time 升序(最老在前)
3. 查询最近一次 CSV 上传元信息(JOIN bill_csv_uploads + users)
响应:
{
"transactions": [
{
"id": "string",
"bill_id": "string",
"category_id": "string | null",
"uploaded_by": "string | null",
"tx_id": "string | null",
"tx_time": "datetime(原始格式,不一定是ISO8601)",
"tx_year": "integer | null",
"tx_month": "integer | null",
"amount": "integer(单位:分)",
"fee": "integer",
"currency": "CNY",
"tx_type": "string | null(支出/收入/退款等)",
"account1": "string | null",
"account2": "string | null",
"account1_id": "string | null",
"account2_id": "string | null",
"target_amount": "integer | null",
"note": "string | null",
"bill_tag": "string | null",
"tag": "string | null",
"bill_image": "string | null",
"category": "string | null(分类名称,CSV导入时填充)",
"sub_category": "string | null",
"reimbursed": "string | null",
"coupon": "string | null",
"bookkeeper": "string | null",
"related_tx": "string | null",
"created_at": "datetime",
"updated_at": "datetime"
}
],
"uploadMeta": {
"file_name": "string",
"uploaded_at": "datetime",
"uploaded_by_nickname": "string | null",
"uploaded_by_phone": "string | null"
} | null
}
前端展示注意:数据库存分,前端展示元需
/100
const displayAmount = amountInCents => (amountInCents / 100).toFixed(2)
category_id 为 NULL:见 P04
POST/api/transactions/:billId/upload
2. 上传 CSV(覆盖式导入)
触发时机: 在 账单页面 选择 CSV 文件后点击「上传」按钮BillPage → 选择 CSV 文件 → 点击"上传" → POST /transactions/:billId/upload
Content-Type: multipart/form-data
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | File | 必填 | CSV 文件,最大 10MB |
| CSV 列名 | 映射字段 | 说明 |
|---|---|---|
| 时间 | tx_time | 交易时间 |
| 分类 | category | 分类名称(注意:不是 category_id) |
| 类型 | tx_type | 支出/收入等 |
| 金额 | amount | 数字,会 parseFloat |
| CSV 列名 | 映射字段 | 类型/说明 |
|---|---|---|
| 二级分类 | sub_category | text |
| ID | tx_id | text |
| 币种 | currency | text,默认 CNY |
| 账户1 | account1 | text |
| 账户2 | account2 | text |
| 备注 | note | text |
| 已报销 | reimbursed | text(未使用) |
| 手续费 | fee | float |
| 优惠券 | coupon | 存为 text,但 parseFloat,默认 0 |
| 记账者 | bookkeeper | text(未使用) |
| 账单标记 | bill_tag | text |
| 标签 | tag | text |
| 账单图片 | bill_image | text |
| 关联单笔 | related_tx | text(未使用) |
editor 或 owner(viewer 不可上传)
3. PapaParse 解析 CSV(header=true,skipEmptyLines=true)
4. 格式校验:必须有 时间/分类/类型/金额 四列
5. 事务:
- DELETE 所有旧交易(覆盖式)
- 分批 INSERT(每批 40 行)
- UPSERT bill_csv_uploads(保留最新一条)
- 更新 bills.updated_at
关键风险 — 覆盖式导入:见 P01
上传新 CSV → 旧交易记录全部删除!
同一账单内上传两次 CSV 会导致第一次数据丢失
amount 解析:CSV 中金额为空时存
null,不是 0
amount: parseFloat(row['金额']) || null
fee 解析:如果 CSV 中该列为空,存 0
fee: parseFloat(row['手续费']) || 0
coupon 字段类型不匹配:见 P18
coupon: parseFloat(row['优惠券']) || 0
// 但 coupon 列类型是 TEXT,会先 parseFloat 转数字再存
// 数据库 schema coupon 是 TEXT 列,可能存了数字字符串
tx_year / tx_month 未生成:见 P05
响应:
{ "success": true, "count": 15 }