Files
cmautobuy/admin/repository/user.go
T

151 lines
5.2 KiB
Go

// 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
}