435 lines
17 KiB
Go
435 lines
17 KiB
Go
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
|
|
}
|