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通过。