package repository import ( "database/sql" "fmt" "strings" "cmautobuy/admin/model" ) // UpsertClient 登记或更新一台客户端。 // // 注意 name 的处理:**只在第一次注册时写入,之后不再更新**。 // 这样操作员在 Admin 界面上改成好记的名字后,客户端每次 claim // 都不会把它覆盖回去。做法是 ON CONFLICT 的 DO UPDATE 里不含 name。 // // name 为空时用 clientID 当显示名,保证列表里不出现空白行。 func UpsertClient(db *sql.DB, c model.Client) error { if c.ClientID == "" { return fmt.Errorf("client_id 不能为空") } name := c.Name if name == "" { name = c.ClientID } now := model.NowISO() _, err := db.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 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, name, c.DeviceAddress, c.Platform, c.PddPackage, c.Capabilities, now, now, now) if err != nil { return fmt.Errorf("登记客户端 %s 失败: %w", c.ClientID, err) } return nil } // TouchClient 只刷新 last_seen_at。 // // claim / result / failure 三个接口都要调。只在 claim 里调的话, // 客户端执行长任务期间不调 claim,会被误判成离线。 func TouchClient(db *sql.DB, clientID string) error { now := model.NowISO() _, err := db.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() }