feat: 迁移 Admin Repository 到 MySQL 8 (#79)

This commit is contained in:
chengma
2026-08-10 02:21:31 +08:00
parent 05793e48b3
commit 13cfced80d
29 changed files with 390 additions and 240 deletions
+26 -9
View File
@@ -5,9 +5,9 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"cmautobuy/admin/model"
"github.com/go-sql-driver/mysql"
)
var (
@@ -63,14 +63,18 @@ func ChangePasswordAndRevokeSessions(db *sql.DB, userID, expectedHash, passwordH
return nil
}
// CreateInitialAdmin 在写事务里再次确认 users 为空后创建首位管理员。
// Open 的 _txlock=immediate 让并发事务从一开始就串行,最多一个能成功。
// CreateInitialAdmin 锁住唯一的初始化哨兵行,再确认 users 为空并创建首位管理员。
// 并发请求会在 FOR UPDATE 处串行,最多一个能成功。
func CreateInitialAdmin(db *sql.DB, user model.User) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开始初始化管理员事务失败: %w", err)
}
defer tx.Rollback()
var lockID int
if err := tx.QueryRow(`SELECT id FROM admin_initialization_lock WHERE id = 1 FOR UPDATE`).Scan(&lockID); err != nil {
return fmt.Errorf("锁定管理员初始化状态失败: %w", err)
}
count, err := CountUsers(tx)
if err != nil {
@@ -189,7 +193,7 @@ func DeleteExpiredSessions(q Execer, nowISO string) error {
// 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 '\'`
where := `WHERE username LIKE ? ESCAPE '!'`
args := []any{pattern}
if status == model.UserActive || status == model.UserDisabled {
where += ` AND status = ?`
@@ -239,7 +243,8 @@ func CreateUser(q Execer, user model.User) error {
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") {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return ErrUsernameExists
}
return fmt.Errorf("创建 Admin 用户失败: %w", err)
@@ -258,15 +263,27 @@ func SetUserStatusAndRevokeSessions(db *sql.DB, userID string, status model.User
var role model.UserRole
var current model.UserStatus
if err := tx.QueryRow(`SELECT role, status FROM users WHERE user_id = ?`, userID).Scan(&role, &current); errors.Is(err, sql.ErrNoRows) {
if err := tx.QueryRow(`SELECT role, status FROM users WHERE user_id = ? FOR UPDATE`, userID).Scan(&role, &current); 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)
rows, err := tx.Query(`SELECT user_id FROM users WHERE role = 'admin' AND status = 'active' FOR UPDATE`)
if err != nil {
return fmt.Errorf("锁定有效管理员失败: %w", err)
}
activeAdmins := 0
for rows.Next() {
var activeID string
if err := rows.Scan(&activeID); err != nil {
rows.Close()
return fmt.Errorf("读取有效管理员失败: %w", err)
}
activeAdmins++
}
if err := rows.Close(); err != nil {
return fmt.Errorf("读取有效管理员失败: %w", err)
}
if activeAdmins <= 1 {
return ErrLastActiveAdmin