260 lines
10 KiB
Go
260 lines
10 KiB
Go
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。
|