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 } } }