|
|
@@ -178,7 +178,14 @@ export default {
|
|
|
// 独立 storage key:记录级归属专用,不会被 user-edit 的用户级消费逻辑清除
|
|
|
inviteRefCodeStorageKey: 'serviceRoleApplyRefCode',
|
|
|
// 未登录点提交后挂起:浮层登录成功回调里续跑提交,避免用户二次点击
|
|
|
- pendingSubmitAfterLogin: false
|
|
|
+ pendingSubmitAfterLogin: false,
|
|
|
+ // 用户级推荐人绑定幂等标记(资产一致性):整个流程至多成功绑定一次。
|
|
|
+ // bindReferralIfNotSelf 成功(含自邀 skipped:true)才置 true;首次失败保持 false 以便重试补绑。
|
|
|
+ // handleReapply/handleAddRole 多次提交不重置它,避免重复走 /api/invite/bind 重复发 F24 CF 资产、
|
|
|
+ // 或触发后端 CommissionService:140「已绑定推荐人,不可修改」误报错。
|
|
|
+ userRefBound: false,
|
|
|
+ // 绑定请求防重入锁:避免失败重试弹窗连点「重试绑定」导致并发 bind 请求
|
|
|
+ bindingRef: false
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -416,37 +423,95 @@ export default {
|
|
|
this.openLoginSheet({ refCode: this.inviteRefCode })
|
|
|
return
|
|
|
}
|
|
|
- await this.doSubmit(newRoles)
|
|
|
+ // 资产一致性(P0,串行):记录级 doSubmit 必须先执行并确认成功,之后才触发用户级绑定。
|
|
|
+ // doSubmit 失败(邀请码无效/查重拒绝/网络失败)会返回 false 并自行 toast,绝不进入绑定、
|
|
|
+ // 绝不给邀请人发 F24 CF 资产;也绝不让绑定链路的空 catch 吞掉 doSubmit 的失败。
|
|
|
+ var submitted = await this.doSubmit(newRoles)
|
|
|
+ if (submitted) {
|
|
|
+ await this.bindUserReferral()
|
|
|
+ }
|
|
|
},
|
|
|
// 就地浮层登录成功回调:自动续跑被挂起的提交(此时 token 已写入 storage)。
|
|
|
- // 用户级 referrerId 由 App.vue onShow / user-edit.vue 的现状链路消费 storage.refCode 写入,
|
|
|
- // 本页不重复调用 bindReferralIfNotSelf,避免记录级链路触发分销副作用。
|
|
|
- onInviteLoginSuccess() {
|
|
|
+ // 就地浮层登录不走 user-edit.vue:714 的推荐人消费点、也不触发 App.vue onShow 带参补绑,
|
|
|
+ // 故本页在「记录级申请成功」后主动补绑一次用户级推荐人(product 裁决:用户级走完整分销链路、
|
|
|
+ // 发 F24 CF 值是接受的语义)。绑定制约:①仅在 doSubmit 成功后串行触发,失败绝不发资产;
|
|
|
+ // ②与 user-edit 路径互斥受 userRefBound 幂等标记保护,整个流程至多成功一次,不重复发资产。
|
|
|
+ async onInviteLoginSuccess() {
|
|
|
if (!this.pendingSubmitAfterLogin) return
|
|
|
this.pendingSubmitAfterLogin = false
|
|
|
|
|
|
- // 用户级 referrerId 消费(推荐人=邀请人):就地浮层登录不走 user-edit.vue:714 的消费点,
|
|
|
- // 也不触发 App.vue onShow 带参补绑,故在此显式消费 App.vue:85 已存入的用户级 storage.refCode。
|
|
|
- // 以下三行严格同构 user-edit.vue:714-733 的「读码 → bindReferralIfNotSelf → 清码」,
|
|
|
- // 只清 refCode,不碰 inviteCode/inviteType(避免影响其他链路)。
|
|
|
- // 两级隔离:用户级 → storage.refCode → users.referrerId(走 /api/invite/bind,固有分销副作用);
|
|
|
- // 记录级 → storage.serviceRoleApplyRefCode → doSubmit 透传 refCode → 后端反查写
|
|
|
- // inviter_user_id(绝不经 /api/invite/bind,绝不触发 applyReferralSideEffects)。
|
|
|
- // 不 await、不阻塞记录级提交;绑定失败由 request() 统一 toast,可见但不影响申请受理。
|
|
|
- const userRefCode = uni.getStorageSync('refCode')
|
|
|
- if (userRefCode) {
|
|
|
- bindReferralIfNotSelf(userRefCode).catch(function() {})
|
|
|
- uni.removeStorageSync('refCode')
|
|
|
- }
|
|
|
-
|
|
|
const newRoles = (this.selectedRoles || []).filter(r => this.approvedRoles.indexOf(r) === -1)
|
|
|
- this.doSubmit(newRoles).catch(function() {})
|
|
|
+ // 资产一致性(P0,串行):与已登录路径完全同构。先确认记录级申请成功,再绑定用户级推荐人。
|
|
|
+ // doSubmit 失败返回 false 并自行 toast,绝不进入绑定、绝不给邀请人发 CF 资产。
|
|
|
+ var submitted = await this.doSubmit(newRoles)
|
|
|
+ if (submitted) {
|
|
|
+ await this.bindUserReferral()
|
|
|
+ }
|
|
|
},
|
|
|
// 浮层关闭(含「暂不登录先逛逛」handleSkipLogin):复位挂起标记,
|
|
|
// 避免用户放弃登录后标记残留,导致下次提交误走自动续提交分支。
|
|
|
onInviteLoginClose() {
|
|
|
this.pendingSubmitAfterLogin = false
|
|
|
},
|
|
|
+ // 用户级推荐人绑定(资产一致性核心 P0/P1)。
|
|
|
+ // 前置:仅由「记录级 doSubmit 成功」后的调用方触发;申请失败/无码绝不进这里,确保绝不误发 CF 资产。
|
|
|
+ // 语义:走完整分销链路 bindReferralIfNotSelf → /api/invite/bind → CommissionService.bindReferral
|
|
|
+ // → applyReferralSideEffects(发 F24 CF 值、directCount+1、排行榜)——用户裁决接受的语义。
|
|
|
+ // 两级隔离:记录级零副作用、强校验;用户级才发资产。两者同源于组件字段 inviteRefCode。
|
|
|
+ // 幂等:userRefBound 成功(含自邀 skipped)置 true,整个流程(含 handleReapply/handleAddRole
|
|
|
+ // 多次提交)至多成功一次,避免重复发资产 / 触发后端 CommissionService:140「已绑定推荐人」误报错。
|
|
|
+ // 失败可恢复:失败时保留 storage.refCode、userRefBound 保持 false,弹可见提示 + 就地重试入口;
|
|
|
+ // 用户可点「重试绑定」就地补绑,或从邀请链接重新进入走完整流程。
|
|
|
+ async bindUserReferral() {
|
|
|
+ if (this.userRefBound) return // 幂等:已成功绑定过,不重复发资产
|
|
|
+ if (this.bindingRef) return // 防重入:重试连点/异常时序不并发
|
|
|
+ // 防错配(P0):与记录级同源于组件字段 inviteRefCode(onLoad 解析的那份),
|
|
|
+ // 绝不从 uni.getStorageSync('refCode') 读,消除「storage 残留旧码 → 用户级绑到旧人」的窄边界;码空不绑定。
|
|
|
+ var code = this.inviteRefCode
|
|
|
+ if (!code) return
|
|
|
+
|
|
|
+ this.bindingRef = true
|
|
|
+ try {
|
|
|
+ // await 串行:绑定结果 settle 后才决定清码/置标记/提示,绝不 fire-and-forget
|
|
|
+ var bindRes = await bindReferralIfNotSelf(code)
|
|
|
+ if (bindRes && (bindRes.code === 200 || bindRes.skipped)) {
|
|
|
+ // 仅成功(含自邀 skipped:true)才置幂等标记 + 清用户级码;
|
|
|
+ // 失败分支绝不清码,保证可重试/重新进入补绑(不丢资产)。
|
|
|
+ this.userRefBound = true
|
|
|
+ uni.removeStorageSync('refCode')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 非 200 且非 skipped 理论上由 request() reject 走 catch,此处防御性兜底按失败处理
|
|
|
+ this.showBindRetryHint(bindRes && bindRes.message)
|
|
|
+ } catch (e) {
|
|
|
+ // 401 由 request() 统一 reLaunch 登录页,不在这里叠加 modal
|
|
|
+ if (e && e.code === 401) return
|
|
|
+ // 失败:保留 storage.refCode、userRefBound 保持 false。
|
|
|
+ // request() 拦截器已对非 200 业务响应 toast 后端具体 message(如「邀请码无效」「已有推荐人」),
|
|
|
+ // 这里用 modal 同时承载后端原文 + 明确重试引导:既不覆盖具体错误,又给出可见的重试入口。
|
|
|
+ this.showBindRetryHint(e && e.message)
|
|
|
+ } finally {
|
|
|
+ this.bindingRef = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 绑定失败提示 + 就地重试入口。申请已成功,故措辞不泛化「提交失败」;
|
|
|
+ // 后端具体 message 放 content 保留可见(request() toast 可能被 modal 盖住,原文不丢)。
|
|
|
+ showBindRetryHint(serverMsg) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '推荐人绑定失败',
|
|
|
+ content: (serverMsg ? serverMsg + '\n' : '') + '申请已提交成功,但推荐人未绑定成功。请重试,或从邀请链接重新进入。',
|
|
|
+ confirmText: '重试绑定',
|
|
|
+ cancelText: '稍后',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ // 幂等标记仍为 false、码仍保留,重试会重新发起绑定;成功即置位并清码
|
|
|
+ this.bindUserReferral()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 返回 boolean:仅当记录级申请真正落库成功(res.code===200)才返回 true。
|
|
|
+ // 调用方据此决定是否触发用户级绑定——这是「资产一致性」的唯一闸门:申请失败绝不发 CF 资产。
|
|
|
async doSubmit(newRoles) {
|
|
|
this.submitting = true
|
|
|
uni.showLoading({ title: '提交中...' })
|
|
|
@@ -471,10 +536,11 @@ export default {
|
|
|
uni.removeStorageSync(this.inviteRefCodeStorageKey)
|
|
|
await this.checkExistingApply()
|
|
|
if (!this.showStatusView) this.showStatusView = true
|
|
|
- } else {
|
|
|
- // 少数业务码(如 5001 无家庭)会被 request() resolve 而非 reject,此时 request 未弹提示,需在此兜底
|
|
|
- uni.showToast({ title: res.message || '提交失败', icon: 'none' })
|
|
|
+ return true
|
|
|
}
|
|
|
+ // 少数业务码(如 5001 无家庭)会被 request() resolve 而非 reject,此时 request 未弹提示,需在此兜底
|
|
|
+ uni.showToast({ title: res.message || '提交失败', icon: 'none' })
|
|
|
+ return false
|
|
|
} catch (e) {
|
|
|
uni.hideLoading()
|
|
|
// 邀请码无效/自邀请等后端业务错误:request() 已 showToast(e.message),这里用同一 message
|
|
|
@@ -483,6 +549,7 @@ export default {
|
|
|
if (e && e.message && e.code !== 401) {
|
|
|
uni.showToast({ title: e.message, icon: 'none' })
|
|
|
}
|
|
|
+ return false
|
|
|
} finally {
|
|
|
this.submitting = false
|
|
|
}
|