130 lines
4.2 KiB
Go
130 lines
4.2 KiB
Go
package yumigiftchallenge
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"chatapp3-golang/internal/model"
|
||
)
|
||
|
||
const (
|
||
defaultSettleDueBatch = 20
|
||
maxSettleDueBatch = 100
|
||
settleDueItemTimeout = 30 * time.Second
|
||
)
|
||
|
||
// SettleDue 是 chatapp-cron 的单轮幂等 runner:冻结到期周期,并恢复 PENDING/过租期 PROCESSING。
|
||
// 本仓库不启动 ticker;同一端点可被多个实例重复调用,数据库门闩和 delivery item 唯一键负责并发幂等。
|
||
func (s *Service) SettleDue(ctx context.Context, batch int) (*SettleDueResult, error) {
|
||
batch = normalizeSettleDueBatch(batch)
|
||
result := &SettleDueResult{FailedKeys: []string{}}
|
||
var periods []model.YumiGiftChallengePeriodSettlement
|
||
if err := s.db.WithContext(ctx).
|
||
Where("status IN ? AND snapshot_due_time <= ?", []string{StatusNotStarted, StatusProcessing}, time.Now()).
|
||
Order("snapshot_due_time ASC, id ASC").Limit(batch).Find(&periods).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, period := range periods {
|
||
if err := ctx.Err(); err != nil {
|
||
return result, err
|
||
}
|
||
result.PeriodsScanned++
|
||
request := SettlementRequest{PeriodType: period.PeriodType}
|
||
if period.StatDate != nil {
|
||
request.StatDate = *period.StatDate
|
||
}
|
||
itemCtx, cancel := context.WithTimeout(ctx, settleDueItemTimeout)
|
||
target, err := s.resolveSettlementTarget(itemCtx, period.ActivityID, request)
|
||
if err == nil {
|
||
err = s.freezeSettlementTarget(itemCtx, target)
|
||
}
|
||
if err == nil {
|
||
// 空榜直接完成;有获奖 parent 时保持 PROCESSING,后面的有界 delivery 恢复负责逐项发放。
|
||
err = s.refreshPeriodStatus(itemCtx, target.activity.ID, target.period, target.periodKey)
|
||
}
|
||
cancel()
|
||
if err != nil {
|
||
result.FailedKeys = append(result.FailedKeys, fmt.Sprintf("PERIOD:%d:%s:%s", period.ActivityID, period.PeriodType, period.PeriodKey))
|
||
continue
|
||
}
|
||
result.PeriodsFrozen++
|
||
}
|
||
if err := s.recoverTaskOwners(ctx, batch, result); err != nil {
|
||
return result, err
|
||
}
|
||
if err := s.recoverSettlementOwners(ctx, batch, result); err != nil {
|
||
return result, err
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s *Service) recoverTaskOwners(ctx context.Context, batch int, result *SettleDueResult) error {
|
||
var rows []model.YumiGiftChallengeUserTaskDaily
|
||
if err := s.db.WithContext(ctx).Select("id").
|
||
Where("delivery_status = ? OR (delivery_status = ? AND update_time <= ?)", DeliveryPending, DeliveryProcessing, s.deliveryLeaseCutoff()).
|
||
Order("update_time ASC, id ASC").Limit(batch).Find(&rows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range rows {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
result.TaskOwnersScanned++
|
||
itemCtx, cancel := context.WithTimeout(ctx, settleDueItemTimeout)
|
||
err := s.deliverOwnerItems(itemCtx, OwnerTask, row.ID)
|
||
cancel()
|
||
if err != nil {
|
||
result.FailedKeys = append(result.FailedKeys, fmt.Sprintf("TASK:%d", row.ID))
|
||
continue
|
||
}
|
||
result.OwnersProcessed++
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) recoverSettlementOwners(ctx context.Context, batch int, result *SettleDueResult) error {
|
||
var rows []model.YumiGiftChallengeSettlement
|
||
if err := s.db.WithContext(ctx).Select("id").
|
||
Where("delivery_status = ? OR (delivery_status = ? AND update_time <= ?)", DeliveryPending, DeliveryProcessing, s.deliveryLeaseCutoff()).
|
||
Order("update_time ASC, id ASC").Limit(batch).Find(&rows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range rows {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
result.SettlementOwnersScanned++
|
||
itemCtx, cancel := context.WithTimeout(ctx, settleDueItemTimeout)
|
||
err := s.ensureSettlementDeliveryItems(itemCtx, row.ID)
|
||
if err == nil {
|
||
err = s.deliverOwnerItems(itemCtx, OwnerSettlement, row.ID)
|
||
}
|
||
cancel()
|
||
if err != nil {
|
||
result.FailedKeys = append(result.FailedKeys, fmt.Sprintf("SETTLEMENT:%d", row.ID))
|
||
continue
|
||
}
|
||
result.OwnersProcessed++
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func normalizeSettleDueBatch(batch int) int {
|
||
if batch <= 0 {
|
||
return defaultSettleDueBatch
|
||
}
|
||
if batch > maxSettleDueBatch {
|
||
return maxSettleDueBatch
|
||
}
|
||
return batch
|
||
}
|
||
|
||
func (s *Service) deliveryLeaseCutoff() time.Time {
|
||
leaseSeconds := s.cfg.YumiGiftChallenge.DeliveryProcessingLeaseSeconds
|
||
if leaseSeconds < 30 {
|
||
leaseSeconds = 300
|
||
}
|
||
return time.Now().Add(-time.Duration(leaseSeconds) * time.Second)
|
||
}
|