package identity import ( "context" "database/sql" "strings" "hyapp/pkg/appcode" "hyapp/pkg/idgen" "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" ) type prettyAssignment struct { // PrettyID 只在靓号池申请和后台发放时存在;旧付费临时靓号没有池表记录,返回身份时只返回展示号。 PrettyID string LeaseID string DisplayUserID string ExpiresAtMs int64 PaymentReceiptID string // Source 决定释放后的池表状态:pool 号码回到 available,admin_grant 号码进入 released 留审计。 Source string ChangeType string Reason string OperatorUserID int64 RequestID string NowMs int64 } type prettyReleaseCommand struct { // LeaseID 为空时按用户当前 active lease 释放;非空时必须匹配当前 active lease,防止旧请求误释放新靓号。 LeaseID string // Reason 写入 lease、展示号、靓号池记录和变更日志,用于区分到期、替换和后台回收。 Reason string // ChangeType 是 display_user_id_change_logs 的业务类型。 ChangeType string // LeaseStatus 表示租约释放后的生命周期状态。 LeaseStatus userdomain.PrettyLeaseStatus // DisplayStatus 表示当前展示号占用释放后的状态,到期用 expired,后台回收用 released。 DisplayStatus userdomain.DisplayUserIDStatus // OperatorAdminID 只有后台回收这类管理动作存在;到期任务为 0。 OperatorAdminID int64 RequestID string NowMs int64 } // ApplyPrettyDisplayUserID 兼容旧付费靓号接口,仍要求租期和支付回执,但允许覆盖旧靓号。 func (r *Repository) ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) { // 旧接口按展示号申请,没有 pretty_id;先懒过期同名到期靓号,避免已过期记录继续占用唯一性。 if err := r.expirePrettyByDisplayUserID(ctx, command.PrettyDisplayID, command.NowMs, command.RequestID); err != nil { return userdomain.Identity{}, "", err } 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 err := r.assertDisplayUserIDGloballyAvailable(ctx, tx, command.PrettyDisplayID); 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 } identity, err := r.replacePrettyInTx(ctx, tx, user, prettyAssignment{ LeaseID: command.LeaseID, DisplayUserID: command.PrettyDisplayID, ExpiresAtMs: command.NowMs + command.LeaseDurationMs, PaymentReceiptID: command.PaymentReceiptID, Source: command.Source, ChangeType: "pretty_apply", Reason: "pretty_apply", OperatorUserID: command.OperatorUserID, RequestID: command.RequestID, NowMs: command.NowMs, }) if err != nil { return userdomain.Identity{}, "", err } if err := tx.Commit(); err != nil { return userdomain.Identity{}, "", err } return identity, command.LeaseID, nil } // ListAvailablePrettyDisplayIDs 返回用户当前等级可申请的靓号池号码。 func (r *Repository) ListAvailablePrettyDisplayIDs(ctx context.Context, query userdomain.AvailablePrettyDisplayIDQuery) ([]userdomain.PrettyDisplayID, int64, error) { appCode := appcode.Normalize(query.AppCode) page, pageSize := normalizeIdentityPage(query.Page, query.PageSize) offset := (page - 1) * pageSize // 可申请列表不接受客户端自报轨道等级;服务层先读取用户真实等级,这里按财富、游戏、魅力三条轨道做 OR 匹配。 levelWhere := ` AND ( (pool.level_track = ? AND pool.min_level <= ? AND pool.max_level >= ?) OR (pool.level_track = ? AND pool.min_level <= ? AND pool.max_level >= ?) OR (pool.level_track = ? AND pool.min_level <= ? AND pool.max_level >= ?) )` levelArgs := []any{ userdomain.LevelTrackWealth, query.Levels.WealthLevel, query.Levels.WealthLevel, userdomain.LevelTrackGame, query.Levels.GameLevel, query.Levels.GameLevel, userdomain.LevelTrackCharm, query.Levels.CharmLevel, query.Levels.CharmLevel, } var total int64 countArgs := append([]any{appCode, userdomain.PrettyDisplayIDStatusAvailable, userdomain.PrettyDisplayIDSourcePool, userdomain.PrettyDisplayIDPoolStatusActive}, levelArgs...) if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM pretty_display_ids pdi JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id WHERE pdi.app_code = ? AND pdi.status = ? AND pdi.source = ? AND pool.status = ? `+levelWhere, countArgs...).Scan(&total); err != nil { return nil, 0, err } selectArgs := append(countArgs, int(pageSize), int(offset)) rows, err := r.db.QueryContext(ctx, ` SELECT `+prettyDisplayIDSelectColumns()+` FROM pretty_display_ids pdi JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id WHERE pdi.app_code = ? AND pdi.status = ? AND pdi.source = ? AND pool.status = ? `+levelWhere+` ORDER BY pool.sort_order ASC, pdi.created_at_ms ASC, pdi.pretty_id ASC LIMIT ? OFFSET ? `, selectArgs...) if err != nil { return nil, 0, err } defer rows.Close() items, err := scanPrettyDisplayIDRows(rows) if err != nil { return nil, 0, err } return items, total, nil } // ApplyPrettyDisplayIDFromPool 申请靓号池号码,号码本身、用户当前等级和用户展示号在同一事务中校验。 func (r *Repository) ApplyPrettyDisplayIDFromPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolApplyCommand) (userdomain.Identity, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.Identity{}, err } defer tx.Rollback() appCode := appcode.Normalize(command.AppCode) // 先锁定 pretty_display_ids 行,保证同一个 pretty_id 只能被一个申请事务从 available 改为 assigned。 item, err := queryPrettyDisplayIDForUpdate(ctx, tx, appCode, command.PrettyID) if err == sql.ErrNoRows { return userdomain.Identity{}, xerr.New(xerr.NotFound, "pretty display id not found") } if err != nil { return userdomain.Identity{}, err } if item.Source != userdomain.PrettyDisplayIDSourcePool || item.Status != userdomain.PrettyDisplayIDStatusAvailable { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display id is not available") } // 池配置也加锁读取,避免管理员同时禁用池或调整等级区间时出现旧规则通过的申请。 pool, err := queryPrettyDisplayIDPoolForUpdate(ctx, tx, appCode, item.PoolID) if err == sql.ErrNoRows { return userdomain.Identity{}, xerr.New(xerr.NotFound, "pretty display id pool not found") } if err != nil { return userdomain.Identity{}, err } if pool.Status != userdomain.PrettyDisplayIDPoolStatusActive { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display id pool is disabled") } // 只校验该池声明的等级轨道;用户在其他轨道等级足够也不能跨池申请。 level, ok := command.Levels.LevelForTrack(pool.LevelTrack) if !ok || level < pool.MinLevel || level > pool.MaxLevel { return userdomain.Identity{}, xerr.New(xerr.PermissionDenied, "user level is not allowed for pretty display id pool") } // 用户行最后锁定,后续替换展示号时以 users 当前快照为准释放旧靓号或挂起默认短号。 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 } // 池申请默认长期持有,expires_at_ms=0;替换当前靓号和写入租约先完成,再把池号码标记为 assigned。 identity, err := r.replacePrettyInTx(ctx, tx, user, prettyAssignment{ PrettyID: item.PrettyID, LeaseID: command.LeaseID, DisplayUserID: item.DisplayUserID, ExpiresAtMs: 0, Source: userdomain.PrettyDisplayIDSourcePool, ChangeType: "pretty_pool_apply", Reason: "pretty_pool_apply", OperatorUserID: command.UserID, RequestID: command.RequestID, NowMs: command.NowMs, }) if err != nil { return userdomain.Identity{}, err } // WHERE status=available 是最后一道并发保护,确保只有锁住并仍可用的池号会被分配。 _, err = tx.ExecContext(ctx, ` UPDATE pretty_display_ids SET status = ?, assigned_user_id = ?, assigned_lease_id = ?, assigned_at_ms = ?, released_at_ms = 0, release_reason = '', updated_at_ms = ? WHERE app_code = ? AND pretty_id = ? AND status = ? `, userdomain.PrettyDisplayIDStatusAssigned, command.UserID, command.LeaseID, command.NowMs, command.NowMs, appCode, command.PrettyID, userdomain.PrettyDisplayIDStatusAvailable) if err != nil { return userdomain.Identity{}, shared.MapPrettyDuplicateError(err) } if err := tx.Commit(); err != nil { return userdomain.Identity{}, err } return identity, 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 { // 申请同名靓号前只处理已经到期的占用;未到期的冲突仍交给唯一性校验返回不可用。 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 > 0 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) { if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty { // 当前已经是默认短号时,过期恢复是幂等操作,直接返回现有身份。 return user.EffectiveIdentity(), nil } if user.CurrentDisplayUserIDExpiresAtMs == 0 || user.CurrentDisplayUserIDExpiresAtMs > nowMs { // expires_at_ms=0 表示长期靓号,正未来时间表示尚未到期,这两类都不能被过期任务释放。 return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not expired") } return r.releaseCurrentPrettyInTx(ctx, tx, user, prettyReleaseCommand{ LeaseID: leaseID, Reason: "pretty_expire", ChangeType: "pretty_expire", LeaseStatus: userdomain.PrettyLeaseStatusExpired, DisplayStatus: userdomain.DisplayUserIDStatusExpired, RequestID: requestID, NowMs: nowMs, }) } // RecyclePrettyDisplayID 强制回收后台列表中仍处于 assigned 的靓号,并把用户恢复到默认短号。 func (r *Repository) RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error) { appCode := appcode.Normalize(command.AppCode) reason := strings.TrimSpace(command.Reason) if reason == "" { // 默认原因必须和到期恢复区分,方便审计和后续排查运营手动动作。 reason = "admin_recycle_pretty" } tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.PrettyDisplayID{}, err } defer tx.Rollback() item, err := queryPrettyDisplayIDForUpdate(ctx, tx, appCode, strings.TrimSpace(command.PrettyID)) if err == sql.ErrNoRows { return userdomain.PrettyDisplayID{}, xerr.New(xerr.NotFound, "pretty display id not found") } if err != nil { return userdomain.PrettyDisplayID{}, err } if item.Status != userdomain.PrettyDisplayIDStatusAssigned || item.AssignedUserID <= 0 || item.AssignedLeaseID == "" { // 只有真实占用中的号码才能回收;可用、禁用、预留和已释放状态仍走普通状态管理。 return userdomain.PrettyDisplayID{}, xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display id is not assigned") } user, err := userstorage.QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", item.AssignedUserID) if err == sql.ErrNoRows { return userdomain.PrettyDisplayID{}, xerr.New(xerr.NotFound, "assigned user not found") } if err != nil { return userdomain.PrettyDisplayID{}, err } if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty || user.CurrentDisplayUserID != item.DisplayUserID { // 列表记录和 users 当前展示号不一致时不能盲目清表,交给后续数据修复处理,避免回收错用户的新靓号。 return userdomain.PrettyDisplayID{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display id lease is not active") } if _, err := r.releaseCurrentPrettyInTx(ctx, tx, user, prettyReleaseCommand{ LeaseID: item.AssignedLeaseID, Reason: reason, ChangeType: "admin_recycle_pretty", LeaseStatus: userdomain.PrettyLeaseStatusCancelled, DisplayStatus: userdomain.DisplayUserIDStatusReleased, OperatorAdminID: command.OperatorAdminID, RequestID: command.RequestID, NowMs: command.NowMs, }); err != nil { return userdomain.PrettyDisplayID{}, err } if err := tx.Commit(); err != nil { return userdomain.PrettyDisplayID{}, err } return queryPrettyDisplayID(ctx, r.db, appCode, item.PrettyID) } func (r *Repository) releaseCurrentPrettyInTx(ctx context.Context, tx *sql.Tx, user userdomain.User, command prettyReleaseCommand) (userdomain.Identity, error) { appCode := appcode.FromContext(ctx) // 先锁 active lease,再按调用方传入的 lease_id 做精确校验;后台回收必须命中列表行里的 lease,过期任务可以空 lease 幂等恢复。 activeLeaseID, _, err := lockActivePrettyLease(ctx, tx, appCode, user.UserID) if err != nil { return userdomain.Identity{}, err } if command.LeaseID != "" && activeLeaseID != command.LeaseID { return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not active") } if command.LeaseID != "" { _, err = tx.ExecContext(ctx, ` UPDATE pretty_display_user_id_leases SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE app_code = ? AND lease_id = ? AND status = ? `, string(command.LeaseStatus), command.NowMs, command.Reason, command.NowMs, appCode, command.LeaseID, string(userdomain.PrettyLeaseStatusActive)) } else { _, err = tx.ExecContext(ctx, ` UPDATE pretty_display_user_id_leases SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE app_code = ? AND user_id = ? AND status = ? `, string(command.LeaseStatus), command.NowMs, command.Reason, command.NowMs, appCode, 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(command.DisplayStatus), command.NowMs, command.Reason, command.NowMs, appCode, user.UserID, user.CurrentDisplayUserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive)) if err != nil { return userdomain.Identity{}, err } if activeLeaseID != "" { // 池号和后台发放号释放后的状态不同,统一通过 lease 反查池表行处理。 if err := releasePrettyDisplayIDByLeaseInTx(ctx, tx, appCode, activeLeaseID, command.NowMs, command.Reason); err != nil { return userdomain.Identity{}, err } } // 默认短号在用户使用靓号期间保持 held;释放时只把原默认号重新激活,不重新生成短号。 _, 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), command.NowMs, command.NowMs, appCode, 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), command.NowMs, appCode, user.UserID) if err != nil { return userdomain.Identity{}, err } if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{ AppCode: appCode, UserID: user.UserID, OldID: user.CurrentDisplayUserID, NewID: user.DefaultDisplayUserID, OldKind: userdomain.DisplayUserIDKindPretty, NewKind: userdomain.DisplayUserIDKindDefault, ChangeType: command.ChangeType, LeaseID: activeLeaseID, Reason: command.Reason, OperatorID: command.OperatorAdminID, RequestID: command.RequestID, CreatedAtMs: command.NowMs, }); err != nil { return userdomain.Identity{}, err } return userdomain.Identity{ AppCode: appCode, UserID: user.UserID, DisplayUserID: user.DefaultDisplayUserID, DefaultDisplayUserID: user.DefaultDisplayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindDefault, Status: userdomain.DisplayUserIDStatusActive, }, nil } func (r *Repository) replacePrettyInTx(ctx context.Context, tx *sql.Tx, user userdomain.User, assignment prettyAssignment) (userdomain.Identity, error) { appCode := appcode.FromContext(ctx) if user.DisplayUserIDExpired(assignment.NowMs) { // 替换前先在同一事务内清理已过期靓号,把 users 快照恢复到默认号,后续日志的 old_id 才准确。 identity, err := r.expirePrettyInTx(ctx, tx, user, "", assignment.NowMs, assignment.RequestID) if err != nil { return userdomain.Identity{}, err } user.CurrentDisplayUserID = identity.DisplayUserID user.CurrentDisplayUserIDKind = identity.DisplayUserIDKind user.CurrentDisplayUserIDExpiresAtMs = 0 user.PrettyID = "" user.PrettyDisplayUserID = "" } if user.CurrentDisplayUserIDKind == userdomain.DisplayUserIDKindPretty { // 用户当前已有 active 靓号时,新靓号直接覆盖旧靓号,旧 lease 必须先取消并记录释放原因。 activeLeaseID, _, err := lockActivePrettyLease(ctx, tx, appCode, user.UserID) if err != nil { return userdomain.Identity{}, err } if 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 = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE app_code = ? AND lease_id = ? AND status = ? `, string(userdomain.PrettyLeaseStatusCancelled), assignment.NowMs, "replaced_by_new_pretty", assignment.NowMs, appCode, activeLeaseID, string(userdomain.PrettyLeaseStatusActive)) if err != nil { return userdomain.Identity{}, err } // active 的 user_display_user_ids 代表当前展示号占用,覆盖时释放该占用,避免同一用户出现两个 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), assignment.NowMs, "replaced_by_new_pretty", assignment.NowMs, appCode, user.UserID, user.CurrentDisplayUserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive)) if err != nil { return userdomain.Identity{}, err } // 旧靓号对应的池表记录按来源释放:池号回可申请,后台发放号保留 released 审计态。 if err := releasePrettyDisplayIDByLeaseInTx(ctx, tx, appCode, activeLeaseID, assignment.NowMs, "replaced_by_new_pretty"); err != nil { return userdomain.Identity{}, err } } else { // 用户从默认短号切到靓号时,不删除默认短号,只把它挂起,后续过期或释放可以原样恢复。 _, 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), assignment.NowMs, appCode, user.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive)) if err != nil { return userdomain.Identity{}, err } } // 租约是靓号生命周期的主记录;长期靓号 expires_at_ms=0,限时靓号写入明确到期时间。 _, 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, released_at_ms, release_reason, payment_receipt_id, source, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, '', ?, ?, ?, ?) `, appCode, assignment.LeaseID, assignment.DisplayUserID, user.UserID, string(userdomain.PrettyLeaseStatusActive), assignment.NowMs, assignment.ExpiresAtMs, shared.NullableString(assignment.PaymentReceiptID), assignment.Source, assignment.NowMs, assignment.NowMs) if err != nil { return userdomain.Identity{}, shared.MapPrettyDuplicateError(err) } // 当前展示号占用单独写入 user_display_user_ids,和 users 快照互相校验,便于全局唯一性检查。 _, 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, assignment.DisplayUserID, user.UserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive), assignment.NowMs, assignment.NowMs, assignment.ExpiresAtMs, assignment.NowMs, assignment.NowMs) if err != nil { return userdomain.Identity{}, shared.MapPrettyDuplicateError(err) } // users 表保存高频读取快照,资料接口不用每次 join 租约表也能返回当前展示号和到期时间。 _, 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 = ? `, assignment.DisplayUserID, string(userdomain.DisplayUserIDKindPretty), assignment.ExpiresAtMs, assignment.NowMs, appCode, user.UserID) if err != nil { return userdomain.Identity{}, shared.MapPrettyDuplicateError(err) } // 展示号变更日志记录 old/new、操作者、请求 ID,覆盖、池申请、后台发放和旧付费接口都复用同一审计结构。 if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{ AppCode: appCode, UserID: user.UserID, OldID: user.CurrentDisplayUserID, NewID: assignment.DisplayUserID, OldKind: user.CurrentDisplayUserIDKind, NewKind: userdomain.DisplayUserIDKindPretty, ChangeType: assignment.ChangeType, LeaseID: assignment.LeaseID, Reason: assignment.Reason, OperatorID: assignment.OperatorUserID, RequestID: assignment.RequestID, CreatedAtMs: assignment.NowMs, }); err != nil { return userdomain.Identity{}, err } return userdomain.Identity{ AppCode: appCode, UserID: user.UserID, DisplayUserID: assignment.DisplayUserID, DefaultDisplayUserID: user.DefaultDisplayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindPretty, DisplayUserIDExpiresAtMs: assignment.ExpiresAtMs, PrettyID: assignment.PrettyID, PrettyDisplayUserID: assignment.DisplayUserID, Status: userdomain.DisplayUserIDStatusActive, }, nil } func lockActivePrettyLease(ctx context.Context, tx *sql.Tx, appCode string, userID int64) (string, string, error) { var leaseID string var displayUserID string err := tx.QueryRowContext(ctx, ` SELECT lease_id, display_user_id FROM pretty_display_user_id_leases WHERE app_code = ? AND user_id = ? AND status = ? FOR UPDATE `, appcode.Normalize(appCode), userID, string(userdomain.PrettyLeaseStatusActive)).Scan(&leaseID, &displayUserID) if err == sql.ErrNoRows { return "", "", nil } if err != nil { return "", "", err } return leaseID, displayUserID, nil } func releasePrettyDisplayIDByLeaseInTx(ctx context.Context, tx *sql.Tx, appCode string, leaseID string, nowMs int64, reason string) error { // source=pool 的号码释放后重新进入池;后台手动发放的自由靓号没有池归属,释放后进入 released 防止自动复用。 _, err := tx.ExecContext(ctx, ` UPDATE pretty_display_ids SET status = CASE WHEN source = ? THEN ? ELSE ? END, assigned_user_id = 0, assigned_lease_id = '', assigned_at_ms = 0, released_at_ms = ?, release_reason = ?, updated_at_ms = ? WHERE app_code = ? AND assigned_lease_id = ? AND status = ? `, userdomain.PrettyDisplayIDSourcePool, userdomain.PrettyDisplayIDStatusAvailable, userdomain.PrettyDisplayIDStatusReleased, nowMs, reason, nowMs, appcode.Normalize(appCode), leaseID, userdomain.PrettyDisplayIDStatusAssigned) return err } func (r *Repository) assertDisplayUserIDGloballyAvailable(ctx context.Context, tx *sql.Tx, displayUserID string) error { // users 表同时覆盖真实 user_id、默认短号和当前展示号,避免管理员发放成任何用户已使用的 ID。 var exists int err := tx.QueryRowContext(ctx, ` SELECT 1 FROM users WHERE app_code = ? AND (CAST(user_id AS CHAR) = ? OR default_display_user_id = ? OR current_display_user_id = ?) LIMIT 1 `, appcode.FromContext(ctx), displayUserID, displayUserID, displayUserID).Scan(&exists) if err != nil && err != sql.ErrNoRows { return err } if exists == 1 { return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } // user_display_user_ids 覆盖 active/held/reserved/blocked 历史占用,默认短号 held 时也不能被靓号抢占。 err = tx.QueryRowContext(ctx, ` SELECT 1 FROM user_display_user_ids WHERE app_code = ? AND display_user_id = ? AND status IN (?, ?, ?, ?) LIMIT 1 `, appcode.FromContext(ctx), displayUserID, string(userdomain.DisplayUserIDStatusActive), string(userdomain.DisplayUserIDStatusHeld), string(userdomain.DisplayUserIDStatusReserved), string(userdomain.DisplayUserIDStatusBlocked)).Scan(&exists) if err != nil && err != sql.ErrNoRows { return err } if exists == 1 { return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } // pretty_display_ids 覆盖池中未发放、已发放、禁用和预留号码,防止批量生成和后台发放互相撞号。 err = tx.QueryRowContext(ctx, ` SELECT 1 FROM pretty_display_ids WHERE app_code = ? AND display_user_id = ? AND status IN (?, ?, ?, ?) LIMIT 1 `, appcode.FromContext(ctx), displayUserID, userdomain.PrettyDisplayIDStatusAvailable, userdomain.PrettyDisplayIDStatusAssigned, userdomain.PrettyDisplayIDStatusDisabled, userdomain.PrettyDisplayIDStatusReserved).Scan(&exists) if err != nil && err != sql.ErrNoRows { return err } if exists == 1 { return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists") } return nil } // ListPrettyDisplayIDPools 分页读取后台靓号池配置。 func (r *Repository) ListPrettyDisplayIDPools(ctx context.Context, query userdomain.PrettyDisplayIDPoolQuery) ([]userdomain.PrettyDisplayIDPool, int64, error) { appCode := appcode.Normalize(query.AppCode) page, pageSize := normalizeIdentityPage(query.Page, query.PageSize) where := []string{"app_code = ?"} args := []any{appCode} if query.Status != "" { where = append(where, "status = ?") args = append(args, query.Status) } if query.LevelTrack != "" { where = append(where, "level_track = ?") args = append(args, query.LevelTrack) } whereSQL := "WHERE " + strings.Join(where, " AND ") var total int64 if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM pretty_display_id_pools "+whereSQL, args...).Scan(&total); err != nil { return nil, 0, err } rows, err := r.db.QueryContext(ctx, ` SELECT app_code, pool_id, name, level_track, min_level, max_level, rule_type, COALESCE(CAST(rule_config_json AS CHAR), ''), status, sort_order, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms FROM pretty_display_id_pools `+whereSQL+` ORDER BY sort_order ASC, created_at_ms DESC, pool_id ASC LIMIT ? OFFSET ? `, append(args, int(pageSize), int((page-1)*pageSize))...) if err != nil { return nil, 0, err } defer rows.Close() pools := make([]userdomain.PrettyDisplayIDPool, 0) for rows.Next() { pool, err := scanPrettyDisplayIDPool(rows) if err != nil { return nil, 0, err } pools = append(pools, pool) } return pools, total, rows.Err() } func (r *Repository) CreatePrettyDisplayIDPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolCommand) (userdomain.PrettyDisplayIDPool, error) { _, err := r.db.ExecContext(ctx, ` INSERT INTO pretty_display_id_pools ( app_code, pool_id, name, level_track, min_level, max_level, rule_type, rule_config_json, status, sort_order, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, appcode.Normalize(command.AppCode), command.PoolID, command.Name, command.LevelTrack, command.MinLevel, command.MaxLevel, command.RuleType, nullableJSON(command.RuleConfigJSON), command.Status, command.SortOrder, command.OperatorAdminID, command.OperatorAdminID, command.NowMs, command.NowMs) if err != nil { return userdomain.PrettyDisplayIDPool{}, err } return queryPrettyDisplayIDPool(ctx, r.db, appcode.Normalize(command.AppCode), command.PoolID) } func (r *Repository) UpdatePrettyDisplayIDPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolCommand) (userdomain.PrettyDisplayIDPool, error) { result, err := r.db.ExecContext(ctx, ` UPDATE pretty_display_id_pools SET name = ?, level_track = ?, min_level = ?, max_level = ?, rule_type = ?, rule_config_json = ?, status = ?, sort_order = ?, updated_by_admin_id = ?, updated_at_ms = ? WHERE app_code = ? AND pool_id = ? `, command.Name, command.LevelTrack, command.MinLevel, command.MaxLevel, command.RuleType, nullableJSON(command.RuleConfigJSON), command.Status, command.SortOrder, command.OperatorAdminID, command.NowMs, appcode.Normalize(command.AppCode), command.PoolID) if err != nil { return userdomain.PrettyDisplayIDPool{}, err } if rows, _ := result.RowsAffected(); rows == 0 { return userdomain.PrettyDisplayIDPool{}, xerr.New(xerr.NotFound, "pretty display id pool not found") } return queryPrettyDisplayIDPool(ctx, r.db, appcode.Normalize(command.AppCode), command.PoolID) } func (r *Repository) GeneratePrettyDisplayIDs(ctx context.Context, command userdomain.PrettyDisplayIDGenerateCommand) (userdomain.PrettyDisplayIDGenerationBatch, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.PrettyDisplayIDGenerationBatch{}, err } defer tx.Rollback() appCode := appcode.Normalize(command.AppCode) pool, err := queryPrettyDisplayIDPoolForUpdate(ctx, tx, appCode, command.PoolID) if err == sql.ErrNoRows { return userdomain.PrettyDisplayIDGenerationBatch{}, xerr.New(xerr.NotFound, "pretty display id pool not found") } if err != nil { return userdomain.PrettyDisplayIDGenerationBatch{}, err } // 生成必须锁池配置,确保管理员同时修改规则或禁用池时,本批次仍以一个确定的池状态落库。 ruleType := command.RuleType if ruleType == "" { ruleType = pool.RuleType } generated := int32(0) skipped := int32(0) if command.RuleConfigJSON == "" { // 生成弹窗没有单独填写规则配置时,沿用靓号池保存的配置,保证“按池生成”不会丢掉排除数字等配置。 command.RuleConfigJSON = pool.RuleConfigJSON } candidates, err := generatePrettyDisplayIDCandidates(ruleType, command.RuleConfigJSON) if err != nil { return userdomain.PrettyDisplayIDGenerationBatch{}, err } // 候选按规则确定顺序扫描,遇到已有用户 ID、默认短号、当前靓号或池内号码就跳过,不随机重试。 for _, candidate := range candidates { if generated >= command.Count { break } if err := r.assertDisplayUserIDGloballyAvailable(ctx, tx, candidate); err != nil { if xerr.IsCode(err, xerr.DisplayUserIDExists) { skipped++ continue } return userdomain.PrettyDisplayIDGenerationBatch{}, err } _, err := tx.ExecContext(ctx, ` INSERT INTO pretty_display_ids ( app_code, pretty_id, pool_id, source, display_user_id, status, generated_batch_id, created_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, appCode, idgen.New("pretty"), command.PoolID, userdomain.PrettyDisplayIDSourcePool, candidate, userdomain.PrettyDisplayIDStatusAvailable, command.BatchID, command.OperatorAdminID, command.NowMs, command.NowMs) if err != nil { // 唯一键冲突和显式查重一样计入 skipped,保证并发生成时不会因为个别撞号中断整个批次。 if xerr.IsCode(shared.MapPrettyDuplicateError(err), xerr.DisplayUserIDPrettyNotAvailable) { skipped++ continue } return userdomain.PrettyDisplayIDGenerationBatch{}, err } generated++ } if generated < command.Count { // 候选空间被耗尽时也要记录缺口,便于后台看到“请求 1000 个但实际生成不足”的原因。 shortfall := command.Count - generated if shortfall > skipped { skipped = shortfall } } // 批次记录和号码记录同事务提交,后台列表能用 batch_id 追溯一次生成实际落了哪些号码。 _, err = tx.ExecContext(ctx, ` INSERT INTO pretty_display_id_generation_batches ( app_code, batch_id, pool_id, rule_type, rule_config_json, requested_count, generated_count, skipped_conflict_count, status, operator_admin_id, request_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, appCode, command.BatchID, command.PoolID, ruleType, nullableJSON(command.RuleConfigJSON), command.Count, generated, skipped, userdomain.PrettyDisplayIDBatchStatusCompleted, command.OperatorAdminID, command.RequestID, command.NowMs, command.NowMs) if err != nil { return userdomain.PrettyDisplayIDGenerationBatch{}, err } if err := tx.Commit(); err != nil { return userdomain.PrettyDisplayIDGenerationBatch{}, err } return userdomain.PrettyDisplayIDGenerationBatch{ AppCode: appCode, BatchID: command.BatchID, PoolID: command.PoolID, RuleType: ruleType, RuleConfigJSON: command.RuleConfigJSON, RequestedCount: command.Count, GeneratedCount: generated, SkippedConflictCount: skipped, Status: userdomain.PrettyDisplayIDBatchStatusCompleted, OperatorAdminID: command.OperatorAdminID, RequestID: command.RequestID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, }, nil } func (r *Repository) ListPrettyDisplayIDs(ctx context.Context, query userdomain.PrettyDisplayIDQuery) ([]userdomain.PrettyDisplayID, int64, error) { appCode := appcode.Normalize(query.AppCode) page, pageSize := normalizeIdentityPage(query.Page, query.PageSize) where := []string{"pdi.app_code = ?"} args := []any{appCode} if query.PoolID != "" { where = append(where, "pdi.pool_id = ?") args = append(args, query.PoolID) } if query.Source != "" { where = append(where, "pdi.source = ?") args = append(args, query.Source) } if query.Status != "" { where = append(where, "pdi.status = ?") args = append(args, query.Status) } if query.AssignedUserID > 0 { where = append(where, "pdi.assigned_user_id = ?") args = append(args, query.AssignedUserID) } if query.Keyword != "" { where = append(where, "(pdi.pretty_id LIKE ? OR pdi.display_user_id LIKE ?)") keyword := "%" + query.Keyword + "%" args = append(args, keyword, keyword) } whereSQL := "WHERE " + strings.Join(where, " AND ") var total int64 if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM pretty_display_ids pdi LEFT JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id `+whereSQL, args...).Scan(&total); err != nil { return nil, 0, err } rows, err := r.db.QueryContext(ctx, ` SELECT `+prettyDisplayIDSelectColumns()+` FROM pretty_display_ids pdi LEFT JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id `+whereSQL+` ORDER BY pdi.created_at_ms DESC, pdi.pretty_id DESC LIMIT ? OFFSET ? `, append(args, int(pageSize), int((page-1)*pageSize))...) if err != nil { return nil, 0, err } defer rows.Close() items, err := scanPrettyDisplayIDRows(rows) if err != nil { return nil, 0, err } return items, total, rows.Err() } func (r *Repository) SetPrettyDisplayIDStatus(ctx context.Context, command userdomain.PrettyDisplayIDStatusCommand) (userdomain.PrettyDisplayID, error) { // 状态切换只允许未占用号码,assigned 号码必须通过覆盖、过期或释放链路改变,避免后台直接改坏用户当前展示号。 result, err := r.db.ExecContext(ctx, ` UPDATE pretty_display_ids SET status = ?, release_reason = ?, updated_at_ms = ? WHERE app_code = ? AND pretty_id = ? AND assigned_user_id = 0 AND assigned_lease_id = '' AND status IN (?, ?, ?) `, command.Status, command.Reason, command.NowMs, appcode.Normalize(command.AppCode), command.PrettyID, userdomain.PrettyDisplayIDStatusAvailable, userdomain.PrettyDisplayIDStatusDisabled, userdomain.PrettyDisplayIDStatusReserved) if err != nil { return userdomain.PrettyDisplayID{}, err } if rows, _ := result.RowsAffected(); rows == 0 { return userdomain.PrettyDisplayID{}, xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display id status can not be changed") } return queryPrettyDisplayID(ctx, r.db, appcode.Normalize(command.AppCode), command.PrettyID) } func (r *Repository) AdminGrantPrettyDisplayID(ctx context.Context, command userdomain.AdminGrantPrettyDisplayIDCommand) (userdomain.Identity, string, error) { // 后台发放可以输入任意合规内容;如果同名旧限时靓号已到期,先懒过期释放后再做全局唯一性校验。 if err := r.expirePrettyByDisplayUserID(ctx, command.DisplayUserID, command.NowMs, command.RequestID); err != nil { return userdomain.Identity{}, "", err } tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userdomain.Identity{}, "", err } defer tx.Rollback() // 后台发放不经过号码池,但仍要和用户真实 ID、默认短号、当前展示号、池号和保留号做全局冲突检查。 if err := r.assertDisplayUserIDGloballyAvailable(ctx, tx, command.DisplayUserID); err != nil { return userdomain.Identity{}, "", err } // 锁目标用户后再插入租约和更新 users,确保同一用户同时被发放或申请池号时只有一个事务成功替换。 user, err := userstorage.QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID) if err == sql.ErrNoRows { return userdomain.Identity{}, "", xerr.New(xerr.NotFound, "user not found") } if err != nil { return userdomain.Identity{}, "", err } expiresAtMs := int64(0) if command.DurationMs > 0 { // duration_ms<=0 在服务层表示长期;只有正数才转换成明确过期时间,供懒过期和定时恢复使用。 expiresAtMs = command.NowMs + command.DurationMs } // 自由输入的后台靓号也先写入 pretty_display_ids,并直接标记 assigned,便于列表、审计和后续释放统一处理。 _, err = tx.ExecContext(ctx, ` INSERT INTO pretty_display_ids ( app_code, pretty_id, pool_id, source, display_user_id, status, assigned_user_id, assigned_lease_id, assigned_at_ms, created_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?) `, appcode.Normalize(command.AppCode), command.PrettyID, userdomain.PrettyDisplayIDSourceAdminGrant, command.DisplayUserID, userdomain.PrettyDisplayIDStatusAssigned, command.TargetUserID, command.LeaseID, command.NowMs, command.OperatorAdminID, command.NowMs, command.NowMs) if err != nil { return userdomain.Identity{}, "", shared.MapPrettyDuplicateError(err) } // 真正覆盖用户当前展示号仍走统一替换事务,确保旧池号释放、旧后台号 released 和默认号 held 行为完全一致。 identity, err := r.replacePrettyInTx(ctx, tx, user, prettyAssignment{ PrettyID: command.PrettyID, LeaseID: command.LeaseID, DisplayUserID: command.DisplayUserID, ExpiresAtMs: expiresAtMs, Source: userdomain.PrettyDisplayIDSourceAdminGrant, ChangeType: "pretty_admin_grant", Reason: command.Reason, OperatorUserID: command.OperatorAdminID, RequestID: command.RequestID, NowMs: command.NowMs, }) if err != nil { return userdomain.Identity{}, "", err } if err := tx.Commit(); err != nil { return userdomain.Identity{}, "", err } return identity, command.LeaseID, nil } func normalizeIdentityPage(page int32, pageSize int32) (int32, int32) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } return page, pageSize } func nullableJSON(value string) any { if strings.TrimSpace(value) == "" { return nil } return value } type rowScanner interface { Scan(dest ...any) error } func scanPrettyDisplayIDPool(scanner rowScanner) (userdomain.PrettyDisplayIDPool, error) { var pool userdomain.PrettyDisplayIDPool err := scanner.Scan( &pool.AppCode, &pool.PoolID, &pool.Name, &pool.LevelTrack, &pool.MinLevel, &pool.MaxLevel, &pool.RuleType, &pool.RuleConfigJSON, &pool.Status, &pool.SortOrder, &pool.CreatedByAdminID, &pool.UpdatedByAdminID, &pool.CreatedAtMs, &pool.UpdatedAtMs, ) return pool, err } func queryPrettyDisplayIDPool(ctx context.Context, q shared.Queryer, appCode string, poolID string) (userdomain.PrettyDisplayIDPool, error) { return scanPrettyDisplayIDPool(q.QueryRowContext(ctx, ` SELECT app_code, pool_id, name, level_track, min_level, max_level, rule_type, COALESCE(CAST(rule_config_json AS CHAR), ''), status, sort_order, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms FROM pretty_display_id_pools WHERE app_code = ? AND pool_id = ? `, appCode, poolID)) } func queryPrettyDisplayIDPoolForUpdate(ctx context.Context, tx *sql.Tx, appCode string, poolID string) (userdomain.PrettyDisplayIDPool, error) { return scanPrettyDisplayIDPool(tx.QueryRowContext(ctx, ` SELECT app_code, pool_id, name, level_track, min_level, max_level, rule_type, COALESCE(CAST(rule_config_json AS CHAR), ''), status, sort_order, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms FROM pretty_display_id_pools WHERE app_code = ? AND pool_id = ? FOR UPDATE `, appCode, poolID)) } func prettyDisplayIDSelectColumns() string { return ` pdi.app_code, pdi.pretty_id, pdi.pool_id, pdi.source, pdi.display_user_id, pdi.status, pdi.assigned_user_id, pdi.assigned_lease_id, pdi.assigned_at_ms, pdi.released_at_ms, pdi.release_reason, pdi.generated_batch_id, pdi.created_by_admin_id, pdi.created_at_ms, pdi.updated_at_ms, COALESCE(pool.app_code, ''), COALESCE(pool.pool_id, ''), COALESCE(pool.name, ''), COALESCE(pool.level_track, ''), COALESCE(pool.min_level, 0), COALESCE(pool.max_level, 0), COALESCE(pool.rule_type, ''), COALESCE(CAST(pool.rule_config_json AS CHAR), ''), COALESCE(pool.status, ''), COALESCE(pool.sort_order, 0), COALESCE(pool.created_by_admin_id, 0), COALESCE(pool.updated_by_admin_id, 0), COALESCE(pool.created_at_ms, 0), COALESCE(pool.updated_at_ms, 0)` } func scanPrettyDisplayIDRows(rows *sql.Rows) ([]userdomain.PrettyDisplayID, error) { items := make([]userdomain.PrettyDisplayID, 0) for rows.Next() { item, err := scanPrettyDisplayID(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func scanPrettyDisplayID(scanner rowScanner) (userdomain.PrettyDisplayID, error) { var item userdomain.PrettyDisplayID err := scanner.Scan( &item.AppCode, &item.PrettyID, &item.PoolID, &item.Source, &item.DisplayUserID, &item.Status, &item.AssignedUserID, &item.AssignedLeaseID, &item.AssignedAtMs, &item.ReleasedAtMs, &item.ReleaseReason, &item.GeneratedBatchID, &item.CreatedByAdminID, &item.CreatedAtMs, &item.UpdatedAtMs, &item.Pool.AppCode, &item.Pool.PoolID, &item.Pool.Name, &item.Pool.LevelTrack, &item.Pool.MinLevel, &item.Pool.MaxLevel, &item.Pool.RuleType, &item.Pool.RuleConfigJSON, &item.Pool.Status, &item.Pool.SortOrder, &item.Pool.CreatedByAdminID, &item.Pool.UpdatedByAdminID, &item.Pool.CreatedAtMs, &item.Pool.UpdatedAtMs, ) return item, err } func queryPrettyDisplayID(ctx context.Context, q shared.Queryer, appCode string, prettyID string) (userdomain.PrettyDisplayID, error) { return scanPrettyDisplayID(q.QueryRowContext(ctx, ` SELECT `+prettyDisplayIDSelectColumns()+` FROM pretty_display_ids pdi LEFT JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id WHERE pdi.app_code = ? AND pdi.pretty_id = ? `, appCode, prettyID)) } func queryPrettyDisplayIDForUpdate(ctx context.Context, tx *sql.Tx, appCode string, prettyID string) (userdomain.PrettyDisplayID, error) { return scanPrettyDisplayID(tx.QueryRowContext(ctx, ` SELECT `+prettyDisplayIDSelectColumns()+` FROM pretty_display_ids pdi LEFT JOIN pretty_display_id_pools pool ON pool.app_code = pdi.app_code AND pool.pool_id = pdi.pool_id WHERE pdi.app_code = ? AND pdi.pretty_id = ? FOR UPDATE `, appCode, prettyID)) }