package store import ( "database/sql" "errors" "strings" "time" "daili/internal/pw" ) // User 管理员账户。 type User struct { ID int64 Username string DisplayName string IsActive bool Color string CreatedAt string LastLoginAt string } // AssigneePalette 是新建管理员时自动分配的颜色(区分度较高的深色系)。 var AssigneePalette = []string{ "#2F5D50", "#8C4A3C", "#3E5C8A", "#7A5AA6", "#A66A1E", "#2C7A7B", "#8A3B5C", "#5B6B2F", } // NormalizeColor 校验并规范化 #RRGGBB;空值返回空。 func NormalizeColor(c string) (string, error) { c = strings.TrimSpace(c) if c == "" { return "", nil } if len(c) != 7 || c[0] != '#' { return "", errors.New("颜色格式应为 #RRGGBB") } for _, r := range c[1:] { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { return "", errors.New("颜色格式应为 #RRGGBB") } } return strings.ToUpper(c), nil } func scanUser(row interface { Scan(dest ...any) error }) (*User, error) { var u User var active int var last sql.NullString if err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &active, &u.CreatedAt, &last, &u.Color); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } u.IsActive = active == 1 u.LastLoginAt = last.String return &u, nil } const userCols = `id, username, display_name, is_active, created_at, IFNULL(last_login_at,''), IFNULL(color,'')` // CountUsers 返回账户数量。 func (db *DB) CountUsers() (int, error) { var n int err := db.sql.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n) return n, err } // CreateUser 新建账户。 func (db *DB) CreateUser(username, displayName, password string) (*User, error) { hash, err := pw.Hash(password) if err != nil { return nil, err } now := nowStamp() color := db.nextAssigneeColor() res, err := db.sql.Exec(`INSERT INTO users(username, display_name, password_hash, is_active, color, created_at) VALUES(?,?,?,1,?,?)`, username, displayName, hash, color, now) if err != nil { if isUnique(err) { return nil, ErrDuplicate } return nil, err } id, _ := res.LastInsertId() return &User{ID: id, Username: username, DisplayName: displayName, IsActive: true, Color: color, CreatedAt: now}, nil } // nextAssigneeColor 依据现有账户数循环取调色板,尽量不重复。 func (db *DB) nextAssigneeColor() string { n, _ := db.CountUsers() return AssigneePalette[n%len(AssigneePalette)] } // SetUserColor 设置管理员颜色(空字符串表示清除)。 func (db *DB) SetUserColor(userID int64, color string) error { c, err := NormalizeColor(color) if err != nil { return err } res, err := db.sql.Exec(`UPDATE users SET color=? WHERE id=?`, c, userID) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } // SetCustomerAssignee 把客户指派给某个打手;userID=0 表示取消指派。 func (db *DB) SetCustomerAssignee(customerID, userID int64) error { var v any if userID > 0 { if _, err := db.UserByID(userID); err != nil { return errors.New("打手不存在") } v = userID } res, err := db.sql.Exec(`UPDATE customers SET assignee_id=?, updated_at=? WHERE id=?`, v, nowStamp(), customerID) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } // ActiveUsers 返回在用的管理员(打手候选)。 func (db *DB) ActiveUsers() ([]User, error) { rows, err := db.sql.Query(`SELECT ` + userCols + ` FROM users WHERE is_active=1 ORDER BY id`) if err != nil { return nil, err } defer rows.Close() out := []User{} for rows.Next() { u, err := scanUser(rows) if err != nil { return nil, err } out = append(out, *u) } return out, rows.Err() } // DeleteUserSessions 清除某个账户的全部登录会话。 func (db *DB) DeleteUserSessions(userID int64) error { _, err := db.sql.Exec(`DELETE FROM sessions WHERE user_id=?`, userID) return err } // UpdateUserByAdmin 由超管修改用户名/昵称,password 非空时同时重置密码。 // 重置密码会强制该账户下线。 func (db *DB) UpdateUserByAdmin(userID int64, username, displayName, password string) error { username = strings.TrimSpace(username) displayName = strings.TrimSpace(displayName) if username == "" { return errors.New("用户名不能为空") } hash := "" if password != "" { var err error hash, err = pw.Hash(password) if err != nil { return err } } if _, err := db.sql.Exec(`UPDATE users SET username=?, display_name=? WHERE id=?`, username, displayName, userID); err != nil { if isUnique(err) { return ErrDuplicate } return err } if hash != "" { if _, err := db.sql.Exec(`UPDATE users SET password_hash=? WHERE id=?`, hash, userID); err != nil { return err } if err := db.DeleteUserSessions(userID); err != nil { return err } } return nil } // Authenticate 校验用户名口令。 func (db *DB) Authenticate(username, password string) (*User, error) { var hash string var active int var id int64 err := db.sql.QueryRow(`SELECT id, password_hash, is_active FROM users WHERE username=?`, username). Scan(&id, &hash, &active) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } if active != 1 || !pw.Verify(password, hash) { return nil, ErrNotFound } _, _ = db.sql.Exec(`UPDATE users SET last_login_at=? WHERE id=?`, nowStamp(), id) return db.UserByID(id) } // UserByID 按 ID 取账户。 func (db *DB) UserByID(id int64) (*User, error) { return scanUser(db.sql.QueryRow(`SELECT `+userCols+` FROM users WHERE id=?`, id)) } // UserByUsername 按用户名取账户。 func (db *DB) UserByUsername(name string) (*User, error) { return scanUser(db.sql.QueryRow(`SELECT `+userCols+` FROM users WHERE username=?`, name)) } // ListUsers 列出账户。 func (db *DB) ListUsers() ([]User, error) { rows, err := db.sql.Query(`SELECT ` + userCols + ` FROM users ORDER BY id`) if err != nil { return nil, err } defer rows.Close() var out []User for rows.Next() { u, err := scanUser(rows) if err != nil { return nil, err } out = append(out, *u) } return out, rows.Err() } // UpdatePassword 修改口令。 func (db *DB) UpdatePassword(userID int64, password string) error { hash, err := pw.Hash(password) if err != nil { return err } _, err = db.sql.Exec(`UPDATE users SET password_hash=? WHERE id=?`, hash, userID) return err } // SetUserActive 启用/停用账户。 func (db *DB) SetUserActive(userID int64, active bool) error { v := 0 if active { v = 1 } _, err := db.sql.Exec(`UPDATE users SET is_active=? WHERE id=?`, v, userID) return err } // ---------- 会话 ---------- // CreateSession 新建登录会话。 func (db *DB) CreateSession(token string, userID int64, ttl time.Duration, ip string) error { now := time.Now() _, err := db.sql.Exec(`INSERT INTO sessions(token,user_id,created_at,expires_at,ip) VALUES(?,?,?,?,?)`, token, userID, now.Format("2006-01-02 15:04:05"), now.Add(ttl).Format("2006-01-02 15:04:05"), ip) return err } // SessionUser 通过令牌取账户,过期则返回 ErrNotFound。 func (db *DB) SessionUser(token string) (*User, error) { var userID int64 var expires string err := db.sql.QueryRow(`SELECT user_id, expires_at FROM sessions WHERE token=?`, token).Scan(&userID, &expires) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } exp, err := time.ParseInLocation("2006-01-02 15:04:05", expires, time.Local) if err != nil || exp.Before(time.Now()) { _ = db.DeleteSession(token) return nil, ErrNotFound } u, err := db.UserByID(userID) if err != nil { return nil, err } if !u.IsActive { return nil, ErrNotFound } return u, nil } // DeleteSession 删除会话。 func (db *DB) DeleteSession(token string) error { _, err := db.sql.Exec(`DELETE FROM sessions WHERE token=?`, token) return err } // PurgeSessions 清理过期会话。 func (db *DB) PurgeSessions() { _, _ = db.sql.Exec(`DELETE FROM sessions WHERE expires_at < ?`, nowStamp()) }