refactor(ECR-001): 接入 ESS 并完成结构对齐 Phase A–E
绑定 ESS 双轨治理,拆分超大 H5 页与 Go 引擎,抽出 membership 服务, 并将 star/fortune 重命名为 outlook(JSON 双写兼容);同时修复 /psy API 代理与首页 + 菜单层级。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,13 +8,15 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/membership"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
// ReportHandler exposes portrait reports and commerce mock.
|
||||
type ReportHandler struct {
|
||||
Svc *report.Service
|
||||
Svc *report.Service
|
||||
Membership *membership.Service
|
||||
}
|
||||
|
||||
// Register mounts report/commerce routes.
|
||||
@@ -200,7 +202,7 @@ func (h *ReportHandler) GetMembership(c *gin.Context) {
|
||||
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
|
||||
return
|
||||
}
|
||||
me, err := h.Svc.GetMembership(c.Request.Context(), userID)
|
||||
me, err := h.Membership.Get(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
|
||||
return
|
||||
@@ -233,7 +235,7 @@ func (h *ReportHandler) CreateOrder(c *gin.Context) {
|
||||
}
|
||||
rid = &id
|
||||
}
|
||||
oid, err := h.Svc.CreateOrder(c.Request.Context(), userID, report.CreateOrderInput{
|
||||
oid, err := h.Membership.CreateOrder(c.Request.Context(), userID, membership.CreateOrderInput{
|
||||
Kind: req.Kind, Plan: req.Plan, ReportID: rid,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -255,7 +257,7 @@ func (h *ReportHandler) PayMock(c *gin.Context) {
|
||||
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := h.Svc.PayMock(c.Request.Context(), userID, oid); err != nil {
|
||||
if err := h.Membership.PayMock(c.Request.Context(), userID, oid); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 30004, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/ask"
|
||||
companionsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/companion"
|
||||
imagecardsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/imagecard"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/membership"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/profile"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/relation"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
|
||||
@@ -38,6 +39,7 @@ func NewRouter(pool *pgxpool.Pool, cfg config.Config) *gin.Engine {
|
||||
Reports: reportRepo,
|
||||
Invites: &repository.SynastryInviteRepo{Pool: pool},
|
||||
}
|
||||
membershipSvc := &membership.Service{Reports: reportRepo}
|
||||
relationSvc := &relation.Service{Profiles: profileRepo, Reports: reportRepo, Relations: relationRepo}
|
||||
scaleSvc := &scale.Service{Repo: &repository.ScaleRepo{Pool: pool}, Profiles: profileRepo}
|
||||
askSvc := &ask.Service{Profiles: profileRepo, Reports: reportRepo, Ask: askRepo, LLM: llm}
|
||||
@@ -64,7 +66,7 @@ func NewRouter(pool *pgxpool.Pool, cfg config.Config) *gin.Engine {
|
||||
authed := api.Group("")
|
||||
authed.Use(middleware.DeviceAuth(pool))
|
||||
(&handler.ProfileHandler{Svc: profileSvc}).Register(authed)
|
||||
(&handler.ReportHandler{Svc: reportSvc}).Register(authed)
|
||||
(&handler.ReportHandler{Svc: reportSvc, Membership: membershipSvc}).Register(authed)
|
||||
(&handler.SynastryHandler{Svc: reportSvc}).Register(authed)
|
||||
(&handler.RelationHandler{Svc: relationSvc}).Register(authed)
|
||||
(&handler.ScaleHandler{Svc: scaleSvc}).Register(authed)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package relation
|
||||
|
||||
import "fmt"
|
||||
|
||||
func complementarityKey(a, b string) string {
|
||||
if a == b {
|
||||
return "same"
|
||||
}
|
||||
// simple buckets by trait family
|
||||
drive := map[string]string{
|
||||
"稳进探索者": "steady", "细腻分析者": "steady", "守护担当者": "steady", "洞察策略者": "steady",
|
||||
"敏锐连接者": "warm", "温和协调者": "warm", "热忱鼓舞者": "warm",
|
||||
"果断行动派": "drive", "自由创造者": "drive",
|
||||
}
|
||||
ak, bk := drive[a], drive[b]
|
||||
if ak == "" || bk == "" {
|
||||
return "mix"
|
||||
}
|
||||
if ak == bk {
|
||||
return "same_family"
|
||||
}
|
||||
if (ak == "steady" && bk == "warm") || (ak == "warm" && bk == "steady") {
|
||||
return "steady_warm"
|
||||
}
|
||||
if (ak == "drive" && bk == "steady") || (ak == "steady" && bk == "drive") {
|
||||
return "drive_steady"
|
||||
}
|
||||
if (ak == "drive" && bk == "warm") || (ak == "warm" && bk == "drive") {
|
||||
return "drive_warm"
|
||||
}
|
||||
return "mix"
|
||||
}
|
||||
|
||||
type compCopy struct {
|
||||
OneLiner, Overview, Chemistry, DeepOverview, CommDeep, ConflictDeep, IntimacyDeep, GrowthDeep string
|
||||
ChemistryPoints, Watchouts, CommTips, ConflictTips, IntimacyTips, GrowthTips, Weekly, Scripts []string
|
||||
}
|
||||
|
||||
func complementarityCopy(key, aName, bName, aLabel, bLabel string) compCopy {
|
||||
base := compCopy{
|
||||
OneLiner: fmt.Sprintf("%s偏「%s」,%s偏「%s」——差异可以写成相处说明书。", aName, aLabel, bName, bLabel),
|
||||
Overview: fmt.Sprintf("双方在表达、节奏与需求上并不相同。把差异看清楚,比急着证明「谁更对」更有用。下面从沟通、冲突、亲密与共同成长几个维度展开。"),
|
||||
Chemistry: "互补往往出现在:一方给结构,另一方给温度;或一方推进,另一方稳住质量。",
|
||||
Weekly: []string{
|
||||
"本周进行一次 20 分钟「非解决问题」闲聊或散步。",
|
||||
"各自写三件「我需要你这样支持我」的具体行为,互换阅读。",
|
||||
"约定一个冲突停火词,任一方说出即暂停 15 分钟。",
|
||||
},
|
||||
Scripts: []string{
|
||||
fmt.Sprintf("%s可以说:我需要先把事实说清楚,再谈感受。", aName),
|
||||
fmt.Sprintf("%s可以说:我希望你先听到我的感受,再给方案。", bName),
|
||||
"我们可以先复述对方一句,再表达自己的需要。",
|
||||
},
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "same":
|
||||
base.OneLiner = fmt.Sprintf("你们风格接近(都偏「%s」),默契来得快,也要防止一起陷入同样的盲区。", aLabel)
|
||||
base.Chemistry = "同类相吸:理解成本低,推进或回避也可能同步发生。"
|
||||
base.DeepOverview = "风格相近意味着你们很容易「懂对方在想什么」,但也可能同时逃避冲突,或同时过度冲刺。建议定期引入外部视角(朋友建议、清单复盘),打破镜像盲区。"
|
||||
base.CommDeep = "沟通效率高,但要刻意练习提出不同意见。安排「唱反调」轮值:每周一人专门提出风险点。"
|
||||
base.ConflictDeep = "冲突可能被快速和好掩盖,问题未真正处理。用「问题清单」追踪未完成议题。"
|
||||
base.IntimacyDeep = "熟悉感强,新鲜感需主动创造:共同学习或小旅行比重复日常更能充电。"
|
||||
base.GrowthDeep = "一起设定一个共同小目标,并互相做问责伙伴。"
|
||||
base.ChemistryPoints = []string{"理解成本低", "节奏容易对齐", "共同语言多"}
|
||||
base.Watchouts = []string{"共享同一盲区", "缺少外部校正", "意见过于一致缺少张力"}
|
||||
base.CommTips = []string{"鼓励提出异议", "重要决定写利弊表", "避免默认对方已懂"}
|
||||
base.ConflictTips = []string{"追踪未完成议题", "避免假性和好", "冷静后再做决定"}
|
||||
base.IntimacyTips = []string{"主动制造新鲜体验", "表达感谢要具体", "保留个人空间"}
|
||||
base.GrowthTips = []string{"共同目标 + 问责", "每月复盘一次关系", "引入可信第三方建议"}
|
||||
case "steady_warm":
|
||||
base.Chemistry = "稳与暖互补:一方提供结构与可靠,另一方提供连接与温度。"
|
||||
base.DeepOverview = fmt.Sprintf("%s与%s之间,最常见的张力是「要先讲清楚」还是「要先被看见」。若能轮流满足这两种需求,关系会既安全又有温度。", aName, bName)
|
||||
base.CommDeep = "沟通协议:情绪话题先共鸣 2 分钟,再进入事实与方案;事务话题先结论,再补感受。"
|
||||
base.ConflictDeep = "稳的一方别用沉默当结束;暖的一方别用追问升级压力。停火后用「我需要…」重开。"
|
||||
base.IntimacyDeep = "暖的一方需要回应频率;稳的一方需要可预期的独处。把两者写进约定。"
|
||||
base.GrowthDeep = "把互补写成分工:谁更擅长安抚,谁更擅长推进落地。"
|
||||
base.ChemistryPoints = []string{"结构 × 温度", "可靠 × 连接", "可形成完整支持系统"}
|
||||
base.Watchouts = []string{"一方觉得被冷落", "一方觉得被情绪淹没", "节奏错位积累委屈"}
|
||||
base.CommTips = []string{"情绪先共鸣再方案", "事务先结论再感受", "用文字确认关键约定"}
|
||||
base.ConflictTips = []string{"禁止用沉默结束话题", "追问前先问是否方便", "停火词机制"}
|
||||
base.IntimacyTips = []string{"约定回应窗口", "尊重独处不被解读为冷淡", "每周一次深度连接"}
|
||||
base.GrowthTips = []string{"按优势分工", "互相学习对方语言", "月度关系复盘"}
|
||||
case "drive_steady":
|
||||
base.Chemistry = "推与稳互补:一方破局加速,另一方把关质量与可持续。"
|
||||
base.DeepOverview = "行动派容易嫌分析派慢;稳健派容易嫌行动派莽。把「速度」用在试验,「稳健」用在关键承诺,冲突会下降。"
|
||||
base.CommDeep = "行动方给时间盒与最小方案;稳健方在时限内给风险清单,而不是无限延期。"
|
||||
base.ConflictDeep = "冲突焦点常是节奏。先对齐「这是可逆试验还是重大决定」,再选速度。"
|
||||
base.IntimacyDeep = "行动方用陪伴质量弥补碎片时间;稳健方减少用担忧浇灭热情,改用「我支持你试,我们设检查点」。"
|
||||
base.GrowthDeep = "共同项目里明确角色:谁启动、谁验收、何时复盘。"
|
||||
base.ChemistryPoints = []string{"破局 × 把关", "速度 × 质量", "试验与承诺可分工"}
|
||||
base.Watchouts = []string{"节奏互斥", "一方压抑热情", "一方焦虑失控"}
|
||||
base.CommTips = []string{"先定义可逆/不可逆", "时间盒决策", "风险清单限时"}
|
||||
base.ConflictTips = []string{"争论节奏前先分类问题", "避免人格化指责", "用检查点代替否决"}
|
||||
base.IntimacyTips = []string{"质量陪伴", "支持试验+检查点", "庆祝小进展"}
|
||||
base.GrowthTips = []string{"项目角色清晰", "复盘节奏", "互相翻译动机"}
|
||||
case "drive_warm":
|
||||
base.Chemistry = "驱动与连接互补:一方带节奏,另一方维系人心与氛围。"
|
||||
base.DeepOverview = "热情与效率碰到一起很有火花,也容易在「推进」与「照顾感受」之间拉扯。约定场景切换:冲刺模式 / 连接模式。"
|
||||
base.CommDeep = "冲刺时短讯同步进度;连接时关掉任务话题。不要用效率语言处理情绪时刻。"
|
||||
base.ConflictDeep = "驱动方避免「你想太多」;连接方避免「你只在乎结果」。改说具体需求。"
|
||||
base.IntimacyDeep = "用共同体验(运动、活动)同时满足推进感与连接感。"
|
||||
base.GrowthDeep = "轮流做「本周关系主理人」,负责安排一次连接或一次共同目标。"
|
||||
base.ChemistryPoints = []string{"推进力 × 氛围", "行动号召力强", "共同体验易充电"}
|
||||
base.Watchouts = []string{"情绪被效率压过", "承诺过多难兑现", "连接变任务化"}
|
||||
base.CommTips = []string{"模式切换:冲刺/连接", "情绪时刻禁用效率话术", "进度短讯化"}
|
||||
base.ConflictTips = []string{"禁止否定感受", "需求具体化", "修复后再推进"}
|
||||
base.IntimacyTips = []string{"共同体验", "兑现小承诺", "非任务陪伴"}
|
||||
base.GrowthTips = []string{"轮值关系主理人", "控制并行承诺", "庆祝与复盘并重"}
|
||||
case "same_family":
|
||||
base.DeepOverview = "你们属于相近气质族,容易互相理解,也要主动制造一点建设性差异,避免舒适区停滞。"
|
||||
base.CommDeep = "沟通顺畅时更要确认细节,防止「好像说好了」其实理解不同。"
|
||||
base.ConflictDeep = "冲突可能被淡化。强制做一次「最担心的三件事」互换。"
|
||||
base.IntimacyDeep = "在舒适之外增加挑战性共同任务,刷新关系动能。"
|
||||
base.GrowthDeep = "互相指出对方一个盲区,并约定本月各改一项小行为。"
|
||||
base.ChemistryPoints = []string{"气质相近", "理解门槛低", "协作起步快"}
|
||||
base.Watchouts = []string{"舒适区停滞", "细节默认错误", "回避尖锐议题"}
|
||||
base.CommTips = []string{"确认细节", "书面关键约定", "鼓励异议"}
|
||||
base.ConflictTips = []string{"互换担忧清单", "不假性和好", "设讨论截止"}
|
||||
base.IntimacyTips = []string{"共同挑战任务", "新鲜体验", "具体感谢"}
|
||||
base.GrowthTips = []string{"互指一个盲区", "月改一小行为", "外部输入"}
|
||||
default:
|
||||
base.DeepOverview = fmt.Sprintf("%s与%s风格路径不同,说明书价值更高。先承认差异合法,再谈协作规则。", aName, bName)
|
||||
base.CommDeep = "建立双通道:事实通道与感受通道,讨论前先声明走哪一条。"
|
||||
base.ConflictDeep = "冲突时回到共同目标句:我们都希望关系更好/事情做成。"
|
||||
base.IntimacyDeep = "用定期同步取代猜测;空间与连接都要有配额。"
|
||||
base.GrowthDeep = "把差异写成「我擅长 / 我需要」对照表,贴在看得见的地方。"
|
||||
base.ChemistryPoints = []string{"视角多样", "可互补决策", "扩展彼此舒适区"}
|
||||
base.Watchouts = []string{"误解成本高", "价值观冲突需早谈", "节奏长期错位"}
|
||||
base.CommTips = []string{"声明沟通通道", "复述再回应", "关键约定书面化"}
|
||||
base.ConflictTips = []string{"回到共同目标", "停火机制", "一次只谈一个议题"}
|
||||
base.IntimacyTips = []string{"定期同步", "空间与连接配额", "具体肯定"}
|
||||
base.GrowthTips = []string{"擅长/需要对照表", "月度复盘", "小步共同目标"}
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -145,319 +145,3 @@ func BuildFull(aBirth, bBirth time.Time, aTime, bTime, aPlace, bPlace *string, a
|
||||
}
|
||||
return Output{Summary: summary, Detail: detail}
|
||||
}
|
||||
|
||||
func relationTypePack(t, aName, bName string) (label, body string, bullets []string) {
|
||||
switch t {
|
||||
case "partner", "恋人", "伴侣":
|
||||
return "伴侣",
|
||||
fmt.Sprintf("%s与%s更适合把差异写成「亲密说明书」:欲望、节奏与安全感都说清楚。", aName, bName),
|
||||
[]string{"每周一次情绪复盘,不谈对错", "亲密请求用「我需要」句式", "边界:疲惫时先暂停再继续"}
|
||||
case "family", "家人", "父母", "亲子":
|
||||
return "家人",
|
||||
fmt.Sprintf("家人关系里,%s与%s容易把旧角色带进新对话。试着把对方当「现在的人」而不是旧剧本。", aName, bName),
|
||||
[]string{"少用「你总是」句式", "大事拆成可协商的小请求", "保留各自的私人空间"}
|
||||
case "friend", "朋友":
|
||||
return "朋友",
|
||||
fmt.Sprintf("友情里%s与%s可以更轻松地互补:约会期待与回应频率说开即可。", aName, bName),
|
||||
[]string{"约见用明确时间,减少猜测", "忙时用短消息保持连结", "冲突后用玩笑或直接道歉都行,别冷处理太久"}
|
||||
default:
|
||||
return "", "", nil
|
||||
}
|
||||
}
|
||||
|
||||
type dimScore struct {
|
||||
Score int
|
||||
Teaser string
|
||||
}
|
||||
|
||||
func dimMap(v any) map[string]dimScore {
|
||||
out := map[string]dimScore{}
|
||||
arr, ok := v.([]map[string]any)
|
||||
if !ok {
|
||||
// Build() uses []map[string]any — also tolerate []any
|
||||
raw, ok2 := v.([]any)
|
||||
if !ok2 {
|
||||
return out
|
||||
}
|
||||
for _, item := range raw {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key := str(m["key"])
|
||||
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for _, m := range arr {
|
||||
key := str(m["key"])
|
||||
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func asInt(v any) int {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case float64:
|
||||
return int(n)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func dimTitle(key string) string {
|
||||
switch key {
|
||||
case "personality":
|
||||
return "性格特点"
|
||||
case "communication":
|
||||
return "沟通方式"
|
||||
case "relation":
|
||||
return "关系模式"
|
||||
case "career":
|
||||
return "事业节奏"
|
||||
case "emotion":
|
||||
return "情绪调节"
|
||||
case "lifestyle":
|
||||
return "生活节奏"
|
||||
default:
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
func dimNote(key string, gap int, aName, bName string) string {
|
||||
if gap > 12 {
|
||||
return fmt.Sprintf("在「%s」上,%s 分值更高,适合由 %s 多给结构,%s 多给弹性。", dimTitle(key), aName, aName, bName)
|
||||
}
|
||||
if gap < -12 {
|
||||
return fmt.Sprintf("在「%s」上,%s 分值更高,相处时可多尊重 %s 的节奏。", dimTitle(key), bName, bName)
|
||||
}
|
||||
return fmt.Sprintf("在「%s」上双方接近,容易形成默契,也要防止都默认对方「应该懂」。", dimTitle(key))
|
||||
}
|
||||
|
||||
func firstTeaser(m map[string]dimScore, key string) string {
|
||||
if d, ok := m[key]; ok && d.Teaser != "" {
|
||||
return d.Teaser
|
||||
}
|
||||
return "表达方式各有节奏"
|
||||
}
|
||||
|
||||
func complementarityKey(a, b string) string {
|
||||
if a == b {
|
||||
return "same"
|
||||
}
|
||||
// simple buckets by trait family
|
||||
drive := map[string]string{
|
||||
"稳进探索者": "steady", "细腻分析者": "steady", "守护担当者": "steady", "洞察策略者": "steady",
|
||||
"敏锐连接者": "warm", "温和协调者": "warm", "热忱鼓舞者": "warm",
|
||||
"果断行动派": "drive", "自由创造者": "drive",
|
||||
}
|
||||
ak, bk := drive[a], drive[b]
|
||||
if ak == "" || bk == "" {
|
||||
return "mix"
|
||||
}
|
||||
if ak == bk {
|
||||
return "same_family"
|
||||
}
|
||||
if (ak == "steady" && bk == "warm") || (ak == "warm" && bk == "steady") {
|
||||
return "steady_warm"
|
||||
}
|
||||
if (ak == "drive" && bk == "steady") || (ak == "steady" && bk == "drive") {
|
||||
return "drive_steady"
|
||||
}
|
||||
if (ak == "drive" && bk == "warm") || (ak == "warm" && bk == "drive") {
|
||||
return "drive_warm"
|
||||
}
|
||||
return "mix"
|
||||
}
|
||||
|
||||
type compCopy struct {
|
||||
OneLiner, Overview, Chemistry, DeepOverview, CommDeep, ConflictDeep, IntimacyDeep, GrowthDeep string
|
||||
ChemistryPoints, Watchouts, CommTips, ConflictTips, IntimacyTips, GrowthTips, Weekly, Scripts []string
|
||||
}
|
||||
|
||||
func complementarityCopy(key, aName, bName, aLabel, bLabel string) compCopy {
|
||||
base := compCopy{
|
||||
OneLiner: fmt.Sprintf("%s偏「%s」,%s偏「%s」——差异可以写成相处说明书。", aName, aLabel, bName, bLabel),
|
||||
Overview: fmt.Sprintf("双方在表达、节奏与需求上并不相同。把差异看清楚,比急着证明「谁更对」更有用。下面从沟通、冲突、亲密与共同成长几个维度展开。"),
|
||||
Chemistry: "互补往往出现在:一方给结构,另一方给温度;或一方推进,另一方稳住质量。",
|
||||
Weekly: []string{
|
||||
"本周进行一次 20 分钟「非解决问题」闲聊或散步。",
|
||||
"各自写三件「我需要你这样支持我」的具体行为,互换阅读。",
|
||||
"约定一个冲突停火词,任一方说出即暂停 15 分钟。",
|
||||
},
|
||||
Scripts: []string{
|
||||
fmt.Sprintf("%s可以说:我需要先把事实说清楚,再谈感受。", aName),
|
||||
fmt.Sprintf("%s可以说:我希望你先听到我的感受,再给方案。", bName),
|
||||
"我们可以先复述对方一句,再表达自己的需要。",
|
||||
},
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "same":
|
||||
base.OneLiner = fmt.Sprintf("你们风格接近(都偏「%s」),默契来得快,也要防止一起陷入同样的盲区。", aLabel)
|
||||
base.Chemistry = "同类相吸:理解成本低,推进或回避也可能同步发生。"
|
||||
base.DeepOverview = "风格相近意味着你们很容易「懂对方在想什么」,但也可能同时逃避冲突,或同时过度冲刺。建议定期引入外部视角(朋友建议、清单复盘),打破镜像盲区。"
|
||||
base.CommDeep = "沟通效率高,但要刻意练习提出不同意见。安排「唱反调」轮值:每周一人专门提出风险点。"
|
||||
base.ConflictDeep = "冲突可能被快速和好掩盖,问题未真正处理。用「问题清单」追踪未完成议题。"
|
||||
base.IntimacyDeep = "熟悉感强,新鲜感需主动创造:共同学习或小旅行比重复日常更能充电。"
|
||||
base.GrowthDeep = "一起设定一个共同小目标,并互相做问责伙伴。"
|
||||
base.ChemistryPoints = []string{"理解成本低", "节奏容易对齐", "共同语言多"}
|
||||
base.Watchouts = []string{"共享同一盲区", "缺少外部校正", "意见过于一致缺少张力"}
|
||||
base.CommTips = []string{"鼓励提出异议", "重要决定写利弊表", "避免默认对方已懂"}
|
||||
base.ConflictTips = []string{"追踪未完成议题", "避免假性和好", "冷静后再做决定"}
|
||||
base.IntimacyTips = []string{"主动制造新鲜体验", "表达感谢要具体", "保留个人空间"}
|
||||
base.GrowthTips = []string{"共同目标 + 问责", "每月复盘一次关系", "引入可信第三方建议"}
|
||||
case "steady_warm":
|
||||
base.Chemistry = "稳与暖互补:一方提供结构与可靠,另一方提供连接与温度。"
|
||||
base.DeepOverview = fmt.Sprintf("%s与%s之间,最常见的张力是「要先讲清楚」还是「要先被看见」。若能轮流满足这两种需求,关系会既安全又有温度。", aName, bName)
|
||||
base.CommDeep = "沟通协议:情绪话题先共鸣 2 分钟,再进入事实与方案;事务话题先结论,再补感受。"
|
||||
base.ConflictDeep = "稳的一方别用沉默当结束;暖的一方别用追问升级压力。停火后用「我需要…」重开。"
|
||||
base.IntimacyDeep = "暖的一方需要回应频率;稳的一方需要可预期的独处。把两者写进约定。"
|
||||
base.GrowthDeep = "把互补写成分工:谁更擅长安抚,谁更擅长推进落地。"
|
||||
base.ChemistryPoints = []string{"结构 × 温度", "可靠 × 连接", "可形成完整支持系统"}
|
||||
base.Watchouts = []string{"一方觉得被冷落", "一方觉得被情绪淹没", "节奏错位积累委屈"}
|
||||
base.CommTips = []string{"情绪先共鸣再方案", "事务先结论再感受", "用文字确认关键约定"}
|
||||
base.ConflictTips = []string{"禁止用沉默结束话题", "追问前先问是否方便", "停火词机制"}
|
||||
base.IntimacyTips = []string{"约定回应窗口", "尊重独处不被解读为冷淡", "每周一次深度连接"}
|
||||
base.GrowthTips = []string{"按优势分工", "互相学习对方语言", "月度关系复盘"}
|
||||
case "drive_steady":
|
||||
base.Chemistry = "推与稳互补:一方破局加速,另一方把关质量与可持续。"
|
||||
base.DeepOverview = "行动派容易嫌分析派慢;稳健派容易嫌行动派莽。把「速度」用在试验,「稳健」用在关键承诺,冲突会下降。"
|
||||
base.CommDeep = "行动方给时间盒与最小方案;稳健方在时限内给风险清单,而不是无限延期。"
|
||||
base.ConflictDeep = "冲突焦点常是节奏。先对齐「这是可逆试验还是重大决定」,再选速度。"
|
||||
base.IntimacyDeep = "行动方用陪伴质量弥补碎片时间;稳健方减少用担忧浇灭热情,改用「我支持你试,我们设检查点」。"
|
||||
base.GrowthDeep = "共同项目里明确角色:谁启动、谁验收、何时复盘。"
|
||||
base.ChemistryPoints = []string{"破局 × 把关", "速度 × 质量", "试验与承诺可分工"}
|
||||
base.Watchouts = []string{"节奏互斥", "一方压抑热情", "一方焦虑失控"}
|
||||
base.CommTips = []string{"先定义可逆/不可逆", "时间盒决策", "风险清单限时"}
|
||||
base.ConflictTips = []string{"争论节奏前先分类问题", "避免人格化指责", "用检查点代替否决"}
|
||||
base.IntimacyTips = []string{"质量陪伴", "支持试验+检查点", "庆祝小进展"}
|
||||
base.GrowthTips = []string{"项目角色清晰", "复盘节奏", "互相翻译动机"}
|
||||
case "drive_warm":
|
||||
base.Chemistry = "驱动与连接互补:一方带节奏,另一方维系人心与氛围。"
|
||||
base.DeepOverview = "热情与效率碰到一起很有火花,也容易在「推进」与「照顾感受」之间拉扯。约定场景切换:冲刺模式 / 连接模式。"
|
||||
base.CommDeep = "冲刺时短讯同步进度;连接时关掉任务话题。不要用效率语言处理情绪时刻。"
|
||||
base.ConflictDeep = "驱动方避免「你想太多」;连接方避免「你只在乎结果」。改说具体需求。"
|
||||
base.IntimacyDeep = "用共同体验(运动、活动)同时满足推进感与连接感。"
|
||||
base.GrowthDeep = "轮流做「本周关系主理人」,负责安排一次连接或一次共同目标。"
|
||||
base.ChemistryPoints = []string{"推进力 × 氛围", "行动号召力强", "共同体验易充电"}
|
||||
base.Watchouts = []string{"情绪被效率压过", "承诺过多难兑现", "连接变任务化"}
|
||||
base.CommTips = []string{"模式切换:冲刺/连接", "情绪时刻禁用效率话术", "进度短讯化"}
|
||||
base.ConflictTips = []string{"禁止否定感受", "需求具体化", "修复后再推进"}
|
||||
base.IntimacyTips = []string{"共同体验", "兑现小承诺", "非任务陪伴"}
|
||||
base.GrowthTips = []string{"轮值关系主理人", "控制并行承诺", "庆祝与复盘并重"}
|
||||
case "same_family":
|
||||
base.DeepOverview = "你们属于相近气质族,容易互相理解,也要主动制造一点建设性差异,避免舒适区停滞。"
|
||||
base.CommDeep = "沟通顺畅时更要确认细节,防止「好像说好了」其实理解不同。"
|
||||
base.ConflictDeep = "冲突可能被淡化。强制做一次「最担心的三件事」互换。"
|
||||
base.IntimacyDeep = "在舒适之外增加挑战性共同任务,刷新关系动能。"
|
||||
base.GrowthDeep = "互相指出对方一个盲区,并约定本月各改一项小行为。"
|
||||
base.ChemistryPoints = []string{"气质相近", "理解门槛低", "协作起步快"}
|
||||
base.Watchouts = []string{"舒适区停滞", "细节默认错误", "回避尖锐议题"}
|
||||
base.CommTips = []string{"确认细节", "书面关键约定", "鼓励异议"}
|
||||
base.ConflictTips = []string{"互换担忧清单", "不假性和好", "设讨论截止"}
|
||||
base.IntimacyTips = []string{"共同挑战任务", "新鲜体验", "具体感谢"}
|
||||
base.GrowthTips = []string{"互指一个盲区", "月改一小行为", "外部输入"}
|
||||
default:
|
||||
base.DeepOverview = fmt.Sprintf("%s与%s风格路径不同,说明书价值更高。先承认差异合法,再谈协作规则。", aName, bName)
|
||||
base.CommDeep = "建立双通道:事实通道与感受通道,讨论前先声明走哪一条。"
|
||||
base.ConflictDeep = "冲突时回到共同目标句:我们都希望关系更好/事情做成。"
|
||||
base.IntimacyDeep = "用定期同步取代猜测;空间与连接都要有配额。"
|
||||
base.GrowthDeep = "把差异写成「我擅长 / 我需要」对照表,贴在看得见的地方。"
|
||||
base.ChemistryPoints = []string{"视角多样", "可互补决策", "扩展彼此舒适区"}
|
||||
base.Watchouts = []string{"误解成本高", "价值观冲突需早谈", "节奏长期错位"}
|
||||
base.CommTips = []string{"声明沟通通道", "复述再回应", "关键约定书面化"}
|
||||
base.ConflictTips = []string{"回到共同目标", "停火机制", "一次只谈一个议题"}
|
||||
base.IntimacyTips = []string{"定期同步", "空间与连接配额", "具体肯定"}
|
||||
base.GrowthTips = []string{"擅长/需要对照表", "月度复盘", "小步共同目标"}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func harmonyIndex(dims []map[string]any) int {
|
||||
if len(dims) == 0 {
|
||||
return 72
|
||||
}
|
||||
sum := 0
|
||||
for _, d := range dims {
|
||||
gap := asInt(d["gap"])
|
||||
sum += 100 - gap*4
|
||||
}
|
||||
avg := sum / len(dims)
|
||||
if avg < 45 {
|
||||
return 45
|
||||
}
|
||||
if avg > 96 {
|
||||
return 96
|
||||
}
|
||||
return avg
|
||||
}
|
||||
|
||||
func fitFromHarmony(score int, aLabel, bLabel string) (string, []string) {
|
||||
switch {
|
||||
case score >= 82:
|
||||
return "默契互补型", []string{
|
||||
fmt.Sprintf("%s与%s节奏接近,适合共同推进小事。", aLabel, bLabel),
|
||||
"把欣赏说出口,默契会更稳。",
|
||||
"每周留一次轻松同步,不必每次谈大事。",
|
||||
}
|
||||
case score >= 68:
|
||||
return "磨合成长型", []string{
|
||||
"差异可见,正好写成相处说明书。",
|
||||
"冲突时先复述再提方案。",
|
||||
"共同目标写清楚,减少猜忌。",
|
||||
}
|
||||
default:
|
||||
return "反差探索型", []string{
|
||||
"反差大不等于不合,关键是边界与节奏。",
|
||||
"重要约定尽量具体、可检查。",
|
||||
"给彼此独处充电的空间。",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func starPairNote(a, b string) string {
|
||||
if a == b {
|
||||
return fmt.Sprintf("同为%s:容易共鸣,也要避免同质盲区。", a)
|
||||
}
|
||||
return fmt.Sprintf("%s × %s:节奏不同,适合「我负责启动 / 你负责收尾」式分工。", a, b)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package relation
|
||||
|
||||
import "fmt"
|
||||
|
||||
func relationTypePack(t, aName, bName string) (label, body string, bullets []string) {
|
||||
switch t {
|
||||
case "partner", "恋人", "伴侣":
|
||||
return "伴侣",
|
||||
fmt.Sprintf("%s与%s更适合把差异写成「亲密说明书」:欲望、节奏与安全感都说清楚。", aName, bName),
|
||||
[]string{"每周一次情绪复盘,不谈对错", "亲密请求用「我需要」句式", "边界:疲惫时先暂停再继续"}
|
||||
case "family", "家人", "父母", "亲子":
|
||||
return "家人",
|
||||
fmt.Sprintf("家人关系里,%s与%s容易把旧角色带进新对话。试着把对方当「现在的人」而不是旧剧本。", aName, bName),
|
||||
[]string{"少用「你总是」句式", "大事拆成可协商的小请求", "保留各自的私人空间"}
|
||||
case "friend", "朋友":
|
||||
return "朋友",
|
||||
fmt.Sprintf("友情里%s与%s可以更轻松地互补:约会期待与回应频率说开即可。", aName, bName),
|
||||
[]string{"约见用明确时间,减少猜测", "忙时用短消息保持连结", "冲突后用玩笑或直接道歉都行,别冷处理太久"}
|
||||
default:
|
||||
return "", "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func starPairNote(a, b string) string {
|
||||
if a == b {
|
||||
return fmt.Sprintf("同为%s:容易共鸣,也要避免同质盲区。", a)
|
||||
}
|
||||
return fmt.Sprintf("%s × %s:节奏不同,适合「我负责启动 / 你负责收尾」式分工。", a, b)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package relation
|
||||
|
||||
import "fmt"
|
||||
|
||||
type dimScore struct {
|
||||
Score int
|
||||
Teaser string
|
||||
}
|
||||
|
||||
func dimMap(v any) map[string]dimScore {
|
||||
out := map[string]dimScore{}
|
||||
arr, ok := v.([]map[string]any)
|
||||
if !ok {
|
||||
// Build() uses []map[string]any — also tolerate []any
|
||||
raw, ok2 := v.([]any)
|
||||
if !ok2 {
|
||||
return out
|
||||
}
|
||||
for _, item := range raw {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key := str(m["key"])
|
||||
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for _, m := range arr {
|
||||
key := str(m["key"])
|
||||
out[key] = dimScore{Score: asInt(m["score"]), Teaser: str(m["teaser"])}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func asInt(v any) int {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case float64:
|
||||
return int(n)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func dimTitle(key string) string {
|
||||
switch key {
|
||||
case "personality":
|
||||
return "性格特点"
|
||||
case "communication":
|
||||
return "沟通方式"
|
||||
case "relation":
|
||||
return "关系模式"
|
||||
case "career":
|
||||
return "事业节奏"
|
||||
case "emotion":
|
||||
return "情绪调节"
|
||||
case "lifestyle":
|
||||
return "生活节奏"
|
||||
default:
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
func dimNote(key string, gap int, aName, bName string) string {
|
||||
if gap > 12 {
|
||||
return fmt.Sprintf("在「%s」上,%s 分值更高,适合由 %s 多给结构,%s 多给弹性。", dimTitle(key), aName, aName, bName)
|
||||
}
|
||||
if gap < -12 {
|
||||
return fmt.Sprintf("在「%s」上,%s 分值更高,相处时可多尊重 %s 的节奏。", dimTitle(key), bName, bName)
|
||||
}
|
||||
return fmt.Sprintf("在「%s」上双方接近,容易形成默契,也要防止都默认对方「应该懂」。", dimTitle(key))
|
||||
}
|
||||
|
||||
func firstTeaser(m map[string]dimScore, key string) string {
|
||||
if d, ok := m[key]; ok && d.Teaser != "" {
|
||||
return d.Teaser
|
||||
}
|
||||
return "表达方式各有节奏"
|
||||
}
|
||||
|
||||
func harmonyIndex(dims []map[string]any) int {
|
||||
if len(dims) == 0 {
|
||||
return 72
|
||||
}
|
||||
sum := 0
|
||||
for _, d := range dims {
|
||||
gap := asInt(d["gap"])
|
||||
sum += 100 - gap*4
|
||||
}
|
||||
avg := sum / len(dims)
|
||||
if avg < 45 {
|
||||
return 45
|
||||
}
|
||||
if avg > 96 {
|
||||
return 96
|
||||
}
|
||||
return avg
|
||||
}
|
||||
|
||||
func fitFromHarmony(score int, aLabel, bLabel string) (string, []string) {
|
||||
switch {
|
||||
case score >= 82:
|
||||
return "默契互补型", []string{
|
||||
fmt.Sprintf("%s与%s节奏接近,适合共同推进小事。", aLabel, bLabel),
|
||||
"把欣赏说出口,默契会更稳。",
|
||||
"每周留一次轻松同步,不必每次谈大事。",
|
||||
}
|
||||
case score >= 68:
|
||||
return "磨合成长型", []string{
|
||||
"差异可见,正好写成相处说明书。",
|
||||
"冲突时先复述再提方案。",
|
||||
"共同目标写清楚,减少猜忌。",
|
||||
}
|
||||
default:
|
||||
return "反差探索型", []string{
|
||||
"反差大不等于不合,关键是边界与节奏。",
|
||||
"重要约定尽量具体、可检查。",
|
||||
"给彼此独处充电的空间。",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Package membership handles growth membership status and mock commerce orders.
|
||||
package membership
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
)
|
||||
|
||||
// Service is membership + order use-cases (extracted from report service).
|
||||
type Service struct {
|
||||
Reports *repository.ReportRepo
|
||||
}
|
||||
|
||||
// CreateOrderInput for commerce.
|
||||
type CreateOrderInput struct {
|
||||
Kind string
|
||||
Plan string
|
||||
ReportID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateOrder starts membership or deep_access order.
|
||||
func (s *Service) CreateOrder(ctx context.Context, userID uuid.UUID, in CreateOrderInput) (uuid.UUID, error) {
|
||||
if in.Kind != "membership" && in.Kind != "deep_access" {
|
||||
return uuid.Nil, errors.New("invalid kind")
|
||||
}
|
||||
if in.Kind == "deep_access" && in.ReportID == nil {
|
||||
return uuid.Nil, errors.New("report_id required")
|
||||
}
|
||||
amount := 990
|
||||
if in.Kind == "membership" {
|
||||
amount = 2500
|
||||
}
|
||||
return s.Reports.CreateOrder(ctx, userID, in.Kind, in.Plan, in.ReportID, amount)
|
||||
}
|
||||
|
||||
// PayMock completes mock payment.
|
||||
func (s *Service) PayMock(ctx context.Context, userID, orderID uuid.UUID) error {
|
||||
return s.Reports.PayMock(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
// Me is the public membership snapshot.
|
||||
type Me struct {
|
||||
Active bool `json:"active"`
|
||||
Plan string `json:"plan,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
AskQuotaLeft int `json:"ask_quota_left,omitempty"`
|
||||
}
|
||||
|
||||
// Get returns current growth membership for the user.
|
||||
func (s *Service) Get(ctx context.Context, userID uuid.UUID) (*Me, error) {
|
||||
row, err := s.Reports.GetMembership(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Me{
|
||||
Active: row.Active,
|
||||
Plan: row.Plan,
|
||||
Status: row.Status,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
AskQuotaLeft: row.AskQuotaLeft,
|
||||
}, nil
|
||||
}
|
||||
@@ -257,54 +257,3 @@ func (s *Service) applyEntitlement(ctx context.Context, userID uuid.UUID, rep *m
|
||||
}
|
||||
return rep, nil
|
||||
}
|
||||
|
||||
// CreateOrderInput for commerce.
|
||||
type CreateOrderInput struct {
|
||||
Kind string
|
||||
Plan string
|
||||
ReportID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateOrder starts membership or deep_access order.
|
||||
func (s *Service) CreateOrder(ctx context.Context, userID uuid.UUID, in CreateOrderInput) (uuid.UUID, error) {
|
||||
if in.Kind != "membership" && in.Kind != "deep_access" {
|
||||
return uuid.Nil, errors.New("invalid kind")
|
||||
}
|
||||
if in.Kind == "deep_access" && in.ReportID == nil {
|
||||
return uuid.Nil, errors.New("report_id required")
|
||||
}
|
||||
amount := 990
|
||||
if in.Kind == "membership" {
|
||||
amount = 2500
|
||||
}
|
||||
return s.Reports.CreateOrder(ctx, userID, in.Kind, in.Plan, in.ReportID, amount)
|
||||
}
|
||||
|
||||
// PayMock completes mock payment.
|
||||
func (s *Service) PayMock(ctx context.Context, userID, orderID uuid.UUID) error {
|
||||
return s.Reports.PayMock(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
// MembershipMe is the public membership snapshot.
|
||||
type MembershipMe struct {
|
||||
Active bool `json:"active"`
|
||||
Plan string `json:"plan,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
AskQuotaLeft int `json:"ask_quota_left,omitempty"`
|
||||
}
|
||||
|
||||
// GetMembership returns current growth membership for the user.
|
||||
func (s *Service) GetMembership(ctx context.Context, userID uuid.UUID) (*MembershipMe, error) {
|
||||
row, err := s.Reports.GetMembership(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MembershipMe{
|
||||
Active: row.Active,
|
||||
Plan: row.Plan,
|
||||
Status: row.Status,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
AskQuotaLeft: row.AskQuotaLeft,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Package star builds 星座 reports (natal chart · fortune · deep copy).
|
||||
// Package star builds 星座 reports (natal chart · period outlook · deep copy).
|
||||
package star
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/fortune"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/outlook"
|
||||
)
|
||||
|
||||
// Output is free summary + gated detail.
|
||||
@@ -21,7 +21,7 @@ type BuildOpts struct {
|
||||
BirthTime *string
|
||||
BirthPlace *string
|
||||
Name string
|
||||
AsOf time.Time // fortune anchor; zero = now
|
||||
AsOf time.Time // period outlook anchor; zero = now
|
||||
}
|
||||
|
||||
// Build generates StarProfile from birth date (compat wrapper).
|
||||
@@ -68,7 +68,7 @@ func BuildWith(opts BuildOpts) (Output, error) {
|
||||
if asOf.IsZero() {
|
||||
asOf = time.Now()
|
||||
}
|
||||
fort := fortune.Build(chart, asOf)
|
||||
fort := outlook.Build(chart, asOf)
|
||||
daily := fort.Daily
|
||||
|
||||
planetsOut := make([]map[string]any, 0, len(chart.Planets))
|
||||
@@ -114,8 +114,10 @@ func BuildWith(opts BuildOpts) (Output, error) {
|
||||
},
|
||||
"planets": planetsOut,
|
||||
"aspects_preview": aspectPreview,
|
||||
"fortune": fort.AsMap(),
|
||||
"transits": fort.AsMap()["transits"],
|
||||
// "fortune" kept for client compat; prefer "outlook" (ECR-002).
|
||||
"fortune": fort.AsMap(),
|
||||
"outlook": fort.AsMap(),
|
||||
"transits": fort.AsMap()["transits"],
|
||||
"daily_soft": map[string]any{
|
||||
"title": daily.Title, "focus": daily.Focus, "tip": daily.Tip,
|
||||
"energy": daily.Score, "note": daily.Label, "score": daily.Score, "label": daily.Label,
|
||||
@@ -169,169 +171,8 @@ func BuildWith(opts BuildOpts) (Output, error) {
|
||||
"behavior_pattern": pack.SunDeep,
|
||||
"relation_style": pack.RelationDeep,
|
||||
"growth_direction": pack.GrowthDeep,
|
||||
"fortune_detail": fort.AsMap(),
|
||||
"fortune_detail": fort.AsMap(), // compat
|
||||
"outlook_detail": fort.AsMap(),
|
||||
}
|
||||
return Output{Summary: summary, Detail: detail}, nil
|
||||
}
|
||||
|
||||
func signIndexOf(key string) int {
|
||||
for i, s := range []string{
|
||||
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||
} {
|
||||
if s == key {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func planetBullets(chart natal.Chart) []string {
|
||||
out := make([]string, 0, 6)
|
||||
for _, key := range []string{"mercury", "venus", "mars", "jupiter", "saturn"} {
|
||||
for _, p := range chart.Planets {
|
||||
if p.Key == key {
|
||||
out = append(out, fmt.Sprintf("%s在%s第%d宫", p.Title, p.Sign, p.House))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func signCard(key, title string, b natal.Body, teaser string, keywords []string) map[string]any {
|
||||
ks := keywords
|
||||
if len(ks) > 3 {
|
||||
ks = ks[:3]
|
||||
}
|
||||
return map[string]any{
|
||||
"key": key, "title": title, "label": b.Sign,
|
||||
"element": b.Element, "modality": b.Modality,
|
||||
"teaser": teaser, "keywords": ks,
|
||||
}
|
||||
}
|
||||
|
||||
func section(title, body string, bullets []string) map[string]any {
|
||||
return map[string]any{"title": title, "body": body, "bullets": bullets}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SignLabels returns sun/moon/rise labels (uses natal engine).
|
||||
func SignLabels(birth time.Time, birthTime *string) (sun, moon, rise string) {
|
||||
return SignLabelsPlace(birth, birthTime, nil)
|
||||
}
|
||||
|
||||
// SignLabelsPlace includes birth place for rising accuracy.
|
||||
func SignLabelsPlace(birth time.Time, birthTime, place *string) (sun, moon, rise string) {
|
||||
c, err := natal.Compute(birth, birthTime, place)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
return c.Sun.Sign, c.Moon.Sign, c.Rise.Sign
|
||||
}
|
||||
|
||||
// NatalChart exposes chart for relation/synastry.
|
||||
func NatalChart(birth time.Time, birthTime, place *string) (natal.Chart, error) {
|
||||
return natal.Compute(birth, birthTime, place)
|
||||
}
|
||||
|
||||
type signMeta struct {
|
||||
Key, Label, Element, Modality string
|
||||
}
|
||||
|
||||
type pack struct {
|
||||
Label, OneLiner, Overview, LifeTip string
|
||||
Keywords []string
|
||||
Scores map[string]int
|
||||
SunTeaser, RelationTeaser, CareerTeaser, GrowthTeaser string
|
||||
SunDeep, MoonDeep, RiseDeep, RelationDeep, CareerDeep, GrowthDeep string
|
||||
SunBullets, MoonBullets, RiseBullets, RelationBullets, CareerBullets, GrowthBullets []string
|
||||
Strengths, BlindSpots, Scripts []string
|
||||
PlanWeek, PlanMonth, PlanLong string
|
||||
FAQ []map[string]string
|
||||
}
|
||||
|
||||
var packs = map[string]pack{}
|
||||
|
||||
func init() {
|
||||
for _, s := range []signMeta{
|
||||
{"aries", "白羊", "火", "开创"}, {"taurus", "金牛", "土", "固定"}, {"gemini", "双子", "风", "变动"},
|
||||
{"cancer", "巨蟹", "水", "开创"}, {"leo", "狮子", "火", "固定"}, {"virgo", "处女", "土", "变动"},
|
||||
{"libra", "天秤", "风", "开创"}, {"scorpio", "天蝎", "水", "固定"}, {"sagittarius", "射手", "火", "变动"},
|
||||
{"capricorn", "摩羯", "土", "开创"}, {"aquarius", "水瓶", "风", "固定"}, {"pisces", "双鱼", "水", "变动"},
|
||||
} {
|
||||
packs[s.Key] = defaultPack(s)
|
||||
}
|
||||
packs["aries"] = enrich(packs["aries"], "开创行动者", "先动起来,再在行动里想清楚。",
|
||||
"你容易被新目标点燃,讨厌拖沓。优势是启动快;需要留意的是收尾与倾听。")
|
||||
packs["taurus"] = enrich(packs["taurus"], "稳健沉淀者", "你重视踏实与感官舒适,变化太快会消耗你。",
|
||||
"你擅长把事情做稳做久。关系与工作里都需要可预期的节奏。")
|
||||
packs["gemini"] = enrich(packs["gemini"], "灵活连接者", "你靠好奇与对话充电,也容易分心。",
|
||||
"信息与交流是你的养分。把想法收成一个可交付的小闭环,会更有成就感。")
|
||||
packs["cancer"] = enrich(packs["cancer"], "细腻守护者", "你对情绪与归属很敏感,安全比热闹更重要。",
|
||||
"你擅长照顾氛围与关系。记得也把自己的需要说清楚,而不是只默默撑着。")
|
||||
packs["leo"] = enrich(packs["leo"], "热烈表达者", "你需要被看见,也愿意照亮别人。",
|
||||
"热情与表达是你的名片。把认可需求说成具体请求,关系会更顺。")
|
||||
packs["virgo"] = enrich(packs["virgo"], "细致完善者", "你看见细节与改进空间,也容易自我要求过高。",
|
||||
"把「足够好」纳入标准,你会轻松很多,交付也会更快。")
|
||||
packs["libra"] = enrich(packs["libra"], "平衡协调者", "你追求公平与和谐,有时会为难自己。",
|
||||
"协调是天赋。重要决定里请给自己一票,而不只是各方折中。")
|
||||
packs["scorpio"] = enrich(packs["scorpio"], "深潜洞察者", "你看重真诚与深度,讨厌浮于表面。",
|
||||
"信任慢、一旦建立则很深。练习用语言同步感受,减少猜疑消耗。")
|
||||
packs["sagittarius"] = enrich(packs["sagittarius"], "开阔探索者", "你需要视野与意义,讨厌被框死。",
|
||||
"探索欲强。给自由一点结构,热情才能变成持续作品。")
|
||||
packs["capricorn"] = enrich(packs["capricorn"], "负责攀登者", "你看长远目标,愿意为结果负责。",
|
||||
"担当是优势。学会求助与休息,攀登才可持续。")
|
||||
packs["aquarius"] = enrich(packs["aquarius"], "独特思考者", "你重视独立与新意,也需要被理解。",
|
||||
"独特视角是礼物。把想法翻译成别人跟得上的一步行动。")
|
||||
packs["pisces"] = enrich(packs["pisces"], "共感想象者", "你感受力强,边界容易被情绪浪潮冲开。",
|
||||
"共情是天赋。区分「理解」与「承包」,你会更稳。")
|
||||
}
|
||||
|
||||
func defaultPack(s signMeta) pack {
|
||||
return pack{
|
||||
Label: s.Label + "风格探索者", OneLiner: "用星座认识自己的节奏与运势起伏。",
|
||||
Overview: "在星座框架里,你的表达与需求有独特侧重。",
|
||||
LifeTip: "本周选一件小事完整做完,并告诉亲近的人你的真实需要。",
|
||||
Keywords: []string{s.Label, s.Element + "象", s.Modality, "星座"},
|
||||
Scores: map[string]int{"sun": 78, "moon": 70, "rise": 72, "relation": 74, "career": 73, "growth": 75},
|
||||
SunTeaser: "核心驱动力清晰,行动有自己的节拍。", RelationTeaser: "关系里需要被理解与尊重节奏。",
|
||||
CareerTeaser: "适合发挥你风格优势的场景。", GrowthTeaser: "下一步是看见盲区并小步调整。",
|
||||
SunDeep: "太阳星座描述你的核心动机与自我表达。",
|
||||
MoonDeep: "月亮星座指向情绪调节与安全感来源。",
|
||||
RiseDeep: "上升星座影响别人对你的第一印象。",
|
||||
RelationDeep: "关系中把需求说具体,比期待对方「应该懂」更有效。",
|
||||
CareerDeep: "工作上优先发挥你的风格优势,并用小里程碑对抗拖延。",
|
||||
GrowthDeep: "成长是认识模式后做可验证的小调整。",
|
||||
SunBullets: []string{"核心动机可被命名", "表达有风格偏好", "适合自我探索"},
|
||||
MoonBullets: []string{"情绪需要出口", "安全感来源因人而异", "独处或连接可充电"},
|
||||
RiseBullets: []string{"第一印象可调节", "外显≠全部自我", "可练习温和表达"},
|
||||
RelationBullets: []string{"需求具体化", "尊重双方节奏", "冲突先复述再方案"},
|
||||
CareerBullets: []string{"发挥风格优势", "小步交付", "复盘节奏"},
|
||||
GrowthBullets: []string{"看见模式", "小步验证", "结合运势调整"},
|
||||
Strengths: []string{"风格清晰", "可探索性强", "利于自我对话"},
|
||||
BlindSpots: []string{"标签固化", "忽略情境差异", "过度解读"},
|
||||
Scripts: []string{"我想先说清我的节奏,再听你的。", "我不是冷淡,我需要一点整理时间。", "我们共同目标是……,下一步只定一件事。"},
|
||||
PlanWeek: "用三句话写下:我的优势 / 我的消耗点 / 我本周要试的一小步。",
|
||||
PlanMonth: "在关系或工作中练习两次「先复述对方,再提需要」。",
|
||||
PlanLong: "建立个人节奏手册:什么充电、什么耗电、如何请求支持。",
|
||||
FAQ: []map[string]string{
|
||||
{"q": "运势是预测吗?", "a": "运势分与建议帮助你调整节奏与决策,请结合现实判断,勿作唯一依据。"},
|
||||
{"q": "星盘准吗?", "a": "出生时与出生地越完整,上升与宫位越贴近;算法为可复现近似星历。"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func enrich(base pack, label, one, overview string) pack {
|
||||
base.Label = label
|
||||
base.OneLiner = one
|
||||
base.Overview = overview
|
||||
base.Keywords = []string{label, "星座", "运势"}
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package star
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/natal"
|
||||
)
|
||||
|
||||
func signIndexOf(key string) int {
|
||||
for i, s := range []string{
|
||||
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||
} {
|
||||
if s == key {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func planetBullets(chart natal.Chart) []string {
|
||||
out := make([]string, 0, 6)
|
||||
for _, key := range []string{"mercury", "venus", "mars", "jupiter", "saturn"} {
|
||||
for _, p := range chart.Planets {
|
||||
if p.Key == key {
|
||||
out = append(out, fmt.Sprintf("%s在%s第%d宫", p.Title, p.Sign, p.House))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func signCard(key, title string, b natal.Body, teaser string, keywords []string) map[string]any {
|
||||
ks := keywords
|
||||
if len(ks) > 3 {
|
||||
ks = ks[:3]
|
||||
}
|
||||
return map[string]any{
|
||||
"key": key, "title": title, "label": b.Sign,
|
||||
"element": b.Element, "modality": b.Modality,
|
||||
"teaser": teaser, "keywords": ks,
|
||||
}
|
||||
}
|
||||
|
||||
func section(title, body string, bullets []string) map[string]any {
|
||||
return map[string]any{"title": title, "body": body, "bullets": bullets}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SignLabels returns sun/moon/rise labels (uses natal engine).
|
||||
func SignLabels(birth time.Time, birthTime *string) (sun, moon, rise string) {
|
||||
return SignLabelsPlace(birth, birthTime, nil)
|
||||
}
|
||||
|
||||
// SignLabelsPlace includes birth place for rising accuracy.
|
||||
func SignLabelsPlace(birth time.Time, birthTime, place *string) (sun, moon, rise string) {
|
||||
c, err := natal.Compute(birth, birthTime, place)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
return c.Sun.Sign, c.Moon.Sign, c.Rise.Sign
|
||||
}
|
||||
|
||||
// NatalChart exposes chart for relation/synastry.
|
||||
func NatalChart(birth time.Time, birthTime, place *string) (natal.Chart, error) {
|
||||
return natal.Compute(birth, birthTime, place)
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
// Package fortune synthesizes daily/weekly/monthly/yearly/lifetime scores and transits.
|
||||
package fortune
|
||||
// Package outlook synthesizes daily/weekly/monthly/yearly/lifetime scores and transits.
|
||||
package outlook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fortune
|
||||
package outlook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -0,0 +1,96 @@
|
||||
package star
|
||||
|
||||
type signMeta struct {
|
||||
Key, Label, Element, Modality string
|
||||
}
|
||||
|
||||
type pack struct {
|
||||
Label, OneLiner, Overview, LifeTip string
|
||||
Keywords []string
|
||||
Scores map[string]int
|
||||
SunTeaser, RelationTeaser, CareerTeaser, GrowthTeaser string
|
||||
SunDeep, MoonDeep, RiseDeep, RelationDeep, CareerDeep, GrowthDeep string
|
||||
SunBullets, MoonBullets, RiseBullets, RelationBullets, CareerBullets, GrowthBullets []string
|
||||
Strengths, BlindSpots, Scripts []string
|
||||
PlanWeek, PlanMonth, PlanLong string
|
||||
FAQ []map[string]string
|
||||
}
|
||||
|
||||
var packs = map[string]pack{}
|
||||
|
||||
func init() {
|
||||
for _, s := range []signMeta{
|
||||
{"aries", "白羊", "火", "开创"}, {"taurus", "金牛", "土", "固定"}, {"gemini", "双子", "风", "变动"},
|
||||
{"cancer", "巨蟹", "水", "开创"}, {"leo", "狮子", "火", "固定"}, {"virgo", "处女", "土", "变动"},
|
||||
{"libra", "天秤", "风", "开创"}, {"scorpio", "天蝎", "水", "固定"}, {"sagittarius", "射手", "火", "变动"},
|
||||
{"capricorn", "摩羯", "土", "开创"}, {"aquarius", "水瓶", "风", "固定"}, {"pisces", "双鱼", "水", "变动"},
|
||||
} {
|
||||
packs[s.Key] = defaultPack(s)
|
||||
}
|
||||
packs["aries"] = enrich(packs["aries"], "开创行动者", "先动起来,再在行动里想清楚。",
|
||||
"你容易被新目标点燃,讨厌拖沓。优势是启动快;需要留意的是收尾与倾听。")
|
||||
packs["taurus"] = enrich(packs["taurus"], "稳健沉淀者", "你重视踏实与感官舒适,变化太快会消耗你。",
|
||||
"你擅长把事情做稳做久。关系与工作里都需要可预期的节奏。")
|
||||
packs["gemini"] = enrich(packs["gemini"], "灵活连接者", "你靠好奇与对话充电,也容易分心。",
|
||||
"信息与交流是你的养分。把想法收成一个可交付的小闭环,会更有成就感。")
|
||||
packs["cancer"] = enrich(packs["cancer"], "细腻守护者", "你对情绪与归属很敏感,安全比热闹更重要。",
|
||||
"你擅长照顾氛围与关系。记得也把自己的需要说清楚,而不是只默默撑着。")
|
||||
packs["leo"] = enrich(packs["leo"], "热烈表达者", "你需要被看见,也愿意照亮别人。",
|
||||
"热情与表达是你的名片。把认可需求说成具体请求,关系会更顺。")
|
||||
packs["virgo"] = enrich(packs["virgo"], "细致完善者", "你看见细节与改进空间,也容易自我要求过高。",
|
||||
"把「足够好」纳入标准,你会轻松很多,交付也会更快。")
|
||||
packs["libra"] = enrich(packs["libra"], "平衡协调者", "你追求公平与和谐,有时会为难自己。",
|
||||
"协调是天赋。重要决定里请给自己一票,而不只是各方折中。")
|
||||
packs["scorpio"] = enrich(packs["scorpio"], "深潜洞察者", "你看重真诚与深度,讨厌浮于表面。",
|
||||
"信任慢、一旦建立则很深。练习用语言同步感受,减少猜疑消耗。")
|
||||
packs["sagittarius"] = enrich(packs["sagittarius"], "开阔探索者", "你需要视野与意义,讨厌被框死。",
|
||||
"探索欲强。给自由一点结构,热情才能变成持续作品。")
|
||||
packs["capricorn"] = enrich(packs["capricorn"], "负责攀登者", "你看长远目标,愿意为结果负责。",
|
||||
"担当是优势。学会求助与休息,攀登才可持续。")
|
||||
packs["aquarius"] = enrich(packs["aquarius"], "独特思考者", "你重视独立与新意,也需要被理解。",
|
||||
"独特视角是礼物。把想法翻译成别人跟得上的一步行动。")
|
||||
packs["pisces"] = enrich(packs["pisces"], "共感想象者", "你感受力强,边界容易被情绪浪潮冲开。",
|
||||
"共情是天赋。区分「理解」与「承包」,你会更稳。")
|
||||
}
|
||||
|
||||
func defaultPack(s signMeta) pack {
|
||||
return pack{
|
||||
Label: s.Label + "风格探索者", OneLiner: "用星座认识自己的节奏与运势起伏。",
|
||||
Overview: "在星座框架里,你的表达与需求有独特侧重。",
|
||||
LifeTip: "本周选一件小事完整做完,并告诉亲近的人你的真实需要。",
|
||||
Keywords: []string{s.Label, s.Element + "象", s.Modality, "星座"},
|
||||
Scores: map[string]int{"sun": 78, "moon": 70, "rise": 72, "relation": 74, "career": 73, "growth": 75},
|
||||
SunTeaser: "核心驱动力清晰,行动有自己的节拍。", RelationTeaser: "关系里需要被理解与尊重节奏。",
|
||||
CareerTeaser: "适合发挥你风格优势的场景。", GrowthTeaser: "下一步是看见盲区并小步调整。",
|
||||
SunDeep: "太阳星座描述你的核心动机与自我表达。",
|
||||
MoonDeep: "月亮星座指向情绪调节与安全感来源。",
|
||||
RiseDeep: "上升星座影响别人对你的第一印象。",
|
||||
RelationDeep: "关系中把需求说具体,比期待对方「应该懂」更有效。",
|
||||
CareerDeep: "工作上优先发挥你的风格优势,并用小里程碑对抗拖延。",
|
||||
GrowthDeep: "成长是认识模式后做可验证的小调整。",
|
||||
SunBullets: []string{"核心动机可被命名", "表达有风格偏好", "适合自我探索"},
|
||||
MoonBullets: []string{"情绪需要出口", "安全感来源因人而异", "独处或连接可充电"},
|
||||
RiseBullets: []string{"第一印象可调节", "外显≠全部自我", "可练习温和表达"},
|
||||
RelationBullets: []string{"需求具体化", "尊重双方节奏", "冲突先复述再方案"},
|
||||
CareerBullets: []string{"发挥风格优势", "小步交付", "复盘节奏"},
|
||||
GrowthBullets: []string{"看见模式", "小步验证", "结合运势调整"},
|
||||
Strengths: []string{"风格清晰", "可探索性强", "利于自我对话"},
|
||||
BlindSpots: []string{"标签固化", "忽略情境差异", "过度解读"},
|
||||
Scripts: []string{"我想先说清我的节奏,再听你的。", "我不是冷淡,我需要一点整理时间。", "我们共同目标是……,下一步只定一件事。"},
|
||||
PlanWeek: "用三句话写下:我的优势 / 我的消耗点 / 我本周要试的一小步。",
|
||||
PlanMonth: "在关系或工作中练习两次「先复述对方,再提需要」。",
|
||||
PlanLong: "建立个人节奏手册:什么充电、什么耗电、如何请求支持。",
|
||||
FAQ: []map[string]string{
|
||||
{"q": "运势是预测吗?", "a": "运势分与建议帮助你调整节奏与决策,请结合现实判断,勿作唯一依据。"},
|
||||
{"q": "星盘准吗?", "a": "出生时与出生地越完整,上升与宫位越贴近;算法为可复现近似星历。"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func enrich(base pack, label, one, overview string) pack {
|
||||
base.Label = label
|
||||
base.OneLiner = one
|
||||
base.Overview = overview
|
||||
base.Keywords = []string{label, "星座", "运势"}
|
||||
return base
|
||||
}
|
||||
Reference in New Issue
Block a user