package mysql import ( "context" "database/sql" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) // GetUserIdentity 按 user_id 返回当前有效展示号快照;过期恢复由 service 层统一触发。 func (r *Repository) GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error) { user, err := r.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 := r.queryUser(ctx, r.db, "WHERE current_display_user_id = ?", displayUserID) if err == sql.ErrNoRows { 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) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.Identity{}, err } defer tx.Rollback() user, err := r.queryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID) if err == sql.ErrNoRows { return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.Identity{}, err } if user.DisplayUserIDExpired(command.ChangedAtMs) { identity, err := r.expirePrettyInTx(ctx, tx, user, "", command.ChangedAtMs, command.RequestID) if err != nil { return userdomain.Identity{}, err } user = userdomain.User{ 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 { 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 } } _, err = tx.ExecContext(ctx, ` UPDATE user_display_user_ids SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ? `, string(userdomain.DisplayUserIDStatusReleased), command.ChangedAtMs, command.Reason, command.ChangedAtMs, 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 (display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, command.NewDisplayUserID, command.UserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive), command.ChangedAtMs, command.ChangedAtMs, command.ChangedAtMs, command.ChangedAtMs) if err != nil { return userdomain.Identity{}, mapDuplicateError(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 user_id = ? `, command.NewDisplayUserID, command.NewDisplayUserID, string(userdomain.DisplayUserIDKindDefault), command.ChangedAtMs, command.UserID) if err != nil { return userdomain.Identity{}, mapDuplicateError(err) } if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{ 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 { return userdomain.Identity{}, err } return userdomain.Identity{ UserID: command.UserID, DisplayUserID: command.NewDisplayUserID, DefaultDisplayUserID: command.NewDisplayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindDefault, Status: userdomain.DisplayUserIDStatusActive, }, nil } // ApplyPrettyDisplayUserID 创建临时靓号租约,并把默认短号置为 held。 func (r *Repository) ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.Identity{}, "", err } defer tx.Rollback() user, err := r.queryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID) if err == sql.ErrNoRows { return userdomain.Identity{}, "", xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.Identity{}, "", err } if user.DisplayUserIDExpired(command.NowMs) { identity, err := r.expirePrettyInTx(ctx, tx, user, "", command.NowMs, command.RequestID) if err != nil { return userdomain.Identity{}, "", err } user.CurrentDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserIDKind = identity.DisplayUserIDKind user.CurrentDisplayUserIDExpiresAtMs = 0 } if user.CurrentDisplayUserIDKind == userdomain.DisplayUserIDKindPretty { return userdomain.Identity{}, "", xerr.New(xerr.DisplayUserIDPrettyActive, "pretty display_user_id is active") } if err := r.assertDisplayUserIDAvailable(ctx, tx, command.PrettyDisplayID, command.UserID, false); err != nil { if xerr.IsCode(err, xerr.DisplayUserIDExists) { return userdomain.Identity{}, "", xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display_user_id is not available") } return userdomain.Identity{}, "", err } expiresAtMs := command.NowMs + command.LeaseDurationMs _, err = tx.ExecContext(ctx, ` UPDATE user_display_user_ids SET status = ?, updated_at_ms = ? WHERE user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ? `, string(userdomain.DisplayUserIDStatusHeld), command.NowMs, command.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive)) if err != nil { return userdomain.Identity{}, "", err } _, err = tx.ExecContext(ctx, ` INSERT INTO pretty_display_user_id_leases (lease_id, display_user_id, user_id, status, starts_at_ms, expires_at_ms, payment_receipt_id, source, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, command.LeaseID, command.PrettyDisplayID, command.UserID, string(userdomain.PrettyLeaseStatusActive), command.NowMs, expiresAtMs, nullableString(command.PaymentReceiptID), command.Source, command.NowMs, command.NowMs) if err != nil { return userdomain.Identity{}, "", mapPrettyDuplicateError(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, expires_at_ms, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, command.PrettyDisplayID, command.UserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive), command.NowMs, command.NowMs, expiresAtMs, command.NowMs, command.NowMs) if err != nil { return userdomain.Identity{}, "", mapPrettyDuplicateError(err) } _, err = tx.ExecContext(ctx, ` UPDATE users SET current_display_user_id = ?, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = ?, updated_at_ms = ? WHERE user_id = ? `, command.PrettyDisplayID, string(userdomain.DisplayUserIDKindPretty), expiresAtMs, command.NowMs, command.UserID) if err != nil { return userdomain.Identity{}, "", mapPrettyDuplicateError(err) } if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{ UserID: command.UserID, OldID: user.CurrentDisplayUserID, NewID: command.PrettyDisplayID, OldKind: user.CurrentDisplayUserIDKind, NewKind: userdomain.DisplayUserIDKindPretty, ChangeType: "pretty_apply", LeaseID: command.LeaseID, Reason: "pretty_apply", OperatorID: command.OperatorUserID, RequestID: command.RequestID, CreatedAtMs: command.NowMs, }); err != nil { return userdomain.Identity{}, "", err } if err := tx.Commit(); err != nil { return userdomain.Identity{}, "", err } return userdomain.Identity{ UserID: command.UserID, DisplayUserID: command.PrettyDisplayID, DefaultDisplayUserID: user.DefaultDisplayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindPretty, DisplayUserIDExpiresAtMs: expiresAtMs, Status: userdomain.DisplayUserIDStatusActive, }, command.LeaseID, nil } // ExpirePrettyDisplayUserID 把到期靓号恢复成默认短号;该方法是定时和懒过期的共同事务。 func (r *Repository) ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.Identity{}, err } defer tx.Rollback() user, err := r.queryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID) if err == sql.ErrNoRows { return userdomain.Identity{}, xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.Identity{}, err } identity, err := r.expirePrettyInTx(ctx, tx, user, command.LeaseID, command.NowMs, command.RequestID) if err != nil { return userdomain.Identity{}, err } if err := tx.Commit(); err != nil { return userdomain.Identity{}, err } return identity, nil } func (r *Repository) expirePrettyByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64, requestID string) error { var userID int64 err := r.db.QueryRowContext(ctx, ` SELECT user_id FROM users WHERE current_display_user_id_kind = ? AND current_display_user_id_expires_at_ms IS NOT NULL AND current_display_user_id_expires_at_ms <= ? AND (current_display_user_id = ? OR default_display_user_id = ?) LIMIT 1 `, string(userdomain.DisplayUserIDKindPretty), nowMs, displayUserID, displayUserID).Scan(&userID) if err == sql.ErrNoRows { return nil } if err != nil { return err } _, err = r.ExpirePrettyDisplayUserID(ctx, userdomain.ExpirePrettyDisplayUserIDCommand{ UserID: userID, RequestID: requestID, NowMs: nowMs, }) return err } func (r *Repository) expirePrettyInTx(ctx context.Context, tx *sql.Tx, user userdomain.User, leaseID string, nowMs int64, requestID string) (userdomain.Identity, error) { user = user.NormalizeDisplayUserIDState() if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty { return user.EffectiveIdentity(), nil } if user.CurrentDisplayUserIDExpiresAtMs > nowMs { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not expired") } var activeLeaseID string err := tx.QueryRowContext(ctx, ` SELECT lease_id FROM pretty_display_user_id_leases WHERE user_id = ? AND status = ? FOR UPDATE `, user.UserID, string(userdomain.PrettyLeaseStatusActive)).Scan(&activeLeaseID) if err != nil && err != sql.ErrNoRows { return userdomain.Identity{}, err } if leaseID != "" && activeLeaseID != "" && leaseID != activeLeaseID { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not active") } _, err = tx.ExecContext(ctx, ` UPDATE pretty_display_user_id_leases SET status = ?, updated_at_ms = ? WHERE user_id = ? AND status = ? `, string(userdomain.PrettyLeaseStatusExpired), nowMs, user.UserID, string(userdomain.PrettyLeaseStatusActive)) if err != nil { return userdomain.Identity{}, err } _, err = tx.ExecContext(ctx, ` UPDATE user_display_user_ids SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ? `, string(userdomain.DisplayUserIDStatusExpired), nowMs, "pretty_expire", nowMs, user.UserID, user.CurrentDisplayUserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive)) if err != nil { return userdomain.Identity{}, err } _, err = tx.ExecContext(ctx, ` UPDATE user_display_user_ids SET status = ?, activated_at_ms = ?, updated_at_ms = ? WHERE user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ? `, string(userdomain.DisplayUserIDStatusActive), nowMs, nowMs, user.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusHeld)) if err != nil { return userdomain.Identity{}, err } _, err = tx.ExecContext(ctx, ` UPDATE users SET current_display_user_id = default_display_user_id, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = NULL, updated_at_ms = ? WHERE user_id = ? `, string(userdomain.DisplayUserIDKindDefault), nowMs, user.UserID) if err != nil { return userdomain.Identity{}, err } if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{ UserID: user.UserID, OldID: user.CurrentDisplayUserID, NewID: user.DefaultDisplayUserID, OldKind: userdomain.DisplayUserIDKindPretty, NewKind: userdomain.DisplayUserIDKindDefault, ChangeType: "pretty_expire", LeaseID: activeLeaseID, Reason: "pretty_expire", RequestID: requestID, CreatedAtMs: nowMs, }); err != nil { return userdomain.Identity{}, err } return userdomain.Identity{ UserID: user.UserID, DisplayUserID: user.DefaultDisplayUserID, DefaultDisplayUserID: user.DefaultDisplayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindDefault, Status: userdomain.DisplayUserIDStatusActive, }, nil } func (r *Repository) assertDisplayUserIDAvailable(ctx context.Context, tx *sql.Tx, displayUserID string, userID int64, allowSameOwner bool) error { var existingUserID int64 err := tx.QueryRowContext(ctx, ` SELECT user_id FROM user_display_user_ids WHERE display_user_id = ? AND status IN (?, ?, ?, ?) FOR UPDATE `, displayUserID, string(userdomain.DisplayUserIDStatusActive), string(userdomain.DisplayUserIDStatusHeld), string(userdomain.DisplayUserIDStatusReserved), string(userdomain.DisplayUserIDStatusBlocked)).Scan(&existingUserID) if err == sql.ErrNoRows { return nil } if err != nil { return err } if allowSameOwner && existingUserID == userID { return nil } return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } func (r *Repository) assertChangeCooldown(ctx context.Context, tx *sql.Tx, command userdomain.DisplayUserIDChangeCommand) error { var lastChangedAt sql.NullInt64 err := tx.QueryRowContext(ctx, ` SELECT MAX(created_at_ms) FROM display_user_id_change_logs WHERE user_id = ? AND change_type = ? `, command.UserID, "default_change").Scan(&lastChangedAt) if err != nil { return err } if lastChangedAt.Valid && command.ChangedAtMs-lastChangedAt.Int64 < command.CooldownMs { return xerr.New(xerr.DisplayUserIDCooldown, "display_user_id change is cooling down") } return nil } type displayUserIDLog struct { UserID int64 OldID string NewID string OldKind userdomain.DisplayUserIDKind NewKind userdomain.DisplayUserIDKind ChangeType string LeaseID string Reason string OperatorID int64 RequestID string CreatedAtMs int64 } func insertDisplayUserIDChangeLog(ctx context.Context, tx *sql.Tx, item displayUserIDLog) error { _, err := tx.ExecContext(ctx, ` INSERT INTO display_user_id_change_logs (user_id, old_display_user_id, new_display_user_id, old_display_user_id_kind, new_display_user_id_kind, change_type, lease_id, reason, operator_user_id, request_id, created_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, item.UserID, item.OldID, item.NewID, string(item.OldKind), string(item.NewKind), item.ChangeType, nullableString(item.LeaseID), item.Reason, nullableOperator(item.OperatorID), item.RequestID, item.CreatedAtMs) return err }