From 515fba0b552f7cc343a5ab45f77b2017be623ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=C3=A7=C2=9Anepiedg?= <你的yunchuansong@163.com> Date: Thu, 19 Mar 2026 15:41:26 +0800 Subject: [PATCH] feat: update smoke profile area handling --- docs/smoke/API.md | 3 +++ docs/sql/smoke.sql | 1 + .../smoke/handler/smoke_profile_handler.go | 9 +++++++ internal/smoke/model/smoke_profile.go | 1 + .../smoke/service/smoke_profile_service.go | 26 +++++++++++++++++++ .../service/smoke_profile_service_test.go | 20 ++++++++++++++ 6 files changed, 60 insertions(+) diff --git a/docs/smoke/API.md b/docs/smoke/API.md index a9fafd3..54c833a 100644 --- a/docs/smoke/API.md +++ b/docs/smoke/API.md @@ -391,6 +391,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \ "pack_price_cent": 2500, "smoke_motivations": ["压力大", "社交"], "quit_motivations": ["身体健康", "省钱"], + "mode": "record", "wake_up_time": "07:30", "sleep_time": "23:30", "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`(单包价格/分):用于看板计算“已省金额”和“恢复时长”等指标(公式可在看板端实现)。 - `smoke_motivations`(抽烟动机):如 `压力大/无聊/社交/提神`,用于 AI 在分析 remark 时更有针对性。 - `quit_motivations`(戒烟动力):如 `身体健康/家人孩子/省钱`,当用户产生动摇时 AI 可用这些信息做“情感阻断/自我提醒”。 +- `mode`(使用模式):`quit` 表示戒烟打卡模式,`record` 表示记录抽烟模式。 - `wake_up_time` + `sleep_time`(作息时间):用于自动规避睡眠时间,防止在用户睡觉时提醒其“坚持”。 - `quit_date`(目标戒烟日期):用于阶段规划或到期提醒。 @@ -444,6 +446,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \ "pack_price_cent": 2500, "smoke_motivations": ["压力大", "社交"], "quit_motivations": ["身体健康", "省钱"], + "mode": "record", "wake_up_time": "07:30", "sleep_time": "23:30", "quit_date": "2026-02-28" diff --git a/docs/sql/smoke.sql b/docs/sql/smoke.sql index 50e7f9d..0275dd5 100644 --- a/docs/sql/smoke.sql +++ b/docs/sql/smoke.sql @@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS `fa_smoke_user_profile` ( `pack_price_cent` int NOT NULL DEFAULT 0 COMMENT '单包价格(分)', `smoke_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)', `sleep_time` varchar(5) NOT NULL DEFAULT '' COMMENT '入睡时间(HH:MM)', `onboarding_completed_at` datetime(3) DEFAULT NULL COMMENT '首次补全完成时间', diff --git a/internal/smoke/handler/smoke_profile_handler.go b/internal/smoke/handler/smoke_profile_handler.go index 29f592f..07acb96 100644 --- a/internal/smoke/handler/smoke_profile_handler.go +++ b/internal/smoke/handler/smoke_profile_handler.go @@ -18,6 +18,7 @@ type upsertSmokeProfileRequest struct { BaselineCigsPerDay *int `json:"baseline_cigs_per_day"` SmokingYears *float64 `json:"smoking_years"` PackPriceCent *int `json:"pack_price_cent"` + Mode *string `json:"mode"` SmokeMotivations *[]string `json:"smoke_motivations"` QuitMotivations *[]string `json:"quit_motivations"` @@ -71,6 +72,13 @@ func (h *SmokeHandler) UpsertProfile(c *gin.Context) { 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 var quitDate *time.Time @@ -91,6 +99,7 @@ func (h *SmokeHandler) UpsertProfile(c *gin.Context) { BaselineCigsPerDay: req.BaselineCigsPerDay, SmokingYears: req.SmokingYears, PackPriceCent: req.PackPriceCent, + Mode: req.Mode, SmokeMotivations: req.SmokeMotivations, QuitMotivations: req.QuitMotivations, WakeUpTime: req.WakeUpTime, diff --git a/internal/smoke/model/smoke_profile.go b/internal/smoke/model/smoke_profile.go index 72c1fe1..8c7a40a 100644 --- a/internal/smoke/model/smoke_profile.go +++ b/internal/smoke/model/smoke_profile.go @@ -67,6 +67,7 @@ type SmokeUserProfile struct { 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"` + 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"` SleepTime string `gorm:"column:sleep_time;size:5;comment:入睡时间(HH:MM)" json:"sleep_time"` diff --git a/internal/smoke/service/smoke_profile_service.go b/internal/smoke/service/smoke_profile_service.go index abd8af4..f0e3ff4 100644 --- a/internal/smoke/service/smoke_profile_service.go +++ b/internal/smoke/service/smoke_profile_service.go @@ -16,6 +16,11 @@ var ( ErrSmokeProfileInvalidTime = errors.New("invalid time format, expected HH:MM") ) +const ( + SmokeModeQuit = "quit" + SmokeModeRecord = "record" +) + type SmokeProfileService struct { db *gorm.DB } @@ -47,6 +52,7 @@ func (s *SmokeProfileService) GetView(ctx context.Context, uid int) (SmokeProfil BaselineIntervalMinute: 0, }, nil } + profile.Mode = normalizedSmokeMode(profile.Mode) awakeMinutes, err := awakeMinutesWithFallback(profile.WakeUpTime, profile.SleepTime) if err != nil { @@ -83,6 +89,7 @@ type UpsertSmokeProfileRequest struct { BaselineCigsPerDay *int SmokingYears *float64 PackPriceCent *int + Mode *string SmokeMotivations *[]string QuitMotivations *[]string @@ -116,6 +123,12 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo *dst = *v } } + applyMode := func(dst *string, v *string) { + if v == nil { + return + } + *dst = normalizedSmokeMode(*v) + } applyTimeStr := func(dst *string, v *string) error { if v == nil { return nil @@ -135,6 +148,7 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo applyInt(&profile.BaselineCigsPerDay, req.BaselineCigsPerDay) applyFloat(&profile.SmokingYears, req.SmokingYears) applyInt(&profile.PackPriceCent, req.PackPriceCent) + applyMode(&profile.Mode, req.Mode) if req.SmokeMotivations != nil { profile.SmokeMotivations = smokemodel.StringSlice(*req.SmokeMotivations) @@ -154,6 +168,7 @@ func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmo } now := time.Now() + profile.Mode = normalizedSmokeMode(profile.Mode) if profile.OnboardingCompletedAt == nil && isSmokeProfileCompleted(profile) { profile.OnboardingCompletedAt = &now } @@ -215,6 +230,17 @@ func baselineIntervalMinutes(awakeMinutes int, baselineCigsPerDay int) int { 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) { s = strings.TrimSpace(s) if len(s) != 5 || s[2] != ':' { diff --git a/internal/smoke/service/smoke_profile_service_test.go b/internal/smoke/service/smoke_profile_service_test.go index 9c22ebf..b4024f3 100644 --- a/internal/smoke/service/smoke_profile_service_test.go +++ b/internal/smoke/service/smoke_profile_service_test.go @@ -106,3 +106,23 @@ func TestIsSmokeProfileCompleted(t *testing.T) { 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) + } + } +}