Files
2026-09-15 23:04:13 +08:00

127 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}