Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

All notable changes are recorded here. Versions follow [SemVer](https://semver.org/).

## v0.7.25 (2026-09-15)

自 v0.7.24 起累计 18 个 PR(#242–#259)。⚠️ **数据库 schema 14 → 15**:新增两条覆盖索引,迁移在启动时执行。本版两条主线:dev 上 NFS 的**查询/写入性能**,以及前端一批「**显示口径必须等于筛选口径、一个事实只有一个真源**」的缺陷。**无 config 变更**(`config.toml` 无需同步改动)。

- **NFS 上不再映射数据库、不再每请求真写(#259 a28ad3e)** — dev 上 `/api/transactions` 1.7–2.3s、`/api/ops/runtime` 2.1s,根因是**两个独立的 NFS 机制**叠加。① `PRAGMA mmap_size=64MB` 在这台 NFS 上是**慢路径**(映射页进不了客户端缓存 ⇒ 每次查询都真读文件):真库同一条覆盖索引 `COUNT(*)`,mmap=64MB **每次 1.7–3.1s、进程实读 250 MiB / 5 次**,mmap=0 **~10.5ms、实读 80 KiB**(同副本放本地盘两者都 7ms ⇒ 不是存储慢);尺寸扫描 0→13ms / 16MB→334–407ms / 64MB→1.7–2.2s / 128MB→2.2–2.8s ⇒ **任何非零值都更差**,故取 0。② **每一次真写都会打掉 NFS 客户端对该文件的缓存** ⇒ 只改 ① 只能 1.9s→~1.1s,到不了目标;而**每个已认证请求**都经过的 `dao::touch_api_key` 会写 `last_used`,加 60 秒时间守卫后(命中 0 行 ⇒ SQLite 不落页、不打掉缓存;实测提交 7ms vs 真写 34ms)把「每请求一次真写」降为「每分钟至多一次」。同 PR 更正了 `db.rs` 里被实测证伪的 v12 mmap 注释(原注释称「整库常驻进程内存、远端只首读一次」)。prod(本地盘、2.26MB)实测中性:200 次 COUNT 11ms(64MB)vs 9ms(0)。
- **应急索引进迁移 + 条件 JOIN 门禁(#242 5d4e6f4)** — v0.7.24 的按需 JOIN 与两条应急索引此前只存在于 dev/prod 的**手工操作**里,新部署仍会踩 22s 慢查询。新增 **v15 迁移**建两条覆盖索引(`transactions(user_id,time,type,pts,tokens,…)` 与 `transactions(key_id,type,pts)`,后者此前在该查询上零索引),并补**计划级验收**:`EXPLAIN QUERY PLAN` 必须报 `COVERING INDEX`(名字断言 ≠ 覆盖性证明),以及第二条 `perf_gate` 不变量(`tx_joins()` 的调用点只在包装器内,且包装器必须保留无 JOIN 分支)。
- **共享页收益改为批量聚合(#243 199566c8)** — `sharing_row` 曾对每行跑一次 `SELECT SUM(pts) … WHERE key_id=?` ⇒ 共享页 N 行 = N 次额外语句;改为一条 13 列 `SELECT` + `LEFT JOIN` 一次性聚合(同文件 `list`/`patch` 两份逐字重复的 SELECT 也随之合并为一处)。实测(20 万行)8 次子查询 1.54ms → 一次批聚合 0.02ms;门禁 `perf_gate::the_sharing_row_builder_runs_no_sql`(行构造器体内零 SQL)。
- **交易页缓存单一写者(#250 a81839b)** — `Live.transactions` 曾有两个写者:`loadTransactions`(`page_size=10`+筛选,并记录 `txTable.loaded*` 作为缓存有效性证据)与 `loadDashboard`(只为「交易笔数」卡拉 `page_size=1`);后者的载荷借守卫通过 ⇒ 再入交易页时**表只 1 行、汇总卡挂「当前筛选」却显示全时段数字、趋势卡谎报「加载失败」**。改为仪表盘用**自己的槽** `Live.tradeCount`;门禁钉 `writes(Live.transactions) == {loadTransactions} == writers(txTable.loaded*)`。
- **会话边界清空缓存 + 钱包视图补 loader(#251 0e4acc2)** — `Live` 缓存活得比填它的身份更久 ⇒ 换账号后侧栏「永久点数」显示**上一个人的钱**且永不自愈。`resetSessionCaches()` 改为**槽名派生自 `Live` 字面量**(不维护名册),并在登出/401 与 boot/登录两个边界各调一次;同时给钱包视图补上此前缺失的 loader。门禁:身份边界必须清空每个槽、`renderView` 每个分支必须既渲染又装载。
- **钱包页月度净变动改用共享写者(#253 f83df40)** — `#month-changes`(钱包)与 `#dash-month-changes`(仪表盘)由同一 `renderMonthChanges()` 画、都读 `Live.dashboard`,而该槽唯一写者是**仪表盘的** `loadDashboard` ⇒ **会话在钱包视图上建立**时(含在钱包页登出再登入)净变化恒印 `0` + 「本月暂无变动」,永不自愈(带 token 刷新看不到,因为 boot 的无条件 `renderView("dashboard")` 顺手装好了槽)。新增 `refreshDashboard()` 作该槽唯一写者,两个 loader 各调一次;门禁升级为**传递闭包**版(渲染闭包读的槽,其 loader 闭包必须写它)。
- **boot 只装载当前目的地视图(#254 6be548a)** — `DOMContentLoaded` 无条件 `renderView("dashboard")` 且跑在 `restoreSession()` **之前** ⇒ 带 token 时仪表盘那套查询在会话还不存在时就发出(`log[0]` 是 `/api/wallet`),随后被 `resetSessionCaches()` 作废、再装一遍;落在 `#/transactions` 也照拉仪表盘那套 5 次;过期 token 时同一条「Session expired」toast 发 6 次(用户看到 3 条)。删掉那句渲染,视图只由 `switchView` 按**当前目的地**装载;门禁 `the_boot_handler_touches_no_view`。
- **模型行身份 = 模型而非下标(#255 e64ae6c)** — `modelsToView()` 曾用**数组下标** `id: i` 当身份,还被持久化进 `localStorage` ⇒ 身份跨渲染/跨会话/跨数组(游客表 7 行、id 从 **1** 起;后端按 `provider, model` 排)⇒「最近使用」芯片指向**另一个模型**、或整条消失、或目录一变改写别的模型,点开的对话也一样。改为 `modelKey(m)=provider/model` 作唯一身份(三处 `data-*` 载体与点击侧全改用它,旧下标一次性丢弃);门禁 `the_model_row_identity_is_the_model_not_its_position`。
- **市场数据源由会话决定(#256 afc9c6f)** — 市场面写成 `Live.models ? 活目录 : 游客表`(按「数据到没到」而非「会话在不在」分支)⇒ 登录态 `/api/models` 失败/超时时:厂商下拉列出 **10** 个厂商(5 个本部署没有)、「最近使用」芯片画游客市场模型(点开只提示加载失败)、徽标显示 **13**(活目录 6、游客市场 7 ⇒ 13 是第三个数)。改为 `marketRows()`/`marketProviders()` 一处真源,判别式为 `loggedIn()`;门禁 `market_tables_follow_the_session_not_the_data`(零豁免清单)。
- **侧栏只 advertise 能按响的键位(#257 9982063)** — 每个导航项印数字角标 + 「快捷键 N」提示,而键盘上只有**一个**数字键处理器;两半是同一契约、必须取自同一数组 `NAV_ORDER`。`renderNav()` 的**游客**分支手搓了一个同形字面量(不属于登记表)⇒ `indexOf` 恒 **-1** ⇒ 角标印 **`0`**(死键)、提示写「Shortcut 0 · Marketplace」,而真正能打开市场的键是 **`2`**(游客永远看不到)。改为游客组从登记表里筛;门禁 `the_sidebar_advertises_only_digits_that_work`(四规则各有独立的牙)。
- **管理员「总余额」卡折进赠送额(#258 4af7cb7)** — 该卡**副标题**写「余额 + 赠送」、正下方「可用」列也是 `balance + gift_balance`,而取值只加永久余额 ⇒ **少掉全部赠送额**。赠送是**可花会过期**的真钱(`gift.rs` 真划走),产品把可用余额定义为 `available = balance + gift_balance`(用户自己看到的「余额」取的就是它)⇒ 方向由产品钉死(「把副标题改窄」不是修法);修法一行=合计折进 `gift_balance`;门禁 `the_admin_total_balance_card_sums_what_its_caption_names`。
- **后端错误全量登记进词表(#249 5183e41)** — `api.js` 把后端 `error` 整串交给 `ERR_MAP`,未登记即**原样返回** ⇒ en 界面显示中文而 CI 全绿(45 条里 20 条泄漏,`删除部门` 409 由可达控件触发)。20 条逐字派生登记 + 名册改为**派生**(`BACKEND_ERROR_SOURCES` 必须覆盖每个产出 `"error"` 字面量的文件)。同 PR 修掉一个**静默三轮**的失真:剥注释函数用 `byte as char` 搬 UTF-8 ⇒ 中文词表被撕成 Latin-1 乱码、**永远比不中**(旧消费者只问 `is_ascii()`,乱码同样非 ASCII),改为 `Vec<u8>` + `String::from_utf8` 并加保真断言。
- **后端不再自造中文展示标签(#252 91a2da1)** — 两处把**自己编的中文标签**放进响应**数据**字段、前端原样渲染(`admin.rs` 的 `'(未分配)'` 部门桶名 → `#usage-dept`;`gateway.rs` 的 plan 兜底名 `'API(按量)'` → 上架表单/`#sf-plan`,config 的 `[[plans]]` 全不写 `name` ⇒ 恒触发),而 `ERR_MAP` 只扫 `"error"` 字面量 ⇒ **这一类零门禁**。同屏/同 payload 早有本地化机制(`T("common.unassigned")`、`showPlanHint`)⇒ 是漏用而非缺功能;改为后端只发**语言中性**标记、前端本地化,并加门禁(中文**数据**字面量集合须恰等于一份**日落清单**)。
- **凭据 401 不再当作会话过期(#245 301ca4d)** — `api.js` 曾把一切 401 当「会话过期」(清 token + 登出 + toast)⇒ 登录失败时会发一条**误报**的「会话已过期」并把用户登出。改为 `opts.on401` 逃生口(401 语义由调用方声明,默认仍是会话失效 ⇒ 业务端点零改动),登录调用点声明为凭据错误;门禁 `credential_401_is_not_a_session_expired`。
- **boot 非 401 失败不再演成「被登出」(#244 3199731)** — boot 的 `catch` 对任何**非 401** 失败都停在登录页(token 仍在、hash 仍指向上次视图)⇒ 与「被踢出」视觉不可区分。改为三档:401 回登录页(不重试);网络错误/5xx 重试一次;其余 4xx 不重试但**仍进 app**、由各视图的降级态(加载失败 + 重试)承接 —— 「加载失败」交各视图,「未登录」只由 401 承担。
- **时间戳不再被手工切片(#246 6e022e1)** — 三个消费者自己切线上时间串:管理员加额行 `.slice(5,16)` 把 ISO 的 `T` 泄进 UI(屏幕上是 `09-13T16:30`、且是 UTC)、设置页 Key「创建时间」`.slice(0,10)` 取 **UTC 日**(东八区 08:00 前显示「昨天」)、交易行 `.replace("T"," ").slice(0,16)` 在渲染器**之前**把秒抹掉 ⇒ 屏幕上的秒永远是伪造的 `00`。改为整串交给现成 helper(`fmtPrecise`/`timeCell`/`timeAgo`);门禁 `wire_timestamps_reach_the_renderer_unsliced`。
- **行内卡片 Enter 从每个字段提交(#247 ecc654c)** — Enter 提交是逐字段手工登记的,模型表单 10 个可输入控件里 **8 个**按 Enter 没反应(另三张卡片是 2/2、1/1、1/1,`#share-form` 本身是真 `<form>`+`type=submit`)。改为 `wireEnterSubmit(卡片, 确认按钮)` 委托到容器(当前与以后的文本控件自动生效;焦点在「取消」上按 Enter 仍只取消;处理体只 `btn.click()` ⇒ 忙碌态/校验/请求仍只有一处);门禁 `enter_submit_is_delegated_to_the_card`(不变量**从 markup 派生**而非人写名册)。
- **市场可用性标签取自该行自身(#248 818ed88)** — `availPill()` 只认 `keys` 计数,而游客兜底表按「虚构数据已移除」**不携带**该字段 ⇒ 游客 **7/7** 行的绿点/按钮说「可用」而这一格写「无 key」(自相矛盾)。改为有计数时按计数分档(≥2 / ==1 / 0)、无计数时落到该行的 `avail` 布尔;门禁同时钉**消费侧**(须出现 `m.avail`)与**数据侧**(`MARKET` 不得携带手写 `keys`,否则等于把虚构运营数据摆给游客)。

## v0.7.24 (2026-09-14)

- **交易页慢查询导致「刷新跳回登录页」(#240 95d69ba)** — 宿主实测:dev 上登录后进 `#/sharing` 刷新会**停在登录页**(URL 仍 `#/sharing`、token 仍在 sessionStorage)。根因是 `/api/transactions` 的 summary / COUNT / trend 三条语句**无条件**拼上 keys/users/api_keys 三个 LEFT JOIN,而这几个 JOIN 一出现 SQLite 就**弃用覆盖索引**、退回逐行回表:同一条聚合语句、同一个库(18.6 万行,NAS/NFS),**不带 JOIN 0.21s、带 JOIN 22.6s(约 100×)**。实际只有 `user_name` / `key_name` 两个筛选会引用 JOIN 表(`model` / `status` / pts 区间全在 `transactions` 自身),故改为**按需 JOIN**(新增单一判定谓词 `needs_joins()`,避免 JOIN 与 `tx_where` 的引用再次分叉);列表查询保留 JOIN(要渲染 user_name / key_label / key_name,且有 `LIMIT` 兜底)。故障链:慢查询长时间占住共享 DB 锁 → `/api/me`、`/api/models` 被排队(实测单次 `/api/me` 被卡 35s、并发下出现网关 504)→ 前端 boot 串行等 4 个请求(其中 `/api/models` 非必需)才 `enterApp()` ⇒ 任一卡住则登录页无限期停留(实测 3 次刷新 2 次卡住)。
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aitokenpool"
version = "0.7.24"
version = "0.7.25"
edition = "2021"
description = "AI Token 共享池 — 企业 key 池 + 公共共享市场"
license = "MIT"
Expand Down
Loading