Files
digital-psychology/apps/api/internal/rhythm/engine.go
T
jackyu66gitandCursor 89756f65b4 feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 01:27:58 +08:00

233 lines
12 KiB
Go

// Package rhythm builds 身心节律 reports (lifestyle exploration, not fortune/medical claims).
package rhythm
import (
"fmt"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/companion"
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
)
// Output free summary + gated detail.
type Output struct {
Summary map[string]any
Detail map[string]any
}
var elements = []string{"木", "火", "土", "金", "水"}
// Build from birth date (asOf = now).
func Build(birth time.Time, displayName string) Output {
return BuildWith(birth, displayName, time.Time{})
}
// BuildWith anchors today/week tips to asOf (zero = now CST).
func BuildWith(birth time.Time, displayName string, asOf time.Time) Output {
name := displayName
if name == "" {
name = "你"
}
primary := elements[(birth.Year()+int(birth.Month())+birth.Day())%5]
secondary := elements[(birth.YearDay())%5]
if secondary == primary {
secondary = elements[(birth.YearDay()+1)%5]
}
tip := tips[primary]
dims := []map[string]any{
{"key": "element", "title": "主导倾向", "teaser": fmt.Sprintf("更偏「%s」隐喻", primary), "score": 80},
{"key": "balance", "title": "平衡关注", "teaser": fmt.Sprintf("可留意「%s」侧滋养", secondary), "score": 68},
{"key": "sleep", "title": "作息", "teaser": tip.Sleep, "score": 72},
{"key": "diet", "title": "饮食节奏", "teaser": tip.Diet, "score": 70},
{"key": "move", "title": "活动", "teaser": tip.Move, "score": 74},
{"key": "mood", "title": "情绪调节", "teaser": tip.Mood, "score": 71},
}
when := asOf
if when.IsZero() {
when = time.Now().In(time.FixedZone("CST", 8*3600))
} else {
when = when.In(time.FixedZone("CST", 8*3600))
}
todayTips := []string{tip.Sleep, tip.Diet, tip.Move, tip.Mood}
todayTip := todayTips[when.YearDay()%len(todayTips)]
weekday := int(when.Weekday())
weekHints := []string{tip.Mood, tip.Move, tip.Diet, tip.Sleep, tip.WeekTip, tip.Move, tip.Mood}
dayKey := when.Format("2006-01-02")
validUntil := time.Date(when.Year(), when.Month(), when.Day()+1, 0, 0, 0, 0, when.Location())
wx := portrait.WuxingBlock(birth)
st := companion.TodaySolar(when)
summary := map[string]any{
"title": "身心节律·基础",
"style_label": primary + "倾向",
"headline": fmt.Sprintf("%s的身心节律更偏「%s」隐喻", name, primary),
"one_liner": tip.OneLiner,
"overview": fmt.Sprintf("%s%s主导倾向「%s」,辅助关注「%s」。以下建议用于生活节奏探索,不构成医疗意见。", name, tip.Overview, primary, secondary),
"life_tip": tip.WeekTip,
"today_tip": todayTip,
"week_focus": weekHints[weekday%len(weekHints)],
"as_of": dayKey,
"valid_until": validUntil.Format(time.RFC3339),
"wuxing": wx,
"solar_term": map[string]any{
"name": st.Name,
"tip": st.Tip,
},
"keywords": []string{primary, secondary, "生活建议", "节律"},
"dimensions": dims,
"primary_element": primary,
"secondary_element": secondary,
"strengths_preview": tip.Strengths[:min(3, len(tip.Strengths))],
"blind_spots_preview": tip.BlindSpots[:min(3, len(tip.BlindSpots))],
"disclaimer_constitution": "体质与调养内容为生活方式参考,非医疗建议。",
}
detail := map[string]any{
"title": "身心节律·完整建议",
"sections": []map[string]any{
{"title": "节律总览", "body": tip.DeepOverview, "bullets": tip.OverviewBullets},
{"title": "作息建议", "body": tip.SleepDeep, "bullets": tip.SleepBullets},
{"title": "饮食节奏", "body": tip.DietDeep, "bullets": tip.DietBullets},
{"title": "活动与放松", "body": tip.MoveDeep, "bullets": tip.MoveBullets},
{"title": "情绪与压力", "body": tip.MoodDeep, "bullets": tip.MoodBullets},
{"title": "与节气联动", "body": "可结合「节气生活」调整当季节奏;变化慢一点,观察身体反馈即可。", "bullets": []string{"查看今日节气", "一次只改一个习惯", "不适就医,勿自行当治疗"}},
{"title": "今日生活建议", "body": todayTip + " " + tip.Mood, "bullets": []string{tip.Move, tip.Diet}},
{"title": "本周节奏", "body": weekHints[weekday%len(weekHints)], "bullets": tip.OverviewBullets},
},
"strengths": tip.Strengths,
"blind_spots": tip.BlindSpots,
"growth_plan": []map[string]any{
{"phase": "本周", "focus": tip.WeekTip},
{"phase": "本月", "focus": tip.MonthTip},
{"phase": "长期", "focus": "形成「充电/耗电」清单,并与节气提醒一起复盘。"},
},
"conversation_scripts": tip.Scripts,
"faq": []map[string]string{
{"q": "这是占卜或看病吗?", "a": "都不是。这是身心节律的自我探索与生活建议,不预测未来,也不构成医疗诊断。"},
},
"behavior_pattern": tip.DeepOverview,
"relation_style": tip.MoodDeep,
"growth_direction": tip.MonthTip,
}
return Output{Summary: summary, Detail: detail}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
type tipPack struct {
OneLiner, Overview, WeekTip, MonthTip, DeepOverview string
Sleep, Diet, Move, Mood string
SleepDeep, DietDeep, MoveDeep, MoodDeep string
OverviewBullets, SleepBullets, DietBullets, MoveBullets, MoodBullets []string
Strengths, BlindSpots, Scripts []string
}
var tips = map[string]tipPack{
"木": {
OneLiner: "你适合有伸展与目标感的节奏,压得太死容易闷。",
Overview: "在节律隐喻里偏「生发」:需要空间与方向。",
WeekTip: "本周安排三次户外或拉伸,每次 15 分钟。",
MonthTip: "给自己一个可完成的小目标,并拆成周检查点。",
DeepOverview: "木倾向的人常需要「向前」的感觉。卡住时,动一动比硬想更有效。注意肩颈与情绪积压。",
Sleep: "尽量固定入睡窗口", Diet: "清淡多样,少暴饮暴食", Move: "伸展、快走、轻度有氧", Mood: "把烦闷说出来或写下来",
SleepDeep: "规律睡眠能显著降低烦躁。睡前一小时减少争执性内容。",
DietDeep: "少油炸重味,增加新鲜蔬菜;饥一顿饱一顿会放大情绪波动。",
MoveDeep: "优先选择有伸展感的活动,避免长期久坐。",
MoodDeep: "愤怒或闷气是常见信号。命名情绪后做一次短活动再对话。",
OverviewBullets: []string{"需要方向感", "忌长期压抑", "动一动有助疏通"},
SleepBullets: []string{"固定入睡", "睡前减刺激"},
DietBullets: []string{"清淡多样", "规律进餐"},
MoveBullets: []string{"伸展快走", "避免久坐"},
MoodBullets: []string{"命名情绪", "先活动再谈"},
Strengths: []string{"行动欲", "目标感", "适应生长"},
BlindSpots: []string{"易急躁", "压怒", "忽视休息"},
Scripts: []string{"我有点闷,想先走走再聊。", "我需要一个小目标来找到节奏。"},
},
"火": {
OneLiner: "你靠热情与连接充电,也要注意过热后的回落。",
Overview: "节律隐喻偏「温煦」:表达与互动重要。",
WeekTip: "保证两晚 23:30 前入睡,并安排一次轻松社交。",
MonthTip: "学会在热情后安排恢复日,而不是连续硬冲。",
DeepOverview: "火倾向的人能量外放。高潮后空虚很常见,回落期做轻松恢复即可。",
Sleep: "避免熬夜透支", Diet: "少辛辣刺激过量", Move: "有节奏的有氧", Mood: "表达后也要倾听",
SleepDeep: "睡眠差会放大冲动。把「今晚早睡」当成任务完成。",
DietDeep: "辛辣咖啡适量;补水与规律三餐稳住节奏。",
MoveDeep: "运动有助释放兴奋,但避免以透支换快乐。",
MoodDeep: "表达是优点。记得给对方回应空间,也给自己降温时间。",
OverviewBullets: []string{"热情连接", "注意回落", "表达重要"},
SleepBullets: []string{"少熬夜", "降温再睡"},
DietBullets: []string{"刺激适量", "规律进餐"},
MoveBullets: []string{"有氧释放", "勿透支"},
MoodBullets: []string{"表达+倾听", "回落期恢复"},
Strengths: []string{"感染力", "行动号召", "乐观"},
BlindSpots: []string{"过热", "承诺过多", "忽视恢复"},
Scripts: []string{"我很兴奋,我们先定本周唯一下一步。", "我需要今晚早点休息。"},
},
"土": {
OneLiner: "你需要稳定与踏实,变化太快会消耗安全感。",
Overview: "节律隐喻偏「承载」:规律比刺激更重要。",
WeekTip: "固定三餐时间,并完成一次环境整理(桌面/房间一角)。",
MonthTip: "建立一个可重复的晨间或睡前小仪式。",
DeepOverview: "土倾向的人靠可预期感充电。突变多时,用清单与仪式稳住自己。",
Sleep: "同一时间上床", Diet: "温热易消化、勿过饥过饱", Move: "散步、力量轻度", Mood: "少同时处理多件事",
SleepDeep: "规律比时长更重要。周末也不要大幅颠倒作息。",
DietDeep: "细嚼慢咽;减少生冷刺激性饮食实验。",
MoveDeep: "稳定、可坚持的运动优于偶尔高强度。",
MoodDeep: "焦虑常来自失控感。把任务拆小,完成一件勾一项。",
OverviewBullets: []string{"需要稳定", "仪式感有用", "忌突变过多"},
SleepBullets: []string{"同时上床", "周末少颠倒"},
DietBullets: []string{"规律温热", "细嚼"},
MoveBullets: []string{"可坚持", "散步力量"},
MoodBullets: []string{"清单拆解", "单线程"},
Strengths: []string{"踏实", "耐心", "可靠"},
BlindSpots: []string{"抗拒变化", "过度担忧", "行动偏慢"},
Scripts: []string{"我想先把节奏排好,再谈变化。", "给我一点准备时间,我会更稳。"},
},
"金": {
OneLiner: "你重视秩序与边界,混乱环境会让你烦躁。",
Overview: "节律隐喻偏「收敛」:清晰与整理很重要。",
WeekTip: "清理数字或实体杂物 20 分钟,并写下三条边界。",
MonthTip: "练习两次温和而清晰的拒绝。",
DeepOverview: "金倾向的人靠清晰边界获得掌控感。过度苛求完美会消耗自己。",
Sleep: "睡前收工仪式", Diet: "少熬夜夜宵", Move: "呼吸、拉伸、节奏运动", Mood: "把标准调到足够好",
SleepDeep: "工作与睡眠边界要划清;床上不处理未完成清单。",
DietDeep: "规律饮食;减少以咖啡硬撑。",
MoveDeep: "注重呼吸与肩颈;短时高质量运动即可。",
MoodDeep: "挑剔有时是焦虑。用「足够好」完成一件事对抗空转。",
OverviewBullets: []string{"边界清晰", "忌混乱", "完美主义留意"},
SleepBullets: []string{"收工仪式", "床上不工作"},
DietBullets: []string{"少硬撑", "规律"},
MoveBullets: []string{"呼吸拉伸", "短时有效"},
MoodBullets: []string{"足够好", "温和拒绝"},
Strengths: []string{"条理", "原则", "质量感"},
BlindSpots: []string{"过苛", "难放手", "紧绷"},
Scripts: []string{"这次我想按我的节奏来。", "这件事我接不住,我们换个方案。"},
},
"水": {
OneLiner: "你感受深、需要沉淀,被催促时容易关闭。",
Overview: "节律隐喻偏「滋润」:休整与感受空间重要。",
WeekTip: "安排两段不被打扰的静默时间(各 20 分钟)。",
MonthTip: "建立睡前放松流程,减少临睡刷信息。",
DeepOverview: "水倾向的人需要内在空间。催促与过载会让你退缩;适度分享感受可减少误解。",
Sleep: "保证连续睡眠", Diet: "温补平和、少冰冷刺激", Move: "游泳、散步、柔软活动", Mood: "先感受再决策",
SleepDeep: "睡眠是情绪水库。缺觉时少做重大决定。",
DietDeep: "规律温热;观察哪些食物让你更稳。",
MoveDeep: "柔和持续的活动比爆发冲刺更适合你。",
MoodDeep: "情绪来时先命名;写三句或找安全的人听你说完。",
OverviewBullets: []string{"需要沉淀", "忌强催", "感受力强"},
SleepBullets: []string{"连续睡眠", "少缺觉决策"},
DietBullets: []string{"温热平和", "规律"},
MoveBullets: []string{"柔和持续", "散步游泳"},
MoodBullets: []string{"先命名", "再决策"},
Strengths: []string{"洞察", "共情", "韧性"},
BlindSpots: []string{"回避", "反刍", "边界模糊"},
Scripts: []string{"我需要一点时间感受一下,再回复你。", "我不是拖延,我在整理自己。"},
},
}