2026-06-30 16:01:08 +08:00

1138 lines
48 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"
"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 号码回到 availableadmin_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))
}