feat: Add callback and exchange features, refactor care plan task generation logic, and update wiki ordering to prioritize hot items.

This commit is contained in:
Blizzard
2026-02-26 08:57:13 +08:00
parent 9d4a5c6375
commit 3f05dccdce
23 changed files with 955 additions and 56 deletions
+110
View File
@@ -0,0 +1,110 @@
package plant
import (
"sundynix-go/global"
"sundynix-go/model/plant"
plantres "sundynix-go/model/plant/response"
"go.uber.org/zap"
"gorm.io/gorm"
)
type CallbackService struct{}
var CallbackServiceApp = new(CallbackService)
// HandleMediaCheckCallback 处理媒体检测回调
func (s *CallbackService) HandleMediaCheckCallback(cb plantres.WeChatCheckResultCallback) error {
global.Logger.Info("收到微信媒体检测回调", zap.String("traceId", cb.TraceId), zap.String("suggest", cb.Result.Suggest))
var checkResult plant.MediaCheckResult
err := global.DB.Where("trace_id = ?", cb.TraceId).First(&checkResult).Error
if err != nil {
global.Logger.Error("回调traceId未找到", zap.String("traceId", cb.TraceId), zap.Error(err))
return err
}
// 1. 更新检测结果状态
status := 0
if cb.Result.Suggest == "pass" {
status = 1
} else {
status = 2
}
err = global.DB.Model(&checkResult).Updates(map[string]interface{}{
"status": status,
"err_msg": cb.Result.Suggest,
}).Error
if err != nil {
global.Logger.Error("更新检测结果失败", zap.Error(err))
return err
}
// 2. 根据结果处理帖子状态
return s.updatePostStatus(checkResult.PostId)
}
// updatePostStatus 更新帖子状态
// 逻辑:
// 1. 如果有任意一个检测结果为违规(2) -> 帖子违规(2)
// 2. 如果所有检测结果都为通过(1) -> 帖子通过(1)
// 3. 否则保持待审核(0)
func (s *CallbackService) updatePostStatus(postId string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var post plant.Post
if err := tx.Where("id = ?", postId).First(&post).Error; err != nil {
return err
}
// 如果帖子已经是违规状态,无需再处理(可能之前已经由文本检测判定违规)
if post.HasReviewed == 2 {
return nil
}
var results []plant.MediaCheckResult
if err := tx.Where("post_id = ?", postId).Find(&results).Error; err != nil {
return err
}
hasRisky := false
allPass := true
for _, res := range results {
if res.Status == 2 {
hasRisky = true
break
}
if res.Status != 1 {
allPass = false
}
}
var newStatus = post.HasReviewed // 默认保持原状态
if hasRisky {
newStatus = 2
// TODO: 这里可以执行额外的封禁逻辑,例如不仅标记违规,还软删除Oss关联等
global.Logger.Warn("帖子包含违规图片,标记为违规", zap.String("postId", postId))
} else if allPass {
// 只有当所有图片都通过,且原状态不是违规时,才标记为通过
// 注意:这里假设文本检测已经通过(文本检测是同步的,若不通过早已设为2)
// 如果文本检测尚未完成(理论上不可能,因为是先文本后图片),这里可能会有竞态,但文本检测在发帖goroutine中是串行的。
// 唯一需要注意的是,如果文本检测还在进行中,这里不应覆盖。
// 但我们在PublishPost中是先改HasReviewed再发图片检查。
// 如果文本通过,HasReviewed会被设为1? 不,根据新逻辑,PublishPost中只有无图才设为1。
// 有图时,PublishPost中HasReviewed保持0。
newStatus = 1
global.Logger.Info("帖子所有图片检测通过,标记为通过", zap.String("postId", postId))
}
if newStatus != post.HasReviewed {
if err := tx.Model(&post).Update("has_reviewed", newStatus).Error; err != nil {
return err
}
}
return nil
})
}
+1
View File
@@ -11,4 +11,5 @@ type ServiceGroup struct {
BadgeConfigService
UserProfileService
CallbackService
ExchangeService
}
+282
View File
@@ -0,0 +1,282 @@
package plant
import (
"errors"
"sundynix-go/global"
"sundynix-go/model/plant"
plantReq "sundynix-go/model/plant/request"
"time"
"gorm.io/gorm"
)
type ExchangeService struct{}
// ==================== 管理端 ====================
// CreateItem 创建兑换商品
func (s *ExchangeService) CreateItem(req plantReq.ExchangeItemCreateReq) error {
item := plant.ExchangeItem{
Name: req.Name,
Description: req.Description,
ImageId: req.ImageId,
Type: req.Type,
CostSunlight: req.CostSunlight,
Stock: req.Stock,
LimitPerUser: req.LimitPerUser,
Sort: req.Sort,
Status: 1, // 默认上架
}
if req.StartTime != "" {
t, _ := time.Parse("2006-01-02 15:04:05", req.StartTime)
item.StartTime = &t
}
if req.EndTime != "" {
t, _ := time.Parse("2006-01-02 15:04:05", req.EndTime)
item.EndTime = &t
}
return global.DB.Create(&item).Error
}
// UpdateItem 更新兑换商品
func (s *ExchangeService) UpdateItem(req plantReq.ExchangeItemUpdateReq) error {
updateMap := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"image_id": req.ImageId,
"type": req.Type,
"cost_sunlight": req.CostSunlight,
"stock": req.Stock,
"limit_per_user": req.LimitPerUser,
"status": req.Status,
"sort": req.Sort,
}
if req.StartTime != "" {
t, _ := time.Parse("2006-01-02 15:04:05", req.StartTime)
updateMap["start_time"] = t
}
if req.EndTime != "" {
t, _ := time.Parse("2006-01-02 15:04:05", req.EndTime)
updateMap["end_time"] = t
}
return global.DB.Model(&plant.ExchangeItem{}).Where("id = ?", req.Id).Updates(updateMap).Error
}
// DeleteItem 删除兑换商品
func (s *ExchangeService) DeleteItem(id string) error {
return global.DB.Where("id = ?", id).Delete(&plant.ExchangeItem{}).Error
}
// AdminItemList 管理端商品列表(含下架)
func (s *ExchangeService) AdminItemList(req plantReq.ExchangeItemListReq) (list []plant.ExchangeItem, total int64, err error) {
db := global.DB.Model(&plant.ExchangeItem{}).Preload("Image")
if req.Type != "" {
db = db.Where("type = ?", req.Type)
}
if req.Status != 0 {
db = db.Where("status = ?", req.Status)
}
if req.Keyword != "" {
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(req.Paginate()).Order("sort asc, created_at desc").Find(&list).Error
return
}
// AdminOrderList 管理端订单列表
func (s *ExchangeService) AdminOrderList(req plantReq.ExchangeOrderListReq) (list []plant.ExchangeOrder, total int64, err error) {
db := global.DB.Model(&plant.ExchangeOrder{}).Preload("Item", func(db *gorm.DB) *gorm.DB {
return db.Preload("Image")
})
if req.Status != 0 {
db = db.Where("status = ?", req.Status)
}
if req.UserId != "" {
db = db.Where("user_id = ?", req.UserId)
}
if req.Keyword != "" {
db = db.Where("item_name LIKE ? OR recipient_name LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(req.Paginate()).Order("created_at desc").Find(&list).Error
return
}
// UpdateOrderStatus 更新订单状态(管理端)
func (s *ExchangeService) UpdateOrderStatus(req plantReq.ExchangeOrderUpdateReq) error {
updateMap := map[string]interface{}{
"status": req.Status,
}
if req.TrackingNo != "" {
updateMap["tracking_no"] = req.TrackingNo
}
if req.Remark != "" {
updateMap["remark"] = req.Remark
}
if req.Status == plant.OrderStatusCompleted {
now := time.Now()
updateMap["completed_at"] = now
}
// 如果取消订单,退还阳光值
if req.Status == plant.OrderStatusCancelled {
return global.DB.Transaction(func(tx *gorm.DB) error {
var order plant.ExchangeOrder
if err := tx.Where("id = ?", req.Id).First(&order).Error; err != nil {
return err
}
if order.Status == plant.OrderStatusCancelled {
return errors.New("订单已取消")
}
// 退还阳光值
if err := tx.Model(&plant.UserProfile{}).Where("user_id = ?", order.UserId).
Update("current_sunlight", gorm.Expr("current_sunlight + ?", order.CostSunlight)).Error; err != nil {
return err
}
// 恢复库存
if err := tx.Model(&plant.ExchangeItem{}).Where("id = ? AND stock >= 0", order.ItemId).
Update("stock", gorm.Expr("stock + ?", order.Quantity)).Error; err != nil {
// 库存为-1(无限)时,WHERE条件不匹配,不影响
}
return tx.Model(&plant.ExchangeOrder{}).Where("id = ?", req.Id).Updates(updateMap).Error
})
}
return global.DB.Model(&plant.ExchangeOrder{}).Where("id = ?", req.Id).Updates(updateMap).Error
}
// ==================== 用户端 ====================
// UserItemList 用户端商品列表(仅上架)
func (s *ExchangeService) UserItemList(req plantReq.ExchangeItemListReq) (list []plant.ExchangeItem, total int64, err error) {
db := global.DB.Model(&plant.ExchangeItem{}).Preload("Image").
Where("status = 1")
if req.Type != "" {
db = db.Where("type = ?", req.Type)
}
if req.Keyword != "" {
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(req.Paginate()).Order("sort asc, created_at desc").Find(&list).Error
return
}
// UserItemDetail 商品详情
func (s *ExchangeService) UserItemDetail(itemId string) (plant.ExchangeItem, error) {
var item plant.ExchangeItem
err := global.DB.Preload("Image").Where("id = ? AND status = 1", itemId).First(&item).Error
return item, err
}
// UserExchange 用户发起兑换
func (s *ExchangeService) UserExchange(req plantReq.ExchangeReq, userId string) error {
if req.Quantity <= 0 {
req.Quantity = 1
}
return global.DB.Transaction(func(tx *gorm.DB) error {
// 1. 查询并锁定商品
var item plant.ExchangeItem
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ? AND status = 1", req.ItemId).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("商品不存在或已下架")
}
return err
}
// 2. 检查有效期
now := time.Now()
if item.StartTime != nil && now.Before(*item.StartTime) {
return errors.New("兑换尚未开始")
}
if item.EndTime != nil && now.After(*item.EndTime) {
return errors.New("兑换已结束")
}
// 3. 检查库存
if item.Stock >= 0 && item.Stock < req.Quantity {
return errors.New("库存不足")
}
// 4. 检查每人限兑
if item.LimitPerUser > 0 {
var count int64
tx.Model(&plant.ExchangeOrder{}).Where("user_id = ? AND item_id = ? AND status != ?",
userId, req.ItemId, plant.OrderStatusCancelled).Count(&count)
if int(count)+req.Quantity > item.LimitPerUser {
return errors.New("已达到兑换上限")
}
}
// 5. 计算总消耗
totalCost := item.CostSunlight * int64(req.Quantity)
// 6. 扣减阳光值
result := tx.Model(&plant.UserProfile{}).
Where("user_id = ? AND current_sunlight >= ?", userId, totalCost).
Update("current_sunlight", gorm.Expr("current_sunlight - ?", totalCost))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("阳光值不足")
}
// 7. 扣减库存
if item.Stock >= 0 {
if err := tx.Model(&plant.ExchangeItem{}).Where("id = ? AND stock >= ?", item.Id, req.Quantity).
Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil {
return err
}
}
// 8. 创建订单
order := plant.ExchangeOrder{
UserId: userId,
ItemId: item.Id,
ItemName: item.Name,
CostSunlight: totalCost,
Quantity: req.Quantity,
Status: plant.OrderStatusPending,
ItemType: item.Type,
RecipientName: req.RecipientName,
Phone: req.Phone,
Address: req.Address,
}
// 虚拟商品自动完成
if item.Type == "VIRTUAL" {
order.Status = plant.OrderStatusCompleted
now := time.Now()
order.CompletedAt = &now
}
return tx.Create(&order).Error
})
}
// UserOrderList 用户订单列表
func (s *ExchangeService) UserOrderList(req plantReq.ExchangeOrderListReq, userId string) (list []plant.ExchangeOrder, total int64, err error) {
db := global.DB.Model(&plant.ExchangeOrder{}).Preload("Item", func(db *gorm.DB) *gorm.DB {
return db.Preload("Image")
}).Where("user_id = ?", userId)
if req.Status != 0 {
db = db.Where("status = ?", req.Status)
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(req.Paginate()).Order("created_at desc").Find(&list).Error
return
}
+9 -8
View File
@@ -161,7 +161,7 @@ func (s *MyPlantService) UpdatePlant(req plantReq.UpdateMyPlant) error {
if err != nil {
return err
}
//3.重新生成任务 CarePlans 结构体中使用钩子函数自动执行
//3.重新生成任务
//3.1 删除旧任务
err = tx.Where("plan_id = ?", plan.Id).Unscoped().Delete(&plant.CareTask{}).Error
if err != nil {
@@ -170,13 +170,14 @@ func (s *MyPlantService) UpdatePlant(req plantReq.UpdateMyPlant) error {
//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,
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 {
+1 -1
View File
@@ -173,7 +173,7 @@ func (s *WikiService) WikiPage(req plantReq.WikiPage, userId string) (list inter
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&wikis).Error
err = db.Limit(limit).Offset(offset).Order("is_hot desc,created_at desc").Find(&wikis).Error
// 优化 N+1 查询
var wikiIds []string