fix(sharing): count the points spent through a key, not its tokens, in keys.used - #217
Merged
Merged
Conversation
…n keys.used
`keys.quota` is point-denominated (the form that collects it is labelled
「声明额度(点数)」), but `keys.used` was accumulated as a **token** count
(`billing::settle` ran `UPDATE keys SET used = used + p.tokens`). The sharing
page divides the two to draw its progress bar and prints both under a 「点」
label, so it rendered a token count as points.
Measured through the real router (one `billing::settle` with
`tokens = 1_000_000` / `pts = 4.5` on a key whose `quota = 5000`):
before: GET /api/sharings -> used = 1000000, quota = 5000
=> 「已用 1000000 / 5000 点」, bar pinned at 100 %
(the ledger for the same call holds consume_pts = 4.5)
after: GET /api/sharings -> used = 4.5, quota = 5000
The project has one unit for 「已用 / 额度」 elsewhere (the departments table
uses `d.month_cost`), and `keys.used` has exactly one reader
(`sharing.rs::sharing_row`) and no enforcement path, so nothing depended on
the column being tokens. Tokens stay recorded per call in
`transactions.tokens` / `usage_records.tokens`.
Changes:
- `billing.rs`: accumulate `p.pts` (same unit as `keys.quota`) instead of
`p.tokens`; correct the module-doc claim; fix the existing split test's
expectation and add a test whose fixture makes tokens and points differ by
five orders of magnitude, asserting `used == SUM(consume pts)` and that
only consume rows count.
- `db.rs`: heal existing deployments once, from the ledger
(`used = SUM(transactions.pts WHERE type='consume')`), gated on
`schema_version < 13`; `SCHEMA_VERSION` 12 -> 13. Also read the version with
`MAX(version)`: the table has no unique constraint, so the previous
single-row read made every start-up append a row (the live DB has 22 rows)
and kept every `v < N` gate permanently true.
- `sharing.rs`: document the `used` field's unit and provenance.
- `docs/architecture.md`: drop the hardcoded schema version so the doc
cannot rot again (it referenced `SCHEMA_VERSION` but also re-typed the
number, which is exactly how it went stale).
No money moves: the ledger, balances and settlement are untouched. This
changes a displayed number's unit and one dead-end column.
Test suite: 217 -> 220 passed. fmt/clippy clean.
argszero
added a commit
that referenced
this pull request
Sep 14, 2026
Ships the 76 PRs merged since v0.7.22 (#161-#237), the largest release so far. Database schema moves 12 -> 14: - v13 (#217): `keys.used` changes unit from tokens to points, and the live values are healed from the ledger (`used = SUM(transactions.pts WHERE type='consume')`), gated on schema_version < 13. - v14 (#234): four indexes for the monthly aggregates — `transactions(user_id, time, type, pts)`, `transactions(time, type, pts)`, `usage_records(time)`, `usage_records(user_id, time)`. Deployments must apply the DeepSeek flash rename (#233) to their own config.toml: `seed_models` is a full sync, so a model absent from the config is deleted at startup; the retired names are gone from config.example.toml. - Cargo.toml / Cargo.lock: 0.7.22 -> 0.7.23. - ui/index.html: asset cache-bust 20260912-4 / 20260912-5 / 20260914-1 / 20260914-4 / 20260914-9 -> 20260914-10 (all five refs). - CHANGELOG.md: v0.7.23 entry, grouped by area with the PR and hash of each fix. Gates: `cargo test` 249 passed / 0 failed, `cargo fmt --check` clean, `node --check` on the four ui/js files OK.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
keysrow keeps a matched pairquota/used, and the sharing pagerenders them in one unit — the 「已用量」 stat card, the 「已用 / 额度」 cell
with its progress bar, and the dashboard mini-list all print them under a 「点」
label.
quotais points (the form that collects it is labelled「声明额度(点数)」), but
usedwas accumulated as a token count:billing::settleranUPDATE keys SET used = used + p.tokens.So the page printed a token count as points, and the ratio
used / quotadivided tokens by points — a progress bar that means nothing.Measured through the real router (one real
billing::settlewithtokens = 1_000_000,pts = 4.5, on a key whosequota = 5000):GET /api/sharingsused = 1000000,quota = 5000used = 4.5,quota = 5000consume_pts = 4.5,earn_pts = 4.05The real deployment agrees with the defect:
atp-data/aitokenpool.dbkey 1 hasquota = 5000andused = 319883396, so its sharing card reads「已用量 319883396 点」 under 「5000 点总额度」.
Why this is a defect rather than a chosen unit:
language packs say 点; the only voice on the other side is one line of module doc.
(
ui/js/app.js: the departments table's 「已用」 isd.month_cost), so theproject has one unit for 「已用 / 额度」 and the sharing side is the odd one out.
used / quotaisonly meaningful within one unit — so 「relabel the number」 is not available.
keys.usedhas exactly one reader (sharing.rs::sharing_row) and noenforcement path (
keys.quotagates nothing; the author deferred that to P1),while the tokens stay recorded per call in
transactions.tokens/usage_records.tokens— nothing depended on the column being tokens.Related Issue
None (no issue exists for this; none was fabricated).
Changes
src/billing.rs— accumulatep.pts(the same unit askeys.quota)instead of
p.tokens, so the value written is one every consumer alreadyassumes; correct the module-doc claim (
keys.used += tokens→ points);fix the existing split test's expectation (that fixture:
tokens = 150,pts = 2.0, sousedmust be2.0) and add a test whose fixture makestokens and points differ by five orders of magnitude.
src/db.rs— heal existing deployments once, from the ledger(
used = SUM(transactions.pts WHERE type='consume'), which is exactly theinvariant the fixed writer maintains, so re-running is a no-op), gated on
schema_version < 13;SCHEMA_VERSION12 → 13.src/db.rs— read the version withMAX(version). Theschema_versiontable has no unique constraint and
INSERT OR REPLACEis a plain INSERTwhen nothing conflicts, so the previous single-row read returned the
oldest row:
v < SCHEMA_VERSIONstayed true forever and every start-upappended a row (the live DB has accumulated 22:
7,8,8,8,9×13,10,10),which also kept every
v < Nmigration gate permanently open.src/routes/sharing.rs— document theusedfield's unit and provenance(comment only).
docs/architecture.md— drop the hardcoded schema version from the tworows that this bump invalidated. They already pointed at
SCHEMA_VERSIONbut also re-typed the number, which is precisely howthey went stale (they said
v12); the number is removed rather thanre-typed, so it cannot rot again.
ui/change and no i18n key change — the strings already say 点, whichis the point of the fix. No config change, so
config/config.example.tomlneeds no sync.
Not touched, on purpose:
keys.quotaenforcement (still deferred to P1 by theauthor), and the
usage_records.costvstransactions.ptsquestion (a separate,host-referred topic). No money moves — the ledger, balances and settlement are
untouched; this changes a displayed number's unit and one dead-end column.
Tests
cargo test— 220 passed (was 217; +2 net after fixture edits).cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean.billing::tests::keys_used_records_consumed_points_not_tokens:drives a real
settlewhosetokensandptsdiffer by five orders ofmagnitude and asserts
keys.used == SUM(consume pts)and that onlyconsume rows count (the same call's
earnrow and atopuprow must notcontribute).
db::tests::keys_used_is_healed_from_the_ledger_on_upgrade:seeds a legacy row (
used = 319883396with a matching consume ledgertotalling
12.5), a never-called key whose column is stale (42→ mustbecome
0), and asserts the heal runs once (a secondmigratemustnot re-run it, so appending a ledger row must leave
usedalone).db::tests::schema_version_gate_uses_the_highest_recorded_version:seeds a multi-row version history and asserts the gate appends nothing on
a second run.
(baseline
220 passed / 0 failed):- M1 writer
used += pts→tokens→ 2 red:keys_used_records_consumed_points_not_tokens,settle_splits_90_10_and_writes_ledger.- M2 heal gate
if v < 13→if false→ 1 red:keys_used_is_healed_from_the_ledger_on_upgrade.- M3 version read back to the oldest row → 2 red:
keys_used_is_healed_from_the_ledger_on_upgrade,schema_version_gate_uses_the_highest_recorded_version.Three mutations, three distinct red sets.
reverted byte-identically):
POST /api/sharings→ 200, one realsettle(
tokens = 1_000_000/pts = 4.5,quota = 5000) →GET /api/sharingsreturns
used = 4.5,quota = 5000, progress bar no longer saturated.Checklist
fix/).fix(sharing): ...).