2026-05-02 13:02:38 +08:00

161 lines
6.8 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 identity
import (
"context"
"database/sql"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
"hyapp/services/user-service/internal/storage/mysql/shared"
userstorage "hyapp/services/user-service/internal/storage/mysql/user"
)
func (r *Repository) GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error) {
// 直接读取 users 当前快照;是否懒过期由 service 层统一判断。
user, err := userstorage.New(r.db).GetUser(ctx, userID)
if err != nil {
return userdomain.Identity{}, err
}
if user.CurrentDisplayUserID == "" {
// 理论上用户创建事务会保证短号存在,该分支保护脏数据。
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDNotFound, "display_user_id not found")
}
return user.EffectiveIdentity(), nil
}
// ResolveDisplayUserID 只解析当前有效展示号。
// 查询前先按输入短号触发懒过期,保证过期靓号不继续解析,默认短号能恢复解析。
func (r *Repository) ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error) {
// 解析前按输入短号触发懒过期,避免过期靓号继续被解析。
if err := r.expirePrettyByDisplayUserID(ctx, displayUserID, nowMs, "lazy_resolve"); err != nil {
return userdomain.Identity{}, err
}
user, err := userstorage.QueryUser(ctx, r.db, "WHERE current_display_user_id = ?", displayUserID)
if err == sql.ErrNoRows {
// held 默认号和过期靓号都不会匹配 current_display_user_id。
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDNotFound, "display_user_id not found")
}
if err != nil {
return userdomain.Identity{}, err
}
return user.EffectiveIdentity(), nil
}
// ChangeDisplayUserID 修改默认短号;有 active 靓号时拒绝,避免过期恢复目标不清晰。
func (r *Repository) ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) {
// 默认短号修改涉及 users、短号记录和变更日志必须在一个事务内完成。
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return userdomain.Identity{}, err
}
defer tx.Rollback()
user, err := userstorage.QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID)
if err == sql.ErrNoRows {
// FOR UPDATE 锁住用户行,避免并发修改默认短号或申请靓号。
return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found")
}
if err != nil {
return userdomain.Identity{}, err
}
if user.DisplayUserIDExpired(command.ChangedAtMs) {
// 修改前先恢复过期靓号,避免默认短号仍处于 held。
identity, err := r.expirePrettyInTx(ctx, tx, user, "", command.ChangedAtMs, command.RequestID)
if err != nil {
return userdomain.Identity{}, err
}
user = userdomain.User{
AppCode: user.AppCode,
UserID: identity.UserID,
DefaultDisplayUserID: identity.DefaultDisplayUserID,
CurrentDisplayUserID: identity.DisplayUserID,
CurrentDisplayUserIDKind: identity.DisplayUserIDKind,
Status: user.Status,
CreatedAtMs: user.CreatedAtMs,
UpdatedAtMs: command.ChangedAtMs,
}
}
if user.CurrentDisplayUserIDKind == userdomain.DisplayUserIDKindPretty {
// active 靓号期间拒绝改默认短号,保证过期恢复目标稳定。
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDPrettyActive, "pretty display_user_id is active")
}
if user.DefaultDisplayUserID == command.NewDisplayUserID {
// 修改为相同默认短号按幂等成功处理,不写日志。
return user.EffectiveIdentity(), nil
}
if err := r.assertDisplayUserIDAvailable(ctx, tx, command.NewDisplayUserID, command.UserID, true); err != nil {
return userdomain.Identity{}, err
}
if command.CooldownMs > 0 {
// 冷却期检查必须在事务内读取变更日志,避免并发绕过。
if err := r.assertChangeCooldown(ctx, tx, command); err != nil {
return userdomain.Identity{}, err
}
}
// 旧默认短号释放,新默认短号插入为 active。
_, err = tx.ExecContext(ctx, `
UPDATE user_display_user_ids
SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ?
WHERE app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
`, string(userdomain.DisplayUserIDStatusReleased), command.ChangedAtMs, command.Reason, command.ChangedAtMs, appcode.Normalize(command.AppCode), command.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive))
if err != nil {
return userdomain.Identity{}, err
}
_, err = tx.ExecContext(ctx, `
INSERT INTO user_display_user_ids (app_code, display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, appcode.Normalize(command.AppCode), command.NewDisplayUserID, command.UserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive), command.ChangedAtMs, command.ChangedAtMs, command.ChangedAtMs, command.ChangedAtMs)
if err != nil {
return userdomain.Identity{}, shared.MapDisplayDuplicateError(err)
}
_, err = tx.ExecContext(ctx, `
UPDATE users
SET default_display_user_id = ?, current_display_user_id = ?, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = NULL, updated_at_ms = ?
WHERE app_code = ? AND user_id = ?
`, command.NewDisplayUserID, command.NewDisplayUserID, string(userdomain.DisplayUserIDKindDefault), command.ChangedAtMs, appcode.Normalize(command.AppCode), command.UserID)
if err != nil {
return userdomain.Identity{}, shared.MapDisplayDuplicateError(err)
}
if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{
AppCode: appcode.Normalize(command.AppCode),
UserID: command.UserID,
OldID: user.DefaultDisplayUserID,
NewID: command.NewDisplayUserID,
OldKind: userdomain.DisplayUserIDKindDefault,
NewKind: userdomain.DisplayUserIDKindDefault,
ChangeType: "default_change",
Reason: command.Reason,
OperatorID: command.OperatorUserID,
RequestID: command.RequestID,
CreatedAtMs: command.ChangedAtMs,
}); err != nil {
return userdomain.Identity{}, err
}
if err := tx.Commit(); err != nil {
// commit 失败时调用方不能使用返回 identity。
return userdomain.Identity{}, err
}
return userdomain.Identity{
AppCode: appcode.Normalize(command.AppCode),
UserID: command.UserID,
DisplayUserID: command.NewDisplayUserID,
DefaultDisplayUserID: command.NewDisplayUserID,
DisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
Status: userdomain.DisplayUserIDStatusActive,
}, nil
}
// ApplyPrettyDisplayUserID 创建临时靓号租约,并把默认短号置为 held。