first commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package tokenhub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqldao"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redisdao"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// fortuneEnsureMu 用于同一实例内串行化当日运势兜底生成,避免并发重复触发 LLM。
|
||||
var fortuneEnsureMu sync.Mutex
|
||||
|
||||
// StartFortuneScheduler 启动后台定时任务:每天 0:00 生成当日运势并入库。
|
||||
func StartFortuneScheduler() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("fortune scheduler panic", "recover", r)
|
||||
}
|
||||
}()
|
||||
slog.Info("fortune scheduler started")
|
||||
|
||||
client := NewClient()
|
||||
horoscopeDao := mysqldao.NewHoroscopeDao()
|
||||
|
||||
for {
|
||||
// 每次循环重新计算到下一个午夜的等待时间,避免漂移累积。
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
||||
wait := next.Sub(now)
|
||||
|
||||
slog.Info("fortune scheduler: next run",
|
||||
"at", next.Format("2006-01-02 15:04:05"),
|
||||
"in", wait.Round(time.Second).String(),
|
||||
)
|
||||
|
||||
time.Sleep(wait)
|
||||
|
||||
// 生成 & 入库(带分布式锁,多实例仅一个执行)。
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("fortune generation panic", "recover", r)
|
||||
}
|
||||
}()
|
||||
runFortuneGenerationWithLock(client, horoscopeDao)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// runFortuneGeneration 调用混元生成运势,写入数据库。
|
||||
func runFortuneGeneration(client *Client, horoscopeDao *mysqldao.HoroscopeDao) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
ctx, span := global.Tracer.Start(ctx, "scheduler.FortuneGeneration")
|
||||
defer span.End()
|
||||
|
||||
slog.InfoContext(ctx, "generating daily fortunes...")
|
||||
|
||||
result, err := client.GenerateDailyFortunes(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
slog.ErrorContext(ctx, "generate fortunes failed", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// result.Date 形如 "2006-01-02",转换为 Horoscope.StatDate 的 YYYYMMDD 整数。
|
||||
date, err := time.Parse("2006-01-02", result.Date)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "parse fortune date failed", "date", result.Date, "error", err)
|
||||
return
|
||||
}
|
||||
statDate, err := strconv.ParseInt(date.Format("20060102"), 10, 64)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "parse stat date failed", "date", result.Date, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, f := range result.Fortunes {
|
||||
horoscope := f.ToHoroscope(statDate)
|
||||
if err := horoscopeDao.HoroscopeCreate(ctx, horoscope); err != nil {
|
||||
slog.ErrorContext(ctx, "save horoscope failed", "zodiac", f.Zodiac, "error", err)
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "daily fortune generation completed",
|
||||
"total", len(result.Fortunes),
|
||||
"saved", successCount,
|
||||
)
|
||||
}
|
||||
|
||||
// runFortuneGenerationWithLock 带 Redis 分布式锁执行当日运势生成:
|
||||
// 多实例部署时仅一个实例执行;锁释放校验持有者 value,防止超时误删其它实例的锁。
|
||||
func runFortuneGenerationWithLock(client *Client, horoscopeDao *mysqldao.HoroscopeDao) {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
lockKey := "ztcloset:lock:fortune:daily:" + today
|
||||
lockVal := uuid.NewString()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
ok, lerr := redisdao.TryLock(ctx, lockKey, lockVal, 5*time.Minute)
|
||||
if lerr != nil || !ok {
|
||||
slog.Info("fortune generation already in progress, skip", "date", today)
|
||||
return
|
||||
}
|
||||
defer redisdao.Unlock(context.Background(), lockKey, lockVal)
|
||||
|
||||
runFortuneGeneration(client, horoscopeDao)
|
||||
}
|
||||
|
||||
// EnsureTodayFortunes 检查今日运势数据是否完整,不完整则自动生成。
|
||||
// 该函数异步执行,适用于服务器启动时兜底。
|
||||
func EnsureTodayFortunes() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("EnsureTodayFortunes panic", "recover", r)
|
||||
}
|
||||
}()
|
||||
// 进程内防重:已有兜底生成在进行中则直接跳过。
|
||||
if !fortuneEnsureMu.TryLock() {
|
||||
return
|
||||
}
|
||||
defer fortuneEnsureMu.Unlock()
|
||||
|
||||
client := NewClient()
|
||||
horoscopeDao := mysqldao.NewHoroscopeDao()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
now := time.Now()
|
||||
today := now.Format("2006-01-02")
|
||||
statDate, err := strconv.ParseInt(now.Format("20060102"), 10, 64)
|
||||
if err != nil {
|
||||
slog.Error("EnsureTodayFortunes: parse stat date failed", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
horoscopes, err := horoscopeDao.HoroscopeGetByDate(ctx, statDate)
|
||||
if err != nil {
|
||||
slog.Warn("EnsureTodayFortunes: query today fortunes failed, will try to generate", "error", err, "date", today)
|
||||
}
|
||||
|
||||
if len(horoscopes) >= 12 {
|
||||
slog.Info("EnsureTodayFortunes: today fortunes already complete, skip", "date", today, "count", len(horoscopes))
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("EnsureTodayFortunes: today fortunes incomplete, generating...",
|
||||
"date", today,
|
||||
"existing", len(horoscopes),
|
||||
"needed", 12,
|
||||
)
|
||||
|
||||
runFortuneGenerationWithLock(client, horoscopeDao)
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user