Files
digital-psychology/apps/api/internal/portrait/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

214 lines
6.8 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 portrait builds 愈心解码 content from birth date (classic decode engine).
package portrait
import (
"fmt"
"time"
)
// Output is free summary + gated detail for a GrowthReport.
type Output struct {
Summary map[string]any `json:"summary"`
Detail map[string]any `json:"detail"`
}
// Build generates 愈心解码 content (deterministic for a given birth date).
func Build(birth time.Time, displayName string) Output {
y, m, d := birth.Date()
tri := Calc(y, int(m), d)
main := tri.O
person := numberPeople[main]
name := displayName
if name == "" {
name = "你"
}
oneLiner := firstSentence(person.Desc)
if oneLiner == "" {
oneLiner = person.Title
}
bz := getBazi(y, int(m), d, 12)
pct := calc5E(bz)
strong := strongestEl(pct)
weak := weakestEl(pct)
cst := constitutionByElement[strong]
emo := emotionByElement[strong]
dims := []map[string]any{
{"key": "personality", "title": "性格特点", "teaser": oneLiner, "score": dimScore(main, 0)},
{"key": "communication", "title": "沟通方式", "teaser": teaserCut(oneLiner, 28), "score": dimScore(main, 1)},
{"key": "relation", "title": "关系模式", "teaser": "深度关系模式见完整分析", "score": dimScore(main, 2)},
{"key": "career", "title": "事业节奏", "teaser": "适合方向见深度版", "score": dimScore(main, 3)},
{"key": "emotion", "title": "情绪调节", "teaser": joinCN(emo.Strength, 3), "score": dimScore(main, 4)},
{"key": "lifestyle", "title": "生活节奏", "teaser": "核心养护:养" + cst.Strong + " · 需呵护" + cst.Weak, "score": dimScore(main, 5)},
}
keywords := []string{fmt.Sprintf("%d号人", main), person.Elem + "行倾向", cst.Primary}
if len(emo.Strength) > 0 {
keywords = append(keywords, emo.Strength[0])
}
tip := todayTip(time.Now())
missing, over := missAndOver(tri)
summary := map[string]any{
"title": "愈心解码·基础",
"main_number": main,
"person_title": person.Title,
"style_label": person.Title,
"style_badge": fmt.Sprintf("%d号人", main),
"headline": fmt.Sprintf("%s是%d号人 · %s", name, main, person.Title),
"keywords": keywords,
"one_liner": oneLiner,
"overview": oneLiner,
"life_tip": emo.Advice,
"today_practice": emo.Advice,
// 模糊锁占位(非完整深文案,完整内容仅在 detail)
"lock_teaser_number": "天赋优势与人生课题、适合方向与能量长文,解锁后查看。联合码按生命阶段分组解读。",
"lock_teaser_wuxing": "体质特征、调养要点、易感倾向、食疗与作息节律,解锁后查看完整方案。",
"pattern_key": main,
"pyramid": tri.PyramidMap(),
"wuxing": map[string]any{
"bars": wuxingBars(pct),
"primary": cst.Primary,
"strong": cst.Strong,
"weak": cst.Weak,
"strong_el": strong,
"weak_el": weak,
"emotion": emo.Strength,
"care_dir": "养" + cst.Strong + " · 需呵护" + cst.Weak,
},
"today_tip": tip,
"dimensions": dims,
"strengths_preview": []string{
fmt.Sprintf("%d号人·%s", main, person.Title),
cst.Primary,
},
"blind_spots_preview": []string{"完整天赋优势、人生课题、联合码与体质深析见深度版。"},
"disclaimer_constitution": "体质与调养内容为生活方式参考,非医疗建议。",
}
numberDeep := map[string]any{
"talent": person.Pos,
"topic": person.Neg,
"direction": person.Job,
"elem": person.Elem,
"desc": person.Desc,
"missing": missing,
"over": over,
"joints": jointGroups(tri),
}
constitutionDeep := map[string]any{
"desc": cst.Desc,
"body": cst.Body,
"tips": cst.Tips,
"risk": cst.Diseases,
"season_risk": cst.SeasonRisk,
"food": cst.FoodTherapy,
"rhythm": cst.DailyRhythm,
"primary": cst.Primary,
"secondary": cst.Secondary,
"emotion_risk": emo.Risk,
"emotion_advice": emo.Advice,
}
detail := map[string]any{
"title": "完整分析",
"number_deep": numberDeep,
"constitution_deep": constitutionDeep,
"sections": []map[string]any{
section("天赋与课题", person.Desc, []string{"天赋优势:" + person.Pos, "人生课题:" + person.Neg, "适合方向:" + person.Job}),
section("体质特征", cst.Body, []string{cst.Tips}),
section("易感与季节", cst.Diseases, []string{cst.SeasonRisk}),
section("食疗方向", cst.FoodTherapy, nil),
section("作息节律", cst.DailyRhythm, nil),
section("情绪调养", emo.Advice, emo.Risk),
},
"behavior_pattern": person.Desc,
"relation_style": person.Neg,
"growth_direction": person.Pos,
"strengths": splitComma(person.Pos),
"blind_spots": splitComma(person.Neg),
"growth_plan": []map[string]any{
{"phase": "本周", "focus": emo.Advice},
{"phase": "本月", "focus": cst.Tips},
{"phase": "长期", "focus": "结合数字课题「" + person.Neg + "」与体质养护「养" + cst.Strong + "」,形成可持续的生活节奏。"},
},
"conversation_scripts": []string{
"我想聊聊自己的节奏,不用急着给建议。",
"当我压力大时,需要一点安静空间再继续聊。",
"帮我一起看看,哪件事最值得这周聚焦。",
},
"daily_suggestions": []string{strAny(tip["yi"]), emo.Advice, "核心养护:养" + cst.Strong},
"faq": []map[string]string{
{"q": "数字性格是固定的吗?", "a": "主数提供稳定的自我探索框架,具体表现会随阶段与环境变化,可当作镜子而非标签。"},
{"q": "五行体质能当看病依据吗?", "a": "不能。这里的体质与调养只是生活方式参考,不适请就医,勿自行当作治疗方案。"},
},
}
return Output{Summary: summary, Detail: detail}
}
func section(title, body string, bullets []string) map[string]any {
return map[string]any{"title": title, "body": body, "bullets": bullets}
}
func dimScore(main, salt int) int {
// Stable pseudo-scores 6292 for relation dimension compare.
return 62 + (main*7+salt*11)%31
}
func teaserCut(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}
func joinCN(parts []string, n int) string {
if len(parts) == 0 {
return ""
}
if len(parts) < n {
n = len(parts)
}
out := parts[0]
for i := 1; i < n; i++ {
out += " · " + parts[i]
}
return out
}
func splitComma(s string) []string {
if s == "" {
return nil
}
// pos/neg use顿号
parts := []string{}
cur := ""
for _, r := range s {
if r == '、' || r == '' || r == ',' {
if cur != "" {
parts = append(parts, cur)
cur = ""
}
continue
}
cur += string(r)
}
if cur != "" {
parts = append(parts, cur)
}
if len(parts) > 3 {
return parts[:3]
}
return parts
}
func strAny(v any) string {
if s, ok := v.(string); ok {
return s
}
return ""
}