package repository import ( "database/sql" "fmt" "strings" "cmautobuy/admin/model" ) // UpsertClient 登记或更新一台客户端。 // // # 名称的更新规则(两个接口不一样,这是有意的) // // explicit=true 用户在设置页点了"保存",是**明确的人为操作**。 // 带了非空名称就更新;名称为空则保留原有名称。 // explicit=false claim 顺带做的隐式登记,是**后台自动调用**。 // 永远不更新名称。 // // 为什么区分:后台每次领取任务都上报一次名称,如果照单全收, // 操作员在 Admin 界面精心改的名字会被客户端的默认值反复冲掉。 // 但用户明确点保存时,又应该能把新名字同步过去。 // // 新建时名称为空则用 clientID 兜底,保证列表里不出现空白行。 func UpsertClient(q Execer, c model.Client, explicit bool) error { if c.ClientID == "" { return fmt.Errorf("client_id 不能为空") } name := strings.TrimSpace(c.Name) insertName := name if insertName == "" { insertName = c.ClientID // 新建时的兜底 } // 只有"显式登记 + 名称非空"才允许覆盖已有名称 updateName := explicit && name != "" now := model.NowISO() _, err := q.Exec(` INSERT INTO clients (client_id, name, device_address, platform, pdd_package, capabilities, last_seen_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(client_id) DO UPDATE SET name = CASE WHEN ? THEN ? ELSE clients.name END, device_address = excluded.device_address, platform = excluded.platform, pdd_package = excluded.pdd_package, capabilities = excluded.capabilities, last_seen_at = excluded.last_seen_at, updated_at = excluded.updated_at`, c.ClientID, insertName, c.DeviceAddress, c.Platform, c.PddPackage, c.Capabilities, now, now, now, updateName, name) if err != nil { return fmt.Errorf("登记客户端 %s 失败: %w", c.ClientID, err) } return nil } // TouchClient 只刷新 last_seen_at。 // // claim / result / failure 三个接口都要调。只在 claim 里调的话, // 客户端执行长任务期间不调 claim,会被误判成离线。 func TouchClient(q Execer, clientID string) error { now := model.NowISO() _, err := q.Exec( `UPDATE clients SET last_seen_at = ?, updated_at = ? WHERE client_id = ?`, now, now, clientID) if err != nil { return fmt.Errorf("刷新客户端 %s 活动时间失败: %w", clientID, err) } return nil } // ListClients 按名称模糊查询客户端,keyword 为空时返回全部。 func ListClients(db *sql.DB, keyword string) ([]model.Client, error) { query := `SELECT client_id, name, device_address, platform, pdd_package, capabilities, last_seen_at, created_at, updated_at FROM clients` args := []any{} if kw := strings.TrimSpace(keyword); kw != "" { // 参数化查询,通配符拼在值里而不是 SQL 里 query += ` WHERE name LIKE ? OR client_id LIKE ?` like := "%" + kw + "%" args = append(args, like, like) } query += ` ORDER BY last_seen_at DESC, client_id` rows, err := db.Query(query, args...) if err != nil { return nil, fmt.Errorf("查询客户端列表失败: %w", err) } defer rows.Close() var out []model.Client for rows.Next() { var c model.Client var name, addr, platform, pkg, caps sql.NullString if err := rows.Scan(&c.ClientID, &name, &addr, &platform, &pkg, &caps, &c.LastSeenAt, &c.CreatedAt, &c.UpdatedAt); err != nil { return nil, fmt.Errorf("读取客户端行失败: %w", err) } c.Name = name.String c.DeviceAddress = addr.String c.Platform = platform.String c.PddPackage = pkg.String c.Capabilities = caps.String out = append(out, c) } return out, rows.Err() } // DeleteClients 批量删除客户端。返回实际删除的条数。 func DeleteClients(db *sql.DB, clientIDs []string) (int64, error) { if len(clientIDs) == 0 { return 0, nil } // 占位符按数量生成,值仍然是参数化传入,不存在注入 placeholders := strings.TrimSuffix(strings.Repeat("?,", len(clientIDs)), ",") args := make([]any, len(clientIDs)) for i, id := range clientIDs { args[i] = id } res, err := db.Exec( `DELETE FROM clients WHERE client_id IN (`+placeholders+`)`, args...) if err != nil { return 0, fmt.Errorf("删除客户端失败: %w", err) } return res.RowsAffected() }