Releases: modelbus/one-api-pro
Release list
v0.0.20
v0.0.20 — 管理后台 UI 全面优化 + 定价 Id 清零兜底
渠道 / 令牌管理界面对齐 web/default 老前端体验:模型可搜索多选、模型重定向、分组下拉、永不过期;模型下拉数据源从编译期硬编码 catalogue 切换到运行时可配置的
model_price表。同时修复「新建令牌触发 Page is not found 报错」与「定价表单残留 id 写入」两条链路隐患。
Channel & token admin UIs catch up with the reference web/default experience: searchable model multi-select, model redirect, group dropdown, "never expire" toggle. The model dropdown source is switched from the compile-time hardcoded catalogue to the runtime-configurable
model_pricetable. Also fixes the "Page is not found" ReferenceError on token create and the residual-id-leak path in pricing forms.
中文
✨ 新增功能
- 渠道编辑弹窗:模型字段改为支持搜索的多选选择器(
web/default-pro/src/views/channel/Channel.vue):- 把原本「逗号分隔字符串」输入框替换为 arco
a-select(multiple+allow-search+allow-clear),挂载时拉取/api/channel/models作为候选。 form.models由string改为[]string,提交时join(',')复用后端逗号存储格式;编辑时parseModelsField把后端返回字符串拆回数组。- 当前表单里已存在的模型名会自动合入候选,保留历史「野模型」可见性。
- 把原本「逗号分隔字符串」输入框替换为 arco
- 渠道编辑弹窗:新增「模型重定向」输入框(
Channel.vue):- 新增
model_mapping字段(对应后端channels.model_mapping),使用a-textarea接受 JSON 对象,提交前用JSON.parse+typeof === 'object'校验必须为普通对象。 - 后端字段早已存在(
model/channel.go:38),前端之前未暴露 — 现在与 web/default 老前端 EditChannel 表单对齐。
- 新增
- 渠道编辑弹窗:分组改为下拉选择器并移到名称下方(
Channel.vue):- 把逗号分隔
a-input改为 arcoa-select(multiple+allow-search+allow-clear),候选来自/api/group/(后端controller/group.go)。 - form-item 从「模型」下方移到「名称」下方,对齐 web/default 老前端顺序。
form.groups改为数组,提交时join(',')复用后端存储格式。
- 把逗号分隔
- 渠道编辑弹窗:模型下拉数据源切换到
model_price表(Channel.vue:fetchAvailableModels):- 下拉候选改读
GET /api/model_price/options(admin 权限即可访问),与计费数据源对齐。 - 运营无需改代码就能给下拉里加新模型,只要在「定价管理」里配好对应
model_name即可。 - 仍保留
form.models合入候选的兜底,编辑历史渠道时已选模型不会丢。
- 下拉候选改读
- 新增只读接口
GET /api/model_price/options(controller/model_price.go):- 仅返回
enabled=true的model_name字符串切片,按名称升序;空model_name自动跳过避免脏数据。 - AdminAuth 即可访问,与 RootAuth 保护的写接口隔离;旧
GET /api/model_price/加// Deprecated:注释保留给 root 查看完整字段。
- 仅返回
- 令牌编辑弹窗:过期时间新增「永不过期」checkbox(
Token.vue):- 过期时间输入框右侧增加「永不过期」checkbox,勾选后 date-picker 禁用。
- 新增
form.never_expire字段;编辑时若现有expired_time=0自动勾选。 - 提交时若勾选或时间留空,
expired_time上报0(对齐后端「0 = 永不过期」约定);表单底部 #extra 文案随勾选状态切换。
- 令牌编辑弹窗:额度限制右侧增加「不限制额度」checkbox(
Token.vue):- 把原本独立成行的 checkbox 折叠到额度限制输入框内右侧,引入通用
.input-with-checkbox横排布局供「永不过期」复用。
- 把原本独立成行的 checkbox 折叠到额度限制输入框内右侧,引入通用
🐛 问题修复
- 修复新建令牌后弹「Page is not found」报错(
Token.vue:handleSubmit):- 原代码误写为
page.value = 0,实际变量是activePage,触发 ReferenceError 后错误冒泡为「Page is not found」提示。 - 改为
activePage.value = 1,新建成功后回到第一页。
- 原代码误写为
- 修复定价弹窗新建时残留 id 写入数据库(4 层防御):
- 前端
PricingSetting.vue新建时清空mpForm.id/gpForm.id,避免把编辑态的旧 id 拼到 POST 请求体。 controller.AddModelPrice/AddGroupPrice在c.ShouldBindJSON之后强制price.Id = 0,即便前端漏改也不会用客户端 id 写入。model.ModelPrice.Insert/GroupPrice.Insert兜底清零p.Id = 0,防御外部调用方 / 未来 caller 把编辑态旧 id 误传到 Insert() 时撞主键冲突。- 这条链路任何一层失守都不会污染数据,但三层全设防是因为该 bug 一旦发生就是主键冲突级别的数据事故。
- 前端
🔧 重构
- 额度限制 checkbox 移入输入框右侧(
Token.vue):合并独立<a-form-item>到输入框内,引入通用.input-with-checkboxflex row 样式(12px gap,checkbox 强制不换行)。 - 模型重定向输入框与提示改为上下排列(
Channel.vue):用.form-stack容器把 textarea 与示例提示竖向排列,新增.form-hint-block修饰符覆盖.form-hint的margin-left、改margin-top,其他 inline hint(key / fallback / fallback_priority)的样式不受影响。
🎨 样式优化
- 过期时间日期选择器移除右侧日历图标(
Token.vue):给<a-date-picker>传空<template #suffix-icon></template>槽覆盖 arco 默认的IconCalendar,让 date-picker 视觉宽度与下方<a-input-number>对齐。 - 模型重定向提示改用
pre-wrap+break-all(Channel.vue):JSON 示例文本较长时自动换行,不再被单行截断。
🧪 测试
ModelPrice/GroupPriceInsert 强制清零 Id 兜底行为(model/model_price_test.go):新增单测覆盖两条 Insert 路径在Id != 0时被清零的兜底。AddModelPrice/AddGroupPriceHTTP 层 Id 清零端到端路径(controller/model_price_test.go):seed 一个id=9999的预存行,再用同样的id=9999提交新增,断言:旧行未被覆盖,新行拿到自增 id 而不是 9999。user_testTestMain 迁移 ModelPrice / GroupPrice 表(controller/user_test.go):让ModelPrice与GroupPrice在 in-memory SQLite 里参与 AutoMigrate,供定价单测复用。ListModelPriceOptionsenabled 过滤与空名兜底(controller/model_price_test.go):seed 2 条 enabled + 1 条 disabled(用 raw SQL 绕开 GORMdefault:true;not null对显式false的覆盖),断言只有 enabled 行出现在响应里;另一条 seed 空model_name断言响应里不出现空字符串。
⚠️ 升级注意事项
- 零数据库迁移:本次不涉及任何表结构变更。
- 需要重新构建前端:
web/default-pro/的修改全部需要npm run build后重新//go:embed进二进制,升级时记得打包新前端。 - 后端二进制必须重启:仅新增
ListModelPriceOptionshandler 与对应路由,运行中的旧进程不会暴露/api/model_price/options。 - 行为变更:
- 渠道编辑弹窗「模型」下拉候选从「编译期硬编码 catalogue」切换为「运行时
model_price表中enabled=true的 model_name」。历史渠道如果选了 catalogue 有但 model_price 没有的「野模型」(如gpt-4-0314、text-davinci-003等历史快照),下拉里将看不到这些选项 — 但前端仍会把form.models合入候选,编辑现有渠道时已选模型仍可见,保存链路无破坏。如需把历史模型纳入下拉,请在「定价管理」里手动添加对应model_name行。 - 渠道编辑弹窗「分组」字段改为下拉选择器,候选来自后端
model.GroupPrice表(/api/group/接口),默认 3 个分组(default / vip / svip)。如需更多分组,请在「定价管理」里给group_prices表加行。 - 令牌编辑弹窗「永不过期」与「不限制额度」checkbox 默认不勾选;编辑现有令牌时,后端值为
0/-1会自动反推勾选对应 checkbox(never_expire = !exp,unlimited_quota = !!record.unlimited_quota)。
- 渠道编辑弹窗「模型」下拉候选从「编译期硬编码 catalogue」切换为「运行时
- 构建与测试:
go build ./...通过。go test ./controller/ ./model/ ./middleware/全绿(新增 4 个定价 / 模型选项相关断言)。cd web/default-pro && npm run build通过。
English
✨ New Features
- Channel modal: searchable multi-select for the model field (
web/default-pro/src/views/channel/Channel.vue):- Replaced the comma-separated
a-inputwith an arcoa-select(multiple+allow-search+allow-clear); the option source is/api/channel/modelson mount. form.modelsswitched fromstringto[]string; submissionsjoin(',')to keep the backend's stored format;parseModelsFieldsplits the backend string back into an array on edit.- Models already present on the form are unioned into the option set so legacy "wild" models stay visible when reopening the dialog.
- Replaced the comma-separated
- Channel modal: new "Model Redirect" textarea (
Channel.vue):- Adds the
model_mappingfield (maps to backendchannels.model_mapping) backed bya-textareaaccepting a JSON object; submission is gated byJSON.parse+typeof === 'object'validation. - The backend column has always existed (
model/channel.go:38); the front-end simply never exposed it. Now aligned with the reference web/default EditChannel form.
- Adds the
- Channel modal: groups converted to a multi-select dropdown and moved under the name field (
Channel.vue):- Replaced the comma-separated
a-inputwith arcoa-select(multiple+allow-search+allow-clear) sourced from/api/group/(controller/group.go). - The form-item moved from under "Models" to under "Name", matching the reference web/default order.
form.groupsswitched to an array; submissionsjoin(',')to preserve the backend's stored format.
- Replaced the comma-separated
- Channel modal: model dropdown source switched to the
model_pricetable (Channel.vue:fetchAvailableModels):- The dropdown now reads
GET /api/model_price/options(admin-accessible), aligning with the billing source of truth. - Operators can add a model to the dropdown without code changes — just add a row in Pricing with the desired
model_name. - The
form.modelsunion fallback is preserved so re-opening the edit dialog on a historical channel still shows its saved selection.
- The dropdown now reads
- New read-only endpoint
GET /api/model_price/options(controller/model_price.go):- Returns only
enabled=truemodel_namestrings, sorted ascending; rows with emptymodel_nameare silently skipped to avoid dirty data in the dropdown. - AdminAuth-accessible; isolated from the RootAuth-protected write endpoints. The old
GET /api/model_price/now carries a// Deprecated:comment and stays for root admins to view the full pricing payload.
- Returns only
- Token modal: "Never Expire" checkbox next to the expiry picker (
Token.vue):- Adds a "永不过期" checkbox to the right of the expiry picker; checking it disables the picker.
- Adds
form.never_expireto the form state; editing auto-checks it when the existingexpired_time=0. - On submit,
expired_timeis sent as0when "Never Expire" is checked or the picker is empty (matching the backend's0 = never expireconvention); the helper text under the field toggles with the checked state.
- Token modal: "Unlimited Quota" checkbox folded inline with the quota input (
Token.vue):- The previously standalone checkbox form-item is collapsed into the right side of the remain_quota input. The shared
.input-with-checkboxflex row layout is reused for the new "Never Expire" checkbox.
- The previously standalone checkbox form-item is collapsed into the right side of the remain_quota input. The shared
🐛 Bug Fixes
- "Page is not found" ReferenceError after creating a token (
Token.vue:handleSubmit):- The code mistakenly wrote
page.value = 0(the actual variable isactivePage); the ReferenceError surfaced as a stray "Page is not found" toast. - Replaced with
activePage.value = 1so a successful create lands on page 1.
- The code mistakenly wrote
- Residual-id leakage when creating pricing entries via the pricing modal (4-layer defence):
- Frontend
PricingSetting.vueclearsmpForm.id/gpForm.idon a fresh "Add" click so the POST body never carries the previously-edited id. controller.AddModelPrice/AddGroupPriceforceprice.Id = 0afterc.ShouldBindJSON, so even a buggy client cannot persist a client-supplied id.model.ModelPrice.Insert/GroupPrice.Insertdefensively zerop.Id = 0as a final guard against future callers passing an edit-state id intoInsert().- Any single layer being skipped is harmless on its own; we keep all three because a failure here is a primary-key collision level data incident.
- Frontend
🔧 Refactor
- "Unlimited Quota" checkbox moved inline with the quota input (
Token.vue): the standalone<a-form-item>is folded into the input row; the shared.input-with-checkboxflex row (12px gap,nowrapcheckbox) is also reused by the new "Never Expire" checkbox. - **Model Redirect input and hint stacked vert...
v0.0.17
v0.0.17 — 管理员运营仪表盘与图表数据统一重构
为管理员引入全新的「数据」运营仪表盘:全站 KPI(用户/资源/营收/配额/收入)+ 7 日请求量/额度/Token 三张趋势折线 + 模型用量分布堆叠柱图 + 使用明细表 + 活跃用户排行榜;图表数据统一由
/api/admin/dashboard/charts一份接口驱动,结构与/api/user/dashboard完全一致([]LogStatistic),方便前后端共享聚合逻辑。同时修复/api/*未匹配路由误返回 OpenAI 风格错误的问题。
中文
✨ 新增功能
后端(Go)
- 新增管理员仪表盘聚合层(
model/admin_dashboard.go,issue #15):GetAdminDashboardOverview(rawRange):一次性聚合 KPI — 用户(总数/今日新增/7 日/30 日/活跃/禁用/已删/今日占比)、资源(令牌/渠道/套餐/兑换码/订阅 各自总数 + 启用/活跃)、配额(今日/7 日/30 日/总)、收入(总营收/充值/订阅/退款),6 段聚合单次往返,每段单条 SQL 避免 N+1。GetAdminTopUsers(rawRange, limit):活跃用户排行榜(近 range 内有 consume 日志 +request_count > 0,按请求数 desc、quota desc 排序),一次性WHERE IN批量补齐当前套餐名(batchTopActivePlanNames)。GetAdminModelDistribution(rawRange, topN)+AdminModelDistribution:全站按 quota 降序的 Top N 模型 + 7 日 day 序列,供堆叠柱图渲染(此接口随后被/charts取代,详见 🔧 重构)。GetAdminUsageDetails(rawRange, topN)+AdminUsageDetails:Top N 模型 × 每日的「透视前」明细行,供「使用明细」表渲染(此接口随后被/charts取代)。ParseAdminDashboardRange/ParseAdminChartsRange:统一的range预设解析(today/7d/30d/all),后者输出 day 对齐的窗口(all收敛为近 30 天避免全量响应过大)。SearchAdminLogsByDayAndModel(start, end):全站day × model聚合,结构与model.SearchLogsByDayAndModel(api/user/dashboard用)完全一致,但不过滤user_id;前端可直接复用同一套图表构建逻辑。
- 新增
/api/admin/dashboard/charts接口(controller/admin/dashboard.go::GetCharts,router/api.go,AdminAuth):- 返回
[]*model.LogStatistic,字段Day / ModelName / RequestCount / Quota / PromptTokens / CompletionTokens,与/api/user/dashboard完全一致。 - 一份数据驱动前端全部图表:请求量/额度/Token 三张折线 + 模型分布堆叠柱 + 使用明细表。
- 返回
- 运营仪表盘配套测试(
model/admin_dashboard_test.go):TestParseAdminDashboardRange:覆盖 4 种 range 边界。TestParseAdminChartsRange:验证 day 对齐窗口与天数。TestGetAdminDashboardOverview_Smoke:覆盖 6 段聚合与 active_7d。TestGetAdminTopUsers:覆盖request_count > 0过滤 + 排序 + 套餐名嵌入。TestSearchAdminLogsByDayAndModel:覆盖[]LogStatistic结构 + 多用户合并 + 字段齐全。- 早期
TestGetAdminModelDistribution/TestGetAdminUsageDetails随 API 重构移除。
前端(web/default-pro/)
- 新增管理员仪表盘页面
views/admin/AdminDashboard.vue(issue #15):- 顶部欢迎条:标题「运营数据」+ 副标题「全站 KPI · 趋势 · 排行 — YYYY-MM-DD」+ 右侧 range 单选(今日/7 日/30 日/全部)+ 最后刷新时间 + 刷新按钮。
- 左侧 16/24 主栏(6 个 panel):
- 用户 KPI:8 张统计卡(总用户/今日新增/7 日/30 日/7 日活跃/禁用/已删/今日占比),带 arco icon + 调色板。
- 营收:总营收/充值/订阅/退款 4 张卡(带「营收」脚注),移至资源上方。
- 资源 / 用量:令牌/渠道/套餐(→/setting/plan)/兑换码(→/redemption)/订阅 5 张可点击卡。
- Token / 请求消耗 + 三张折线:4 张配额卡 + 分隔条 + 请求量/额度/Token 三张 160px 折线图(vue-echarts),样式与仪表盘一致。
- 模型分布(近 7 日):独立全宽 panel,堆叠柱图(Top 8 模型 × 7 天),legend 底部滚动。
- 使用明细:表格分页 8 行(日期 / 模型 / 请求数 / 消耗 / Token),dash-table 样式与仪表盘一致。
- 右侧 8/24 栏:
- 广告位占位:渐变蓝紫 AD 卡片。
- 用户排行:卡片式(rank 徽章 + 用户名/邮箱 + 请求/消耗双列 + 套餐 tag),radio 切换今日/本周/本月。
- 系统公告:3 条公告列表。
- 更新日志:3 条 changelog 条目。
- 资源:官方文档 + GitHub 外链。
- 样式:所有 panel/grid/trend-cell/dash-table 样式与 Dashboard.vue 同构(行高/间距/字号一致)。
- 新增侧边栏「数据」菜单(
layouts/AdminLayout.vue,admin only):- 位置:渠道之上,icon
icon-bar-chart,与「渠道/订单/兑换码/用户/订阅/设置」并列。 - 路由
/admin/dashboard由router/index.js注册,isAdminRouteallow-list 加入AdminDashboard,非管理员访问被重定向到/dashboard。
- 位置:渠道之上,icon
- 新增前端 API 模块
src/api/admin.js:adminApi.overview(range)→GET /api/admin/dashboard/overview。adminApi.topUsers(range, limit)→GET /api/admin/dashboard/top-users。adminApi.charts(range)→GET /api/admin/dashboard/charts(统一图表接口)。adminApi.modelDistribution/adminApi.usageDetails在重构后移除(被charts取代)。
- 新增 i18n 文案(zh/en,
src/i18n/locales.js,admin.*命名空间):- 页面标题
pageTitle: '运营数据' / 'Overview'。 - 5 个分区标题:
sectionUsers / sectionRevenue / sectionResources / sectionQuota / sectionLeaderboard。 - 卡片 label:用户 KPI、营收、资源、Token/请求消耗 等约 40 个 key。
- 时间筛选 label:
todayLabel: '今日' / 'Today'、weekLabel: '本周' / 'This Week'、monthLabel: '本月' / 'This Month'、rangeToday/7d/30d/all。 - 排行榜 tag:
sectionLeaderboard: '用户排行'、列名colUsername/colRequestCount/colQuota/colBalance/colPlan。 - 图表标题精简:
chartRequests: '请求量'、chartQuota: '额度'、chartTokens: 'Token'、chartModelDist: '模型分布(近 7 日)'。 - 公告/更新日志/资源/广告位等辅助文案。
- 页面标题
- 网页 Title 动态化:
- 落地页默认 Title 改为
ONE-API-PRO—企业级AI API 网关(web/default-pro/index.html)。 - 路由切换时
router.afterEach钩子按meta.title拼接{菜单名}—ONE-API-PRO,如「控制台—ONE-API-PRO」。
- 落地页默认 Title 改为
🐛 问题修复
- 修复
/api/*未匹配路由误返回 OpenAI 风格invalid_request_error误导排查(router/web.go):- 原来
NoRoute对/v1/*与/api/*都返回RelayNotFound(OpenAIinvalid_request_error),导致缺失的管理接口被误判为 relay 路由问题。 - 现仅
/v1/*保留 OpenAI 错误(relay 客户端需要兼容),/api/*未匹配返回标准{success:false, message:"接口不存在: METHOD PATH"}404 信封,便于定位路由缺失。
- 原来
- 修复 admin_dashboard 结构体 GORM column 推断错位(
model/admin_dashboard.go):AdminDashboardUsers/AdminDashboardResources/AdminDashboardTopUserRow等聚合行结构体默认按 GORM 字段名 snake_case 推断列名(New7d→new7d),与 SQL 别名new_7d不匹配,导致聚合全部填充 0。- 显式补齐
gorm:"column:..."标签后正常。 - 由
TestGetAdminDashboardOverview_Smoke触发并修复。
- 修复 Vue
<style scoped>跨组件不生效:AdminDashboard 引用的.panel / .stat-grid / .trend-cell / .dash-table等与 Dashboard.vue 同名样式,因 scoped hash 不同不会跨文件共享;将 AdminDashboard 实际用到的样式全部内联到本组件的<style scoped>,并去除.admin-dashboard容器多余 padding,改用.dashboard { display: flex; flex-direction: column; gap: 16px }与 Dashboard.vue 布局一致。 - 修复 admin 折线图无数据时全 0 看不见线:
yAxis.min = 0+max = maxV > 0 ? undefined : 1(旧版在 0 数据时max = undefined导致 auto-scale 把线压成一条不可见线);areaStyle透明度由02提到30;symbolSize = 6+itemStyle.borderWidth = 2强化折线点。 - 修复 admin 折线图「9-11 标签被右边界裁切」:
grid.right: 16+xAxis.axisLabel.margin: 8+hideOverlap: false,确保最新日期可见。 - 修复 admin 折线图缺今日:
AdminTrendPoint/aggregateTrends在某日无数据时只回{day}(因omitempty)导致前端undefined;改用[]LogStatistic一致结构 + 前端本地补全 N 天日期序列(含今天),彻底解决「9-11 今天的没显示」。 - 修复 AdminDashboard 残留
IconHistogram/IconRollback引用:npm run build不报错(运行时才崩),统一替换为 arco icon set 内可用名称IconBarChart/IconArrowFall。 - 修复 9-11 类型 UTC 对齐:Go
todayStart = now - (now%daySec)按 UTC 边界 + SQLitestrftimeUTC + CST 本地 9:00 之间偏差 1 天的隐患,统一在测试日志时间戳 +100s 保证 UTC 与本地对齐。
🔧 重构
后端(Go)
- 图表数据从 overview 拆分独立:
- 移除
AdminTrendPoint/AdminDashboardTrends/aggregateTrends三个旧结构/函数。 AdminDashboardOverview移除Trends字段,专注 KPI。- 移除
AdminModelDistribution*/GetAdminModelDistribution与AdminUsageDetails*/GetAdminUsageDetails,由/charts统一驱动。 - 移除对应 controller handlers + router 路由(
/api/admin/dashboard/model-distribution、/api/admin/dashboard/usage-details)。 model/admin_dashboard.go总行数减少 ~320 行;删除sort/time等不再需要的 import。
- 移除
- 响应结构统一:单一
/charts接口返回与/api/user/dashboard同构的[]LogStatistic,前端无需分别请求 3 个端点拼装数据。
前端
- AdminDashboard 全面重构:从 trend row 内嵌模型分布改为独立全宽模型分布 panel;折线图数量从「请求量/Quota/模型分布」改为「请求量/额度/Token」3 张折线(与 Dashboard.vue 一致)。
adminApi.modelDistribution/adminApi.usageDetails移除,统一为adminApi.charts一份接口。AdminDashboard.vue新增chartData / daySeries / distModels / usageRows四个 computed,本地构建日期序列 + 客户端聚合 Top 8 模型,避免依赖后端预聚合。lineOption从overview.trends改读chartData([{date, value}])。modelBarOption从distDays + distItems改读distModels(前端 Top N 排序)。- 使用明细表从
usageDetails改读usageRows(扁平 day×model,日期 desc + 消耗 desc 排序)。
🚀 性能 / 运维
- 修复 release-docker workflow arm64 emulated
npm install触发 SIGILL(Dockerfile+.github/workflows/release-docker.yml):- 改为在 release-docker job 中预构建
web/build,docker build 不再npm install,避免 QEMU 模拟的 Node.js 二进制崩溃。 - 详见
ci(release-docker): 预构建 web/build,避免 arm64 emulated npm install 触发 SIGILL。
- 改为在 release-docker job 中预构建
⚠️ 升级注意事项
- 零数据库迁移:所有 admin dashboard 新增字段都是聚合响应或新接口,无表结构变更。
- 后端路由变更(
router/api.go,admin 路由组):- 新增
GET /api/admin/dashboard/charts(AdminAuth)。 - 移除
GET /api/admin/dashboard/model-distribution、GET /api/admin/dashboard/usage-details(被/charts取代)。 - 如有外部监控/脚本调用旧路由,请改用
/charts+ 前端聚合逻辑(或直接用/api/admin/dashboard/overview)。
- 新增
- 后端二进制必须重启:
/charts是新路由,编译进二进制后才生效。升级后必须重启one-api-pro进程。 - 前端 chunk 缓存:浏览器需要硬刷(Cmd+Shift+R)以加载新
AdminDashboardchunk;旧的AdminDashboard-*.jschunk 仍可能引用/usage-details导致 404。 - 未匹配
/api/*错误格式变化:升级后若遗漏注册路由,前端会收到{success:false, message:"接口不存在: GET /api/..."}(HTTP 404)而非 OpenAI 风格错误,更易定位。 - 后端构建与测试:
go build ./...通过。go test ./model/ ./controller/ ./middleware/全绿(含 3 个新增 admin_dashboard 测试 + 已有测试)。- 前端:
cd web/default-pro && npm run build通过。
- 页面标题变更:浏览器 tab 标题从「One Api Pro——企业级 API 网关」变为
ONE-API-PRO—企业级AI API 网关,后台路由切换时动态显示「{菜单名}—ONE-API-PRO」。如有外部监控依赖旧标题文案请知悉。
English
✨ New Features
Go backend
- New admin dashboard aggregate layer (
model/admin_dashboard.go, issue #15):GetAdminDashboardOverview(rawRange): one-shot KPI aggregation — users (total / today / 7d / 30d / active / disabled / deleted / today-ratio), resources (tokens / channels / plans / redemptions / subscriptions with enabled/active counts), quota (today / 7d / 30d / total), revenue (total / top-up / subscription / refund). Six sections in one round-trip; one SQL per section, no N+1.GetAdminTopUsers(rawRange, limit): active-user leaderboard (consumed logs in window +request_count > 0, sorted by request count desc then quota desc); current plan names batch-embedded via a singleWHERE IN.GetAdminModelDistribution(rawRange, topN)+AdminModelDistribution: whole-site top-N models by quota + 7-day series for stacked bar (later superseded by/charts; see 🔧 Refactor).GetAdminUsageDetails(rawRange, topN)+AdminUsageDetails: top-N model × per-day breakdown for the "usage details" table (later superseded by/charts).ParseAdminDashboardRange/ParseAdminChartsRange: unified range preset parsing (today/7d/30d/all); the latter emits a day-aligned window and capsallto the last 30 days to keep responses bounded.- `Se...
v0.0.16
v0.0.16 — 修复 Anthropic 流式计费 usage 双计与缓存语义
修复 Anthropic / Vertex Claude / AWS Bedrock Claude 渠道流式请求中 usage 被双计(input/cache 令牌按约两倍扣费)的问题,并统一 Anthropic 缓存计费语义:cache read 折入 prompt 计价、补齐 AWS 渠道此前完全缺失的 cache 统计。
中文
🐛 问题修复
- 修复 Anthropic 流式计费「usage 双计」(issue #13):
- 新版 Messages API 下
message_start与message_delta都携带整段请求的累计 usage(message_delta重复message_start的 input/cache 字段并带上最终output_tokens)。 - 原实现按
+=当作增量累加,导致input_tokens/cache_read_input_tokens被计两遍,典型场景多扣约 50%(如 3.5-sonnet 下 4522 vs 正确 3000)。 - 现改为对累计值取 max 合并,并兼容旧形态(
message_delta仅带 output、input 为 0)不丢message_start的 input 计数。
- 新版 Messages API 下
- 修复 cache read 语义错配造成的欠费:Anthropic 的
input_tokens与cache_read/cache_creation_input_tokens互斥且不含彼此,而共享计费公式按 OpenAI「cached ⊆ prompt」扣减(input×(prompt−cached)),导致 cache read 被从 prompt 中重复扣减、按 read 价计费的同时又丢了一次 input 价;现归一化把 read 折入 prompt,公式自动还原出input×input + readPrice×read的正确金额。 - 修复 AWS Bedrock Claude 渠道 cache 完全不参与计费:流式与非流式路径此前都没有把
cache_read_input_tokens写入PromptTokensDetails.CachedTokens,现统一补齐。 - 修复 cache_creation(写入令牌)解析后从不计费:
cache_creation_input_tokens此前仅被解析、未进入 usage;现折入 prompt 按输入价计费(真实写入价约为 1.25×input,此为已知近似,见 #13)。
🔧 重构
- 新增
relay/adaptor/anthropic/usage.go:ClaudeUsage2OpenAI(Claude→OpenAI usage 归一化)与MergeClaudeUsage(累计流式事件取 max 合并)。 - native Anthropic / Vertex AI Claude / AWS Bedrock Claude 三条消费路径统一复用上述 helper,消除三处手写累计逻辑的漂移。
🧪 测试
relay/adaptor/anthropic/main_test.go:message_deltafixture 从旧形态(input_tokens: 0)更新为真实累计形态,避免继续掩盖双计 bug。- 新增
TestClaudeUsage2OpenAI/TestMergeClaudeUsage行为单测:覆盖累计序列不双计、旧形态兼容、read/creation 折入、cached ⊆ prompt不变式。
⚠️ 升级注意事项
- 零数据库迁移、零配置变更。
- 计费口径调整:修复后 Anthropic 渠道不再双计,扣费恢复正常;
prompt_tokens消费日志将包含折入的 cache read/creation,口径与 OpenAI 一致(历史消费记录不追溯修正)。 - 运行验证:
go build ./...;go test ./relay/adaptor/anthropic/ ./relay/billing/ratio/ ./model/ ./controller/ ./middleware/。
English
🐛 Bug Fixes
- Fixed double-counted streaming usage for Anthropic (issue #13):
- Under the current Messages API, both
message_startandmessage_deltacarry cumulative usage for the whole request (message_deltarepeatsmessage_start's input/cache fields and adds the finaloutput_tokens). - The old code accumulated with
+=as if each event were incremental, soinput_tokens/cache_read_input_tokenswere counted twice — typically ~50% over-billing (e.g. 4522 vs the correct 3000 on claude-3.5-sonnet). - Events are now merged by max of the cumulative values, and the legacy shape (delta with only
output_tokens, zeroed input) still keepsmessage_start's input count.
- Under the current Messages API, both
- Fixed cache-read semantic mismatch causing under-billing: Claude's
input_tokensis disjoint fromcache_read/cache_creation_input_tokens, yet the shared billing formula assumes OpenAI's "cached ⊆ prompt" (input×(prompt−cached)). Cache reads were therefore subtracted twice — charged at read price while also dropping one input-price charge. Reads are now folded intoPromptTokens, so the formula yields the correctinput×input + readPrice×read. - Fixed AWS Bedrock Claude channels never billing cache: neither the streaming nor the non-streaming path wrote
cache_read_input_tokensintoPromptTokensDetails.CachedTokens; both now do. - Fixed cache-creation tokens being parsed but never billed:
cache_creation_input_tokenswas unmarshalled yet dropped from usage; it is now folded intoPromptTokensand billed at input price (actual write price is ≈1.25×input — a documented approximation, see #13).
🔧 Refactor
- New
relay/adaptor/anthropic/usage.go:ClaudeUsage2OpenAI(Claude→OpenAI usage normalization) andMergeClaudeUsage(max-merge of cumulative stream events). - The three consumer paths — native Anthropic, Vertex AI Claude, and AWS Bedrock Claude — now share these helpers, removing three divergent hand-written accumulators.
🧪 Tests
relay/adaptor/anthropic/main_test.go: themessage_deltafixture was updated from the legacy zeroed-input_tokensshape to the real cumulative shape so the double-count bug can no longer hide.- Added
TestClaudeUsage2OpenAI/TestMergeClaudeUsagebehavior tests covering cumulative sequences, legacy-shape compatibility, read/creation folding, and thecached ⊆ promptinvariant.
⚠️ Upgrade Notes
- Zero database migration and zero configuration changes.
- Billing behavior: Anthropic channels no longer double count and charge correctly;
prompt_tokensin usage logs now includes folded cache read/creation, consistent with OpenAI semantics (historical logs are not retroactively adjusted). - Verification:
go build ./...;go test ./relay/adaptor/anthropic/ ./relay/billing/ratio/ ./model/ ./controller/ ./middleware/.
v0.0.15
v0.0.15 — 管理员订单管理后台与订阅诊断端点
管理员可在后台「订单」页面集中管理全部用户订单(套餐订阅 + 充值),支持多维筛选、状态流转(标记已付 / 退款 / 删除)与详情查看;同步新增
/api/diag/subscriptions诊断端点,配套UserPlan.Insert防御性校验,方便排查plan_id异常问题。
中文
✨ 新增功能
后端(Go)
- 新增管理员诊断端点
GET /api/diag/subscriptions(controller/diag.go,AdminAuth):- 拉取
user_plans表前 100 行(GetAllUserPlans(0, 100, 0, -1)),逐行对比orders表中对应订单的plan_id/type,并嵌入用户摘要。 - 返回体含
total_user_plans(扫描行数)、missing_plan_id(plan_id<=0的疑似脏数据行数)、rows(逐行明细:user_plan+order_no+order_plan_id+order_type+user)。 - 用户信息复用
GetUsersBriefByIds单次WHERE IN查询,无 N+1。 router/api.go注册/api/diag/subscriptions路由(AdminAuth 中间件兜底)。
- 拉取
model/user增加UserBrief与批量查询(model/user.go):- 新增
UserBrief结构体(id/username/display_name三个字段),用于列表嵌入场景。 - 新增
GetUsersBriefByIds(ids []int) (map[int]*UserBrief, error):空ids短路不查 DB,否则单条WHERE IN查询,并对缺失 id 容错。
- 新增
- 订单管理接口支持多维过滤 + 用户嵌入(
controller/order+model/order):- 新增
model.OrderAdminFilter结构体与applyTohelper;GetAllOrders/SearchOrders改用*OrderAdminFilter参数。 model.Order新增User *UserBrief(gorm:"-")瞬时字段,admin 列表响应中作为user字段返回。controller新增parseOrderAdminFilter:从 query 解析type/status/source/user_id/plan_id/keyword六个过滤维度。status空字符串表示「全部」,避免与OrderStatusPending(0)值重叠。- 新增
enrichOrdersWithUserBrief批量补全User字段:单次WHERE IN查询全部目标 user id,无 N+1。
- 新增
UserPlan.Insert防御性校验拒绝plan_id<=0(model/user_plan.go):Insert入口新增前置校验:PlanId <= 0时直接返回错误user_plan.plan_id 不能为空,杜绝脏数据写入。- 配套新增
model/subscription_defense_test.go,覆盖Insert与ActivatePackageByOrder两处防御路径。
前端(web/default-pro/)
- 新增管理员订单列表页
views/admin/AdminOrders.vue:- 顶部欢迎条显示总条数 meta-chip。
- 独立搜索卡:订单号 / 支付流水号搜索 + 类型(套餐 / 充值)/ 状态(待支付 / 已支付 / 已取消 / 已退款)/ 来源(用户自助 / 管理员)三个
<a-select>下拉筛选 + 重置 / 刷新按钮。 - 11 列 grid 行展示:ID / 订单号 / 类型 / 用户 / 套餐 / 金额 / 支付方式 / 状态 / 来源 / 创建时间 / 操作,用户列渲染嵌入的
user.display_name。 - 操作列支持:查看详情 / 标记已付(
status=0→1)/ 退款(status=1→3)/ 删除(root 限定,仅status≠1可删)。 - 详情弹窗展示完整字段,并异步拉取最新 user 信息;标记已付弹窗选择
pay_method+ 可选pay_trade_no。 <a-spin>设置style="width:100%"避免布局塌陷(AGENTS.md §10.4)。
- 注册
/admin/orders路由与侧边栏菜单:router/index.js:注册/admin/orders路由;isAdminRoute加入AdminOrders判断。layouts/AdminLayout.vue:在渠道与兑换码之间插入「订单」菜单项(iconicon-storage)。api/order.js扩展:orderApi.list(params)接受分页与多过滤参数;search扩展支持status/source/user_id/plan_id。- 新增
buildAdminParamshelper,仅透传非零 / 非空字段,避免污染 query。
🐛 问题修复
- 修复
UserPlan.Insert写入plan_id=0导致 admin 列表套餐列空白的脏数据问题:通过新增PlanId<=0前置校验防御(见上),异常来源追溯到ActivatePackageByOrder流程同样增加防御。 - 修复 dashboard 与 admin 之间因缺乏统一订单管理入口导致的运维盲区:通过新增 AdminOrders 页面 +
/api/diag/subscriptions端点,提供集中可视化的订单与订阅诊断能力。
🧪 测试
- 新增
model/order_admin_filter_test.go(order_admin_filter_test测试集):setupOrderAdminTestDB用glebarez/sqlite内存库 +gorm.AutoMigrate建users+orders表。seedUsersAndOrders通用夹具 helper:每个 user 必须设AccessToken/AffCode以满足 unique 索引。TestOrderAdminFilter_ApplyTo:验证零值全字段为 no-op。TestGetAllOrders_FilterMatrix:9 个 subtest 覆盖type/status/source/user_id/plan_id/keyword五个过滤维度及组合。TestSearchOrders_StatusFilter:验证搜索结果也按status过滤。TestGetUsersBriefByIds_EmptyAndIn:验证空 ids 短路 +WHERE IN查询 + 缺失 id 容错。
- 新增
model/subscription_defense_test.go:覆盖UserPlan.Insert与ActivatePackageByOrder两处防御性校验路径。
📚 文档
README.md新增 QQ 群信息(979158b add qq group)。
⚠️ 升级注意事项
- 零数据库迁移:
UserBrief是新类型、Order.User是gorm:"-"瞬时字段、OrderAdminFilter仅是 controller 层的解析结构;不涉及表结构变更。 - 新增 admin 路由:
GET /api/diag/subscriptions(AdminAuth):诊断端点,生产环境视情况可关闭或仅暴露给 root。- admin 订单接口
/api/order/与/api/order/search新增可选 query 参数status/source/user_id/plan_id/keyword,向后兼容(不传则忽略)。
- 防御性变更:
UserPlan.Insert与ActivatePackageByOrder在plan_id<=0时会直接拒绝;若有历史代码路径依赖此容错行为,需同步修复上游调用方(理论上不应有此类调用)。 - 前端:新增
views/admin/AdminOrders.vue与侧边栏菜单项;api/order.js扩展,无破坏性变更。 - 后端构建:
go build ./...;测试运行go test ./model/ ./controller/ ./middleware/。
English
✨ New Features
Go backend
- New admin diagnostic endpoint
GET /api/diag/subscriptions(controller/diag.go, AdminAuth):- Pulls up to 100 rows from
user_plansviaGetAllUserPlans(0, 100, 0, -1), joins each with the source order'splan_id/type, and embeds a user brief. - Response includes
total_user_plans(rows scanned),missing_plan_id(rows withplan_id<=0), androws(per-row details:user_plan+order_no+order_plan_id+order_type+user). - User data is fetched via
GetUsersBriefByIdswith a singleWHERE INquery — no N+1. router/api.goregisters/api/diag/subscriptionsbehind the AdminAuth middleware.
- Pulls up to 100 rows from
model/useraddsUserBriefand batch lookup (model/user.go):- New
UserBriefstruct (id/username/display_name) for list-embedding scenarios. - New
GetUsersBriefByIds(ids []int) (map[int]*UserBrief, error): short-circuits on emptyids; otherwise runs a singleWHERE INquery and tolerates missing ids.
- New
- Order-management endpoints gain multi-dimensional filters + user embedding (
controller/order+model/order):- New
model.OrderAdminFilterstruct +applyTohelper;GetAllOrders/SearchOrdersnow take*OrderAdminFilter. model.Ordergains a transientUser *UserBrieffield (gorm:"-") that admin list responses serialize asuser.controlleraddsparseOrderAdminFilter, readingtype/status/source/user_id/plan_id/keywordfrom query params.- Empty
statusmeans "all", avoiding theOrderStatusPending(0)value clash. - New
enrichOrdersWithUserBriefhelper batches user embedding via a singleWHERE IN.
- New
UserPlan.Insertdefensively rejectsplan_id<=0(model/user_plan.go):Insertnow refuses to write whenPlanId<=0, returning the erroruser_plan.plan_id 不能为空.- Companion test
model/subscription_defense_test.gocovers bothInsertandActivatePackageByOrderdefensive paths.
Frontend (web/default-pro/)
- New admin order-list page
views/admin/AdminOrders.vue:- Welcome bar with total-count meta-chip.
- Standalone search card: order-no / pay-trade-no search + three
<a-select>filters (type: plan / recharge, status: pending / paid / cancelled / refunded, source: user / admin) + reset / refresh buttons. - 11-column grid row: ID / order-no / type / user / plan / amount / pay-method / status / source / created-at / actions; user column renders embedded
user.display_name. - Actions: view details / mark-paid (
status=0→1) / refund (status=1→3) / delete (root-only, only whenstatus≠1). - Detail modal shows full fields and lazily fetches the latest user info; mark-paid modal picks
pay_methodand optionalpay_trade_no. <a-spin>carriesstyle="width:100%"per AGENTS.md §10.4 to avoid layout collapse.
- Register
/admin/ordersroute and sidebar menu:router/index.js: registers/admin/orders;isAdminRouteincludesAdminOrders.layouts/AdminLayout.vue: inserts the "订单" menu item (iconicon-storage) between Channels and Redemption.api/order.jsextension:orderApi.list(params)accepts paging + multi-filter params;searchextends withstatus/source/user_id/plan_id.- New
buildAdminParamshelper only forwards truthy fields, keeping query strings clean.
🐛 Bug Fixes
- Fixed dirty
plan_id=0writes causing empty plan columns in the admin list: now blocked at theUserPlan.Insertboundary (see above), with the source flowActivatePackageByOrderalso defensively guarded. - Closed the operations blind-spot between the dashboard and admin by adding a unified order-management entry point: the new AdminOrders page plus
/api/diag/subscriptionsendpoint provides a single, visual place to inspect orders and diagnose subscriptions.
🧪 Tests
- New
model/order_admin_filter_test.go(theorder_admin_filter_testsuite):setupOrderAdminTestDBspins upglebarez/sqlitein-memory +gorm.AutoMigrateforusersandorders.seedUsersAndOrdersshared fixture helper; every user must setAccessToken/AffCodeto satisfy unique indexes.TestOrderAdminFilter_ApplyTo: verifies zero-value filter is a no-op.TestGetAllOrders_FilterMatrix: 9 subtests coveringtype/status/source/user_id/plan_id/keywordand combinations.TestSearchOrders_StatusFilter: verifies the search path also honoursstatus.TestGetUsersBriefByIds_EmptyAndIn: short-circuit on empty ids +WHERE INquery + missing-id tolerance.
- New
model/subscription_defense_test.go: covers both theUserPlan.InsertandActivatePackageByOrderdefensive paths.
📚 Documentation
README.mdadds QQ group info (979158b add qq group).
⚠️ Upgrade Notes
- Zero database migration:
UserBriefis a new type,Order.Useris agorm:"-"transient field, andOrderAdminFilteris a controller-layer parsing struct — no schema changes. - New admin routes:
GET /api/diag/subscriptions(AdminAuth): diagnostic endpoint; consider restricting or root-onlying it in production as needed.- Admin order endpoints
/api/order/and/api/order/searchnow accept optional query paramsstatus/source/user_id/plan_id/keyword. Fully backward-compatible — absent params are ignored.
- Defensive change:
UserPlan.InsertandActivatePackageByOrdernow refuseplan_id<=0. If any legacy code path relied on this tolerance, the upstream caller must be fixed (none should). - Frontend: new
views/admin/AdminOrders.vueplus a sidebar entry;api/order.jsextended, no breaking changes...
v0.0.13
v0.0.13 — 套餐 features 字段重构 & 用户时间戳自动填充
套餐
features字段从字符串重构为array<string>(新增StringSlice类型,三种历史形态零迁移兼容),后台套餐弹窗改为动态行编辑器避免用户手敲 JSON;同时修复User模型created_at始终为 0、updated_at不刷新的历史 bug,并在用户列表新增「注册时间」列。
中文
✨ 新增功能
后端(Go)
- 套餐
features字段重构为array<string>(Issue #11):model/plan.go新增StringSlice类型,同时实现MarshalJSON/UnmarshalJSON/driver.Valuer/sql.Scanner四个接口,作为[]string在 JSON wire 与 DB text 列之间的双向桥接。- JSON wire 格式统一为
["A","B","C"];DB 仍以 JSON 字符串写入text列,AutoMigrate无 schema 变更。 UnmarshalJSON/Scan兼容历史三种形态:JSON 数组字符串 / 换行分隔纯文本 / JSON 对象,统一归一为[]string。- 新增
Plan.GetFeatures()返回拷贝后的[]string,供 controller 序列化使用,避免外部修改共享底层 slice。 - 新增
model/plan_test.go:16 个用例 覆盖 Marshal/Unmarshal/Value/Scan 往返、nil、空、空白、CRLF、非法 JSON 等边界。
GetCurrentPlan返回 features 数组形态(controller/plan.go):data["features"]从裸字符串改为Plan.GetFeatures(),与/api/plan/detail一致。前端Plans.vue::parseFeatures已Array.isArray兼容,无需联动修改。User模型时间戳自动填充(model/user.go):- 新增
User.BeforeCreate钩子:CreatedAt/UpdatedAt为 0 时自动写入helper.GetTimestamp()。 - 新增
User.BeforeUpdate钩子:任何Updates(...)路径都会刷新UpdatedAt。 - 一并覆盖
Register/CreateUser/UpdateUser/ManageUser/EmailBind/GenerateAccessToken等所有写入路径。 - 修复
/api/user/?p=0接口中created_at始终为 0、updated_at不刷新的历史 bug。
- 新增
前端(web/default-pro/)
- 后台套餐弹窗「特性说明」改为动态行编辑器(
views/setting/PlanSetting.vue):- 删除原
<a-textarea>,替换为.features-editor容器:每行<a-input allow-clear>+ 删除按钮,末尾「添加特性」虚线按钮。 - 新增
formFeaturesref 与form.features解耦,专管动态行;提供addFeature/addFeatureAfter/removeFeature/updateFeature四个操作函数。 - 某行
<a-input>按 Enter 自动新增一行;末行空且唯一时禁用删除(防误清空)。 openModal用utils/plan.featuresFromRecord回填(向后兼容旧数据形态);handleSave用sanitizeFeaturesList过滤空行后直接以数组形态POST/PUT/api/plan/,零格式歧义。- 新增
.features-editor/.features-editor-row/.features-editor-add/.features-editor-remove样式。
- 删除原
- 后台用户列表新增「注册时间」列(
views/user/User.vue):- 表头新增「注册时间」列,渲染
u.created_at;单元格显示YYYY-MM-DD短日期,hover tooltip 显示完整本地时间。 - 新增
formatDate/formatDateTime工具函数,无值时回退-。 - 调整
.list-head/.list-row的 grid 模板,新增 120px 列宽。
- 表头新增「注册时间」列,渲染
- 新增
src/utils/plan.js纯函数工具 + 14 个node:test单测:normalizeFeaturesRaw(raw):把后端可能返回的多种形态(数组 / JSON 字符串 / 换行文本 / CRLF / JSON 对象 / null / 非法 JSON / 数字)统一归一为[]string,丢弃空项。sanitizeFeaturesList(list):清洗表单输入,去空白、返回新数组不修改入参。featuresFromRecord(raw):把后端字段转成弹窗表单数组(空记录回退到 1 行空输入)。buildEmptyFeaturesForm():返回 1 个空字符串的初始表单。
🐛 问题修复
- 修复
User表created_at始终为 0、updated_at不刷新的历史 bug:通过新增User.BeforeCreate/BeforeUpdate钩子(见上)覆盖所有写入路径。 - 修复后台套餐弹窗「特性说明」易填错导致前端为空:原本要求 admin 手敲
["A","B","C"]JSON 字符串,用户格式填错前端就显示空;改为每行<a-input>动态行编辑器,提交时由sanitizeFeaturesList清洗,零格式歧义。
📚 文档
docs/API.md同步更新套餐 features 字段类型(Issue #11):- 字段说明表
features行:string→array<string>,措辞改为「功能特性列表,每项一行展示在用户端套餐卡」。 GET /api/plan/响应体示例:"features": "功能特性描述"→["API 调用 1000 次/月", "支持 GPT-4o"]。POST /api/plan/请求体示例同步为数组形式。
- 字段说明表
⚠️ 升级注意事项
- 零数据库迁移:
Plan.Features字段类型未变(仍是text列),StringSlice仅在 Go 层做 JSON ↔ DB 双向桥接;现有数据的features文本("A\nB\nC"或"{\"API\":true}"等历史形态)会在首次读取时自动归一化为数组。 - API 兼容性:
GET /api/plan/与GET /api/plan/current的features字段由字符串改为数组;前端Plans.vue::parseFeatures早已Array.isArray兼容,现有用户端无感知。 - 管理端操作:admin 进入「设置 → 套餐管理」编辑任意套餐时,特性说明将以「每行一个 input + 添加/删除」形式展示,老数据自动按行展开。
- 后端编译:新增
model/plan_test.go与model/user_test.go,纯单元测试使用glebarez/sqlite内存库,不依赖外部 DB。 - 前端构建:无新增依赖;建议
pnpm install && npm run build后发布。
English
✨ New Features
Go backend
- Refactored the plan
featuresfield toarray<string>(Issue #11):- Added a new
StringSlicetype inmodel/plan.gothat implementsMarshalJSON,UnmarshalJSON,driver.Valuer, andsql.Scanner— it bridges[]stringbetween the JSON wire format and the DBtextcolumn. - JSON wire format unified to
["A","B","C"]; the DB still stores a JSON string in thetextcolumn, soAutoMigrateis a no-op (zero schema migration). UnmarshalJSON/Scanaccept all three historical shapes (JSON array string, newline-delimited plain text, JSON object) and normalize them to[]string.- Added
Plan.GetFeatures()returning a copy-safe[]stringfor controller serialization so external code never mutates the underlying slice. - Added
model/plan_test.go: 16 cases covering Marshal/Unmarshal/Value/Scan round-trips plus nil, empty, whitespace, CRLF, and invalid JSON edge cases.
- Added a new
GetCurrentPlanreturns features as an array (controller/plan.go): replaceddata["features"] = up.Plan.Features(raw string) withdata["features"] = up.Plan.GetFeatures()so it matches/api/plan/detail. The user-sidePlans.vue::parseFeaturesalready handledArray.isArray, no coordinated front-end change required.Usermodel auto-fills timestamps (model/user.go):- Added
User.BeforeCreate: whenCreatedAt/UpdatedAtare 0 they are auto-filled viahelper.GetTimestamp(). - Added
User.BeforeUpdate: everyUpdates(...)path now refreshesUpdatedAt. - Covers every write path:
Register,CreateUser,UpdateUser,ManageUser,EmailBind,GenerateAccessToken, etc. - Fixes the historical bug where
/api/user/?p=0returnedcreated_at=0and never-refreshedupdated_at.
- Added
Frontend (web/default-pro/)
- Admin plan modal's "Feature description" is now a dynamic row editor (
views/setting/PlanSetting.vue):- Replaced the
<a-textarea>with a.features-editorcontainer: one<a-input allow-clear>per row + a delete button, with a trailing dashed "Add feature" button. - Introduced a
formFeaturesref decoupled fromform.features; addedaddFeature/addFeatureAfter/removeFeature/updateFeaturefor row manipulation. - Pressing Enter on any row auto-appends an empty row; when the last row is empty and the only one, delete is disabled to prevent accidental emptying.
openModalbackfills viautils/plan.featuresFromRecord(backward-compatible with legacy shapes);handleSavePOSTs/PUTs/api/plan/with asanitizeFeaturesList-cleaned array — zero format ambiguity.- Added
.features-editor/.features-editor-row/.features-editor-add/.features-editor-removestyles.
- Replaced the
- Added a "Registration time" column to the admin user list (
views/user/User.vue):- New header column rendering
u.created_at; cell showsYYYY-MM-DD, hover tooltip reveals the full local datetime. - Added
formatDate/formatDateTimehelpers; falls back to-on missing/zero timestamps. - Adjusted
.list-head/.list-rowgrid template to add a 120px column.
- New header column rendering
- New
src/utils/plan.jspure-function helpers + 14node:testunit tests:normalizeFeaturesRaw(raw): normalize any of the historical shapes (array / JSON string / newline text / CRLF / JSON object / null / invalid JSON / numbers) to[]string, dropping blanks.sanitizeFeaturesList(list): trim, filter blanks, return a fresh array without mutating the input.featuresFromRecord(raw): convert the backend field into modal form rows (empty record falls back to a single empty row).buildEmptyFeaturesForm(): returns a single empty-string initial form.
🐛 Bug Fixes
- Fixed the historical
Userbug wherecreated_atwas always 0 andupdated_atnever refreshed: addressed by the newUser.BeforeCreate/BeforeUpdatehooks above, covering every write path. - Fixed the admin plan modal "Feature description" being easy to mistype and rendering empty on the user side: the textarea required admin to hand-type a JSON
["A","B","C"]string, and any typo produced an empty list. The new dynamic-row editor withsanitizeFeaturesListremoves the format ambiguity.
📚 Documentation
docs/API.mdupdated to reflect the newfeaturestype (Issue #11):- Field-table row
features:string→array<string>; wording updated to "list of features, one per row in the user-facing plan card". GET /api/plan/response sample:"features": "功能特性描述"→["API 调用 1000 次/月", "支持 GPT-4o"].POST /api/plan/request sample synced to the array form.
- Field-table row
⚠️ Upgrade Notes
- Zero database migration:
Plan.Featurescolumn type is unchanged (stilltext);StringSliceis purely a Go-side bridge between JSON and the DB column. Existing data — whether plain text like"A\nB\nC"or legacy JSON-object shapes — is normalized to[]stringautomatically on first read. - API compatibility:
GET /api/plan/andGET /api/plan/currentnow returnfeaturesas an array. The user-sidePlans.vue::parseFeaturesalready handledArray.isArray, so existing user-facing code is unaffected. - Admin UX: when an admin opens any plan in Settings → Plan Management, the feature list now renders as one
<a-input>per row with add/delete buttons; legacy data is split into rows automatically. - Backend build: added
model/plan_test.goandmodel/user_test.go, both pure unit tests againstglebarez/sqlitein-memory +gorm.AutoMigrate, no external DB required. - Frontend build: no new dependencies. Recommend
pnpm install && npm run buildbefore publishing.
v0.0.12
v0.0.11 — 在线充值(topup)全链路打通
用户可在控制台「我的余额」卡片直接发起在线充值,复用同一套微信 / 支付宝支付通道;后台新增独立的「充值」设置 Tab,管理员可配置快捷金额、自定义金额与换算比例;同步沉淀《AGENTS.md》开发规范并更新全语种 README。
中文
✨ 新增功能
后端(Go)
- 新增「在线充值」业务模块(
OrderTypeTopup=2):- 新增
model/topup.go,定义TopupPreset、CreateTopupOrderInput、TopupOrderPlanInfo等结构,提供CreateTopupOrder、ActivateTopupByOrder、ResolveTopupAmount、GetTopupSettings、SaveTopupSettings等业务函数。 - 复用
GenerateOrderNo("TP")生成充值订单号(TP 前缀),写入与套餐订单同一张orders表的type=2记录,节省一张表。 ActivateTopupByOrder幂等:订单已支付直接返回;激活时调IncreaseUserQuota加额度并RecordTopupLog,TODO标注退款时不回退 quota(待统一订单管理上线)。- 启动时通过
model.AutoMigrate自动建表,无需手写 DDL。
- 新增
- 新增充值系统设置键(
model/system_setting.go):topup.enabled(总开关)、topup.allow_custom(是否允许自定义金额)、topup.presets(JSON 数组:[{amount, bonus_quota}, ...])、topup.exchange_rate(默认1,1 元 = = 1 quota,仅作用于自定义金额)。- 分类常量
SystemSettingCategoryTopup = "topup"。
- 新增 HTTP 接口:
POST /api/topup/order(CreateTopupOrder,用户认证):参数{ amount, preset_amount, pay_method },前置校验开关 / 通道 / 金额,命中预设或自定义规则后写订单,复用controller/buildPayInfo拿 pay_url / qr_code。GET /api/setting/topup与PUT /api/setting/topup(GetTopupSettings/PutTopupSettings,Root 权限):读写上述 4 个 key。
- 支付回调按订单类型分发(
controller/payment.go::processNotify):order.Type == OrderTypeTopup→ 调ActivateTopupByOrder给用户加 quota;- 其余 → 调
ActivatePackageByOrder激活套餐。 MockPay(admin 测试接口)同步按 type 分发,提示文案分别显示「余额已到账」/「套餐已激活」。
前端(web/default-pro/)
- 新增可复用组件
components/TopupModal.vue:- props:
modelValue、settings、title;emits:update:modelValue、success、error、pay。 - 顶部「选择金额」chip 列表:依次渲染所有
settings.presets,最后一个 chip 固定为「自定义」(当settings.allow_custom=true时显示),点击后才显示金额输入框。 - 中部支付方式选择器(
pay-picker-item微信#07C160/ 支付宝#1677FF官方品牌色),复用 Plans.vue 既有样式。 - 底部「支付金额」+「确认充值」按钮;选中自定义但未填金额时禁用提交。
- props:
- 新增设置页
views/setting/TopupSetting.vue(root 可见):表单竖排三段:①总开关(enabled)②允许自定义金额(allow_custom)③自定义金额 1 元 = X quota(exchange_rate,默认1);快捷金额用<a-table>行行内编辑(amount/bonus_quota),支持新增 / 删除;提交前前端预校验金额重复(validateTopupPresets)。 - 新增路由
/setting/topup:router/index.js注册子路由,Setting.vue菜单新增「充值」项(root 可见,iconicon-subscribe-add)。 - 新增
api/topup.js:topupApi.createOrder({ amount, preset_amount, pay_method })。 - 扩展
api/setting.js:getTopup()/putTopup(data)。 - 新增
utils/topup.js纯函数工具:formatNumber/formatAmount/validateTopupPresets/calcCustomBonus,可在 Node 环境下用node:test单测。 - 仪表盘接入充值弹窗:
- 控制台
Dashboard.vue余额卡的「充值」按钮改为弹出TopupModal,点击前预拉/api/setting/topup+/api/payment/status,任一失败给出明确 toast;<a-modal>使用:visible+@update:visible(踩坑修复:曾误用:model-value,导致弹窗永远不显示)。 - 同时弹出二维码 / 转账信息
<a-modal,刷新余额(/api/user/self)。
- 控制台
- 订单管理表格新增「订单类型」列(
Orders.vue):套餐 / 充值 两种两色 chip(type-plan蓝 /type-topup橙),方便用户区分。 - 移除运营 Tab 内的「允许余额充值」占位开关(
OperationSetting.vue):该字段已迁移到新充值 Tab,UI 与后端GetPlanSettings/PutPlanSettings同步删除读写;DB 中旧plan.allow_topup行保留无害。 - 修复套餐界面硬编码套餐名白名单(
Plans.vue):删除VALID_PLAN_NAMES = ['lite','air','pro','max']过滤逻辑,改为显示后端返回的所有套餐(用户反映该过滤导致测试套餐不可见)。
🐛 问题修复
- 修复仪表盘充值按钮点击无反应:原
<TopupModal>在TopupModal.vue内部把 arco<a-modal>的可见性绑定到了:model-value,而 arco 的<a-modal>可见性属性是visible,导致v-model="topupModalVisible=true时弹窗永远不显示。已改为:visible="modelValue"+@update:visible="(v) => emit('update:modelValue', v)"。 - 修复充值按钮缓存导致的「管理员开启充值但仪表盘无反应」:原代码仅在
topupSettings.value为 null 时拉取一次,admin 刚开启充值但仪表盘缓存旧值时按钮不会再次请求。已改为每次点击都强制重新拉取/api/setting/topup,并把错误提示文案改成「请联系管理员在「设置-充值」中开启」。 - 修复
<a-spin>在 TopupSetting 设置页父容器宽度异常:补齐style="width: 100%"。 - 修复 TopupSetting 表单横排三列不对齐:改为
layout="vertical"三段竖排。
🚀 数据完整性 / 校验
- 快捷金额金额不能重复:后端
model.SaveTopupSettings用seenAmounts map检测重复金额,重复时返回「快捷金额重复:XX 元已存在」并整体拒绝保存;前端utils/topup.js::validateTopupPresets提前预校验,避免一次无效请求。
📚 文档
- 新增项目根目录
AGENTS.md(480 行):面向 Agent / 贡献者的开发规范速查,覆盖 TL;DR / 项目结构 / 开发环境 / 提交规范(Conventional Commits + 中英双语 body + 一文件一 commit 粒度)/ 版本号 / 命名(topup全局统一)/ 注释约定(文件级 4 行注释 + 导出符号注释 + TODO/XXX + 修改时如何更新)/ 后端规范(响应格式、model 不依赖 Gin、订单号前缀 TB/UP/TP)/ 前端规范(arco<a-modal>用:visible而非:model-value、<a-spin>必须style="width:100%"、工具函数用 node:test)/ 数据库 / 踩坑清单 / 工作流程(先 Plan 模式输出方案再编码)。 - 同步更新
README.md与 7 个语言版本(readme/README.{en,zh-TW,ja,ko,ar,de,ru}.md):新增「💰 在线充值(余额)」功能亮点章节;订单与支付章节补充订单类型列 +processNotify分发说明;开发计划「在线充值」「充值设置中心」从规划移入已完成,新增「充值退款闭环」为进行中。
🔧 重构 / 工程
- 移除运营 Tab 的
plan.allow_topup字段读写(controller/setting_payment.go):精简GetPlanSettings/PutPlanSettings返回结构,DB 行保留(不会主动删除)。 - 清理调试日志:移除为排查充值按钮不响应问题加在
Dashboard.vue::onRechargeClick的console.log。
⚠️ 升级注意事项
- 数据库迁移:新增 4 个
system_settings键(topup.enabled/topup.allow_custom/topup.presets/topup.exchange_rate),无需迁移脚本,AutoMigrate+ 首次调用会按需写入。 orders表复用:本次新增 type=2 的充值订单与既有 type=1 套餐订单存同一张表,无 DDL 变更。- 运营 Tab 「套餐运营」小节:UI 上不再有「允许余额充值(仅占位)」开关;如需启用充值,请进入设置 → 充值。
- 充值功能默认关闭(
topup.enabled未设置时为 false),管理员需到「设置 → 充值」手动开启并配置至少 1 个 preset 或允许自定义金额,前端充值按钮才会触发弹窗。 - 退款:本期不支持;充值订单
status=3仅标记,quota 不回退,待统一订单管理上线后补全。 - 前端构建:新增依赖
qrcode(已在 pnpm-lock.yaml),建议pnpm install && npm run build后发布。
English
✨ New Features
Go backend
- New online recharge module (
OrderTypeTopup=2):- Added
model/topup.godefiningTopupPreset,CreateTopupOrderInput,TopupOrderPlanInfo, plus business functionsCreateTopupOrder,ActivateTopupByOrder,ResolveTopupAmount,GetTopupSettings,SaveTopupSettings. - Reuses
GenerateOrderNo("TP")for recharge order numbers (prefixTP); writes them to the sameorderstable astype=2, avoiding an extra table. ActivateTopupByOrderis idempotent (paid orders return immediately); on activation it callsIncreaseUserQuotaand writesRecordTopupLog. ATODOmarks quota non-reversal on refund (awaiting unified admin order management).model.AutoMigratecreates the table at startup — no manual DDL.
- Added
- New recharge system-setting keys (
model/system_setting.go):topup.enabled(master switch),topup.allow_custom(allow custom amount),topup.presets(JSON array of[{amount, bonus_quota}, ...]),topup.exchange_rate(default1, i.e. 1 CNY = 1 quota; applies only to custom amounts).- Category constant
SystemSettingCategoryTopup = "topup".
- New HTTP endpoints:
POST /api/topup/order(CreateTopupOrder, user-auth): body{ amount, preset_amount, pay_method }; pre-validates the switch / channel / amount, then persists the order and reusescontroller/buildPayInfoforpay_url/qr_code.GET /api/setting/topup&PUT /api/setting/topup(GetTopupSettings/PutTopupSettings, root only): read/write the four keys above.
- Payment callback dispatched by order type (
controller/payment.go::processNotify):order.Type == OrderTypeTopup→ActivateTopupByOrderto credit quota.- Otherwise →
ActivatePackageByOrderto activate the plan. MockPay(admin test endpoint) dispatches the same way and surfaces "balance credited" / "plan activated" copy.
Frontend (web/default-pro/)
- New reusable component
components/TopupModal.vue:- Props:
modelValue,settings,title; emits:update:modelValue,success,error,pay. - Top "Choose amount" chip row: renders all
settings.presetsin order; the last chip is always "Custom" whensettings.allow_custom=true. The amount input is shown only after clicking Custom. - Payment-method picker (
pay-picker-item) with WeChat (#07C160) and Alipay (#1677FF) brand colors, reusing the styles fromPlans.vue. - Footer shows "Pay amount" + "Confirm recharge"; submission is disabled when Custom is selected but no amount is entered.
- Props:
- New settings view
views/setting/TopupSetting.vue(root-only): vertical layout with three sections — ① master switch (enabled), ② allow custom amount (allow_custom), ③ custom-amount 1 CNY = X quota (exchange_rate, default1); preset rows use an<a-table>with inline-editableamount/bonus_quotacolumns, plus add / delete buttons. Front-end pre-validates duplicate amounts viavalidateTopupPresetsbefore submission. - New route
/setting/topup: registered inrouter/index.js; sidebar menu entry "充值" added inSetting.vue(root-only, iconicon-subscribe-add). - New
api/topup.js:topupApi.createOrder({ amount, preset_amount, pay_method }). - Extended
api/setting.js:getTopup()/putTopup(data). - New
utils/topup.jspure-function helpers:formatNumber,formatAmount,validateTopupPresets,calcCustomBonus— usable in Node.js vianode:test. - Dashboard recharge button wired up:
Dashboard.vuenow opensTopupModalfrom the "Recharge" button on the balance card, prefetching/api/setting/topup+/api/payment/statusto give precise error copy when either is missing. The underlying<a-modal>uses:visible+@update:visible(bug fix: the previous:model-valuebinding never showed the dialog because Arco's<a-modal>prop isvisible).- Also opens a QR-code / bank-transfer modal and refreshes the balance via
/api/user/self.
- Order-management table gets an "Order type" column (
Orders.vue): two-color chips for "Plan" / "Recharge" (type-planblue,type-topuporange). - Removed the "Allow balance recharge" placeholder switch from the Operations tab (
OperationSetting.vue): that field has migrated to the new Top-up tab; the UI and backendGetPlanSettings/PutPlanSettingsno longer read or write it. The DB row is kept for backwards compatibility. - Removed the hard-coded plan-name whitelist from
Plans.vue: theVALID_PLAN_NAMES = ['lite','air','pro','max']filter was hiding test plans reported by the user; we now render ...
v0.0.10
v0.0.10 — Docker 镜像发布能力
新增官方 Docker 镜像构建与发布能力:每次推送形如
v*.*.*的 tag 时,自动构建并推送多架构(linux/amd64 + linux/arm64)镜像到 GitHub Container Registry;提供docker run一键部署、约定挂载目录/app/config+/app/data、自动加载.env文件,并支持通过 CLI 参数自定义启动行为。
中文
✨ 新增功能
官方 Docker 镜像
- 新增根目录
Dockerfile,采用三阶段多阶段构建:- 阶段一使用
node:20-alpine构建web/default-pro前端; - 阶段二使用
golang:1.22-alpine,CGO_ENABLED=0静态编译 Go 二进制(项目使用glebarez/sqlite纯 Go 实现,无需 CGO),并自动下载go.mod声明的 1.25 toolchain; - 阶段三以
alpine:latest为运行时基础镜像,仅安装ca-certificates、tzdata、wget,体积小、安全性高。
- 阶段一使用
- 默认以非 root 用户
app运行,内置/api/statusHTTP 健康检查,运行时声明VOLUME [/app/config, /app/data]。 - 默认环境变量:
PORT=3000、LOG_DIR=/app/data/logs、SQLITE_PATH=/app/data/one-api-pro.db、CONFIG_DIR=/app/config。
智能启动入口(docker-entrypoint.sh)
- 新增
docker-entrypoint.sh,作为容器ENTRYPOINT,提供三项能力:- 自动加载
$CONFIG_DIR/.env:容器启动时若检测到该文件,自动作为--env <path>参数传入 one-api-pro,用户挂载.env即可生效,无需修改docker run命令; - 透传用户 CLI 参数:
docker run image --port 8080 --log-dir /xxx这类参数会被原样转发到 one-api-pro; - 调试模式直通:当 CMD 首参既不是
one-api-pro也不是其绝对路径时(例如docker run image bash),入口直接exec透传给用户命令,方便进 shell 排查。
- 自动加载
- 使用
exec替换当前进程,保证SIGTERM等信号正确传递到 one-api-pro。
🔧 工程 / CI
- 新增
.github/workflows/release-docker.yml,与现有release.yml(二进制发布)解耦:- 触发条件:
push推送形如v*.*.*的 tag,或手动workflow_dispatch(支持手动指定 tag); - 强制校验
CHANGELOG/<tag>.md存在,与release.yml保持一致的发布流程; - 通过
docker/setup-qemu-action+docker/setup-buildx-action启用多架构构建,linux/amd64+linux/arm64并行出图; - 使用
docker/metadata-action自动生成 semver tag(:0.0.10、:0.0、:0),latest=auto策略自动跳过预发布后缀(-rc/-beta等); - 注入 OCI 元数据:
org.opencontainers.image.{title,description,source,licenses,revision,created}; - 启用 GHA 层缓存(
cache-from: type=gha/cache-to: type=gha,mode=max),后续构建秒级复用; - 通过
${{ secrets.GITHUB_TOKEN }}直接登录ghcr.io(无需额外配置 PAT); - 构建结束后调用
gh apiPATCH/users/{owner}/packages/container/{repo}或/orgs/{owner}/packages/container/{repo},自动把包设为public,continue-on-error: true保证权限受限时不影响镜像推送。
- 触发条件:
- 新增
.dockerignore:与.gitignore对齐,排除.git、.github、node_modules、dist、logs、*.db、web/air、web/berry、web/default等已废弃主题与构建产物。
📚 文档
- 在
README.md新增「🐳 Docker 部署」章节,位于「手动部署」与「多机部署」之间,包含:- 镜像地址表(
latest/ 指定版本 / 大版本); - 挂载目录约定(
/app/config配置 +/app/data数据); - 快速开始(SQLite 单文件)、
.env配置示例; - 切换 MySQL / PostgreSQL(
SQL_DSN)、修改端口的三种等价方式(-e/.env/ CLI 参数); - 全部 CLI 参数表(
--port、--log-dir、--env、--version、--help); docker-compose.yml完整示例、调试模式(进 shell)、升级流程。
- 镜像地址表(
⚠️ 升级注意事项
- 本次为纯工程 / CI / 文档变更,无后端代码改动,无数据库迁移,可直接升级。
- 现有二进制用户完全不受影响:未升级到 Docker 部署方式的二进制部署流程与之前一致。
- 首次发布 Docker 镜像后请到 GitHub 仓库的 Packages 页面确认包已设为
public(CI 会自动尝试,少数组织仓库可能需要管理员手动确认)。 - 若使用 docker-compose,请使用
docker compose pull && docker compose up -d拉取新镜像并重启。
English
✨ New Features
Official Docker image
- Added a root-level
Dockerfileusing a three-stage multi-stage build:- Stage 1 uses
node:20-alpineto build theweb/default-profrontend; - Stage 2 uses
golang:1.22-alpine, builds the Go binary statically withCGO_ENABLED=0(the project uses the pure-Goglebarez/sqlite, so no CGO toolchain is needed), and auto-downloads thego.mod-declared 1.25 toolchain viaGOTOOLCHAIN=auto; - Stage 3 uses
alpine:latestas the runtime base, installing onlyca-certificates,tzdata, andwgetfor a small, secure footprint.
- Stage 1 uses
- Runs as non-root user
appby default, ships with an HTTP health check against/api/status, and declaresVOLUME [/app/config, /app/data]. - Default environment variables:
PORT=3000,LOG_DIR=/app/data/logs,SQLITE_PATH=/app/data/one-api-pro.db,CONFIG_DIR=/app/config.
Smart entrypoint (docker-entrypoint.sh)
- New
docker-entrypoint.shserves as the containerENTRYPOINTand provides three behaviors:- Auto-load
$CONFIG_DIR/.env: if the file exists at startup it is forwarded to one-api-pro as--env <path>, so users can mount a.envand have it take effect without touching thedocker runcommand; - CLI argument passthrough: arguments such as
docker run image --port 8080 --log-dir /xxxare forwarded verbatim to one-api-pro; - Debug passthrough: when the CMD's first argument is neither
one-api-pronor its absolute path (e.g.docker run image bash), the entrypointexecs the user-provided command directly so it is easy to drop into a shell.
- Auto-load
- Uses
execto replace the current process so that signals such asSIGTERMare correctly delivered to one-api-pro.
🔧 Tooling / CI
- Added
.github/workflows/release-docker.yml, decoupled from the existingrelease.yml(binary release):- Triggers:
pushof any tag matchingv*.*.*, or manualworkflow_dispatch(with optional tag input); - Enforces that
CHANGELOG/<tag>.mdexists, keeping the release process consistent withrelease.yml; - Enables multi-arch builds via
docker/setup-qemu-action+docker/setup-buildx-action, producinglinux/amd64andlinux/arm64images in parallel; - Uses
docker/metadata-actionto auto-generate semver tags (:0.0.10,:0.0,:0); thelatest=autoflavor skips pre-release suffixes (e.g.-rc,-beta); - Injects OCI metadata:
org.opencontainers.image.{title,description,source,licenses,revision,created}; - Enables GHA layer caching (
cache-from: type=gha/cache-to: type=gha,mode=max) so subsequent builds reuse cached layers in seconds; - Logs in to
ghcr.iodirectly with${{ secrets.GITHUB_TOKEN }}— no extra PAT needed; - After build/push, calls
gh apitoPATCHeither/users/{owner}/packages/container/{repo}or/orgs/{owner}/packages/container/{repo}to flip the package visibility topublic;continue-on-error: trueensures that a permission failure never blocks the image push.
- Triggers:
- Added
.dockerignore: aligned with.gitignoreto exclude.git,.github,node_modules,dist,logs,*.db, the deprecatedweb/air/web/berry/web/defaultthemes, and other build artifacts.
📚 Documentation
- Added a new "🐳 Docker 部署" section to
README.md, placed between "手动部署" and "多机部署", covering:- Image address reference (
latest/ pinned version / major); - Mount-point convention (
/app/configfor config,/app/datafor data); - Quick start (SQLite single-file) and
.envconfiguration example; - Switching to MySQL / PostgreSQL via
SQL_DSN, and three equivalent ways to change the listen port (-e/.env/ CLI arg); - A full CLI argument table (
--port,--log-dir,--env,--version,--help); - A complete
docker-compose.ymlexample, debug-mode entry, and upgrade workflow.
- Image address reference (
⚠️ Upgrade Notes
- This is a purely engineering / CI / documentation release — no backend code changes, no database migration, safe to upgrade.
- Existing binary users are completely unaffected: the binary deployment flow is unchanged.
- After the first Docker image is published, please verify on the GitHub repo's Packages page that the package is
public(the CI attempts this automatically; some organization repositories may require a maintainer to confirm). - docker-compose users should run
docker compose pull && docker compose up -dto pull the new image and restart.
v0.0.9
v0.0.9 — 套餐订阅支付流程全面优化
重构「套餐 → 下单 → 支付 → 兑换」整条链路:套餐界面支持微信/支付宝切换并展示官方品牌色图标,订单界面不再跳转而是选择支付方式直接发起支付,兑换成功后以绿色内联提示替代弹窗;同时后端新增支付通道状态查询与「重新支付已有订单」接口,并以
pay.status为前端提供明确的下单结果反馈,未开通支付通道时不再错误生成订单。
中文
✨ 新增功能
后端(/api)
- 新增公开接口
GET /api/payment/status:返回所有支付通道(微信 / 支付宝 / 银行)的启用状态与any_enabled汇总,前端据此判断是否展示购买入口或直接提示管理员先配置支付。 - 新增接口
POST /api/order/self/:id/pay:用户可对自己名下的未支付订单重新发起支付,并可通过pay_method切换支付方式;返回结构复用下单接口,前端可直接接入二维码/转账弹窗。 CreatePlanOrder/PayMyOrder返回的pay对象新增status字段:取值success(二维码 / 转账说明已就绪)或warning(通道未注册 / 未启用 / SDK 调用失败,附warning文案),供前端明确区分「可以支付」与「配置有问题」,不再靠pay_url是否为空来猜测。- 未开通任何支付通道时不再创建订单:下单 / 重新支付请求会直接返回「系统尚未开通任何支付通道,请设置后开启支付」,订单不落库,避免订单列表出现无效记录。
- 抽取
buildPayInfo()辅助函数,统一CreatePlanOrder与PayMyOrder的支付参数拼装逻辑,消除重复代码。
套餐界面(Plans)
- 点击「立即订阅 / 升级」前预检支付通道:未启用任何通道时弹出 toast 提示且不打开确认弹窗,杜绝「确认后才发现无法支付」的体验。
- 购买 / 升级确认弹窗内新增支付方式选择器:微信支付(
IconWechatpay)与支付宝支付(IconAlipayCircle),使用各自官方品牌色(微信#07C160、支付宝#1677FF),用户可点击切换,点击「确认购买 / 确认升级」时携带所选pay_method提交。 - 支付弹窗随所选方式动态切换:标题与提示文案(「微信扫码支付 / 支付宝扫码支付」)跟随用户选择变化。
- 根据
pay.status驱动行为:success时展示二维码弹窗(或银行转账说明);warning时仅弹出错误 toast,不再跳转到订单列表页。
订单界面(Orders)
- 「支付」按钮不再跳转套餐页:改为弹出「选择支付方式」弹窗,选中后调用
POST /api/order/self/:id/pay对既有未支付订单直接发起支付。 - 支付方式选择器改用 Arco 官方品牌图标:微信 / 支付宝图标以官方色渲染,选中项高亮描边并显示同色勾选。
- 根据
pay.status驱动展示:success时展示二维码或转账信息弹窗;warning时弹出错误提示,不再出现误导性的「跳转支付中」文案或强制跳转。
兑换界面(Redeem)
- 移除「兑换成功」弹窗:原弹窗含「前往套餐」按钮,现改为在输入框下方以绿色内联文本展示成功提示(与错误提示同构),兑换成功后立即重新拉取「当前额度 / 已使用」数据,额度数字动态递增并带轻微放大动效。
🐛 问题修复
- 修复套餐界面支付图标不显示:
Plans.vue未显式importIconWechatpay/IconAlipayCircle,Arco 的 SVG 图标不会像组件库那样全局注册,导致icon-wechatpay/icon-alipay-circle渲染为空白;现已显式导入。 - 修复支付方式勾选符号位于右上角:将勾选符号从
position: absolute角落定位改为 flex 垂直居中,与按钮内图标、名称对齐。 - 修复「无支付通道」时错误生成订单并跳转订单页:此前即使未配置任何支付通道也会创建订单并跳转,现改为后端拒绝创建、前端仅弹错误提示。
📚 文档
- 为多语言 README 补充在线 Demo 链接(英 / 繁中 / 日 / 俄 / 韩 / 阿 / 德)。
⚠️ 升级注意事项
- 本次为代码 / 前端改动,无数据库迁移,可直接升级。
- 前端已新增依赖(
@arco-design/web-vue/es/icon品牌图标),建议重新执行npm install && npm run build后发布。 - 若希望用户可自助「选择支付方式 + 重新支付已有订单」,请确保后台「系统设置 - 支付」中至少启用一个在线支付通道(微信 / 支付宝);否则套餐页会直接提示需先开通支付。
English
✨ New Features
Backend (/api)
- Added public endpoint
GET /api/payment/status: returns the enabled state of every payment channel (WeChat / Alipay / Bank) plus anany_enabledsummary so the frontend can decide whether to show the purchase flow or prompt the admin to configure payments first. - Added endpoint
POST /api/order/self/:id/pay: lets a user re-invoke payment for one of their own pending orders, optionally switching thepay_method; the response reuses the order-creation shape so the frontend can reuse the same QR / transfer modal. - Added a
pay.statusfield toCreatePlanOrder/PayMyOrderresponses: values aresuccess(QR / transfer note is ready) orwarning(channel not registered / disabled / SDK failure, with awarningmessage). The frontend can now tell "ready to pay" from "misconfigured" instead of guessing based on whetherpay_urlis empty. - No order is created when no payment channel is enabled: order / re-pay requests now return "No payment channel has been enabled yet" and the row is not persisted, keeping the order list clean.
- Extracted a
buildPayInfo()helper that unifies payment-parameter assembly for bothCreatePlanOrderandPayMyOrder, removing duplicated code.
Plans page
- Pre-flight payment check before opening the subscribe / upgrade flow: if no channel is enabled, a toast is shown and the confirm modal never opens — the user is never led to discover "can't pay" after confirming.
- Added a payment-method picker inside the subscribe / upgrade confirm modals: WeChat Pay (
IconWechatpay) and Alipay (IconAlipayCircle), each rendered in its official brand color (WeChat#07C160, Alipay#1677FF). Users can switch and the chosenpay_methodis sent on "Confirm". - The payment modal adapts to the chosen method: title and tip ("WeChat QR pay / Alipay QR pay") follow the user's selection.
- Driven by
pay.status: onsuccessit shows the QR (or bank-transfer note) modal; onwarningit only shows an error toast — no more auto-redirect to the orders list.
Orders page
- The "Pay" button no longer redirects to the plans page: it now opens a "Choose payment method" modal; after selection it calls
POST /api/order/self/:id/payto initiate payment for the existing pending order directly. - Payment picker uses Arco official brand icons: WeChat / Alipay icons are rendered in official colors, and the selected option is highlighted with a matching colored outline and check mark.
- Driven by
pay.status: onsuccessit shows the QR or transfer-info modal; onwarningit shows an error — the misleading "redirecting to pay…" message and forced navigation are gone.
Redeem page
- Removed the "Redeemed!" modal: the former modal (with a "View Plans" button) is replaced by a green inline success message below the input (mirroring the error style). After redeeming, the "Current Quota / Used" data is re-fetched immediately so the number visibly increases with a subtle scale animation.
🐛 Bug Fixes
- Fixed the payment icons not rendering on the Plans page:
Plans.vuewas not importingIconWechatpay/IconAlipayCircle, and Arco SVG icons are not globally registered like the component library, so<icon-wechatpay>/<icon-alipay-circle>rendered blank; the icons are now explicitly imported. - Fixed the check mark sitting in the top-right corner: it was absolutely positioned; it now uses flexbox to center vertically, aligned with the icon and label inside the option.
- Fixed wrongly creating an order and redirecting when no channel is enabled: previously the backend would still create an order and the frontend would navigate away; now the backend refuses to create it and the frontend only shows an error toast.
📚 Documentation
- Added online demo links to the multi-language READMEs (English / Traditional Chinese / Japanese / Russian / Korean / Arabic / German).
⚠️ Upgrade Notes
- Code/frontend-only release — no database migration; safe to upgrade in place.
- New front-end dependency on Arco brand icons (
@arco-design/web-vue/es/icon); please re-runnpm install && npm run buildbefore publishing. - To let users self-serve "pick a payment method and re-pay an existing order", make sure at least one online channel (WeChat / Alipay) is enabled under System Settings → Payment; otherwise the Plans page will prompt that payment must be configured first.
v0.0.8
v0.0.8 — 安全加固与登录页改版
修复 3 个可导致提权 / 凭据泄露的安全问题(指定渠道越权、用户列表暴露 access_token),并全面改版登录注册页:新增《服务条款》《隐私政策》页面、注册需同意条款、补充无障碍支持;同时把 README 国际化扩展至 8 种语言,并加入"启动时校验内嵌主题"的防御性检查,避免升级后因数据库残留旧主题字段导致后台空白页。
中文
🔒 安全
- 修复指定渠道越权漏洞(one-api#2410):此前任意已登录用户均可通过
/v1/oneapi/proxy/:channelid/*target指定任意上游渠道,绕过分组隔离与模型白名单并触发上游凭据转发。现在该 URL 参数路径与 token 后缀路径一样,仅管理员可用。 - 指定渠道时重新校验分组 / 模型白名单(one-api#2410):即使管理员使用指定渠道功能,转发前也会再次确认该渠道确实服务于当前用户分组与所请求模型,作为纵深防御。
- 修复用户列表接口泄露 access_token(one-api#2425):管理员批量用户接口(
GET /api/user/、GET /api/user/search)不再返回用户的 access_token,避免被重放后提权为 root(现有单用户读取路径的保护保持一致)。 - 为上述修复补充模型层辅助方法
Channel.ContainsGroup/Channel.ContainsModel及相应回归测试。
✨ 新增功能
- 登录 / 注册 / 找回密码 / 重置密码四个鉴权页全面改版:抽出新的
AuthLayout(带装饰性渐变光晕背景与品牌位 Logo),统一语义化标题(h1+ 副标题)与表单无障碍aria-label;forgot-link/form-alert等局部样式整理后视觉与交互更一致。 - 新增 《服务条款》页面(
/terms) 与 《隐私政策》页面(/privacy),使用新的LegalLayout(顶部导航 + 法律文件版式)排版;路由免登录即可访问,并已在router/index.js的白名单中放行,避免跳转登录页。 - 注册流程新增「我同意《服务条款》和《隐私政策》」勾选(带
aria-label),未勾选不可提交注册,并在条款文案上提供/terms、/privacy直达链接。 - 套餐页新增空状态:未配置任何套餐时展示友好的插画 + 提示文案("暂无可用套餐 / 请联系管理员配置套餐后再来查看"),替代空白网格。
- 新增
Channel.ContainsGroup/Channel.ContainsModel白名单辅助方法(按,拆分、精确匹配,空配置视为全部放行),为下游的鉴权与重校验逻辑提供复用基础。 - 启动时校验前端主题:新增
common.ValidateEmbeddedTheme(buildFS, themesRoot, theme)与common.ListEmbeddedThemes(buildFS, themesRoot)辅助函数;main.go在using theme <name>日志之后调用校验器,如果当前主题未被打入二进制,会输出[ERROR]日志并指出web/build/<theme>/index.html缺失、列出实际已内嵌的全部主题,并给出三种修复建议(修改options.theme/ 调整THEME环境变量 / 重新打包时把期望主题加入web/THEMES)。该检查非致命,服务器仍会启动以便运维修复,但能立刻把"后台空白"问题暴露在日志里。
🐛 问题修复
- 修复订单页多了一层
page-container内边距导致与其它页面边距不一致的问题。 - 修复普通用户可绕过分组隔离 / 模型白名单指定上游渠道的安全问题(详见「安全」)。
- 修复管理员用户列表 / 搜索接口可能暴露 access_token 的安全问题(详见「安全」)。
router/web.go不再静默吞掉web/build/<theme>/index.html读取失败的错误,改为通过logger.SysError打印明确错误("theme %q is not embedded ...")作为兜底,确保即使绕过启动检查也能在路由层看到失败原因。
📚 文档
- README 国际化扩展至 8 种语言:新增德语、阿拉伯语、韩语、俄语、日语、繁体中文 README,主 README 与英文 README 的语言导航同步更新。
🔧 工程 / CI
- 补充三组回归测试:中间件指定渠道鉴权(
middleware/auth_test.go)、用户列表access_token脱敏(controller/user_test.go)、模型白名单辅助方法(model/channel_contains_test.go)。 - 为内嵌主题校验辅助函数补充 5 个单元测试(
common/embed_theme_test.go,含embed_theme_testdata/测试资源),覆盖:列出主题、主题存在、主题缺失、空主题、根路径带尾斜杠容错。
⚠️ 升级注意事项
- 本次为代码与前端改动,无数据库迁移,可直接升级。
- 登录 / 注册 / 找回密码页面已改版,建议升级后清除浏览器缓存或以无痕模式验证。
- 若你在开放注册场景下依赖「匿名指定渠道代理」能力,请注意该能力现已被移除:仅管理员可通过 URL 参数或 token 后缀指定渠道。
- 数据库残留的旧
options.theme会被新校验器捕获:如果你从较早版本(例如内置default主题的旧 one-api / one-api-pro)升级,启动日志中若出现theme "default" is not embedded in this binary ...的[ERROR],说明 MySQLoptions表里仍保留着旧的theme=default记录。请执行UPDATE options SET value='default-pro' WHERE \key`='theme';`(或直接删除该行)后重启即可。
English
🔒 Security
- Fixed privilege escalation via URL-parameter channel pinning (one-api#2410): any authenticated user could previously call
/v1/oneapi/proxy/:channelid/*targetto pin an arbitrary upstream channel, bypassing group isolation and per-channel model allowlists while triggering upstream-credential forwarding. The URL-parameter path now requires an admin, matching the token-suffix path. - Re-validate group / model allowlist on pinned channels (one-api#2410): even when an admin pins a channel, the distributor now re-checks that the channel actually serves the caller's user group and requested model before forwarding — defense in depth.
- Stopped leaking
access_tokenin admin user APIs (one-api#2425): the admin batch endpoints (GET /api/user/,GET /api/user/search) no longer return users' access_tokens, preventing token replay that could escalate to root (consistent with the existing single-user read path). - Added
Channel.ContainsGroup/Channel.ContainsModelmodel helpers and regression tests covering all of the above.
✨ New Features
- Reworked all four auth pages — Login, Register, Password Reset, and Password Reset Confirm — around a new
AuthLayout(decorative gradient-orb background + branded logo slot), unified semantic headings (h1+ subtitle) and formaria-labels; tidied upforgot-link/form-alertstyles so all four pages look and behave consistently. - Added Terms of Service (
/terms) and Privacy Policy (/privacy) pages rendered via a newLegalLayout(top nav + legal document layout). Both routes are whitelisted in the router so visitors can reach them without first logging in. - Registration now requires checking "I agree to the Terms of Service and Privacy Policy" (with
aria-label) before submitting, and the copy includes direct/terms//privacylinks. - Added an empty state to the Plans page so users see a friendly illustration + hint ("暂无可用套餐 / 请联系管理员配置套餐后再来查看") instead of a blank grid when no plans are configured.
- Added
Channel.ContainsGroup/Channel.ContainsModelallowlist helpers (comma-split, exact match; empty config allows all), providing the reusable building block for downstream auth and re-validation logic. - Embedded-theme check at startup: added
common.ValidateEmbeddedTheme(buildFS, themesRoot, theme)andcommon.ListEmbeddedThemes(buildFS, themesRoot)helpers.main.goinvokes the validator right after loggingusing theme <name>; if the configured theme is not embedded in the binary, it emits a loud[ERROR]that names the missingweb/build/<theme>/index.html, lists every theme that IS embedded, and points at three fixes (updateoptions.theme/ adjust theTHEMEenv / add the theme toweb/THEMESand rebuild). The check is non-fatal — the server still starts so the operator can fix the misconfiguration — but it puts the "blank admin page" failure mode straight into the log.
🐛 Bug Fixes
- Removed an extra
page-containerpadding in the Orders page so its margins match the other pages. - Fixed the security issue where ordinary users could bypass group isolation / model allowlists by pinning an upstream channel (see Security).
- Fixed the security issue where admin user list / search responses could expose
access_token(see Security). router/web.gono longer silently swallows the error from readingweb/build/<theme>/index.html; it now logstheme %q is not embedded ...vialogger.SysErroras a safety net, so the failure is still visible even ifSetWebRouteris ever reached before the startup check (tests, future refactors).
📚 Documentation
- README i18n now covers 8 languages: added German, Arabic, Korean, Russian, Japanese and Traditional Chinese READMEs, and updated the language navigation in both the main and English READMEs.
🔧 Tooling / CI
- Added three groups of regression tests: middleware channel-pinning auth (
middleware/auth_test.go), user-listaccess_tokenredaction (controller/user_test.go), and model allowlist helpers (model/channel_contains_test.go). - Added 5 unit tests for the embedded-theme validator (
common/embed_theme_test.go, with synthetic fixtures incommon/embed_theme_testdata/): list themes, theme present, theme missing, empty theme, and trailing-slash tolerance on the themes root.
⚠️ Upgrade Notes
- Code/frontend-only release — no database migration; safe to upgrade in place.
- Login / register / password-reset pages have been redesigned; please clear the browser cache or verify in an incognito window after upgrading.
- If you relied on anonymous "pin channel by URL parameter" proxying in open-registration deployments, note that this is no longer allowed: only admins can pin a channel (via URL parameter or token suffix).
- Stale
options.themefrom older releases will now be caught: when upgrading from an older one-api / one-api-pro that shipped the legacydefaulttheme, you may see[ERROR] theme "default" is not embedded in this binary ...at startup — that means the MySQLoptionstable still holds the oldtheme=defaultrow, which overrides the env default. RunUPDATE options SET value='default-pro' WHERE \key`='theme';` (or delete the row) and restart.