6cf7eb2294
- Added new API endpoint `GET /api/v1/smoke/next_smoke_time` to provide AI-generated suggestions for the next smoking time based on user data. - Introduced a new database table `fa_smoke_ai_next_smoke` to store structured AI time node suggestions. - Updated smoke handler and service to integrate the new AI next smoke time functionality. - Enhanced documentation to reflect the new API endpoint and its usage, including details on how to generate AI time nodes.
373 lines
9.5 KiB
Go
373 lines
9.5 KiB
Go
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"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|