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

fix(signup) 修改注册后不调用接口bug

bill 2 недель назад
Родитель
Сommit
2f4db7e07d

+ 11 - 3
docs/refactor/signup-welcome-pack.md

@@ -65,10 +65,10 @@
 7. 提交使用 `POST /v1/user/guest-dec`。在原业务字段之外,将 `draftId` 与 `accessToken` 放在请求体最外层,不嵌套为 `draft`。两项缺失时阻止提交;草稿只保存在页面内存,不写入会员缓存。
 8. 提交响应按 `response.data || response` 读取订单,包含 `orderSn`、`amount`、`currency`、`reference`、`accessCode` 等支付信息;缺少 `accessCode` 或 `reference` 时显示错误。`authorizationUrl` 即使返回,当前也不用于整页跳转。
 9. 当前支付由 `GuestPaystack` 加载 `https://js.paystack.co/v2/inline.js`,调用 `resumeTransaction(accessCode)`。金额用于展示,不由前端重新创建交易或按旧流程乘 100 初始化扣款;交易金额与币种由服务端交易决定。
-10. 关闭弹窗仅隐藏,不删除订单。再次点击 Continue Payment 复用当前订单,不重复 POST;SDK 内防止重复打开。支付回调的 `reference` 必须与当前订单一致,随后停留注册页并提示等待订单确认,不跳转 `/shop/member-order`,也没有成功倒计时。
+10. 关闭弹窗仅隐藏,不删除订单。每次有效点击 Continue Payment 都重新校验当前表单并调用 `POST /v1/user/guest-dec`,成功后使用本次返回的支付信息打开弹窗;请求中阻止重复点击,失败或缺少交易标识时不打开旧支付信息。是否复用已有后台订单由接口决定。SDK 内仍防止重复打开,支付回调的 `reference` 必须与最新订单一致;随后停留注册页等待确认,不跳转 `/shop/member-order`,也没有成功倒计时。
 11. `paymentComplete` 后按钮显示 Payment submitted 并禁用;这只表示前端收到了匹配回调,不代表服务端已确认注册或付款成功。
 
-邀请链接形如 `/#/login?ref=会员ID`,登录页 Register 已透传查询参数。注册页创建时读取单个字符串 `ref`,去掉首尾空格后同时作为 `form.recUserName` 和 `form.conUserName` 的默认值;没有有效 `ref` 时留空,不猜测推荐人。初始化不触发推荐人查询或其他接口,也不反复覆盖手动修改;两项始终可编辑。手动更改推荐人仍会清空实际推荐人,需要重新填写并确认关系。创建订单传递用户最终填写的两项,已有订单的 Continue Payment 仍复用原交易。
+邀请链接形如 `/#/login?ref=会员ID`,登录页 Register 已透传查询参数。注册页创建时读取单个字符串 `ref`,去掉首尾空格后同时作为 `form.recUserName` 和 `form.conUserName` 的默认值;没有有效 `ref` 时留空,不猜测推荐人。初始化不触发推荐人查询或其他接口,也不反复覆盖手动修改;两项始终可编辑。手动更改推荐人仍会清空实际推荐人,需要重新填写并确认关系。Save 和 Continue Payment 都传递用户当前填写的两项。
 
 ## 5. 新 UI 必需的适配
 
@@ -78,7 +78,7 @@
 - 原默认选中支付列表第二项;保留该优先级,仅在单项或空列表时回退,避免读取不存在的第二项导致界面异常。
 - 支付弹窗为 420px 的白色圆角容器,展示 Paystack Payment、说明、只读邮箱、带币种与千分位的金额条,以及整行 Continue to Paystack 按钮;邮箱来自 `form.email`,金额和币种来自当前订单。
 - 旧 `vue-paystack` 的 V1 路径不能恢复当前服务端交易,新页通过局部继承组件使用 V2;不修改依赖或旧会员页面。组件原生按钮隐藏,由 Element 按钮调用 `payWithPaystack()`。
-- 支付取消保留订单与 accessCode;SDK 加载 / 交易错误转发至页面提示,并允许重试同一交易。当前没有订单删除、订单页导航或成功倒计时。
+- 支付取消不删除订单;再次点击页面的 Continue Payment 重新请求接口获取支付信息,弹窗内 Continue to Paystack 使用本次返回的 accessCode。SDK 加载 / 交易错误继续转发至页面提示;当前没有订单删除、订单页导航或成功倒计时。
 - 保留现有密码默认值与 maxlength,不在文档更新中新增业务校验或改写控件。
 
 上述适配仅发生在新注册页,不回写旧实现。
@@ -123,3 +123,11 @@
 - 定向 lint 为 0 error,保留 Country 标签既有的 2 条空格格式 warning;注册页模板编译通过。
 - Chromium 加载当前本地开发服务,验证邀请链接经登录 Register 的 query 传递、两项默认值、编辑、必填标记、刷新、空值提示与无 ref 的全新入口。所有 API 请求均在隔离浏览器内拦截,未创建真实订单或触发支付。
 - 本轮未运行生产构建;真实推荐关系校验、注册成功与支付仍需授权环境联调。
+
+## 10. 继续支付每次重新请求(2026-10-14)
+
+- 根据用户要求,移除 `onSubmit` 中检测到旧 accessCode 就直接打开弹窗的分支。页面的 Continue Payment 每次都经过原表单校验,再调用 `createGuestDec`;弹窗内的 Continue to Paystack 仍负责打开当前交易的收银台,商品展示组件不承载提交逻辑。
+- 保留请求中防重复提交与已收到支付回调后的禁用行为;返回信息完整后才更新支付信息并打开弹窗,失败时提示并解除 loading,不使用旧交易信息打开弹窗。
+- 先运行新回归用例确认旧逻辑下 4 项失败,再修改实现;两份支付 / 注册测试合计 43 项通过,覆盖连续请求、最新表单、请求并发、失败、不完整响应和原表单校验。定向 lint 为 0 error / 0 warning。
+- Chromium 在本地开发服务验证两次 Continue Payment 均发送 POST、使用最新邮箱 / 商品数量并展示本次返回金额;第三次模拟失败时弹窗保持关闭。API 和 SDK 均拦截替换,没有创建真实订单或触发真实支付。
+- 本轮未运行生产构建或真实后端联调,后台订单复用策略仍由接口决定;未提交或推送 Git。

+ 1 - 5
src/ui-refactor/views/signup/components/welcomePackRegistration.js

@@ -285,10 +285,6 @@ export default {
     // 游客报单
     onSubmit() {
       if (this.submitButtonStat || this.paymentComplete) return
-      if (this.paymentOrder.accessCode) {
-        this.visible = true
-        return
-      }
       if (!this.form.decLv) {
         this.$message({
           message: this.$t('profile.selectLevel'),
@@ -401,7 +397,7 @@ export default {
         })
     },
     handleClose() {
-      // 保留后端已创建的交易,再次支付时使用同一个 accessCode。
+      // 关闭只隐藏弹窗,继续支付时重新向后端获取支付信息。
       this.visible = false
     },
     processPayment(response) {

+ 61 - 3
tests/unit/ui-refactor/signup.spec.js

@@ -366,20 +366,78 @@ describe('Signup welcome-pack compatibility', () => {
     expect(vm.$message).toHaveBeenCalledWith({ message: expect.any(String), type: 'error' })
   })
 
-  it('reopens the same payment after closing without creating or deleting an order', () => {
+  it('refreshes payment details on every continuation using the current form and prevents concurrent requests', async() => {
     const vm = instance()
+    vm.draft = { draftId: 'test-draft', accessToken: 'test-only-guest-token' }
+    Object.assign(vm.form, {
+      decLv: 'level', recUserName: 'test-referrer', conUserName: 'test-actual-referrer',
+      payType: 'pay_stack', email: 'first@example.invalid'
+    })
+    vm.multipleSelection = [{ ID: 'goods', goodsNum: 1, MARKET_PRICE: 120 }]
     vm.paymentOrder = { orderSn: 'test-order', accessCode: 'test-access-code', reference: 'test-reference' }
+    const firstOrder = { accessCode: 'first-access-code', reference: 'first-reference', amount: 120, currency: 'NGN' }
+    const secondOrder = { accessCode: 'second-access-code', reference: 'second-reference', amount: 240, currency: 'NGN' }
+    request.mockResolvedValueOnce({ data: firstOrder }).mockResolvedValueOnce({ data: secondOrder })
     vm.visible = true
     vm.handleClose()
     expect(vm.visible).toBe(false)
     vm.onSubmit()
+    expect(vm.visible).toBe(false)
+    expect(vm.submitButtonStat).toBe(true)
+    vm.onSubmit()
+    expect(request).toHaveBeenCalledTimes(1)
+    await settle()
     expect(vm.visible).toBe(true)
-    expect(vm.paymentOrder.accessCode).toBe('test-access-code')
-    expect(request).not.toHaveBeenCalled()
+    expect(vm.paymentOrder).toEqual(firstOrder)
+    expect(vm.submitButtonStat).toBe(false)
+    vm.handleClose()
+    vm.form.email = 'updated@example.invalid'
+    vm.multipleSelection[0].goodsNum = 2
+    vm.onSubmit()
+    expect(request).toHaveBeenCalledTimes(2)
+    expect(request).toHaveBeenLastCalledWith({
+      url: '/v1/user/guest-dec', method: 'post', data: expect.objectContaining({
+        draftId: 'test-draft', accessToken: 'test-only-guest-token',
+        email: 'updated@example.invalid', goodsId: ['goods'], goodsNum: [2]
+      })
+    })
+    expect(vm.visible).toBe(false)
+    await settle()
+    expect(vm.visible).toBe(true)
+    expect(vm.paymentOrder).toEqual(secondOrder)
+    expect(vm.submitButtonStat).toBe(false)
     expect(window.location.assign).not.toHaveBeenCalled()
     expect(vm.$router.push).not.toHaveBeenCalled()
   })
 
+  it.each([false, true])('does not reopen cached payment after a failed or incomplete refresh: %s', async(incomplete) => {
+    const vm = instance()
+    vm.draft = { draftId: 'test-draft', accessToken: 'test-only-guest-token' }
+    Object.assign(vm.form, { decLv: 'level', recUserName: 'test-referrer', conUserName: 'test-actual-referrer', payType: 'pay_stack' })
+    vm.multipleSelection = [{ ID: 'goods', goodsNum: 1, MARKET_PRICE: 120 }]
+    vm.paymentOrder = { accessCode: 'old-access-code', reference: 'old-reference' }
+    if (incomplete) request.mockResolvedValue({ data: { reference: 'new-reference' }})
+    else request.mockRejectedValue(new Error('Unable to refresh payment.'))
+    vm.onSubmit()
+    expect(request).toHaveBeenCalledTimes(1)
+    await settle()
+    expect(vm.visible).toBe(false)
+    expect(vm.submitButtonStat).toBe(false)
+    expect(vm.$message).toHaveBeenCalledWith({
+      message: incomplete ? 'Payment details are missing. Please contact customer service.' : 'Unable to refresh payment.',
+      type: 'error'
+    })
+  })
+
+  it('validates the edited form before requesting continued payment', () => {
+    const vm = instance()
+    vm.paymentOrder = { accessCode: 'old-access-code', reference: 'old-reference' }
+    vm.onSubmit()
+    expect(request).not.toHaveBeenCalled()
+    expect(vm.visible).toBe(false)
+    expect(vm.$message).toHaveBeenCalledWith({ message: 'profile.selectLevel', type: 'error' })
+  })
+
   it('stays on signup after the payment callback and prevents another submission', () => {
     const vm = instance()
     vm.paymentOrder = { accessCode: 'test-access-code', reference: 'test-reference' }