225 lines
8.4 KiB
Go
225 lines
8.4 KiB
Go
package threshold_activity
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
func (s *service) Join(ctx context.Context, activityID int64, userID int64) error {
|
||
if activityID <= 0 || userID <= 0 {
|
||
return errors.New("活动或用户无效")
|
||
}
|
||
var activity Activity
|
||
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ? AND deleted_at IS NULL", activityID).First(&activity).Error; err != nil {
|
||
return err
|
||
}
|
||
now := time.Now()
|
||
if !isJoinWindowOpen(activity, now) {
|
||
if now.Before(activity.StartTime) {
|
||
return errors.New("活动未开始")
|
||
}
|
||
return errors.New("当前不在活动参与时间内")
|
||
}
|
||
progress, period, err := s.evaluateQualification(ctx, activity, userID, now)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !qualificationSatisfied(activity, progress) {
|
||
return errors.New(joinQualificationError(activity, progress))
|
||
}
|
||
participant := &Participant{
|
||
ActivityID: activityID,
|
||
UserID: userID,
|
||
PeriodKey: period,
|
||
QualificationSource: progress.QualificationSource,
|
||
PaidAmountSnapshot: progress.CurrentPaid,
|
||
EffectiveInviteCountSnapshot: progress.EffectiveInviteCount,
|
||
}
|
||
return s.repo.GetDbW().WithContext(ctx).Create(participant).Error
|
||
}
|
||
|
||
func (s *service) ListParticipants(ctx context.Context, activityID int64, page int, pageSize int) (*ParticipantResponse, error) {
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = 20
|
||
}
|
||
if pageSize > 100 {
|
||
pageSize = 100
|
||
}
|
||
var total int64
|
||
db := s.repo.GetDbR().WithContext(ctx).Table("threshold_activity_participants").Where("activity_id = ?", activityID)
|
||
if err := db.Count(&total).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
var list []ParticipantAvatar
|
||
err := s.repo.GetDbR().WithContext(ctx).Table("threshold_activity_participants p").
|
||
Select("p.user_id, COALESCE(u.nickname, '') AS nickname, COALESCE(u.avatar, '') AS avatar").
|
||
Joins("LEFT JOIN users u ON u.id = p.user_id").
|
||
Where("p.activity_id = ?", activityID).
|
||
Order("p.id DESC").
|
||
Offset((page - 1) * pageSize).
|
||
Limit(pageSize).
|
||
Scan(&list).Error
|
||
return &ParticipantResponse{Page: page, PageSize: pageSize, Total: total, List: list}, err
|
||
}
|
||
|
||
func (s *service) ListJoinableActivitiesForUser(ctx context.Context, userID int64) ([]JoinableActivityItem, error) {
|
||
now := time.Now()
|
||
var activities []Activity
|
||
if err := s.repo.GetDbR().WithContext(ctx).Where("deleted_at IS NULL AND status = ? AND start_time <= ? AND end_time > ? AND draw_time > ?", StatusActive, now, now, now).Order("draw_time ASC, id ASC").Find(&activities).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if len(activities) == 0 {
|
||
return []JoinableActivityItem{}, nil
|
||
}
|
||
items := make([]JoinableActivityItem, 0, len(activities))
|
||
for _, activity := range activities {
|
||
progress, period, err := s.evaluateQualification(ctx, activity, userID, now)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var count int64
|
||
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ? AND period_key = ?", activity.ID, userID, period).Count(&count)
|
||
joined := count > 0
|
||
canJoin := !joined && qualificationSatisfied(activity, progress)
|
||
items = append(items, JoinableActivityItem{
|
||
ActivityID: activity.ID,
|
||
Title: activity.Title,
|
||
Type: activity.Type,
|
||
QualificationMode: activity.QualificationMode,
|
||
SpendThresholdAmount: activity.SpendThresholdAmount,
|
||
InviteThresholdCount: activity.InviteThresholdCount,
|
||
InviteEffectiveAmount: activity.InviteEffectiveAmount,
|
||
MinParticipants: activity.MinParticipants,
|
||
QualificationProgress: progress,
|
||
CanJoin: canJoin,
|
||
Joined: joined,
|
||
StartTime: activity.StartTime,
|
||
EndTime: activity.EndTime,
|
||
DrawTime: activity.DrawTime,
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
func (s *service) evaluateQualification(ctx context.Context, activity Activity, userID int64, now time.Time) (QualificationProgress, string, error) {
|
||
start, end, period := periodRange(activity.Type, now)
|
||
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
||
if err != nil {
|
||
return QualificationProgress{}, "", err
|
||
}
|
||
inviteCount, err := s.countEffectiveInvites(ctx, userID, activity.StartTime, start, end, activity.InviteEffectiveAmount)
|
||
if err != nil {
|
||
return QualificationProgress{}, "", err
|
||
}
|
||
progress := QualificationProgress{
|
||
CurrentPaid: paid,
|
||
EffectiveInviteCount: inviteCount,
|
||
SpendQualified: activity.SpendThresholdAmount > 0 && paid >= activity.SpendThresholdAmount,
|
||
InviteQualified: activity.InviteThresholdCount > 0 && inviteCount >= activity.InviteThresholdCount,
|
||
}
|
||
switch {
|
||
case progress.SpendQualified && progress.InviteQualified:
|
||
progress.QualificationSource = QualificationSourceBoth
|
||
case progress.SpendQualified:
|
||
progress.QualificationSource = QualificationSourceSpend
|
||
case progress.InviteQualified:
|
||
progress.QualificationSource = QualificationSourceInvite
|
||
}
|
||
return progress, period, nil
|
||
}
|
||
|
||
func qualificationSatisfied(activity Activity, progress QualificationProgress) bool {
|
||
switch normalizeQualificationMode(activity.QualificationMode) {
|
||
case QualificationModeSpendOnly:
|
||
return progress.SpendQualified
|
||
case QualificationModeInviteOnly:
|
||
return progress.InviteQualified
|
||
default:
|
||
return progress.SpendQualified || progress.InviteQualified
|
||
}
|
||
}
|
||
|
||
func joinQualificationError(activity Activity, progress QualificationProgress) string {
|
||
switch normalizeQualificationMode(activity.QualificationMode) {
|
||
case QualificationModeSpendOnly:
|
||
return fmt.Sprintf("未达到消费门槛,还差%d分", activity.SpendThresholdAmount-progress.CurrentPaid)
|
||
case QualificationModeInviteOnly:
|
||
return fmt.Sprintf("未达到有效邀请门槛,还差%d人", activity.InviteThresholdCount-progress.EffectiveInviteCount)
|
||
default:
|
||
if activity.SpendThresholdAmount > 0 && progress.CurrentPaid < activity.SpendThresholdAmount {
|
||
return fmt.Sprintf("未达到参与门槛:消费还差%d分,或邀请还差%d人", activity.SpendThresholdAmount-progress.CurrentPaid, maxInt64(0, activity.InviteThresholdCount-progress.EffectiveInviteCount))
|
||
}
|
||
return fmt.Sprintf("未达到参与门槛:邀请还差%d人", maxInt64(0, activity.InviteThresholdCount-progress.EffectiveInviteCount))
|
||
}
|
||
}
|
||
|
||
func (s *service) sumPaidAmount(ctx context.Context, userID int64, start time.Time, end time.Time) (int64, error) {
|
||
var total int64
|
||
err := s.repo.GetDbR().WithContext(ctx).Table("orders").
|
||
Select("COALESCE(SUM(actual_amount), 0)").
|
||
Where("user_id = ? AND status = 2 AND actual_amount > 0", userID).
|
||
Where("COALESCE(NULLIF(paid_at, '1970-01-01 00:00:00'), created_at) >= ?", start).
|
||
Where("COALESCE(NULLIF(paid_at, '1970-01-01 00:00:00'), created_at) < ?", end).
|
||
Scan(&total).Error
|
||
return total, err
|
||
}
|
||
|
||
func (s *service) countEffectiveInvites(ctx context.Context, inviterID int64, activityStart time.Time, start time.Time, end time.Time, thresholdAmount int64) (int64, error) {
|
||
if thresholdAmount <= 0 {
|
||
return 0, nil
|
||
}
|
||
var count int64
|
||
query := `
|
||
SELECT COUNT(1)
|
||
FROM (
|
||
SELECT ui.invitee_id
|
||
FROM user_invites ui
|
||
JOIN orders o ON o.user_id = ui.invitee_id
|
||
WHERE ui.inviter_id = ?
|
||
AND ui.created_at >= ?
|
||
AND o.status = 2
|
||
AND o.actual_amount > 0
|
||
AND COALESCE(NULLIF(o.paid_at, '1970-01-01 00:00:00'), o.created_at) >= ?
|
||
AND COALESCE(NULLIF(o.paid_at, '1970-01-01 00:00:00'), o.created_at) < ?
|
||
GROUP BY ui.invitee_id
|
||
HAVING COALESCE(SUM(o.actual_amount), 0) >= ?
|
||
) t
|
||
`
|
||
if err := s.repo.GetDbR().WithContext(ctx).Raw(query, inviterID, activityStart, start, end, thresholdAmount).Scan(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
func periodRange(activityType string, now time.Time) (time.Time, time.Time, string) {
|
||
loc := now.Location()
|
||
switch activityType {
|
||
case TypeWeekly:
|
||
dayOffset := (int(now.Weekday()) + 6) % 7
|
||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -dayOffset)
|
||
end := start.AddDate(0, 0, 7)
|
||
y, w := start.ISOWeek()
|
||
return start, end, fmt.Sprintf("%04d-W%02d", y, w)
|
||
case TypeMonthly:
|
||
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
|
||
end := start.AddDate(0, 1, 0)
|
||
return start, end, start.Format("2006-01")
|
||
default:
|
||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||
end := start.AddDate(0, 0, 1)
|
||
return start, end, start.Format("2006-01-02")
|
||
}
|
||
}
|
||
|
||
func maxInt64(a, b int64) int64 {
|
||
if a > b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|