feat: update smoke profile area handling

This commit is contained in:
你çšnepiedg
2026-03-19 15:41:26 +08:00
parent 9278260681
commit 515fba0b55
6 changed files with 60 additions and 0 deletions
+3
View File
@@ -391,6 +391,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
"pack_price_cent": 2500, "pack_price_cent": 2500,
"smoke_motivations": ["压力大", "社交"], "smoke_motivations": ["压力大", "社交"],
"quit_motivations": ["身体健康", "省钱"], "quit_motivations": ["身体健康", "省钱"],
"mode": "record",
"wake_up_time": "07:30", "wake_up_time": "07:30",
"sleep_time": "23:30", "sleep_time": "23:30",
"quit_date": "2026-02-28T00:00:00+08:00", "quit_date": "2026-02-28T00:00:00+08:00",
@@ -423,6 +424,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
- `smoking_years`(烟龄/年)+ `pack_price_cent`(单包价格/分):用于看板计算“已省金额”和“恢复时长”等指标(公式可在看板端实现)。 - `smoking_years`(烟龄/年)+ `pack_price_cent`(单包价格/分):用于看板计算“已省金额”和“恢复时长”等指标(公式可在看板端实现)。
- `smoke_motivations`(抽烟动机):如 `压力大/无聊/社交/提神`,用于 AI 在分析 remark 时更有针对性。 - `smoke_motivations`(抽烟动机):如 `压力大/无聊/社交/提神`,用于 AI 在分析 remark 时更有针对性。
- `quit_motivations`(戒烟动力):如 `身体健康/家人孩子/省钱`,当用户产生动摇时 AI 可用这些信息做“情感阻断/自我提醒”。 - `quit_motivations`(戒烟动力):如 `身体健康/家人孩子/省钱`,当用户产生动摇时 AI 可用这些信息做“情感阻断/自我提醒”。
- `mode`(使用模式):`quit` 表示戒烟打卡模式,`record` 表示记录抽烟模式。
- `wake_up_time` + `sleep_time`(作息时间):用于自动规避睡眠时间,防止在用户睡觉时提醒其“坚持”。 - `wake_up_time` + `sleep_time`(作息时间):用于自动规避睡眠时间,防止在用户睡觉时提醒其“坚持”。
- `quit_date`(目标戒烟日期):用于阶段规划或到期提醒。 - `quit_date`(目标戒烟日期):用于阶段规划或到期提醒。
@@ -444,6 +446,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
"pack_price_cent": 2500, "pack_price_cent": 2500,
"smoke_motivations": ["压力大", "社交"], "smoke_motivations": ["压力大", "社交"],
"quit_motivations": ["身体健康", "省钱"], "quit_motivations": ["身体健康", "省钱"],
"mode": "record",
"wake_up_time": "07:30", "wake_up_time": "07:30",
"sleep_time": "23:30", "sleep_time": "23:30",
"quit_date": "2026-02-28" "quit_date": "2026-02-28"
+1
View File
@@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS `fa_smoke_user_profile` (
`pack_price_cent` int NOT NULL DEFAULT 0 COMMENT '单包价格(分)', `pack_price_cent` int NOT NULL DEFAULT 0 COMMENT '单包价格(分)',
`smoke_motivations` json DEFAULT NULL COMMENT '抽烟动机(JSON数组)', `smoke_motivations` json DEFAULT NULL COMMENT '抽烟动机(JSON数组)',
`quit_motivations` json DEFAULT NULL COMMENT '戒烟动力(JSON数组)', `quit_motivations` json DEFAULT NULL COMMENT '戒烟动力(JSON数组)',
`mode` varchar(16) NOT NULL DEFAULT 'record' COMMENT '使用模式(quit=戒烟打卡,record=记录抽烟)',
`wake_up_time` varchar(5) NOT NULL DEFAULT '' COMMENT '起床时间(HH:MM)', `wake_up_time` varchar(5) NOT NULL DEFAULT '' COMMENT '起床时间(HH:MM)',
`sleep_time` varchar(5) NOT NULL DEFAULT '' COMMENT '入睡时间(HH:MM)', `sleep_time` varchar(5) NOT NULL DEFAULT '' COMMENT '入睡时间(HH:MM)',
`onboarding_completed_at` datetime(3) DEFAULT NULL COMMENT '首次补全完成时间', `onboarding_completed_at` datetime(3) DEFAULT NULL COMMENT '首次补全完成时间',
@@ -18,6 +18,7 @@ type upsertSmokeProfileRequest struct {
BaselineCigsPerDay *int `json:"baseline_cigs_per_day"` BaselineCigsPerDay *int `json:"baseline_cigs_per_day"`
SmokingYears *float64 `json:"smoking_years"` SmokingYears *float64 `json:"smoking_years"`
PackPriceCent *int `json:"pack_price_cent"` PackPriceCent *int `json:"pack_price_cent"`
Mode *string `json:"mode"`
SmokeMotivations *[]string `json:"smoke_motivations"` SmokeMotivations *[]string `json:"smoke_motivations"`
QuitMotivations *[]string `json:"quit_motivations"` QuitMotivations *[]string `json:"quit_motivations"`
@@ -71,6 +72,13 @@ func (h *SmokeHandler) UpsertProfile(c *gin.Context) {
return return
} }
} }
if req.Mode != nil {
mode := strings.TrimSpace(*req.Mode)
if mode != "" && mode != smokeservice.SmokeModeQuit && mode != smokeservice.SmokeModeRecord {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "mode 仅支持 quit 或 record"))
return
}
}
quitDateProvided := false quitDateProvided := false
var quitDate *time.Time var quitDate *time.Time
@@ -91,6 +99,7 @@ func (h *SmokeHandler) UpsertProfile(c *gin.Context) {
BaselineCigsPerDay: req.BaselineCigsPerDay, BaselineCigsPerDay: req.BaselineCigsPerDay,
SmokingYears: req.SmokingYears, SmokingYears: req.SmokingYears,
PackPriceCent: req.PackPriceCent, PackPriceCent: req.PackPriceCent,
Mode: req.Mode,
SmokeMotivations: req.SmokeMotivations, SmokeMotivations: req.SmokeMotivations,
QuitMotivations: req.QuitMotivations, QuitMotivations: req.QuitMotivations,
WakeUpTime: req.WakeUpTime, WakeUpTime: req.WakeUpTime,
+1
View File
@@ -67,6 +67,7 @@ type SmokeUserProfile struct {
SmokeMotivations StringSlice `gorm:"column:smoke_motivations;type:json;comment:抽烟动机(JSON数组)" json:"smoke_motivations"` SmokeMotivations StringSlice `gorm:"column:smoke_motivations;type:json;comment:抽烟动机(JSON数组)" json:"smoke_motivations"`
QuitMotivations StringSlice `gorm:"column:quit_motivations;type:json;comment:戒烟动力(JSON数组)" json:"quit_motivations"` QuitMotivations StringSlice `gorm:"column:quit_motivations;type:json;comment:戒烟动力(JSON数组)" json:"quit_motivations"`
Mode string `gorm:"column:mode;size:16;default:record;comment:使用模式(quit=戒烟打卡,record=记录抽烟)" json:"mode,omitempty"`
WakeUpTime string `gorm:"column:wake_up_time;size:5;comment:起床时间(HH:MM)" json:"wake_up_time"` WakeUpTime string `gorm:"column:wake_up_time;size:5;comment:起床时间(HH:MM)" json:"wake_up_time"`
SleepTime string `gorm:"column:sleep_time;size:5;comment:入睡时间(HH:MM)" json:"sleep_time"` SleepTime string `gorm:"column:sleep_time;size:5;comment:入睡时间(HH:MM)" json:"sleep_time"`
@@ -16,6 +16,11 @@ var (
ErrSmokeProfileInvalidTime = errors.New("invalid time format, expected HH:MM") ErrSmokeProfileInvalidTime = errors.New("invalid time format, expected HH:MM")
) )
const (
SmokeModeQuit = "quit"
SmokeModeRecord = "record"
)
type SmokeProfileService struct { type SmokeProfileService struct {
db *gorm.DB db *gorm.DB
} }
@@ -47,6 +52,7 @@ func (s *SmokeProfileService) GetView(ctx context.Context, uid int) (SmokeProfil
BaselineIntervalMinute: 0, BaselineIntervalMinute: 0,
}, nil }, nil
} }
profile.Mode = normalizedSmokeMode(profile.Mode)
awakeMinutes, err := awakeMinutesWithFallback(profile.WakeUpTime, profile.SleepTime) awakeMinutes, err := awakeMinutesWithFallback(profile.WakeUpTime, profile.SleepTime)
if err != nil { if err != nil {
@@ -83,6 +89,7 @@ type UpsertSmokeProfileRequest struct {
BaselineCigsPerDay *int BaselineCigsPerDay *int
SmokingYears *float64 SmokingYears *float64
PackPriceCent *int PackPriceCent *int
Mode *string
SmokeMotivations *[]string SmokeMotivations *[]string
QuitMotivations *[]string QuitMotivations *[]string
@@ -116,6 +123,12 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo
*dst = *v *dst = *v
} }
} }
applyMode := func(dst *string, v *string) {
if v == nil {
return
}
*dst = normalizedSmokeMode(*v)
}
applyTimeStr := func(dst *string, v *string) error { applyTimeStr := func(dst *string, v *string) error {
if v == nil { if v == nil {
return nil return nil
@@ -135,6 +148,7 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo
applyInt(&profile.BaselineCigsPerDay, req.BaselineCigsPerDay) applyInt(&profile.BaselineCigsPerDay, req.BaselineCigsPerDay)
applyFloat(&profile.SmokingYears, req.SmokingYears) applyFloat(&profile.SmokingYears, req.SmokingYears)
applyInt(&profile.PackPriceCent, req.PackPriceCent) applyInt(&profile.PackPriceCent, req.PackPriceCent)
applyMode(&profile.Mode, req.Mode)
if req.SmokeMotivations != nil { if req.SmokeMotivations != nil {
profile.SmokeMotivations = smokemodel.StringSlice(*req.SmokeMotivations) profile.SmokeMotivations = smokemodel.StringSlice(*req.SmokeMotivations)
@@ -154,6 +168,7 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo
} }
now := time.Now() now := time.Now()
profile.Mode = normalizedSmokeMode(profile.Mode)
if profile.OnboardingCompletedAt == nil && isSmokeProfileCompleted(profile) { if profile.OnboardingCompletedAt == nil && isSmokeProfileCompleted(profile) {
profile.OnboardingCompletedAt = &now profile.OnboardingCompletedAt = &now
} }
@@ -215,6 +230,17 @@ func baselineIntervalMinutes(awakeMinutes int, baselineCigsPerDay int) int {
return interval return interval
} }
func normalizedSmokeMode(mode string) string {
switch strings.TrimSpace(mode) {
case SmokeModeQuit:
return SmokeModeQuit
case SmokeModeRecord:
return SmokeModeRecord
default:
return SmokeModeRecord
}
}
func parseHHMMToMinutes(s string) (int, error) { func parseHHMMToMinutes(s string) (int, error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if len(s) != 5 || s[2] != ':' { if len(s) != 5 || s[2] != ':' {
@@ -106,3 +106,23 @@ func TestIsSmokeProfileCompleted(t *testing.T) {
t.Fatalf("isSmokeProfileCompleted: expected false when quit_motivations missing") t.Fatalf("isSmokeProfileCompleted: expected false when quit_motivations missing")
} }
} }
func TestNormalizedSmokeMode(t *testing.T) {
t.Parallel()
cases := []struct {
in string
want string
}{
{in: SmokeModeQuit, want: SmokeModeQuit},
{in: SmokeModeRecord, want: SmokeModeRecord},
{in: "", want: SmokeModeRecord},
{in: "unknown", want: SmokeModeRecord},
}
for _, c := range cases {
if got := normalizedSmokeMode(c.in); got != c.want {
t.Fatalf("normalizedSmokeMode(%q): got %q, want %q", c.in, got, c.want)
}
}
}