2026-06-11 01:02:20 +08:00

260 lines
10 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) {
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。