1083 lines
33 KiB
Go
1083 lines
33 KiB
Go
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
|
||
}
|