Files
digital-psychology/apps/api/internal/star/synastry/report.go
T
jackyu66gitandCursor bd22d9dddd feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 11:37:53 +08:00

276 lines
10 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 synastry
import (
"fmt"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
)
// Report is free summary + gated detail for a synastry GrowthReport.
type Report struct {
Summary map[string]any
Detail map[string]any
}
// BuildReport builds multi-chart synastry content (五主盘 + 三推运).
func BuildReport(a, b natal.Chart, aName, bName string, asOf time.Time) (Report, error) {
if aName == "" {
aName = "我"
}
if bName == "" {
bName = "TA"
}
if asOf.IsZero() {
asOf = time.Now().In(time.FixedZone("CST", 8*3600))
}
idx := Compute(a, b, aName, bName)
cross := CrossAspects(a, b)
crossMaps := natal.AspectsAsMaps(cross)
previewN := 4
if len(crossMaps) < previewN {
previewN = len(crossMaps)
}
pack, err := BuildCharts(a, b, asOf)
if err != nil {
return Report{}, err
}
asOfStr := asOf.Format("2006-01-02")
chartsSummary := map[string]any{
"compare": map[string]any{
"tip": "比较盘:双方叠合,看互动与吸引。",
"chart_a": map[string]any{
"asc_lon": a.Rise.Lon, "planets": bodiesOut(a),
"houses": housesOut(a), "note": a.Note, "place": a.PlaceLabel,
"sun": a.Sun.Sign, "moon": a.Moon.Sign, "rise": a.Rise.Sign,
},
"chart_b": map[string]any{
"asc_lon": b.Rise.Lon, "planets": bodiesOut(b),
"houses": housesOut(b), "note": b.Note, "place": b.PlaceLabel,
"sun": b.Sun.Sign, "moon": b.Moon.Sign, "rise": b.Rise.Sign,
},
"aspects_preview": crossMaps[:previewN],
},
"composite": chartSummaryOut(pack.Composite, "组合盘:关系整体气质。"),
"davison": chartSummaryOut(pack.Davison, "时空盘:长期现实走向。"),
"marks_me": chartSummaryOut(pack.MarksMe, "马克斯盘·我:我对这段关系的内在态度。"),
"marks_other": chartSummaryOut(pack.MarksOther, "马克斯盘·TA:TA对这段关系的内在态度。"),
"overlay": overlayOut(pack.Overlay),
"composite_progressed": chartSummaryOut(pack.CompositeProgressed, "组合次限:关系当下成长态。"),
"davison_progressed": chartSummaryOut(pack.DavisonProgressed, "时空次限:现实课题的阶段性。"),
"marks_progressed": chartSummaryOut(pack.MarksProgressed, "马盘推运:我对关系的阶段心态。"),
}
summary := map[string]any{
"title": "合盘·基础",
"headline": idx.Summary,
"one_liner": fmt.Sprintf("%s与%s:五盘合观互动、气质与现实节奏。", aName, bName),
"overview": fmt.Sprintf("%s(日%s月%s升%s)× %s(日%s月%s升%s)。", aName, a.Sun.Sign, a.Moon.Sign, a.Rise.Sign, bName, b.Sun.Sign, b.Moon.Sign, b.Rise.Sign),
"me_name": aName,
"other_name": bName,
"love_index": idx.Love,
"friend_index": idx.Friend,
"marriage_index": idx.Marriage,
"match_indices": idx.AsMap(),
"love_note": idx.LoveNote,
"friend_note": idx.FriendNote,
"marriage_note": idx.MarriageNote,
"as_of": asOfStr,
// backward-compat top-level compare charts
"chart_a": map[string]any{
"asc_lon": a.Rise.Lon, "planets": bodiesOut(a),
"houses": housesOut(a), "note": a.Note, "place": a.PlaceLabel,
},
"chart_b": map[string]any{
"asc_lon": b.Rise.Lon, "planets": bodiesOut(b),
"houses": housesOut(b), "note": b.Note, "place": b.PlaceLabel,
},
"aspects_preview": crossMaps[:previewN],
"charts": chartsSummary,
"keywords": []string{"合盘", "比较盘", "组合盘", "时空盘", fmt.Sprintf("恋爱%d", idx.Love)},
"strengths_preview": []string{
fmt.Sprintf("恋爱指数 %d", idx.Love),
fmt.Sprintf("友情指数 %d", idx.Friend),
fmt.Sprintf("婚姻指数 %d", idx.Marriage),
},
"blind_spots_preview": []string{"完整相位、推运深文案与落宫解读见深度版。"},
}
bullets := make([]string, 0, min(10, len(cross)))
for i, c := range cross {
if i >= 10 {
break
}
bullets = append(bullets, c.Label)
}
compAsp := natal.AspectsAsMaps(natal.Aspects(pack.Composite))
davAsp := natal.AspectsAsMaps(natal.Aspects(pack.Davison))
progAsp := natal.AspectsAsMaps(natal.Aspects(pack.CompositeProgressed))
detail := map[string]any{
"title": "合盘·完整分析",
"as_of": asOfStr,
"aspects": crossMaps,
"charts": map[string]any{
"compare": map[string]any{"aspects": crossMaps},
"composite": map[string]any{"aspects": compAsp, "planets": bodiesOut(pack.Composite)},
"davison": map[string]any{"aspects": davAsp, "planets": bodiesOut(pack.Davison)},
"marks_me": map[string]any{"aspects": natal.AspectsAsMaps(natal.Aspects(pack.MarksMe)), "planets": bodiesOut(pack.MarksMe)},
"marks_other": map[string]any{"aspects": natal.AspectsAsMaps(natal.Aspects(pack.MarksOther)), "planets": bodiesOut(pack.MarksOther)},
"overlay": overlayOut(pack.Overlay),
"composite_progressed": map[string]any{"aspects": progAsp, "planets": bodiesOut(pack.CompositeProgressed), "note": pack.CompositeProgressed.Note},
"davison_progressed": map[string]any{"aspects": natal.AspectsAsMaps(natal.Aspects(pack.DavisonProgressed)), "planets": bodiesOut(pack.DavisonProgressed), "note": pack.DavisonProgressed.Note},
"marks_progressed": map[string]any{"aspects": natal.AspectsAsMaps(natal.Aspects(pack.MarksProgressed)), "planets": bodiesOut(pack.MarksProgressed), "note": pack.MarksProgressed.Note},
},
"sections": []map[string]any{
{"title": "恋爱互动", "body": idx.LoveNote, "bullets": []string{"说清需要比猜测更有效", "张力相位日宜慢半拍回应"}},
{"title": "友情节奏", "body": idx.FriendNote, "bullets": []string{"共同兴趣维系轻松感", "尊重彼此独处需求"}},
{"title": "长期相处", "body": idx.MarriageNote, "bullets": []string{"共同节奏与边界同等重要", "把期待写成说明书"}},
{"title": "跨盘相位", "body": "比较盘相位描述双方能量如何彼此激活或拉扯。", "bullets": bullets},
{"title": "组合与时空", "body": "组合盘看关系气质,时空盘看共同现实课题;次限呈现阶段性成长。", "bullets": []string{
fmt.Sprintf("组合盘日%s月%s升%s", pack.Composite.Sun.Sign, pack.Composite.Moon.Sign, pack.Composite.Rise.Sign),
fmt.Sprintf("时空盘日%s月%s升%s", pack.Davison.Sun.Sign, pack.Davison.Moon.Sign, pack.Davison.Rise.Sign),
fmt.Sprintf("推运日期 %s", asOfStr),
}},
{"title": "配对落宫", "body": pack.Overlay.Tip, "bullets": overlayBullets(pack.Overlay)},
},
"growth_plan": []map[string]any{
{"phase": "本周", "focus": "约一次轻松同步:各说一件欣赏与一件需要。"},
{"phase": "本月", "focus": "为高频摩擦点写一条可执行约定。"},
{"phase": "长期", "focus": "每季度复盘三指数变化与相处舒适度。"},
},
"conversation_scripts": []string{
"我想听听你怎么看我们的节奏,不急着定结论。",
"我需要一点空间整理,不是拒绝连接。",
"我们共同目标是……,这一步只做一件事。",
},
"behavior_pattern": idx.LoveNote,
"relation_style": idx.FriendNote,
"growth_direction": idx.MarriageNote,
}
return Report{Summary: summary, Detail: detail}, nil
}
func overlayBullets(o OverlayChart) []string {
out := make([]string, 0, 6)
for i, e := range o.Entries {
if i >= 6 {
break
}
if e.PlanetKey == "sun" || e.PlanetKey == "moon" || e.PlanetKey == "venus" || e.PlanetKey == "mars" {
out = append(out, fmt.Sprintf("%s在我方%d宫(%s", e.Planet, e.House, e.HouseTip))
}
}
if len(out) == 0 && len(o.Entries) > 0 {
e := o.Entries[0]
out = append(out, fmt.Sprintf("%s在我方%d宫", e.Planet, e.House))
}
return out
}
// CrossAspects finds aspects between chart A bodies and chart B bodies.
func CrossAspects(a, b natal.Chart) []natal.Aspect {
keys := []string{"sun", "moon", "rise", "mercury", "venus", "mars", "jupiter", "saturn"}
byA := map[string]natal.Body{}
byB := map[string]natal.Body{}
for _, p := range a.Planets {
byA[p.Key] = p
}
for _, p := range b.Planets {
byB[p.Key] = p
}
defs := []struct {
Type string
Angle float64
Orb float64
Label string
}{
{"conjunction", 0, 8, "合相"},
{"sextile", 60, 6, "六合"},
{"square", 90, 7, "刑相"},
{"trine", 120, 7, "拱相"},
{"opposition", 180, 8, "对冲"},
}
out := make([]natal.Aspect, 0)
for _, ka := range keys {
ba, oka := byA[ka]
if !oka {
continue
}
for _, kb := range keys {
bb, okb := byB[kb]
if !okb {
continue
}
diff := natal.AngleDiff(ba.Lon, bb.Lon)
for _, def := range defs {
orb := absF(diff - def.Angle)
if orb <= def.Orb {
out = append(out, natal.Aspect{
A: "a:" + ba.Key, B: "b:" + bb.Key,
ATitle: aNameTitle(ba.Title), BTitle: bNameTitle(bb.Title),
Type: def.Type, Angle: def.Angle, Orb: round1(orb),
Label: fmt.Sprintf("我方%s%s对方%s(容许%.1f°)", ba.Title, def.Label, bb.Title, orb),
})
break
}
}
}
}
for i := 0; i < len(out); i++ {
for j := i + 1; j < len(out); j++ {
if out[j].Orb < out[i].Orb {
out[i], out[j] = out[j], out[i]
}
}
}
if len(out) > 24 {
out = out[:24]
}
return out
}
func aNameTitle(t string) string { return "我·" + t }
func bNameTitle(t string) string { return "TA·" + t }
func bodiesOut(c natal.Chart) []map[string]any {
out := make([]map[string]any, 0, len(c.Planets))
for _, p := range c.Planets {
out = append(out, 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,
"lon": p.Lon, "element": p.Element, "modality": p.Modality,
})
}
return out
}
func housesOut(c natal.Chart) []map[string]any {
out := make([]map[string]any, 0, len(c.Houses))
for _, h := range c.Houses {
out = append(out, map[string]any{"num": h.Num, "sign": h.Sign, "sign_key": h.Key})
}
return out
}
func absF(x float64) float64 {
if x < 0 {
return -x
}
return x
}
func round1(x float64) float64 {
return float64(int(x*10+0.5)) / 10
}
func min(a, b int) int {
if a < b {
return a
}
return b
}