fix(common): 新增文件类型检测工具并更新 Redis 依赖
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DetectFileType 根据二进制内容推断 contentType + 扩展名(尽量稳定)。
|
||||
// 纯 stdlib 实现(http.DetectContentType + 常见类型映射),不依赖模型网关。
|
||||
// 本函数是统一实现;model-gateway/common/util/files.go 的本地副本后续委托到此处。
|
||||
func DetectFileType(data []byte) (contentType string, ext string) {
|
||||
if len(data) == 0 {
|
||||
return "application/octet-stream", ""
|
||||
}
|
||||
ct := http.DetectContentType(data)
|
||||
// DetectContentType 可能带 charset 等参数:text/plain; charset=utf-8
|
||||
if idx := strings.Index(ct, ";"); idx > 0 {
|
||||
ct = strings.TrimSpace(ct[:idx])
|
||||
}
|
||||
switch ct {
|
||||
case "audio/mpeg":
|
||||
return ct, ".mp3"
|
||||
case "audio/wave", "audio/wav", "audio/x-wav":
|
||||
return ct, ".wav"
|
||||
case "video/mp4":
|
||||
return ct, ".mp4"
|
||||
case "image/png":
|
||||
return ct, ".png"
|
||||
case "image/jpeg":
|
||||
return ct, ".jpg"
|
||||
case "application/pdf":
|
||||
return ct, ".pdf"
|
||||
case "text/plain":
|
||||
return ct, ".txt"
|
||||
case "application/json":
|
||||
return ct, ".json"
|
||||
default:
|
||||
// 兜底:尝试从 ct 截取 subtype 作为后缀(例如 application/json)
|
||||
if parts := strings.Split(ct, "/"); len(parts) == 2 {
|
||||
sub := parts[1]
|
||||
// 避免出现 "plain; charset=utf-8" 之类的后缀
|
||||
if idx := strings.Index(sub, ";"); idx > 0 {
|
||||
sub = strings.TrimSpace(sub[:idx])
|
||||
}
|
||||
return ct, "." + sub
|
||||
}
|
||||
return ct, ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// HeadersOptions 控制请求头透传的差异层。基础两层恒有:
|
||||
// 1. 透传 HTTP 请求头(含 Authorization/X-User-Info)
|
||||
// 2. X-User-Info 为空且 ctx 携带 user(恢复续跑/异步任务注入的合成用户)时补 X-User-Info
|
||||
type HeadersOptions struct {
|
||||
ResolveToken bool // X-User-Info 仍为空时,解析调用方 token 得到用户注入(直连场景)
|
||||
TokenFromQuery bool // Authorization 为空时从 URL query ?token= 补 Bearer(WS 握手)
|
||||
}
|
||||
|
||||
// HeadersFromCtx 统一构造调用方请求头透传 map。全项目唯一的拼头入口
|
||||
//
|
||||
// 放这里而非 common/http:本函数只依赖 GetUserInfo(同包),与 gclient/gsvc/consul/jaeger 客户端
|
||||
// 基础设施零耦合;common/http 空导入 common/consul、common/jaeger(init 里 g.Cfg().MustGet 无默认值,
|
||||
// 无配置文件环境会 panic),common/oss 等消费方特意避开它,放 utils 依赖链干净。
|
||||
func HeadersFromCtx(ctx context.Context, opts ...HeadersOptions) map[string]string {
|
||||
op := HeadersOptions{}
|
||||
if len(opts) > 0 {
|
||||
op = opts[0]
|
||||
}
|
||||
headers := make(map[string]string)
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Request.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if headers["X-User-Info"] == "" {
|
||||
if u := ctx.Value("user"); !g.IsNil(u) {
|
||||
headers["X-User-Info"] = gconv.String(u)
|
||||
}
|
||||
}
|
||||
if op.TokenFromQuery && headers["Authorization"] == "" {
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
if t := r.Request.URL.Query().Get("token"); t != "" {
|
||||
headers["Authorization"] = "Bearer " + t
|
||||
}
|
||||
}
|
||||
}
|
||||
if op.ResolveToken && headers["X-User-Info"] == "" {
|
||||
if user, err := GetUserInfo(ctx); err == nil && user != nil {
|
||||
headers["X-User-Info"] = gconv.String(user)
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ossObjectPathPattern 匹配 MinIO 上传生成的对象路径(不带 http 前缀的相对路径):
|
||||
// /YYYY-MM-DD/32位uuid.扩展名,如 /2026-08-19/1e9d9e48-3f6b-4a2c-8d5e-1f2a3b4c.png
|
||||
var ossObjectPathPattern = regexp.MustCompile(`^/\d{4}-\d{2}-\d{2}/[0-9a-fA-F-]{32}\.[a-zA-Z0-9]{1,10}$`)
|
||||
|
||||
// IsOSSPath 判断字符串是否为 MinIO 对象路径(无 http(s) 前缀)。
|
||||
// 模型网关把结果转存 OSS 后返回该裸路径,消费方据此识别"已是文件路径"而不再重复上传。
|
||||
// 对象命名规则见 oss/minio 的 ensureBucketAndObjectName,格式变化只需改这一处。
|
||||
func IsOSSPath(s string) bool {
|
||||
return ossObjectPathPattern.MatchString(s)
|
||||
}
|
||||
|
||||
// GetFileAddressPrefix 拼接图片前缀地址
|
||||
func GetFileAddressPrefix(ctx context.Context) (imageUrl string, err error) {
|
||||
// 拼接图片前缀地址
|
||||
bucketName, err := GetBucketName(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
imageUrl = fmt.Sprintf("%s/%s", g.Cfg().MustGet(ctx, "filePrefix").String(), bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
// GetBucketName 获取bucket名称
|
||||
func GetBucketName(ctx context.Context) (bucketName string, err error) {
|
||||
user, err := GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bucketName = fmt.Sprintf("tenantid-%d", user.TenantId)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Package utils 提供统一的 Redis 计数原语(与 redislock 的「互斥锁」互补,本包是「计数」一族)。
|
||||
//
|
||||
// 只保留 model-gateway 实际业务场景用到、且全仓有调用方的两个原语:
|
||||
// - Semaphore:INCR 信号量(model-gateway/service/queue/semaphore.go),并发额度计数
|
||||
// —— asyncWorker 按模型并发上限抢信号量,超限放回队列(task/worker.go:51);
|
||||
// - SafeDecr:安全回减(model-gateway/model_call_service.go decrSlotLua),计数防负
|
||||
// —— 并发名额释放、超限回滚时原子夹在 >=0。
|
||||
//
|
||||
// (ZSET 槽位幂等闸门 queue_gate.go、固定窗口限流 rate_limiter.go 全仓无调用方,已删除;后续用到再加。)
|
||||
//
|
||||
// 与 redislock 一致,本包不写 Lua 脚本:
|
||||
// - 多步原子操作(判定 + 写)经 go-redis WATCH/MULTI/EXEC 事务组合原生命令,
|
||||
// 通过 gogf 官方 escape hatch GetAdapter().Client() 取底层客户端执行。
|
||||
//
|
||||
// 组合模式(如 model-gateway reserveSlot 的 锁+计数):
|
||||
// redislock.WithLock + 本包 SafeDecr。
|
||||
//
|
||||
// 兼容红线:只用 gf v2.9.5 已有 API(Set/Get/Incr/Decr、GetAdapter().Client())。
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// errSkip 内部信号:WATCH 事务里判定不通过,不执行写。
|
||||
// 调用方据 errors.Is(err, errSkip) 视为「本次操作失败但非异常」。
|
||||
var errSkip = errors.New("rediscount: 条件不满足,跳过写入")
|
||||
|
||||
// rawClient 取底层 go-redis 客户端(gogf 官方 escape hatch)。
|
||||
// 各服务 main.go 空导入 contrib/nosql/redis/v2 后 Adapter 为 go-redis 实现,
|
||||
// Client() 返回 redis.UniversalClient(单节点 / 哨兵 / 集群均实现 Watch)。
|
||||
func rawClient() (goredis.UniversalClient, error) {
|
||||
universal, ok := g.Redis().GetAdapter().Client().(goredis.UniversalClient)
|
||||
if !ok {
|
||||
return nil, errors.New("redis 底层客户端非 UniversalClient,无法执行 WATCH 原子事务")
|
||||
}
|
||||
return universal, nil
|
||||
}
|
||||
|
||||
// tx 在 WATCH/MULTI/EXEC 事务里执行 fn(先读后写,无 Lua):
|
||||
// fn 里先发读命令判定,命中条件后经 tx.TxPipelined 排队写命令,EXEC 原子提交。
|
||||
// 判定不通过时 fn 返回 errSkip(不写);WATCH 冲突(TxFailedErr,读与写之间被并发改动)自动重试。
|
||||
func tx(ctx context.Context, key string, fn func(tx *goredis.Tx) error) error {
|
||||
c, err := rawClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
err := c.Watch(ctx, func(tx *goredis.Tx) error { return fn(tx) }, key)
|
||||
if errors.Is(err, goredis.TxFailedErr) {
|
||||
continue // 冲突,重读重试
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// pipedInt 取出事务写队列中第 n 条命令(INCR/DECR)的执行结果。
|
||||
func pipedInt(cmds []goredis.Cmder, n int) (int64, error) {
|
||||
return cmds[n].(*goredis.IntCmd).Result()
|
||||
}
|
||||
|
||||
// SemaphoreAcquire 获取并发额度:WATCH key → GET 判满 → MULTI/INCR + 首设 EXPIRE/EXEC。
|
||||
// 原子判定未满才 INCR;首次(计数从 0/不存在起)顺手设 TTL 自动回收防泄漏;超限不写、返回 false。
|
||||
// 对应 model-gateway asyncWorker 按模型并发上限抢信号量。max<=0 表示不限制;ttlSeconds<=0 时按 3600 兜底。
|
||||
func SemaphoreAcquire(ctx context.Context, key string, max int, ttlSeconds int64) (bool, error) {
|
||||
if max <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
if ttlSeconds <= 0 {
|
||||
ttlSeconds = 3600
|
||||
}
|
||||
err := tx(ctx, key, func(tx *goredis.Tx) error {
|
||||
current, err := tx.Get(ctx, key).Int64()
|
||||
if errors.Is(err, goredis.Nil) {
|
||||
current = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if current >= int64(max) {
|
||||
return errSkip // 已满
|
||||
}
|
||||
_, err = tx.TxPipelined(ctx, func(pipe goredis.Pipeliner) error {
|
||||
pipe.Incr(ctx, key)
|
||||
if current == 0 {
|
||||
pipe.Expire(ctx, key, time.Duration(ttlSeconds)*time.Second)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if errors.Is(err, errSkip) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取并发额度失败: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SemaphoreRelease 释放并发额度(幂等,计数归零自动删除 key)。
|
||||
// WATCH key → GET → MULTI:计数>1 仅 DECR;==1 时 DECR+DEL;<=0 直接 DEL(清理残留,不造负)。
|
||||
// 全部原子,避免「DECR 后发现归零、中间被并发 INCR 抢占、DEL 误删新额度」的丢失更新。
|
||||
func SemaphoreRelease(ctx context.Context, key string) error {
|
||||
err := tx(ctx, key, func(tx *goredis.Tx) error {
|
||||
current, err := tx.Get(ctx, key).Int64()
|
||||
if errors.Is(err, goredis.Nil) {
|
||||
current = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.TxPipelined(ctx, func(pipe goredis.Pipeliner) error {
|
||||
switch {
|
||||
case current <= 0:
|
||||
pipe.Del(ctx, key)
|
||||
case current == 1:
|
||||
pipe.Decr(ctx, key)
|
||||
pipe.Del(ctx, key)
|
||||
default:
|
||||
pipe.Decr(ctx, key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("释放并发额度失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SafeDecr 安全回减:WATCH key → GET,仅当 key 存在且值>0 才 MULTI/DECR/EXEC,
|
||||
// 杜绝 Redis DECR 对缺失 Key 按 0-1 处理把计数造负。
|
||||
// 对应 model-gateway decrSlot:并发名额释放 / 超限回滚时原子夹在 >=0。
|
||||
// 返回回减后的计数值;未回减(key 不存在或已为 0)返回 -1。
|
||||
func SafeDecr(ctx context.Context, key string) (int64, error) {
|
||||
var result int64
|
||||
err := tx(ctx, key, func(tx *goredis.Tx) error {
|
||||
current, err := tx.Get(ctx, key).Int64()
|
||||
if errors.Is(err, goredis.Nil) {
|
||||
return errSkip // 不存在,不写
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current <= 0 {
|
||||
return errSkip // 已为 0,不造负
|
||||
}
|
||||
cmds, err := tx.TxPipelined(ctx, func(pipe goredis.Pipeliner) error {
|
||||
pipe.Decr(ctx, key)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err = pipedInt(cmds, 0)
|
||||
return err
|
||||
})
|
||||
if errors.Is(err, errSkip) {
|
||||
return -1, nil
|
||||
}
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Package utils 提供统一的 Redis 分布式锁(防并发互斥)。
|
||||
//
|
||||
// 收敛全仓「SET key token EX ttl NX + 归属校验释放」范式
|
||||
// (此前 ai-agent / shop-user-trade / model-gateway 三处逐字复制)。
|
||||
// 只暴露一个入口:
|
||||
// - WithLock:抢锁 + 自动续期 + 临界区 fn + 返回时保证释放。
|
||||
//
|
||||
// 正确性要点:
|
||||
// - value 存 uuid token 标识持有者(不存 true),key 标识资源;
|
||||
// - 释放 / 续期均按 token 比对归属:锁 TTL 过期易主后,旧持有者的释放不会误删新持有者的锁,
|
||||
// 续期也不会把锁无限续到他人头上;
|
||||
// - 归属比对经 go-redis WATCH/MULTI/EXEC 原子事务完成(通过 gogf 官方 escape hatch
|
||||
// GetAdapter().Client() 取底层客户端),不手写 Lua 脚本。
|
||||
//
|
||||
// 兼容红线:本包只用 gf v2.9.5 已有 API(Set+SetOption{NX,EX}、GetAdapter().Client())。
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gredis"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/google/uuid"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Lock 分布式锁(防并发互斥),委托 common/redislock.WithLock(4 次尝试,间隔 500ms)。
|
||||
// 释放 / 续期按 token 归属比对(WATCH/MULTI/EXEC 原子事务,不写 Lua):
|
||||
// 锁 TTL 过期易主后,旧持有者的释放不会误删新持有者的锁。
|
||||
// fn 返回 err 时 success=false、err 上抛。
|
||||
func Lock(ctx context.Context, key string, expireSeconds int64, fn func(ctx context.Context) error) (success bool, err error) {
|
||||
_, err = WithLock(ctx, key, expireSeconds, fn, 4)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// WithLock 函数式锁:抢锁(可选重试次数),成功后自动续期(默认超过一半 TTL 就续期),
|
||||
// 执行 fn,返回时停止续期并保证释放(WATCH/MULTI 按 token 归属释放,防误删他人锁)。
|
||||
//
|
||||
// retryTimes 传了循环次数:最多尝试那么多次,仍抢不到返回锁占用错误;
|
||||
// retryTimes 未传:无限等待,一直重试直到抢到锁或 ctx 取消。
|
||||
// 两次尝试间隔固定 500ms。
|
||||
//
|
||||
// 返回值:
|
||||
// - (true, nil):抢到锁并执行成功;
|
||||
// - (true, fn 的 err):抢到锁、fn 执行失败(业务错误由 err 单独表达);
|
||||
// - (false, err):重试次数耗尽(锁占用)、Redis 故障或 ctx 被取消,均以 err 表达。
|
||||
//
|
||||
// 续期与释放均用 context.WithoutCancel(ctx):即使 fn 中途 ctx 被取消,锁仍能正常续期并释放,
|
||||
// 不会残留到 TTL 造成下一个持有者等待。
|
||||
func WithLock(ctx context.Context, key string, ttlSeconds int64, fn func(ctx context.Context) error, retryTimes ...int) (bool, error) {
|
||||
maxRetries := -1
|
||||
if len(retryTimes) > 0 {
|
||||
maxRetries = retryTimes[0]
|
||||
}
|
||||
l := &lock{key: key, token: uuid.NewString(), ttl: ttlSeconds}
|
||||
|
||||
for attempt := 0; ; attempt++ {
|
||||
if maxRetries >= 0 && attempt >= maxRetries {
|
||||
return false, errors.New("redis lock busy")
|
||||
}
|
||||
ok, err := l.acquire(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
go l.renewLoop(context.WithoutCancel(ctx), stop)
|
||||
defer func() {
|
||||
close(stop)
|
||||
_ = l.release(context.WithoutCancel(ctx))
|
||||
}()
|
||||
|
||||
if err := fn(ctx); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// lock 单次锁会话:token 标识持有者身份,释放 / 续期按 token 原子比对。
|
||||
// 不导出——生命周期全部由 WithLock 编排。
|
||||
type lock struct {
|
||||
key string
|
||||
token string
|
||||
ttl int64 // 秒
|
||||
}
|
||||
|
||||
// acquire 抢锁;返回 true 表示抢到(SET NX 成功)。
|
||||
// 注意:gogf 新版 SetNX(ctx,key,value) 不接收 TTL 参数,直接 SETNX 会永不过期(崩溃后死锁);
|
||||
// 故改用 Set + SetOption{NX,TTLOption{EX}},原子地执行 `SET key token EX <ttl> NX`。
|
||||
func (l *lock) acquire(ctx context.Context) (bool, error) {
|
||||
r, err := g.Redis().Set(ctx, l.key, l.token, gredis.SetOption{
|
||||
TTLOption: gredis.TTLOption{EX: &l.ttl},
|
||||
NX: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// SET NX 失败时 Redis 返回空回复(nil),gogf 转为值 nil 的 gvar;成功时返回 "OK"
|
||||
return !r.IsNil(), nil
|
||||
}
|
||||
|
||||
// release 释放锁:WATCH key → GET 比对 token → MULTI/DEL/EXEC,仅当仍为本锁 token 时删除。
|
||||
// 非持有者调用是 no-op:锁过期易主后,旧持有者的释放不会删掉新持有者的锁。
|
||||
func (l *lock) release(ctx context.Context) error {
|
||||
return l.compareAndWrite(ctx, false)
|
||||
}
|
||||
|
||||
// renew 续期:WATCH key → GET 比对 token → MULTI/EXPIRE/EXEC,仅当仍为本锁 token 时重置 TTL。
|
||||
// 锁已易主时 no-op,避免续到他人锁上(把新持有者的锁无限延长)。
|
||||
func (l *lock) renew(ctx context.Context) error {
|
||||
return l.compareAndWrite(ctx, true)
|
||||
}
|
||||
|
||||
// compareAndWrite 归属比对 + 原子写(WATCH/MULTI/EXEC,无 Lua):
|
||||
// - 锁不存在(已过期 / 被删):no-op;
|
||||
// - key 值 ≠ 本锁 token(已易主):no-op;
|
||||
// - key 值 == 本锁 token:renew=true 时重置 TTL,否则删除。
|
||||
//
|
||||
// WATCH 保证:比对与写之间若 key 被其他客户端改动,EXEC 会被服务端中止(返回空),写不生效。
|
||||
// 经 gogf 官方 escape hatch(GetAdapter().Client())取底层 go-redis 客户端执行原子事务;
|
||||
// 各服务 main.go 空导入 contrib/nosql/redis/v2 后 Adapter 为 go-redis 实现,
|
||||
// Client() 返回 redis.UniversalClient(单节点 / 哨兵 / 集群均实现 Watch)。
|
||||
func (l *lock) compareAndWrite(ctx context.Context, renew bool) error {
|
||||
universal, ok := g.Redis().GetAdapter().Client().(goredis.UniversalClient)
|
||||
if !ok {
|
||||
return errors.New("redis 底层客户端非 UniversalClient,无法执行 WATCH 原子事务")
|
||||
}
|
||||
return universal.Watch(ctx, func(tx *goredis.Tx) error {
|
||||
val, err := tx.Get(ctx, l.key).Result()
|
||||
if errors.Is(err, goredis.Nil) {
|
||||
return nil // 锁已过期 / 被删
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val != l.token {
|
||||
return nil // 锁已易主,不动他人锁
|
||||
}
|
||||
_, err = tx.TxPipelined(ctx, func(pipe goredis.Pipeliner) error {
|
||||
if renew {
|
||||
pipe.Expire(ctx, l.key, time.Duration(l.ttl)*time.Second)
|
||||
} else {
|
||||
pipe.Del(ctx, l.key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}, l.key)
|
||||
}
|
||||
|
||||
// renewLoop 自动续期看门狗:每 ttl/2 续一次(「超过一半就续期」)。
|
||||
// 续期失败仅记日志,不打断临界区(WATCH 事务在锁易主时会因 token 不匹配安全地 no-op)。
|
||||
// WithLock 内部启动,临界区结束即停止。
|
||||
func (l *lock) renewLoop(ctx context.Context, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(time.Duration(l.ttl) * time.Second / 2)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := l.renew(context.WithoutCancel(ctx)); err != nil {
|
||||
g.Log().Warningf(ctx, "redislock 续期失败: key=%s err=%v", l.key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-39
@@ -3,7 +3,6 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
@@ -14,12 +13,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gredis"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/tiger1103/gfast-token/gftoken"
|
||||
@@ -107,7 +104,8 @@ func GetUserInfo(ctx context.Context) (user *beans.User, err error) {
|
||||
|
||||
if !g.IsNil(ctx.Value("token")) {
|
||||
var tokenData *gftoken.TokenData
|
||||
tokenData, _, err = gft.GetTokenData(ctx, ctx.Value("token").(string))
|
||||
tk := ctx.Value("token").(string)
|
||||
tokenData, _, err = gft.GetTokenData(ctx, tk)
|
||||
if err != nil {
|
||||
return user, gerror.Wrap(err, "ctx token 解析失败")
|
||||
}
|
||||
@@ -117,7 +115,8 @@ func GetUserInfo(ctx context.Context) (user *beans.User, err error) {
|
||||
}
|
||||
} else if g.RequestFromCtx(ctx) != nil {
|
||||
// 解析 token
|
||||
data, err = gft.ParseToken(g.RequestFromCtx(ctx))
|
||||
req := g.RequestFromCtx(ctx)
|
||||
data, err = gft.ParseToken(req)
|
||||
if err != nil {
|
||||
return user, gerror.Wrap(err, "token 解析失败")
|
||||
}
|
||||
@@ -389,40 +388,6 @@ func intPow10(n int) int {
|
||||
return result
|
||||
}
|
||||
|
||||
// Lock 分布式锁
|
||||
func Lock(ctx context.Context, key string, expireSeconds int64, fn func(ctx context.Context) error) (success bool, err error) {
|
||||
limit := 3
|
||||
LOOP:
|
||||
if limit < 0 {
|
||||
return false, errors.New("锁重试次数耗尽")
|
||||
}
|
||||
limit--
|
||||
var val *gvar.Var
|
||||
if val, err = g.Redis().Set(ctx, key, true, gredis.SetOption{
|
||||
TTLOption: gredis.TTLOption{
|
||||
EX: &expireSeconds,
|
||||
},
|
||||
NX: true,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if val.Bool() {
|
||||
defer func(ctx context.Context, key string) {
|
||||
if _, err = g.Redis().Del(ctx, key); err != nil {
|
||||
glog.Errorf(ctx, "redis client Del error: %v", err)
|
||||
}
|
||||
}(ctx, key)
|
||||
if err = fn(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
goto LOOP
|
||||
}
|
||||
|
||||
// IsLocalIP 判断是否是本地IP
|
||||
func IsLocalIP(ip string) bool {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
|
||||
Reference in New Issue
Block a user