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:
jackyu66git
2026-08-03 11:37:53 +08:00
co-authored by Cursor
parent 15a9db374a
commit bd22d9dddd
248 changed files with 26309 additions and 842 deletions
@@ -0,0 +1,123 @@
package portrait
type ConstitutionDetail struct {
Primary string
Secondary string
Strong string
Weak string
Desc string
Body string
Tips string
Diseases string
SeasonRisk string
FoodTherapy string
DailyRhythm string
}
var constitutionByElement = map[string]ConstitutionDetail{
"木": {
Primary: "气郁质",
Secondary: "血瘀质",
Strong: "肝",
Weak: "脾",
Desc: "你的身体像一棵需要舒展的大树。肝气就像树的枝叶,需要自由伸展才能舒畅。你积极进取,但也容易因压力而紧绷——胸闷、偏头痛、情绪波动都是身体在说\"我需要透透气\"。春天是你最敏感的季节,别把情绪关在心里。",
Body: "体型偏瘦或适中,面色偏青黄。指甲容易脆裂,眼睛容易干涩——这都是肝血在提醒你。春季容易过敏,舌边偏红。",
Tips: "疏肝理气是头等大事。多运动、多表达、别把委屈憋在心里。玫瑰花茶是你的好朋友,明亮通风的环境就是你的充电站。",
Diseases: "偏头痛、高血压、甲状腺结节、乳腺增生、月经不调、胁间神经痛。长期压抑情绪会\"化火伤阴\"。",
SeasonRisk: "春天最敏感——过敏、血压波动。秋天金克木,容易情绪低落。",
FoodTherapy: "绿色蔬菜是主角(菠菜、芹菜、西兰花)。少来点酸的(山楂、柠檬),别过量。玫瑰花茶、薄荷茶疏肝解郁。荞麦、小米养肝血。",
DailyRhythm: "早上醒来拉伸5分钟,像猫一样舒展。上午11点前搞定重要决策(肝气最旺)。晚上11点前入睡养肝血。",
},
"火": {
Primary: "湿热质",
Secondary: "阴虚质",
Strong: "心",
Weak: "肺",
Desc: "你的内心有一团温暖的火。热情、感染力强是你的超能力,但这团火也容易烧得太旺——心浮气躁、失眠多梦、口腔溃疡都是信号。夏天是你最需要注意的季节,学会给内心降温。",
Body: "面色偏红,手心脚心热乎乎的,容易出汗。皮肤容易出油长痘,舌尖偏红。夏天尤其难熬,总想喝冰的。",
Tips: "清心降火、静养心神。午休(11-13点)是黄金养心时间。莲子心茶、菊花茶是你的清凉剂。少熬夜,少参与激烈争吵——伤的是自己。",
Diseases: "失眠、口腔溃疡反复、心悸焦虑、高血压、皮肤痘痘。严重时心火下移,可能引起尿路问题。",
SeasonRisk: "夏天最敏感——暑热+心火,容易心烦中暑。冬天水克火,注意防寒护心。",
FoodTherapy: "苦味清心(苦瓜、莲子心)。红色食物养心(红枣、枸杞、番茄)。绿豆汤、百合莲子羹清心安神。西瓜、黄瓜解暑生津。",
DailyRhythm: "午时(11-13点)小憩15-30分钟,像给手机充电。傍晚微微出汗的运动最好。睡前温水泡脚,把火气引下去。",
},
"土": {
Primary: "痰湿质",
Secondary: "气虚质",
Strong: "脾",
Weak: "肾",
Desc: "你像大地一样稳重可靠,是所有人都想依靠的人。但思虑太多会伤脾——饭后犯困、身体沉重、大便不成形,都是脾胃在喊累。长夏湿热季节是你最需要照顾自己的时候。脾胃好,一切都好。",
Body: "体型偏丰满,肌肉松软,腹部容易囤积。面色偏黄,舌头胖大。梅雨季会特别不舒服,头重脚轻。",
Tips: "健脾祛湿是核心功课。吃饭七分饱,细嚼慢咽——这不是规矩,是爱自己。山药薏米粥、陈皮茶都是好帮手。别老坐着,动起来帮脾胃运转。",
Diseases: "肥胖、高血脂、脂肪肝、慢性胃炎、消化不良、眩晕、湿疹。",
SeasonRisk: "长夏(7-8月)最难熬——湿热交蒸,浑身沉重。春天肝气犯脾,容易胃痛腹泻。",
FoodTherapy: "甘淡的食物最养脾(山药、茯苓、薏米)。黄色食物是脾胃的好朋友(小米、南瓜、玉米)。陈皮理气,生姜暖中。冰的、甜的、腻的——少碰。",
DailyRhythm: "早餐7-9点必须吃好吃饱(胃经当令)。饭后散步10分钟助消化。晚餐早一点、少一点。",
},
"金": {
Primary: "阴虚质",
Secondary: "气郁质",
Strong: "肺",
Weak: "肝",
Desc: "你像一把精密的乐器,条理分明、追求完美。但肺为\"娇脏\",怕燥怕干——皮肤干燥、秋冬咳嗽、喉咙不适都是它在提醒你。秋天是你最要呵护自己的季节,给自己多一点滋润。",
Body: "偏瘦,皮肤容易干燥起皮,嘴唇干裂。手心脚心热,晚上容易盗汗。秋天特别敏感,容易便秘。",
Tips: "滋阴润肺是关键。白色食物是你的好朋友(银耳、百合、雪梨)。辛辣烧烤油炸——能躲就躲。早睡比什么都养阴,腹式呼吸能帮你增强肺力。",
Diseases: "慢性咽炎、干咳、支气管炎、皮肤干燥、便秘、过敏性鼻炎、荨麻疹。",
SeasonRisk: "秋天最敏感——燥邪伤肺,干咳咽痒。夏天火克金,心肺都要护。",
FoodTherapy: "白色食物润肺(银耳、百合、山药、雪梨、莲藕)。蜂蜜、麦冬、沙参滋阴润燥。菊花、薄荷清肺。远离辛辣、烧烤、烟酒。",
DailyRhythm: "清晨(5-7点大肠经)排清宿便。上午深呼吸5分钟。午后吃个梨或喝银耳羹。晚上9点前入睡养肺阴。保持房间湿度50%-60%。",
},
"水": {
Primary: "阳虚质",
Secondary: "痰湿质",
Strong: "肾",
Weak: "心",
Desc: "你像深潭里的水,沉静而有深度。智慧是你的底色,但阳气不足会让你畏寒怕冷、腰膝酸软、缺乏安全感。肾是先天之本,冬天是你最需要温暖的季节——对自己好一点,从保暖开始。",
Body: "体型偏丰满或有浮肿感,面色偏白或晦暗。手脚冰凉,晚上起夜多,头发容易掉。腰膝冷痛是常见信号。",
Tips: "温阳补肾是首要任务。多晒太阳,特别是晒后背——那是你的\"阳气充电板\"。艾灸关元、命门、腰要暖、脚要热。黑豆、核桃、肉桂都是你的好朋友。",
Diseases: "慢性肾炎、甲减、骨质疏松、腰椎问题、耳鸣耳聋、不孕不育。寒湿导致关节痛。",
SeasonRisk: "冬天最难熬——寒气伤阳,浑身发冷。长夏湿气困肾,水肿加重。",
FoodTherapy: "黑色食物补肾(黑豆、黑芝麻、黑米、桑葚)。核桃、山药、枸杞温补肾阳。海带、紫菜适量。冰镇饮料、生冷瓜果——戒了。",
DailyRhythm: "早晨7-9点晒太阳15分钟补阳气。上午搓腰眼至发热。下午5-7点(肾经当令)休息养肾。晚上热水泡脚至微微出汗。冬天早睡晚起。",
},
}
type EmotionProfile struct {
Strength []string
Risk []string
Advice string
Color string
}
var emotionByElement = map[string]EmotionProfile{
"木": {
Strength: []string{"创造力", "决策力", "学习能力", "进取心", "目标感"},
Risk: []string{"焦虑", "易怒", "急躁", "偏头痛倾向", "经前紧张(女)"},
Advice: "遇事深呼吸三秒再回应。每日快走30分钟疏解肝气。学习正念冥想。",
Color: "绿",
},
"火": {
Strength: []string{"热情", "感染力", "表达力", "社交能力", "行动力"},
Risk: []string{"失眠", "焦躁", "心神不宁", "多梦", "舌尖溃疡"},
Advice: "午时(11-13点)小憩养心。少刷手机,睡前温水泡脚。练习书法静心。",
Color: "红",
},
"土": {
Strength: []string{"稳重", "包容", "耐心", "执行力", "可信赖"},
Risk: []string{"思虑过度", "脾胃不适", "优柔寡断", "过度操心"},
Advice: "饭后散步10分钟助运化。学会说\"不\",减轻负担。培养一个动手爱好。",
Color: "黄",
},
"金": {
Strength: []string{"条理", "决断", "专注", "毅力", "原则性"},
Risk: []string{"悲伤", "忧愁", "呼吸道敏感", "皮肤干燥", "便秘"},
Advice: "练习腹式呼吸强肺。多与朋友交流倾诉。保持环境湿度。",
Color: "白",
},
"水": {
Strength: []string{"智慧", "洞察", "冷静", "适应力", "深度思考"},
Risk: []string{"恐惧", "孤独感", "腰膝酸软", "听力下降", "记忆力减退"},
Advice: "冬季注意腰腹保暖。练习站桩固肾。多听音乐舒缓恐惧。",
Color: "黑",
},
}
+179 -112
View File
@@ -1,5 +1,4 @@
// Package portrait builds deterministic personal-portrait content from birth date.
// Copy follows .ai/product/lexicon.md — exploration / analysis, not fortune-telling.
// Package portrait builds 愈心解码 content from birth date (classic decode engine).
package portrait
import (
@@ -13,134 +12,202 @@ type Output struct {
Detail map[string]any `json:"detail"`
}
// Build generates 个人画像 content from a birth date (deterministic).
// Build generates 愈心解码 content (deterministic for a given birth date).
func Build(birth time.Time, displayName string) Output {
y, m, d := birth.Date()
num := reduce(y) + reduce(int(m)) + reduce(d)
for num > 9 {
num = reduce(num)
}
trait := traits[num%len(traits)]
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": "基础画像",
"headline": fmt.Sprintf("%s更偏「%s」的互动风格", name, trait.Label),
"keywords": trait.Keywords,
"one_liner": trait.OneLiner,
"life_tip": trait.LifeTip,
"pattern_key": num,
"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": "完整分析",
"behavior_pattern": trait.Behavior,
"relation_style": trait.Relation,
"growth_direction": trait.Growth,
"daily_suggestions": []string{
trait.LifeTip,
"遇到分歧时,先复述对方观点再表达自己的需要。",
"用一周记录情绪与精力高峰,找到更适合自己的节奏。",
"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}
}
type trait struct {
Label string
Keywords []string
OneLiner string
LifeTip string
Behavior string
Relation string
Growth string
func section(title, body string, bullets []string) map[string]any {
return map[string]any{"title": title, "body": body, "bullets": bullets}
}
var traits = []trait{
{
Label: "稳健探索", Keywords: []string{"条理", "耐心", "观察"},
OneLiner: "你习惯先理解再行动,适合把复杂事情拆成小步。",
LifeTip: "今天给自己一段不被打断的专注时间。",
Behavior: "决策偏审慎,信息充分时执行力更强;压力下可能拖延。",
Relation: "更愿意用行动表达关心,需要对方给予明确反馈。",
Growth: "练习在信息不完整时做小步试验,积累行动信心。",
},
{
Label: "热情连接", Keywords: []string{"表达", "共鸣", "主动"},
OneLiner: "你容易带动气氛,也需要被真诚回应。",
LifeTip: "把想说的话写下来,再选择合适的时机分享。",
Behavior: "行动快、反馈敏感;情绪起伏会影响专注时长。",
Relation: "重视即时沟通,冷处理容易让你感到不安。",
Growth: "学会区分「被回应」与「被认同」,减少过度解读。",
},
{
Label: "理性澄清", Keywords: []string{"分析", "边界", "清晰"},
OneLiner: "你擅长把模糊感受变成可讨论的问题。",
LifeTip: "睡前做一次简短复盘:今天最有价值的一件事是什么。",
Behavior: "偏好逻辑框架;在情绪场域可能显得抽离。",
Relation: "沟通时需要结构和具体例子,忌空泛安慰。",
Growth: "在分析之外,练习先接纳情绪再谈方案。",
},
{
Label: "柔韧调节", Keywords: []string{"适应", "体察", "平衡"},
OneLiner: "你善于照顾氛围,也别忘了照顾自己的节奏。",
LifeTip: "安排一次轻度活动,帮助身心回到平稳状态。",
Behavior: "弹性强,容易迁就;长期可能积压需求。",
Relation: "更在意关系和谐,冲突时倾向先安抚场面。",
Growth: "练习用「我需要…」表达边界,而不是只做协调者。",
},
{
Label: "目标推进", Keywords: []string{"决断", "效率", "成果"},
OneLiner: "你推动事情落地的能力突出,记得留白给休息。",
LifeTip: "把今日目标收束到一件最重要的事。",
Behavior: "结果导向,节奏偏快;对低效协作耐心有限。",
Relation: "欣赏直接沟通的人,含糊表态会消耗信任。",
Growth: "把「效率」与「关系维护」列为同等优先级的周目标。",
},
{
Label: "内观沉淀", Keywords: []string{"深度", "独立", "洞察"},
OneLiner: "你习惯向内理解世界,适合深度思考类任务。",
LifeTip: "留出安静独处时间,整理近期的想法与感受。",
Behavior: "思考深入,对外表达可能滞后于内心结论。",
Relation: "需要安全感和节奏感,突然施压会让你退回内在。",
Growth: "把洞察翻译成可分享的语言,让重要的人跟上你。",
},
{
Label: "创意发散", Keywords: []string{"想象", "灵感", "可能"},
OneLiner: "你容易看到多种可能性,适合用原型验证想法。",
LifeTip: "用纸笔画出今天最想尝试的一个小实验。",
Behavior: "点子多、切换快;收尾与复盘是短板。",
Relation: "喜欢有趣的对话,重复与僵化会让你疏离。",
Growth: "为每个灵感设定「最小完成定义」,提高闭环率。",
},
{
Label: "责任担当", Keywords: []string{"可靠", "承诺", "稳定"},
OneLiner: "你重视承诺与秩序,是团队里让人安心的存在。",
LifeTip: "检查一下是否把别人的期待误当成自己的必须。",
Behavior: "可靠且自律;过度负责时容易耗竭。",
Relation: "用持续在场表达在乎,需要被看见付出。",
Growth: "练习委托与求助,让支持系统真正运转起来。",
},
{
Label: "敏锐觉察", Keywords: []string{"细腻", "直觉", "体贴"},
OneLiner: "你对情绪与细节敏感,适合需要同理的场景。",
LifeTip: "觉察身体信号:紧张时先放慢呼吸再回应。",
Behavior: "感知力强;信息过载时容易内耗。",
Relation: "能很快读到对方状态,也易被情绪感染。",
Growth: "建立「感受—事实—选择」三步,减少被情绪牵着走。",
},
func dimScore(main, salt int) int {
// Stable pseudo-scores 6292 for relation dimension compare.
return 62 + (main*7+salt*11)%31
}
func reduce(n int) int {
if n < 0 {
n = -n
func teaserCut(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
sum := 0
for n > 0 {
sum += n % 10
n /= 10
}
return sum
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 ""
}
+72 -7
View File
@@ -1,24 +1,89 @@
package portrait
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestBuildDeterministic(t *testing.T) {
birth := time.Date(1990, 1, 15, 0, 0, 0, 0, time.UTC)
func TestCalcMatchesClassicJS(t *testing.T) {
// Snapshot from js/common.js calc(1990,5,12)
v := Calc(1990, 5, 12)
if v.O != 9 || v.M != 8 || v.N != 1 || v.I != 3 || v.J != 5 || v.K != 1 || v.L != 9 {
t.Fatalf("triangle mismatch: %+v", v)
}
if v.Q != 1 || v.P != 8 || v.R != 9 || v.T != 2 || v.S != 4 || v.U != 6 {
t.Fatalf("outer mismatch: %+v", v)
}
}
func TestBuildDecodeShape(t *testing.T) {
birth := time.Date(1990, 5, 12, 0, 0, 0, 0, time.UTC)
a := Build(birth, "小愈")
b := Build(birth, "小愈")
if a.Summary["headline"] != b.Summary["headline"] {
t.Fatalf("expected deterministic headline")
}
if a.Summary["one_liner"] == nil || a.Detail["growth_direction"] == nil {
t.Fatalf("missing summary/detail fields")
if a.Summary["main_number"].(int) != 9 {
t.Fatalf("main_number want 9 got %#v", a.Summary["main_number"])
}
for _, kw := range []string{"运势", "吉凶", "算命"} {
s := a.Summary["one_liner"].(string) + a.Detail["behavior_pattern"].(string)
if strings.Contains(s, kw) {
if a.Summary["person_title"] == nil || a.Summary["pyramid"] == nil {
t.Fatalf("missing decode free fields")
}
wx, ok := a.Summary["wuxing"].(map[string]any)
if !ok || wx["bars"] == nil || wx["primary"] == nil {
t.Fatalf("missing wuxing summary: %#v", a.Summary["wuxing"])
}
if a.Summary["today_tip"] == nil {
t.Fatalf("missing today_tip")
}
nd, ok := a.Detail["number_deep"].(map[string]any)
if !ok || nd["talent"] == nil || nd["joints"] == nil {
t.Fatalf("missing number_deep")
}
cd, ok := a.Detail["constitution_deep"].(map[string]any)
if !ok || cd["body"] == nil || cd["food"] == nil {
t.Fatalf("missing constitution_deep")
}
dims, ok := a.Summary["dimensions"].([]map[string]any)
if !ok || len(dims) < 6 {
t.Fatalf("expected 6 dimensions for relation compat")
}
assertNoForbidden(t, a)
}
func TestWuxingDeterministic(t *testing.T) {
bz := getBazi(1990, 5, 12, 12)
p := calc5E(bz)
// Snapshot aligned with js calc5E(getBazi(new Date(1990,4,12),12))
want := map[string]int{"木": 25, "火": 50, "土": 13, "金": 13, "水": 0}
for k, w := range want {
if p[k] != w {
t.Fatalf("wuxing[%s]=%d want %d (full %#v)", k, p[k], w, p)
}
}
if strongestEl(p) != "火" {
t.Fatalf("strong want 火 got %s", strongestEl(p))
}
}
func TestAllMainNumbersLexicon(t *testing.T) {
for day := 1; day <= 28; day++ {
out := Build(time.Date(1990, 3, day, 0, 0, 0, 0, time.UTC), "测")
assertNoForbidden(t, out)
}
}
func assertNoForbidden(t *testing.T, out Output) {
t.Helper()
raw, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
blob := string(raw)
for _, kw := range []string{"占卜", "算命", "改命", "塔罗"} {
if strings.Contains(blob, kw) {
t.Fatalf("forbidden word %q in portrait copy", kw)
}
}
+107
View File
@@ -0,0 +1,107 @@
package portrait
// Joint code short blurbs (JOINT from js/common.js).
var jointCodes = map[string]string{
"112": "独当一面,做事靠谱有担当",
"123": "天生讲师,开口就能圈粉",
"134": "灵感落地,把创意变成现实",
"145": "运筹帷幄,天生的规划师",
"156": "八方来财,处处遇贵人",
"167": "白手起家,靠自己闯天下",
"178": "成就他人,身边贵人不断",
"189": "停不下来的工作狂",
"191": "独立开创,没人能阻挡你",
"213": "口才炸裂,感染力爆棚",
"224": "心太软,善良是你的底牌",
"235": "沟通达人,三言两语暖人心",
"246": "贵人环绕,销冠体质",
"257": "直觉导航,跟着感觉走就对了",
"268": "重情重义,一诺千金",
"279": "异性缘超好,走到哪都是焦点",
"281": "大单收割机,谈判桌王者",
"292": "幕后军师,运筹帷幄之中",
"314": "创意执行力,说干就干",
"325": "感性沟通,用温度打动人心",
"336": "资源连接器,人脉即财富",
"347": "深耕专业,做领域里的王",
"358": "行动派领袖,带队冲锋",
"369": "万花筒人生,十八般武艺样样通",
"415": "规划力MAX,步步为营",
"426": "智慧型销售,成交于无形",
"437": "学术大佬,坐冷板凳也能发光",
"448": "靠谱天花板,交给你就放心了",
"459": "策划大师,脑子比电脑快",
"461": "稳扎稳打,厚积薄发",
"472": "火眼金睛,精准分析一切",
"483": "技术大牛,专业领域独孤求败",
"494": "完美规划师,细节控本控",
"516": "方向感+大智慧,走哪都是对的",
"527": "感性直觉,第六感超准",
"538": "天生领袖,气场两米八",
"549": "规划型赢家,稳操胜券",
"551": "风一样自由,方向感自带导航",
"562": "会赚也会花,享受生活",
"573": "贵人相助命,总有高人来指点",
"584": "责任+规划,双核驱动",
"595": "梦想家体质,心有多大舞台就有多大",
"617": "白手起家,命运由我不由天",
"628": "财富+沟通,双商在线",
"639": "多才多艺,斜杠青年本青",
"641": "务实派经营者,脚踏实地",
"652": "财富雷达,赚钱直觉超准",
"663": "完美主义+财富,精品人生",
"674": "天生傲骨,不甘平凡",
"685": "商业+责任,事业标杆",
"696": "极致追求,匠人精神",
"718": "钻研成王,责任铸就传奇",
"729": "桃花运旺,魅力无法挡",
"731": "创新领袖,敢为天下先",
"742": "神机妙算,谋定而后动",
"753": "直觉行动派,干就完了",
"764": "精打细算,每一分钱都花在刀刃上",
"775": "王者基因,注定站在C位",
"786": "商业领袖,运筹帷幄",
"797": "极致深钻,领域天花板",
"819": "亲力亲为,掌控全局",
"821": "大单高手,说服力满格",
"832": "行动+沟通,效率拉满",
"843": "技术专家,一技傍身走天下",
"854": "规划+自由,收放自如",
"865": "财富+责任,扛得住世界",
"876": "商业王者,霸气外露",
"887": "强势担当,说到做到",
"898": "大事业格局,志在千里",
"911": "大爱无疆,独立而温柔",
"922": "温柔至善,人间小天使",
"933": "巧手匠心,万物皆可造",
"944": "稳中求进,计划周全",
"955": "自由博爱,灵魂有趣",
"966": "完美+智慧,人间清醒",
"977": "深度大爱,为理想燃烧",
"988": "商业帝国缔造者",
"999": "极致巅峰,满分人生",
}
var missMsg = map[int]string{
1: "内心的领袖力还在沉睡——试着在小事上果断决策,你会发现自己比想象中更强大",
2: "你的声音值得被听见——从今天开始,勇敢说出你的想法,别怕被拒绝",
3: "行动力是等待点燃的火种——别再想了,先迈出第一步,剩下的路会自动展开",
4: "安全感不是等来的,是建出来的——给自己一个可以依靠的小习惯,它会慢慢长大",
5: "你的指南针正在校准中——去尝试那些让你心跳加速的事,方向自会出现",
6: "财富嗅觉是可以培养的——从关注自己的价值开始,你会发现自己值得更多",
7: "深度的力量需要聚焦——找一个让你好奇的问题,像侦探一样追下去",
8: "你的使命感正在觉醒——找到那件让你热血沸腾的事,扛起来走下去",
9: "大爱需要边界——学会对自己说\"够了\",你的能量会用在对的地方",
}
var missStrong = map[int]string{
1: "自信女神本神,偶尔听一下别人的意见会更完美",
2: "沟通力溢出屏幕,但别忘了先连接自己的内心",
3: "活力四射到飞起,偶尔刹车踩一踩,走得更远",
4: "稳得像座山,但偶尔翻个墙看看外面的世界也不错",
5: "自由到炸裂,找个值得停靠的港湾也很酷",
6: "完美主义拉满,对自己说一句\"已经够好了\"试试",
7: "沉迷思考无法自拔,偶尔出来晒晒太阳吧",
8: "责任扛得太多啦,分一些给信得过的人",
9: "爱心泛滥到溢出,聚焦一件最想改变的事会更有力量",
}
+86
View File
@@ -0,0 +1,86 @@
package portrait
// NumberPerson is classic NUM[1..9] copy from js/common.js.
type NumberPerson struct {
Title string
Elem string
Pos string
Neg string
Job string
Desc string
}
var numberPeople = map[int]NumberPerson{
1: {
Title: "天生领袖·开拓者",
Elem: "金",
Pos: "自信果敢、创造力爆棚、天生自带光芒",
Neg: "偶尔孤傲、不擅示弱、容易一个人扛下所有",
Job: "创始人、管理者、艺术家、独立设计师",
Desc: "你是天生的领航员,内心有一团不灭的火。你不需要别人告诉你该做什么——你比谁都清楚。独立、果敢、行动力超强,这让你在人群中自带光环。但请记住,真正的强大不是独自承担一切,而是学会信任和依靠。偶尔放下铠甲,你会看到更广阔的世界。",
},
2: {
Title: "温柔沟通·共情者",
Elem: "水",
Pos: "善解人意、沟通力满分、团队里的粘合剂",
Neg: "太在意他人眼光、容易委屈自己、选择困难",
Job: "心理咨询师、公关专家、教育者、客户关系",
Desc: "你有一颗比常人更柔软的心。你能听见别人没说出口的话,感受到空气里的情绪——这是你独一无二的天赋。你是团队里最温暖的存在,总能让人感到被理解。但要记得,你的感受同样重要。温柔不是软弱,拒绝不是伤害。学会为自己发声,你的温柔会更有力量。",
},
3: {
Title: "创意火花·行动派",
Elem: "木",
Pos: "灵感不断、感染力满分、想到就去做",
Neg: "三分钟热度、急躁冒进、耐不住寂寞",
Job: "演员、讲师、创意总监、自媒体人",
Desc: "你的大脑永远在高速运转,灵感像烟花一样炸开。你就是那个能把想法变成现实的人——当别人还在犹豫,你已经冲出去了。你的热情能点燃整个房间。但有时候,慢一点反而更快。不是每个火花都需要燃烧,学会聚焦,你的光芒会更耀眼。",
},
4: {
Title: "稳妥规划·筑造者",
Elem: "木",
Pos: "逻辑缜密、靠谱担当、凡事井井有条",
Neg: "过于保守、害怕改变、安全感容易动摇",
Job: "工程师、会计师、项目经理、架构师",
Desc: "你是那个让所有人放心的人。当世界一片混乱时,你已经在默默列清单了。你的稳不是无趣,是把混乱变成秩序的超能力。每一步都走得扎实,每个细节都心中有数。但生活不是待办清单,偶尔打破计划,会有惊喜。允许自己不那么完美,你已经足够好了。",
},
5: {
Title: "自由灵魂·探索者",
Elem: "土",
Pos: "无拘无束、方向感超强、幽默感爆棚",
Neg: "讨厌束缚、容易逃避、很难安定下来",
Job: "旅行博主、作家、自由职业者、冒险家",
Desc: "你就是风,没人能把你关在笼子里。你的灵魂渴望远方,对世界永远保持好奇。你能在迷茫中找到方向,这是你最珍贵的天赋。你的人生不是按部就班,而是充满冒险的故事。但也要记得,自由不是逃避,真正的自由是拥有选择的能力。找到值得你停留的热爱,那里就是你的家。",
},
6: {
Title: "智慧远见·鉴赏家",
Elem: "金",
Pos: "眼光毒辣、财富直觉、精益求精",
Neg: "完美主义、容易焦虑、对自己太狠",
Job: "投资人、咨询顾问、医生、律师",
Desc: "你有一双看透本质的眼睛。别人看到表面,你看到价值;别人追逐潮流,你预判趋势。你对品质的要求不是挑剔,是对生活的尊重。但完美不是终点,生活有瑕疵才真实。偶尔放低标准,你会发现世界比你想象的更宽容。你值得拥有最好的,包括对自己温柔一点。",
},
7: {
Title: "深度洞察·求真者",
Elem: "水",
Pos: "钻研到底、直觉惊人、看透事物本质",
Neg: "疏离冷漠、疑心重、社交电量容易耗尽",
Job: "科学家、侦探、程序员、学者",
Desc: "别人看热闹,你看门道。你的大脑是永动的探索引擎,对世界的真相充满饥渴。你不需要浅薄的社交,你在意的是深度和意义。这份专注让你在专业领域无人能敌。但请偶尔走出自己的世界,那些看似平凡的人间烟火,也能给你答案。独处是力量,连接是温暖,两者你都值得拥有。",
},
8: {
Title: "责任担当·成就者",
Elem: "土",
Pos: "责任感爆棚、商业嗅觉、野心勃勃",
Neg: "压力山大、控制欲强、容易透支自己",
Job: "企业家、高管、投资人、政治家",
Desc: "你注定不是普通人。你肩膀上扛着比常人更重的使命,心中有比常人更大的蓝图。你的责任感和行动力让你天然成为领袖。但你不需要扛起整个世界,真正的领导者懂得分担负重。学会休息不是懈怠,是为了走更远的路。你值得被照顾,不只是照顾别人。",
},
9: {
Title: "博爱大器·成功者",
Elem: "火",
Pos: "慈悲大爱、机会磁铁、自带成功气场",
Neg: "贪多嚼不烂、难以专注、理想主义过头",
Job: "慈善家、艺人、灵性导师、社会活动家",
Desc: "你身上有一种让人想靠近的魔力。你的心中装着比个人更大的东西——爱与使命感是你最强的驱动力。机会总是向你涌来,因为你值得。你天生自带好运体质。但不需要抓住所有机会,挑一个最让你心动的,全力以赴。你的存在,本身就是一种治愈。",
},
}
+237
View File
@@ -0,0 +1,237 @@
package portrait
// Triangle is the classic life-number pyramid from js/common.js calc().
type Triangle struct {
AB, CD, EF, GH [2]int
I, J, K, L int
M, N, O int
Q, P, R int
T, S, U int
V, W, X int
}
// Calc builds the digital psychology triangle for a birth date.
func Calc(y, m, d int) Triangle {
dd := digs(d, 2)
mm := digs(m, 2)
yy := digs(y, 4)
a, b := dd[0], dd[1]
c, dd1 := mm[0], mm[1]
e, f, g, h := yy[0], yy[1], yy[2], yy[3]
v := Triangle{
AB: [2]int{a, b},
CD: [2]int{c, dd1},
EF: [2]int{e, f},
GH: [2]int{g, h},
}
v.I = s1(a + b)
v.J = s1(c + dd1)
v.K = s1(e + f)
v.L = s1(g + h)
v.M = s1(v.I + v.J)
v.N = s1(v.K + v.L)
v.O = s1(v.M + v.N)
v.Q = s1(v.N + v.O)
v.P = s1(v.M + v.O)
v.R = s1(v.Q + v.P)
v.T = s1(v.I + v.M)
v.S = s1(v.J + v.M)
v.U = s1(v.T + v.S)
v.V = s1(v.K + v.N)
v.W = s1(v.L + v.N)
v.X = s1(v.V + v.W)
return v
}
// PyramidMap returns grid values for H5 mini-triangle rendering.
func (v Triangle) PyramidMap() map[string]int {
return map[string]int{
"I": v.I, "J": v.J, "K": v.K, "L": v.L,
"M": v.M, "N": v.N, "O": v.O,
"Q": v.Q, "P": v.P, "R": v.R,
"T": v.T, "S": v.S, "U": v.U,
"V": v.V, "W": v.W, "X": v.X,
}
}
func s1(n int) int {
for n >= 10 {
sum := 0
for n > 0 {
sum += n % 10
n /= 10
}
n = sum
}
return n
}
func digs(n, l int) []int {
s := fmtPad(n, l)
out := make([]int, l)
for i := 0; i < l; i++ {
out[i] = int(s[i] - '0')
}
return out
}
func fmtPad(n, l int) string {
if n < 0 {
n = -n
}
buf := make([]byte, l)
for i := l - 1; i >= 0; i-- {
buf[i] = byte('0' + n%10)
n /= 10
}
return string(buf)
}
func cell(v Triangle, key string) int {
switch key {
case "I":
return v.I
case "J":
return v.J
case "K":
return v.K
case "L":
return v.L
case "M":
return v.M
case "N":
return v.N
case "O":
return v.O
case "Q":
return v.Q
case "P":
return v.P
case "R":
return v.R
case "T":
return v.T
case "S":
return v.S
case "U":
return v.U
case "V":
return v.V
case "W":
return v.W
case "X":
return v.X
default:
return 0
}
}
// jointGroups builds age-grouped joint codes (付费层).
func jointGroups(v Triangle) []map[string]any {
pairs := [][2]string{
{"I", "J"}, {"J", "M"}, {"M", "I"}, {"K", "L"},
{"L", "N"}, {"N", "K"}, {"M", "O"}, {"O", "N"},
{"M", "N"}, {"Q", "P"}, {"Q", "O"}, {"P", "O"},
}
joints := make([]map[string]any, 0, len(pairs))
for _, p := range pairs {
code := fmtInt(cell(v, p[0])) + fmtInt(cell(v, p[1]))
label := "—"
for k, desc := range jointCodes {
if len(k) >= len(code) && k[:len(code)] == code {
label = desc
break
}
}
joints = append(joints, map[string]any{"code": code, "label": label})
}
g1Desc := "由潜意识码主导,反映你在职场上升期和社交圈中的表现模式。"
if v.I+v.J+v.M >= 10 {
g1Desc += "能量较强,事业心旺盛,社交活跃。"
} else {
g1Desc += "能量内敛,更注重深度而非广度。"
}
g2Desc := "由内心码和主性格主导,体现你对下一代和下级的培养方式及中年价值观。"
if v.K+v.L+v.N >= 10 {
g2Desc += "有较强的责任感和支配欲。"
} else {
g2Desc += "倾向于给予空间,放手让他人成长。"
}
g3Desc := "由外心码主导,展现你晚年的生活智慧和家庭关系的最终呈现。"
if v.M+v.N+v.O >= 10 {
g3Desc += "晚年活跃,持续发挥影响力。"
} else {
g3Desc += "晚年宁静,享受内在平和。"
}
return []map[string]any{
{"label": "21—40岁 · 工作和朋友", "codes": joints[0:4], "desc": g1Desc},
{"label": "41—60岁 · 儿女和下属", "codes": joints[4:8], "desc": g2Desc},
{"label": "61岁至晚年 · 晚年和家庭", "codes": joints[8:12], "desc": g3Desc},
}
}
func missAndOver(v Triangle) (missing []map[string]any, over []map[string]any) {
innerSet := map[int]bool{v.O: true, v.M: true, v.N: true, v.I: true, v.J: true, v.K: true, v.L: true}
outerVals := []int{v.Q, v.P, v.R, v.T, v.S, v.U, v.V, v.W, v.X}
present := map[int]bool{}
for n := range innerSet {
present[n] = true
}
for _, n := range outerVals {
present[n] = true
}
for _, n := range []int{v.AB[0], v.AB[1], v.CD[0], v.CD[1], v.EF[0], v.EF[1], v.GH[0], v.GH[1]} {
present[n] = true
}
freq := map[int]int{}
for i := 1; i <= 9; i++ {
freq[i] = 0
}
allVals := []int{
v.O, v.M, v.N, v.I, v.J, v.K, v.L, v.Q, v.P, v.R, v.T, v.S, v.U, v.V, v.W, v.X,
v.AB[0], v.AB[1], v.CD[0], v.CD[1], v.EF[0], v.EF[1], v.GH[0], v.GH[1],
}
for _, x := range allVals {
if x >= 1 && x <= 9 {
freq[x]++
}
}
missBoth := []int{}
for n := 0; n <= 9; n++ {
if !present[n] {
missBoth = append(missBoth, n)
}
}
missSet := map[int]bool{}
for _, n := range missBoth {
missSet[n] = true
if n >= 1 && n <= 9 {
missing = append(missing, map[string]any{"n": n, "msg": missMsg[n]})
}
}
for i := 1; i <= 9; i++ {
if freq[i] >= 4 && !missSet[i] {
over = append(over, map[string]any{"n": i, "msg": missStrong[i], "count": freq[i]})
}
}
return missing, over
}
func fmtInt(n int) string {
if n < 0 {
n = -n
}
if n < 10 {
return string(rune('0' + n))
}
return fmtPad(n, 2)
}
func firstSentence(s string) string {
for i, r := range s {
if r == '。' || r == '.' {
return s[:i]
}
}
return s
}
+138
View File
@@ -0,0 +1,138 @@
package portrait
import (
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/companion"
)
var (
tg = []string{"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"}
dz = []string{"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"}
tge = map[string]string{"甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土", "己": "土", "庚": "金", "辛": "金", "壬": "水", "癸": "水"}
dze = map[string]string{"子": "水", "丑": "土", "寅": "木", "卯": "木", "辰": "土", "巳": "火", "午": "火", "未": "土", "申": "金", "酉": "金", "戌": "土", "亥": "水"}
eo = []string{"木", "火", "土", "金", "水"}
ec = map[string]string{"木": "#5CB85C", "火": "#E54D42", "土": "#E8985A", "金": "#C8923A", "水": "#4A90E2"}
)
var yiJi = map[string]struct{ Yi, Ji string }{
"木": {"早睡养肝血 · 舒展拉伸 · 绿色蔬菜", "久坐 · 生闷气 · 熬夜"},
"火": {"午休15分钟 · 菊花/莲子 · 平心静气", "暴晒 · 辛辣油腻 · 过度兴奋"},
"土": {"规律三餐 · 健脾山药 · 饭后散步", "思虑过度 · 生冷 · 暴饮暴食"},
"金": {"滋润养肺 · 白色食物 · 深呼吸", "干燥受凉 · 悲忧 · 剧烈耗气"},
"水": {"早卧晚起 · 温补黑色食物 · 护腰腹", "受寒 · 过劳 · 惊恐紧张"},
}
// seasonEl maps month index (0=Jan) → 时令五行 (decode.js SEASON_EL).
var seasonEl = []string{"水", "水", "木", "木", "木", "火", "火", "火", "土", "金", "金", "水"}
type pillar struct{ tg, dz string }
type bazi struct {
year, month, day, hour pillar
}
// getBazi mirrors js/common.js getBazi (hour default 12 = 午时).
// Day-pillar index uses civil days since 1900-01-01 minus 1, matching legacy
// browser Date local-midnight flooring under historical CN TZ/DST (decode.js).
func getBazi(y, m, d, h int) bazi {
gzY := pillar{tg: tg[(y-4+10000)%10], dz: dz[(y-4+12000)%12]}
birth := time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC)
dayOfYear := birth.YearDay()
lcDay := 35
mi := int(float64((dayOfYear-lcDay+365)%365) / 30.44)
if mi < 0 {
mi += 12
}
if mi >= 12 {
mi = 11
}
mTg := tg[(((y-4+10000)%10)*2+mi)%10]
mDz := dz[(mi+2)%12]
base := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
dd := int(birth.Sub(base).Hours()/24) - 1
dTg := tg[((dd%10)+10)%10]
dDz := dz[((dd%12)+12)%12]
hDz := dz[((h+1)/2)%12]
hTg := tg[(indexOf(tg, dTg)*2+indexOf(dz, hDz))%10]
return bazi{
year: gzY,
month: pillar{tg: mTg, dz: mDz},
day: pillar{tg: dTg, dz: dDz},
hour: pillar{tg: hTg, dz: hDz},
}
}
func calc5E(bz bazi) map[string]int {
c := map[string]int{"木": 0, "火": 0, "土": 0, "金": 0, "水": 0}
for _, p := range []pillar{bz.year, bz.month, bz.day, bz.hour} {
c[tge[p.tg]]++
c[dze[p.dz]]++
}
t := 0
for _, e := range eo {
t += c[e]
}
r := map[string]int{}
for _, e := range eo {
if t == 0 {
r[e] = 0
} else {
r[e] = int(float64(c[e])/float64(t)*100 + 0.5) // Math.round
}
}
return r
}
func strongestEl(p map[string]int) string {
best := eo[0]
for _, e := range eo[1:] {
if p[e] > p[best] {
best = e
}
}
return best
}
func weakestEl(p map[string]int) string {
worst := eo[0]
for _, e := range eo[1:] {
if p[e] < p[worst] {
worst = e
}
}
return worst
}
func wuxingBars(p map[string]int) []map[string]any {
bars := make([]map[string]any, 0, 5)
for _, e := range eo {
bars = append(bars, map[string]any{
"el": e, "pct": p[e], "color": ec[e],
})
}
return bars
}
func todayTip(now time.Time) map[string]any {
st := companion.TodaySolar(now)
el := seasonEl[int(now.Month())-1]
yj := yiJi[el]
return map[string]any{
"solar_term": st.Name,
"season_el": el,
"yi": yj.Yi,
"ji": yj.Ji,
"tip": st.Tip,
}
}
func indexOf(arr []string, s string) int {
for i, v := range arr {
if v == s {
return i
}
}
return 0
}