413 lines
14 KiB
Go
413 lines
14 KiB
Go
package threshold_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,
|
|
QualificationMode: normalizeQualificationMode(req.QualificationMode),
|
|
SpendThresholdAmount: req.SpendThresholdAmount,
|
|
InviteThresholdCount: req.InviteThresholdCount,
|
|
InviteEffectiveAmount: req.InviteEffectiveAmount,
|
|
MinParticipants: normalizeMinParticipants(req.MinParticipants),
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
DrawTime: req.DrawTime,
|
|
Status: normalizeStatus(req.Status),
|
|
Description: req.Description,
|
|
CoverImage: req.CoverImage,
|
|
}
|
|
if 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)
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
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,
|
|
"qualification_mode": normalizeQualificationMode(req.QualificationMode),
|
|
"spend_threshold_amount": req.SpendThresholdAmount,
|
|
"invite_threshold_count": req.InviteThresholdCount,
|
|
"invite_effective_amount": req.InviteEffectiveAmount,
|
|
"min_participants": normalizeMinParticipants(req.MinParticipants),
|
|
"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 || current.Status == StatusAborted {
|
|
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,
|
|
QualificationMode: req.QualificationMode,
|
|
SpendThresholdAmount: req.SpendThresholdAmount,
|
|
InviteThresholdCount: req.InviteThresholdCount,
|
|
InviteEffectiveAmount: req.InviteEffectiveAmount,
|
|
MinParticipants: req.MinParticipants,
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
DrawTime: req.DrawTime,
|
|
Status: normalizeStatus(req.Status),
|
|
CoverImage: src.CoverImage,
|
|
Description: src.Description,
|
|
}
|
|
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("threshold_activity_participants").Where("activity_id = ?", row.ID).Count(&item.ParticipantCount)
|
|
s.repo.GetDbR().WithContext(ctx).Table("threshold_activity_winners").Where("activity_id = ?", row.ID).Count(&item.WinnerCount)
|
|
s.repo.GetDbR().WithContext(ctx).Table("threshold_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 {
|
|
progress, period, err := s.evaluateQualification(ctx, item, userID, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
detail.QualificationProgress = progress
|
|
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && qualificationSatisfied(item, progress)
|
|
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
|
|
if detail.Joined {
|
|
detail.CanJoin = false
|
|
}
|
|
}
|
|
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.SpendThresholdAmount < 0 || req.InviteThresholdCount < 0 || req.InviteEffectiveAmount < 0 {
|
|
return errors.New("门槛不能小于0")
|
|
}
|
|
if req.QualificationMode == QualificationModeSpendOnly && req.SpendThresholdAmount <= 0 {
|
|
return errors.New("消费门槛活动必须设置消费门槛")
|
|
}
|
|
if req.QualificationMode == QualificationModeInviteOnly && (req.InviteThresholdCount <= 0 || req.InviteEffectiveAmount <= 0) {
|
|
return errors.New("邀请门槛活动必须设置有效邀请人数和有效消费门槛")
|
|
}
|
|
if req.QualificationMode == QualificationModeEither && req.SpendThresholdAmount <= 0 && (req.InviteThresholdCount <= 0 || req.InviteEffectiveAmount <= 0) {
|
|
return errors.New("任一达标活动至少需要配置一条有效资格线")
|
|
}
|
|
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 {
|
|
switch strings.TrimSpace(status) {
|
|
case StatusFinished:
|
|
return StatusFinished
|
|
case StatusAborted:
|
|
return StatusAborted
|
|
default:
|
|
return StatusActive
|
|
}
|
|
}
|
|
|
|
func normalizeQualificationMode(mode string) string {
|
|
switch strings.TrimSpace(mode) {
|
|
case QualificationModeSpendOnly:
|
|
return QualificationModeSpendOnly
|
|
case QualificationModeInviteOnly:
|
|
return QualificationModeInviteOnly
|
|
default:
|
|
return QualificationModeEither
|
|
}
|
|
}
|
|
|
|
func normalizeMinParticipants(v int64) int64 {
|
|
if v <= 0 {
|
|
return 1
|
|
}
|
|
return v
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|