feat: add RelationInsight API and wire H5 relation/profile pages

Second P1 growth engine: compare two profiles, gate full tips behind
deep-access mock payment, and list personal archives on /profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-02 16:28:10 +08:00
co-authored by Cursor
parent 0f320e040b
commit 14b836f53b
12 changed files with 465 additions and 14 deletions
+83
View File
@@ -0,0 +1,83 @@
// Package relation builds 关系理解 content from two birth-based portraits.
package relation
import (
"fmt"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
)
// Output is free summary + gated detail.
type Output struct {
Summary map[string]any
Detail map[string]any
}
// Build compares two profiles' exploration styles (lexicon-safe copy).
func Build(aBirth, bBirth time.Time, aName, bName string) Output {
if aName == "" {
aName = "我"
}
if bName == "" {
bName = "TA"
}
pa := portrait.Build(aBirth, aName)
pb := portrait.Build(bBirth, bName)
aLabel := str(pa.Summary["headline"])
bLabel := str(pb.Summary["headline"])
aKeys := strSlice(pa.Summary["keywords"])
bKeys := strSlice(pb.Summary["keywords"])
summary := map[string]any{
"title": "关系理解·基础",
"me_style": aLabel,
"other_style": bLabel,
"me_keywords": aKeys,
"other_keywords": bKeys,
"diff_one_liner": fmt.Sprintf("%s更偏内在节奏,%s的表达方式不同——差异可以成为互补,而不是对立。", aName, bName),
"share_hint": "了解彼此的沟通方式,查看关系理解",
}
detail := map[string]any{
"title": "相处建议·完整分析",
"communication": []string{
fmt.Sprintf("%s:先讲清楚事实与需要,再谈感受。", aName),
fmt.Sprintf("%s:先确认被听见,再进入方案讨论。", bName),
"冲突时约定「复述对方一句」再回应,降低误解。",
},
"interaction": []string{
"用「我观察到…我需要…」代替指责句式。",
"每周留一次轻松同步:最近什么在消耗/滋养这段关系。",
},
"maintenance": []string{
"差异标签不是评分,而是协作说明书。",
"重要决定前,双方各写三点顾虑再合并。",
},
"me_behavior": str(pa.Detail["behavior_pattern"]),
"other_behavior": str(pb.Detail["behavior_pattern"]),
}
return Output{Summary: summary, Detail: detail}
}
func str(v any) string {
s, _ := v.(string)
return s
}
func strSlice(v any) []string {
arr, ok := v.([]string)
if ok {
return arr
}
raw, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, x := range raw {
if s, ok := x.(string); ok {
out = append(out, s)
}
}
return out
}