2026-08-06 16:56:18 +08:00
package repository
import (
"database/sql"
2026-08-10 15:11:38 +08:00
"encoding/json"
2026-08-09 16:02:25 +08:00
"errors"
2026-08-06 16:56:18 +08:00
"fmt"
"strings"
"cmautobuy/admin/model"
)
2026-08-10 15:11:38 +08:00
// ClientPurchaseMode 返回客户端最后一次登记时声明的采购能力。
// 旧数据、空值或无法解析的 JSON 一律按 dry_run 处理,这是安全默认值。
func ClientPurchaseMode ( q Execer , clientID string ) ( string , error ) {
var raw sql . NullString
if err := q . QueryRow ( `SELECT capabilities FROM clients WHERE client_id = ?` , clientID ). Scan ( & raw ); errors . Is ( err , sql . ErrNoRows ) {
return "" , ErrClientNotFound
} else if err != nil {
return "" , fmt . Errorf ( "读取客户端 %s 的采购能力失败: %w" , clientID , err )
}
return ParseClientPurchaseMode ( raw . String ), nil
}
// ParseClientPurchaseMode 把登记能力转成安全的固定值,供列表和创建校验共用。
func ParseClientPurchaseMode ( raw string ) string {
var capabilities struct {
PurchaseMode string `json:"purchase_mode"`
}
if json . Unmarshal ([] byte ( raw ), & capabilities ) == nil && capabilities . PurchaseMode == string ( model . TaskExecutionLive ) {
return capabilities . PurchaseMode
}
return string ( model . TaskExecutionDryRun )
}
2026-08-09 16:02:25 +08:00
var (
ErrClientNotFound = errors . New ( "客户端不存在" )
ErrPurchaserNotActive = errors . New ( "采购员不存在或已禁用" )
ErrClientNotAssigned = errors . New ( "客户端当前未绑定采购员" )
)
// ClientWithAssignee 是客户端列表查询结果。归属为空表示尚未绑定。
type ClientWithAssignee struct {
model . Client
AssignedUserID string
AssignedUsername string
}
2026-08-06 16:56:18 +08:00
// UpsertClient 登记或更新一台客户端。
//
2026-08-06 17:31:38 +08:00
// # 名称的更新规则(两个接口不一样,这是有意的)
2026-08-06 16:56:18 +08:00
//
2026-08-06 17:31:38 +08:00
// explicit=true 用户在设置页点了"保存",是**明确的人为操作**。
// 带了非空名称就更新;名称为空则保留原有名称。
// explicit=false claim 顺带做的隐式登记,是**后台自动调用**。
// 永远不更新名称。
//
// 为什么区分:后台每次领取任务都上报一次名称,如果照单全收,
// 操作员在 Admin 界面精心改的名字会被客户端的默认值反复冲掉。
// 但用户明确点保存时,又应该能把新名字同步过去。
//
// 新建时名称为空则用 clientID 兜底,保证列表里不出现空白行。
func UpsertClient ( q Execer , c model . Client , explicit bool ) error {
2026-08-06 16:56:18 +08:00
if c . ClientID == "" {
return fmt . Errorf ( "client_id 不能为空" )
}
2026-08-06 17:31:38 +08:00
name := strings . TrimSpace ( c . Name )
insertName := name
if insertName == "" {
insertName = c . ClientID // 新建时的兜底
}
// 只有"显式登记 + 名称非空"才允许覆盖已有名称
updateName := explicit && name != ""
now := model . NowISO ()
_ , err := q . Exec ( `
2026-08-06 16:56:18 +08:00
INSERT INTO clients (client_id, name, device_address, platform,
pdd_package, capabilities,
last_seen_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2026-08-10 02:21:31 +08:00
ON DUPLICATE KEY UPDATE
2026-08-06 17:31:38 +08:00
name = CASE WHEN ? THEN ? ELSE clients.name END,
2026-08-10 02:21:31 +08:00
device_address = VALUES(device_address),
platform = VALUES(platform),
pdd_package = VALUES(pdd_package),
capabilities = VALUES(capabilities),
last_seen_at = VALUES(last_seen_at),
updated_at = VALUES(updated_at)` ,
2026-08-06 17:31:38 +08:00
c . ClientID , insertName , c . DeviceAddress , c . Platform ,
c . PddPackage , c . Capabilities , now , now , now ,
updateName , name )
2026-08-06 16:56:18 +08:00
if err != nil {
return fmt . Errorf ( "登记客户端 %s 失败: %w" , c . ClientID , err )
}
return nil
}
// TouchClient 只刷新 last_seen_at。
//
// claim / result / failure 三个接口都要调。只在 claim 里调的话,
// 客户端执行长任务期间不调 claim,会被误判成离线。
2026-08-06 17:04:49 +08:00
func TouchClient ( q Execer , clientID string ) error {
2026-08-06 16:56:18 +08:00
now := model . NowISO ()
2026-08-06 17:04:49 +08:00
_ , err := q . Exec (
2026-08-06 16:56:18 +08:00
`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 ()
}
2026-08-09 21:51:20 +08:00
// clientListFilter 统一生成客户端分页、总数和在线统计的可见范围。
func clientListFilter ( keyword , visibleUserID string ) ( string , [] any ) {
2026-08-09 16:02:25 +08:00
where := make ([] string , 0 , 2 )
args := make ([] any , 0 , 3 )
if visibleUserID != "" {
where = append ( where , `a.user_id = ?` )
args = append ( args , visibleUserID )
}
if kw := strings . TrimSpace ( keyword ); kw != "" {
where = append ( where , `(c.name LIKE ? OR c.client_id LIKE ?)` )
like := "%" + kw + "%"
args = append ( args , like , like )
}
2026-08-09 21:51:20 +08:00
if len ( where ) == 0 {
return "" , args
2026-08-09 16:02:25 +08:00
}
2026-08-09 21:51:20 +08:00
return ` WHERE ` + strings . Join ( where , ` AND ` ), args
}
const clientListFrom = ` FROM clients c
LEFT JOIN client_user_assignments a
ON a.client_id = c.client_id AND a.ended_at IS NULL
LEFT JOIN users u ON u.user_id = a.user_id`
func listClientsForUserPage ( db * sql . DB , keyword , visibleUserID string , limit , offset int ) ([] ClientWithAssignee , error ) {
where , args := clientListFilter ( keyword , visibleUserID )
query := `SELECT c.client_id, c.name, c.device_address, c.platform, c.pdd_package,
c.capabilities, c.last_seen_at, c.created_at, c.updated_at,
a.user_id, u.username` + clientListFrom + where +
2026-08-10 02:21:31 +08:00
` ORDER BY c.last_seen_at DESC, c.client_id`
if limit >= 0 {
query += ` LIMIT ? OFFSET ?`
args = append ( args , limit , offset )
}
2026-08-09 16:02:25 +08:00
rows , err := db . Query ( query , args ... )
if err != nil {
return nil , fmt . Errorf ( "查询客户端归属列表失败: %w" , err )
}
defer rows . Close ()
out := make ([] ClientWithAssignee , 0 )
for rows . Next () {
var row ClientWithAssignee
var name , addr , platform , pkg , caps , userID , username sql . NullString
if err := rows . Scan ( & row . ClientID , & name , & addr , & platform , & pkg , & caps ,
& row . LastSeenAt , & row . CreatedAt , & row . UpdatedAt , & userID , & username ); err != nil {
return nil , fmt . Errorf ( "读取客户端归属行失败: %w" , err )
}
row . Name , row . DeviceAddress , row . Platform = name . String , addr . String , platform . String
row . PddPackage , row . Capabilities = pkg . String , caps . String
row . AssignedUserID , row . AssignedUsername = userID . String , username . String
out = append ( out , row )
}
return out , rows . Err ()
}
2026-08-09 21:51:20 +08:00
// ListClientsForUser 返回带当前负责人的全部客户端,供需要完整候选集的业务使用。
func ListClientsForUser ( db * sql . DB , keyword , visibleUserID string ) ([] ClientWithAssignee , error ) {
return listClientsForUserPage ( db , keyword , visibleUserID , - 1 , 0 )
}
2026-08-09 22:55:14 +08:00
// ClientVisibleToUser 校验客户端是否存在且位于当前账号可见范围。
// visibleUserID 为空表示管理员,可选择任意已登记客户端。
func ClientVisibleToUser ( q Execer , clientID , visibleUserID string ) ( bool , error ) {
query := `SELECT COUNT(*) FROM clients c`
args := [] any {}
if visibleUserID != "" {
query += ` JOIN client_user_assignments a
ON a.client_id = c.client_id AND a.ended_at IS NULL
AND a.user_id = ?`
args = append ( args , visibleUserID )
}
query += ` WHERE c.client_id = ?`
args = append ( args , clientID )
var count int
if err := q . QueryRow ( query , args ... ). Scan ( & count ); err != nil {
return false , fmt . Errorf ( "校验客户端可见范围失败: %w" , err )
}
return count == 1 , nil
}
2026-08-09 21:51:20 +08:00
// ListClientsForUserPage 返回一页客户端和同一可见范围内的总数。
func ListClientsForUserPage ( db * sql . DB , keyword , visibleUserID string , limit , offset int ) ([] ClientWithAssignee , int , error ) {
where , args := clientListFilter ( keyword , visibleUserID )
var total int
if err := db . QueryRow ( `SELECT COUNT(*)` + clientListFrom + where , args ... ). Scan ( & total ); err != nil {
return nil , 0 , fmt . Errorf ( "统计客户端列表失败: %w" , err )
}
rows , err := listClientsForUserPage ( db , keyword , visibleUserID , limit , offset )
return rows , total , err
}
// CountOnlineClientsForUser 统计同一筛选和权限范围内的在线客户端。
2026-08-10 02:21:31 +08:00
// last_seen_at 统一以 UTC RFC3339 保存,固定宽度字符串的字典序就是时间顺序。
2026-08-09 21:51:20 +08:00
func CountOnlineClientsForUser ( db * sql . DB , keyword , visibleUserID , cutoffISO string ) ( int , error ) {
where , args := clientListFilter ( keyword , visibleUserID )
args = append ( args , cutoffISO )
var online int
query := `SELECT COUNT(*)` + clientListFrom + where
if where == "" {
2026-08-10 02:21:31 +08:00
query += ` WHERE c.last_seen_at > ?`
2026-08-09 21:51:20 +08:00
} else {
2026-08-10 02:21:31 +08:00
query += ` AND c.last_seen_at > ?`
2026-08-09 21:51:20 +08:00
}
if err := db . QueryRow ( query , args ... ). Scan ( & online ); err != nil {
return 0 , fmt . Errorf ( "统计在线客户端失败: %w" , err )
}
return online , nil
}
2026-08-09 16:02:25 +08:00
// ListActivePurchasers 返回可成为新负责人的启用采购员。
func ListActivePurchasers ( db * sql . DB ) ([] model . User , error ) {
rows , err := db . Query ( `
SELECT user_id, username, role, status, password_changed_at, created_at, updated_at
FROM users WHERE role = ? AND status = ? ORDER BY username` ,
model . RolePurchaser , model . UserActive )
if err != nil {
return nil , fmt . Errorf ( "查询可绑定采购员失败: %w" , err )
}
defer rows . Close ()
users := make ([] model . User , 0 )
for rows . Next () {
var user model . User
if err := rows . Scan ( & user . UserID , & user . Username , & user . Role , & user . Status ,
& user . PasswordChangedAt , & user . CreatedAt , & user . UpdatedAt ); err != nil {
return nil , fmt . Errorf ( "读取可绑定采购员失败: %w" , err )
}
users = append ( users , user )
}
return users , rows . Err ()
}
// AssignClient 原子完成首次绑定或转交。返回 changed、transferred。
func AssignClient ( db * sql . DB , assignment model . ClientUserAssignment ) ( bool , bool , error ) {
tx , err := db . Begin ()
if err != nil {
return false , false , fmt . Errorf ( "开始绑定客户端事务失败: %w" , err )
}
defer tx . Rollback ()
2026-08-10 02:21:31 +08:00
var lockedClientID string
if err := tx . QueryRow ( `SELECT client_id FROM clients WHERE client_id = ? FOR UPDATE` , assignment . ClientID ). Scan ( & lockedClientID ); errors . Is ( err , sql . ErrNoRows ) {
return false , false , ErrClientNotFound
} else if err != nil {
2026-08-09 16:02:25 +08:00
return false , false , fmt . Errorf ( "检查客户端失败: %w" , err )
}
2026-08-10 02:21:31 +08:00
var exists int
2026-08-09 16:02:25 +08:00
if err := tx . QueryRow ( `SELECT COUNT(*) FROM users WHERE user_id = ? AND role = ? AND status = ?` ,
assignment . UserID , model . RolePurchaser , model . UserActive ). Scan ( & exists ); err != nil {
return false , false , fmt . Errorf ( "检查采购员失败: %w" , err )
}
if exists == 0 {
return false , false , ErrPurchaserNotActive
}
var currentID , currentUserID string
err = tx . QueryRow ( `SELECT assignment_id, user_id FROM client_user_assignments
WHERE client_id = ? AND ended_at IS NULL` , assignment . ClientID ). Scan ( & currentID , & currentUserID )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return false , false , fmt . Errorf ( "读取当前客户端归属失败: %w" , err )
}
if err == nil && currentUserID == assignment . UserID {
return false , false , nil
}
transferred := err == nil
if transferred {
if _ , err := tx . Exec ( `UPDATE client_user_assignments
SET ended_at = ?, ended_by_user_id = ?, end_reason = 'transfer'
WHERE assignment_id = ? AND ended_at IS NULL` ,
assignment . StartedAt , assignment . AssignedByUserID , currentID ); err != nil {
return false , false , fmt . Errorf ( "结束原客户端归属失败: %w" , err )
}
}
if _ , err := tx . Exec ( `INSERT INTO client_user_assignments
(assignment_id, client_id, user_id, started_at, assigned_by_user_id)
VALUES (?, ?, ?, ?, ?)` , assignment . AssignmentID , assignment . ClientID ,
assignment . UserID , assignment . StartedAt , assignment . AssignedByUserID ); err != nil {
return false , false , fmt . Errorf ( "保存客户端归属失败: %w" , err )
}
if err := tx . Commit (); err != nil {
return false , false , fmt . Errorf ( "提交客户端绑定事务失败: %w" , err )
}
return true , transferred , nil
}
// UnassignClient 原子结束当前归属,历史记录保留。
func UnassignClient ( db * sql . DB , clientID , actorUserID , endedAt string ) error {
tx , err := db . Begin ()
if err != nil {
return fmt . Errorf ( "开始解绑客户端事务失败: %w" , err )
}
defer tx . Rollback ()
res , err := tx . Exec ( `UPDATE client_user_assignments
SET ended_at = ?, ended_by_user_id = ?, end_reason = 'unbind'
WHERE client_id = ? AND ended_at IS NULL` , endedAt , actorUserID , clientID )
if err != nil {
return fmt . Errorf ( "结束客户端归属失败: %w" , err )
}
n , err := res . RowsAffected ()
if err != nil {
return fmt . Errorf ( "读取解绑结果失败: %w" , err )
}
if n == 0 {
return ErrClientNotAssigned
}
if err := tx . Commit (); err != nil {
return fmt . Errorf ( "提交客户端解绑事务失败: %w" , err )
}
return nil
}
2026-08-06 16:56:18 +08:00
// 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 ()
}