Add user profile management for smoking data
- Introduced new API endpoints `GET /api/v1/smoke/profile` and `PUT /api/v1/smoke/profile` for retrieving and updating user smoking profiles. - Added a new database table `fa_smoke_user_profile` to store user-specific smoking data, including daily smoking habits and motivations. - Updated the smoke handler and service to integrate user profile data into AI advice generation. - Enhanced documentation to reflect the new user profile features and their usage.
This commit is contained in:
@@ -26,7 +26,7 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAdvicePromptVersion = "v1"
|
||||
DefaultAdvicePromptVersion = "v2"
|
||||
defaultTemperature = 0.7
|
||||
)
|
||||
|
||||
@@ -61,6 +61,20 @@ type adviceSnapshot struct {
|
||||
Date string `json:"date"`
|
||||
TotalNum int `json:"total_num"`
|
||||
Nodes []adviceSnapshotNode `json:"nodes"`
|
||||
Profile *adviceUserProfile `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
type adviceUserProfile struct {
|
||||
BaselineCigsPerDay int `json:"baseline_cigs_per_day,omitempty"`
|
||||
SmokingYears float64 `json:"smoking_years,omitempty"`
|
||||
PackPriceCent int `json:"pack_price_cent,omitempty"`
|
||||
SmokeMotivations []string `json:"smoke_motivations,omitempty"`
|
||||
QuitMotivations []string `json:"quit_motivations,omitempty"`
|
||||
WakeUpTime string `json:"wake_up_time,omitempty"`
|
||||
SleepTime string `json:"sleep_time,omitempty"`
|
||||
AwakeMinutes int `json:"awake_minutes,omitempty"`
|
||||
BaselineIntervalMinutes int `json:"baseline_interval_minutes,omitempty"`
|
||||
OnboardingCompletedAtISO string `json:"onboarding_completed_at,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SmokeAIAdviceService) GetOrGenerate(ctx context.Context, user *usermodel.User, adviceDate time.Time, promptVersion string) (*smokemodel.SmokeAIAdvice, error) {
|
||||
@@ -217,6 +231,8 @@ func (s *SmokeAIAdviceService) buildSnapshot(ctx context.Context, uid int, advic
|
||||
return adviceSnapshot{}, nil, ErrNoSmokeLogs
|
||||
}
|
||||
|
||||
profile := s.loadAdviceProfile(ctx, uid)
|
||||
|
||||
type timedLog struct {
|
||||
log smokemodel.SmokeLog
|
||||
eventAt time.Time
|
||||
@@ -273,6 +289,7 @@ func (s *SmokeAIAdviceService) buildSnapshot(ctx context.Context, uid int, advic
|
||||
Date: dateOnly(adviceDate).Format("2006-01-02"),
|
||||
TotalNum: total,
|
||||
Nodes: nodes,
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(snap)
|
||||
@@ -282,6 +299,42 @@ func (s *SmokeAIAdviceService) buildSnapshot(ctx context.Context, uid int, advic
|
||||
return snap, b, nil
|
||||
}
|
||||
|
||||
func (s *SmokeAIAdviceService) loadAdviceProfile(ctx context.Context, uid int) *adviceUserProfile {
|
||||
var profile smokemodel.SmokeUserProfile
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("uid = ? AND deleted_at IS NULL", uid).
|
||||
First(&profile).Error
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
awake := defaultAwakeMinutes
|
||||
wake := strings.TrimSpace(profile.WakeUpTime)
|
||||
sleep := strings.TrimSpace(profile.SleepTime)
|
||||
if v, err := awakeMinutesWithFallback(wake, sleep); err == nil {
|
||||
awake = v
|
||||
} else {
|
||||
wake = ""
|
||||
sleep = ""
|
||||
}
|
||||
|
||||
out := adviceUserProfile{
|
||||
BaselineCigsPerDay: profile.BaselineCigsPerDay,
|
||||
SmokingYears: profile.SmokingYears,
|
||||
PackPriceCent: profile.PackPriceCent,
|
||||
SmokeMotivations: []string(profile.SmokeMotivations),
|
||||
QuitMotivations: []string(profile.QuitMotivations),
|
||||
WakeUpTime: wake,
|
||||
SleepTime: sleep,
|
||||
AwakeMinutes: awake,
|
||||
BaselineIntervalMinutes: baselineIntervalMinutes(awake, profile.BaselineCigsPerDay),
|
||||
}
|
||||
if profile.OnboardingCompletedAt != nil {
|
||||
out.OnboardingCompletedAtISO = profile.OnboardingCompletedAt.In(time.Local).Format(time.RFC3339)
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func (s *SmokeAIAdviceService) callAI(ctx context.Context, snap adviceSnapshot) (string, string, *int, *int, error) {
|
||||
if s.cfg.APIKey == "" || s.cfg.Model == "" || s.cfg.BaseURL == "" {
|
||||
return "", "", nil, nil, ErrAIServiceDisabled
|
||||
@@ -293,8 +346,12 @@ func (s *SmokeAIAdviceService) callAI(ctx context.Context, snap adviceSnapshot)
|
||||
1) 用中文输出;
|
||||
2) 先给出对昨天模式的简短分析(1-3条);
|
||||
3) 给出今天的具体行动方案(至少5条,包含替代行为、触发场景应对、时间节点策略);
|
||||
4) 给出一个“如果忍不住想抽”的 60 秒应对流程;
|
||||
5) 语气友好、不指责;不提供医疗诊断。
|
||||
4) 如果 profile 中提供了「作息时间」,建议的执行时间点要避开用户睡眠区间;
|
||||
5) 如果 profile 中提供了「抽烟动机/戒烟动力」,你需要在建议中更有针对性地引用它们:
|
||||
- 动机:用于解释触发场景与 remark 的关联,给出替代行为;
|
||||
- 动力:用于“情感阻断/动摇时的自我提醒”(给 2-3 条可复述的话术);
|
||||
6) 给出一个“如果忍不住想抽”的 60 秒应对流程;
|
||||
7) 语气友好、不指责;不提供医疗诊断。
|
||||
`)
|
||||
|
||||
userPrompt := fmt.Sprintf("用户昨日数据(JSON):\n%s", mustJSON(snap))
|
||||
|
||||
Reference in New Issue
Block a user