落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
6.5 KiB
Go
204 lines
6.5 KiB
Go
// 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)
|
||
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": WuxingBlock(birth),
|
||
"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 62–92 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 ""
|
||
}
|