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

274 lines
12 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) ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) {
// 靓号申请涉及租约、默认短号 held、当前展示号覆盖和变更日志必须事务化。
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 {
// 用户不存在不能创建租约。
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 {
// 同一用户不能叠加 active 靓号。
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) {
// 靓号冲突使用专用 reason便于前端展示购买失败原因。
return userdomain.Identity{}, "", xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display_user_id is not available")
}
return userdomain.Identity{}, "", err
}
// 默认短号从 active 改为 held过期时会重新激活。
expiresAtMs := command.NowMs + command.LeaseDurationMs
_, err = tx.ExecContext(ctx, `
UPDATE user_display_user_ids
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
`, string(userdomain.DisplayUserIDStatusHeld), command.NowMs, 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 pretty_display_user_id_leases (app_code, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, appcode.Normalize(command.AppCode), command.LeaseID, command.PrettyDisplayID, command.UserID, string(userdomain.PrettyLeaseStatusActive), command.NowMs, expiresAtMs, shared.NullableString(command.PaymentReceiptID), command.Source, command.NowMs, command.NowMs)
if err != nil {
return userdomain.Identity{}, "", shared.MapPrettyDuplicateError(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, expires_at_ms, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, appcode.Normalize(command.AppCode), 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{}, "", shared.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 app_code = ? AND user_id = ?
`, command.PrettyDisplayID, string(userdomain.DisplayUserIDKindPretty), expiresAtMs, command.NowMs, appcode.Normalize(command.AppCode), command.UserID)
if err != nil {
return userdomain.Identity{}, "", shared.MapPrettyDuplicateError(err)
}
if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{
AppCode: appcode.Normalize(command.AppCode),
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 {
// commit 失败时租约和展示号覆盖都不能视为成功。
return userdomain.Identity{}, "", err
}
return userdomain.Identity{
AppCode: appcode.Normalize(command.AppCode),
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 := userstorage.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 {
// 只找可能受输入短号影响的过期 pretty 用户,避免每次解析全表扫描。
var userID int64
err := r.db.QueryRowContext(ctx, `
SELECT user_id
FROM users
WHERE app_code = ?
AND 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
`, appcode.FromContext(ctx), 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
}
// ExpirePrettyByDisplayUserID 给认证存储复用短号懒过期事务。
func ExpirePrettyByDisplayUserID(ctx context.Context, db *sql.DB, displayUserID string, nowMs int64, requestID string) error {
return New(db).expirePrettyByDisplayUserID(ctx, displayUserID, nowMs, requestID)
}
func (r *Repository) expirePrettyInTx(ctx context.Context, tx *sql.Tx, user userdomain.User, leaseID string, nowMs int64, requestID string) (userdomain.Identity, error) {
// 调用方必须已经锁住 users 行;本函数继续锁 active lease 行。
if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty {
// 非 pretty 状态重复过期按幂等成功返回当前身份。
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 app_code = ? AND user_id = ? AND status = ?
FOR UPDATE
`, appcode.FromContext(ctx), user.UserID, string(userdomain.PrettyLeaseStatusActive)).Scan(&activeLeaseID)
if err != nil && err != sql.ErrNoRows {
return userdomain.Identity{}, err
}
if leaseID != "" && activeLeaseID != "" && leaseID != activeLeaseID {
// 指定 lease_id 不匹配 active lease 时拒绝,防止旧任务误释放新租约。
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not active")
}
// 租约、pretty 短号记录、默认短号记录和 users 快照一起恢复。
_, err = tx.ExecContext(ctx, `
UPDATE pretty_display_user_id_leases
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND user_id = ? AND status = ?
`, string(userdomain.PrettyLeaseStatusExpired), nowMs, appcode.FromContext(ctx), 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 app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
`, string(userdomain.DisplayUserIDStatusExpired), nowMs, "pretty_expire", nowMs, appcode.FromContext(ctx), 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 app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
`, string(userdomain.DisplayUserIDStatusActive), nowMs, nowMs, appcode.FromContext(ctx), 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 app_code = ? AND user_id = ?
`, string(userdomain.DisplayUserIDKindDefault), nowMs, appcode.FromContext(ctx), user.UserID)
if err != nil {
return userdomain.Identity{}, err
}
if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{
AppCode: appcode.FromContext(ctx),
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{
AppCode: appcode.FromContext(ctx),
UserID: user.UserID,
DisplayUserID: user.DefaultDisplayUserID,
DefaultDisplayUserID: user.DefaultDisplayUserID,
DisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
Status: userdomain.DisplayUserIDStatusActive,
}, nil
}