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,337 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user