package service import ( "context" "encoding/json" "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) { return nil, errcode.UserNotFound } return nil, errcode.DatabaseError } if user.BirthDay == 0 { 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 json.Unmarshal(data, &response) == nil { return &response, nil } } // 2. 回落 MySQL。 h, derr := s.horoscopeDao.HoroscopeGetByZodiacAndDate(ctx, zodiac, statDate) if derr != nil { if isNotFound(derr) { // 今日运势尚未生成,异步触发兜底生成,本次返回未生成。 tokenhub.EnsureTodayFortunes() return nil, errcode.HoroscopeNotFound } return nil, errcode.DatabaseError } response := buildHoroscopeResponse(h) // 回填缓存,TTL 至次日凌晨。 if data, merr := json.Marshal(response); merr == nil { _ = redisdao.HoroscopeSet(ctx, zodiac, statDate, data, ttlUntilNextMidnight()) } 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 "摩羯座" }