feat(affiliate): 完善邀请返利系统

- 修复返利不到账的根因:tryClaimAffiliateRebateAudit 中 PostgreSQL 参数类型推断冲突
  - 补全 OAuth 注册路径(LinuxDo/OIDC/WeChat/Pending Flow)的邀请码绑定
  - 前端 OAuth 注册页面传递 aff_code 参数
  - 新增返利冻结期机制:可配置冻结时间,到期后自动解冻(懒解冻)
  - 新增返利有效期:绑定后 N 天内有效,过期不再产生返利
  - 新增单人返利上限:超出上限部分精确截断
  - 增强返利流程 slog 结构化日志,便于排查问题
  - 已邀请用户列表增加返利明细列
This commit is contained in:
shaw
2026-04-26 12:31:52 +08:00
parent 496469ac4e
commit 9b6dcc57bd
42 changed files with 852 additions and 104 deletions

View File

@@ -15,17 +15,20 @@
<LinuxDoOAuthSection
v-if="linuxdoOAuthEnabled"
:disabled="isLoading"
:aff-code="formData.aff_code"
:show-divider="false"
/>
<WechatOAuthSection
v-if="wechatOAuthEnabled"
:disabled="isLoading"
:aff-code="formData.aff_code"
:show-divider="false"
/>
<OidcOAuthSection
v-if="oidcOAuthEnabled"
:disabled="isLoading"
:provider-name="oidcOAuthProviderName"
:aff-code="formData.aff_code"
:show-divider="false"
/>
<div class="flex items-center gap-3">
@@ -293,6 +296,11 @@ import {
isRegistrationEmailSuffixAllowed,
normalizeRegistrationEmailSuffixWhitelist
} from '@/utils/registrationEmailPolicy'
import {
clearAffiliateReferralCode,
loadAffiliateReferralCode,
resolveAffiliateReferralCode
} from '@/utils/oauthAffiliate'
const { t, locale } = useI18n()
@@ -378,9 +386,19 @@ watch(validationToastMessage, (value, previousValue) => {
}
})
function syncAffiliateReferralCode(): string {
const code = resolveAffiliateReferralCode(route.query.aff, route.query.aff_code)
if (code) {
formData.aff_code = code
}
return code
}
// ==================== Lifecycle ====================
onMounted(async () => {
syncAffiliateReferralCode()
try {
const settings = await getPublicSettings()
registrationEnabled.value = settings.registration_enabled
@@ -407,10 +425,7 @@ onMounted(async () => {
await validatePromoCodeDebounced(promoParam)
}
}
const affParam = (route.query.aff as string) || (route.query.aff_code as string)
if (affParam) {
formData.aff_code = affParam.trim()
}
syncAffiliateReferralCode()
} catch (error) {
console.error('Failed to load public settings:', error)
} finally {
@@ -418,6 +433,13 @@ onMounted(async () => {
}
})
watch(
() => [route.query.aff, route.query.aff_code],
() => {
syncAffiliateReferralCode()
}
)
onUnmounted(() => {
if (promoValidateTimeout) {
clearTimeout(promoValidateTimeout)
@@ -702,6 +724,11 @@ async function handleRegister(): Promise<void> {
isLoading.value = true
try {
const affCode = formData.aff_code.trim() || loadAffiliateReferralCode()
if (affCode) {
formData.aff_code = affCode
}
// If email verification is enabled, redirect to verification page
if (emailVerifyEnabled.value) {
// Store registration data in sessionStorage
@@ -713,7 +740,7 @@ async function handleRegister(): Promise<void> {
turnstile_token: turnstileToken.value,
promo_code: formData.promo_code || undefined,
invitation_code: formData.invitation_code || undefined,
...(formData.aff_code ? { aff_code: formData.aff_code } : {})
...(affCode ? { aff_code: affCode } : {})
})
)
@@ -729,8 +756,9 @@ async function handleRegister(): Promise<void> {
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
promo_code: formData.promo_code || undefined,
invitation_code: formData.invitation_code || undefined,
...(formData.aff_code ? { aff_code: formData.aff_code } : {})
...(affCode ? { aff_code: affCode } : {})
})
clearAffiliateReferralCode()
// Show success toast
appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value }))