落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
4.4 KiB
Go
160 lines
4.4 KiB
Go
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
|
|
}
|
|
|
|
// WuxingBlock builds the shared 五行 summary block (bars + tendency labels).
|
|
func WuxingBlock(birth time.Time) map[string]any {
|
|
y, m, d := birth.Date()
|
|
bz := getBazi(y, int(m), d, 12)
|
|
pct := calc5E(bz)
|
|
strong := strongestEl(pct)
|
|
weak := weakestEl(pct)
|
|
cst := constitutionByElement[strong]
|
|
emo := emotionByElement[strong]
|
|
return 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,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|