This commit is contained in:
2026-09-15 23:04:13 +08:00
parent ade2453083
commit 8c2ea84939
17 changed files with 749 additions and 161 deletions
+126
View File
@@ -0,0 +1,126 @@
package redisdao
import (
"context"
"encoding/json"
"fmt"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
"github.com/redis/go-redis/v9"
)
// aiRecommendTaskTTL 任务快照保留时长,需覆盖前端轮询窗口。
const aiRecommendTaskTTL = 15 * time.Minute
// aiRecommendReleaseScript 仅当活跃位仍属于本次任务时才删除,
// 避免任务超时释放后又被后续任务占用,却被旧任务误删。
var aiRecommendReleaseScript = redis.NewScript(`
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
`)
// AIRecommendTask AI 推荐异步任务快照。
type AIRecommendTask struct {
TaskID string `json:"task_id"`
UserID int64 `json:"user_id"`
Status int8 `json:"status"` // 1=进行中 2=成功 3=失败
Result json.RawMessage `json:"result,omitempty"`
ErrorCode int `json:"error_code,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
CreateTime int64 `json:"create_time"`
}
// AIRecommendTaskAcquire 抢占用户的 AI 推荐活跃位。
// - 抢占成功:(true, "", nil);
// - 已有任务在执行:(false, 进行中的 task_id, nil),调用方应把它当作幂等结果返回;
// - Redis 异常:(false, "", err)。
func AIRecommendTaskAcquire(ctx context.Context, userID int64, taskID string, ttl time.Duration) (bool, string, error) {
ctx, span := global.Tracer.Start(ctx, "redisdao.AIRecommendTaskAcquire")
defer span.End()
key := fmt.Sprintf(redismodel.KeyLockAIRecommend, userID)
ok, err := global.RedisEngine.SetNX(ctx, key, taskID, ttl).Result()
if err != nil {
span.RecordError(err)
return false, "", err
}
if ok {
return true, "", nil
}
// 已有任务在跑:读回它的 task_id,让前端继续轮询同一个任务。
running, gerr := global.RedisEngine.Get(ctx, key).Result()
if gerr == redis.Nil {
// 活跃位正好在两次调用之间过期,交由上层按「稍后重试」处理。
return false, "", nil
}
if gerr != nil {
span.RecordError(gerr)
return false, "", gerr
}
return false, running, nil
}
// AIRecommendTaskRelease 释放活跃位,仅当值仍等于本次 task_id 时才删除。
// 失败仅由调用方记录,不阻断主流程。
func AIRecommendTaskRelease(ctx context.Context, userID int64, taskID string) error {
if global.RedisEngine == nil {
return nil
}
ctx, span := global.Tracer.Start(ctx, "redisdao.AIRecommendTaskRelease")
defer span.End()
key := fmt.Sprintf(redismodel.KeyLockAIRecommend, userID)
if err := aiRecommendReleaseScript.Run(ctx, global.RedisEngine, []string{key}, taskID).Err(); err != nil {
span.RecordError(err)
return err
}
return nil
}
// AIRecommendTaskSave 写入任务快照(覆盖式),TTL 由 aiRecommendTaskTTL 决定。
func AIRecommendTaskSave(ctx context.Context, task *AIRecommendTask) error {
ctx, span := global.Tracer.Start(ctx, "redisdao.AIRecommendTaskSave")
defer span.End()
data, err := json.Marshal(task)
if err != nil {
span.RecordError(err)
return fmt.Errorf("marshal ai recommend task: %w", err)
}
key := fmt.Sprintf(redismodel.KeyAIRecommendTask, task.TaskID)
if err := global.RedisEngine.Set(ctx, key, data, aiRecommendTaskTTL).Err(); err != nil {
span.RecordError(err)
return err
}
return nil
}
// AIRecommendTaskGet 读取任务快照,任务不存在或已过期返回 (nil, nil)。
func AIRecommendTaskGet(ctx context.Context, taskID string) (*AIRecommendTask, error) {
ctx, span := global.Tracer.Start(ctx, "redisdao.AIRecommendTaskGet")
defer span.End()
key := fmt.Sprintf(redismodel.KeyAIRecommendTask, taskID)
data, err := global.RedisEngine.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, nil
}
if err != nil {
span.RecordError(err)
return nil, err
}
var task AIRecommendTask
if err := json.Unmarshal(data, &task); err != nil {
span.RecordError(err)
return nil, fmt.Errorf("unmarshal ai recommend task: %w", err)
}
return &task, nil
}