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) { identity, err := r.queryActiveIdentity(ctx, "du.user_id = ?", userID) if err == nil { return identity, nil } if err != sql.ErrNoRows { return userdomain.Identity{}, err } // 旧数据如果缺少 user_display_user_ids active 行,仍回退 users 快照,避免身份表补数据前直接阻断用户。 user, err := userstorage.New(r.db).GetUser(ctx, userID) if err != nil { return userdomain.Identity{}, err } 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 } identity, err := r.queryActiveIdentity(ctx, "du.display_user_id = ? AND (du.expires_at_ms IS NULL OR du.expires_at_ms = 0 OR du.expires_at_ms > ?)", displayUserID, nowMs) if err == sql.ErrNoRows { // held 默认号、释放号和过期靓号都不能作为当前展示号解析。 return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDNotFound, "display_user_id not found") } if err != nil { return userdomain.Identity{}, err } if err := r.repairUserCurrentIdentity(ctx, identity, nowMs); err != nil { return userdomain.Identity{}, err } return identity, nil } func (r *Repository) queryActiveIdentity(ctx context.Context, clause string, args ...any) (userdomain.Identity, error) { // user_display_user_ids 的 active 行是展示号归属事实,users.current_display_user_id 只是高频读取快照。 // 解析入口优先读取事实表,可以容忍历史脚本或异常中断导致 users 快照暂时没有同步到最新靓号。 appCode := appcode.FromContext(ctx) queryArgs := []any{ string(userdomain.DisplayUserIDKindPretty), appCode, string(userdomain.DisplayUserIDStatusActive), } queryArgs = append(queryArgs, args...) row := r.db.QueryRowContext(ctx, ` SELECT du.app_code, du.user_id, du.display_user_id, u.default_display_user_id, du.display_user_id_kind, COALESCE(du.expires_at_ms, 0), COALESCE(( SELECT pdi.pretty_id FROM pretty_display_user_id_leases pdl LEFT JOIN pretty_display_ids pdi ON pdi.app_code = pdl.app_code AND pdi.assigned_lease_id = pdl.lease_id AND pdi.assigned_user_id = pdl.user_id WHERE pdl.app_code = du.app_code AND pdl.user_id = du.user_id AND pdl.display_user_id = du.display_user_id AND pdl.status = 'active' ORDER BY pdl.starts_at_ms DESC LIMIT 1 ), ''), CASE WHEN du.display_user_id_kind = ? THEN du.display_user_id ELSE '' END, du.status FROM user_display_user_ids du INNER JOIN users u ON u.app_code = du.app_code AND u.user_id = du.user_id WHERE du.app_code = ? AND du.status = ? AND `+clause+` LIMIT 1 `, queryArgs...) var identity userdomain.Identity var kind string var status string if err := row.Scan( &identity.AppCode, &identity.UserID, &identity.DisplayUserID, &identity.DefaultDisplayUserID, &kind, &identity.DisplayUserIDExpiresAtMs, &identity.PrettyID, &identity.PrettyDisplayUserID, &status, ); err != nil { return userdomain.Identity{}, err } identity.DisplayUserIDKind = userdomain.DisplayUserIDKind(kind) identity.Status = userdomain.DisplayUserIDStatus(status) return identity, nil } func (r *Repository) repairUserCurrentIdentity(ctx context.Context, identity userdomain.Identity, nowMs int64) error { // users.current_display_user_id 是读取快照,真正归属以 user_display_user_ids active 行为准。 // 读路径发现快照滞后时做幂等同步,保证后续 GetUser、token 和 H5 账号确认都返回同一个当前展示号。 _, err := r.db.ExecContext(ctx, ` UPDATE users SET current_display_user_id = ?, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = ?, updated_at_ms = GREATEST(updated_at_ms, ?) WHERE app_code = ? AND user_id = ? AND ( current_display_user_id <> ? OR current_display_user_id_kind <> ? OR COALESCE(current_display_user_id_expires_at_ms, 0) <> ? ) `, identity.DisplayUserID, string(identity.DisplayUserIDKind), nullableExpiresAt(identity.DisplayUserIDExpiresAtMs), nowMs, appcode.Normalize(identity.AppCode), identity.UserID, identity.DisplayUserID, string(identity.DisplayUserIDKind), identity.DisplayUserIDExpiresAtMs) return err } func nullableExpiresAt(value int64) any { if value <= 0 { return nil } return value } // 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。