落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
10 KiB
Go
276 lines
10 KiB
Go
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
|
||
}
|