Files
wx_service/internal/admin/service/smoke_service.go
T
2026-04-16 11:06:14 +08:00

1083 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"strings"
"time"
smokemodel "wx_service/internal/smoke/model"
"gorm.io/gorm"
)
// ===== 戒烟记录(fa_smoke_log =====
// ListSmokeLogsQuery 定义戒烟记录列表查询参数。
// 说明:后台列表默认按 id 倒序,支持按 uid 与日期区间过滤。
type ListSmokeLogsQuery struct {
Page int
PageSize int
UID int
DateFrom *time.Time
DateTo *time.Time
}
type SmokeLogItem struct {
ID int `json:"id"`
UID int `json:"uid"`
SmokeTime *time.Time `json:"smoke_time,omitempty"`
SmokeAt *time.Time `json:"smoke_at,omitempty"`
Remark string `json:"remark"`
ReasonTags smokemodel.StringSlice `json:"reason_tags,omitempty"`
Level int64 `json:"level"`
Num int `json:"num"`
CreateTime *int64 `json:"createtime,omitempty"`
UpdateTime *int64 `json:"updatetime,omitempty"`
}
type ListSmokeLogsResult struct {
List []SmokeLogItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// SmokeLogUpsertInput 用于新增与更新戒烟记录。
// 说明:更新时可只传部分字段(指针字段支持局部更新)。
type SmokeLogUpsertInput struct {
UID *int
SmokeTime **time.Time
SmokeAt **time.Time
Remark *string
ReasonTags *smokemodel.StringSlice
Level *int64
Num *int
}
func (s *Service) ListSmokeLogs(ctx context.Context, query ListSmokeLogsQuery) (*ListSmokeLogsResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
dbQuery := s.db.WithContext(ctx).
Model(&smokemodel.SmokeLog{}).
Where("deletetime IS NULL OR deletetime = 0")
if query.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", query.UID)
}
if query.DateFrom != nil {
dbQuery = dbQuery.Where("smoke_time >= ?", query.DateFrom.Format("2006-01-02"))
}
if query.DateTo != nil {
dbQuery = dbQuery.Where("smoke_time <= ?", query.DateTo.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeLog, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").
Limit(query.PageSize).
Offset((query.Page - 1) * query.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeLogItem, 0, len(rows))
for _, row := range rows {
list = append(list, SmokeLogItem{
ID: row.ID,
UID: row.UID,
SmokeTime: row.SmokeTime,
SmokeAt: row.SmokeAt,
Remark: row.Remark,
ReasonTags: row.ReasonTags,
Level: row.Level,
Num: row.Num,
CreateTime: row.CreateTime,
UpdateTime: row.UpdateTime,
})
}
return &ListSmokeLogsResult{
List: list,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}, nil
}
func (s *Service) GetSmokeLog(ctx context.Context, id int) (*SmokeLogItem, error) {
var row smokemodel.SmokeLog
if err := s.db.WithContext(ctx).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeLogNotFound
}
return nil, err
}
return &SmokeLogItem{
ID: row.ID,
UID: row.UID,
SmokeTime: row.SmokeTime,
SmokeAt: row.SmokeAt,
Remark: row.Remark,
ReasonTags: row.ReasonTags,
Level: row.Level,
Num: row.Num,
CreateTime: row.CreateTime,
UpdateTime: row.UpdateTime,
}, nil
}
func (s *Service) CreateSmokeLog(ctx context.Context, input SmokeLogUpsertInput) (*SmokeLogItem, error) {
if input.UID == nil || *input.UID <= 0 {
return nil, ErrInvalidInput
}
level := int64(1)
if input.Level != nil && *input.Level > 0 {
level = *input.Level
}
num := 1
if input.Num != nil && *input.Num > 0 {
num = *input.Num
}
nowUnix := nowUnixPtr()
row := &smokemodel.SmokeLog{
UID: *input.UID,
Level: level,
Num: num,
CreateTime: nowUnix,
UpdateTime: nowUnix,
}
if input.SmokeTime != nil {
row.SmokeTime = *input.SmokeTime
}
if input.SmokeAt != nil {
row.SmokeAt = *input.SmokeAt
}
if input.Remark != nil {
row.Remark = strings.TrimSpace(*input.Remark)
}
if input.ReasonTags != nil {
row.ReasonTags = *input.ReasonTags
}
if err := s.db.WithContext(ctx).Create(row).Error; err != nil {
return nil, err
}
return s.GetSmokeLog(ctx, row.ID)
}
func (s *Service) UpdateSmokeLog(ctx context.Context, id int, input SmokeLogUpsertInput) (*SmokeLogItem, error) {
updates := map[string]interface{}{
"updatetime": time.Now().Unix(),
}
if input.UID != nil && *input.UID > 0 {
updates["uid"] = *input.UID
}
if input.SmokeTime != nil {
updates["smoke_time"] = *input.SmokeTime
}
if input.SmokeAt != nil {
updates["smoke_at"] = *input.SmokeAt
}
if input.Remark != nil {
updates["remark"] = strings.TrimSpace(*input.Remark)
}
if input.ReasonTags != nil {
updates["reason_tags"] = *input.ReasonTags
}
if input.Level != nil {
if *input.Level <= 0 {
return nil, ErrInvalidInput
}
updates["level"] = *input.Level
}
if input.Num != nil {
if *input.Num <= 0 {
return nil, ErrInvalidInput
}
updates["num"] = *input.Num
}
result := s.db.WithContext(ctx).
Model(&smokemodel.SmokeLog{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(updates)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, ErrSmokeLogNotFound
}
return s.GetSmokeLog(ctx, id)
}
func (s *Service) DeleteSmokeLog(ctx context.Context, id int) error {
nowUnix := time.Now().Unix()
result := s.db.WithContext(ctx).
Model(&smokemodel.SmokeLog{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(map[string]interface{}{
"deletetime": nowUnix,
"updatetime": nowUnix,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeLogNotFound
}
return nil
}
// ===== 用户画像(fa_smoke_user_profile =====
type ListSmokeProfilesQuery struct {
Page int
PageSize int
UID int
}
type SmokeProfileItem struct {
ID uint `json:"id"`
UID int `json:"uid"`
Mode string `json:"mode,omitempty"`
BaselineCigsPerDay int `json:"baseline_cigs_per_day"`
SmokingYears float64 `json:"smoking_years"`
PackPriceCent int `json:"pack_price_cent"`
SmokeMotivations smokemodel.StringSlice `json:"smoke_motivations"`
QuitMotivations smokemodel.StringSlice `json:"quit_motivations"`
WakeUpTime string `json:"wake_up_time"`
SleepTime string `json:"sleep_time"`
QuitDate *time.Time `json:"quit_date,omitempty"`
AchievementThemeID *uint `json:"achievement_theme_id,omitempty"`
OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListSmokeProfilesResult struct {
List []SmokeProfileItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type SmokeProfileUpsertInput struct {
UID *int
BaselineCigsPerDay *int
SmokingYears *float64
PackPriceCent *int
SmokeMotivations *smokemodel.StringSlice
QuitMotivations *smokemodel.StringSlice
WakeUpTime *string
SleepTime *string
QuitDate **time.Time
OnboardingCompletedAt **time.Time
}
func (s *Service) ListSmokeProfiles(ctx context.Context, query ListSmokeProfilesQuery) (*ListSmokeProfilesResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
dbQuery := s.db.WithContext(ctx).Model(&smokemodel.SmokeUserProfile{})
if query.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", query.UID)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeUserProfile, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").
Limit(query.PageSize).
Offset((query.Page - 1) * query.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeProfileItem, 0, len(rows))
for _, row := range rows {
list = append(list, convertSmokeProfile(row))
}
return &ListSmokeProfilesResult{List: list, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (s *Service) GetSmokeProfile(ctx context.Context, id uint) (*SmokeProfileItem, error) {
var row smokemodel.SmokeUserProfile
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeProfileNotFound
}
return nil, err
}
item := convertSmokeProfile(row)
return &item, nil
}
func (s *Service) CreateSmokeProfile(ctx context.Context, input SmokeProfileUpsertInput) (*SmokeProfileItem, error) {
if input.UID == nil || *input.UID <= 0 {
return nil, ErrInvalidInput
}
row := smokemodel.SmokeUserProfile{UID: *input.UID}
applySmokeProfileInput(&row, input)
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrInvalidInput
}
return nil, err
}
return s.GetSmokeProfile(ctx, row.ID)
}
func (s *Service) UpdateSmokeProfile(ctx context.Context, id uint, input SmokeProfileUpsertInput) (*SmokeProfileItem, error) {
var row smokemodel.SmokeUserProfile
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeProfileNotFound
}
return nil, err
}
applySmokeProfileInput(&row, input)
if err := s.db.WithContext(ctx).Save(&row).Error; err != nil {
return nil, err
}
return s.GetSmokeProfile(ctx, id)
}
func (s *Service) DeleteSmokeProfile(ctx context.Context, id uint) error {
result := s.db.WithContext(ctx).Delete(&smokemodel.SmokeUserProfile{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeProfileNotFound
}
return nil
}
func convertSmokeProfile(row smokemodel.SmokeUserProfile) SmokeProfileItem {
return SmokeProfileItem{
ID: row.ID,
UID: row.UID,
Mode: row.Mode,
BaselineCigsPerDay: row.BaselineCigsPerDay,
SmokingYears: row.SmokingYears,
PackPriceCent: row.PackPriceCent,
SmokeMotivations: row.SmokeMotivations,
QuitMotivations: row.QuitMotivations,
WakeUpTime: row.WakeUpTime,
SleepTime: row.SleepTime,
QuitDate: row.QuitDate,
AchievementThemeID: row.AchievementThemeID,
OnboardingCompletedAt: row.OnboardingCompletedAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func applySmokeProfileInput(row *smokemodel.SmokeUserProfile, input SmokeProfileUpsertInput) {
if input.UID != nil && *input.UID > 0 {
row.UID = *input.UID
}
if input.BaselineCigsPerDay != nil {
row.BaselineCigsPerDay = *input.BaselineCigsPerDay
}
if input.SmokingYears != nil {
row.SmokingYears = *input.SmokingYears
}
if input.PackPriceCent != nil {
row.PackPriceCent = *input.PackPriceCent
}
if input.SmokeMotivations != nil {
row.SmokeMotivations = *input.SmokeMotivations
}
if input.QuitMotivations != nil {
row.QuitMotivations = *input.QuitMotivations
}
if input.WakeUpTime != nil {
row.WakeUpTime = strings.TrimSpace(*input.WakeUpTime)
}
if input.SleepTime != nil {
row.SleepTime = strings.TrimSpace(*input.SleepTime)
}
if input.QuitDate != nil {
row.QuitDate = *input.QuitDate
}
if input.OnboardingCompletedAt != nil {
row.OnboardingCompletedAt = *input.OnboardingCompletedAt
}
}
// ===== AI 建议(fa_smoke_ai_advice =====
type ListSmokeAIAdvicesQuery struct {
Page int
PageSize int
UID int
Type string
AdviceDate *time.Time
}
type SmokeAIAdviceItem struct {
ID uint `json:"id"`
UID int `json:"uid"`
Type string `json:"type"`
AdviceDate time.Time `json:"advice_date"`
PromptVersion string `json:"prompt_version"`
Provider string `json:"provider"`
Model string `json:"model"`
InputSnapshot string `json:"input_snapshot"`
Advice string `json:"advice"`
TokensIn *int `json:"tokens_in,omitempty"`
TokensOut *int `json:"tokens_out,omitempty"`
CostCent *int `json:"cost_cent,omitempty"`
CreateTime *int64 `json:"createtime,omitempty"`
UpdateTime *int64 `json:"updatetime,omitempty"`
}
type ListSmokeAIAdvicesResult struct {
List []SmokeAIAdviceItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type SmokeAIAdviceUpsertInput struct {
UID *int
Type *string
AdviceDate *time.Time
PromptVersion *string
Provider *string
Model *string
InputSnapshot *string
Advice *string
TokensIn *int
TokensOut *int
CostCent *int
}
func (s *Service) ListSmokeAIAdvices(ctx context.Context, query ListSmokeAIAdvicesQuery) (*ListSmokeAIAdvicesResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
query.Type = strings.TrimSpace(query.Type)
dbQuery := s.db.WithContext(ctx).
Model(&smokemodel.SmokeAIAdvice{}).
Where("deletetime IS NULL OR deletetime = 0")
if query.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", query.UID)
}
if query.Type != "" {
dbQuery = dbQuery.Where("type = ?", query.Type)
}
if query.AdviceDate != nil {
dbQuery = dbQuery.Where("advice_date = ?", query.AdviceDate.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeAIAdvice, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").
Limit(query.PageSize).
Offset((query.Page - 1) * query.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeAIAdviceItem, 0, len(rows))
for _, row := range rows {
list = append(list, convertSmokeAIAdvice(row))
}
return &ListSmokeAIAdvicesResult{List: list, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (s *Service) GetSmokeAIAdvice(ctx context.Context, id uint) (*SmokeAIAdviceItem, error) {
var row smokemodel.SmokeAIAdvice
if err := s.db.WithContext(ctx).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeAIAdviceNotFound
}
return nil, err
}
item := convertSmokeAIAdvice(row)
return &item, nil
}
func (s *Service) CreateSmokeAIAdvice(ctx context.Context, input SmokeAIAdviceUpsertInput) (*SmokeAIAdviceItem, error) {
if input.UID == nil || *input.UID <= 0 || input.Advice == nil || strings.TrimSpace(*input.Advice) == "" || input.AdviceDate == nil {
return nil, ErrInvalidInput
}
now := nowUnixPtr()
typeValue := "daily_advice"
if input.Type != nil && strings.TrimSpace(*input.Type) != "" {
typeValue = strings.TrimSpace(*input.Type)
}
promptVersion := "v1"
if input.PromptVersion != nil && strings.TrimSpace(*input.PromptVersion) != "" {
promptVersion = strings.TrimSpace(*input.PromptVersion)
}
row := smokemodel.SmokeAIAdvice{
UID: *input.UID,
Type: typeValue,
AdviceDate: *input.AdviceDate,
PromptVersion: promptVersion,
Advice: strings.TrimSpace(*input.Advice),
CreateTime: now,
UpdateTime: now,
}
applySmokeAIAdviceInput(&row, input)
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrInvalidInput
}
return nil, err
}
return s.GetSmokeAIAdvice(ctx, row.ID)
}
func (s *Service) UpdateSmokeAIAdvice(ctx context.Context, id uint, input SmokeAIAdviceUpsertInput) (*SmokeAIAdviceItem, error) {
var row smokemodel.SmokeAIAdvice
if err := s.db.WithContext(ctx).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeAIAdviceNotFound
}
return nil, err
}
applySmokeAIAdviceInput(&row, input)
now := time.Now().Unix()
row.UpdateTime = &now
if err := s.db.WithContext(ctx).Save(&row).Error; err != nil {
return nil, err
}
return s.GetSmokeAIAdvice(ctx, id)
}
func (s *Service) DeleteSmokeAIAdvice(ctx context.Context, id uint) error {
now := time.Now().Unix()
result := s.db.WithContext(ctx).
Model(&smokemodel.SmokeAIAdvice{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(map[string]interface{}{"deletetime": now, "updatetime": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeAIAdviceNotFound
}
return nil
}
func convertSmokeAIAdvice(row smokemodel.SmokeAIAdvice) SmokeAIAdviceItem {
return SmokeAIAdviceItem{
ID: row.ID,
UID: row.UID,
Type: row.Type,
AdviceDate: row.AdviceDate,
PromptVersion: row.PromptVersion,
Provider: row.Provider,
Model: row.Model,
InputSnapshot: string(row.InputSnapshot),
Advice: row.Advice,
TokensIn: row.TokensIn,
TokensOut: row.TokensOut,
CostCent: row.CostCent,
CreateTime: row.CreateTime,
UpdateTime: row.UpdateTime,
}
}
func applySmokeAIAdviceInput(row *smokemodel.SmokeAIAdvice, input SmokeAIAdviceUpsertInput) {
if input.UID != nil && *input.UID > 0 {
row.UID = *input.UID
}
if input.Type != nil && strings.TrimSpace(*input.Type) != "" {
row.Type = strings.TrimSpace(*input.Type)
}
if input.AdviceDate != nil {
row.AdviceDate = *input.AdviceDate
}
if input.PromptVersion != nil && strings.TrimSpace(*input.PromptVersion) != "" {
row.PromptVersion = strings.TrimSpace(*input.PromptVersion)
}
if input.Provider != nil {
row.Provider = strings.TrimSpace(*input.Provider)
}
if input.Model != nil {
row.Model = strings.TrimSpace(*input.Model)
}
if input.InputSnapshot != nil {
row.InputSnapshot = []byte(strings.TrimSpace(*input.InputSnapshot))
}
if input.Advice != nil {
row.Advice = strings.TrimSpace(*input.Advice)
}
if input.TokensIn != nil {
row.TokensIn = input.TokensIn
}
if input.TokensOut != nil {
row.TokensOut = input.TokensOut
}
if input.CostCent != nil {
row.CostCent = input.CostCent
}
}
// ===== AI 解锁(fa_smoke_ai_advice_unlocks =====
type ListSmokeAIUnlocksQuery struct {
Page int
PageSize int
UID int
UnlockDate *time.Time
}
type SmokeAIUnlockItem struct {
ID uint `json:"id"`
UID int `json:"uid"`
UnlockDate time.Time `json:"unlock_date"`
AdWatchedAt time.Time `json:"ad_watched_at"`
CreateTime *int64 `json:"createtime,omitempty"`
UpdateTime *int64 `json:"updatetime,omitempty"`
}
type ListSmokeAIUnlocksResult struct {
List []SmokeAIUnlockItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type SmokeAIUnlockUpsertInput struct {
UID *int
UnlockDate *time.Time
AdWatchedAt *time.Time
}
func (s *Service) ListSmokeAIUnlocks(ctx context.Context, query ListSmokeAIUnlocksQuery) (*ListSmokeAIUnlocksResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
dbQuery := s.db.WithContext(ctx).
Model(&smokemodel.SmokeAIAdviceUnlock{}).
Where("deletetime IS NULL OR deletetime = 0")
if query.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", query.UID)
}
if query.UnlockDate != nil {
dbQuery = dbQuery.Where("unlock_date = ?", query.UnlockDate.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeAIAdviceUnlock, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").Limit(query.PageSize).Offset((query.Page - 1) * query.PageSize).Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeAIUnlockItem, 0, len(rows))
for _, row := range rows {
list = append(list, SmokeAIUnlockItem{ID: row.ID, UID: row.UID, UnlockDate: row.UnlockDate, AdWatchedAt: row.AdWatchedAt, CreateTime: row.CreateTime, UpdateTime: row.UpdateTime})
}
return &ListSmokeAIUnlocksResult{List: list, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (s *Service) GetSmokeAIUnlock(ctx context.Context, id uint) (*SmokeAIUnlockItem, error) {
var row smokemodel.SmokeAIAdviceUnlock
if err := s.db.WithContext(ctx).Where("id = ?", id).Where("deletetime IS NULL OR deletetime = 0").First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeAIUnlockNotFound
}
return nil, err
}
return &SmokeAIUnlockItem{ID: row.ID, UID: row.UID, UnlockDate: row.UnlockDate, AdWatchedAt: row.AdWatchedAt, CreateTime: row.CreateTime, UpdateTime: row.UpdateTime}, nil
}
func (s *Service) CreateSmokeAIUnlock(ctx context.Context, input SmokeAIUnlockUpsertInput) (*SmokeAIUnlockItem, error) {
if input.UID == nil || *input.UID <= 0 || input.UnlockDate == nil || input.AdWatchedAt == nil {
return nil, ErrInvalidInput
}
now := nowUnixPtr()
row := smokemodel.SmokeAIAdviceUnlock{
UID: *input.UID,
UnlockDate: *input.UnlockDate,
AdWatchedAt: *input.AdWatchedAt,
CreateTime: now,
UpdateTime: now,
}
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrInvalidInput
}
return nil, err
}
return s.GetSmokeAIUnlock(ctx, row.ID)
}
func (s *Service) UpdateSmokeAIUnlock(ctx context.Context, id uint, input SmokeAIUnlockUpsertInput) (*SmokeAIUnlockItem, error) {
updates := map[string]interface{}{"updatetime": time.Now().Unix()}
if input.UID != nil && *input.UID > 0 {
updates["uid"] = *input.UID
}
if input.UnlockDate != nil {
updates["unlock_date"] = *input.UnlockDate
}
if input.AdWatchedAt != nil {
updates["ad_watched_at"] = *input.AdWatchedAt
}
result := s.db.WithContext(ctx).Model(&smokemodel.SmokeAIAdviceUnlock{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(updates)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, ErrSmokeAIUnlockNotFound
}
return s.GetSmokeAIUnlock(ctx, id)
}
func (s *Service) DeleteSmokeAIUnlock(ctx context.Context, id uint) error {
now := time.Now().Unix()
result := s.db.WithContext(ctx).Model(&smokemodel.SmokeAIAdviceUnlock{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(map[string]interface{}{"deletetime": now, "updatetime": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeAIUnlockNotFound
}
return nil
}
// ===== AI 下次抽烟节点(fa_smoke_ai_next_smoke =====
type ListSmokeAINextsQuery struct {
Page int
PageSize int
UID int
PlanDate *time.Time
}
type SmokeAINextItem struct {
ID uint `json:"id"`
UID int `json:"uid"`
PlanDate time.Time `json:"plan_date"`
AIAdviceID uint `json:"ai_advice_id"`
NodeType string `json:"node_type"`
NodeAt time.Time `json:"node_at"`
CreateTime *int64 `json:"createtime,omitempty"`
UpdateTime *int64 `json:"updatetime,omitempty"`
}
type ListSmokeAINextsResult struct {
List []SmokeAINextItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type SmokeAINextUpsertInput struct {
UID *int
PlanDate *time.Time
AIAdviceID *uint
NodeType *string
NodeAt *time.Time
}
func (s *Service) ListSmokeAINexts(ctx context.Context, query ListSmokeAINextsQuery) (*ListSmokeAINextsResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
dbQuery := s.db.WithContext(ctx).
Model(&smokemodel.SmokeAINextSmoke{}).
Where("deletetime IS NULL OR deletetime = 0")
if query.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", query.UID)
}
if query.PlanDate != nil {
dbQuery = dbQuery.Where("plan_date = ?", query.PlanDate.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeAINextSmoke, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").Limit(query.PageSize).Offset((query.Page - 1) * query.PageSize).Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeAINextItem, 0, len(rows))
for _, row := range rows {
list = append(list, SmokeAINextItem{ID: row.ID, UID: row.UID, PlanDate: row.PlanDate, AIAdviceID: row.AIAdviceID, NodeType: row.NodeType, NodeAt: row.NodeAt, CreateTime: row.CreateTime, UpdateTime: row.UpdateTime})
}
return &ListSmokeAINextsResult{List: list, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (s *Service) GetSmokeAINext(ctx context.Context, id uint) (*SmokeAINextItem, error) {
var row smokemodel.SmokeAINextSmoke
if err := s.db.WithContext(ctx).Where("id = ?", id).Where("deletetime IS NULL OR deletetime = 0").First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeAINextNotFound
}
return nil, err
}
return &SmokeAINextItem{ID: row.ID, UID: row.UID, PlanDate: row.PlanDate, AIAdviceID: row.AIAdviceID, NodeType: row.NodeType, NodeAt: row.NodeAt, CreateTime: row.CreateTime, UpdateTime: row.UpdateTime}, nil
}
func (s *Service) CreateSmokeAINext(ctx context.Context, input SmokeAINextUpsertInput) (*SmokeAINextItem, error) {
if input.UID == nil || *input.UID <= 0 || input.PlanDate == nil || input.AIAdviceID == nil || *input.AIAdviceID == 0 || input.NodeType == nil || strings.TrimSpace(*input.NodeType) == "" || input.NodeAt == nil {
return nil, ErrInvalidInput
}
now := nowUnixPtr()
row := smokemodel.SmokeAINextSmoke{
UID: *input.UID,
PlanDate: *input.PlanDate,
AIAdviceID: *input.AIAdviceID,
NodeType: strings.TrimSpace(*input.NodeType),
NodeAt: *input.NodeAt,
CreateTime: now,
UpdateTime: now,
}
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrInvalidInput
}
return nil, err
}
return s.GetSmokeAINext(ctx, row.ID)
}
func (s *Service) UpdateSmokeAINext(ctx context.Context, id uint, input SmokeAINextUpsertInput) (*SmokeAINextItem, error) {
updates := map[string]interface{}{"updatetime": time.Now().Unix()}
if input.UID != nil && *input.UID > 0 {
updates["uid"] = *input.UID
}
if input.PlanDate != nil {
updates["plan_date"] = *input.PlanDate
}
if input.AIAdviceID != nil && *input.AIAdviceID > 0 {
updates["ai_advice_id"] = *input.AIAdviceID
}
if input.NodeType != nil {
nodeType := strings.TrimSpace(*input.NodeType)
if nodeType == "" {
return nil, ErrInvalidInput
}
updates["node_type"] = nodeType
}
if input.NodeAt != nil {
updates["node_at"] = *input.NodeAt
}
result := s.db.WithContext(ctx).Model(&smokemodel.SmokeAINextSmoke{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(updates)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, ErrSmokeAINextNotFound
}
return s.GetSmokeAINext(ctx, id)
}
func (s *Service) DeleteSmokeAINext(ctx context.Context, id uint) error {
now := time.Now().Unix()
result := s.db.WithContext(ctx).Model(&smokemodel.SmokeAINextSmoke{}).
Where("id = ?", id).
Where("deletetime IS NULL OR deletetime = 0").
Updates(map[string]interface{}{"deletetime": now, "updatetime": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeAINextNotFound
}
return nil
}
// ===== 激励语模板(fa_smoke_motivation_quote =====
type ListSmokeMotivationsQuery struct {
Page int
PageSize int
Scene string
Type string
Enabled *bool
}
type SmokeMotivationItem struct {
ID uint `json:"id"`
Scene string `json:"scene"`
Type string `json:"type"`
Message string `json:"message"`
AIPrompt string `json:"ai_prompt"`
Enabled bool `json:"enabled"`
Weight int `json:"weight"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListSmokeMotivationsResult struct {
List []SmokeMotivationItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type SmokeMotivationUpsertInput struct {
Scene *string
Type *string
Message *string
AIPrompt *string
Enabled *bool
Weight *int
}
func (s *Service) ListSmokeMotivations(ctx context.Context, query ListSmokeMotivationsQuery) (*ListSmokeMotivationsResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
query.Scene = strings.TrimSpace(query.Scene)
query.Type = strings.TrimSpace(query.Type)
dbQuery := s.db.WithContext(ctx).Model(&smokemodel.SmokeMotivationQuote{})
if query.Scene != "" {
dbQuery = dbQuery.Where("scene = ?", query.Scene)
}
if query.Type != "" {
dbQuery = dbQuery.Where("type = ?", query.Type)
}
if query.Enabled != nil {
dbQuery = dbQuery.Where("enabled = ?", *query.Enabled)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]smokemodel.SmokeMotivationQuote, 0)
if total > 0 {
if err := dbQuery.Order("id DESC").Limit(query.PageSize).Offset((query.Page - 1) * query.PageSize).Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]SmokeMotivationItem, 0, len(rows))
for _, row := range rows {
list = append(list, SmokeMotivationItem{ID: row.ID, Scene: row.Scene, Type: row.Type, Message: row.Message, AIPrompt: row.AIPrompt, Enabled: row.Enabled, Weight: row.Weight, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt})
}
return &ListSmokeMotivationsResult{List: list, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (s *Service) GetSmokeMotivation(ctx context.Context, id uint) (*SmokeMotivationItem, error) {
var row smokemodel.SmokeMotivationQuote
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeMotivationNotFound
}
return nil, err
}
return &SmokeMotivationItem{ID: row.ID, Scene: row.Scene, Type: row.Type, Message: row.Message, AIPrompt: row.AIPrompt, Enabled: row.Enabled, Weight: row.Weight, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
}
func (s *Service) CreateSmokeMotivation(ctx context.Context, input SmokeMotivationUpsertInput) (*SmokeMotivationItem, error) {
if input.Scene == nil || strings.TrimSpace(*input.Scene) == "" || input.Type == nil || strings.TrimSpace(*input.Type) == "" || input.Message == nil || strings.TrimSpace(*input.Message) == "" {
return nil, ErrInvalidInput
}
row := smokemodel.SmokeMotivationQuote{Scene: strings.TrimSpace(*input.Scene), Type: strings.TrimSpace(*input.Type), Message: strings.TrimSpace(*input.Message), Enabled: true, Weight: 1}
applySmokeMotivationInput(&row, input)
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
return nil, err
}
return s.GetSmokeMotivation(ctx, row.ID)
}
func (s *Service) UpdateSmokeMotivation(ctx context.Context, id uint, input SmokeMotivationUpsertInput) (*SmokeMotivationItem, error) {
var row smokemodel.SmokeMotivationQuote
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSmokeMotivationNotFound
}
return nil, err
}
applySmokeMotivationInput(&row, input)
if err := s.db.WithContext(ctx).Save(&row).Error; err != nil {
return nil, err
}
return s.GetSmokeMotivation(ctx, id)
}
func (s *Service) DeleteSmokeMotivation(ctx context.Context, id uint) error {
result := s.db.WithContext(ctx).Delete(&smokemodel.SmokeMotivationQuote{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrSmokeMotivationNotFound
}
return nil
}
func applySmokeMotivationInput(row *smokemodel.SmokeMotivationQuote, input SmokeMotivationUpsertInput) {
if input.Scene != nil {
row.Scene = strings.TrimSpace(*input.Scene)
}
if input.Type != nil {
row.Type = strings.TrimSpace(*input.Type)
}
if input.Message != nil {
row.Message = strings.TrimSpace(*input.Message)
}
if input.AIPrompt != nil {
row.AIPrompt = strings.TrimSpace(*input.AIPrompt)
}
if input.Enabled != nil {
row.Enabled = *input.Enabled
}
if input.Weight != nil {
row.Weight = *input.Weight
}
}
func nowUnixPtr() *int64 {
now := time.Now().Unix()
return &now
}