// Package star builds 星座 reports (natal chart · period outlook · deep copy). package star import ( "fmt" "time" "github.com/yuxingu/digital-psychology/apps/api/internal/star/natal" "github.com/yuxingu/digital-psychology/apps/api/internal/star/outlook" ) // 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 // period outlook 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 := outlook.Build(chart, asOf) daily := fort.Daily fortMap := fort.AsMap() 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, "outlook": fortMap, "transits": fortMap["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.Boost, }), section("一生运势", fort.Lifetime.Tip, []string{fort.Lifetime.Caution, fort.Lifetime.Boost}), 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, "outlook_detail": fortMap, } return Output{Summary: summary, Detail: detail}, nil }