// 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 "" }