2025-04-08 17:15:23 +08:00
|
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2025-05-10 16:59:41 +08:00
|
|
|
|
g "platform/web/globals"
|
2025-04-17 18:29:44 +08:00
|
|
|
|
"strconv"
|
2025-04-08 17:15:23 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"github.com/jxskiss/base62"
|
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-04-12 18:03:44 +08:00
|
|
|
|
var ID = IdService{}
|
2025-04-08 17:15:23 +08:00
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-06-26 09:28:42 +08:00
|
|
|
|
func (s *IdService) GenSerial() (string, error) {
|
2025-04-08 17:15:23 +08:00
|
|
|
|
now := time.Now().Unix()
|
2025-12-05 17:30:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 脚本实现原子操作
|
|
|
|
|
|
script := redis.NewScript(`
|
|
|
|
|
|
local current = tonumber(redis.call('GET', KEYS[1])) or 0
|
|
|
|
|
|
if current >= tonumber(ARGV[1]) then
|
|
|
|
|
|
return redis.error_reply('sequence overflow')
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local sequence = current + 1
|
|
|
|
|
|
redis.call('SET', KEYS[1], sequence, 'EX', ARGV[2])
|
|
|
|
|
|
|
|
|
|
|
|
return sequence
|
|
|
|
|
|
`)
|
|
|
|
|
|
sequence, err := script.Run(context.Background(), g.Redis, []string{idSerialKey(now)}, maxSequence, redisTTL).Int64()
|
2025-04-08 17:15:23 +08:00
|
|
|
|
if err != nil {
|
2025-04-17 18:29:44 +08:00
|
|
|
|
return "", err
|
2025-04-08 17:15:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 组装最终ID
|
|
|
|
|
|
id := uint64((now << timestampShift) | sequence)
|
2025-12-05 17:30:18 +08:00
|
|
|
|
return strconv.FormatUint(id, 10), nil
|
2025-04-08 17:15:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2025-12-05 17:30:18 +08:00
|
|
|
|
return fmt.Sprintf("id:serial:%d", timestamp)
|
2025-04-08 17:15:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|