mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 13:40:44 +08:00
feat(affiliate): 完善邀请返利系统
- 修复返利不到账的根因:tryClaimAffiliateRebateAudit 中 PostgreSQL 参数类型推断冲突 - 补全 OAuth 注册路径(LinuxDo/OIDC/WeChat/Pending Flow)的邀请码绑定 - 前端 OAuth 注册页面传递 aff_code 参数 - 新增返利冻结期机制:可配置冻结时间,到期后自动解冻(懒解冻) - 新增返利有效期:绑定后 N 天内有效,过期不再产生返利 - 新增单人返利上限:超出上限部分精确截断 - 增强返利流程 slog 结构化日志,便于排查问题 - 已邀请用户列表增加返利明细列
This commit is contained in:
48
frontend/src/utils/__tests__/oauthAffiliate.spec.ts
Normal file
48
frontend/src/utils/__tests__/oauthAffiliate.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
clearAffiliateReferralCode,
|
||||
clearOAuthAffiliateCode,
|
||||
loadAffiliateReferralCode,
|
||||
loadOAuthAffiliateCode,
|
||||
resolveAffiliateReferralCode,
|
||||
storeAffiliateReferralCode,
|
||||
storeOAuthAffiliateCode
|
||||
} from '@/utils/oauthAffiliate'
|
||||
|
||||
describe('oauthAffiliate', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('persists affiliate referral code across pages', () => {
|
||||
expect(resolveAffiliateReferralCode(' 5579J7CFG9PF ')).toBe('5579J7CFG9PF')
|
||||
expect(loadAffiliateReferralCode()).toBe('5579J7CFG9PF')
|
||||
expect(resolveAffiliateReferralCode()).toBe('5579J7CFG9PF')
|
||||
})
|
||||
|
||||
it('expires stale affiliate referral code', () => {
|
||||
const now = Date.UTC(2026, 0, 1)
|
||||
storeAffiliateReferralCode('AFF123', now)
|
||||
|
||||
expect(loadAffiliateReferralCode(now + 30 * 24 * 60 * 60 * 1000 - 1)).toBe('AFF123')
|
||||
expect(loadAffiliateReferralCode(now + 30 * 24 * 60 * 60 * 1000 + 1)).toBe('')
|
||||
expect(localStorage.getItem('affiliate_referral_code')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps oauth transient code separate from persistent referral code', () => {
|
||||
storeAffiliateReferralCode('PERSISTED')
|
||||
storeOAuthAffiliateCode('OAUTH')
|
||||
|
||||
expect(loadAffiliateReferralCode()).toBe('PERSISTED')
|
||||
expect(loadOAuthAffiliateCode()).toBe('OAUTH')
|
||||
|
||||
clearOAuthAffiliateCode()
|
||||
expect(loadOAuthAffiliateCode()).toBe('')
|
||||
expect(loadAffiliateReferralCode()).toBe('PERSISTED')
|
||||
|
||||
clearAffiliateReferralCode()
|
||||
expect(loadAffiliateReferralCode()).toBe('')
|
||||
})
|
||||
})
|
||||
133
frontend/src/utils/oauthAffiliate.ts
Normal file
133
frontend/src/utils/oauthAffiliate.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
const OAUTH_AFFILIATE_CODE_KEY = 'oauth_aff_code'
|
||||
const AFFILIATE_REFERRAL_CODE_KEY = 'affiliate_referral_code'
|
||||
const AFFILIATE_REFERRAL_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
interface StoredAffiliateReferralCode {
|
||||
code: string
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
export function normalizeOAuthAffiliateCode(value?: unknown): string {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
return typeof raw === 'string' ? raw.trim() : ''
|
||||
}
|
||||
|
||||
export function pickOAuthAffiliateCode(...values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
const code = normalizeOAuthAffiliateCode(value)
|
||||
if (code) {
|
||||
return code
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function storeAffiliateReferralCode(value?: unknown, now = Date.now()): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const code = normalizeOAuthAffiliateCode(value)
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload: StoredAffiliateReferralCode = {
|
||||
code,
|
||||
expiresAt: now + AFFILIATE_REFERRAL_TTL_MS
|
||||
}
|
||||
window.localStorage.setItem(AFFILIATE_REFERRAL_CODE_KEY, JSON.stringify(payload))
|
||||
} catch {
|
||||
// 忽略浏览器存储异常。
|
||||
}
|
||||
}
|
||||
|
||||
export function loadAffiliateReferralCode(now = Date.now()): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
const raw = window.localStorage.getItem(AFFILIATE_REFERRAL_CODE_KEY)
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
const parsed = JSON.parse(raw) as Partial<StoredAffiliateReferralCode>
|
||||
const code = normalizeOAuthAffiliateCode(parsed.code)
|
||||
const expiresAt = Number(parsed.expiresAt) || 0
|
||||
if (!code || expiresAt <= now) {
|
||||
clearAffiliateReferralCode()
|
||||
return ''
|
||||
}
|
||||
return code
|
||||
} catch {
|
||||
clearAffiliateReferralCode()
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAffiliateReferralCode(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
window.localStorage.removeItem(AFFILIATE_REFERRAL_CODE_KEY)
|
||||
} catch {
|
||||
// 忽略浏览器存储异常。
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAffiliateReferralCode(...values: unknown[]): string {
|
||||
const code = pickOAuthAffiliateCode(...values)
|
||||
if (code) {
|
||||
storeAffiliateReferralCode(code)
|
||||
return code
|
||||
}
|
||||
return loadAffiliateReferralCode()
|
||||
}
|
||||
|
||||
export function storeOAuthAffiliateCode(value?: unknown): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const code = normalizeOAuthAffiliateCode(value)
|
||||
try {
|
||||
if (code) {
|
||||
window.sessionStorage.setItem(OAUTH_AFFILIATE_CODE_KEY, code)
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_AFFILIATE_CODE_KEY)
|
||||
}
|
||||
} catch {
|
||||
// 忽略浏览器存储异常。
|
||||
}
|
||||
}
|
||||
|
||||
export function loadOAuthAffiliateCode(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
return normalizeOAuthAffiliateCode(window.sessionStorage.getItem(OAUTH_AFFILIATE_CODE_KEY))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function clearOAuthAffiliateCode(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
window.sessionStorage.removeItem(OAUTH_AFFILIATE_CODE_KEY)
|
||||
} catch {
|
||||
// 忽略浏览器存储异常。
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAllAffiliateReferralCodes(): void {
|
||||
clearOAuthAffiliateCode()
|
||||
clearAffiliateReferralCode()
|
||||
}
|
||||
|
||||
export function oauthAffiliatePayload(value?: unknown): { aff_code?: string } {
|
||||
const code = normalizeOAuthAffiliateCode(value)
|
||||
return code ? { aff_code: code } : {}
|
||||
}
|
||||
Reference in New Issue
Block a user