Repository navigation
v0.0.20
·
1041 commits
to master
since this release
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 vertically (
Channel.vue): wrapped in a.form-stackflex column; new.form-hint-blockmodifier overrides.form-hint'smargin-leftin favour ofmargin-top, leaving other inline hints (key / fallback / fallback_priority) untouched.
🎨 Style
- Date-picker suffix icon removed (
Token.vue): passing<template #suffix-icon></template>overrides arco's defaultIconCalendar, aligning the picker width with the<a-input-number>on the remain_quota row. - Model Redirect hint uses
pre-wrap+break-all(Channel.vue): long JSON sample text wraps naturally instead of being truncated to a single line.
🧪 Tests
ModelPrice/GroupPriceInsert Id-zeroing fallback (model/model_price_test.go): assertsInsert()strips a non-zeroIdto avoid primary-key collisions.AddModelPrice/AddGroupPriceHTTP-layer Id-zeroing end-to-end (controller/model_price_test.go): seeds a pre-existing row atid=9999, then submits an "add" withid=9999; asserts the old row is intact and the new row gets an auto-incremented id rather than 9999.user_testTestMain migrated to include ModelPrice / GroupPrice tables (controller/user_test.go): the in-memory SQLite now auto-migratesModelPriceandGroupPriceso the pricing tests share the same harness.ListModelPriceOptionsenabled-filter + empty-name guard (controller/model_price_test.go): seeds 2 enabled + 1 disabled row (raw SQL bypasses GORM'sdefault:true;not nullcoercion on explicitfalse); asserts only enabled rows are returned and an emptymodel_namenever leaks into the payload.
⚠️ Upgrade Notes
- Zero database migration: no schema changes.
- Frontend rebuild required: all changes under
web/default-pro/requirenpm run buildso the new assets are re-embedded into the Go binary via//go:embed. - Backend binary restart required: the new
ListModelPriceOptionshandler and its route only become available after the new binary is running. - Behavior changes:
- The channel modal's "Model" dropdown source is switched from the compile-time catalogue to the runtime
model_pricetable. Historical channels that have saved models absent frommodel_price(e.g.gpt-4-0314,text-davinci-003) will not see those options in the dropdown — but theform.modelsunion fallback keeps the saved selection visible when re-opening the edit dialog, so the save path remains intact. To bring a historical model into the dropdown, add a row in Pricing with the correspondingmodel_name. - The channel modal's "Groups" field is now a dropdown sourced from
/api/group/(defaults todefault/vip/svipfromgroup_prices). Add agroup_pricesrow to expose additional groups. - The token modal's "Never Expire" and "Unlimited Quota" checkboxes default to unchecked; editing an existing token auto-checks them when the backend value is
0/-1(never_expire = !exp,unlimited_quota = !!record.unlimited_quota).
- The channel modal's "Model" dropdown source is switched from the compile-time catalogue to the runtime
- Build & test:
go build ./...passes.go test ./controller/ ./model/ ./middleware/is green (4 new pricing / model-option assertions).cd web/default-pro && npm run buildpasses.