|
|
@@ -125,11 +125,17 @@
|
|
|
<text>提交后将在 1-3 个工作日内完成审核</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <!-- 团队邀请:未登录提交时就地弹出登录浮层,登录后自动续提交。
|
|
|
+ 用本地浮层而非 401 → reLaunch 整页登录,是为了不销毁本组件:
|
|
|
+ 表单已填数据、inviteRefCode 均保留,登录后回到表单且不丢码。
|
|
|
+ login-sheet 由 main.js 全局注册;全局 loginSheetMixin 的 openLoginSheet 会命中本 ref。 -->
|
|
|
+ <login-sheet ref="loginSheet" @success="onInviteLoginSuccess" @close="onInviteLoginClose" />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { submitServiceRoleApply, getMyServiceRoleApply, uploadServiceRoleImage } from '../../utils/api'
|
|
|
+import { submitServiceRoleApply, getMyServiceRoleApply, uploadServiceRoleImage, bindReferralIfNotSelf } from '../../utils/api'
|
|
|
import { pickImage } from '../../utils/image-picker.js'
|
|
|
import AddressPicker from '@/components/address-picker.vue'
|
|
|
import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
@@ -162,7 +168,24 @@ export default {
|
|
|
},
|
|
|
submitting: false,
|
|
|
applyRecord: null,
|
|
|
- showStatusView: false
|
|
|
+ showStatusView: false,
|
|
|
+ // 团队邀请:直达链接 /pages/profile/service-role-apply?refCode=xxx 携带的邀请码(记录级归属)。
|
|
|
+ // 用户级推荐人链路的 storage.refCode 会被 user-edit.vue:733 消费并清除,绝不能复用;
|
|
|
+ // 故记录级另存独立 key 'serviceRoleApplyRefCode',与用户级彻底解耦、并存互不覆盖。
|
|
|
+ // 本组件字段用于常规提交;若未登录触发整页登录导致组件销毁,登录后回跳无参时由
|
|
|
+ // onLoad 从独立 key 恢复本字段,保证 refCode 端到端不丢。
|
|
|
+ inviteRefCode: '',
|
|
|
+ // 独立 storage key:记录级归属专用,不会被 user-edit 的用户级消费逻辑清除
|
|
|
+ inviteRefCodeStorageKey: 'serviceRoleApplyRefCode',
|
|
|
+ // 未登录点提交后挂起:浮层登录成功回调里续跑提交,避免用户二次点击
|
|
|
+ pendingSubmitAfterLogin: false,
|
|
|
+ // 用户级推荐人绑定幂等标记(资产一致性):整个流程至多成功绑定一次。
|
|
|
+ // bindReferralIfNotSelf 成功(含自邀 skipped:true)才置 true;首次失败保持 false 以便重试补绑。
|
|
|
+ // handleReapply/handleAddRole 多次提交不重置它,避免重复走 /api/invite/bind 重复发 F24 CF 资产、
|
|
|
+ // 或触发后端 CommissionService:140「已绑定推荐人,不可修改」误报错。
|
|
|
+ userRefBound: false,
|
|
|
+ // 绑定请求防重入锁:避免失败重试弹窗连点「重试绑定」导致并发 bind 请求
|
|
|
+ bindingRef: false
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -224,7 +247,19 @@ export default {
|
|
|
}[this.applyStatus] || ''
|
|
|
}
|
|
|
},
|
|
|
- onLoad() {
|
|
|
+ onLoad(options) {
|
|
|
+ // 团队邀请:解析直达链接的邀请码,存组件字段 + 独立 storage key(记录级归属专用)。
|
|
|
+ // 不用 storage.refCode:那是「用户级推荐人」链路(App.vue 捕获→user-edit.vue:733 消费并清除),
|
|
|
+ // 与记录级彻底解耦、互不覆盖。
|
|
|
+ if (options && options.refCode) {
|
|
|
+ this.inviteRefCode = options.refCode
|
|
|
+ uni.setStorageSync(this.inviteRefCodeStorageKey, options.refCode)
|
|
|
+ } else {
|
|
|
+ // 未登录提交触发整页登录 / 401 reLaunch 时组件会被销毁;登录成功回跳若不带 refCode 参数,
|
|
|
+ // 从独立 key 恢复,确保「登录后回到表单且 inviteRefCode 不丢」。
|
|
|
+ var cached = uni.getStorageSync(this.inviteRefCodeStorageKey)
|
|
|
+ if (cached) this.inviteRefCode = cached
|
|
|
+ }
|
|
|
this.checkExistingApply()
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -378,6 +413,109 @@ export default {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // 团队邀请:未登录时先就地弹登录浮层,登录后自动续提交。
|
|
|
+ // 不走接口层 401 → reLaunch 整页登录的原因:那会销毁本组件、丢失已填表单与 inviteRefCode,
|
|
|
+ // 且登录成功后回不到本表单。就地浮层由本页 <login-sheet> 承载,组件不销毁。
|
|
|
+ if (!uni.getStorageSync('token')) {
|
|
|
+ this.pendingSubmitAfterLogin = true
|
|
|
+ // refCode 一并透传给浮层:未登录态 App.vue 已把分享码存入用户级 storage.refCode,
|
|
|
+ // 这里再以 options.refCode 覆盖同源值(互不冲突),用户级 referrerId 仍由现状链路消费。
|
|
|
+ this.openLoginSheet({ refCode: this.inviteRefCode })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 资产一致性(P0,串行):记录级 doSubmit 必须先执行并确认成功,之后才触发用户级绑定。
|
|
|
+ // doSubmit 失败(邀请码无效/查重拒绝/网络失败)会返回 false 并自行 toast,绝不进入绑定、
|
|
|
+ // 绝不给邀请人发 F24 CF 资产;也绝不让绑定链路的空 catch 吞掉 doSubmit 的失败。
|
|
|
+ var submitted = await this.doSubmit(newRoles)
|
|
|
+ if (submitted) {
|
|
|
+ await this.bindUserReferral()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 就地浮层登录成功回调:自动续跑被挂起的提交(此时 token 已写入 storage)。
|
|
|
+ // 就地浮层登录不走 user-edit.vue:714 的推荐人消费点、也不触发 App.vue onShow 带参补绑,
|
|
|
+ // 故本页在「记录级申请成功」后主动补绑一次用户级推荐人(product 裁决:用户级走完整分销链路、
|
|
|
+ // 发 F24 CF 值是接受的语义)。绑定制约:①仅在 doSubmit 成功后串行触发,失败绝不发资产;
|
|
|
+ // ②与 user-edit 路径互斥受 userRefBound 幂等标记保护,整个流程至多成功一次,不重复发资产。
|
|
|
+ async onInviteLoginSuccess() {
|
|
|
+ if (!this.pendingSubmitAfterLogin) return
|
|
|
+ this.pendingSubmitAfterLogin = false
|
|
|
+
|
|
|
+ const newRoles = (this.selectedRoles || []).filter(r => this.approvedRoles.indexOf(r) === -1)
|
|
|
+ // 资产一致性(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「已绑定推荐人」误报错。
|
|
|
+ // 失败可恢复:绑定源 this.inviteRefCode 组件字段全程未清、userRefBound 与 bindingRef 均保持 false,
|
|
|
+ // 配合 modal「重试绑定」回调即可就地重新发起绑定(真实可达的恢复路径)。
|
|
|
+ // 注意:storage.refCode 已成僵尸字段(不参与绑定、清不清都不影响),可重试不依赖它;
|
|
|
+ // 「从邀请链接重新进入」也不是补绑路径——重进会命中 checkExistingApply 的 status=0 记录进入审核态、无补绑入口,
|
|
|
+ // 故离开本页后只能联系邀请人/客服补绑,modal 文案据实说明、不承诺走不通的路径。
|
|
|
+ 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)才置幂等标记,并清掉僵尸字段 storage.refCode(不参与绑定,仅为避免残留)。
|
|
|
+ // 可重试的真正保证是 this.inviteRefCode 组件字段未清 + userRefBound=false + modal 回调,与 storage.refCode 无关。
|
|
|
+ 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
|
|
|
+ // 失败:userRefBound 保持 false、inviteRefCode 未清,故可就地重试(不依赖 storage.refCode)。
|
|
|
+ // 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、bindingRef 已归 false、inviteRefCode 未清,重试重新发起绑定;成功即置位
|
|
|
+ this.bindUserReferral()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 返回 boolean:仅当记录级申请真正落库成功(res.code===200)才返回 true。
|
|
|
+ // 调用方据此决定是否触发用户级绑定——这是「资产一致性」的唯一闸门:申请失败绝不发 CF 资产。
|
|
|
+ async doSubmit(newRoles) {
|
|
|
this.submitting = true
|
|
|
uni.showLoading({ title: '提交中...' })
|
|
|
try {
|
|
|
@@ -390,19 +528,31 @@ export default {
|
|
|
city: this.form.city,
|
|
|
district: this.form.district,
|
|
|
street: this.form.street,
|
|
|
- addressDetail: this.form.addressDetail
|
|
|
+ addressDetail: this.form.addressDetail,
|
|
|
+ // 团队邀请记录级归属:透传直达链接的 refCode,由后端服务端反查邀请人 userId 写
|
|
|
+ // inviter_user_id。不传前端自算 inviterUserId(防冒领),不触发任何分销副作用。
|
|
|
+ refCode: this.inviteRefCode
|
|
|
})
|
|
|
uni.hideLoading()
|
|
|
if (res.code === 200) {
|
|
|
- // 提交成功,刷新状态
|
|
|
+ // 提交成功,刷新状态。此刻邀请归属已落库,清除独立 key 防止下次无码进入时误挂旧邀请人。
|
|
|
+ uni.removeStorageSync(this.inviteRefCodeStorageKey)
|
|
|
await this.checkExistingApply()
|
|
|
if (!this.showStatusView) this.showStatusView = true
|
|
|
- } else {
|
|
|
- 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()
|
|
|
- uni.showToast({ title: '提交失败,请重试', icon: 'none' })
|
|
|
+ // 邀请码无效/自邀请等后端业务错误:request() 已 showToast(e.message),这里用同一 message
|
|
|
+ // 再弹一次作双保险(同内容覆盖,不会泛化),确保「邀请码绑定失败必须有可见错误反馈」。
|
|
|
+ // 绝不用「提交失败,请重试」覆盖后端具体原因;401/纯网络异常由 request() 统一提示,此处静默。
|
|
|
+ if (e && e.message && e.code !== 401) {
|
|
|
+ uni.showToast({ title: e.message, icon: 'none' })
|
|
|
+ }
|
|
|
+ return false
|
|
|
} finally {
|
|
|
this.submitting = false
|
|
|
}
|