基于策略模式与模板方法的统一支付实现方案(Java+Vue)
在电商、服务类等系统中,支付功能是核心模块之一,需支持支付宝、微信等多种支付渠道,同时为便于后续扩展新渠道(如银联支付),需保证代码的可维护性与可扩展性。本文采用策略模式封装不同支付渠道的差异逻辑,结合模板方法模式固定支付流程的通用步骤,实现统一支付服务;后端使用Java(SpringBoot)开发,前端采用Vue框架实现交互,完整支持支付宝支付、微信支付及两者的聚合扫码支付功能。
一、整体设计思路
1.1 核心需求
-
提供统一的支付入口,支持支付宝、微信、聚合扫码(支付宝+微信)三种支付方式;
-
支付流程遵循“参数验证→构建支付参数→发起支付→结果处理”的通用逻辑;
-
新增支付渠道时,无需修改现有核心代码,仅需新增对应策略实现;
-
前端可根据业务场景选择不同支付方式,聚合扫码支付支持生成统一二维码供用户选择支付渠道。
1.2 设计模式适配
-
策略模式:定义支付策略接口,将支付宝、微信、聚合扫码支付的差异化逻辑(如参数构建、支付发起)封装为具体策略类,通过策略工厂动态选择对应实现;
-
模板方法模式:抽取支付流程的通用步骤(参数验证、结果处理)作为模板方法,将差异化步骤(构建支付参数、发起支付)定义为抽象方法,由具体策略类实现,确保流程统一性。
1.3 整体架构
系统分为三层架构:
-
前端层(Vue):提供支付方式选择界面、聚合二维码展示、支付结果回调处理;
-
后端服务层(SpringBoot):包含支付策略接口、模板抽象类、具体策略实现、策略工厂、统一支付控制器;
-
第三方支付层:对接支付宝SDK、微信支付SDK,处理支付请求与回调。
二、后端实现(Java+SpringBoot)
2.1 环境准备与依赖配置
在pom.xml中引入支付宝SDK、微信支付SDK及核心依赖:
<!-- 支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.34.0.ALL</version>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.4.7</version>
</dependency>
<!-- SpringBoot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 二维码生成依赖(聚合扫码用) -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
2.2 核心模型定义
2.2.1 支付枚举
定义支付渠道枚举,用于策略工厂选择具体策略:
import lombok.Getter;
@Getter
public enum PayChannelEnum {
ALIPAY("alipay", "支付宝支付"),
WECHAT_PAY("wechat_pay", "微信支付"),
COMBINED_SCAN_PAY("combined_scan_pay", "聚合扫码支付");
private final String code;
private final String name;
PayChannelEnum(String code, String name) {
this.code = code;
this.name = name;
}
// 根据编码获取枚举
public static PayChannelEnum getByCode(String code) {
for (PayChannelEnum channel : values()) {
if (channel.getCode().equals(code)) {
return channel;
}
}
throw new IllegalArgumentException("不支持的支付渠道:" + code);
}
}
2.2.2 支付请求与响应模型
统一支付请求参数与响应结果格式:
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
// 支付请求参数
@Data
public class PayRequest {
// 支付渠道编码(对应PayChannelEnum)
private String payChannelCode;
// 订单号
private String orderNo;
// 订单金额(单位:元)
private BigDecimal amount;
// 订单描述
private String description;
// 客户端IP
private String clientIp;
// 支付结果通知地址
private String notifyUrl;
// 支付超时时间(默认30分钟)
private LocalDateTime expireTime = LocalDateTime.now().plusMinutes(30);
}
// 支付响应结果
@Data
@Builder
public class PayResponse {
// 是否成功
private Boolean success;
// 错误码
private String errorCode;
// 错误信息
private String errorMsg;
// 支付参数(前端调起支付所需,如支付宝签名信息、微信prepayId)
private String payParams;
// 二维码链接(扫码支付用)
private String codeUrl;
// 预支付会话ID(微信APP支付用)
private String prepayId;
// 成功响应静态方法
public static PayResponse success(String payParams, String codeUrl, String prepayId) {
return PayResponse.builder()
.success(true)
.payParams(payParams)
.codeUrl(codeUrl)
.prepayId(prepayId)
.build();
}
// 失败响应静态方法
public static PayResponse fail(String errorCode, String errorMsg) {
return PayResponse.builder()
.success(false)
.errorCode(errorCode)
.errorMsg(errorMsg)
.build();
}
}
2.3 策略模式与模板方法核心实现
2.3.1 支付策略接口
定义支付核心方法,由具体策略实现:
public interface PaymentStrategy {
// 执行支付
PayResponse executePay(PayRequest request);
}
2.3.2 支付模板抽象类(模板方法模式核心)
抽取支付通用流程,固定模板方法,抽象差异化步骤:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public abstract class AbstractPaymentTemplate implements PaymentStrategy {
// 模板方法:固定支付流程,不可重写
@Override
public final PayResponse executePay(PayRequest request) {
try {
// 1. 通用步骤:验证支付请求参数
validateRequest(request);
// 2. 差异化步骤:构建支付参数(由具体策略实现)
String payParams = buildPayParams(request);
// 3. 差异化步骤:发起支付(由具体策略实现)
PayResponse payResponse = doPay(request, payParams);
// 4. 通用步骤:记录支付日志
recordPayLog(request, payResponse);
return payResponse;
} catch (IllegalArgumentException e) {
log.error("支付请求参数验证失败:{}", e.getMessage());
return PayResponse.fail("PARAM_ERROR", e.getMessage());
} catch (Exception e) {
log.error("支付执行失败:{}", e.getMessage(), e);
return PayResponse.fail("PAY_ERROR", "支付服务异常,请稍后重试");
}
}
// 通用步骤1:验证支付请求参数
private void validateRequest(PayRequest request) {
if (request == null) {
throw new IllegalArgumentException("支付请求参数不能为空");
}
if (request.getOrderNo() == null || request.getOrderNo().isEmpty()) {
throw new IllegalArgumentException("订单号不能为空");
}
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
if (request.getNotifyUrl() == null || request.getNotifyUrl().isEmpty()) {
throw new IllegalArgumentException("支付结果通知地址不能为空");
}
}
// 通用步骤4:记录支付日志(可根据业务扩展,如存入数据库)
private void recordPayLog(PayRequest request, PayResponse response) {
log.info("支付日志记录:订单号={}, 支付渠道={}, 支付结果={}, 错误信息={}",
request.getOrderNo(), request.getPayChannelCode(),
response.getSuccess() ? "成功" : "失败",
response.getSuccess() ? "-" : response.getErrorMsg());
}
// 抽象方法1:构建支付参数(差异化步骤,由具体策略实现)
protected abstract String buildPayParams(PayRequest request) throws Exception;
// 抽象方法2:发起支付(差异化步骤,由具体策略实现)
protected abstract PayResponse doPay(PayRequest request, String payParams) throws Exception;
}
2.3.3 具体支付策略实现
2.3.3.1 支付宝支付策略
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("alipayStrategy")
public class AlipayStrategy extends AbstractPaymentTemplate {
// 支付宝配置(从配置文件读取)
@Value("${alipay.gateway-url}")
private String gatewayUrl;
@Value("${alipay.app-id}")
private String appId;
@Value("${alipay.private-key}")
private String privateKey;
@Value("${alipay.public-key}")
private String publicKey;
@Override
protected String buildPayParams(PayRequest request) throws Exception {
// 构建支付宝支付请求参数(以电脑网站支付为例,扫码支付类似)
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl("http://localhost:8080/pay/return"); // 同步回调地址
alipayRequest.setNotifyUrl(request.getNotifyUrl()); // 异步回调地址
// 封装业务参数
String bizContent = String.format("{\"out_trade_no\":\"%s\",\"total_amount\":\"%s\",\"subject\":\"%s\",\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}",
request.getOrderNo(), request.getAmount(), request.getDescription());
alipayRequest.setBizContent(bizContent);
return alipayRequest.getBizContent();
}
@Override
protected PayResponse doPay(PayRequest request, String payParams) throws Exception {
// 初始化支付宝客户端
AlipayClient alipayClient = new DefaultAlipayClient(
gatewayUrl, appId, privateKey, "json", "UTF-8", publicKey, "RSA2");
// 发起电脑网站支付请求
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl("http://localhost:8080/pay/return");
alipayRequest.setNotifyUrl(request.getNotifyUrl());
alipayRequest.setBizContent(payParams);
AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest);
if (response.isSuccess()) {
// 返回支付宝支付页面HTML(前端直接渲染即可调起支付)
return PayResponse.success(response.getBody(), null, null);
} else {
throw new Exception("支付宝支付发起失败:" + response.getMsg());
}
}
}
2.3.3.2 微信支付策略
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.math.BigDecimal;
@Component("wechatPayStrategy")
public class WechatPayStrategy extends AbstractPaymentTemplate {
// 微信支付配置(从配置文件读取)
@Value("${wechatpay.mch-id}")
private String mchId;
@Value("${wechatpay.mch-private-key-path}")
private String mchPrivateKeyPath;
@Value("${wechatpay.wechatpay-certificate-path}")
private String wechatpayCertificatePath;
@Value("${wechatpay.app-id}")
private String appId;
private NativePayService nativePayService;
// 初始化微信支付服务
@PostConstruct
public void init() {
RSAAutoCertificateConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(mchPrivateKeyPath)
.merchantCertificateFromPath(wechatpayCertificatePath)
.wechatpayCertificateFromPath(wechatpayCertificatePath)
.build();
nativePayService = new NativePayService.Builder().config(config).build();
}
@Override
protected String buildPayParams(PayRequest request) throws Exception {
// 构建微信Native支付请求参数
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(appId);
prepayRequest.setMchid(mchId);
prepayRequest.setDescription(request.getDescription());
prepayRequest.setOutTradeNo(request.getOrderNo());
prepayRequest.setNotifyUrl(request.getNotifyUrl());
Amount amount = new Amount();
// 微信支付金额单位为分,需转换
amount.setTotal(request.getAmount().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
prepayRequest.setAttach("clientIp=" + request.getClientIp());
return prepayRequest.toString();
}
@Override
protected PayResponse doPay(PayRequest request, String payParams) throws Exception {
// 发起微信Native支付(生成支付二维码)
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(appId);
prepayRequest.setMchid(mchId);
prepayRequest.setDescription(request.getDescription());
prepayRequest.setOutTradeNo(request.getOrderNo());
prepayRequest.setNotifyUrl(request.getNotifyUrl());
Amount amount = new Amount();
amount.setTotal(request.getAmount().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
PrepayResponse response = nativePayService.prepay(prepayRequest);
// 返回微信支付二维码链接(前端生成二维码)
return PayResponse.success(null, response.getCodeUrl(), null);
}
}
2.3.3.3 聚合扫码支付策略
聚合扫码支付核心逻辑:生成统一订单标识,关联支付宝、微信支付二维码,用户扫码后展示渠道选择界面,选择后跳转到对应支付页面。
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Component("combinedScanPayStrategy")
public class CombinedScanPayStrategy extends AbstractPaymentTemplate {
// 聚合支付页面地址(前端页面,用于展示支付宝/微信选择按钮)
@Value("${combined-pay.scan-page-url}")
private String scanPageUrl;
@Autowired
private AlipayStrategy alipayStrategy;
@Autowired
private WechatPayStrategy wechatPayStrategy;
// 用于缓存聚合支付订单与对应支付渠道参数
private Map<String, Map<String, String>> combinedPayCache = new HashMap<>();
@Override
protected String buildPayParams(PayRequest request) throws Exception {
// 1. 生成聚合支付订单唯一标识(可使用订单号)
String combinedOrderNo = request.getOrderNo();
// 2. 预生成支付宝、微信支付参数(缓存起来,用户选择后直接使用)
String alipayCodeUrl = getAlipayScanCodeUrl(request);
String wechatCodeUrl = getWechatScanCodeUrl(request);
// 3. 缓存聚合订单与支付渠道参数
Map<String, String> channelParams = new HashMap<>();
channelParams.put("alipayCodeUrl", alipayCodeUrl);
channelParams.put("wechatCodeUrl", wechatCodeUrl);
combinedPayCache.put(combinedOrderNo, channelParams);
// 4. 构建聚合扫码链接(拼接聚合支付页面地址与订单号)
return scanPageUrl + "?orderNo=" + combinedOrderNo;
}
@Override
protected PayResponse doPay(PayRequest request, String payParams) throws Exception {
// 生成聚合支付二维码(内容为聚合支付页面地址)
String combinedQrCodeBase64 = generateQrCodeBase64(payParams, 300, 300);
// 返回聚合支付二维码Base64字符串(前端直接渲染)
return PayResponse.success(null, combinedQrCodeBase64, null);
}
// 获取支付宝扫码支付二维码链接
private String getAlipayScanCodeUrl(PayRequest request) throws Exception {
String payParams = alipayStrategy.buildPayParams(request);
PayResponse payResponse = alipayStrategy.doPay(request, payParams);
return payResponse.getCodeUrl();
}
// 获取微信扫码支付二维码链接
private String getWechatScanCodeUrl(PayRequest request) throws Exception {
String payParams = wechatPayStrategy.buildPayParams(request);
PayResponse payResponse = wechatPayStrategy.doPay(request, payParams);
return payResponse.getCodeUrl();
}
// 生成二维码Base64字符串(前端直接渲染)
private String generateQrCodeBase64(String content, int width, int height) throws WriterException, IOException {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.MARGIN, 1);
MultiFormatWriter writer = new MultiFormatWriter();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(
writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints),
"PNG",
outputStream
);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
// 供前端调用,根据选择的渠道获取对应支付二维码
public PayResponse getChannelPayCode(String orderNo, String channelCode) {
Map<String, String> channelParams = combinedPayCache.get(orderNo);
if (channelParams == null) {
return PayResponse.fail("ORDER_NOT_FOUND", "聚合支付订单不存在或已过期");
}
String codeUrl = channelParams.get(channelCode + "CodeUrl");
if (codeUrl == null) {
return PayResponse.fail("CHANNEL_NOT_SUPPORT", "不支持的支付渠道");
}
return PayResponse.success(null, codeUrl, null);
}
}
2.3.4 策略工厂类
根据支付渠道编码动态获取对应策略实现:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class PaymentStrategyFactory {
// Spring自动注入所有实现PaymentStrategy接口的Bean,key为Bean名称
@Autowired
private Map<String, PaymentStrategy> strategyMap;
// 根据支付渠道编码获取策略
public PaymentStrategy getStrategy(String payChannelCode) {
PayChannelEnum channelEnum = PayChannelEnum.getByCode(payChannelCode);
String strategyBeanName = "";
switch (channelEnum) {
case ALIPAY:
strategyBeanName = "alipayStrategy";
break;
case WECHAT_PAY:
strategyBeanName = "wechatPayStrategy";
break;
case COMBINED_SCAN_PAY:
strategyBeanName = "combinedScanPayStrategy";
break;
default:
throw new IllegalArgumentException("不支持的支付渠道:" + payChannelCode);
}
PaymentStrategy strategy = strategyMap.get(strategyBeanName);
if (strategy == null) {
throw new RuntimeException("支付策略未实现:" + strategyBeanName);
}
return strategy;
}
}
2.4 统一支付控制器
提供统一支付接口,供前端调用:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/pay")
public class PaymentController {
@Autowired
private PaymentStrategyFactory strategyFactory;
@Autowired
private CombinedScanPayStrategy combinedScanPayStrategy;
// 统一支付入口
@PostMapping("/execute")
public PayResponse executePay(@RequestBody PayRequest request) {
PaymentStrategy strategy = strategyFactory.getStrategy(request.getPayChannelCode());
return strategy.executePay(request);
}
// 聚合扫码支付:根据选择的渠道获取对应支付二维码
@GetMapping("/combined/channel")
public PayResponse getCombinedChannelPayCode(@RequestParam String orderNo, @RequestParam String channelCode) {
return combinedScanPayStrategy.getChannelPayCode(orderNo, channelCode);
}
// 支付结果异步回调(支付宝/微信回调此接口)
@PostMapping("/notify/{payChannelCode}")
public String payNotify(@PathVariable String payChannelCode, @RequestBody String notifyData) {
// 不同支付渠道的回调处理逻辑(验证签名、更新订单状态等)
// 此处省略实现,可参考各支付SDK的回调处理文档
return "success";
}
}
三、前端实现(Vue)
3.1 环境准备与依赖
安装核心依赖:
# 安装axios(请求后端接口)
npm install axios --save
# 安装vue-qrcode(生成二维码)
npm install vue-qrcode --save
# 安装element-plus(UI组件库,可选)
npm install element-plus --save
3.2 核心组件实现
3.2.1 支付方式选择组件(PaySelect.vue)
请使用微信扫描二维码支付请使用手机扫描二维码,选择支付方式<div v-else 支付发起失败:{{ payResponse.errorMsg }}(错误码:{{ payResponse.errorCode }})
3.2.2 聚合扫码支付选择页面(CombinedScanPage.vue)
用户扫描聚合二维码后跳转至此页面,选择支付渠道:
<el-button type="primary" size="large" @click="selectChannel('alipay')">
支付宝支付
</el-button>
<el-button type="primary" size="large" @click="selectChannel('wechat')">
微信支付
</el-button>
<qrcode :value="channelCodeUrl" size="300" />
请使用{{ selectedChannelName }}扫描二维码支付
3.3 路由配置(router/index.js)
import { createRouter, createWebHistory } from 'vue-router';
import PaySelect from '../components/PaySelect.vue';
import CombinedScanPage from '../components/CombinedScanPage.vue';
const routes = [
{
path: '/',
name: 'PaySelect',
component: PaySelect
},
{
path: '/combined/scan',
name: 'CombinedScanPage',
component: CombinedScanPage
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
四、功能测试与扩展说明
4.1 功能测试
-
支付宝支付:选择支付宝支付,点击“确认支付”,页面渲染支付宝支付表单,提交后跳转到支付宝支付页面,支付完成后回调更新订单状态;
-
微信支付:选择微信支付,生成微信支付二维码,使用微信扫描后完成支付,回调接口处理支付结果;
-
聚合扫码支付:选择聚合扫码支付,生成聚合二维码,扫描后跳转至渠道选择页面,选择对应渠道后生成该渠道支付二维码,完成支付。
4.2 扩展说明
新增支付渠道(如银联支付)时,仅需执行以下步骤:
-
在
PayChannelEnum中新增银联支付枚举项; -
创建
UnionPayStrategy类,继承AbstractPaymentTemplate,实现buildPayParams和doPay方法; -
在策略工厂
PaymentStrategyFactory中添加银联支付策略的映射逻辑; -
前端支付方式选择组件中新增银联支付选项,无需修改核心支付逻辑。
4.3 注意事项
-
支付配置信息(如appId、密钥等)需放在配置文件中,生产环境建议加密存储,避免硬编码;
-
支付回调接口需进行签名验证,防止恶意请求篡改支付结果;
-
聚合支付订单缓存建议使用Redis等分布式缓存,支持集群部署;
-
需添加支付超时处理逻辑,超时后关闭订单,释放资源。
五、总结
本文通过策略模式封装不同支付渠道的差异化逻辑,结合模板方法模式固定支付流程的通用步骤,实现了可扩展、易维护的统一支付服务。后端基于Java SpringBoot开发,支持支付宝、微信及聚合扫码支付;前端通过Vue框架实现友好的支付交互界面。该方案遵循开闭原则,新增支付渠道时无需修改现有核心代码,大幅提升了代码的可维护性与扩展性,适用于各类需要多支付渠道的业务系统。
(注:文档部分内容可能由 AI 生成)
934

被折叠的 条评论
为什么被折叠?



