feat: 实现 Admin 采购员账号管理 (#51)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
)
|
||||
@@ -14,6 +15,8 @@ var (
|
||||
ErrUsersAlreadyExist = errors.New("管理员已经初始化")
|
||||
ErrUserNotFound = errors.New("用户不存在")
|
||||
ErrSessionNotFound = errors.New("网页登录会话不存在")
|
||||
ErrUsernameExists = errors.New("用户名已经存在")
|
||||
ErrLastActiveAdmin = errors.New("不能禁用最后一个有效管理员")
|
||||
)
|
||||
|
||||
// CountUsers 返回所有用户数量,disabled 也算。即使所有账号被禁用,
|
||||
@@ -148,3 +151,131 @@ func DeleteExpiredSessions(q Execer, nowISO string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUsers 按用户名搜索并分页。列表和 COUNT 共用同一段筛选,避免页码漂移。
|
||||
func ListUsers(q Execer, keyword string, status model.UserStatus, limit, offset int) ([]model.User, int, error) {
|
||||
pattern := "%" + escapeLike(keyword) + "%"
|
||||
where := `WHERE username LIKE ? ESCAPE '\'`
|
||||
args := []any{pattern}
|
||||
if status == model.UserActive || status == model.UserDisabled {
|
||||
where += ` AND status = ?`
|
||||
args = append(args, status)
|
||||
}
|
||||
var total int
|
||||
if err := q.QueryRow(`SELECT COUNT(*) FROM users `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, fmt.Errorf("统计 Admin 用户失败: %w", err)
|
||||
}
|
||||
listArgs := append(append([]any{}, args...), limit, offset)
|
||||
rows, err := q.Query(`
|
||||
SELECT user_id, username, role, status, last_login_at,
|
||||
password_changed_at, created_at, updated_at
|
||||
FROM users `+where+`
|
||||
ORDER BY created_at DESC, username ASC
|
||||
LIMIT ? OFFSET ?`, listArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询 Admin 用户列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]model.User, 0)
|
||||
for rows.Next() {
|
||||
var user model.User
|
||||
var lastLogin sql.NullString
|
||||
if err := rows.Scan(&user.UserID, &user.Username, &user.Role, &user.Status,
|
||||
&lastLogin, &user.PasswordChangedAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
|
||||
return nil, 0, fmt.Errorf("读取 Admin 用户列表失败: %w", err)
|
||||
}
|
||||
user.LastLoginAt = lastLogin.String
|
||||
users = append(users, user)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, fmt.Errorf("遍历 Admin 用户列表失败: %w", err)
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// CreateUser 创建一个已经完成密码哈希的账号。用户名由数据库 NOCASE 唯一约束
|
||||
// 做最终并发保护。
|
||||
func CreateUser(q Execer, user model.User) error {
|
||||
_, err := q.Exec(`
|
||||
INSERT INTO users
|
||||
(user_id, username, password_hash, role, status, last_login_at,
|
||||
password_changed_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
|
||||
user.UserID, user.Username, user.PasswordHash, user.Role, user.Status,
|
||||
user.PasswordChangedAt, user.CreatedAt, user.UpdatedAt)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") {
|
||||
return ErrUsernameExists
|
||||
}
|
||||
return fmt.Errorf("创建 Admin 用户失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUserStatusAndRevokeSessions 在同一写事务里修改状态并撤销全部 Session。
|
||||
// 禁用管理员前会在事务内保护最后一个有效管理员。
|
||||
func SetUserStatusAndRevokeSessions(db *sql.DB, userID string, status model.UserStatus, updatedAt string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始修改用户状态事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var role model.UserRole
|
||||
var current model.UserStatus
|
||||
if err := tx.QueryRow(`SELECT role, status FROM users WHERE user_id = ?`, userID).Scan(&role, ¤t); errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrUserNotFound
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("读取待修改用户失败: %w", err)
|
||||
}
|
||||
if status == model.UserDisabled && current == model.UserActive && role == model.RoleAdmin {
|
||||
var activeAdmins int
|
||||
if err := tx.QueryRow(`SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active'`).Scan(&activeAdmins); err != nil {
|
||||
return fmt.Errorf("统计有效管理员失败: %w", err)
|
||||
}
|
||||
if activeAdmins <= 1 {
|
||||
return ErrLastActiveAdmin
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE users SET status = ?, updated_at = ? WHERE user_id = ?`, status, updatedAt, userID); err != nil {
|
||||
return fmt.Errorf("修改用户状态失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM web_sessions WHERE user_id = ?`, userID); err != nil {
|
||||
return fmt.Errorf("撤销用户 Session 失败: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交用户状态事务失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetUserPasswordAndRevokeSessions 原子更新密码哈希并撤销该用户所有 Session。
|
||||
func ResetUserPasswordAndRevokeSessions(db *sql.DB, userID, passwordHash, updatedAt string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始重置密码事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
result, err := tx.Exec(`
|
||||
UPDATE users
|
||||
SET password_hash = ?, password_changed_at = ?, updated_at = ?
|
||||
WHERE user_id = ?`, passwordHash, updatedAt, updatedAt, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("重置用户密码失败: %w", err)
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("确认重置密码结果失败: %w", err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM web_sessions WHERE user_id = ?`, userID); err != nil {
|
||||
return fmt.Errorf("撤销用户 Session 失败: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交重置密码事务失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user