feat: 增加客户端采购员归属管理 (#54)
This commit is contained in:
@@ -2,12 +2,26 @@ package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClientNotFound = errors.New("客户端不存在")
|
||||
ErrPurchaserNotActive = errors.New("采购员不存在或已禁用")
|
||||
ErrClientNotAssigned = errors.New("客户端当前未绑定采购员")
|
||||
)
|
||||
|
||||
// ClientWithAssignee 是客户端列表查询结果。归属为空表示尚未绑定。
|
||||
type ClientWithAssignee struct {
|
||||
model.Client
|
||||
AssignedUserID string
|
||||
AssignedUsername string
|
||||
}
|
||||
|
||||
// UpsertClient 登记或更新一台客户端。
|
||||
//
|
||||
// # 名称的更新规则(两个接口不一样,这是有意的)
|
||||
@@ -112,6 +126,153 @@ func ListClients(db *sql.DB, keyword string) ([]model.Client, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ListClientsForUser 返回带当前负责人的客户端。
|
||||
// visibleUserID 为空表示管理员全量视图,否则只返回当前绑定给该用户的客户端。
|
||||
func ListClientsForUser(db *sql.DB, keyword, visibleUserID string) ([]ClientWithAssignee, error) {
|
||||
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
|
||||
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`
|
||||
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)
|
||||
}
|
||||
if len(where) > 0 {
|
||||
query += ` WHERE ` + strings.Join(where, ` AND `)
|
||||
}
|
||||
query += ` ORDER BY c.last_seen_at DESC, c.client_id`
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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()
|
||||
var exists int
|
||||
if err := tx.QueryRow(`SELECT COUNT(*) FROM clients WHERE client_id = ?`, assignment.ClientID).Scan(&exists); err != nil {
|
||||
return false, false, fmt.Errorf("检查客户端失败: %w", err)
|
||||
}
|
||||
if exists == 0 {
|
||||
return false, false, ErrClientNotFound
|
||||
}
|
||||
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(¤tID, ¤tUserID)
|
||||
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
|
||||
}
|
||||
|
||||
// DeleteClients 批量删除客户端。返回实际删除的条数。
|
||||
func DeleteClients(db *sql.DB, clientIDs []string) (int64, error) {
|
||||
if len(clientIDs) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user