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