Zuncle 6127dc1a35 fix(activity): 允许后台编辑未开始的福利活动
中文说明:新增后台专用福利活动详情查询,后台编辑未开始活动时不再复用前台的未开始拦截逻辑,避免后台打开编辑弹窗提示“活动未开始”。
2026-05-03 23:03:52 +08:00

364 lines
12 KiB
Go

package welfare_activity
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"bindbox-game/internal/repository/mysql/model"
"gorm.io/gorm"
)
func (s *service) CreateActivity(ctx context.Context, req SaveActivityRequest) (*Activity, error) {
if err := validateActivity(req); err != nil {
return nil, err
}
item := &Activity{
Title: normalizeActivityTitle(req.Title),
Type: req.Type,
ThresholdAmount: req.ThresholdAmount,
StartTime: req.StartTime,
EndTime: req.EndTime,
DrawTime: req.DrawTime,
Status: normalizeStatus(req.Status),
Description: req.Description,
CoverImage: req.CoverImage,
}
err := s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(item).Error; err != nil {
return err
}
return s.replacePrizes(ctx, tx, item.ID, req.Prizes)
})
return item, err
}
func (s *service) UpdateActivity(ctx context.Context, id int64, req SaveActivityRequest) error {
if id <= 0 {
return errors.New("活动ID无效")
}
if err := validateActivity(req); err != nil {
return err
}
var existing Activity
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id).First(&existing).Error; err != nil {
return err
}
return s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
status := req.Status
if strings.TrimSpace(status) == "" {
status = existing.Status
}
updates := map[string]interface{}{
"title": normalizeActivityTitle(req.Title),
"type": req.Type,
"threshold_amount": req.ThresholdAmount,
"start_time": req.StartTime,
"end_time": req.EndTime,
"draw_time": req.DrawTime,
"status": normalizeStatus(status),
"description": req.Description,
"cover_image": req.CoverImage,
}
var current Activity
if err := tx.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id).First(&current).Error; err != nil {
return err
}
if current.Status == StatusFinished {
return errors.New("已结束活动不可编辑")
}
if err := tx.Model(&Activity{}).Where("id = ? AND deleted_at IS NULL", id).Updates(updates).Error; err != nil {
return err
}
return s.replacePrizes(ctx, tx, id, req.Prizes)
})
}
func (s *service) CopyActivity(ctx context.Context, id int64, req SaveActivityRequest) (int64, error) {
var src Activity
db := s.repo.GetDbR().WithContext(ctx)
if err := db.Where("id = ? AND deleted_at IS NULL", id).First(&src).Error; err != nil {
return 0, err
}
var prizes []Prize
if err := db.Where("activity_id = ?", id).Order("sort ASC, id ASC").Find(&prizes).Error; err != nil {
return 0, err
}
copyReq := SaveActivityRequest{
Title: req.Title,
Type: req.Type,
ThresholdAmount: req.ThresholdAmount,
StartTime: req.StartTime,
EndTime: req.EndTime,
DrawTime: req.DrawTime,
Status: normalizeStatus(req.Status),
CoverImage: src.CoverImage,
}
for _, p := range prizes {
copyReq.Prizes = append(copyReq.Prizes, PrizeInput{
RewardType: p.RewardType,
RewardRefID: p.RewardRefID,
Quantity: p.Quantity,
Sort: p.Sort,
})
}
item, err := s.CreateActivity(ctx, copyReq)
if err != nil {
return 0, err
}
return item.ID, nil
}
func (s *service) ListActivities(ctx context.Context, req ListActivitiesRequest) (*ListActivitiesResponse, error) {
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
now := time.Now()
db := s.repo.GetDbR().WithContext(ctx).Model(&Activity{}).Where("deleted_at IS NULL")
if req.Type != "" {
db = db.Where("type = ?", req.Type)
}
if req.Status != "" {
db = db.Where("status = ?", req.Status)
if req.Status == StatusActive {
db = db.Where("start_time <= ? AND end_time >= ? AND draw_time >= ?", now, now, now)
}
}
if strings.TrimSpace(req.Title) != "" {
db = db.Where("title LIKE ?", "%"+strings.TrimSpace(req.Title)+"%")
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
var rows []Activity
if err := db.Order("id DESC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&rows).Error; err != nil {
return nil, err
}
list := make([]ActivityListItem, 0, len(rows))
for _, row := range rows {
item := ActivityListItem{Activity: row}
s.repo.GetDbR().WithContext(ctx).Table("welfare_activity_participants").Where("activity_id = ?", row.ID).Count(&item.ParticipantCount)
s.repo.GetDbR().WithContext(ctx).Table("welfare_activity_winners").Where("activity_id = ?", row.ID).Count(&item.WinnerCount)
s.repo.GetDbR().WithContext(ctx).Table("welfare_activity_winners").Select("COALESCE(SUM(cost_cents),0)").Where("activity_id = ?", row.ID).Scan(&item.CostCents)
list = append(list, item)
}
return &ListActivitiesResponse{Page: req.Page, PageSize: req.PageSize, Total: total, List: list}, nil
}
func (s *service) GetActivity(ctx context.Context, id int64, userID int64) (*ActivityDetail, error) {
var item Activity
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id).First(&item).Error; err != nil {
return nil, err
}
if userID <= 0 && item.Status == StatusActive && time.Now().Before(item.StartTime) {
return nil, errors.New("活动未开始")
}
return s.buildActivityDetail(ctx, item, userID)
}
func (s *service) GetActivityAdmin(ctx context.Context, id int64) (*ActivityDetail, error) {
var item Activity
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id).First(&item).Error; err != nil {
return nil, err
}
return s.buildActivityDetail(ctx, item, 0)
}
func (s *service) buildActivityDetail(ctx context.Context, item Activity, userID int64) (*ActivityDetail, error) {
detail := &ActivityDetail{Activity: item}
s.repo.GetDbR().WithContext(ctx).Where("activity_id = ?", item.ID).Order("sort ASC, id ASC").Find(&detail.Prizes)
s.fillPrizeMeta(ctx, detail.Prizes)
participants, _ := s.ListParticipants(ctx, item.ID, 1, 20)
if participants != nil {
detail.ParticipantCount = participants.Total
detail.Participants = participants.List
}
winners, _ := s.ListWinners(ctx, item.ID, 1, 20)
if winners != nil {
detail.Winners = winners.List
}
if userID > 0 {
start, end, period := periodRange(item.Type, time.Now())
detail.CurrentPaid, _ = s.sumPaidAmount(ctx, userID, start, end)
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && detail.CurrentPaid >= item.ThresholdAmount
var count int64
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ? AND period_key = ?", item.ID, userID, period).Count(&count)
detail.Joined = count > 0
}
return detail, nil
}
func (s *service) DeleteActivity(ctx context.Context, id int64) error {
if id <= 0 {
return errors.New("活动ID无效")
}
now := time.Now()
return s.repo.GetDbW().WithContext(ctx).Model(&Activity{}).Where("id = ? AND deleted_at IS NULL", id).Update("deleted_at", now).Error
}
func (s *service) SavePrizes(ctx context.Context, activityID int64, prizes []PrizeInput) error {
return s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.replacePrizes(ctx, tx, activityID, prizes)
})
}
func (s *service) replacePrizes(ctx context.Context, tx *gorm.DB, activityID int64, inputs []PrizeInput) error {
if err := tx.WithContext(ctx).Where("activity_id = ?", activityID).Delete(&Prize{}).Error; err != nil {
return err
}
for _, input := range inputs {
prizeInput, err := normalizePrizeInput(input)
if err != nil {
return err
}
if prizeInput.Quantity <= 0 {
return errors.New("奖品数量不能为空")
}
prize, err := s.buildPrizeSnapshot(ctx, tx, activityID, prizeInput)
if err != nil {
return err
}
if err := tx.WithContext(ctx).Create(prize).Error; err != nil {
return err
}
}
return nil
}
func validateActivity(req SaveActivityRequest) error {
if strings.TrimSpace(req.Title) == "" {
return errors.New("活动标题不能为空")
}
if req.Type != TypeDaily && req.Type != TypeWeekly && req.Type != TypeMonthly {
return errors.New("活动类型无效")
}
if req.ThresholdAmount < 0 {
return errors.New("参与门槛不能小于0")
}
if req.StartTime.IsZero() || req.EndTime.IsZero() || req.DrawTime.IsZero() {
return errors.New("活动时间和开奖时间不能为空")
}
if !req.EndTime.After(req.StartTime) {
return errors.New("结束时间必须晚于开始时间")
}
if req.DrawTime.Before(req.StartTime) {
return errors.New("开奖时间不能早于开始时间")
}
return nil
}
func normalizeStatus(status string) string {
if status == "" {
return StatusActive
}
if status == StatusFinished {
return StatusFinished
}
return StatusActive
}
func firstProductImage(imagesJSON string) string {
imagesJSON = strings.TrimSpace(imagesJSON)
if imagesJSON == "" {
return ""
}
var images []string
if err := json.Unmarshal([]byte(imagesJSON), &images); err == nil && len(images) > 0 {
return strings.TrimSpace(images[0])
}
return ""
}
func normalizeActivityTitle(title string) string {
title = strings.TrimSpace(title)
if title == "" {
return time.Now().Format("2006-01-02")
}
return title
}
func normalizePrizeInput(input PrizeInput) (PrizeInput, error) {
if input.RewardType == "" && input.ProductID > 0 {
input.RewardType = RewardTypeProduct
input.RewardRefID = input.ProductID
}
if input.RewardType == "" || input.RewardRefID <= 0 {
return PrizeInput{}, errors.New("奖品类型和资源不能为空")
}
switch input.RewardType {
case RewardTypeProduct, RewardTypeItemCard, RewardTypeCoupon:
return input, nil
default:
return PrizeInput{}, errors.New("奖品类型无效")
}
}
func (s *service) buildPrizeSnapshot(ctx context.Context, tx *gorm.DB, activityID int64, input PrizeInput) (*Prize, error) {
prize := &Prize{
ActivityID: activityID,
RewardType: input.RewardType,
RewardRefID: input.RewardRefID,
Quantity: input.Quantity,
RemainingQuantity: input.Quantity,
Sort: input.Sort,
}
switch input.RewardType {
case RewardTypeProduct:
var product model.Products
if err := tx.WithContext(ctx).Where("id = ?", input.RewardRefID).First(&product).Error; err != nil {
return nil, fmt.Errorf("商品不存在: %d", input.RewardRefID)
}
prize.RewardNameSnapshot = product.Name
prize.RewardImageSnapshot = firstProductImage(product.ImagesJSON)
prize.RewardValueSnapshotCents = product.Price
prize.CostSnapshotCents = product.CostPrice
if prize.CostSnapshotCents <= 0 {
prize.CostSnapshotCents = product.Price
}
case RewardTypeItemCard:
var card model.SystemItemCards
if err := tx.WithContext(ctx).Where("id = ? AND status = 1", input.RewardRefID).First(&card).Error; err != nil {
return nil, fmt.Errorf("道具卡不存在或未启用: %d", input.RewardRefID)
}
prize.RewardNameSnapshot = card.Name
prize.RewardImageSnapshot = ""
prize.RewardValueSnapshotCents = card.Price
prize.CostSnapshotCents = card.Price
case RewardTypeCoupon:
var coupon model.SystemCoupons
if err := tx.WithContext(ctx).Where("id = ? AND status = 1", input.RewardRefID).First(&coupon).Error; err != nil {
return nil, fmt.Errorf("优惠券不存在或未启用: %d", input.RewardRefID)
}
prize.RewardNameSnapshot = coupon.Name
prize.RewardImageSnapshot = ""
prize.RewardValueSnapshotCents = coupon.DiscountValue
prize.CostSnapshotCents = coupon.DiscountValue
}
return prize, nil
}
func (s *service) fillPrizeMeta(ctx context.Context, prizes []Prize) {
for i := range prizes {
if prizes[i].RewardNameSnapshot != "" {
prizes[i].Name = prizes[i].RewardNameSnapshot
}
if prizes[i].RewardImageSnapshot != "" {
prizes[i].Image = prizes[i].RewardImageSnapshot
}
if prizes[i].RewardValueSnapshotCents > 0 {
prizes[i].PriceCents = prizes[i].RewardValueSnapshotCents
}
}
}