Files

74 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 查询优化、关联记账与多语言验收(2026-10-02)
实际环境:Node 24.21.0、MySQL 8.0.45、Prisma 6.19.0、NestJS 11.2.6、React 19、Vite 7.3.6、TypeScript 5.9.3,以锁文件为准。保留 MySQL、Decimal 和已有用户隔离规则;归档项目仍参与总额。Revision.amount 是变更后的余额。
## 查询与计算
- `/positions?kind=account|asset|debt` 只带当前余额、必要资料、可见关联及本位币折算;`/positions/:id` 返回必要资料与当前原币余额。两者不带历史。
- `/overview` 只带当前总资产、负债、净资产、必要明细及汇率状态。
- `/history`、`/positions/:id/history`、`/transfers` 支持 `limit`、`cursor`、`from`、`to`;历史另支持 `positionId`。默认 50,最多 100,最近流水使用同一接口 limit=20。返回 `{items,nextCursor,revealed}`。历史按 effectiveDate DESC、唯一 sequence DESC;往来按 effectiveDate DESC、唯一 id DESC。
- 历史先从每个可见项目的复合索引取至多 limit+1 条,再合并页;只对入选记录定位真实前序余额,分页首条的 before 不假定为零。项目非常多时,查询数仍随项目数增长。
- 最新余额使用按项目分支的参数化 UNION ALL,每个分支按 `[positionId,effectiveDate,sequence]` 反向索引扫描 LIMIT 1;最多 100 个分支一批。没有反规范化当前余额字段,因此无需余额回填或缓存重建,历史补录、更正、导入恢复后直接从历史重算。
- 已捕获 Prisma 实际 SQL:单父项目 nested take:1 有数据库 LIMIT;多父项目 nested take:1 产生无 LIMIT 的 revisions 查询,由客户端裁剪。不能仅靠 take 判断查询代价。本次使用明确的数据库分支限制。
- 更正只查目标及相关配对约束。不能改配对记录或在其之前插入/更正;同业务时间使用 sequence 比较,配对后同分钟的普通余额记录可以更正。保留业务约束,避免破坏双方账。
- `/trend` 默认最近 90 天。day 最多 366 天、week 最多 1096 天、month 最多 3653 天。使用区间前最后余额和汇率作种子,数据库取范围内每日最后余额;应用排序一次并顺序推进。响应不带每日期完整项目明细。
- 周以周一至周日、月以自然月取期末余额;变化归因累加该段每日余额变化与汇率变化,首尾不完整周期按实际范围。缺失汇率标记不完整,沿用时保留实际 rateDate。仅查询相关币种、本位币的区间汇率和必要前序汇率;当前总额只查询最新可用汇率。
## 实测证据
随机独立测试用户,原服务 3100 与优化后隔离服务 33101 使用同一数据库;每项 3 次取中位数,全部为实际 HTTP 测量,响应大小为未压缩 JSON 字节。测试记录集中在 90 天内,金额虚构,测试结束清理该测试用户。完整 SQL、EXPLAIN ANALYZE、处理函数计时及 HTTP 结果见 [原始证据](performance-final.json)。原始证据中的 HTTP queries=0 表示远端 HTTP 客户端未采集 SQL 事件,不代表数据库零查询。
| 数据 | 账户列表:前 → 后 | 列表响应:前 → 后 | 当前总览:前 → 后 | 历史 50 条 | 日趋势 90 天 |
| -------------- | -----------------: | -------------------: | ------------------: | ---------: | -----------: |
| 单账户 1 万 | 288.13 → 11.59 ms | 6,097,025 → 368 B | 992.35 → 7.36 ms | 19.20 ms | 54.64 ms |
| 单账户 10 万 | 2,826.90 → 5.56 ms | 61,267,028 → 370 B | 9,885.51 → 5.39 ms | 38.50 ms | 585.14 ms |
| 10 账户各 1 万 | 2,869.42 → 6.19 ms | 60,970,241 → 3,671 B | 15,636.96 → 5.48 ms | 34.32 ms | 568.12 ms |
单账户 10 万条 EXPLAIN ANALYZE:最新余额索引节点实际 rows=1 loops=1;历史候选索引节点 rows=51 loops=1;每条前序余额子查询实际 rows=1。多账户每分支分别限量。资金往来最终采用 FORCE INDEX + STRAIGHT_JOIN,以用户/时间/id 为驱动;60 条同时间测试记录,首批索引实际 rows=51,游标下一页 rows=10,无全量排序,见 [往来分页计划](transfers-plan.json)。隐藏或指定账户过滤需要跳过不匹配行,实际扫描可多于返回条数。旧列表实际返回全部 1 万/10 万历史,旧总览也读取全部历史;没有采集旧查询精确存储引擎 rows_examined,因此不将返回行数冒充扫描统计。新增全局日期索引曾导致优化器选择全局历史路径,已通过第二迁移移除,最终使用项目复合索引。EXPLAIN 中 cost/estimated rows 与 actual rows 含义不同。
前端请求日志及浏览器实测:总览为 overview、trend、history(limit20) 共 3 个财务 GET;账户列表 1 个;详情 2 个;加载下一页只 1 个 history;变化记录页 2 个;打开资金往来表单才取账户/债务选项。旧统一加载为 positions、overview、transfers、settings 共 4 个 GET,各页面都请求全部历史。总览现在虽为 3 个 GET,但每个职责有界;账户列表从 4 降到 1。语言切换为 0 个财务 GET。认证 me 和交互活动另计;启动 me 通过共享 Promise 避免开发 StrictMode 重复。日期范围改变只请求 trend;过期响应由会话、加载序号和当前选择校验拦截。
这些是本机对当前数据库的结果,不是生产并发/p95 保证。区间内 10 万条趋势仍需数据库窗口排序,约 0.6 秒;未引入每日快照、缓存或新基础设施。游标页在并发历史更正后应重新加载,未提供跨请求冻结快照。
## 借入、借出及手续费
债务 side=liability 表示借入/应付,side=asset 表示借出/应收。资金往来复用 `/transfers`,operation 为 transfer、borrow、lend、collect、repay。来源为启用中的资产账户,目标为另一资产账户或对应方向的债务。同币种账户本金与债务本金相等,跨币种填写实际金额。
借入到账:账户 +(amount-fee),应付 +received;借出:账户 -(amount+fee),应收 +received;收回应收:账户 +(amount-fee),应收 -received;偿还应付:账户 -(amount+fee),应付 -received。手续费可为负数表示优惠,绝对优惠不超过本金;收款正手续费不超过本金;禁止负余额或超额还款。金额仍为 Decimal。
双方行按 ID 顺序加锁,最新余额读取、幂等校验、双边历史、往来记录及自动关联同处 Serializable 事务。死锁/写冲突有限重试(最多 3 次重试)。账户详情预选当前账户;债务详情按关联预选账户。借入/借出操作、流水、详情互相可导航。新建债务可先录已有余额;新发生借贷建议建 0 余额后使用联动操作,避免重复计入。
ZIP v6 增加 operation;保留 v3/v4/v5 ZIP、v1/v2 JSON 读取。恢复校验双边前后余额、操作方向、费用和配对原因,不改变历史金额含义。
## 三种语言
顶部及登录页支持简中、English、繁中,语言保存在本浏览器 localStorage。静态词典覆盖页面、表单、图标交互和常见服务端错误;用户名称、备注、原币金额保持原文。繁中由开发时 OpenCC 生成静态 JSON,不在浏览器运行转换。修改英文词典后执行 `node apps/web/scripts/generate-locales.cjs`;安全清空确认短语仍精确要求“确定清空”。
## 迁移、部署与复验
本次已对配置数据库执行 migrate deploy,四个迁移:
1. `20261002000000_query_indexes`:Revision 项目/日期/顺序及配对约束索引、Transfer 用户/日期/id 索引。
2. `20261002000100_remove_global_revision_index`:移除实测不合适的全局日期索引。
3. `20261002000200_debt_movements`:Transfer.operation 默认 transfer,旧记录自动获得原转账语义。
4. `20261002000300_chinese_comments`:8 张业务表及 73 个字段增加中文注释,Prisma 模型同步文档注释。迁移保留 SHOW CREATE TABLE 原列定义,不改变字段类型、默认值和自增;数据库元数据验证见 [注释核验](database-comments.json)。Prisma 自身的迁移元数据表不作为业务表修改。
无余额列回填;未删除真实历史。其他环境需先备份,停止 API 释放 Windows Prisma DLL,再按下面步骤部署同步后端与前端:
```powershell
pnpm install --frozen-lockfile
pnpm db:generate
pnpm db:migrate
pnpm db:status
pnpm typecheck
pnpm build
pnpm test
# 启动优化后的 API 后
$env:TEST_API_URL='http://127.0.0.1:3100/api'
pnpm test:integration
```
最终验证:类型检查和生产构建通过;18 个单元测试、8 个真实 MySQL 集成测试通过。涵盖历史补录/更正、同时间顺序、前序余额/分页、归档、隐藏解锁/用户隔离、缺失汇率/本位币切换、负手续费、并发幂等转账与收款、借入借出/还款、ZIP 完整恢复。浏览器验证了 50+10 条分页、详情转账预选、债务关联导航、还款表单默认账户及中英繁切换/刷新持久化。
基准可重跑:在 apps/api 执行 `pnpm exec tsx scripts/performance.ts rerun`;TEST_API_URL 指向优化后服务,BASELINE_API_URL 可选指向独立旧版本服务,结果输出 docs/performance-rerun.json。脚本只生成/清理随机测试用户。连续反复认证集成测试可能触发现有每 IP 15 分钟 30 次认证限速;本次通过重启隔离测试进程复验,生产限制未放宽。