225 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}