feat: update smoke profile area handling
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user