fix(channel):修复渠道权重被设置为0的问题
This commit is contained in:
1 parent
0e8280e42c
commit
85ccf083d2
5 files changed
+32
-13
No files matched your search
@@ -83,8 +83,8 @@ type ChannelUpdateReq struct {
|
||||
Type *int `json:"type" v:"min:1#请选择供应商类型" dc:"供应商类型(留空不修改)"`
|
||||
BaseURL string `json:"base_url" dc:"API 基础地址"`
|
||||
ApiKey *string `json:"api_key" dc:"更新 API Key(留空不更新)"`
|
||||
Priority int `json:"priority" dc:"优先级"`
|
||||
Weight int `json:"weight" dc:"权重"`
|
||||
Priority *int `json:"priority" dc:"优先级(留空不更新)"`
|
||||
Weight *int `json:"weight" dc:"权重(留空不更新)"`
|
||||
MaxConcurrency *int `json:"max_concurrency" v:"min:0" dc:"最大并发请求数(0=按上游 429 水位自动估算,留空不更新)"`
|
||||
TestModel string `json:"test_model" dc:"测试模型名"`
|
||||
Remark string `json:"remark" dc:"备注"`
|
||||
|
||||
@@ -699,3 +699,23 @@ err := g.DB().Transaction(dbCtx, func(txCtx context.Context, tx gdb.TX) error {
|
||||
| 长流水线编排(relay handler) | 入口与各昂贵阶段前 `ctx.Err()` fail-fast;下游报错先判 `errors.Is(err, context.Canceled)`,勿映射为业务错误("无可用渠道"/"余额不足"等) |
|
||||
|
||||
排查信号:同一 request_id 在日志里留下一串不同模块、不同语义的报错,尾部都是 `context canceled`——说明缺 fail-fast 入口闸门,且各模块把取消误当成了自己领域的故障。
|
||||
|
||||
### 2026-08-14(五):更新接口非指针 int 字段被零值无条件覆写(渠道 weight/priority 被洗成 0)
|
||||
|
||||
**问题**:渠道创建后数据库 `chn_channels.weight` 是 0,但创建表单明明提交了 100。前端"仅切换状态"或"仅更新 API Key"的 PUT 请求会把已有渠道的 weight/priority 洗成 0。
|
||||
|
||||
**原因**:`ChannelUpdateReq.Weight/Priority` 是非指针 `int`,JSON 里不带该字段时解析为 Go 零值 0,无法区分"未提交"与"显式提交 0";而 `UpdateChannel` 里 `data.Weight = req.Weight` 无条件写入,`do` 结构体的非 nil 字段(含 0)都会进 UPDATE SET 列表,于是 0 覆盖了库里的 100。前端曾在多处用"提交时带上当前 weight"打补丁,但详情页的状态切换/Key 更新漏带,且编辑表单 `weight || 100` 的 falsy 兜底把数据损坏掩盖成"显示正常"。
|
||||
|
||||
**修复**:
|
||||
|
||||
1. `ChannelUpdateReq.Weight/Priority` 改为 `*int`(与同结构体 `MaxConcurrency`、`Type` 等"留空不更新"字段风格一致),Logic 里仅在非 nil 时赋值;
|
||||
2. 删除前端"必须携带当前 priority/weight"的 workaround(ChannelsPage 的状态切换/行内权重/行内层级三处),只提交变更字段;
|
||||
3. 详情页编辑表单去掉 `detail.weight || 100` 兜底,显示真实库值,避免再次掩盖数据异常。
|
||||
|
||||
**正确做法(通用规则)**:
|
||||
|
||||
- **更新接口的"可选更新"字段一律用指针类型**(`*int`/`*string`/`*bool`),`nil` = 不更新,非 nil(含零值)= 显式更新;非指针字段意味着"调用方必须每次全量携带",要在 API 注释里写明并让所有前端调用点遵守。
|
||||
- **do 结构体字段是 `interface{}`**:nil 才会被 ORM 过滤,非 nil 的 0/""/false 都会写入——"零值是否合法的业务值"必须在 Req 层用指针区分,不能指望 ORM 帮你跳过。
|
||||
- **前端编辑表单慎用 `x || 默认值` 兜底**:对数值字段,0 会被 falsy 规则吞掉,既掩盖真实数据,又可能在保存时把合法的 0 改写为默认值。
|
||||
|
||||
排查信号:某字段在库里莫名变成 0/空串,而创建时明明写过合法值——查该表所有 UPDATE 路径中是否存在非指针字段的无条件赋值。
|
||||
@@ -347,8 +347,14 @@ func (s *sAdmin) UpdateChannel(ctx context.Context, req *v1.ChannelUpdateReq) (*
|
||||
if req.BaseURL != "" {
|
||||
data.BaseUrl = req.BaseURL
|
||||
}
|
||||
data.Priority = req.Priority
|
||||
data.Weight = req.Weight
|
||||
// priority/weight 为指针字段,仅在显式传入时更新,避免不含这两个字段的
|
||||
// 局部更新(如仅切换状态、仅换 Key)把已有值洗成 0。
|
||||
if req.Priority != nil {
|
||||
data.Priority = *req.Priority
|
||||
}
|
||||
if req.Weight != nil {
|
||||
data.Weight = *req.Weight
|
||||
}
|
||||
if req.MaxConcurrency != nil {
|
||||
data.MaxConcurrency = *req.MaxConcurrency
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ function openEditModal() {
|
||||
name: detail.value.name || '',
|
||||
base_url: detail.value.base_url || '',
|
||||
priority: detail.value.priority || 0,
|
||||
weight: detail.value.weight || 100,
|
||||
weight: detail.value.weight,
|
||||
max_concurrency: detail.value.max_concurrency ?? 100,
|
||||
tier: detail.value.tier || 'primary',
|
||||
strict_capacity: detail.value.strict_capacity || false,
|
||||
|
||||
@@ -290,7 +290,6 @@ async function resetHealth(row: any) {
|
||||
}
|
||||
|
||||
// 快捷启用/禁用渠道:复用 PUT /admin/channels/{id}。
|
||||
// 后端 UpdateChannel 会无条件写入 priority/weight,故必须带上当前值,否则会被重置为 0。
|
||||
// status 为 active → 禁用;其它(disabled/testing)→ 启用为 active。
|
||||
async function toggleStatus(record: any) {
|
||||
if (record._toggling) return
|
||||
@@ -302,8 +301,6 @@ async function toggleStatus(record: any) {
|
||||
try {
|
||||
await request.put(`/admin/channels/${record.id}`, {
|
||||
status: nextStatus,
|
||||
priority: record.priority,
|
||||
weight: record.weight,
|
||||
})
|
||||
message.success(nextStatus === 'active' ? '已启用' : '已禁用')
|
||||
} catch {
|
||||
@@ -314,8 +311,7 @@ async function toggleStatus(record: any) {
|
||||
}
|
||||
|
||||
// === 行内编辑(权重 / 层级) ===
|
||||
// 行内编辑复用 PUT /admin/channels/{id}。后端 UpdateChannel 会无条件写入 priority/weight,
|
||||
// 故每次提交必须携带当前 priority 与 weight,避免被重置为 0。
|
||||
// 行内编辑复用 PUT /admin/channels/{id},仅提交变更字段(其余字段留空不更新)。
|
||||
const weightTimers = new Map<number, any>()
|
||||
|
||||
function onWeightChange(record: any, val: number | undefined) {
|
||||
@@ -337,7 +333,6 @@ async function saveWeight(record: any, val: number) {
|
||||
record._saving = true
|
||||
try {
|
||||
await request.put(`/admin/channels/${record.id}`, {
|
||||
priority: record.priority,
|
||||
weight: val,
|
||||
})
|
||||
record._savedWeight = val
|
||||
@@ -355,8 +350,6 @@ async function saveTier(record: any, val: string) {
|
||||
record._saving = true
|
||||
try {
|
||||
await request.put(`/admin/channels/${record.id}`, {
|
||||
priority: record.priority,
|
||||
weight: record.weight,
|
||||
tier: val,
|
||||
})
|
||||
record._tierSaved = val
|
||||
|
||||
Reference in new issue
Block a user