Files
wx_service/internal/smoke/service/smoke_profile_service.go
T
nepiedg 9200600b1c Enhance smoking tracking API with new features and improvements
- Added a new API endpoint `GET /api/v1/smoke/home` to consolidate core modules for the home dashboard, reducing the need for multiple requests.
- Updated the `smoke` routes to include the new home endpoint and improved user profile management with the addition of a `quit_date` field.
- Enhanced the algorithm for calculating daily targets and next smoke suggestions, ensuring accurate future time handling and user-specific recommendations.
- Improved API documentation to reflect new endpoints, response formats, and detailed field descriptions for better clarity and usability.
- Refactored user authentication handling in various handlers to streamline the process and ensure consistent error responses.
2026-01-29 17:16:35 +00:00

235 lines
5.9 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
smokemodel "wx_service/internal/smoke/model"
)
var (
ErrSmokeProfileInvalidTime = errors.New("invalid time format, expected HH:MM")
)
type SmokeProfileService struct {
db *gorm.DB
}
func NewSmokeProfileService(db *gorm.DB) *SmokeProfileService {
return &SmokeProfileService{db: db}
}
type SmokeProfileView struct {
Exists bool `json:"exists"`
Profile *smokemodel.SmokeUserProfile `json:"profile,omitempty"`
IsCompleted bool `json:"is_completed"`
AwakeMinutes int `json:"awake_minutes"`
BaselineIntervalMinute int `json:"baseline_interval_minutes"`
}
func (s *SmokeProfileService) GetView(ctx context.Context, uid int) (SmokeProfileView, error) {
profile, err := s.Get(ctx, uid)
if err != nil {
return SmokeProfileView{}, err
}
if profile == nil {
return SmokeProfileView{
Exists: false,
Profile: nil,
IsCompleted: false,
AwakeMinutes: defaultAwakeMinutes,
BaselineIntervalMinute: 0,
}, nil
}
awakeMinutes, err := awakeMinutesWithFallback(profile.WakeUpTime, profile.SleepTime)
if err != nil {
return SmokeProfileView{}, err
}
isCompleted := isSmokeProfileCompleted(*profile)
interval := baselineIntervalMinutes(awakeMinutes, profile.BaselineCigsPerDay)
return SmokeProfileView{
Exists: true,
Profile: profile,
IsCompleted: isCompleted,
AwakeMinutes: awakeMinutes,
BaselineIntervalMinute: interval,
}, nil
}
func (s *SmokeProfileService) Get(ctx context.Context, uid int) (*smokemodel.SmokeUserProfile, error) {
var profile smokemodel.SmokeUserProfile
err := s.db.WithContext(ctx).
Where("uid = ? AND deleted_at IS NULL", uid).
First(&profile).Error
if err == nil {
return &profile, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("load smoke profile: %w", err)
}
type UpsertSmokeProfileRequest struct {
BaselineCigsPerDay *int
SmokingYears *float64
PackPriceCent *int
SmokeMotivations *[]string
QuitMotivations *[]string
WakeUpTime *string
SleepTime *string
QuitDateProvided bool
QuitDate *time.Time
}
func (s *SmokeProfileService) Upsert(ctx context.Context, uid int, req UpsertSmokeProfileRequest) (SmokeProfileView, error) {
var profile smokemodel.SmokeUserProfile
tx := s.db.WithContext(ctx)
err := tx.Where("uid = ? AND deleted_at IS NULL", uid).First(&profile).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return SmokeProfileView{}, fmt.Errorf("load smoke profile: %w", err)
}
isNew := errors.Is(err, gorm.ErrRecordNotFound)
if isNew {
profile = smokemodel.SmokeUserProfile{UID: uid}
}
applyInt := func(dst *int, v *int) {
if v != nil {
*dst = *v
}
}
applyFloat := func(dst *float64, v *float64) {
if v != nil {
*dst = *v
}
}
applyTimeStr := func(dst *string, v *string) error {
if v == nil {
return nil
}
value := strings.TrimSpace(*v)
if value == "" {
*dst = ""
return nil
}
if _, err := parseHHMMToMinutes(value); err != nil {
return ErrSmokeProfileInvalidTime
}
*dst = value
return nil
}
applyInt(&profile.BaselineCigsPerDay, req.BaselineCigsPerDay)
applyFloat(&profile.SmokingYears, req.SmokingYears)
applyInt(&profile.PackPriceCent, req.PackPriceCent)
if req.SmokeMotivations != nil {
profile.SmokeMotivations = smokemodel.StringSlice(*req.SmokeMotivations)
}
if req.QuitMotivations != nil {
profile.QuitMotivations = smokemodel.StringSlice(*req.QuitMotivations)
}
if err := applyTimeStr(&profile.WakeUpTime, req.WakeUpTime); err != nil {
return SmokeProfileView{}, err
}
if err := applyTimeStr(&profile.SleepTime, req.SleepTime); err != nil {
return SmokeProfileView{}, err
}
if req.QuitDateProvided {
profile.QuitDate = req.QuitDate
}
now := time.Now()
if profile.OnboardingCompletedAt == nil && isSmokeProfileCompleted(profile) {
profile.OnboardingCompletedAt = &now
}
if isNew {
if err := tx.Create(&profile).Error; err != nil {
return SmokeProfileView{}, fmt.Errorf("create smoke profile: %w", err)
}
} else {
if err := tx.Save(&profile).Error; err != nil {
return SmokeProfileView{}, fmt.Errorf("save smoke profile: %w", err)
}
}
return s.GetView(ctx, uid)
}
func isSmokeProfileCompleted(p smokemodel.SmokeUserProfile) bool {
return p.BaselineCigsPerDay > 0 &&
p.PackPriceCent > 0 &&
len(p.QuitMotivations) > 0 &&
strings.TrimSpace(p.WakeUpTime) != "" &&
strings.TrimSpace(p.SleepTime) != ""
}
const defaultAwakeMinutes = 16 * 60
func awakeMinutesWithFallback(wakeUp, sleep string) (int, error) {
wakeUp = strings.TrimSpace(wakeUp)
sleep = strings.TrimSpace(sleep)
if wakeUp == "" || sleep == "" {
return defaultAwakeMinutes, nil
}
wakeMin, err := parseHHMMToMinutes(wakeUp)
if err != nil {
return 0, ErrSmokeProfileInvalidTime
}
sleepMin, err := parseHHMMToMinutes(sleep)
if err != nil {
return 0, ErrSmokeProfileInvalidTime
}
if sleepMin == wakeMin {
return 24 * 60, nil
}
if sleepMin > wakeMin {
return sleepMin - wakeMin, nil
}
return (24*60 - wakeMin) + sleepMin, nil
}
func baselineIntervalMinutes(awakeMinutes int, baselineCigsPerDay int) int {
if awakeMinutes <= 0 || baselineCigsPerDay <= 0 {
return 0
}
interval := awakeMinutes / baselineCigsPerDay
if interval <= 0 {
return 1
}
return interval
}
func parseHHMMToMinutes(s string) (int, error) {
s = strings.TrimSpace(s)
if len(s) != 5 || s[2] != ':' {
return 0, ErrSmokeProfileInvalidTime
}
h1, h2 := s[0], s[1]
m1, m2 := s[3], s[4]
if h1 < '0' || h1 > '9' || h2 < '0' || h2 > '9' || m1 < '0' || m1 > '9' || m2 < '0' || m2 > '9' {
return 0, ErrSmokeProfileInvalidTime
}
hour := int(h1-'0')*10 + int(h2-'0')
min := int(m1-'0')*10 + int(m2-'0')
if hour < 0 || hour > 23 || min < 0 || min > 59 {
return 0, ErrSmokeProfileInvalidTime
}
return hour*60 + min, nil
}