# 会员费虚拟支付开关实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 在会员费支付链路增加 `member_pay_virtual_enabled` 配置开关,通过后端 `payType` 字段分流到虚拟支付(`requestVirtualPayment`)或普通微信支付(`wx.requestPayment`),并打通 v3 JSAPI 回调到会员开通。 **架构:** 后端在 `MembershipService.createOrderWithPayType()` 内读 `sys_config` 开关,返回带 `payType` 字段的统一响应;前端 `pay.vue` 根据 `payType` 分支调用对应支付 SDK;普通支付 v3 回调通过 `PaymentService.handleWechatNotify()` ORD 前缀路由到 `processPaymentCallback()`。 **技术栈:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus + 微信支付 v3 API(JSAPI)+ uni-app Vue 2 --- ## 文件结构 | 文件 | 变更类型 | 职责 | |------|---------|------| | `cfc-backend/.../service/MembershipService.java` | 修改 | 新增 `createOrderWithPayType()`;`createOrder()` 拆单逻辑增加 `splitEnabled` 控制 | | `cfc-backend/.../controller/MembershipController.java` | 修改 | `createOrder()` 接收 `openid`,调用新方法 | | `cfc-backend/.../service/PaymentService.java` | 修改 | `handleWechatNotify()` 补充 ORD 前缀路由;注入 `MembershipService`(`@Lazy`) | | `cfc-backend/src/test/java/.../service/MembershipPaySwitchTest.java` | 创建 | 单元测试覆盖开关分流逻辑 | | `cfc-frontend/utils/api.js` | 修改 | `createOrder()` 透传 `openid` 参数 | | `cfc-frontend/pages/membership/pay.vue` | 修改 | 按 `payType` 分支,新增 `payWechat()` 方法 | --- ## 任务 1:MembershipService 新增 createOrderWithPayType() **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java` - 测试:`cfc-backend/src/test/java/com/etotem/cfc/service/MembershipPaySwitchTest.java` **前置知识:** - `sysConfigService.getValue("member_pay_virtual_enabled")` 返回 null 或 "1" 表示开启虚拟支付;返回 "0" 表示关闭 - `virtualPayService.generateMembershipPayParams(orderNo, sessionKey)` 生成虚拟支付参数,返回 null 表示道具未配置 - `paymentService.createWechatPrepay(orderNo, description, amount, openid)` 生成 v3 JSAPI 预付参数 - 拆单逻辑在 `createOrder()` L369-L384,基于 `virtual_goods_config.goods_price` 判断——关闭虚拟支付时应跳过(`splitEnabled=false`) - `createOrder()` 的 3 个调用方(L190/210/231)均走虚拟支付,不受影响 **步骤 1.1:在 MembershipService 中注入 PaymentService** 在 `MembershipService.java` 现有的 `@Resource` 块(L27 起)追加: ```java @Resource private PaymentService paymentService; ``` **步骤 1.2:为 createOrder() 拆单逻辑增加 splitEnabled 控制** 在 `MembershipService.java` L295 找到 `createOrder()` 方法,找到 L369 行: ```java // 修改前: if (!"trial".equals(paymentType)) { // 修改后: if (!"trial".equals(paymentType)) { ``` 替换为: ```java // 在 createOrder() 方法开头(L295 之后)增加 splitEnabled=true 默认值 // 原 createOrder() 签名不变(保持向后兼容);拆单条件改为始终执行(默认拆单开启) if (!"trial".equals(paymentType)) { ``` > **说明**:`createOrder()` 现有调用方均为虚拟支付路径,拆单行为不变。新建的 `createOrderWithPayType()` 不走 `createOrder()`,而是直接内联订单创建逻辑,避免拆单。 **步骤 1.3:新增 createOrderWithPayType() 方法(内联订单创建)** 在 `MembershipService.java` 中 `createOrder()` 方法之后新增。该方法完整复制 `createOrder()` 的订单创建逻辑,但增加 `splitEnabled` 参数(关闭时跳过 L369-L384 的拆单判断): ```java /** * 创建会员订单并按 member_pay_virtual_enabled 开关决定支付方式。 * * @param sessionKey 当前用户 sessionKey(虚拟支付签名用;普通支付时忽略) * @param openid 普通支付时需要(JSAPI payer.openid);虚拟支付时可为 null * @return 统一响应 map,含 payType("virtual"/"wechat"/"none")及对应字段; * 虚拟支付道具未配置时返回 null(调用方转 46001) */ public Map createOrderWithPayType(Long userId, Long familyId, String levelCode, String paymentType, String period, Long userCouponId, String sessionKey, String openid) { // === 内联订单创建(复制 createOrder() 核心逻辑,splitEnabled 由开关控制)=== if (familyId == null) { throw new RuntimeException("请先创建或加入家庭后再购买会员"); } MembershipLevel level = levelMapper.selectOne( new LambdaQueryWrapper() .eq(MembershipLevel::getLevelCode, levelCode) .last("LIMIT 1")); if (level == null) { throw new RuntimeException("会员等级不存在"); } // 根据 period 确定费用(分)— 与 createOrder() 逻辑完全一致 Integer amount; String effectivePeriod = period != null ? period : "yearly"; if ("FAMILY".equals(levelCode)) { effectivePeriod = "yearly"; String feeStr = sysConfigService.getValue("member_fee_family_yearly"); if (feeStr == null) { feeStr = sysConfigService.getValue("member_fee_family"); } amount = feeStr != null ? Integer.parseInt(feeStr) : 131400; } else if ("PROVIDER".equals(levelCode)) { String configKey = "member_fee_standard_" + effectivePeriod; String feeStr = sysConfigService.getValue(configKey); if (feeStr == null) { feeStr = sysConfigService.getValue("member_fee_standard"); } amount = feeStr != null ? Integer.parseInt(feeStr) : 1314; } else if ("PREMIUM".equals(levelCode)) { if ("monthly".equals(effectivePeriod)) { String feeStr = sysConfigService.getValue("member_fee_premium_monthly"); amount = feeStr != null ? Integer.parseInt(feeStr) : 131400; } else { String feeStr = sysConfigService.getValue("member_fee_premium"); amount = feeStr != null ? Integer.parseInt(feeStr) : 1316800; } } else { if ("monthly".equals(effectivePeriod)) { amount = level.getPriceMonthly(); } else if ("quarterly".equals(effectivePeriod)) { amount = level.getPriceQuarterly() != null ? level.getPriceQuarterly() : level.getPriceMonthly() * 3; } else { amount = level.getPriceYearly(); } } int originalAmount = amount; int finalAmount = originalAmount; Long appliedCouponId = null; if (!"trial".equals(paymentType)) { if (userCouponId != null) { Integer discount = couponService.apply(userId, userCouponId, "MEMBERSHIP", originalAmount); if (discount != null && discount > 0) { finalAmount = Math.max(0, originalAmount - discount); appliedCouponId = userCouponId; } } } else { finalAmount = 0; } // 开关判定:缺省/null/"1"=虚拟支付(拆单开启);"0"=普通支付(拆单关闭) boolean virtualEnabled = !"0".equals(sysConfigService.getValue("member_pay_virtual_enabled")); // 拆单:查 MEMBERSHIP 道具配置的 goods_price,按整倍数判断(virtualEnabled=false 时跳过) String groupNo = null; int totalPeriods = 1; Integer splitPeriodAmount = null; if (!"trial".equals(paymentType) && virtualEnabled) { VirtualGoodsConfig goodsConfig = virtualGoodsConfigService.getEnabled("MEMBERSHIP", levelCode); Integer goodsPrice = goodsConfig != null ? goodsConfig.getGoodsPrice() : null; if (goodsPrice != null && goodsPrice > 0) { if (finalAmount % goodsPrice != 0) { throw new RuntimeException("支付金额必须是道具价格的整数倍,无法支付"); } totalPeriods = finalAmount / goodsPrice; if (totalPeriods > 1) { splitPeriodAmount = goodsPrice; groupNo = OrderNoGenerator.generate("G"); } else { splitPeriodAmount = goodsPrice; } } } // 创建订单 PaymentOrder mainOrder = null; List allOrders = new ArrayList<>(); if (groupNo != null) { for (int i = 1; i <= totalPeriods; i++) { PaymentOrder sub = new PaymentOrder(); sub.setOrderNo(generateOrderNo()); sub.setFamilyId(familyId); sub.setLevelCode(levelCode); sub.setPaymentType(paymentType); sub.setPeriod(effectivePeriod); sub.setAmount(splitPeriodAmount); sub.setUserCouponId(i == 1 ? appliedCouponId : null); sub.setStatus("pending"); sub.setGroupNo(groupNo); sub.setPeriodNo(i); sub.setCreatedAt(new Date()); sub.setUpdatedAt(new Date()); paymentOrderMapper.insert(sub); allOrders.add(sub); if (i == 1) { mainOrder = sub; } } } else { PaymentOrder order = new PaymentOrder(); order.setOrderNo(generateOrderNo()); order.setFamilyId(familyId); order.setLevelCode(levelCode); order.setPaymentType(paymentType); order.setPeriod(effectivePeriod); order.setAmount(finalAmount); order.setUserCouponId(appliedCouponId); order.setStatus("trial".equals(paymentType) ? "paid" : "pending"); order.setPeriodNo(1); order.setCreatedAt(new Date()); order.setUpdatedAt(new Date()); paymentOrderMapper.insert(order); allOrders.add(order); mainOrder = order; } if ("trial".equals(paymentType)) { activateTrialMembership(userId, familyId, mainOrder); } // === 构造响应 === Map resp = new HashMap<>(); resp.put("orderNo", mainOrder.getOrderNo()); resp.put("status", mainOrder.getStatus()); resp.put("levelCode", mainOrder.getLevelCode()); resp.put("amount", mainOrder.getAmount()); resp.put("groupNo", mainOrder.getGroupNo()); resp.put("periodNo", mainOrder.getPeriodNo()); resp.put("totalPeriods", totalPeriods); // trial 单已直接开通,无需支付 if ("trial".equals(paymentType)) { resp.put("payType", "none"); return resp; } if (virtualEnabled) { VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams(mainOrder.getOrderNo(), sessionKey); if (dto == null) { return null; // 道具未配置,调用方转 46001 } resp.put("payType", "virtual"); resp.put("orderInfo", dto.getOrderInfo()); resp.put("extInfo", dto.getExtInfo()); resp.put("sign", dto.getSign()); resp.put("signature", dto.getSignature()); resp.put("env", dto.getEnv()); resp.put("signType", dto.getSignType()); resp.put("currentPeriod", dto.getCurrentPeriod()); return resp; } // 普通微信支付(v3 JSAPI) String description = "浠艾福-" + levelCode + "会员"; Map payParams = paymentService.createWechatPrepay( mainOrder.getOrderNo(), description, mainOrder.getAmount(), openid); resp.put("payType", "wechat"); resp.put("wechatPayParams", payParams); return resp; } ``` **步骤 1.4:编写单元测试** 创建 `cfc-backend/src/test/java/com/etotem/cfc/service/MembershipPaySwitchTest.java`: ```java package com.etotem.cfc.service; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import com.etotem.cfc.dto.PaymentOrderDTO; import com.etotem.cfc.dto.VirtualPayParamsDTO; import com.etotem.cfc.entity.MembershipLevel; import com.etotem.cfc.entity.PaymentOrder; import com.etotem.cfc.mapper.*; import com.etotem.cfc.entity.Family; import com.etotem.cfc.entity.User; @ExtendWith(MockitoExtension.class) class MembershipPaySwitchTest { @InjectMocks private MembershipService membershipService; @Mock private MembershipLevelMapper levelMapper; @Mock private FamilyMembershipMapper membershipMapper; @Mock private FamilyMapper familyMapper; @Mock private UserMapper userMapper; @Mock private PaymentOrderMapper paymentOrderMapper; @Mock private CommissionService commissionService; @Mock private SysConfigService sysConfigService; @Mock private MemberUpgradeRecordMapper memberUpgradeRecordMapper; @Mock private MembershipBenefitsMapper benefitsMapper; @Mock private CouponService couponService; @Mock private TrialMembershipMapper trialMembershipMapper; @Mock private VirtualGoodsConfigService virtualGoodsConfigService; @Mock private VirtualPayService virtualPayService; @Mock private PaymentService paymentService; // --- 虚拟支付开启(默认) --- @Test void createOrderWithPayType_virtualEnabled_returnsVirtualPayType() { when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn("1"); when(sysConfigService.getValue("member_fee_family")).thenReturn("131400"); when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null)); when(paymentOrderMapper.selectCount(any())).thenReturn(0L); when(couponService.apply(anyLong(), anyLong(), anyString(), anyInt())).thenReturn(null); when(paymentOrderMapper.insert(any())).thenReturn(1); VirtualPayParamsDTO mockDto = new VirtualPayParamsDTO(); mockDto.setOrderInfo(new HashMap<>()); mockDto.setSign("mock-sign"); mockDto.setSignature("mock-sig"); when(virtualPayService.generateMembershipPayParams(anyString(), anyString())).thenReturn(mockDto); User user = new User(); user.setFamilyId(1L); when(userMapper.selectById(1L)).thenReturn(user); Map result = membershipService.createOrderWithPayType( 1L, 1L, "FAMILY", "pay", "yearly", null, "mockSessionKey", null); assertNotNull(result); assertEquals("virtual", result.get("payType")); assertEquals("ORD", result.get("orderNo") != null ? result.get("orderNo").toString().substring(0, 3) : ""); assertNotNull(result.get("orderInfo")); assertNotNull(result.get("sign")); } // --- 虚拟支付关闭 --- @Test void createOrderWithPayType_virtualDisabled_returnsWechatPayType() { when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn("0"); when(sysConfigService.getValue("member_fee_family")).thenReturn("131400"); when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null)); when(paymentOrderMapper.selectCount(any())).thenReturn(0L); when(couponService.apply(anyLong(), anyLong(), anyString(), anyInt())).thenReturn(null); when(paymentOrderMapper.insert(any())).thenReturn(1); Map mockWechatParams = new HashMap<>(); mockWechatParams.put("appId", "wx5ba8038ef16fb245"); mockWechatParams.put("timeStamp", "1724900000"); mockWechatParams.put("package", "prepay_id=wx_mock"); when(paymentService.createWechatPrepay(anyString(), anyString(), anyInt(), anyString())) .thenReturn(mockWechatParams); User user = new User(); user.setFamilyId(1L); when(userMapper.selectById(1L)).thenReturn(user); Map result = membershipService.createOrderWithPayType( 1L, 1L, "FAMILY", "pay", "yearly", null, "mockSessionKey", "mockOpenid"); assertNotNull(result); assertEquals("wechat", result.get("payType")); assertNotNull(result.get("wechatPayParams")); } @Test void createOrderWithPayType_virtualDisabled_noSplit() { when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn("0"); when(sysConfigService.getValue("member_fee_family")).thenReturn("131400"); when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null)); when(paymentOrderMapper.selectCount(any())).thenReturn(0L); when(couponService.apply(anyLong(), anyLong(), anyString(), anyInt())).thenReturn(null); when(paymentOrderMapper.insert(any())).thenReturn(1); Map mockWechatParams = new HashMap<>(); mockWechatParams.put("package", "prepay_id=wx_mock"); when(paymentService.createWechatPrepay(anyString(), anyString(), anyInt(), anyString())) .thenReturn(mockWechatParams); User user = new User(); user.setFamilyId(1L); when(userMapper.selectById(1L)).thenReturn(user); Map result = membershipService.createOrderWithPayType( 1L, 1L, "FAMILY", "pay", "yearly", null, "mockSessionKey", "mockOpenid"); assertNotNull(result); assertEquals("wechat", result.get("payType")); assertNull(result.get("groupNo")); assertEquals(1, result.get("totalPeriods")); } // --- trial 路径 --- @Test void createOrderWithPayType_trial_returnsNone() { when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn("1"); when(sysConfigService.getValue("member_fee_family")).thenReturn("131400"); when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null)); when(paymentOrderMapper.selectCount(any())).thenReturn(0L); when(paymentOrderMapper.insert(any())).thenReturn(1); User user = new User(); user.setFamilyId(1L); when(userMapper.selectById(1L)).thenReturn(user); Family family = new Family(); family.setId(1L); when(familyMapper.selectById(1L)).thenReturn(family); Map result = membershipService.createOrderWithPayType( 1L, 1L, "FAMILY", "trial", null, null, "mockSessionKey", null); assertNotNull(result); assertEquals("none", result.get("payType")); } // --- 道具未配置 --- @Test void createOrderWithPayType_virtualEnabled_configMissing_returnsNull() { when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn("1"); when(sysConfigService.getValue("member_fee_family")).thenReturn("131400"); when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null)); when(paymentOrderMapper.selectCount(any())).thenReturn(0L); when(couponService.apply(anyLong(), anyLong(), anyString(), anyInt())).thenReturn(null); when(paymentOrderMapper.insert(any())).thenReturn(1); when(virtualPayService.generateMembershipPayParams(anyString(), anyString())).thenReturn(null); User user = new User(); user.setFamilyId(1L); when(userMapper.selectById(1L)).thenReturn(user); Map result = membershipService.createOrderWithPayType( 1L, 1L, "FAMILY", "pay", "yearly", null, "mockSessionKey", null); assertNull(result); } // --- 辅助方法 --- private MembershipLevel buildLevel(String code, int yearly, Integer monthly, Integer quarterly) { MembershipLevel level = new MembershipLevel(); level.setLevelCode(code); level.setPriceYearly(yearly); level.setPriceMonthly(monthly); level.setPriceQuarterly(quarterly); return level; } } ``` **步骤 1.5:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile ``` 预期:`BUILD SUCCESS` **步骤 1.6:运行单元测试** ```bash cd /sc-data/cfc/cfc-backend && mvn test -Dtest=MembershipPaySwitchTest -DfailIfNoTests=false ``` 预期:4 tests passed,0 failures **步骤 1.7:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java \ cfc-backend/src/test/java/com/etotem/cfc/service/MembershipPaySwitchTest.java git commit -m "feat(membership): 新增 createOrderWithPayType 开关分流虚拟/普通支付" ``` --- ## 任务 2:MembershipController 改造 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java` **步骤 2.1:改造 createOrder() 方法** 将 `MembershipController.java` 中 `/orders` 端点(L217-L254)替换为: ```java /** * 创建支付订单 */ @Operation(summary = "创建订单") @PostMapping("/orders") public Result createOrder(@RequestBody Map params, @RequestAttribute("userId") Long userId) { if (userId == null) { return Result.error("用户未登录"); } User user = userMapper.selectById(userId); if (user == null) { return Result.error("用户不存在"); } Long familyId = membershipService.getUserFamilyId(userId); String levelCode = params.get("levelCode").toString(); String paymentType = params.get("paymentType").toString(); String period = params.get("period") != null ? params.get("period").toString() : "yearly"; Long couponId = ParamUtils.getLong(params.get("couponId")); String openid = params.get("openid") != null ? params.get("openid").toString() : null; // trial 单:createOrder 内已直接开通(status=paid),无需支付,原样返回 if ("trial".equals(paymentType)) { PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId); return Result.success(order); } // testMode:保持现有模拟逻辑(不触发真实虚拟/普通支付) if (testMode) { PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId); return Result.success(order); } // 调用新方法,按开关分流 Map result = membershipService.createOrderWithPayType( userId, familyId, levelCode, paymentType, period, couponId, user.getSessionKey(), openid); if (result == null) { return Result.error(46001, "该商品暂未开放购买"); } return Result.success(result); } ``` **步骤 2.2:确认 testMode 字段存在** `MembershipController.java` L42 已有: ```java @Value("${wechat.test-mode:false}") private boolean testMode; ``` 无需修改。 **步骤 2.3:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile ``` 预期:`BUILD SUCCESS` **步骤 2.4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java git commit -m "feat(membership): Controller 改为调用 createOrderWithPayType,支持 openid 参数" ``` --- ## 任务 3:PaymentService ORD 回调路由 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java` **步骤 3.1:在 PaymentService 中注入 MembershipService(@Lazy 防循环)** 在 `PaymentService.java` 的 `@Resource` 块中追加: ```java @Lazy @Resource private MembershipService membershipService; ``` **步骤 3.2:修改 handleWechatNotify() 的 ORD 前缀路由** 在 `PaymentService.java` 找到 `handleWechatNotify()` 方法中的路由逻辑(L329-334): ```java // 修改前: if (orderNo != null && orderNo.startsWith("PO")) { productOrderService.handlePaymentSuccess(orderNo, transactionId); } else { handlePaymentCallback(orderNo, transactionId, "wechat"); } ``` 替换为: ```java // 修改后:ORD→会员开通,PO→商城,其余→PackageOrder(原有 fallback) if (orderNo != null && orderNo.startsWith("ORD")) { membershipService.processPaymentCallback(orderNo, transactionId, "wechat"); } else if (orderNo != null && orderNo.startsWith("PO")) { productOrderService.handlePaymentSuccess(orderNo, transactionId); } else { handlePaymentCallback(orderNo, transactionId, "wechat"); } ``` **步骤 3.3:编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile ``` 预期:`BUILD SUCCESS`(`@Lazy` 解决循环依赖) **步骤 3.4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java git commit -m "feat(pay): handleWechatNotify 增加 ORD 前缀路由到会员开通回调" ``` --- ## 任务 4:前端 — api.js 和 pay.vue **文件:** - 修改:`cfc-frontend/utils/api.js` - 修改:`cfc-frontend/pages/membership/pay.vue` **步骤 4.1:修改 api.js 中 createOrder() 透传 openid** 在 `cfc-frontend/utils/api.js` 中找到 `createOrder` 函数(L330): ```js // 修改前: export const createOrder = (levelCode, paymentType, period, couponId) => { var data = { levelCode: levelCode, paymentType: paymentType } if (period) data.period = period if (couponId) data.couponId = couponId return request('/api/membership/orders', 'POST', data) } // 修改后: export const createOrder = (levelCode, paymentType, period, couponId, openid) => { var data = { levelCode: levelCode, paymentType: paymentType } if (period) data.period = period if (couponId) data.couponId = couponId if (openid) data.openid = openid return request('/api/membership/orders', 'POST', data) } ``` **步骤 4.2:修改 pay.vue handlePay() 支持 payType 分流** 在 `cfc-frontend/pages/membership/pay.vue` 的 `handlePay` 方法(L217)中,将现有的 if-else 分支替换为: ```js handlePay: function() { var self = this if (this.paying) return this.paying = true var couponId = this.selectedCoupon ? this.selectedCoupon.id : null var openid = uni.getStorageSync('openid') || '' createOrder(this.levelCode, 'pay', this.period, couponId, openid).then(function(res) { var data = res.data || {} self.orderNo = data.orderNo var payType = data.payType || 'virtual' if (payType === 'virtual' && data.orderInfo && data.sign) { // 虚拟支付(现有逻辑不变) self.totalPeriods = data.totalPeriods || 1 self.currentPeriod = data.currentPeriod || 1 self.splitAmount = data.orderInfo.goodsPrice || (self.finalPrice / (self.totalPeriods || 1)) self.payInstallments(data) } else if (payType === 'wechat' && data.wechatPayParams) { // 普通微信支付(新增分支) self.payWechat(data.wechatPayParams) } else { // testMode/trial:模拟支付成功 self.orderNo = data.orderNo uni.showToast({ title: '支付成功,开通中…', icon: 'none' }) setTimeout(function() { self.paying = false uni.redirectTo({ url: '/pages/membership/result?orderNo=' + (self.orderNo || '') + '&status=success' }) }, 800) } }).catch(function(e) { self.paying = false if (e.code === 46001) { uni.showToast({ title: '该商品暂未开放购买', icon: 'none' }) } else { uni.showToast({ title: e.message || '下单失败', icon: 'none' }) } }) }, ``` **步骤 4.3:新增 payWechat() 方法** 在 `pay.vue` 的 `methods` 中,`finishPay` 方法之前插入: ```js payWechat: function(pp) { var self = this if (!pp || !pp.package) { self.paying = false uni.showToast({ title: '支付参数异常', icon: 'none' }) return } uni.requestPayment({ provider: 'wxpay', timeStamp: pp.timeStamp, nonceStr: pp.nonceStr, package: pp.package, signType: pp.signType || 'RSA', paySign: pp.paySign || pp.sign, success: function() { uni.showToast({ title: '支付成功,开通中…', icon: 'none' }) setTimeout(function() { self.paying = false uni.redirectTo({ url: '/pages/membership/result?orderNo=' + (self.orderNo || '') + '&status=success' }) }, 800) }, fail: function(err) { self.paying = false var errCode = err && err.errCode var msg = '支付取消' if (errCode === -15007 || errCode === -15005) msg = '登录态已过期,请重新登录' else if (err && err.errMsg) msg = err.errMsg uni.showToast({ title: msg, icon: 'none' }) setTimeout(function() { uni.redirectTo({ url: '/pages/membership/result?orderNo=' + (self.orderNo || '') + '&status=fail' }) }, 800) } }) } ``` **步骤 4.4:语法校验** ```bash # api.js 语法检查 node --check /sc-data/cfc/cfc-frontend/utils/api.js && echo "api.js OK" # pay.vue script 块语法检查 sed -n '/