2026-04-27 02:29:42 +08:00

142 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package mysql 实现 user-service 的 MySQL 持久化边界。
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) {
// 普通查询不加锁,靓号过期恢复由 service 层判断后调用 identity 事务。
user, err := r.queryUser(ctx, r.db, "WHERE user_id = ?", userID)
if err == sql.ErrNoRows {
// users 表无记录时返回稳定 NotFound。
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) {
// 调用方已校验 userIDs 非空且合法,这里直接构造 IN 查询。
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() {
// 缺失用户不会返回,调用方按 map key 判断存在性。
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 {
// 唯一键冲突转换成 display_user_id exists供 service 有限重试。
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) {
// clause 由本包内部固定拼接,不能接受外部原始 SQL。
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) {
// scanUser 是 users 表到领域模型的唯一转换点。
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)
// Normalize 兼容迁移窗口中默认短号或 kind 为空的历史数据。
return user.NormalizeDisplayUserIDState(), nil
}
// insertUserIdentity 是跨 auth/user 创建用户用例共享的事务片段。
// 它只在调用方已经开启事务后执行,避免 service 层手动拼跨表事务。
func insertUserIdentity(ctx context.Context, tx *sql.Tx, user userdomain.User, identity userdomain.Identity) error {
// 调用方已开启事务,本函数只负责插入 users 和默认 active display_user_id。
user = user.NormalizeDisplayUserIDState()
if user.DefaultDisplayUserID == "" {
// 创建路径允许 user 中未预填默认短号,以 identity 为准。
user.DefaultDisplayUserID = identity.DisplayUserID
}
if user.CurrentDisplayUserID == "" {
user.CurrentDisplayUserID = identity.DisplayUserID
}
if user.CurrentDisplayUserIDKind == "" {
// 新用户默认短号永远是 default kind。
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)
// user_display_user_ids 唯一约束保证默认短号不和 active/held/reserved/blocked 记录冲突。
return err
}