package service import ( "context" "errors" "fmt" "time" "gorm.io/gorm" smokemodel "wx_service/internal/smoke/model" ) var ( ErrSmokeLogNotFound = errors.New("smoke log not found") ) type SmokeLogService struct { db *gorm.DB } func NewSmokeLogService(db *gorm.DB) *SmokeLogService { return &SmokeLogService{db: db} } type CreateSmokeLogRequest struct { SmokeTime *time.Time SmokeAt *time.Time Remark string Level int64 Num int } func (s *SmokeLogService) Create(ctx context.Context, uid int, req CreateSmokeLogRequest) (*smokemodel.SmokeLog, error) { now := time.Now().Unix() createTime := now updateTime := now level := req.Level if level < 0 { level = 1 } num := req.Num if num < 0 { num = 1 } smokeAt := req.SmokeAt // smokeTime 用于“按天筛选”;如果传了 smokeAt,建议用其日期部分回填 smokeTime,避免出现不一致。 smokeTime := req.SmokeTime if smokeAt != nil { day := time.Date(smokeAt.Year(), smokeAt.Month(), smokeAt.Day(), 0, 0, 0, 0, smokeAt.Location()) smokeTime = &day } if smokeTime == nil { today := time.Now() startOfDay := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()) smokeTime = &startOfDay } record := smokemodel.SmokeLog{ UID: uid, SmokeTime: smokeTime, SmokeAt: smokeAt, Remark: req.Remark, CreateTime: &createTime, UpdateTime: &updateTime, Level: level, Num: num, } if err := s.db.WithContext(ctx).Create(&record).Error; err != nil { return nil, fmt.Errorf("create smoke log: %w", err) } return &record, nil } func (s *SmokeLogService) GetByID(ctx context.Context, uid int, id int) (*smokemodel.SmokeLog, error) { var record smokemodel.SmokeLog err := s.db.WithContext(ctx). Where("id = ? AND uid = ? AND (deletetime IS NULL OR deletetime = 0)", id, uid). First(&record).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrSmokeLogNotFound } return nil, fmt.Errorf("load smoke log: %w", err) } return &record, nil } type ListSmokeLogsRequest struct { Page int PageSize int Start *time.Time End *time.Time } type ListSmokeLogsResult struct { Items []smokemodel.SmokeLog Total int64 Page int PageSize int } // SmokeDashboardRequest 定义了看板概览的时间范围(包含起止日期)。 type SmokeDashboardRequest struct { Start time.Time End time.Time } // SmokeDashboardResult 用于返回看板概览的关键指标。 type SmokeDashboardResult struct { TodayCount int `json:"today_count"` MinutesSinceLast *int `json:"minutes_since_last,omitempty"` Weekly []DashboardWeeklyStat `json:"weekly"` } // SmokeHomeSummary 汇总首页所需的关键指标。 type SmokeHomeSummary struct { LastSmokeAt *time.Time TodayCount int ResistedCount int ReducedFromYesterday int ExceededYesterday bool } // DashboardWeeklyStat 表示某一天的抽烟支数以及是否为今天。 type DashboardWeeklyStat struct { Date string `json:"date"` Count int `json:"count"` IsToday bool `json:"is_today"` } func (s *SmokeLogService) List(ctx context.Context, uid int, req ListSmokeLogsRequest) (ListSmokeLogsResult, error) { page := req.Page if page <= 0 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 20 } if pageSize > 200 { pageSize = 200 } tx := s.db.WithContext(ctx).Model(&smokemodel.SmokeLog{}). Where("uid = ? AND (deletetime IS NULL OR deletetime = 0)", uid) if req.Start != nil { tx = tx.Where("smoke_time >= ?", req.Start.Format("2006-01-02")) } if req.End != nil { tx = tx.Where("smoke_time <= ?", req.End.Format("2006-01-02")) } var total int64 if err := tx.Count(&total).Error; err != nil { return ListSmokeLogsResult{}, fmt.Errorf("count smoke logs: %w", err) } var items []smokemodel.SmokeLog offset := (page - 1) * pageSize if err := tx. Order("smoke_time DESC"). Order("id DESC"). Limit(pageSize). Offset(offset). Find(&items).Error; err != nil { return ListSmokeLogsResult{}, fmt.Errorf("list smoke logs: %w", err) } return ListSmokeLogsResult{ Items: items, Total: total, Page: page, PageSize: pageSize, }, nil } func (s *SmokeLogService) Dashboard(ctx context.Context, uid int, req SmokeDashboardRequest) (SmokeDashboardResult, error) { start := dateOnly(req.Start) end := dateOnly(req.End) type dailyCount struct { SmokeTime time.Time `gorm:"column:smoke_time"` Total int64 `gorm:"column:total"` } var rows []dailyCount if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Select("smoke_time, SUM(num) AS total"). 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")). Group("smoke_time"). Find(&rows).Error; err != nil { return SmokeDashboardResult{}, fmt.Errorf("aggregate smoke logs: %w", err) } counts := make(map[string]int64, len(rows)) for _, row := range rows { key := dateOnly(row.SmokeTime).Format("2006-01-02") counts[key] = row.Total } today := dateOnly(time.Now()) todayKey := today.Format("2006-01-02") var todayCount int64 if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Where("uid = ? AND (deletetime IS NULL OR deletetime = 0) AND smoke_time = ?", uid, todayKey). Select("COALESCE(SUM(num), 0)"). Scan(&todayCount).Error; err != nil { return SmokeDashboardResult{}, fmt.Errorf("count today smoke logs: %w", err) } var minutesSinceLast *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)"). Order("COALESCE(smoke_at, FROM_UNIXTIME(createtime), smoke_time) DESC"). Order("id DESC"). Limit(1). Take(&last).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return SmokeDashboardResult{}, fmt.Errorf("load last smoke log: %w", err) } } else { if lastTime, ok := lastEventTime(last); ok { diff := int(time.Since(lastTime).Minutes()) if diff < 0 { diff = 0 } minutesSinceLast = &diff } } var weekly []DashboardWeeklyStat for day := start; !day.After(end); day = day.AddDate(0, 0, 1) { key := day.Format("2006-01-02") count := counts[key] weekly = append(weekly, DashboardWeeklyStat{ Date: key, Count: int(count), IsToday: key == todayKey, }) } return SmokeDashboardResult{ TodayCount: int(todayCount), MinutesSinceLast: minutesSinceLast, Weekly: weekly, }, nil } // HomeSummary 返回首页所需的汇总数据(不包含时间范围的周统计)。 func (s *SmokeLogService) HomeSummary(ctx context.Context, uid int, asOf time.Time) (SmokeHomeSummary, error) { today := dateOnly(asOf) todayKey := today.Format("2006-01-02") yesterdayKey := today.AddDate(0, 0, -1).Format("2006-01-02") var todayCount int64 if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Where("uid = ? AND (deletetime IS NULL OR deletetime = 0) AND smoke_time = ?", uid, todayKey). Select("COALESCE(SUM(num), 0)"). Scan(&todayCount).Error; err != nil { return SmokeHomeSummary{}, fmt.Errorf("count today smoke logs: %w", err) } var resistedCount int64 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 AND smoke_time = ?", todayKey). Count(&resistedCount).Error; err != nil { return SmokeHomeSummary{}, fmt.Errorf("count resisted logs: %w", err) } var yesterdayCount int64 if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Where("uid = ? AND (deletetime IS NULL OR deletetime = 0) AND smoke_time = ?", uid, yesterdayKey). Select("COALESCE(SUM(num), 0)"). Scan(&yesterdayCount).Error; err != nil { return SmokeHomeSummary{}, fmt.Errorf("count yesterday smoke logs: %w", err) } reduced := int(yesterdayCount - todayCount) exceeded := reduced < 0 var lastSmokeAt *time.Time 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)"). Order("COALESCE(smoke_at, FROM_UNIXTIME(createtime), smoke_time) DESC"). Order("id DESC"). Limit(1). Take(&last).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return SmokeHomeSummary{}, fmt.Errorf("load last smoke log: %w", err) } } else if t, ok := lastEventTime(last); ok { lastSmokeAt = &t } return SmokeHomeSummary{ LastSmokeAt: lastSmokeAt, TodayCount: int(todayCount), ResistedCount: int(resistedCount), ReducedFromYesterday: reduced, ExceededYesterday: exceeded, }, nil } func (s *SmokeLogService) ListLatest(ctx context.Context, uid int, limit int) ([]smokemodel.SmokeLog, error) { if limit <= 0 { limit = 20 } if limit > 100 { limit = 100 } var items []smokemodel.SmokeLog if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Select("id, uid, smoke_time, smoke_at, remark, level, num, createtime, updatetime, deletetime"). Where("uid = ? AND (deletetime IS NULL OR deletetime = 0)", uid). Order("COALESCE(smoke_at, FROM_UNIXTIME(createtime), smoke_time) DESC"). Order("id DESC"). Limit(limit). Find(&items).Error; err != nil { return nil, fmt.Errorf("list latest smoke logs: %w", err) } return items, nil } func lastEventTime(log smokemodel.SmokeLog) (time.Time, bool) { if log.SmokeAt != nil { return log.SmokeAt.In(time.Local), true } if log.CreateTime != nil { return time.Unix(*log.CreateTime, 0).In(time.Local), true } if log.SmokeTime != nil { day := dateOnly(*log.SmokeTime) return day, true } return time.Time{}, false } type UpdateSmokeLogRequest struct { // SmokeTimeProvided 用于区分: // - false:前端没传 smoke_time(不修改) // - true:前端传了 smoke_time(可以设置为具体日期,也可以清空为 NULL) SmokeTimeProvided bool SmokeTime *time.Time // SmokeAtProvided 用于区分: // - false:前端没传 smoke_at(不修改) // - true:前端传了 smoke_at(可以设置为具体时间,也可以清空为 NULL) SmokeAtProvided bool SmokeAt *time.Time Remark *string Level *int64 Num *int } func (s *SmokeLogService) Update(ctx context.Context, uid int, id int, req UpdateSmokeLogRequest) (*smokemodel.SmokeLog, error) { record, err := s.GetByID(ctx, uid, id) if err != nil { return nil, err } updates := map[string]interface{}{} if req.SmokeTimeProvided { updates["smoke_time"] = req.SmokeTime } if req.SmokeAtProvided { updates["smoke_at"] = req.SmokeAt if req.SmokeAt != nil { day := time.Date(req.SmokeAt.Year(), req.SmokeAt.Month(), req.SmokeAt.Day(), 0, 0, 0, 0, req.SmokeAt.Location()) updates["smoke_time"] = &day } } if req.Remark != nil { updates["remark"] = *req.Remark } if req.Level != nil { if *req.Level < 0 { updates["level"] = int64(1) } else { updates["level"] = *req.Level } } if req.Num != nil { if *req.Num < 0 { updates["num"] = 1 } else { updates["num"] = *req.Num } } now := time.Now().Unix() updates["updatetime"] = now if len(updates) == 1 { return record, nil } if err := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Where("id = ? AND uid = ?", id, uid). Updates(updates).Error; err != nil { return nil, fmt.Errorf("update smoke log: %w", err) } return s.GetByID(ctx, uid, id) } func (s *SmokeLogService) Delete(ctx context.Context, uid int, id int) error { now := time.Now().Unix() result := s.db.WithContext(ctx). Model(&smokemodel.SmokeLog{}). Where("id = ? AND uid = ? AND (deletetime IS NULL OR deletetime = 0)", id, uid). Updates(map[string]interface{}{ "deletetime": now, "updatetime": now, }) if result.Error != nil { return fmt.Errorf("delete smoke log: %w", result.Error) } if result.RowsAffected == 0 { return ErrSmokeLogNotFound } return nil }