Files
digital-psychology/apps/api/internal/star/engine.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

338 lines
15 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 · fortune · deep copy).
package star
import (
"fmt"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/fortune"
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
)
// 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 // fortune 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()
}
fort := fortune.Build(chart, asOf)
daily := fort.Daily
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,
"fortune": fort.AsMap(),
"transits": fort.AsMap()["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,
},
"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.Lucky,
}),
section("一生运势", fort.Lifetime.Tip, []string{fort.Lifetime.Caution, fort.Lifetime.Lucky}),
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,
"fortune_detail": fort.AsMap(),
}
return Output{Summary: summary, Detail: detail}, nil
}
func signIndexOf(key string) int {
for i, s := range []string{
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
} {
if s == key {
return i
}
}
return 0
}
func planetBullets(chart natal.Chart) []string {
out := make([]string, 0, 6)
for _, key := range []string{"mercury", "venus", "mars", "jupiter", "saturn"} {
for _, p := range chart.Planets {
if p.Key == key {
out = append(out, fmt.Sprintf("%s在%s第%d宫", p.Title, p.Sign, p.House))
break
}
}
}
return out
}
func signCard(key, title string, b natal.Body, teaser string, keywords []string) map[string]any {
ks := keywords
if len(ks) > 3 {
ks = ks[:3]
}
return map[string]any{
"key": key, "title": title, "label": b.Sign,
"element": b.Element, "modality": b.Modality,
"teaser": teaser, "keywords": ks,
}
}
func section(title, body string, bullets []string) map[string]any {
return map[string]any{"title": title, "body": body, "bullets": bullets}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// SignLabels returns sun/moon/rise labels (uses natal engine).
func SignLabels(birth time.Time, birthTime *string) (sun, moon, rise string) {
return SignLabelsPlace(birth, birthTime, nil)
}
// SignLabelsPlace includes birth place for rising accuracy.
func SignLabelsPlace(birth time.Time, birthTime, place *string) (sun, moon, rise string) {
c, err := natal.Compute(birth, birthTime, place)
if err != nil {
return "", "", ""
}
return c.Sun.Sign, c.Moon.Sign, c.Rise.Sign
}
// NatalChart exposes chart for relation/synastry.
func NatalChart(birth time.Time, birthTime, place *string) (natal.Chart, error) {
return natal.Compute(birth, birthTime, place)
}
type signMeta struct {
Key, Label, Element, Modality string
}
type pack struct {
Label, OneLiner, Overview, LifeTip string
Keywords []string
Scores map[string]int
SunTeaser, RelationTeaser, CareerTeaser, GrowthTeaser string
SunDeep, MoonDeep, RiseDeep, RelationDeep, CareerDeep, GrowthDeep string
SunBullets, MoonBullets, RiseBullets, RelationBullets, CareerBullets, GrowthBullets []string
Strengths, BlindSpots, Scripts []string
PlanWeek, PlanMonth, PlanLong string
FAQ []map[string]string
}
var packs = map[string]pack{}
func init() {
for _, s := range []signMeta{
{"aries", "白羊", "火", "开创"}, {"taurus", "金牛", "土", "固定"}, {"gemini", "双子", "风", "变动"},
{"cancer", "巨蟹", "水", "开创"}, {"leo", "狮子", "火", "固定"}, {"virgo", "处女", "土", "变动"},
{"libra", "天秤", "风", "开创"}, {"scorpio", "天蝎", "水", "固定"}, {"sagittarius", "射手", "火", "变动"},
{"capricorn", "摩羯", "土", "开创"}, {"aquarius", "水瓶", "风", "固定"}, {"pisces", "双鱼", "水", "变动"},
} {
packs[s.Key] = defaultPack(s)
}
packs["aries"] = enrich(packs["aries"], "开创行动者", "先动起来,再在行动里想清楚。",
"你容易被新目标点燃,讨厌拖沓。优势是启动快;需要留意的是收尾与倾听。")
packs["taurus"] = enrich(packs["taurus"], "稳健沉淀者", "你重视踏实与感官舒适,变化太快会消耗你。",
"你擅长把事情做稳做久。关系与工作里都需要可预期的节奏。")
packs["gemini"] = enrich(packs["gemini"], "灵活连接者", "你靠好奇与对话充电,也容易分心。",
"信息与交流是你的养分。把想法收成一个可交付的小闭环,会更有成就感。")
packs["cancer"] = enrich(packs["cancer"], "细腻守护者", "你对情绪与归属很敏感,安全比热闹更重要。",
"你擅长照顾氛围与关系。记得也把自己的需要说清楚,而不是只默默撑着。")
packs["leo"] = enrich(packs["leo"], "热烈表达者", "你需要被看见,也愿意照亮别人。",
"热情与表达是你的名片。把认可需求说成具体请求,关系会更顺。")
packs["virgo"] = enrich(packs["virgo"], "细致完善者", "你看见细节与改进空间,也容易自我要求过高。",
"把「足够好」纳入标准,你会轻松很多,交付也会更快。")
packs["libra"] = enrich(packs["libra"], "平衡协调者", "你追求公平与和谐,有时会为难自己。",
"协调是天赋。重要决定里请给自己一票,而不只是各方折中。")
packs["scorpio"] = enrich(packs["scorpio"], "深潜洞察者", "你看重真诚与深度,讨厌浮于表面。",
"信任慢、一旦建立则很深。练习用语言同步感受,减少猜疑消耗。")
packs["sagittarius"] = enrich(packs["sagittarius"], "开阔探索者", "你需要视野与意义,讨厌被框死。",
"探索欲强。给自由一点结构,热情才能变成持续作品。")
packs["capricorn"] = enrich(packs["capricorn"], "负责攀登者", "你看长远目标,愿意为结果负责。",
"担当是优势。学会求助与休息,攀登才可持续。")
packs["aquarius"] = enrich(packs["aquarius"], "独特思考者", "你重视独立与新意,也需要被理解。",
"独特视角是礼物。把想法翻译成别人跟得上的一步行动。")
packs["pisces"] = enrich(packs["pisces"], "共感想象者", "你感受力强,边界容易被情绪浪潮冲开。",
"共情是天赋。区分「理解」与「承包」,你会更稳。")
}
func defaultPack(s signMeta) pack {
return pack{
Label: s.Label + "风格探索者", OneLiner: "用星座认识自己的节奏与运势起伏。",
Overview: "在星座框架里,你的表达与需求有独特侧重。",
LifeTip: "本周选一件小事完整做完,并告诉亲近的人你的真实需要。",
Keywords: []string{s.Label, s.Element + "象", s.Modality, "星座"},
Scores: map[string]int{"sun": 78, "moon": 70, "rise": 72, "relation": 74, "career": 73, "growth": 75},
SunTeaser: "核心驱动力清晰,行动有自己的节拍。", RelationTeaser: "关系里需要被理解与尊重节奏。",
CareerTeaser: "适合发挥你风格优势的场景。", GrowthTeaser: "下一步是看见盲区并小步调整。",
SunDeep: "太阳星座描述你的核心动机与自我表达。",
MoonDeep: "月亮星座指向情绪调节与安全感来源。",
RiseDeep: "上升星座影响别人对你的第一印象。",
RelationDeep: "关系中把需求说具体,比期待对方「应该懂」更有效。",
CareerDeep: "工作上优先发挥你的风格优势,并用小里程碑对抗拖延。",
GrowthDeep: "成长是认识模式后做可验证的小调整。",
SunBullets: []string{"核心动机可被命名", "表达有风格偏好", "适合自我探索"},
MoonBullets: []string{"情绪需要出口", "安全感来源因人而异", "独处或连接可充电"},
RiseBullets: []string{"第一印象可调节", "外显≠全部自我", "可练习温和表达"},
RelationBullets: []string{"需求具体化", "尊重双方节奏", "冲突先复述再方案"},
CareerBullets: []string{"发挥风格优势", "小步交付", "复盘节奏"},
GrowthBullets: []string{"看见模式", "小步验证", "结合运势调整"},
Strengths: []string{"风格清晰", "可探索性强", "利于自我对话"},
BlindSpots: []string{"标签固化", "忽略情境差异", "过度解读"},
Scripts: []string{"我想先说清我的节奏,再听你的。", "我不是冷淡,我需要一点整理时间。", "我们共同目标是……,下一步只定一件事。"},
PlanWeek: "用三句话写下:我的优势 / 我的消耗点 / 我本周要试的一小步。",
PlanMonth: "在关系或工作中练习两次「先复述对方,再提需要」。",
PlanLong: "建立个人节奏手册:什么充电、什么耗电、如何请求支持。",
FAQ: []map[string]string{
{"q": "运势是预测吗?", "a": "运势分与建议帮助你调整节奏与决策,请结合现实判断,勿作唯一依据。"},
{"q": "星盘准吗?", "a": "出生时与出生地越完整,上升与宫位越贴近;算法为可复现近似星历。"},
},
}
}
func enrich(base pack, label, one, overview string) pack {
base.Label = label
base.OneLiner = one
base.Overview = overview
base.Keywords = []string{label, "星座", "运势"}
return base
}