package auth import ( "context" "database/sql" "fmt" "hash/fnv" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" ) const ( displayUserIDMin int64 = 1000000 displayUserIDSpace int64 = 9000000 displayUserIDPermutationBase uint32 = 3000 displayUserIDPermutationRound = 7 displayUserIDAllocateAttempts = 4096 ) var displayUserIDPermutationKeys = [displayUserIDPermutationRound]uint32{ 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, } type displayUserIDQueryer interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } // AllocateDisplayUserID 为当前 App 分配 7 位随机外观默认短号。 // 数据库只保存递增内部序号;对外短号由 3000x3000 Feistel 置换得到,所以同一 App 内序号不重复就不会产出重复短号。 // 旧库里可能已有随机号或人工种子号,因此分配时仍用唯一索引点查跳过已占用值,最终创建事务再用唯一键兜住并发窗口。 func (r *Repository) AllocateDisplayUserID(ctx context.Context, appCode string) (string, error) { normalized := appcode.Normalize(appCode) tx, err := r.db.BeginTx(ctx, nil) if err != nil { return "", err } defer tx.Rollback() nowMs := time.Now().UTC().UnixMilli() nextValue, err := r.lockDisplayUserIDSequence(ctx, tx, normalized, nowMs) if err != nil { return "", err } for attempt := 0; attempt < displayUserIDAllocateAttempts; attempt++ { if nextValue < 0 || nextValue >= displayUserIDSpace { return "", xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id space exhausted") } candidate, err := displayUserIDFromOrdinal(normalized, nextValue) if err != nil { return "", err } occupied, err := displayUserIDOccupied(ctx, tx, normalized, candidate) if err != nil { return "", err } nextValue++ if occupied { // 历史随机号、测试种子号或人工保留号可能已经占住当前置换结果;推进内部序号继续找下一个可用值。 continue } if _, err := tx.ExecContext(ctx, ` UPDATE display_user_id_sequences SET next_value = ?, updated_at_ms = ? WHERE app_code = ? `, nextValue, nowMs, normalized); err != nil { return "", err } if err := tx.Commit(); err != nil { return "", err } return candidate, nil } return "", xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed") } func (r *Repository) lockDisplayUserIDSequence(ctx context.Context, tx *sql.Tx, appCode string, nowMs int64) (int64, error) { var nextValue int64 err := tx.QueryRowContext(ctx, ` SELECT next_value FROM display_user_id_sequences WHERE app_code = ? FOR UPDATE `, appCode).Scan(&nextValue) if err == nil { return nextValue, nil } if err != sql.ErrNoRows { return 0, err } initialValue, err := initialDisplayUserIDSequenceValue(ctx, tx, appCode) if err != nil { return 0, err } if _, err := tx.ExecContext(ctx, ` INSERT IGNORE INTO display_user_id_sequences (app_code, next_value, updated_at_ms) VALUES (?, ?, ?) `, appCode, initialValue, nowMs); err != nil { return 0, err } if err := tx.QueryRowContext(ctx, ` SELECT next_value FROM display_user_id_sequences WHERE app_code = ? FOR UPDATE `, appCode).Scan(&nextValue); err != nil { return 0, err } return nextValue, nil } func initialDisplayUserIDSequenceValue(ctx context.Context, queryer displayUserIDQueryer, appCode string) (int64, error) { // 老库补表时用现有占用量作为内部序号起点;这不是唯一性来源,只是减少迁移后从 0 开始撞历史号的概率。 var initialValue int64 err := queryer.QueryRowContext(ctx, ` SELECT GREATEST( (SELECT COUNT(*) FROM user_display_user_ids WHERE app_code = ? AND live_display_user_id IS NOT NULL), (SELECT COUNT(*) FROM users WHERE app_code = ?) ) `, appCode, appCode).Scan(&initialValue) if err != nil { return 0, err } return initialValue, nil } func displayUserIDFromOrdinal(appCode string, ordinal int64) (string, error) { if ordinal < 0 || ordinal >= displayUserIDSpace { return "", xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id space exhausted") } return fmt.Sprintf("%07d", displayUserIDMin+int64(permuteDisplayUserIDOrdinal(appCode, uint32(ordinal)))), nil } func permuteDisplayUserIDOrdinal(appCode string, ordinal uint32) uint32 { // 7 位账号空间是 9,000,000 = 3000 * 3000;把内部序号拆成两个 3000 进制半区后做 Feistel 置换。 // 每一轮都是可逆映射,因此整个函数是 0..8,999,999 上的一一置换,不会像随机取号一样产生生日碰撞。 left := ordinal / displayUserIDPermutationBase right := ordinal % displayUserIDPermutationBase appHash := displayUserIDAppHash(appCode) for round := 0; round < displayUserIDPermutationRound; round++ { nextLeft := right nextRight := (left + displayUserIDRoundValue(right, appHash, round)) % displayUserIDPermutationBase left, right = nextLeft, nextRight } return left*displayUserIDPermutationBase + right } func displayUserIDRoundValue(right uint32, appHash uint32, round int) uint32 { // 轮函数只需要伪随机分布,不需要可逆;Feistel 结构负责整体可逆性。 x := right + appHash + displayUserIDPermutationKeys[round] + uint32(round)*0x9e3779b9 x ^= x >> 15 x *= 0x2c1b3c6d x ^= x >> 12 x *= 0x297a2d39 x ^= x >> 15 return x % displayUserIDPermutationBase } func displayUserIDAppHash(appCode string) uint32 { hash := fnv.New32a() _, _ = hash.Write([]byte(appcode.Normalize(appCode))) return hash.Sum32() } func displayUserIDOccupied(ctx context.Context, queryer displayUserIDQueryer, appCode string, displayUserID string) (bool, error) { var exists int err := queryer.QueryRowContext(ctx, ` SELECT 1 FROM ( SELECT 1 FROM user_display_user_ids WHERE app_code = ? AND live_display_user_id = ? UNION ALL SELECT 1 FROM users WHERE app_code = ? AND default_display_user_id = ? UNION ALL SELECT 1 FROM users WHERE app_code = ? AND current_display_user_id = ? ) occupied_display_user_ids LIMIT 1 `, appCode, displayUserID, appCode, displayUserID, appCode, displayUserID).Scan(&exists) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return true, nil }