绑定 ESS 双轨治理,拆分超大 H5 页与 Go 引擎,抽出 membership 服务, 并将 star/fortune 重命名为 outlook(JSON 双写兼容);同时修复 /psy API 代理与首页 + 菜单层级。 Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
8.8 KiB
Go
262 lines
8.8 KiB
Go
// 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,
|
||
}
|
||
}
|