Files
2026-04-27 10:43:42 +08:00

667 lines
20 KiB
Go
Raw Permalink 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 plant
import (
"context"
"errors"
"sundynix-go/global"
common "sundynix-go/model/commom/request"
"sundynix-go/model/plant"
plantReq "sundynix-go/model/plant/request"
plantRes "sundynix-go/model/plant/response"
"sundynix-go/model/system"
"sundynix-go/utils/timer"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
type MyPlantService struct{}
var MyPlantServiceApp = new(MyPlantService)
// AddPlant 添加植物
func (s *MyPlantService) AddPlant(req plantReq.CreateMyPlant, userId string) error {
err := global.DB.Transaction(func(tx *gorm.DB) error {
//1. 验证oss是否存在
var ossList []*system.Oss
err := tx.Where("id in ?", req.OssIds).Find(&ossList).Error
if err != nil {
return err
}
bgTime := timer.GetZeroTime()
//2. 处理养护计划
var carePlans []*plant.CarePlan
for _, v := range req.CarePlans {
carePlans = append(carePlans, &plant.CarePlan{
UserId: userId,
Name: v.Name,
Icon: v.Icon,
Period: v.Period,
TargetAction: v.TargetAction,
})
}
//3.保存数据 myPlant钩子函数自动处理创建careTask
myPlant := plant.MyPlant{
UserId: userId,
Name: req.Name,
PlantTime: bgTime,
Status: 1,
Placement: req.Placement,
PotMaterial: req.PotMaterial,
PotSize: req.PotSize,
Sunlight: req.Sunlight,
PlantingMaterial: req.PlantingMaterial,
CarePlans: carePlans,
}
err = tx.Create(&myPlant).Error
if err != nil {
return err
}
//4.处理图片关系
if len(ossList) > 0 {
var relations []map[string]interface{}
for _, oss := range ossList {
relations = append(relations, map[string]interface{}{
"my_plant_id": myPlant.Id,
"oss_id": oss.Id,
})
}
err = tx.Table("sundynix_my_plant_oss").Create(relations).Error
if err != nil {
return err
}
}
//5.更新用户中心数据
err = tx.Model(&plant.UserProfile{}).Where("user_id = ?", userId).
Update("plant_count", gorm.Expr("plant_count + 1")).Error
if err != nil {
return err
}
return nil
})
return err
}
// PlantPage 植物列表
func (s *MyPlantService) PlantPage(req common.PageInfo, userId string) (list interface{}, total int64, err error) {
limit := req.PageSize
offset := req.PageSize * (req.Current - 1)
db := global.DB.Model(&plant.MyPlant{}).Preload("ImgList", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
})
var myPlants []*plant.MyPlant
db = db.Where("user_id = ?", userId)
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&myPlants).Error
return myPlants, total, err
}
// PlantDetail 植物详情
func (s *MyPlantService) PlantDetail(id string) (p plant.MyPlant, err error) {
var res plant.MyPlant
err = global.DB.Where("id = ?", id).
Preload("ImgList", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}).
Preload("CarePlans").
Preload("CareRecords", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}).
Preload("GrowthRecords", func(db *gorm.DB) *gorm.DB {
return db.Preload("ImgList", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}).Order("created_at desc")
}).
First(&res).Error
//不存在的时候不要返回错误,而是返回nil
if err != nil {
return res, err
}
return res, nil
}
func (s *MyPlantService) UpdatePlant(req plantReq.UpdateMyPlant) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var myPlant plant.MyPlant
err := tx.Where("id = ?", req.Id).First(&myPlant).Error
if err != nil {
return err
}
// 以map形式更新 先构建map
updateMap := map[string]interface{}{
"name": req.Name,
"placement": req.Placement,
"planting_material": req.PlantingMaterial,
"pot_material": req.PotMaterial,
"pot_size": req.PotSize,
"sunlight": req.Sunlight,
}
//1.修改基本信息
err = tx.Model(&plant.MyPlant{}).Where("id = ?", req.Id).Updates(updateMap).Error
if err != nil {
return err
}
//2.修改计划
if len(req.CarePlans) > 0 {
today := timer.GetZeroTime()
for _, plan := range req.CarePlans {
err = tx.Model(&plant.CarePlan{}).Where("id = ?", plan.Id).Updates(map[string]interface{}{
"icon": plan.Icon,
"name": plan.Name,
"period": plan.Period,
"target_action": plan.TargetAction,
}).Error
if err != nil {
return err
}
//3.重新生成任务
//3.1 删除旧任务
err = tx.Where("plan_id = ?", plan.Id).Unscoped().Delete(&plant.CareTask{}).Error
if err != nil {
return err
}
//3.2 创建新任务
dueDate := today.AddDate(0, 0, plan.Period)
task := plant.CareTask{
UserId: myPlant.UserId,
PlantId: myPlant.Id,
PlanId: plan.Id,
Name: plan.Name,
Icon: plan.Icon,
DueDate: dueDate,
Status: 1,
TargetAction: plan.TargetAction,
}
err = tx.Create(&task).Error
if err != nil {
return err
}
}
}
return nil
})
}
// TodayTask 今日任务
func (s *MyPlantService) TodayTask(userId string) ([]plantRes.PlantTaskVO, error) {
today := timer.GetZeroTime()
endOfToday := today.Add(24 * time.Hour)
var tasks []*plant.CareTask
// 查询条件:1.未完成的任务(包含今天和以前逾期的) 2.今天已经完成的任务
err := global.DB.Where("user_id = ? AND ("+
"(status = 1 AND due_date < ?) OR "+
"(status = 2 AND completed_at >= ? AND completed_at < ?)"+
")", userId, endOfToday, today, endOfToday).
Order("due_date ASC").
Find(&tasks).Error
if err != nil {
return nil, err
}
// 2.内存处理:聚合数据
plantTaskMap := make(map[string][]*plant.CareTask)
expiredMap := make(map[string]bool)
var plantIds []string
for _, t := range tasks {
plantIds = append(plantIds, t.PlantId)
plantTaskMap[t.PlantId] = append(plantTaskMap[t.PlantId], t)
// 判定右上角红标:状态为待办 且 应做时间早于今天
if t.Status == 1 && t.DueDate.Before(today) {
expiredMap[t.PlantId] = true
}
}
//3.批量查询植物
var myPlants []*plant.MyPlant
err = global.DB.Where("id in ?", plantIds).Preload("ImgList", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}).Find(&myPlants).Error
if err != nil {
return nil, err
}
//4.组装结果
var res []plantRes.PlantTaskVO
for _, p := range myPlants {
plantTaskVO := plantRes.PlantTaskVO{
MyPlant: p,
HasExpired: expiredMap[p.Id],
Tasks: plantTaskMap[p.Id],
}
res = append(res, plantTaskVO)
}
return res, nil
}
// CompleteTask 完成任务
func (s *MyPlantService) CompleteTask(req plantReq.CompleteTask, userId string) (*plantRes.TaskCompletionResult, error) {
var result plantRes.TaskCompletionResult
err := global.DB.Transaction(func(tx *gorm.DB) error {
var task plant.CareTask
if err := tx.Where("id = ?", req.TaskId).First(&task).Error; err != nil {
return err
}
//1.更新当前任务为完成
updateData := map[string]interface{}{
"status": 2,
"completed_at": time.Now(),
}
if err := tx.Model(&task).Where("id = ?", req.TaskId).Updates(updateData).Error; err != nil {
return err
}
//2.获取计划模版
var plan plant.CarePlan
if err := tx.Where("id = ?", task.PlanId).First(&plan).Error; err != nil {
return err
}
//3.生成下个周期的任务
today := timer.GetZeroTime()
nextDueDate := today.AddDate(0, 0, plan.Period)
newTask := plant.CareTask{
UserId: plan.UserId,
PlantId: plan.PlantId,
PlanId: plan.Id,
Name: plan.Name,
Icon: plan.Icon,
TargetAction: plan.TargetAction,
DueDate: nextDueDate,
Status: 1,
}
if err := tx.Create(&newTask).Error; err != nil {
return err
}
//4.保存养护记录
record := plant.CareRecord{
UserId: plan.UserId,
PlantId: plan.PlantId,
PlanId: plan.Id,
Name: plan.Name,
Icon: plan.Icon,
Remark: req.Remark,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
//4.等级与阳光计算
var profile plant.UserProfile
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("user_id = ?", userId).First(&profile).Error; err != nil {
return err
}
fieldMap := map[string]string{
"ACT_WATER": "water_count",
"ACT_FERTILIZE": "fertilize_count",
"ACT_REPOT": "repot_count",
"ACT_PRUNE": "prune_count",
}
column, ok := fieldMap[task.TargetAction]
if !ok {
column = "care_count" // 默认备用
}
const TaskReward = 50
newTotalSunlight := profile.TotalSunlight + TaskReward
newCurrentSunlight := profile.CurrentSunlight + TaskReward
// 5. 等级判定 (根据累计阳光查出当前应有的等级)
var latestLevel plant.LevelConfig
if err := tx.Where("min_sunlight <= ?", newTotalSunlight).
Order("min_sunlight DESC").First(&latestLevel).Error; err != nil {
return errors.New("等级配置异常")
}
result.CurrentLevel = &latestLevel
result.IsLevelUp = latestLevel.Id != profile.LevelId
// 6. 执行 Profile 更新 (任务阳光 + 计数自增 + 等级同步)
profileData := map[string]interface{}{
column: gorm.Expr(column + " + 1"),
"current_sunlight": newCurrentSunlight,
"total_sunlight": newTotalSunlight,
"level_id": latestLevel.Id,
}
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", userId).Updates(profileData).Error; err != nil {
return err
}
// 7. 徽章判定逻辑
// 7.1 计算当前动作的最新逻辑数值
var currentActionVal int64
switch task.TargetAction {
case "ACT_WATER":
currentActionVal = profile.WaterCount + 1
case "ACT_FERTILIZE":
currentActionVal = profile.FertilizeCount + 1
case "ACT_REPOT":
currentActionVal = profile.RepotCount + 1
case "ACT_PRUNE":
currentActionVal = profile.PruneCount + 1
default:
currentActionVal = 0
}
// 7.2 查询已拥有的徽章 ID (用于去重)
var ownedBadgeIds []string
tx.Model(&plant.UserBadge{}).Where("user_id = ?", userId).Pluck("badge_id", &ownedBadgeIds)
ownedMap := make(map[string]bool)
for _, id := range ownedBadgeIds {
ownedMap[id] = true
}
// 7.3 筛选当前 Action 下满足条件的最高级徽章
var badgeConfigs []plant.BadgeConfig
tx.Where("target_action = ?", task.TargetAction).Preload("Icon").Find(&badgeConfigs)
for i := range badgeConfigs {
conf := &badgeConfigs[i]
// 如果没拿过且数值达标
if !ownedMap[conf.Id] && currentActionVal >= conf.Threshold {
// 寻找 Tier 最大的那个
if result.NewBadge == nil || conf.Tier > result.NewBadge.Tier {
result.NewBadge = conf
result.IsGetBadge = true
}
}
}
// 8. 奖励入库:如果产生了新徽章
if result.NewBadge != nil {
// 写入获得记录
ub := plant.UserBadge{
UserId: userId,
BadgeId: result.NewBadge.Id, // 注意这里 Id 是 string
}
if err := tx.Create(&ub).Error; err != nil {
return err
}
// 发放徽章额外奖励阳光
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", userId).
Update("current_sunlight", gorm.Expr("current_sunlight + ?", result.NewBadge.RewardSunlight)).Error; err != nil {
return err
}
}
return nil
})
return &result, err
}
// DeletePlants 删除植物
func (s *MyPlantService) DeletePlants(req common.IdsReq) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var imgIds []string
tx.Table("sundynix_my_plant_oss").Where("my_plant_id IN ?", req.Ids).Pluck("oss_id", &imgIds)
// 2. 清理中间表记录 (解开多对多关系)
// 使用 Exec 直接操作中间表比循环 Clear 快得多
if err := tx.Exec("DELETE FROM sundynix_my_plant_oss WHERE my_plant_id IN ?", req.Ids).Error; err != nil {
return err
}
// 3. 物理删除图片记录本身
if len(imgIds) > 0 {
if err := tx.Unscoped().Where("id IN ?", imgIds).Delete(&system.Oss{}).Error; err != nil {
return err
}
}
//4.批量删除养护计划 养护任务 养护记录 成长记录
if err := tx.Unscoped().Where("plant_id in ?", req.Ids).Delete(&plant.CarePlan{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("plant_id in ?", req.Ids).Delete(&plant.CareTask{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("plant_id in ?", req.Ids).Delete(&plant.CareRecord{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("plant_id in ?", req.Ids).Delete(&plant.GrowthRecord{}).Error; err != nil {
return err
}
//5.删除植物本身
if err := tx.Unscoped().Where("id in ?", req.Ids).Delete(&plant.MyPlant{}).Error; err != nil {
return err
}
return nil
})
}
// DeletePlans 删除任务
func (s *MyPlantService) DeletePlans(req common.IdsReq) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var plans []plant.CarePlan
if err := tx.Where("id in ?", req.Ids).Find(&plans).Error; err != nil {
return err
}
var tasks []plant.CareTask
if err := tx.Where("plan_id in ?", req.Ids).Find(&tasks).Error; err != nil {
return err
}
//1.删除计划
err := tx.Unscoped().Delete(&plans).Error
if err != nil {
return err
}
//2.删除任务
err = tx.Unscoped().Delete(&tasks).Error
if err != nil {
return err
}
return nil
})
}
// AddCarePlan 添加CarePlan
func (s *MyPlantService) AddCarePlan(req plantReq.AddPlans) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
for _, plan := range req.CarePlan {
var myPlant plant.MyPlant
err := tx.Where("id = ?", plan.PlantId).First(&myPlant).Error
if err != nil {
return err
}
//1.新增计划
newPlan := plant.CarePlan{
UserId: myPlant.UserId,
PlantId: myPlant.Id,
Name: plan.Name,
Icon: plan.Icon,
Period: plan.Period,
TargetAction: plan.TargetAction,
}
err = tx.Create(&newPlan).Error
if err != nil {
return err
}
//2.新增任务
today := timer.GetZeroTime()
dueDate := today.AddDate(0, 0, plan.Period)
task := plant.CareTask{
UserId: myPlant.UserId,
PlantId: myPlant.Id,
PlanId: newPlan.Id,
Name: plan.Name,
Icon: plan.Icon,
TargetAction: plan.TargetAction,
DueDate: dueDate,
Status: 1,
}
err = tx.Create(&task).Error
if err != nil {
return err
}
}
return nil
})
}
// DeletePlan 删除CarePlan
func (s *MyPlantService) DeletePlan(id string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var plan plant.CarePlan
if err := tx.Where("id = ?", id).First(&plan).Error; err != nil {
return err
}
var tasks []plant.CareTask
if err := tx.Where("plan_id = ?", plan.Id).Find(&tasks).Error; err != nil {
return err
}
err := tx.Unscoped().Delete(&plan).Error
if err != nil {
return err
}
err = tx.Unscoped().Delete(&tasks).Error
if err != nil {
return err
}
return nil
})
}
// AddGrowthRecord 添加成长记录
func (s *MyPlantService) AddGrowthRecord(req plantReq.CreateGrowthRecord, userId string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
//1.验证图片是否存在
var ossList []*system.Oss
err := tx.Where("id in ?", req.OssIds).Find(&ossList).Error
if err != nil {
return err
}
//2.保存记录
record := plant.GrowthRecord{
UserId: userId,
PlantId: req.PlantId,
Name: req.Name,
Content: req.Content,
Desc: req.Desc,
Tag: req.Tag,
}
err = tx.Create(&record).Error
if err != nil {
return err
}
//3.保存图片关系
if len(ossList) > 0 {
var relations []map[string]interface{}
for _, oss := range ossList {
relations = append(relations, map[string]interface{}{
"growth_record_id": record.Id,
"oss_id": oss.Id,
})
}
err = tx.Table("sundynix_growth_record_oss").Create(relations).Error
if err != nil {
return err
}
}
return nil
})
}
// QuickCare 快捷养护记录(无需预设任务,直接创建养护记录)
func (s *MyPlantService) QuickCare(req plantReq.QuickCare, userId string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
// 1.验证植物存在且属于该用户
var myPlant plant.MyPlant
if err := tx.Where("id = ? AND user_id = ?", req.PlantId, userId).First(&myPlant).Error; err != nil {
return errors.New("植物不存在")
}
// 2.直接创建养护记录
record := plant.CareRecord{
UserId: userId,
PlantId: req.PlantId,
Name: req.Name,
Icon: req.Icon,
Remark: req.Remark,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
// 3.更新用户 profile 对应计数(尝试从 icon JSON 解析 targetAction
column := "care_count"
if req.Icon != "" {
// 解析 icon JSON 中的 id 字段来判断动作类型
actionMap := map[string]string{
"water": "water_count", "fertilize": "fertilize_count",
"prune": "prune_count", "repot": "repot_count",
}
// 简单提取: 不引入 encoding/json 解析,用名称匹配
nameMap := map[string]string{
"浇水": "water_count", "施肥": "fertilize_count",
"修剪": "prune_count", "换盆": "repot_count",
}
if col, ok := nameMap[req.Name]; ok {
column = col
} else {
// 尝试从 actionMap (icon.id) 匹配
for key, col := range actionMap {
if len(req.Icon) > 0 && contains(req.Icon, key) {
column = col
break
}
}
}
}
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", userId).
Update(column, gorm.Expr(column+" + 1")).Error; err != nil {
return err
}
return nil
})
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
}
func containsStr(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
// 完成任务后异步执行 更新用户等级 sunlight
func (s *MyPlantService) handleCompleteTaskThenUpdateProfile(ctx context.Context, userId string) {
//5.更新用户profile
var profile plant.UserProfile
if asyncErr := global.DB.Where("user_id = ?", userId).First(&profile).Error; asyncErr != nil {
global.Logger.Error("完成任务异步操作-----查询用户profile失败", zap.Error(asyncErr))
}
totalSunlight := profile.TotalSunlight + 50
updateData := map[string]interface{}{
"care_count": profile.CareCount + 1,
"current_sunlight": profile.CurrentSunlight + 50,
"total_sunlight": totalSunlight,
}
//5.1 判断是否到达下个等级
var currentLevel plant.LevelConfig
levelErr := global.DB.Where("min_sunlight <= ?", totalSunlight).Order("min_sunlight DESC").First(&currentLevel).Error
if levelErr != nil {
global.Logger.Error("完成任务异步操作-----查询用户等级失败", zap.Error(levelErr))
}
updateData["level_id"] = currentLevel.Id
//5.2 更新用户profile
updateProfileErr := global.DB.Model(&plant.UserProfile{}).Where("user_id = ?", userId).Updates(updateData).Error
if updateProfileErr != nil {
global.Logger.Error("完成任务异步操作-----更新用户profile失败", zap.Error(updateProfileErr))
}
}
// todo 完成任务后异步执行 处理徽章
func (s *MyPlantService) handleCompleteTaskThenHandleBadge(ctx context.Context, taskId, userId string) {
//1.查询任务
var task plant.CareTask
if asyncErr := global.DB.Where("id = ?", taskId).First(&task).Error; asyncErr != nil {
global.Logger.Error("完成任务异步操作-----查询任务失败", zap.Error(asyncErr))
}
//2.查询徽章
var badges []plant.BadgeConfig
if asyncErr := global.DB.Where("target_action = ?", task.TargetAction).Order("tier ASC").Find(&badges).Error; asyncErr != nil {
global.Logger.Error("完成任务异步操作-----查询徽章失败", zap.Error(asyncErr))
}
//3.用户的徽章
var userBadges []plant.UserBadge
if asyncErr := global.DB.Where("user_id = ?", userId).Find(&userBadges).Error; asyncErr != nil {
global.Logger.Error("完成任务异步操作-----查询用户徽章失败", zap.Error(asyncErr))
}
//4.用户个人资料
}