feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
package synastry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/ephemeris"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
|
||||
)
|
||||
|
||||
// ChartPack holds all synastry chart modes for one pair.
|
||||
type ChartPack struct {
|
||||
Composite natal.Chart
|
||||
Davison natal.Chart
|
||||
MarksMe natal.Chart
|
||||
MarksOther natal.Chart
|
||||
Overlay OverlayChart
|
||||
CompositeProgressed natal.Chart
|
||||
DavisonProgressed natal.Chart
|
||||
MarksProgressed natal.Chart
|
||||
AsOf time.Time
|
||||
}
|
||||
|
||||
// OverlayHouse is B's planet in A's whole-sign house.
|
||||
type OverlayHouse struct {
|
||||
PlanetKey string `json:"planet_key"`
|
||||
Planet string `json:"planet"`
|
||||
Sign string `json:"sign"`
|
||||
House int `json:"house"`
|
||||
HouseTip string `json:"house_tip"`
|
||||
}
|
||||
|
||||
// OverlayChart is the pairing/overlay table.
|
||||
type OverlayChart struct {
|
||||
HousesA []natal.House `json:"houses_a"`
|
||||
Entries []OverlayHouse `json:"entries"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
// BuildCharts computes five main charts + three progressed variants.
|
||||
func BuildCharts(a, b natal.Chart, asOf time.Time) (ChartPack, error) {
|
||||
if asOf.IsZero() {
|
||||
asOf = time.Now().In(time.FixedZone("CST", 8*3600))
|
||||
}
|
||||
comp := CompositeChart(a, b)
|
||||
dav, err := DavisonChart(a, b)
|
||||
if err != nil {
|
||||
return ChartPack{}, err
|
||||
}
|
||||
marksMe := MarksChart(dav, a)
|
||||
marksOther := MarksChart(dav, b)
|
||||
overlay := Overlay(a, b)
|
||||
|
||||
progA, err := ProgressChart(a, asOf)
|
||||
if err != nil {
|
||||
return ChartPack{}, err
|
||||
}
|
||||
progB, err := ProgressChart(b, asOf)
|
||||
if err != nil {
|
||||
return ChartPack{}, err
|
||||
}
|
||||
compProg := CompositeChart(progA, progB)
|
||||
davProg, err := ProgressChart(dav, asOf)
|
||||
if err != nil {
|
||||
return ChartPack{}, err
|
||||
}
|
||||
marksProg := MarksChart(davProg, progA)
|
||||
|
||||
return ChartPack{
|
||||
Composite: comp, Davison: dav,
|
||||
MarksMe: marksMe, MarksOther: marksOther,
|
||||
Overlay: overlay,
|
||||
CompositeProgressed: compProg,
|
||||
DavisonProgressed: davProg,
|
||||
MarksProgressed: marksProg,
|
||||
AsOf: asOf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompositeChart midpoints each body longitude (shortest arc); ASC mid of rises.
|
||||
func CompositeChart(a, b natal.Chart) natal.Chart {
|
||||
lons := map[string]float64{}
|
||||
for _, k := range ephemeris.PlanetOrder {
|
||||
lons[k] = natal.MidpointLon(natal.BodyLon(a, k), natal.BodyLon(b, k))
|
||||
}
|
||||
asc := natal.MidpointLon(a.Rise.Lon, b.Rise.Lon)
|
||||
lat := (a.Lat + b.Lat) / 2
|
||||
lng := midLng(a.Lng, b.Lng)
|
||||
ch := natal.ChartFromLons(lons, asc, lat, lng, "组合盘", a.HasTime && b.HasTime, a.HasPlace || b.HasPlace)
|
||||
ch.Instant = midTime(a.Instant, b.Instant)
|
||||
ch.Note = "组合盘:双方行星黄经中点,看关系整体气质。"
|
||||
return ch
|
||||
}
|
||||
|
||||
// DavisonChart uses midpoint birth time + midpoint coordinates, then recomputes.
|
||||
func DavisonChart(a, b natal.Chart) (natal.Chart, error) {
|
||||
inst := midTime(a.Instant, b.Instant)
|
||||
lat := (a.Lat + b.Lat) / 2
|
||||
lng := midLng(a.Lng, b.Lng)
|
||||
ch, err := natal.ComputeAt(inst, lat, lng, "时空盘", true, true)
|
||||
if err != nil {
|
||||
return natal.Chart{}, err
|
||||
}
|
||||
ch.Note = "时空盘:出生时刻与坐标中点再排盘,看长期现实走向。"
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// MarksChart midpoints each Davison body with a natal chart (我/TA 视角).
|
||||
func MarksChart(davison, natalCh natal.Chart) natal.Chart {
|
||||
lons := map[string]float64{}
|
||||
for _, k := range ephemeris.PlanetOrder {
|
||||
lons[k] = natal.MidpointLon(natal.BodyLon(davison, k), natal.BodyLon(natalCh, k))
|
||||
}
|
||||
asc := natal.MidpointLon(davison.Rise.Lon, natalCh.Rise.Lon)
|
||||
ch := natal.ChartFromLons(lons, asc, natalCh.Lat, natalCh.Lng, "马克斯盘", true, true)
|
||||
ch.Instant = midTime(davison.Instant, natalCh.Instant)
|
||||
ch.Note = "马克斯盘:时空盘与本命中点,看关系中的内在态度。"
|
||||
return ch
|
||||
}
|
||||
|
||||
// Overlay maps B planets into A's whole-sign houses.
|
||||
func Overlay(a, b natal.Chart) OverlayChart {
|
||||
ascIdx := natal.SignIndex(a.Rise.Lon)
|
||||
tips := houseTips()
|
||||
entries := make([]OverlayHouse, 0, len(b.Planets))
|
||||
for _, p := range b.Planets {
|
||||
if p.Key == "rise" {
|
||||
continue
|
||||
}
|
||||
h := natal.WholeSignHouse(ascIdx, natal.SignIndex(p.Lon))
|
||||
tip := tips[h]
|
||||
entries = append(entries, OverlayHouse{
|
||||
PlanetKey: p.Key, Planet: p.Title, Sign: p.Sign, House: h, HouseTip: tip,
|
||||
})
|
||||
}
|
||||
return OverlayChart{
|
||||
HousesA: a.Houses,
|
||||
Entries: entries,
|
||||
Tip: "配对盘:对方行星落入我方整宫制宫位,提示生活领域互动。",
|
||||
}
|
||||
}
|
||||
|
||||
// ProgressChart applies secondary progression (1 day ≈ 1 year) from chart.Instant to asOf.
|
||||
func ProgressChart(ch natal.Chart, asOf time.Time) (natal.Chart, error) {
|
||||
if ch.Instant.IsZero() {
|
||||
return ch, nil
|
||||
}
|
||||
years := asOf.UTC().Sub(ch.Instant).Hours() / 24.0 / 365.24219
|
||||
if years < 0 {
|
||||
years = 0
|
||||
}
|
||||
// Add whole days to avoid time.Duration overflow/precision issues for large ages.
|
||||
days := int(years + 0.5)
|
||||
progUTC := ch.Instant.AddDate(0, 0, days)
|
||||
out, err := natal.ComputeAt(progUTC, ch.Lat, ch.Lng, ch.PlaceLabel+"·次限", ch.HasTime, ch.HasPlace)
|
||||
if err != nil {
|
||||
return natal.Chart{}, err
|
||||
}
|
||||
out.Note = fmt.Sprintf("次限推运至 %s(约 %.1f 年)。", asOf.Format("2006-01-02"), years)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func midTime(a, b time.Time) time.Time {
|
||||
if a.IsZero() && b.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
if a.IsZero() {
|
||||
return b
|
||||
}
|
||||
if b.IsZero() {
|
||||
return a
|
||||
}
|
||||
return a.Add(b.Sub(a) / 2)
|
||||
}
|
||||
|
||||
func midLng(a, b float64) float64 {
|
||||
// Geographic mean (CN MVP; both births typically East Asia).
|
||||
return (a + b) / 2
|
||||
}
|
||||
|
||||
func houseTips() map[int]string {
|
||||
return map[int]string{
|
||||
1: "自我与第一印象", 2: "资源与安全感", 3: "沟通与日常", 4: "家庭与根基",
|
||||
5: "恋爱表达与创造", 6: "协作与健康节奏", 7: "一对一关系", 8: "深度与共享",
|
||||
9: "视野与信念", 10: "事业与对外形象", 11: "社群与愿景", 12: "内在与疗愈",
|
||||
}
|
||||
}
|
||||
|
||||
func chartSummaryOut(c natal.Chart, tip string) map[string]any {
|
||||
asp := natal.Aspects(c)
|
||||
aspMaps := natal.AspectsAsMaps(asp)
|
||||
previewN := 4
|
||||
if len(aspMaps) < previewN {
|
||||
previewN = len(aspMaps)
|
||||
}
|
||||
t := tip
|
||||
if t == "" {
|
||||
t = c.Note
|
||||
}
|
||||
return map[string]any{
|
||||
"asc_lon": c.Rise.Lon,
|
||||
"planets": bodiesOut(c),
|
||||
"houses": housesOut(c),
|
||||
"note": c.Note,
|
||||
"place": c.PlaceLabel,
|
||||
"tip": t,
|
||||
"aspects_preview": aspMaps[:previewN],
|
||||
"sun": c.Sun.Sign,
|
||||
"moon": c.Moon.Sign,
|
||||
"rise": c.Rise.Sign,
|
||||
}
|
||||
}
|
||||
|
||||
func overlayOut(o OverlayChart) map[string]any {
|
||||
entries := make([]map[string]any, 0, len(o.Entries))
|
||||
for _, e := range o.Entries {
|
||||
entries = append(entries, map[string]any{
|
||||
"planet_key": e.PlanetKey, "planet": e.Planet, "sign": e.Sign,
|
||||
"house": e.House, "house_tip": e.HouseTip,
|
||||
})
|
||||
}
|
||||
return map[string]any{
|
||||
"tip": o.Tip, "entries": entries, "houses_a": housesOut(natal.Chart{Houses: o.HousesA}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user