Files
2026-09-16 00:23:49 +08:00

188 lines
5.1 KiB
Go

package service
import (
"context"
"encoding/json"
"log/slog"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqldao"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redisdao"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/tokenhub"
)
// HoroscopeService 今日运势业务服务。
type HoroscopeService struct {
horoscopeDao *mysqldao.HoroscopeDao
userDao *mysqldao.UserDao
}
// NewHoroscopeService 创建运势服务。
func NewHoroscopeService() *HoroscopeService {
return &HoroscopeService{
horoscopeDao: mysqldao.NewHoroscopeDao(),
userDao: mysqldao.NewUserDao(),
}
}
// GetToday 获取当前用户今日运势:由生日推算星座,Redis 优先、未命中回落 MySQL 并回填缓存。
func (s *HoroscopeService) GetToday(ctx context.Context, userID int64) (*dto.HoroscopeResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.HoroscopeGetToday")
defer span.End()
user, err := s.userDao.UserGetByID(ctx, userID)
if err != nil {
if isNotFound(err) {
slog.WarnContext(ctx, "horoscope: user not found", "user_id", userID)
return nil, errcode.UserNotFound
}
slog.ErrorContext(ctx, "horoscope: query user failed", "user_id", userID, "error", err)
return nil, errcode.DatabaseError
}
if user.BirthDay == 0 {
slog.InfoContext(ctx, "horoscope: birth day missing", "user_id", userID)
return nil, errcode.InvalidParams.WithDetails("请先完善生日信息")
}
zodiac := zodiacByUnix(user.BirthDay)
statDate := todayStatDate()
// 1. Redis 优先。
if data, cerr := redisdao.HoroscopeGet(ctx, zodiac, statDate); cerr == nil && len(data) > 0 {
var response dto.HoroscopeResponse
if uerr := json.Unmarshal(data, &response); uerr == nil {
return &response, nil
} else {
slog.DebugContext(ctx, "horoscope: unmarshal cached data failed, fallback to db",
"zodiac", zodiac, "error", uerr)
}
} else if cerr != nil {
slog.DebugContext(ctx, "horoscope: cache read failed, fallback to db", "zodiac", zodiac, "error", cerr)
}
// 2. 回落 MySQL。
h, derr := s.horoscopeDao.HoroscopeGetByZodiacAndDate(ctx, zodiac, statDate)
if derr != nil {
if isNotFound(derr) {
// 今日运势尚未生成,异步触发兜底生成,本次返回未生成。
slog.InfoContext(ctx, "horoscope: today data missing, trigger fallback generation", "zodiac", zodiac)
tokenhub.EnsureTodayFortunes()
return nil, errcode.HoroscopeNotFound
}
slog.ErrorContext(ctx, "horoscope: query db failed", "zodiac", zodiac, "error", derr)
return nil, errcode.DatabaseError
}
response := buildHoroscopeResponse(h)
// 回填缓存,TTL 至次日凌晨。
if data, merr := json.Marshal(response); merr == nil {
if serr := redisdao.HoroscopeSet(ctx, zodiac, statDate, data, ttlUntilNextMidnight()); serr != nil {
slog.DebugContext(ctx, "horoscope: cache backfill failed", "zodiac", zodiac, "error", serr)
}
} else {
slog.DebugContext(ctx, "horoscope: marshal response failed", "zodiac", zodiac, "error", merr)
}
return &response, nil
}
// buildHoroscopeResponse 将运势模型映射为响应。
func buildHoroscopeResponse(h *mysqlmodel.Horoscope) dto.HoroscopeResponse {
return dto.HoroscopeResponse{
StatDate: h.StatDate,
Zodiac: h.Zodiac,
OverallScore: h.OverallScore,
LoveScore: h.LoveScore,
CareerScore: h.CareerScore,
WealthScore: h.WealthScore,
DailyText: h.DailyText,
LuckyColor: h.LuckyColor,
LuckyNumber: h.LuckyNumber,
}
}
// ttlUntilNextMidnight 计算到次日 0 点的时长,用于运势缓存过期。
func ttlUntilNextMidnight() time.Duration {
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
return next.Sub(now)
}
// zodiacByUnix 根据生日(秒级 unix 时间戳)计算星座(中文名,与运势数据对齐)。
func zodiacByUnix(birthUnix int64) string {
t := time.Unix(birthUnix, 0)
return zodiacByMonthDay(int(t.Month()), t.Day())
}
// zodiacByMonthDay 根据月日返回星座中文名。
func zodiacByMonthDay(month, day int) string {
switch month {
case 1:
if day <= 19 {
return "摩羯座"
}
return "水瓶座"
case 2:
if day <= 18 {
return "水瓶座"
}
return "双鱼座"
case 3:
if day <= 20 {
return "双鱼座"
}
return "白羊座"
case 4:
if day <= 19 {
return "白羊座"
}
return "金牛座"
case 5:
if day <= 20 {
return "金牛座"
}
return "双子座"
case 6:
if day <= 21 {
return "双子座"
}
return "巨蟹座"
case 7:
if day <= 22 {
return "巨蟹座"
}
return "狮子座"
case 8:
if day <= 22 {
return "狮子座"
}
return "处女座"
case 9:
if day <= 22 {
return "处女座"
}
return "天秤座"
case 10:
if day <= 23 {
return "天秤座"
}
return "天蝎座"
case 11:
if day <= 22 {
return "天蝎座"
}
return "射手座"
case 12:
if day <= 21 {
return "射手座"
}
return "摩羯座"
}
return "摩羯座"
}