面向 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() 方法 |
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.javacfc-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 起)追加:
@Resource
private PaymentService paymentService;
步骤 1.2:为 createOrder() 拆单逻辑增加 splitEnabled 控制
在 MembershipService.java L295 找到 createOrder() 方法,找到 L369 行:
// 修改前:
if (!"trial".equals(paymentType)) {
// 修改后:
if (!"trial".equals(paymentType)) {
替换为:
// 在 createOrder() 方法开头(L295 之后)增加 splitEnabled=true 默认值
// 原 createOrder() 签名不变(保持向后兼容);拆单条件改为始终执行(默认拆单开启)
if (!"trial".equals(paymentType)) {
说明:
createOrder()现有调用方均为虚拟支付路径,拆单行为不变。新建的createOrderWithPayType()不走createOrder(),而是直接内联订单创建逻辑,避免拆单。
步骤 1.3:新增 createOrderWithPayType() 方法(内联订单创建)
在 MembershipService.java 中 createOrder() 方法之后新增。该方法完整复制 createOrder() 的订单创建逻辑,但增加 splitEnabled 参数(关闭时跳过 L369-L384 的拆单判断):
/**
* 创建会员订单并按 member_pay_virtual_enabled 开关决定支付方式。
*
* @param sessionKey 当前用户 sessionKey(虚拟支付签名用;普通支付时忽略)
* @param openid 普通支付时需要(JSAPI payer.openid);虚拟支付时可为 null
* @return 统一响应 map,含 payType("virtual"/"wechat"/"none")及对应字段;
* 虚拟支付道具未配置时返回 null(调用方转 46001)
*/
public Map<String, Object> 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<MembershipLevel>()
.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<PaymentOrder> 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<String, Object> 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<String, Object> 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:
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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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:编译验证
cd /sc-data/cfc/cfc-backend && mvn clean compile
预期:BUILD SUCCESS
步骤 1.6:运行单元测试
cd /sc-data/cfc/cfc-backend && mvn test -Dtest=MembershipPaySwitchTest -DfailIfNoTests=false
预期:4 tests passed,0 failures
步骤 1.7:Commit
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 开关分流虚拟/普通支付"
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java步骤 2.1:改造 createOrder() 方法
将 MembershipController.java 中 /orders 端点(L217-L254)替换为:
/**
* 创建支付订单
*/
@Operation(summary = "创建订单")
@PostMapping("/orders")
public Result<Object> createOrder(@RequestBody Map<String, Object> 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<String, Object> 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 已有:
@Value("${wechat.test-mode:false}")
private boolean testMode;
无需修改。
步骤 2.3:编译验证
cd /sc-data/cfc/cfc-backend && mvn clean compile
预期:BUILD SUCCESS
步骤 2.4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
git commit -m "feat(membership): Controller 改为调用 createOrderWithPayType,支持 openid 参数"
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java步骤 3.1:在 PaymentService 中注入 MembershipService(@Lazy 防循环)
在 PaymentService.java 的 @Resource 块中追加:
@Lazy
@Resource
private MembershipService membershipService;
步骤 3.2:修改 handleWechatNotify() 的 ORD 前缀路由
在 PaymentService.java 找到 handleWechatNotify() 方法中的路由逻辑(L329-334):
// 修改前:
if (orderNo != null && orderNo.startsWith("PO")) {
productOrderService.handlePaymentSuccess(orderNo, transactionId);
} else {
handlePaymentCallback(orderNo, transactionId, "wechat");
}
替换为:
// 修改后: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:编译验证
cd /sc-data/cfc/cfc-backend && mvn clean compile
预期:BUILD SUCCESS(@Lazy 解决循环依赖)
步骤 3.4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java
git commit -m "feat(pay): handleWechatNotify 增加 ORD 前缀路由到会员开通回调"
文件:
cfc-frontend/utils/api.jscfc-frontend/pages/membership/pay.vue步骤 4.1:修改 api.js 中 createOrder() 透传 openid
在 cfc-frontend/utils/api.js 中找到 createOrder 函数(L330):
// 修改前:
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 分支替换为:
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 方法之前插入:
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:语法校验
# api.js 语法检查
node --check /sc-data/cfc/cfc-frontend/utils/api.js && echo "api.js OK"
# pay.vue script 块语法检查
sed -n '/<script>/,/<\/script>/p' /sc-data/cfc/cfc-frontend/pages/membership/pay.vue | sed '1d;$d' > /tmp/pay-check.js
node --check /tmp/pay-check.js && echo "pay.vue OK"
预期:两行均输出 OK
步骤 4.5:Commit
git add cfc-frontend/utils/api.js cfc-frontend/pages/membership/pay.vue
git commit -m "feat(frontend): pay.vue 支持 payType 分流,新增普通微信支付路径"
步骤 5.1:后端编译
cd /sc-data/cfc/cfc-backend && mvn clean compile
预期:BUILD SUCCESS
步骤 5.2:路由重复检查
grep -rn '@Mapping' /sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
预期:无 /api/membership/orders 重复路径
步骤 5.3:Bean 命名冲突检查
# 确认 PaymentService 和 MembershipService 无循环注入问题
cd /sc-data/cfc/cfc-backend && mvn compile -q 2>&1 | grep -i "circular\|cyclic\|bean.*conflict" || echo "no conflicts"
预期:no conflicts
member_pay_virtual_enabled=1(或省略)→ 接口返回 payType=virtual,含 orderInfo/sign/signaturemember_pay_virtual_enabled=0 → 接口返回 payType=wechat,含 wechatPayParamsgroupNo,totalPeriods=1(单订单全额支付)MembershipService.processPaymentCallback()(幂等,含补开通逻辑)paymentType=trial)返回 payType=none,行为不变mvn clean compile 通过