// Package star builds 星座 reports (natal chart · fortune · deep copy). package star import ( "fmt" "time" "github.com/yuxingu/digital-psychology/apps/api/internal/star/fortune" "github.com/yuxingu/digital-psychology/apps/api/internal/star/natal" ) // Output is free summary + gated detail. type Output struct { Summary map[string]any `json:"summary"` Detail map[string]any `json:"detail"` } // BuildOpts configures report generation. type BuildOpts struct { Birth time.Time BirthTime *string BirthPlace *string Name string AsOf time.Time // fortune anchor; zero = now } // Build generates StarProfile from birth date (compat wrapper). func Build(birth time.Time, birthTime *string, displayName string) (Output, error) { return BuildWith(BuildOpts{Birth: birth, BirthTime: birthTime, Name: displayName}) } // BuildWith generates a full star report. func BuildWith(opts BuildOpts) (Output, error) { name := opts.Name if name == "" { name = "你" } chart, err := natal.Compute(opts.Birth, opts.BirthTime, opts.BirthPlace) if err != nil { return Output{}, err } sun := chart.Sun moon := chart.Moon rise := chart.Rise pack := packs[sun.SignKey] if pack.Label == "" { pack = defaultPack(signMeta{Key: sun.SignKey, Label: sun.Sign, Element: sun.Element, Modality: sun.Modality}) } moonPack := packs[moon.SignKey] risePack := packs[rise.SignKey] signCards := []map[string]any{ signCard("sun", "太阳星座", sun, pack.SunTeaser, pack.Keywords), signCard("moon", "月亮星座", moon, fmt.Sprintf("情绪底色更偏「%s」", moon.Sign), moonPack.Keywords), signCard("rise", "上升星座", rise, fmt.Sprintf("第一印象更偏「%s」", rise.Sign), risePack.Keywords), } dims := []map[string]any{ {"key": "sun", "title": "太阳风格", "teaser": pack.SunTeaser, "score": pack.Scores["sun"]}, {"key": "moon", "title": "月亮节奏", "teaser": fmt.Sprintf("情绪底色更偏「%s」", moon.Sign), "score": pack.Scores["moon"]}, {"key": "rise", "title": "上升表达", "teaser": fmt.Sprintf("第一印象更偏「%s」", rise.Sign), "score": pack.Scores["rise"]}, {"key": "relation", "title": "关系互动", "teaser": pack.RelationTeaser, "score": pack.Scores["relation"]}, {"key": "career", "title": "事业节奏", "teaser": pack.CareerTeaser, "score": pack.Scores["career"]}, {"key": "growth", "title": "成长方向", "teaser": pack.GrowthTeaser, "score": pack.Scores["growth"]}, } asOf := opts.AsOf if asOf.IsZero() { asOf = time.Now() } fort := fortune.Build(chart, asOf) daily := fort.Daily planetsOut := make([]map[string]any, 0, len(chart.Planets)) for _, p := range chart.Planets { planetsOut = append(planetsOut, map[string]any{ "key": p.Key, "title": p.Title, "sign": p.Sign, "sign_key": p.SignKey, "degree": fmt.Sprintf("%.1f°", p.Degree), "house": p.House, "element": p.Element, "modality": p.Modality, "lon": p.Lon, }) } housesOut := make([]map[string]any, 0, len(chart.Houses)) for _, h := range chart.Houses { // Whole-sign house cusp ≈ asc sign start + (n-1)*30 cusp := float64((signIndexOf(h.Key)) * 30) housesOut = append(housesOut, map[string]any{ "num": h.Num, "sign": h.Sign, "sign_key": h.Key, "cusp_lon": cusp, }) } aspects := natal.Aspects(chart) aspectMaps := natal.AspectsAsMaps(aspects) previewN := min(4, len(aspectMaps)) aspectPreview := aspectMaps[:previewN] summary := map[string]any{ "title": "星座·基础", "style_label": pack.Label, "headline": fmt.Sprintf("%s的星座更偏「%s」", name, pack.Label), "one_liner": pack.OneLiner, "overview": fmt.Sprintf("%s%s(太阳:%s;月亮:%s;上升:%s)。", name, pack.Overview, sun.Sign, moon.Sign, rise.Sign), "life_tip": pack.LifeTip, "keywords": pack.Keywords, "dimensions": dims, "sign_cards": signCards, "sun_sign": sun.Sign, "moon_sign": moon.Sign, "rise_sign": rise.Sign, "chart": map[string]any{ "note": chart.Note, "place": chart.PlaceLabel, "has_time": chart.HasTime, "has_place": chart.HasPlace, "lat": chart.Lat, "lng": chart.Lng, "asc_lon": rise.Lon, "houses": housesOut, }, "planets": planetsOut, "aspects_preview": aspectPreview, "fortune": fort.AsMap(), "transits": fort.AsMap()["transits"], "daily_soft": map[string]any{ "title": daily.Title, "focus": daily.Focus, "tip": daily.Tip, "energy": daily.Score, "note": daily.Label, "score": daily.Score, "label": daily.Label, }, "strengths_preview": pack.Strengths[:min(3, len(pack.Strengths))], "blind_spots_preview": []string{"完整相位与年运详解见深度版。"}, "interaction_tags": []string{ fmt.Sprintf("太阳·%s", sun.Sign), fmt.Sprintf("月亮·%s", moon.Sign), fmt.Sprintf("上升·%s", rise.Sign), pack.Label, }, } aspectBullets := make([]string, 0, min(8, len(aspects))) for i, a := range aspects { if i >= 8 { break } aspectBullets = append(aspectBullets, a.Label) } detail := map[string]any{ "title": "星座·完整分析", "aspects": aspectMaps, "sections": []map[string]any{ section("太阳星座 · "+sun.Sign, pack.SunDeep, pack.SunBullets), section("月亮星座 · "+moon.Sign, fmt.Sprintf("情绪底色带有「%s」特质:%s", moon.Sign, moonPack.MoonDeep), moonPack.MoonBullets), section("上升星座 · "+rise.Sign, fmt.Sprintf("对外第一印象偏「%s」:%s", rise.Sign, risePack.RiseDeep), risePack.RiseBullets), section("行星相位要点", "主要相位帮助理解行动、情感与责任节奏之间的张力与互补。", aspectBullets), section("行星落座", "落座与宫位是日常节奏的参照。", planetBullets(chart)), section("年运详解", fort.Yearly.Tip+" "+fort.Yearly.Caution, []string{ fmt.Sprintf("综合分 %d(%s)", fort.Yearly.Score, fort.Yearly.Label), fmt.Sprintf("感情 %d · 事业 %d · 财务 %d · 心情 %d", fort.Yearly.Dims["love"], fort.Yearly.Dims["career"], fort.Yearly.Dims["money"], fort.Yearly.Dims["mood"]), fort.Yearly.Lucky, }), section("一生运势", fort.Lifetime.Tip, []string{fort.Lifetime.Caution, fort.Lifetime.Lucky}), section("关系互动", pack.RelationDeep, pack.RelationBullets), section("事业与学习节奏", pack.CareerDeep, pack.CareerBullets), section("成长方向", pack.GrowthDeep, pack.GrowthBullets), }, "strengths": pack.Strengths, "blind_spots": pack.BlindSpots, "growth_plan": []map[string]any{ {"phase": "本周", "focus": pack.PlanWeek}, {"phase": "本月", "focus": pack.PlanMonth}, {"phase": "长期", "focus": pack.PlanLong}, }, "conversation_scripts": pack.Scripts, "faq": pack.FAQ, "behavior_pattern": pack.SunDeep, "relation_style": pack.RelationDeep, "growth_direction": pack.GrowthDeep, "fortune_detail": fort.AsMap(), } return Output{Summary: summary, Detail: detail}, nil } func signIndexOf(key string) int { for i, s := range []string{ "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces", } { if s == key { return i } } return 0 } func planetBullets(chart natal.Chart) []string { out := make([]string, 0, 6) for _, key := range []string{"mercury", "venus", "mars", "jupiter", "saturn"} { for _, p := range chart.Planets { if p.Key == key { out = append(out, fmt.Sprintf("%s在%s第%d宫", p.Title, p.Sign, p.House)) break } } } return out } func signCard(key, title string, b natal.Body, teaser string, keywords []string) map[string]any { ks := keywords if len(ks) > 3 { ks = ks[:3] } return map[string]any{ "key": key, "title": title, "label": b.Sign, "element": b.Element, "modality": b.Modality, "teaser": teaser, "keywords": ks, } } func section(title, body string, bullets []string) map[string]any { return map[string]any{"title": title, "body": body, "bullets": bullets} } func min(a, b int) int { if a < b { return a } return b } // SignLabels returns sun/moon/rise labels (uses natal engine). func SignLabels(birth time.Time, birthTime *string) (sun, moon, rise string) { return SignLabelsPlace(birth, birthTime, nil) } // SignLabelsPlace includes birth place for rising accuracy. func SignLabelsPlace(birth time.Time, birthTime, place *string) (sun, moon, rise string) { c, err := natal.Compute(birth, birthTime, place) if err != nil { return "", "", "" } return c.Sun.Sign, c.Moon.Sign, c.Rise.Sign } // NatalChart exposes chart for relation/synastry. func NatalChart(birth time.Time, birthTime, place *string) (natal.Chart, error) { return natal.Compute(birth, birthTime, place) } type signMeta struct { Key, Label, Element, Modality string } type pack struct { Label, OneLiner, Overview, LifeTip string Keywords []string Scores map[string]int SunTeaser, RelationTeaser, CareerTeaser, GrowthTeaser string SunDeep, MoonDeep, RiseDeep, RelationDeep, CareerDeep, GrowthDeep string SunBullets, MoonBullets, RiseBullets, RelationBullets, CareerBullets, GrowthBullets []string Strengths, BlindSpots, Scripts []string PlanWeek, PlanMonth, PlanLong string FAQ []map[string]string } var packs = map[string]pack{} func init() { for _, s := range []signMeta{ {"aries", "白羊", "火", "开创"}, {"taurus", "金牛", "土", "固定"}, {"gemini", "双子", "风", "变动"}, {"cancer", "巨蟹", "水", "开创"}, {"leo", "狮子", "火", "固定"}, {"virgo", "处女", "土", "变动"}, {"libra", "天秤", "风", "开创"}, {"scorpio", "天蝎", "水", "固定"}, {"sagittarius", "射手", "火", "变动"}, {"capricorn", "摩羯", "土", "开创"}, {"aquarius", "水瓶", "风", "固定"}, {"pisces", "双鱼", "水", "变动"}, } { packs[s.Key] = defaultPack(s) } packs["aries"] = enrich(packs["aries"], "开创行动者", "先动起来,再在行动里想清楚。", "你容易被新目标点燃,讨厌拖沓。优势是启动快;需要留意的是收尾与倾听。") packs["taurus"] = enrich(packs["taurus"], "稳健沉淀者", "你重视踏实与感官舒适,变化太快会消耗你。", "你擅长把事情做稳做久。关系与工作里都需要可预期的节奏。") packs["gemini"] = enrich(packs["gemini"], "灵活连接者", "你靠好奇与对话充电,也容易分心。", "信息与交流是你的养分。把想法收成一个可交付的小闭环,会更有成就感。") packs["cancer"] = enrich(packs["cancer"], "细腻守护者", "你对情绪与归属很敏感,安全比热闹更重要。", "你擅长照顾氛围与关系。记得也把自己的需要说清楚,而不是只默默撑着。") packs["leo"] = enrich(packs["leo"], "热烈表达者", "你需要被看见,也愿意照亮别人。", "热情与表达是你的名片。把认可需求说成具体请求,关系会更顺。") packs["virgo"] = enrich(packs["virgo"], "细致完善者", "你看见细节与改进空间,也容易自我要求过高。", "把「足够好」纳入标准,你会轻松很多,交付也会更快。") packs["libra"] = enrich(packs["libra"], "平衡协调者", "你追求公平与和谐,有时会为难自己。", "协调是天赋。重要决定里请给自己一票,而不只是各方折中。") packs["scorpio"] = enrich(packs["scorpio"], "深潜洞察者", "你看重真诚与深度,讨厌浮于表面。", "信任慢、一旦建立则很深。练习用语言同步感受,减少猜疑消耗。") packs["sagittarius"] = enrich(packs["sagittarius"], "开阔探索者", "你需要视野与意义,讨厌被框死。", "探索欲强。给自由一点结构,热情才能变成持续作品。") packs["capricorn"] = enrich(packs["capricorn"], "负责攀登者", "你看长远目标,愿意为结果负责。", "担当是优势。学会求助与休息,攀登才可持续。") packs["aquarius"] = enrich(packs["aquarius"], "独特思考者", "你重视独立与新意,也需要被理解。", "独特视角是礼物。把想法翻译成别人跟得上的一步行动。") packs["pisces"] = enrich(packs["pisces"], "共感想象者", "你感受力强,边界容易被情绪浪潮冲开。", "共情是天赋。区分「理解」与「承包」,你会更稳。") } func defaultPack(s signMeta) pack { return pack{ Label: s.Label + "风格探索者", OneLiner: "用星座认识自己的节奏与运势起伏。", Overview: "在星座框架里,你的表达与需求有独特侧重。", LifeTip: "本周选一件小事完整做完,并告诉亲近的人你的真实需要。", Keywords: []string{s.Label, s.Element + "象", s.Modality, "星座"}, Scores: map[string]int{"sun": 78, "moon": 70, "rise": 72, "relation": 74, "career": 73, "growth": 75}, SunTeaser: "核心驱动力清晰,行动有自己的节拍。", RelationTeaser: "关系里需要被理解与尊重节奏。", CareerTeaser: "适合发挥你风格优势的场景。", GrowthTeaser: "下一步是看见盲区并小步调整。", SunDeep: "太阳星座描述你的核心动机与自我表达。", MoonDeep: "月亮星座指向情绪调节与安全感来源。", RiseDeep: "上升星座影响别人对你的第一印象。", RelationDeep: "关系中把需求说具体,比期待对方「应该懂」更有效。", CareerDeep: "工作上优先发挥你的风格优势,并用小里程碑对抗拖延。", GrowthDeep: "成长是认识模式后做可验证的小调整。", SunBullets: []string{"核心动机可被命名", "表达有风格偏好", "适合自我探索"}, MoonBullets: []string{"情绪需要出口", "安全感来源因人而异", "独处或连接可充电"}, RiseBullets: []string{"第一印象可调节", "外显≠全部自我", "可练习温和表达"}, RelationBullets: []string{"需求具体化", "尊重双方节奏", "冲突先复述再方案"}, CareerBullets: []string{"发挥风格优势", "小步交付", "复盘节奏"}, GrowthBullets: []string{"看见模式", "小步验证", "结合运势调整"}, Strengths: []string{"风格清晰", "可探索性强", "利于自我对话"}, BlindSpots: []string{"标签固化", "忽略情境差异", "过度解读"}, Scripts: []string{"我想先说清我的节奏,再听你的。", "我不是冷淡,我需要一点整理时间。", "我们共同目标是……,下一步只定一件事。"}, PlanWeek: "用三句话写下:我的优势 / 我的消耗点 / 我本周要试的一小步。", PlanMonth: "在关系或工作中练习两次「先复述对方,再提需要」。", PlanLong: "建立个人节奏手册:什么充电、什么耗电、如何请求支持。", FAQ: []map[string]string{ {"q": "运势是预测吗?", "a": "运势分与建议帮助你调整节奏与决策,请结合现实判断,勿作唯一依据。"}, {"q": "星盘准吗?", "a": "出生时与出生地越完整,上升与宫位越贴近;算法为可复现近似星历。"}, }, } } func enrich(base pack, label, one, overview string) pack { base.Label = label base.OneLiner = one base.Overview = overview base.Keywords = []string{label, "星座", "运势"} return base }