Skip to content

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_price table. 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 把后端返回字符串拆回数组。
    • 当前表单里已存在的模型名会自动合入候选,保留历史「野模型」可见性。
  • 渠道编辑弹窗:新增「模型重定向」输入框(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 改为 arco a-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 横排布局供「永不过期」复用。

🐛 问题修复 ​

  • 修复新建令牌后弹「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-checkbox flex 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 / GroupPrice Insert 强制清零 Id 兜底行为(model/model_price_test.go):新增单测覆盖两条 Insert 路径在 Id != 0 时被清零的兜底。
  • AddModelPrice / AddGroupPrice HTTP 层 Id 清零端到端路径(controller/model_price_test.go):seed 一个 id=9999 的预存行,再用同样的 id=9999 提交新增,断言:旧行未被覆盖,新行拿到自增 id 而不是 9999。
  • user_test TestMain 迁移 ModelPrice / GroupPrice 表(controller/user_test.go):让 ModelPrice 与 GroupPrice 在 in-memory SQLite 里参与 AutoMigrate,供定价单测复用。
  • ListModelPriceOptions enabled 过滤与空名兜底(controller/model_price_test.go):seed 2 条 enabled + 1 条 disabled(用 raw SQL 绕开 GORM default:true;not null 对显式 false 的覆盖),断言只有 enabled 行出现在响应里;另一条 seed 空 model_name 断言响应里不出现空字符串。

⚠️ 升级注意事项 ​

  • 零数据库迁移:本次不涉及任何表结构变更。
  • 需要重新构建前端:web/default-pro/ 的修改全部需要 npm run build 后重新 //go:embed 进二进制,升级时记得打包新前端。
  • 后端二进制必须重启:仅新增 ListModelPriceOptions handler 与对应路由,运行中的旧进程不会暴露 /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)。
  • 构建与测试:
    • go build ./... 通过。
    • go test ./controller/ ./model/ ./middleware/ 全绿(新增 4 个定价 / 模型选项相关断言)。
    • cd web/default-pro && npm run build 通过。