feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,9 @@ 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.
|
||||
@@ -14,8 +17,13 @@ type Output struct {
|
||||
|
||||
var elements = []string{"木", "火", "土", "金", "水"}
|
||||
|
||||
// Build from birth date.
|
||||
// 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 = "你"
|
||||
@@ -36,9 +44,20 @@ func Build(birth time.Time, displayName string) Output {
|
||||
{"key": "mood", "title": "情绪调节", "teaser": tip.Mood, "score": 71},
|
||||
}
|
||||
|
||||
todayTip := tip.Sleep
|
||||
weekday := int(time.Now().Weekday())
|
||||
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 + "倾向",
|
||||
@@ -48,12 +67,20 @@ func Build(birth time.Time, displayName string) Output {
|
||||
"life_tip": tip.WeekTip,
|
||||
"today_tip": todayTip,
|
||||
"week_focus": weekHints[weekday%len(weekHints)],
|
||||
"keywords": []string{primary, secondary, "生活建议", "节律"},
|
||||
"dimensions": dims,
|
||||
"primary_element": primary,
|
||||
"secondary_element": secondary,
|
||||
"strengths_preview": tip.Strengths[:min(3, len(tip.Strengths))],
|
||||
"blind_spots_preview": []string{"完整习惯方案见深度版。"},
|
||||
"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{
|
||||
@@ -65,8 +92,8 @@ func Build(birth time.Time, displayName string) Output {
|
||||
{"title": "活动与放松", "body": tip.MoveDeep, "bullets": tip.MoveBullets},
|
||||
{"title": "情绪与压力", "body": tip.MoodDeep, "bullets": tip.MoodBullets},
|
||||
{"title": "与节气联动", "body": "可结合「节气生活」调整当季节奏;变化慢一点,观察身体反馈即可。", "bullets": []string{"查看今日节气", "一次只改一个习惯", "不适就医,勿自行当治疗"}},
|
||||
{"title": "今日生活建议", "body": tip.Sleep + " " + tip.Mood, "bullets": []string{tip.Move, tip.Diet}},
|
||||
{"title": "本周节奏", "body": tip.WeekTip, "bullets": tip.OverviewBullets},
|
||||
{"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,
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package rhythm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
out := Build(time.Date(1992, 6, 8, 0, 0, 0, 0, time.UTC), "我")
|
||||
if out.Summary["primary_element"] == nil {
|
||||
t.Fatal("missing element")
|
||||
func TestBuildIncludesWuxingBars(t *testing.T) {
|
||||
birth := time.Date(1992, 6, 8, 0, 0, 0, 0, time.UTC)
|
||||
out := BuildWith(birth, "测", time.Date(2026, 8, 12, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)))
|
||||
wx, ok := out.Summary["wuxing"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("missing wuxing")
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
for _, bad := range []string{"运势", "吉凶", "算命", "疗效", "治病"} {
|
||||
if strings.Contains(string(raw), bad) {
|
||||
t.Fatalf("forbidden %q", bad)
|
||||
bars, ok := wx["bars"].([]map[string]any)
|
||||
if !ok || len(bars) != 5 {
|
||||
t.Fatalf("bars=%#v", wx["bars"])
|
||||
}
|
||||
if wx["strong_el"] == nil || wx["care_dir"] == nil {
|
||||
t.Fatalf("incomplete wuxing: %#v", wx)
|
||||
}
|
||||
prev, ok := out.Summary["blind_spots_preview"].([]string)
|
||||
if !ok || len(prev) == 0 {
|
||||
t.Fatalf("blind_spots_preview=%#v", out.Summary["blind_spots_preview"])
|
||||
}
|
||||
for _, s := range prev {
|
||||
if strings.Contains(s, "深度版") {
|
||||
t.Fatalf("preview should not be upsell: %q", s)
|
||||
}
|
||||
}
|
||||
st, ok := out.Summary["solar_term"].(map[string]any)
|
||||
if !ok || st["name"] == nil {
|
||||
t.Fatalf("missing solar_term: %#v", out.Summary["solar_term"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user