package mysql import ( "context" "database/sql" "fmt" "strings" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) // GetUser 从 users 表读取用户主状态;调用方负责按业务时钟触发懒过期。 func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User, error) { user, err := r.queryUser(ctx, r.db, "WHERE user_id = ?", userID) if err == sql.ErrNoRows { return userdomain.User{}, xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.User{}, err } return user, nil } // BatchGetUsers 批量读取用户主状态,缺失用户不会出现在返回 map 中。 func (r *Repository) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) { placeholders := make([]string, 0, len(userIDs)) args := make([]any, 0, len(userIDs)) for _, userID := range userIDs { placeholders = append(placeholders, "?") args = append(args, userID) } rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind, COALESCE(current_display_user_id_expires_at_ms, 0), status, created_at_ms, updated_at_ms FROM users WHERE user_id IN (%s) `, strings.Join(placeholders, ",")), args...) if err != nil { return nil, err } defer rows.Close() result := make(map[int64]userdomain.User, len(userIDs)) for rows.Next() { user, err := scanUser(rows) if err != nil { return nil, err } result[user.UserID] = user } return result, rows.Err() } // CreateUserWithIdentity 是用户创建的事务入口。 // 它同时写 users 和默认短号 active 记录,避免出现没有默认门牌号的用户。 func (r *Repository) CreateUserWithIdentity(ctx context.Context, user userdomain.User, identity userdomain.Identity) error { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if err := insertUserIdentity(ctx, tx, user, identity); err != nil { return mapDuplicateError(err) } return tx.Commit() } // queryUser 读取 users 快照字段;既服务普通查询,也服务 identity 事务中的 FOR UPDATE 读。 func (r *Repository) queryUser(ctx context.Context, q queryer, clause string, args ...any) (userdomain.User, error) { row := q.QueryRowContext(ctx, ` SELECT user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind, COALESCE(current_display_user_id_expires_at_ms, 0), status, created_at_ms, updated_at_ms FROM users `+clause, args...) return scanUser(row) } // scanUser 固定 users 投影到领域模型的转换,并补齐迁移窗口中的默认值。 func scanUser(scanner interface { Scan(dest ...any) error }) (userdomain.User, error) { var user userdomain.User var status string var kind string err := scanner.Scan(&user.UserID, &user.DefaultDisplayUserID, &user.CurrentDisplayUserID, &kind, &user.CurrentDisplayUserIDExpiresAtMs, &status, &user.CreatedAtMs, &user.UpdatedAtMs) if err != nil { return userdomain.User{}, err } user.Status = userdomain.Status(status) user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKind(kind) return user.NormalizeDisplayUserIDState(), nil } // insertUserIdentity 是跨 auth/user 创建用户用例共享的事务片段。 // 它只在调用方已经开启事务后执行,避免 service 层手动拼跨表事务。 func insertUserIdentity(ctx context.Context, tx *sql.Tx, user userdomain.User, identity userdomain.Identity) error { user = user.NormalizeDisplayUserIDState() if user.DefaultDisplayUserID == "" { user.DefaultDisplayUserID = identity.DisplayUserID } if user.CurrentDisplayUserID == "" { user.CurrentDisplayUserID = identity.DisplayUserID } if user.CurrentDisplayUserIDKind == "" { user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault } _, err := tx.ExecContext(ctx, ` INSERT INTO users (user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind, current_display_user_id_expires_at_ms, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, NULL, ?, ?, ?) `, user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs) if err != nil { return err } _, err = tx.ExecContext(ctx, ` INSERT INTO user_display_user_ids (display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, identity.DisplayUserID, user.UserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive), user.CreatedAtMs, user.CreatedAtMs, user.CreatedAtMs, user.UpdatedAtMs) return err }