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:
@@ -83,6 +83,7 @@ func (s *SmokeLogService) Stats(ctx context.Context, uid int, req SmokeStatsRequ
|
||||
if err != nil {
|
||||
return SmokeStatsResult{}, err
|
||||
}
|
||||
trend = limitTrend(trend, 7)
|
||||
|
||||
dayCount := daysBetweenInclusive(start, end)
|
||||
dailyAvg := 0
|
||||
@@ -109,7 +110,7 @@ func (s *SmokeLogService) Stats(ctx context.Context, uid int, req SmokeStatsRequ
|
||||
return SmokeStatsResult{}, err
|
||||
}
|
||||
|
||||
money := s.computeMoney(profile, int(total), dayCount)
|
||||
money := s.computeMoney(ctx, uid, profile, int(total), start, end)
|
||||
health, err := s.computeHealth(ctx, uid, req.AsOf)
|
||||
if err != nil {
|
||||
return SmokeStatsResult{}, err
|
||||
@@ -130,6 +131,24 @@ func (s *SmokeLogService) Stats(ctx context.Context, uid int, req SmokeStatsRequ
|
||||
}, nil
|
||||
}
|
||||
|
||||
func limitTrend(items []SmokeStatsTrend, max int) []SmokeStatsTrend {
|
||||
if max <= 0 || len(items) <= max {
|
||||
return items
|
||||
}
|
||||
lastIndex := len(items) - 1
|
||||
out := make([]SmokeStatsTrend, 0, max)
|
||||
seen := make(map[int]struct{}, max)
|
||||
for i := 0; i < max; i++ {
|
||||
pos := int(math.Round(float64(i) * float64(lastIndex) / float64(max-1)))
|
||||
if _, ok := seen[pos]; ok {
|
||||
continue
|
||||
}
|
||||
seen[pos] = struct{}{}
|
||||
out = append(out, items[pos])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *SmokeLogService) loadDailyTrend(ctx context.Context, uid int, start, end time.Time) ([]SmokeStatsTrend, int64, error) {
|
||||
type dailyCount struct {
|
||||
SmokeTime time.Time `gorm:"column:smoke_time"`
|
||||
@@ -225,7 +244,7 @@ func (s *SmokeLogService) countResisted(ctx context.Context, uid int, start, end
|
||||
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: %w", err)
|
||||
@@ -267,12 +286,31 @@ func (s *SmokeLogService) computeStreakDays(ctx context.Context, uid int, asOf t
|
||||
return streak, nil
|
||||
}
|
||||
|
||||
func (s *SmokeLogService) computeMoney(profile *smokemodel.SmokeUserProfile, actualTotal int, dayCount int) SmokeStatsMoney {
|
||||
if profile == nil || profile.BaselineCigsPerDay <= 0 || profile.PackPriceCent <= 0 || dayCount <= 0 {
|
||||
func (s *SmokeLogService) computeMoney(ctx context.Context, uid int, profile *smokemodel.SmokeUserProfile, actualTotal int, start, end time.Time) SmokeStatsMoney {
|
||||
if profile == nil || profile.BaselineCigsPerDay <= 0 || profile.PackPriceCent <= 0 {
|
||||
return SmokeStatsMoney{Available: false}
|
||||
}
|
||||
expectedTotal := profile.BaselineCigsPerDay * dayCount
|
||||
|
||||
activeDays, err := s.countLogDays(ctx, uid, start, end)
|
||||
if err != nil {
|
||||
return SmokeStatsMoney{Available: false}
|
||||
}
|
||||
if activeDays <= 0 {
|
||||
return SmokeStatsMoney{
|
||||
Available: true,
|
||||
PackPriceCent: profile.PackPriceCent,
|
||||
CigsPerPack: defaultCigsPerPack,
|
||||
ExpectedTotal: 0,
|
||||
ActualTotal: actualTotal,
|
||||
SavedCent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
expectedTotal := profile.BaselineCigsPerDay * activeDays
|
||||
savedCigs := expectedTotal - actualTotal
|
||||
if savedCigs < 0 {
|
||||
savedCigs = 0
|
||||
}
|
||||
savedPacks := float64(savedCigs) / float64(defaultCigsPerPack)
|
||||
savedCent := int(math.Round(savedPacks * float64(profile.PackPriceCent)))
|
||||
return SmokeStatsMoney{
|
||||
@@ -285,6 +323,25 @@ func (s *SmokeLogService) computeMoney(profile *smokemodel.SmokeUserProfile, act
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SmokeLogService) countLogDays(ctx context.Context, uid int, start, end time.Time) (int, error) {
|
||||
start = dateOnly(start)
|
||||
end = dateOnly(end)
|
||||
|
||||
type row struct {
|
||||
SmokeTime time.Time `gorm:"column:smoke_time"`
|
||||
}
|
||||
var rows []row
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&smokemodel.SmokeLog{}).
|
||||
Distinct("smoke_time").
|
||||
Where("uid = ? AND (deletetime IS NULL OR deletetime = 0)", uid).
|
||||
Where("smoke_time BETWEEN ? AND ?", start.Format("2006-01-02"), end.Format("2006-01-02")).
|
||||
Find(&rows).Error; err != nil {
|
||||
return 0, fmt.Errorf("count log days: %w", err)
|
||||
}
|
||||
return len(rows), nil
|
||||
}
|
||||
|
||||
func (s *SmokeLogService) computeHealth(ctx context.Context, uid int, asOf time.Time) (SmokeStatsHealth, error) {
|
||||
lastSmokeAt, err := s.loadLastActualSmokeAt(ctx, uid)
|
||||
if err != nil {
|
||||
@@ -311,7 +368,7 @@ func (s *SmokeLogService) loadLastActualSmokeAt(ctx context.Context, uid int) (*
|
||||
var last smokemodel.SmokeLog
|
||||
if 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).
|
||||
|
||||
Reference in New Issue
Block a user