落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.1 KiB
Go
128 lines
3.1 KiB
Go
package report
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/model"
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/rhythm"
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/star"
|
|
)
|
|
|
|
func cstLoc() *time.Location {
|
|
return time.FixedZone("CST", 8*3600)
|
|
}
|
|
|
|
func dayKey(t time.Time) string {
|
|
return t.In(cstLoc()).Format("2006-01-02")
|
|
}
|
|
|
|
func summaryDay(raw json.RawMessage) string {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return ""
|
|
}
|
|
s, _ := m["as_of"].(string)
|
|
return s
|
|
}
|
|
|
|
func hasWuxingBars(raw json.RawMessage) bool {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return false
|
|
}
|
|
wx, _ := m["wuxing"].(map[string]any)
|
|
if wx == nil {
|
|
return false
|
|
}
|
|
bars, ok := wx["bars"].([]any)
|
|
return ok && len(bars) > 0
|
|
}
|
|
|
|
func rhythmNeedsContentBackfill(raw json.RawMessage) bool {
|
|
if !hasWuxingBars(raw) {
|
|
return true
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return true
|
|
}
|
|
prev, _ := m["blind_spots_preview"].([]any)
|
|
if len(prev) == 0 {
|
|
return true
|
|
}
|
|
for _, item := range prev {
|
|
s, _ := item.(string)
|
|
if s == "完整习惯方案见深度版。" || strings.Contains(s, "见深度版") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// refreshTemporalIfNeeded updates star/rhythm day-scoped tips in place when as_of is stale.
|
|
func (s *Service) refreshTemporalIfNeeded(ctx context.Context, userID uuid.UUID, rep *model.GrowthReport) (*model.GrowthReport, error) {
|
|
if rep == nil || (rep.Type != "star" && rep.Type != "rhythm") {
|
|
return rep, nil
|
|
}
|
|
now := time.Now().In(cstLoc())
|
|
today := dayKey(now)
|
|
staleDay := summaryDay(rep.Summary) != today
|
|
needBackfill := rep.Type == "rhythm" && rhythmNeedsContentBackfill(rep.Summary)
|
|
if !staleDay && !needBackfill {
|
|
return stampValidUntil(rep, now), nil
|
|
}
|
|
p, err := s.Profiles.GetForUser(ctx, userID, rep.ProfileID)
|
|
if err != nil {
|
|
return rep, nil
|
|
}
|
|
var sum, det json.RawMessage
|
|
switch rep.Type {
|
|
case "star":
|
|
out, err := star.BuildWith(star.BuildOpts{
|
|
Birth: p.BirthDate, BirthTime: p.BirthTime, BirthPlace: p.BirthPlace,
|
|
Name: p.DisplayName, AsOf: now,
|
|
})
|
|
if err != nil {
|
|
return rep, nil
|
|
}
|
|
sum, _ = json.Marshal(out.Summary)
|
|
det, _ = json.Marshal(out.Detail)
|
|
case "rhythm":
|
|
out := rhythm.BuildWith(p.BirthDate, p.DisplayName, now)
|
|
sum, _ = json.Marshal(out.Summary)
|
|
det, _ = json.Marshal(out.Detail)
|
|
default:
|
|
return rep, nil
|
|
}
|
|
if err := s.Reports.UpdateContent(ctx, userID, rep.ID, sum, det); err != nil {
|
|
return rep, nil
|
|
}
|
|
rep.Summary = sum
|
|
rep.Detail = det
|
|
return rep, nil
|
|
}
|
|
|
|
func stampValidUntil(rep *model.GrowthReport, now time.Time) *model.GrowthReport {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(rep.Summary, &m); err != nil {
|
|
return rep
|
|
}
|
|
if _, ok := m["valid_until"].(string); ok && m["as_of"] == dayKey(now) {
|
|
return rep
|
|
}
|
|
m["as_of"] = dayKey(now)
|
|
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
|
m["valid_until"] = next.Format(time.RFC3339)
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
return rep
|
|
}
|
|
rep.Summary = raw
|
|
return rep
|
|
}
|