实现余额购买接口 & 实现全局 id 生成器

This commit is contained in:
2025-04-08 17:15:23 +08:00
parent c02d843dbc
commit 4c47a71f30
10 changed files with 506 additions and 116 deletions

178
web/services/id.go Normal file
View File

@@ -0,0 +1,178 @@
package services
import (
"context"
"errors"
"fmt"
"platform/pkg/rds"
"strings"
"time"
"github.com/google/uuid"
"github.com/jxskiss/base62"
"github.com/redis/go-redis/v9"
)
var ID IdService = IdService{}
type IdService struct {
}
// region SerialID
const (
// 保留位确保最高位为0防止产生负值
reservedBits = 1
// 时间戳位数
timestampBits = 41
// 序列号位数
sequenceBits = 22
// 最大序列号掩码2^22 - 1
maxSequence = (1 << sequenceBits) - 1
// 位移计算常量
timestampShift = sequenceBits
// Redis 缓存过期时间(秒)
redisTTL = 5
)
var (
ErrSequenceOverflow = errors.New("sequence overflow")
)
func (s *IdService) GenSerial(ctx context.Context) (uint64, error) {
// 构造Redis键
now := time.Now().Unix()
key := idSerialKey(now)
// 使用Redis事务确保原子操作
var sequence int64
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
// 获取当前序列号
currentVal, err := tx.Get(ctx, key).Int64()
if err != nil && !errors.Is(err, redis.Nil) {
return err
}
if errors.Is(err, redis.Nil) {
currentVal = 0
}
sequence = currentVal + 1
// 检查序列号是否溢出
if sequence > maxSequence {
return ErrSequenceOverflow
}
// 将更新后的序列号保存回Redis设置5秒过期时间
pipe := tx.Pipeline()
pipe.Set(ctx, key, sequence, redisTTL*time.Second)
_, err = pipe.Exec(ctx)
return err
}, key)
if err != nil {
return 0, err
}
// 组装最终ID
id := uint64((now << timestampShift) | sequence)
return id, nil
}
// ParseSerial 解析ID返回其组成部分
func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
// 通过位运算和掩码提取各部分
timestamp = int64(id >> timestampShift)
sequence = int64(id & maxSequence)
return
}
// idSerialKey 根据时间戳生成Redis键
func idSerialKey(timestamp int64) string {
return fmt.Sprintf("global:id:serial:%d", timestamp)
}
// endregion
// region ReadableID
// GenReadable 根据给定的标签生成易读的全局唯一标识符
// tag 参数用于标识 ID 的用途,如 "usr" 表示用户ID"ord" 表示订单ID等
// 生成的 ID 格式为:<tag>_<encoded-uuid>例如usr_7NLmVLeHwqS73enFZ1i8tB
func (s *IdService) GenReadable(tag string) string {
// 生成 UUID
id := uuid.New()
// 将 UUID 编码为 Base62 字符串(更短,更易读)
encoded := base62.EncodeToString(id[:])
// 如标签为空,则直接返回编码后的字符串
if tag == "" {
return encoded
}
// 标准化标签:转换为小写并移除特殊字符
tag = normalizeTag(tag)
// 组合最终 ID
return fmt.Sprintf("%s_%s", tag, encoded)
}
// ParseReadableID 解析易读ID返回其标签和编码部分
func (s *IdService) ParseReadableID(id string) (tag string, encoded string) {
parts := strings.SplitN(id, "_", 2)
if len(parts) != 2 {
return "", id
}
return parts[0], parts[1]
}
// TryDecodeID 尝试将编码部分解码回 UUID
// 如果解码失败,返回错误
func (s *IdService) TryDecodeID(encoded string) (uuid.UUID, error) {
// 尝试解码 Base62 编码
bytes, err := base62.DecodeString(encoded)
if err != nil {
return uuid.UUID{}, err
}
// 确保长度正确
if len(bytes) != 16 {
return uuid.UUID{}, fmt.Errorf("invalid UUID length after decoding: %d", len(bytes))
}
// 转换为 UUID
var result uuid.UUID
copy(result[:], bytes)
return result, nil
}
// normalizeTag 标准化标签
// 转换为小写,移除特殊字符,最多保留 5 个字符
func normalizeTag(tag string) string {
// 转换为小写
tag = strings.ToLower(tag)
// 移除特殊字符
var sb strings.Builder
for _, c := range tag {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
sb.WriteRune(c)
}
}
// 截取最多 5 个字符
result := sb.String()
if len(result) > 5 {
result = result[:5]
}
return result
}
// endregion