364 lines
12 KiB
Go
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(¤t).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
|
|
}
|
|
}
|
|
}
|