Files
digital-psychology/apps/api/internal/star/engine.go
T
jackyu66gitandCursor 89756f65b4 feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 01:27:58 +08:00

183 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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().In(time.FixedZone("CST", 8*3600))
} else {
asOf = asOf.In(time.FixedZone("CST", 8*3600))
}
fort := outlook.Build(chart, asOf)
daily := fort.Daily
fortMap := fort.AsMap()
dayKey := asOf.Format("2006-01-02")
validUntil := time.Date(asOf.Year(), asOf.Month(), asOf.Day()+1, 0, 0, 0, 0, asOf.Location())
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,
},
"as_of": dayKey,
"valid_until": validUntil.Format(time.RFC3339),
"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
}