2026-06-30 20:50:50 +08:00

208 lines
6.2 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 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
}