Files
digital-psychology/apps/api/internal/relation/engine.go
T
jackyu66gitandCursor 19d3cd5945 refactor(ECR-001): 接入 ESS 并完成结构对齐 Phase A–E
绑定 ESS 双轨治理,拆分超大 H5 页与 Go 引擎,抽出 membership 服务,
并将 star/fortune 重命名为 outlook(JSON 双写兼容);同时修复 /psy API 代理与首页 + 菜单层级。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 17:51:40 +08:00

148 lines
5.9 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 relation builds 人格匹配 / 合盘向 content from two profiles.
package relation
import (
"fmt"
"math"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
"github.com/yuxingu/digital-psychology/apps/api/internal/star"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/synastry"
)
// Output is free summary + gated detail.
type Output struct {
Summary map[string]any
Detail map[string]any
}
// Build compares two profiles' exploration styles.
func Build(aBirth, bBirth time.Time, aName, bName string) Output {
return BuildWithType(aBirth, bBirth, aName, bName, "")
}
// BuildWithType adds relation-type flavored tips (partner/family/friend).
func BuildWithType(aBirth, bBirth time.Time, aName, bName, relationType string) Output {
return BuildFull(aBirth, bBirth, nil, nil, nil, nil, aName, bName, relationType)
}
// BuildFull includes birth time/place for natal synastry indices.
func BuildFull(aBirth, bBirth time.Time, aTime, bTime, aPlace, bPlace *string, aName, bName, relationType string) Output {
if aName == "" {
aName = "我"
}
if bName == "" {
bName = "TA"
}
pa := portrait.Build(aBirth, aName)
pb := portrait.Build(bBirth, bName)
aLabel := firstNonEmpty(str(pa.Summary["style_label"]), str(pa.Summary["headline"]))
bLabel := firstNonEmpty(str(pb.Summary["style_label"]), str(pb.Summary["headline"]))
aKeys := strSlice(pa.Summary["keywords"])
bKeys := strSlice(pb.Summary["keywords"])
aDims := dimMap(pa.Summary["dimensions"])
bDims := dimMap(pb.Summary["dimensions"])
compKey := complementarityKey(aLabel, bLabel)
comp := complementarityCopy(compKey, aName, bName, aLabel, bLabel)
dimCompare := []map[string]any{}
for _, key := range []string{"personality", "communication", "relation", "career", "emotion", "lifestyle"} {
title := dimTitle(key)
as, aok := aDims[key]
bs, bok := bDims[key]
if !aok || !bok {
continue
}
gap := as.Score - bs.Score
abs := int(math.Abs(float64(gap)))
note := dimNote(key, gap, aName, bName)
dimCompare = append(dimCompare, map[string]any{
"key": key, "title": title,
"me_score": as.Score, "other_score": bs.Score,
"me_teaser": as.Teaser, "other_teaser": bs.Teaser,
"gap": abs, "note": note,
})
}
aSun, aMoon, aRise := star.SignLabelsPlace(aBirth, aTime, aPlace)
bSun, bMoon, bRise := star.SignLabelsPlace(bBirth, bTime, bPlace)
ca, errA := star.NatalChart(aBirth, aTime, aPlace)
cb, errB := star.NatalChart(bBirth, bTime, bPlace)
if errA != nil || errB != nil {
ca, cb = natal.Chart{}, natal.Chart{}
}
match := synastry.Compute(ca, cb, aName, bName)
harmony := harmonyIndex(dimCompare)
fitLabel, fitTips := fitFromHarmony(harmony, aLabel, bLabel)
summary := map[string]any{
"title": "人格匹配·基础",
"me_style": aLabel,
"other_style": bLabel,
"me_keywords": aKeys,
"other_keywords": bKeys,
"diff_one_liner": comp.OneLiner,
"overview": comp.Overview,
"chemistry": comp.Chemistry,
"watchouts_preview": comp.Watchouts[:min(2, len(comp.Watchouts))],
"dimension_compare": dimCompare,
"fit_label": fitLabel,
"fit_tips": fitTips,
"harmony_index": harmony,
"love_index": match.Love,
"friend_index": match.Friend,
"marriage_index": match.Marriage,
"match_indices": match.AsMap(),
"star_compare": []map[string]any{
{"key": "sun", "title": "太阳", "me": aSun, "other": bSun, "note": starPairNote(aSun, bSun)},
{"key": "moon", "title": "月亮", "me": aMoon, "other": bMoon, "note": starPairNote(aMoon, bMoon)},
{"key": "rise", "title": "上升", "me": aRise, "other": bRise, "note": starPairNote(aRise, bRise)},
},
"share_hint": "了解彼此的沟通方式,查看人格匹配",
}
detail := map[string]any{
"title": "人格匹配·完整分析",
"sections": []map[string]any{
{"title": "匹配总览", "body": comp.DeepOverview, "bullets": comp.ChemistryPoints},
{"title": "星座合盘与匹配指数", "body": fmt.Sprintf("%s。%s太阳%s / %s太阳%s;月亮%s与%s。", match.Summary, aName, aSun, bName, bSun, aMoon, bMoon),
"bullets": []string{match.LoveNote, match.FriendNote, match.MarriageNote, starPairNote(aSun, bSun)}},
{"title": "沟通协作", "body": comp.CommDeep, "bullets": comp.CommTips},
{"title": "冲突修复", "body": comp.ConflictDeep, "bullets": comp.ConflictTips},
{"title": "亲密与边界", "body": comp.IntimacyDeep, "bullets": comp.IntimacyTips},
{"title": "共同成长", "body": comp.GrowthDeep, "bullets": comp.GrowthTips},
},
// legacy flat lists (tests / older clients)
"communication": append([]string{
fmt.Sprintf("%s%s):%s", aName, aLabel, firstTeaser(aDims, "communication")),
fmt.Sprintf("%s%s):%s", bName, bLabel, firstTeaser(bDims, "communication")),
}, comp.CommTips...),
"interaction": comp.IntimacyTips,
"maintenance": comp.GrowthTips,
"me_behavior": str(pa.Detail["behavior_pattern"]),
"other_behavior": str(pb.Detail["behavior_pattern"]),
"strengths_together": comp.ChemistryPoints,
"friction_points": comp.Watchouts,
"weekly_practice": comp.Weekly,
"conversation_scripts": comp.Scripts,
"faq": []map[string]string{
{"q": "差异大是不是不合适?", "a": "差异本身不是问题,关键是双方是否愿意把差异当成说明书,而不是评分表。"},
{"q": "总吵怎么办?", "a": "先停火复述,再谈方案;把「谁对谁错」换成「我们各自需要什么」。"},
},
}
relLabel, relBody, relBullets := relationTypePack(relationType, aName, bName)
if relLabel != "" {
summary["relation_type"] = relationType
summary["relation_type_label"] = relLabel
secs, _ := detail["sections"].([]map[string]any)
detail["sections"] = append([]map[string]any{
{"title": "关系类型:" + relLabel, "body": relBody, "bullets": relBullets},
}, secs...)
}
return Output{Summary: summary, Detail: detail}
}