feat: add redeem code affiliate rebate, batch concurrency API, and markdown page rendering
1. Redeem code affiliate rebate: balance-type redeem codes now trigger invite rebate for the inviter. Payment fulfillment uses context key to prevent double-rebate. 2. Batch concurrency update: new POST /admin/users/batch-concurrency endpoint supporting mode=set/add with all=true for all users. 3. Markdown page rendering: new GET /api/v1/pages/:slug API serves local .md files. Custom menu items with url="md:slug" render markdown with collapsible TOC sidebar, scroll spy, and copy buttons on code blocks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a1106e8167
commit
4cbd4932a0
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,15 @@ const (
|
||||
redeemLockDuration = 10 * time.Second // 锁超时时间,防止死锁
|
||||
)
|
||||
|
||||
type ctxKeySkipRedeemAffiliate struct{}
|
||||
|
||||
// ContextSkipRedeemAffiliate returns a context that suppresses the redeem-level
|
||||
// affiliate rebate. Used by payment fulfillment which handles rebate separately
|
||||
// via applyAffiliateRebateForOrder (with audit-log deduplication).
|
||||
func ContextSkipRedeemAffiliate(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, ctxKeySkipRedeemAffiliate{}, true)
|
||||
}
|
||||
|
||||
// RedeemCache defines cache operations for redeem service
|
||||
type RedeemCache interface {
|
||||
GetRedeemAttemptCount(ctx context.Context, userID int64) (int, error)
|
||||
@@ -80,6 +90,7 @@ type RedeemService struct {
|
||||
billingCacheService *BillingCacheService
|
||||
entClient *dbent.Client
|
||||
authCacheInvalidator APIKeyAuthCacheInvalidator
|
||||
affiliateService *AffiliateService
|
||||
}
|
||||
|
||||
// NewRedeemService 创建兑换码服务实例
|
||||
@@ -91,6 +102,7 @@ func NewRedeemService(
|
||||
billingCacheService *BillingCacheService,
|
||||
entClient *dbent.Client,
|
||||
authCacheInvalidator APIKeyAuthCacheInvalidator,
|
||||
affiliateService *AffiliateService,
|
||||
) *RedeemService {
|
||||
return &RedeemService{
|
||||
redeemRepo: redeemRepo,
|
||||
@@ -100,6 +112,7 @@ func NewRedeemService(
|
||||
billingCacheService: billingCacheService,
|
||||
entClient: entClient,
|
||||
authCacheInvalidator: authCacheInvalidator,
|
||||
affiliateService: affiliateService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +382,11 @@ func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (
|
||||
// 事务提交成功后失效缓存
|
||||
s.invalidateRedeemCaches(ctx, userID, redeemCode)
|
||||
|
||||
// 余额类正数兑换码触发邀请返利(best-effort,失败不影响兑换结果)
|
||||
if redeemCode.Type == RedeemTypeBalance && redeemCode.Value > 0 {
|
||||
s.tryAccrueAffiliateRebateForRedeem(ctx, userID, redeemCode.Value)
|
||||
}
|
||||
|
||||
// 重新获取更新后的兑换码
|
||||
redeemCode, err = s.redeemRepo.GetByID(ctx, redeemCode.ID)
|
||||
if err != nil {
|
||||
@@ -418,6 +436,26 @@ func (s *RedeemService) invalidateRedeemCaches(ctx context.Context, userID int64
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RedeemService) tryAccrueAffiliateRebateForRedeem(ctx context.Context, userID int64, amount float64) {
|
||||
if ctx.Value(ctxKeySkipRedeemAffiliate{}) != nil {
|
||||
return
|
||||
}
|
||||
if s.affiliateService == nil {
|
||||
return
|
||||
}
|
||||
if !s.affiliateService.IsEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
rebate, err := s.affiliateService.AccrueInviteRebate(ctx, userID, amount)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.redeem", "[Redeem] affiliate rebate failed for user %d amount %.2f: %v", userID, amount, err)
|
||||
return
|
||||
}
|
||||
if rebate > 0 {
|
||||
logger.LegacyPrintf("service.redeem", "[Redeem] affiliate rebate accrued %.8f for inviter of user %d", rebate, userID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取兑换码
|
||||
func (s *RedeemService) GetByID(ctx context.Context, id int64) (*RedeemCode, error) {
|
||||
code, err := s.redeemRepo.GetByID(ctx, id)
|
||||
|
||||
Reference in New Issue
Block a user