// Package outlook synthesizes daily/weekly/monthly/yearly/lifetime scores and transits. package outlook import ( "fmt" "math" "time" "github.com/yuxingu/digital-psychology/apps/api/internal/star/natal" ) // Period is one fortune block. type Period struct { Key string `json:"key"` Title string `json:"title"` Score int `json:"score"` Label string `json:"label"` Dims map[string]int `json:"dims"` Tip string `json:"tip"` Lucky string `json:"lucky"` Caution string `json:"caution"` Focus string `json:"focus"` } // Transit is a simplified day transit tip relative to natal. type Transit struct { Key string `json:"key"` Title string `json:"title"` Aspect string `json:"aspect"` Tip string `json:"tip"` } // Bundle holds all periods plus lifetime and transits. type Bundle struct { Daily Period `json:"daily"` Weekly Period `json:"weekly"` Monthly Period `json:"monthly"` Yearly Period `json:"yearly"` Lifetime Period `json:"lifetime"` Transits []Transit `json:"transits"` } // Build returns fortune for natal chart as of asOf (date matters). func Build(chart natal.Chart, asOf time.Time) Bundle { if asOf.IsZero() { asOf = time.Now() } sun := chart.Sun.Lon moon := chart.Moon.Lon daySeed := asOf.Year()*1000 + asOf.YearDay() + int(sun) + int(moon) daily := period("daily", "今日运势", daySeed, sun, moon, chart, []string{"行动", "沟通", "情绪", "财运", "桃花"}, []string{ fmt.Sprintf("发挥太阳「%s」的主动性,先完成一件小事。", chart.Sun.Sign), fmt.Sprintf("照顾月亮「%s」的情绪需要,给自己缓冲。", chart.Moon.Sign), "适合推进沟通:先复述再提方案。", "宜整理待办与开销,给节奏一点秩序。", "轻社交或独处充电均可,按电量选择。", }) weekSeed := asOf.Year()*100 + isoWeek(asOf) + int(sun)/3 weekly := period("weekly", "本周运势", weekSeed, sun, moon, chart, []string{"事业", "关系", "学习", "休息", "决策"}, []string{ "本周适合定一个可交付的小目标并公开承诺。", "关系上主动约一次轻松同步,不谈对错。", "学习/复盘:把灵感写成三条行动。", "安排半日空档恢复精力。", "重大决定用「可逆/不可逆」分类后再选速度。", }) monthSeed := asOf.Year()*12 + int(asOf.Month()) + int(moon)/5 monthly := period("monthly", "本月运势", monthSeed, sun, moon, chart, []string{"感情", "事业", "财务", "健康节奏", "人际"}, []string{ fmt.Sprintf("本月主题贴近「%s」:把热情落成节奏。", chart.Sun.Sign), "感情上说清需要,比猜测更有效。", "财务宜做一次月度复盘,砍掉低价值开销。", "作息与运动选可持续的小剂量。", "拓展一个人际弱连接,可能带来信息增益。", }) yearSeed := asOf.Year() + int(sun) + int(chart.Rise.Lon)/10 yearly := period("yearly", "今年运势", yearSeed, sun, moon, chart, []string{"成长主线", "关系课题", "事业方向", "财富节奏", "身心"}, []string{ fmt.Sprintf("今年宜强化太阳「%s」优势,并补月亮「%s」的安全感建设。", chart.Sun.Sign, chart.Moon.Sign), "关系课题:边界与亲密并重,写成说明书。", "事业上选择能看到里程碑的路径。", "财富:先稳现金流,再谈进取配置。", "身心:季度体检式复盘情绪与睡眠。", }) life := lifetime(chart) tr := transits(chart, asOf) return Bundle{ Daily: daily, Weekly: weekly, Monthly: monthly, Yearly: yearly, Lifetime: life, Transits: tr, } } func lifetime(chart natal.Chart) Period { sat := bodyLon(chart, "saturn") seed := int(chart.Sun.Lon) + int(sat)/2 + int(chart.Moon.Lon)/3 score := 62 + seed%28 stages := []string{ fmt.Sprintf("成长主线贴近太阳「%s」:用可见行动定义自我。", chart.Sun.Sign), fmt.Sprintf("情感底色来自月亮「%s」:安全感与表达节奏需要长期经营。", chart.Moon.Sign), fmt.Sprintf("对外姿态偏上升「%s」:第一印象可以主动校准。", chart.Rise.Sign), } tip := stages[seed%len(stages)] + " 人生阶段不必一次做完,按十年课题拆解更稳。" return Period{ Key: "lifetime", Title: "一生运势摘要", Score: score, Label: scoreLabel(score), Dims: map[string]int{ "love": 50 + seed%40, "career": 52 + (seed*3)%40, "money": 48 + (seed*5)%42, "mood": 55 + (seed*7)%35, }, Tip: tip, Focus: "人生阶段", Lucky: "长期复盘 · 边界清晰", Caution: "避免把阶段标签当成宿命;可调整节奏与选择。", } } func transits(chart natal.Chart, asOf time.Time) []Transit { // Approximate "transit" sun/moon using same ephemeris at asOf noon CST. loc := time.FixedZone("CST", 8*3600) local := time.Date(asOf.Year(), asOf.Month(), asOf.Day(), 12, 0, 0, 0, loc) tChart, err := natal.Compute(local, strPtr("12:00"), placePtr(chart.PlaceLabel)) if err != nil { return nil } tSun, tMoon := tChart.Sun.Lon, tChart.Moon.Lon out := []Transit{} out = append(out, transitHit("transit_sun_natal_sun", "行运太阳×本命太阳", tSun, chart.Sun.Lon, "适合主动推进与自我表达相关的事。", "宜复盘目标,不急着扩张战线。", "注意沟通语气,先对齐再行动。")) out = append(out, transitHit("transit_moon_natal_moon", "行运月亮×本命月亮", tMoon, chart.Moon.Lon, "情绪敏感日,给自己更多缓冲。", "适合温柔连结与休息充电。", "避免情绪化决策,先写下来再说。")) out = append(out, transitHit("transit_sun_natal_moon", "行运太阳×本命月亮", tSun, chart.Moon.Lon, "外在节奏触动内在需求,适合说清感受。", "工作与情绪平衡日,留一点空白。", "别硬撑社交,按电量选择场合。")) return out } func transitHit(key, title string, a, b float64, conj, soft, hard string) Transit { diff := natal.AngleDiff(a, b) aspect, tip := "弱互动", "节奏平常,按计划推进即可。" switch { case near(diff, 0, 8): aspect, tip = "合相", conj case near(diff, 60, 6) || near(diff, 120, 7): aspect, tip = "和谐相位", soft case near(diff, 90, 7) || near(diff, 180, 8): aspect, tip = "张力相位", hard } return Transit{Key: key, Title: title, Aspect: aspect, Tip: tip} } func near(diff, target, orb float64) bool { return math.Abs(diff-target) <= orb } func bodyLon(c natal.Chart, key string) float64 { for _, p := range c.Planets { if p.Key == key { return p.Lon } } return 0 } func strPtr(s string) *string { return &s } func placePtr(s string) *string { if s == "" || s == "默认(未填出生地)" { return nil } return &s } func period(key, title string, seed int, sun, moon float64, chart natal.Chart, focuses, tips []string) Period { score := 55 + (seed*7+int(sun)+int(moon))%41 // 55–95 love := 50 + (seed*3+int(moon))%46 career := 50 + (seed*5+int(sun))%46 money := 48 + (seed*11+int(chart.Rise.Lon))%47 mood := 52 + (seed*13+int(moon)/2)%45 i := seed % len(tips) return Period{ Key: key, Title: title, Score: score, Label: scoreLabel(score), Dims: map[string]int{ "love": love, "career": career, "money": money, "mood": mood, }, Tip: tips[i], Focus: focuses[i%len(focuses)], Lucky: luckyFrom(seed), Caution: cautionFrom(seed, chart), } } func scoreLabel(s int) string { switch { case s >= 85: return "大吉" case s >= 75: return "吉" case s >= 65: return "中平偏吉" case s >= 55: return "平稳" default: return "需谨慎" } } func luckyFrom(seed int) string { colors := []string{"红色", "金色", "蓝色", "绿色", "紫色", "白色"} nums := []string{"3", "6", "7", "8", "9"} return colors[seed%len(colors)] + " · 数字 " + nums[seed%len(nums)] } func cautionFrom(seed int, chart natal.Chart) string { cautions := []string{ "避免冲动承诺,尤其财务相关。", fmt.Sprintf("「%s」能量过强时易急躁,先深呼吸再回复。", chart.Sun.Sign), "少开多线任务,完成比完美重要。", "情绪波动时先写下来,再找人聊。", } return cautions[seed%len(cautions)] } func isoWeek(t time.Time) int { _, w := t.ISOWeek() return w } // AsMap for JSON embedding. func (b Bundle) AsMap() map[string]any { tr := make([]map[string]any, 0, len(b.Transits)) for _, t := range b.Transits { tr = append(tr, map[string]any{ "key": t.Key, "title": t.Title, "aspect": t.Aspect, "tip": t.Tip, }) } return map[string]any{ "daily": periodMap(b.Daily), "weekly": periodMap(b.Weekly), "monthly": periodMap(b.Monthly), "yearly": periodMap(b.Yearly), "lifetime": periodMap(b.Lifetime), "transits": tr, } } func periodMap(p Period) map[string]any { return map[string]any{ "key": p.Key, "title": p.Title, "score": p.Score, "label": p.Label, "dims": p.Dims, "tip": p.Tip, "lucky": p.Lucky, "caution": p.Caution, "focus": p.Focus, } }