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.
This commit is contained in:
@@ -20,21 +20,22 @@ func NewSmokeNextService(db *gorm.DB) *SmokeNextService {
|
||||
}
|
||||
|
||||
type NextSmokeSuggestion struct {
|
||||
LastSmokeAt *time.Time `json:"last_smoke_at,omitempty"`
|
||||
NextSmokeAt *time.Time `json:"next_smoke_at,omitempty"`
|
||||
BaseIntervalMinutes int `json:"base_interval_minutes"`
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
Stage int `json:"stage"`
|
||||
Resisted7d int `json:"resisted_7d"`
|
||||
SleepAdjusted bool `json:"sleep_adjusted"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
AsOf string `json:"as_of"`
|
||||
LastSmokeAt *time.Time `json:"last_smoke_at,omitempty"`
|
||||
NextSmokeAt *time.Time `json:"next_smoke_at,omitempty"`
|
||||
BaseIntervalMinutes int `json:"base_interval_minutes"`
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
Stage int `json:"stage"`
|
||||
Resisted7d int `json:"resisted_7d"`
|
||||
SleepAdjusted bool `json:"sleep_adjusted"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
AsOf string `json:"as_of"`
|
||||
}
|
||||
|
||||
// GetDefaultSuggestion 返回“未使用 AI 时”的默认下次抽烟时间建议(阶梯式延时算法)。
|
||||
func (s *SmokeNextService) GetDefaultSuggestion(ctx context.Context, uid int, asOf time.Time, planDate time.Time, profileView SmokeProfileView) (NextSmokeSuggestion, error) {
|
||||
now := asOf.In(time.Local)
|
||||
planDay := dateOnly(planDate)
|
||||
today := dateOnly(now)
|
||||
|
||||
base := profileView.BaselineIntervalMinute
|
||||
if base <= 0 {
|
||||
@@ -46,8 +47,11 @@ func (s *SmokeNextService) GetDefaultSuggestion(ctx context.Context, uid int, as
|
||||
return NextSmokeSuggestion{}, err
|
||||
}
|
||||
if !ok {
|
||||
nowCopy := now
|
||||
lastSmokeAt = &nowCopy
|
||||
lastCopy := now
|
||||
lastSmokeAt = &lastCopy
|
||||
} else if lastSmokeAt.After(now) {
|
||||
clamped := now
|
||||
lastSmokeAt = &clamped
|
||||
}
|
||||
|
||||
resisted, err := s.countResistedLastDays(ctx, uid, 7)
|
||||
@@ -68,7 +72,22 @@ func (s *SmokeNextService) GetDefaultSuggestion(ctx context.Context, uid int, as
|
||||
interval = 240
|
||||
}
|
||||
|
||||
next := lastSmokeAt.Add(time.Duration(interval) * time.Minute)
|
||||
intervalDuration := time.Duration(interval) * time.Minute
|
||||
next := lastSmokeAt.Add(intervalDuration)
|
||||
|
||||
if !planDay.After(today) && next.Before(now) {
|
||||
if intervalDuration <= 0 {
|
||||
next = now
|
||||
} else {
|
||||
elapsed := now.Sub(*lastSmokeAt)
|
||||
missed := int(elapsed / intervalDuration)
|
||||
if missed < 0 {
|
||||
missed = 0
|
||||
}
|
||||
next = lastSmokeAt.Add(time.Duration(missed+1) * intervalDuration)
|
||||
}
|
||||
}
|
||||
|
||||
sleepAdjusted := false
|
||||
|
||||
var wakeUp, sleep string
|
||||
@@ -78,7 +97,6 @@ func (s *SmokeNextService) GetDefaultSuggestion(ctx context.Context, uid int, as
|
||||
}
|
||||
|
||||
// 如果是“生成某一天的计划”(例如明天),默认不早于该日的起床时间(若未配置则使用 07:00)。
|
||||
today := dateOnly(now)
|
||||
if planDay.After(today) {
|
||||
minNotBefore := time.Date(planDay.Year(), planDay.Month(), planDay.Day(), 7, 0, 0, 0, time.Local)
|
||||
if wakeUp != "" {
|
||||
@@ -105,7 +123,7 @@ func (s *SmokeNextService) GetDefaultSuggestion(ctx context.Context, uid int, as
|
||||
out := NextSmokeSuggestion{
|
||||
BaseIntervalMinutes: base,
|
||||
IntervalMinutes: interval,
|
||||
Stage: stage,
|
||||
Stage: stage,
|
||||
Resisted7d: resisted,
|
||||
SleepAdjusted: sleepAdjusted,
|
||||
Algorithm: "staircase_delay_v1",
|
||||
@@ -123,7 +141,7 @@ func (s *SmokeNextService) loadLastActualSmokeAt(ctx context.Context, uid int) (
|
||||
var last smokemodel.SmokeLog
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("uid = ? AND (deletetime IS NULL OR deletetime = 0)", uid).
|
||||
Where("NOT (level = 0 AND num = 0)").
|
||||
Where("num > 0").
|
||||
Order("COALESCE(smoke_at, FROM_UNIXTIME(createtime), smoke_time) DESC").
|
||||
Order("id DESC").
|
||||
Limit(1).
|
||||
@@ -152,7 +170,7 @@ func (s *SmokeNextService) countResistedLastDays(ctx context.Context, uid int, d
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&smokemodel.SmokeLog{}).
|
||||
Where("uid = ? AND (deletetime IS NULL OR deletetime = 0)", uid).
|
||||
Where("level = 0 AND num = 0").
|
||||
Where("num = 0").
|
||||
Where("smoke_time BETWEEN ? AND ?", start.Format("2006-01-02"), end.Format("2006-01-02")).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, fmt.Errorf("count resisted logs: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user