// Admin 网页用户与 Session 的数据库读写。 package repository import ( "database/sql" "errors" "fmt" "cmautobuy/admin/model" ) var ( // ErrUsersAlreadyExist 表示首次初始化入口已经永久关闭。 ErrUsersAlreadyExist = errors.New("管理员已经初始化") ErrUserNotFound = errors.New("用户不存在") ErrSessionNotFound = errors.New("网页登录会话不存在") ) // CountUsers 返回所有用户数量,disabled 也算。即使所有账号被禁用, // 首次初始化入口也不能重新开放。 func CountUsers(q Execer) (int, error) { var count int if err := q.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil { return 0, fmt.Errorf("统计 Admin 用户失败: %w", err) } return count, nil } // CreateInitialAdmin 在写事务里再次确认 users 为空后创建首位管理员。 // Open 的 _txlock=immediate 让并发事务从一开始就串行,最多一个能成功。 func CreateInitialAdmin(db *sql.DB, user model.User) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("开始初始化管理员事务失败: %w", err) } defer tx.Rollback() count, err := CountUsers(tx) if err != nil { return err } if count != 0 { return ErrUsersAlreadyExist } _, err = tx.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 { return fmt.Errorf("创建首位管理员失败: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("提交初始化管理员事务失败: %w", err) } return nil } // FindUserByUsername 使用 users.username 的 NOCASE 规则查账号。 func FindUserByUsername(q Execer, username string) (*model.User, error) { var user model.User var lastLogin sql.NullString err := q.QueryRow(` SELECT user_id, username, password_hash, role, status, last_login_at, password_changed_at, created_at, updated_at FROM users WHERE username = ?`, username, ).Scan(&user.UserID, &user.Username, &user.PasswordHash, &user.Role, &user.Status, &lastLogin, &user.PasswordChangedAt, &user.CreatedAt, &user.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } if err != nil { return nil, fmt.Errorf("查询 Admin 用户失败: %w", err) } user.LastLoginAt = lastLogin.String return &user, nil } // CreateLoginSession 原子写入新 Session 并记录最后登录时间。 func CreateLoginSession(db *sql.DB, session model.WebSession, loginAt string) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("开始登录事务失败: %w", err) } defer tx.Rollback() if _, err := tx.Exec(` INSERT INTO web_sessions (session_hash, user_id, expires_at, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)`, session.SessionHash, session.UserID, session.ExpiresAt, session.CreatedAt, session.LastSeenAt); err != nil { return fmt.Errorf("保存 Web Session 失败: %w", err) } if _, err := tx.Exec(` UPDATE users SET last_login_at = ?, updated_at = ? WHERE user_id = ?`, loginAt, loginAt, session.UserID); err != nil { return fmt.Errorf("更新用户最后登录时间失败: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("提交登录事务失败: %w", err) } return nil } // FindUserBySessionHash 返回 Session 及其用户。账号状态由 service 判断, // 这样“禁用账号立即失效”不依赖额外清理是否及时。 func FindUserBySessionHash(q Execer, sessionHash string) (*model.WebSession, *model.User, error) { var session model.WebSession var user model.User var lastLogin sql.NullString err := q.QueryRow(` SELECT s.session_hash, s.user_id, s.expires_at, s.created_at, s.last_seen_at, u.user_id, u.username, u.password_hash, u.role, u.status, u.last_login_at, u.password_changed_at, u.created_at, u.updated_at FROM web_sessions s JOIN users u ON u.user_id = s.user_id WHERE s.session_hash = ?`, sessionHash, ).Scan(&session.SessionHash, &session.UserID, &session.ExpiresAt, &session.CreatedAt, &session.LastSeenAt, &user.UserID, &user.Username, &user.PasswordHash, &user.Role, &user.Status, &lastLogin, &user.PasswordChangedAt, &user.CreatedAt, &user.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil, ErrSessionNotFound } if err != nil { return nil, nil, fmt.Errorf("查询 Web Session 失败: %w", err) } user.LastLoginAt = lastLogin.String return &session, &user, nil } // DeleteSession 删除当前浏览器对应的 Session。不存在也视为成功,退出幂等。 func DeleteSession(q Execer, sessionHash string) error { if _, err := q.Exec(`DELETE FROM web_sessions WHERE session_hash = ?`, sessionHash); err != nil { return fmt.Errorf("删除 Web Session 失败: %w", err) } return nil } // DeleteExpiredSessions 清理固定过期的 Session,不需要后台常驻线程。 func DeleteExpiredSessions(q Execer, nowISO string) error { if _, err := q.Exec(`DELETE FROM web_sessions WHERE expires_at <= ?`, nowISO); err != nil { return fmt.Errorf("清理过期 Web Session 失败: %w", err) } return nil }