Transactions 模块 API 文档

基础信息

  • Base URL: /api/transactions
  • 认证方式: JWT Bearer Token

数据库表

transactions 表

字段类型必填说明
idtext (PK)必填10位随机ID
bill_idtext (FK→bills)必填所属账单
category_idtext (FK→categories)选填分类ID(通常为 NULL
uploaded_bytext (FK→users)选填上传者用户ID
tx_idtext选填原始CSV中的ID
tx_timedatetime必填交易时间(原始格式,不转换
tx_yearinteger选填年份(生成字段)
tx_monthinteger选填月份(生成字段)
amountinteger必填金额(单位:分,整数存储)
feeinteger必填手续费(默认0)
currencytext必填币种,默认 CNY
tx_typetext选填交易类型(非固定枚举:支出/收入/退款等)
account1text选填账户1
account2text选填账户2
account1_idtext (FK→accounts)选填账户1 ID
account2_idtext (FK→accounts)选填账户2 ID
target_amountinteger选填目标金额(转账用)
notetext选填备注
bill_tagtext选填账单标记
tagtext选填标签
bill_imagetext选填账单图片路径
created_atdatetime必填创建时间
updated_atdatetime必填更新时间
categorytext选填分类名称(迁移后新增,与 category_id 并存)
sub_categorytext选填二级分类名称(迁移后新增)
reimbursedtext选填已报销标记(迁移后新增)
coupontext选填优惠券金额(迁移后新增,存为text)
bookkeepertext选填记账者(迁移后新增)
related_txtext选填关联单笔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 表

字段类型必填说明
idtext (PK)必填10位随机ID
bill_idtext (FK→bills)必填所属账单
file_nametext必填上传文件名
checksumtext选填未使用
rows_importedinteger选填未使用(返回用 count
statustext选填未使用(默认 pending)
uploaded_bytext (FK→users)选填上传者
created_atdatetime必填创建时间
updated_atdatetime必填更新时间(作为实际上传时间)

接口列表


GET/api/transactions/:billId

1. 获取账单的所有交易记录

触发时机: 切换到某个账单时,由 fetchBillDetails 触发
selectBill(billId) → fetchBillDetails(billId) → Promise.all([
  GET /bills/:id,           ← 返回 bill + members + categories(含在本接口)
  GET /transactions/:id     ← 本接口
])
注意memberscategories 字段来自 /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 请求参数:
参数类型必填说明
fileFile必填CSV 文件,最大 10MB
CSV 必填列(大小写敏感):
CSV 列名映射字段说明
时间tx_time交易时间
分类category分类名称(注意:不是 category_id)
类型tx_type支出/收入等
金额amount数字,会 parseFloat
CSV 选填列:
CSV 列名映射字段类型/说明
二级分类sub_categorytext
IDtx_idtext
币种currencytext,默认 CNY
账户1account1text
账户2account2text
备注notetext
已报销reimbursedtext(未使用)
手续费feefloat
优惠券coupon存为 text,但 parseFloat,默认 0
记账者bookkeepertext(未使用)
账单标记bill_tagtext
标签tagtext
账单图片bill_imagetext
关联单笔related_txtext(未使用)
后端处理: 1. 校验文件存在(最大 10MB) 2. 校验用户是 editorownerviewer 不可上传) 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 列,可能存了数字字符串
未使用的 CSV 列已报销记账者关联单笔 被读取但从未被 INSERT 到数据库(见 P19
tx_year / tx_month 未生成:见 P05
响应:
{ "success": true, "count": 15 }