Просмотр исходного кода

fix(team-invite): 收敛 P2 幂等抛错误报循环 + P1 status 视图补绑入口(纯前端1文件)\n\nP2 幂等抛错误报循环收敛:后端已实际绑定成功(CF 已发)但响应在传输中丢失时,\n用户点「重试绑定」会命中后端 bindReferral:141 抛错式幂等「已绑定推荐人,不可修改」,\n前端 catch 误弹失败 modal、userRefBound 永远 false 无法收敛。\n在 bindUserReferral 两处失败出口(防御性非200兜底 + catch)加判定:\ne.message / bindRes.message 含「已绑定推荐人」→ 按实质成功收敛——置 userRefBound=true、\n清僵尸码 storage.refCode、toast「推荐人已绑定」、return,不弹失败 modal、不置 bindingRef 失败态\n(finally 仍归 false)。判定位置在 401 提前 return 之后、showBindRetryHint 之前,401 逻辑不动。\n匹配用 indexOf(后端文案带后缀「,不可修改」),禁止改后端。\n\nP1 status 视图补绑入口:checkExistingApply 命中已有记录(status 0/1/2) → showStatusView=true\n→ 表单隐藏,此前绑定失败无任何补绑入口。在 status-view 模板 action-buttons 之前新增:\n v-if="!userRefBound && inviteRefCode" 的 rebind-section,含「推荐人尚未绑定成功,请在网络稳定后补绑」\n 文案 + 「补绑推荐人」按钮(:disabled="bindingRef" 防连点)。\n铁律:@click 直连 bindUserReferral,绝不接 handleSubmit/doSubmit(表单隐藏 + 后端查重会拒)。\n样式沿用 status-tip 卡片 + .btn-reapply 尺寸与品牌渐变,无新增 emoji。\n\n自验:vue-template-compiler 模板编译 + acorn script parse 双 OK(FAIL=0);无可选链/CSS Grid/新增 emoji;\n仅改 1 文件。后端零改动。基于最新 origin/cfclub a0ecc6067,独立分支 feature/team-invite-debt-fix。

Agnes Agent 16 часов назад
Родитель
Сommit
9bcec21e51
1 измененных файлов с 54 добавлено и 1 удалено
  1. 54 1
      cfc-frontend/pages/profile/service-role-apply.vue

+ 54 - 1
cfc-frontend/pages/profile/service-role-apply.vue

@@ -14,6 +14,12 @@
         <text class="reject-label">拒绝原因</text>
         <text class="reject-reason">{{ applyRecord && applyRecord.rejectReason }}</text>
       </view>
+      <!-- P1 补绑入口:记录已存在(status 0/1/2)、表单隐藏后,绑定失败时唯一可达的就地补绑路径。
+           @click 直连 bindUserReferral,绝不接 handleSubmit/doSubmit(表单隐藏 + 后端查重 ServiceImpl:74-76 会拒)。 -->
+      <view v-if="!userRefBound && inviteRefCode" class="rebind-section">
+        <text class="rebind-desc">推荐人尚未绑定成功,请在网络稳定后补绑</text>
+        <button class="btn-rebind" :disabled="bindingRef" @click="bindUserReferral">补绑推荐人</button>
+      </view>
       <view class="action-buttons">
         <button v-if="applyStatus === 'rejected'" class="btn-reapply" @click="handleReapply">
           重新申请
@@ -485,10 +491,28 @@ export default {
           return
         }
         // 非 200 且非 skipped 理论上由 request() reject 走 catch,此处防御性兜底按失败处理
-        this.showBindRetryHint(bindRes && bindRes.message)
+        // P2 幂等抛错误报循环收敛:后端已绑定成功但响应丢失、重试命中「已绑定推荐人」时按实质成功收敛。
+        var retryMsg = bindRes && bindRes.message
+        if (retryMsg && String(retryMsg).indexOf('已绑定推荐人') > -1) {
+          this.userRefBound = true
+          uni.removeStorageSync('refCode')
+          uni.showToast({ title: '推荐人已绑定', icon: 'none' })
+          return
+        }
+        this.showBindRetryHint(retryMsg)
       } catch (e) {
         // 401 由 request() 统一 reLaunch 登录页,不在这里叠加 modal
         if (e && e.code === 401) return
+        // P2 幂等抛错误报循环收敛(在 401 检查之后、showBindRetryHint 之前):
+        // 后端已实际绑定成功(CF 资产已发)但响应在传输中丢失 → 重试命中「已绑定推荐人,不可修改」,
+        // 此时按实质成功收敛:置幂等标记 + 清僵尸码 + toast,不弹失败 modal,
+        // 避免 userRefBound 永远 false、流程无法收敛为成功态。
+        if (e && e.message && String(e.message).indexOf('已绑定推荐人') > -1) {
+          this.userRefBound = true
+          uni.removeStorageSync('refCode')
+          uni.showToast({ title: '推荐人已绑定', icon: 'none' })
+          return
+        }
         // 失败:userRefBound 保持 false、inviteRefCode 未清,故可就地重试(不依赖 storage.refCode)。
         // request() 拦截器已对非 200 业务响应 toast 后端具体 message(如「邀请码无效」「已有推荐人」),
         // 这里用 modal 同时承载后端原文 + 明确重试引导:既不覆盖具体错误,又给出可见的重试入口。
@@ -675,6 +699,35 @@ export default {
   line-height: 1.6;
   display: block;
 }
+/* P1 推荐人补绑区:与 status-tip 同款卡片,按钮沿用 .btn-reapply 尺寸与品牌渐变,视觉同体系 */
+.rebind-section {
+  background: $app-surface;
+  border-radius: 16rpx;
+  padding: 24rpx 28rpx;
+  margin-bottom: 40rpx;
+  width: 100%;
+  box-sizing: border-box;
+}
+.rebind-desc {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $status-warning-strong;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.btn-rebind {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 24rpx;
+  font-size: calc(30rpx * var(--font-scale, 1));
+  text-align: center;
+  border: none;
+  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  color: $app-surface;
+}
+.btn-rebind[disabled] {
+  opacity: 0.5;
+}
 .action-buttons {
   width: 100%;
   display: flex;