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:
jackyu66git
2026-08-13 01:27:58 +08:00
co-authored by Cursor
parent 13860bf1ef
commit 89756f65b4
227 changed files with 7405 additions and 1407 deletions
+122
View File
@@ -0,0 +1,122 @@
package scale
import (
"encoding/json"
"strconv"
)
// JungianQuestionMeta is parsed from scale_questions.body for OEJTS-style items.
type JungianQuestionMeta struct {
Dimension string `json:"dimension"`
Format string `json:"format"`
Left string `json:"left"`
Right string `json:"right"`
Prompt string `json:"prompt"`
}
// ScoreJungian tallies 15 Likert answers per EI/SN/TF/JP (OEJTS rules).
// Left poles: E, S, F, J · Right poles: I, N, T, P · threshold = perDim*3.
func ScoreJungian(answers map[string]string, qMeta map[string]JungianQuestionMeta, perDim int) (typeCode string, raw map[string]int, pct map[string]int) {
raw = map[string]int{"EI": 0, "SN": 0, "TF": 0, "JP": 0}
if perDim <= 0 {
perDim = 8
}
for qid, ans := range answers {
meta, ok := qMeta[qid]
if !ok || meta.Dimension == "" {
continue
}
v, err := strconv.Atoi(ans)
if err != nil || v < 1 || v > 5 {
v = 3
}
raw[meta.Dimension] += v
}
thr := perDim * 3
pick := func(score int, left, right string) string {
if score > thr {
return right
}
return left
}
typeCode = pick(raw["EI"], "E", "I") +
pick(raw["SN"], "S", "N") +
pick(raw["TF"], "F", "T") +
pick(raw["JP"], "J", "P")
pct = map[string]int{}
minS, maxS := perDim, perDim*5
span := float64(maxS - minS)
rightPct := func(score int) int {
p := int(float64(score-minS)/span*100 + 0.5)
if p < 0 {
return 0
}
if p > 100 {
return 100
}
return p
}
eiR := rightPct(raw["EI"])
snR := rightPct(raw["SN"])
tfR := rightPct(raw["TF"])
jpR := rightPct(raw["JP"])
pct["E"], pct["I"] = 100-eiR, eiR
pct["S"], pct["N"] = 100-snR, snR
pct["F"], pct["T"] = 100-tfR, tfR
pct["J"], pct["P"] = 100-jpR, jpR
return typeCode, raw, pct
}
// ParseJungianMeta extracts dimension metadata from question body JSON.
func ParseJungianMeta(body json.RawMessage) JungianQuestionMeta {
var m JungianQuestionMeta
_ = json.Unmarshal(body, &m)
return m
}
// BuildJungianResult builds rich result with 4-letter code + preference bars.
func BuildJungianResult(typeCode string, pct map[string]int) map[string]interface{} {
label := MBTILabel(typeCode)
base := mbtiPack(typeCode, label)
base.Label = typeCode + " · " + label
base.ShareLine = "我的类型探索:" + typeCode + " · " + label
base.Dimensions = []map[string]interface{}{
{"title": "E/I 能量", "score": maxPct(pct, "E", "I"), "note": prefNote("E", "I", pct, "外向互动", "内向思考")},
{"title": "S/N 信息", "score": maxPct(pct, "S", "N"), "note": prefNote("S", "N", pct, "具体事实", "模式可能")},
{"title": "T/F 决策", "score": maxPct(pct, "T", "F"), "note": prefNote("T", "F", pct, "逻辑一致", "价值与人际")},
{"title": "J/P 节奏", "score": maxPct(pct, "J", "P"), "note": prefNote("J", "P", pct, "计划结构", "灵活开放")},
}
out := map[string]interface{}{
"title": "MBTI 探索结果",
"style_key": typeCode,
"type_code": typeCode,
"label": base.Label,
"summary": base.Summary,
"overview": base.Overview,
"share_line": base.ShareLine,
"dimensions": base.Dimensions,
"preferences": pct,
"strengths": base.Strengths,
"watchouts": base.Watchouts,
"tips": base.Tips,
"scripts": base.Scripts,
"growth_plan": base.GrowthPlan,
"faq": base.FAQ,
}
return out
}
func maxPct(pct map[string]int, a, b string) int {
if pct[a] >= pct[b] {
return pct[a]
}
return pct[b]
}
func prefNote(a, b string, pct map[string]int, aDesc, bDesc string) string {
if pct[a] >= pct[b] {
return a + " " + strconv.Itoa(pct[a]) + "% · " + aDesc
}
return b + " " + strconv.Itoa(pct[b]) + "% · " + bDesc
}