feat(server): 查看面数据面(阶段 5)

- logs/query:明细 1:1(除 dedupe_key)+ 当前 token_name(join)+ amount_rmb;分页(limit/offset,默认 50 / 上限 200);排序 created_at DESC;type 过滤
- summary:卡片汇总(含 cache_tokens 的 jsonb 聚合)+ 折算累加 + unpriced_count
- meta(约定 22/23):data_until 取参与 key 中非 null 的最慢值、per-key 状态、未收录 key_hint 即时生成;全未收录仍 200 空结果
- 折算链(pricing.ts):按归属购买取尺(quota × paid_amount ÷ quota_granted = 按 quota 占比分摊实付);wallet 路径按未折算(null);假设 #6 的多条匹配规则
- 修复 jsonb 双重编码(实测定位):drizzle + bun-sql 直接传 JS 对象会落成 jsonb string,SQL 侧 ->> / ? 全失效 → 统一 toJsonb()(sql`${obj}::jsonb`),影响 other 与 model_limits
- seed 调整为「为行数最多的订阅登记购买」,保留未折算展示样本
- 实测:jsonb_typeof=object、SQL 聚合恢复、金额与 unpriced_count 计数正确、data_until 取最慢
This commit is contained in:
2026-09-15 00:51:39 +08:00
parent e1c6d2153e
commit 8252b25396
7 changed files with 546 additions and 40 deletions
+44 -13
View File
@@ -8,7 +8,8 @@
* 查询面可用该常量 key 命中已登记 key)
* 2. `usage_logs`:`api.log.token.json` 的 1000 条样例日志(`ON CONFLICT DO NOTHING`)
* 3. `qpu_history` 基线:`api.status.json` 的 `quota_per_unit`(已有数据则跳过)
* 4. 一条示例 `purchases`(订阅路径;**金额为占位值**,用于验证折算链)
* 4. 一条示例 `purchases`:为样例中**日志行数最多**的订阅登记(**金额为占位值**);
* 其余订阅保持未登记——保留「未折算(`amount_rmb = null`)」的展示样本
*
* 注:快照文件未入库(`.gitignore` 的 `/api*.json`),本脚本只在本地开发环境使用。
*/
@@ -20,8 +21,9 @@ import { DEFAULT_QUOTA_PER_UNIT } from '@naku/shared'
import { getDb } from '../src/db/client'
import { apiKeys, purchases, qpuHistory, usageLogs } from '../src/db/schema'
import { getKeySecrets } from '../src/lib/keys'
import { readSubscriptionId } from '../src/lib/pricing'
import { fromUnixSeconds } from '../src/lib/time'
import { parseOther, toLogRow } from '../src/sync/log-row'
import { parseOther, toJsonb, toLogRow } from '../src/sync/log-row'
import type { UpstreamLogRecord } from '../src/upstream/types'
/** 假 key 常量:查看面 / 管理面用它做本地验证(可用 env SEED_KEY 覆盖) */
@@ -73,7 +75,7 @@ if (!keyRow) {
totalUsed,
totalAvailable: totalGranted - totalUsed,
unlimitedQuota: false,
modelLimits: {},
modelLimits: toJsonb({}),
modelLimitsEnabled: false,
expiresAt: null,
})
@@ -103,12 +105,38 @@ if (!qpuRow) {
}
// ---------- 4. 示例 purchases(金额为占位值) ----------
// 样例快照里可能有多个订阅(实测 89 / 90 两组):只为行数最多的那笔登记购买记录,
// 其余保持未登记——既保留「未折算」样本,也贴近真实的「多订阅并存」形状
interface SubInfo {
count: number
earliest: number
total: number
plan: string | null
}
const bySubscription = new Map<number, SubInfo>()
for (const log of logs) {
const other = parseOther(log.other)
const sid = readSubscriptionId(other)
if (sid === null) continue
const cur =
bySubscription.get(sid) ??
({ count: 0, earliest: Number.POSITIVE_INFINITY, total: 0, plan: null } satisfies SubInfo)
cur.count += 1
cur.earliest = Math.min(cur.earliest, log.created_at)
cur.total = Math.max(cur.total, asNumber(other.subscription_total, 0))
cur.plan = typeof other.subscription_plan_title === 'string' ? other.subscription_plan_title : cur.plan
bySubscription.set(sid, cur)
}
const ranked = [...bySubscription.entries()].sort((a, b) => b[1].count - a[1].count)
const [purchaseRow] = await db.select().from(purchases).limit(1)
if (!purchaseRow) {
const first = logs[0]!
const other = parseOther(first.other)
const earliestSeconds = Math.min(...logs.map((l) => l.created_at))
const earliest = fromUnixSeconds(earliestSeconds)
if (purchaseRow) {
console.log(`[seed] purchases:已有数据(id=${purchaseRow.id}),跳过`)
} else if (ranked.length === 0) {
console.log('[seed] purchases:样例日志中无 subscription_id,跳过')
} else {
const [mainSid, info] = ranked[0]!
const earliest = fromUnixSeconds(info.earliest)
await db.insert(purchases).values({
kind: 'subscription',
paidAt: earliest,
@@ -116,12 +144,15 @@ if (!purchaseRow) {
note: 'seed 示例(金额为占位值,非真实)',
periodStart: earliest,
periodEnd: null,
subscriptionId: asNumber(other.subscription_id, 90),
quotaGranted: asNumber(other.subscription_total, 800_000_000),
subscriptionId: mainSid,
quotaGranted: info.total > 0 ? info.total : 800_000_000,
})
console.log('[seed] purchases:已插入示例一条(折算链验证用)')
} else {
console.log(`[seed] purchases:已有数据(id=${purchaseRow.id}),跳过`)
console.log(
`[seed] purchases:为订阅 #${mainSid}(${info.plan ?? '未知计划'},${info.count} 行日志)插入示例一条`,
)
for (const [sid, extra] of ranked.slice(1)) {
console.log(`[seed] 订阅 #${sid}(${extra.count} 行)保持未登记——用于演示「未折算」展示`)
}
}
console.log(`[seed] 完成。查询面可用假 key「${SEED_KEY}」命中已登记 key。`)