package model import ( "context" "strings" "time" userentity "spark-server/internal/module/user/entity" "spark-server/internal/pkg/errs" ) func (m *Repo) ListUsers(ctx context.Context, emailLike string, page, size int) ([]UserRow, int64, error) { db, err := m.session(ctx) if err != nil { return nil, 0, err } base := db.Table("t_users AS u").Joins("LEFT JOIN t_user_emails e ON e.user_id = u.id") emailLike = strings.TrimSpace(emailLike) if emailLike != "" { base = base.Where("e.email LIKE ?", "%"+emailLike+"%") } var total int64 _ = base.Count(&total).Error var rows []userScanRow if err := base.Select(userListSelect).Order("u.id DESC").Offset(page * size).Limit(size).Scan(&rows).Error; err != nil { return nil, 0, mapDBErr(err) } return userScanToRows(rows), total, nil } const userListSelect = `u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, COALESCE(e.email,'') AS email, COALESCE((SELECT d.device_id FROM t_user_devices d WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), 0) AS device_id, COALESCE((SELECT CAST(dev.device_type AS CHAR) FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), '') AS platform` func (m *Repo) UsersByEmail(ctx context.Context, email string) ([]UserRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []userScanRow err = db.Raw(` SELECT u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, e.email, COALESCE((SELECT d.device_id FROM t_user_devices d WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), 0) AS device_id, COALESCE((SELECT CAST(dev.device_type AS CHAR) FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), '') AS platform FROM t_user_emails e INNER JOIN t_users u ON u.id = e.user_id WHERE e.email = ?`, email).Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } return userScanToRows(rows), nil } func (m *Repo) UserByID(ctx context.Context, id int64) (*UserRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []userScanRow err = db.Raw(` SELECT u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, COALESCE(e.email,'') AS email, COALESCE((SELECT d.device_id FROM t_user_devices d WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), 0) AS device_id, COALESCE((SELECT CAST(dev.device_type AS CHAR) FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id WHERE d.user_id = u.id ORDER BY d.last_login_at DESC LIMIT 1), '') AS platform FROM t_users u LEFT JOIN t_user_emails e ON e.user_id = u.id WHERE u.id = ?`, id).Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } if len(rows) == 0 || rows[0].ID == 0 { return nil, errs.ErrNotFound } r := userScanToRow(&rows[0]) return &r, nil } func (m *Repo) UsersByNickname(ctx context.Context, nickname string) ([]UserRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []userScanRow err = db.Raw(` SELECT u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, COALESCE(e.email,'') AS email, 0 AS device_id, '' AS platform FROM t_users u LEFT JOIN t_user_emails e ON e.user_id = u.id WHERE u.nickname LIKE ? ORDER BY u.id DESC LIMIT 50`, "%"+strings.TrimSpace(nickname)+"%").Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } return userScanToRows(rows), nil } func (m *Repo) UsersByDevice(ctx context.Context, deviceID string, deviceIDNum uint64) ([]UserRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []userScanRow err = db.Raw(` SELECT u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, COALESCE(e.email,'') AS email, d.device_id, CAST(dev.device_type AS CHAR) AS platform FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id INNER JOIN t_users u ON u.id = d.user_id LEFT JOIN t_user_emails e ON e.user_id = u.id WHERE CAST(d.device_id AS CHAR) = ? OR d.device_id = ? ORDER BY d.last_login_at DESC LIMIT 50`, deviceID, deviceIDNum).Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } return userScanToRows(rows), nil } func (m *Repo) UsersByIP(ctx context.Context, ip string) ([]UserRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []userScanRow err = db.Raw(` SELECT DISTINCT u.id, u.spark_id, u.nickname, u.avatar, u.status, u.last_active_at, u.created_at, COALESCE(e.email,'') AS email, d.device_id, CAST(dev.device_type AS CHAR) AS platform FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id INNER JOIN t_users u ON u.id = d.user_id LEFT JOIN t_user_emails e ON e.user_id = u.id WHERE dev.last_client_ip = ? ORDER BY u.id DESC LIMIT 100`, ip).Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } return userScanToRows(rows), nil } func (m *Repo) SetUserStatusByEmail(ctx context.Context, email string, status userentity.Status) error { db, err := m.session(ctx) if err != nil { return err } res := db.Exec(` UPDATE t_users u INNER JOIN t_user_emails e ON e.user_id = u.id SET u.status = ?, u.updated_at = ? WHERE e.email = ?`, int(status), time.Now(), email) if res.Error != nil { return mapDBErr(res.Error) } if res.RowsAffected == 0 { return errs.ErrNotFound } return nil } func (m *Repo) SetNicknameByEmail(ctx context.Context, email, nickname string) error { db, err := m.session(ctx) if err != nil { return err } res := db.Exec(` UPDATE t_users u INNER JOIN t_user_emails e ON e.user_id = u.id SET u.nickname = ?, u.updated_at = ? WHERE e.email = ?`, nickname, time.Now(), email) if res.Error != nil { return mapDBErr(res.Error) } if res.RowsAffected == 0 { return errs.ErrNotFound } return nil } func (m *Repo) ClearAvatarByEmail(ctx context.Context, email string) error { db, err := m.session(ctx) if err != nil { return err } res := db.Exec(` UPDATE t_users u INNER JOIN t_user_emails e ON e.user_id = u.id SET u.avatar = '', u.updated_at = ? WHERE e.email = ?`, time.Now(), email) if res.Error != nil { return mapDBErr(res.Error) } if res.RowsAffected == 0 { return errs.ErrNotFound } return nil } type SameIPRankRow struct { IP string UserCount int64 DeviceCount int64 } func (m *Repo) SameIPRank(ctx context.Context, limit int) ([]SameIPRankRow, error) { db, err := m.session(ctx) if err != nil { return nil, err } var rows []struct { IP string `gorm:"column:ip"` UserCount int64 `gorm:"column:user_count"` DeviceCount int64 `gorm:"column:device_count"` } err = db.Raw(` SELECT dev.last_client_ip AS ip, COUNT(DISTINCT d.user_id) AS user_count, COUNT(*) AS device_count FROM t_user_devices d INNER JOIN t_devices dev ON dev.device_id = d.device_id WHERE dev.last_client_ip IS NOT NULL AND dev.last_client_ip <> '' AND dev.last_client_ip NOT LIKE '127.%' AND dev.last_client_ip <> '::1' GROUP BY dev.last_client_ip ORDER BY user_count DESC LIMIT ?`, limit).Scan(&rows).Error if err != nil { return nil, mapDBErr(err) } out := make([]SameIPRankRow, len(rows)) for i := range rows { out[i] = SameIPRankRow{IP: rows[i].IP, UserCount: rows[i].UserCount, DeviceCount: rows[i].DeviceCount} } return out, nil } func (m *Repo) LookupUserIDByEmail(ctx context.Context, email string) (int64, error) { db, err := m.session(ctx) if err != nil { return 0, err } var id int64 err = db.Table("t_user_emails").Select("user_id").Where("email = ?", email).Scan(&id).Error return id, mapDBErr(err) } type userScanRow struct { ID int64 `gorm:"column:id"` SparkID string `gorm:"column:spark_id"` Nickname string `gorm:"column:nickname"` Avatar string `gorm:"column:avatar"` Status int `gorm:"column:status"` LastActiveAt *time.Time `gorm:"column:last_active_at"` CreatedAt time.Time `gorm:"column:created_at"` Email string `gorm:"column:email"` DeviceID uint64 `gorm:"column:device_id"` Platform string `gorm:"column:platform"` } func userScanToRows(rows []userScanRow) []UserRow { out := make([]UserRow, 0, len(rows)) for i := range rows { out = append(out, userScanToRow(&rows[i])) } return out } func userScanToRow(r *userScanRow) UserRow { return UserRow(*r) }