设计模式实战:策略模式+模板方法+工厂模式打造可扩展支付系统
本文通过一个真实的支付系统案例,深入讲解如何将策略模式、模板方法模式、工厂模式三者结合,构建一个高扩展性、易维护的多渠道支付系统。
一、背景与需求分析
1.1 业务场景
在电商系统中,支付模块是核心功能之一。随着业务发展,需要接入多种支付渠道:
- 支付宝:国内主流支付方式
- 微信支付:社交场景支付首选
- 银联支付:传统银行卡支付
- 京东钱包:京东生态内支付
1.2 面临的挑战
+------------------+ +------------------+ +------------------+
| 支付宝支付 | | 微信支付 | | 银联支付 |
| - 签名方式不同 | | - 签名方式不同 | | - 签名方式不同 |
| - 请求参数不同 | | - 请求参数不同 | | - 请求参数不同 |
| - 回调处理不同 | | - 回调处理不同 | | - 回调处理不同 |
+------------------+ +------------------+ +------------------+
| | |
+-----------------------------------------------+
|
+------------------------+
| 如何优雅地扩展? |
| 如何减少重复代码? |
| 如何统一管理? |
+------------------------+
1.3 设计目标
- 开闭原则:新增支付渠道时,不修改现有代码
- 代码复用:抽取公共流程,减少重复代码
- 易于维护:清晰的代码结构,便于定位问题
- 灵活扩展:支持运行时动态切换支付渠道
二、设计模式简介
2.1 策略模式 (Strategy Pattern)
+-------------------+ +----------------------+
| Context | | <<interface>> |
|-------------------| | Strategy |
| - strategy |-------->|----------------------|
| + setStrategy() | | + execute() |
| + executeStrategy | +----------------------+
+-------------------+ ^
|
+-------------------------+-------------------------+
| | |
+------------------+ +------------------+ +------------------+
| ConcreteStrategyA| | ConcreteStrategyB| | ConcreteStrategyC|
|------------------| |------------------| |------------------|
| + execute() | | + execute() | | + execute() |
+------------------+ +------------------+ +------------------+
核心思想:定义一系列算法,将每个算法封装起来,使它们可以互相替换。
2.2 模板方法模式 (Template Method Pattern)
+--------------------------------+
| AbstractClass |
|--------------------------------|
| + templateMethod() { |
| step1(); |
| step2(); // 抽象方法 |
| step3(); // 钩子方法 |
| } |
| # step1() { ... } |
| # abstract step2(); |
| # step3() { ... } // 可选覆盖 |
+--------------------------------+
^
|
+---------+---------+
| |
+----------+ +----------+
| SubClassA| | SubClassB|
|----------| |----------|
| # step2()| | # step2()|
| # step3()| +----------+
+----------+
核心思想:定义算法骨架,将某些步骤延迟到子类实现。
2.3 工厂模式 (Factory Pattern)
+------------------+ +----------------------+
| Client | | <<interface>> |
|------------------| | Product |
| + main() |-------->|----------------------|
+------------------+ | + operation() |
| +----------------------+
| ^
v |
+------------------+ +---------+---------+
| Factory | | |
|------------------| +----------+ +----------+
| + create(type) |--->| ProductA | | ProductB |
+------------------+ +----------+ +----------+
核心思想:将对象的创建与使用分离,通过工厂统一管理对象创建。
三、架构设计
3.1 整体架构
+------------------------------------------------------------------+
| 支付系统架构 |
+------------------------------------------------------------------+
| |
| +-------------------+ +-------------------+ |
| | Controller层 |--->| Service层 | |
| +-------------------+ +-------------------+ |
| | |
| v |
| +----------------------------------------------------------+ |
| | PaymentContext (上下文) | |
| | +----------------------------------------------------+ | |
| | | PaymentStrategyFactory (工厂) | | |
| | +----------------------------------------------------+ | |
| | | | |
| | v | |
| | +----------------------------------------------------+ | |
| | | AbstractPaymentStrategy (模板方法) | | |
| | | +----------------------------------------------+ | | |
| | | | 1. validateParams() - 参数校验 | | | |
| | | | 2. buildRequest() - 构建请求 (抽象) | | | |
| | | | 3. sign() - 签名 (抽象) | | | |
| | | | 4. doRequest() - 发送请求 | | | |
| | | | 5. parseResponse() - 解析响应 (抽象) | | | |
| | | | 6. afterPay() - 后置处理 (钩子) | | | |
| | | +----------------------------------------------+ | | |
| | +----------------------------------------------------+ | |
| | ^ | |
| | +--------------------+--------------------+ | |
| | | | | | | |
| | +-------+ +--------+ +----------+ +--------+ | |
| | |支付宝 | | 微信 | | 银联 | | 京东 | | |
| | +-------+ +--------+ +----------+ +--------+ | |
| +----------------------------------------------------------+ |
+------------------------------------------------------------------+
3.2 项目模块结构
java-pay/
├── pom.xml # 父POM
├── pay-common/ # 公共模块
│ ├── pom.xml
│ └── src/main/java/
│ └── com/pay/common/
│ ├── enums/ # 枚举类
│ ├── exception/ # 异常类
│ └── utils/ # 工具类
├── pay-core/ # 核心模块
│ ├── pom.xml
│ └── src/main/java/
│ └── com/pay/core/
│ ├── context/ # 上下文
│ ├── factory/ # 工厂
│ ├── strategy/ # 策略接口与抽象类
│ └── model/ # 数据模型
├── pay-channel-alipay/ # 支付宝渠道
├── pay-channel-wechat/ # 微信渠道
├── pay-channel-unionpay/ # 银联渠道
├── pay-channel-jdpay/ # 京东渠道
└── pay-spring-boot-starter/ # Spring Boot Starter
四、代码实现
4.1 公共模块 (pay-common)
支付渠道枚举
package com.pay.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付渠道枚举
*/
@Getter
@AllArgsConstructor
public enum PayChannelEnum {
ALIPAY("alipay", "支付宝"),
WECHAT("wechat", "微信支付"),
UNIONPAY("unionpay", "银联支付"),
JDPAY("jdpay", "京东钱包");
private final String code;
private final String desc;
public static PayChannelEnum getByCode(String code) {
for (PayChannelEnum channel : values()) {
if (channel.getCode().equals(code)) {
return channel;
}
}
throw new IllegalArgumentException("Unknown pay channel: " + code);
}
}
支付状态枚举
package com.pay.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付状态枚举
*/
@Getter
@AllArgsConstructor
public enum PayStatusEnum {
WAITING("WAITING", "待支付"),
SUCCESS("SUCCESS", "支付成功"),
FAILED("FAILED", "支付失败"),
CLOSED("CLOSED", "已关闭"),
REFUND("REFUND", "已退款");
private final String code;
private final String desc;
}
统一异常类
package com.pay.common.exception;
import lombok.Getter;
/**
* 支付异常
*/
@Getter
public class PayException extends RuntimeException {
private final String code;
private final String message;
public PayException(String code, String message) {
super(message);
this.code = code;
this.message = message;
}
public PayException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
}
4.2 核心模块 (pay-core)
支付请求基类
package com.pay.core.model;
import lombok.Data;
import java.math.BigDecimal;
/**
* 支付请求基类
*/
@Data
public class PayRequest {
/** 商户订单号 */
private String outTradeNo;
/** 支付金额 */
private BigDecimal amount;
/** 商品描述 */
private String subject;
/** 商品详情 */
private String body;
/** 回调地址 */
private String notifyUrl;
/** 返回地址 */
private String returnUrl;
/** 客户端IP */
private String clientIp;
/** 扩展参数 */
private Map<String, Object> extParams;
}
支付响应基类
package com.pay.core.model;
import com.pay.common.enums.PayStatusEnum;
import lombok.Data;
/**
* 支付响应基类
*/
@Data
public class PayResponse {
/** 是否成功 */
private boolean success;
/** 错误码 */
private String code;
/** 错误信息 */
private String message;
/** 商户订单号 */
private String outTradeNo;
/** 渠道交易号 */
private String tradeNo;
/** 支付状态 */
private PayStatusEnum status;
/** 支付链接/二维码内容 */
private String payUrl;
/** 原始响应 */
private String rawResponse;
public static PayResponse success(String outTradeNo, String tradeNo) {
PayResponse response = new PayResponse();
response.setSuccess(true);
response.setOutTradeNo(outTradeNo);
response.setTradeNo(tradeNo);
response.setStatus(PayStatusEnum.SUCCESS);
return response;
}
public static PayResponse fail(String code, String message) {
PayResponse response = new PayResponse();
response.setSuccess(false);
response.setCode(code);
response.setMessage(message);
response.setStatus(PayStatusEnum.FAILED);
return response;
}
}
支付策略接口 (策略模式)
package com.pay.core.strategy;
import com.pay.common.enums.PayChannelEnum;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
/**
* 支付策略接口 - 策略模式核心
*/
public interface PaymentStrategy {
/**
* 获取支付渠道
*/
PayChannelEnum getChannel();
/**
* 执行支付
*/
PayResponse pay(PayRequest request);
/**
* 查询支付结果
*/
PayResponse query(String outTradeNo);
/**
* 关闭订单
*/
PayResponse close(String outTradeNo);
/**
* 退款
*/
PayResponse refund(String outTradeNo, BigDecimal refundAmount);
}
抽象支付策略 (模板方法模式)
package com.pay.core.strategy;
import com.pay.common.exception.PayException;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import lombok.extern.slf4j.Slf4j;
/**
* 抽象支付策略 - 模板方法模式核心
* 定义支付流程骨架,具体步骤由子类实现
*/
@Slf4j
public abstract class AbstractPaymentStrategy implements PaymentStrategy {
/**
* 模板方法 - 定义支付流程
*/
@Override
public final PayResponse pay(PayRequest request) {
try {
// Step 1: 参数校验
validateParams(request);
log.info("[{}] 参数校验通过, outTradeNo={}", getChannel(), request.getOutTradeNo());
// Step 2: 构建请求参数 (抽象方法)
Map<String, String> params = buildRequestParams(request);
log.info("[{}] 构建请求参数完成", getChannel());
// Step 3: 签名 (抽象方法)
String sign = sign(params);
params.put("sign", sign);
log.info("[{}] 签名完成", getChannel());
// Step 4: 发送请求
String response = doRequest(params);
log.info("[{}] 请求发送完成", getChannel());
// Step 5: 解析响应 (抽象方法)
PayResponse payResponse = parseResponse(response);
payResponse.setOutTradeNo(request.getOutTradeNo());
// Step 6: 后置处理 (钩子方法)
afterPay(request, payResponse);
return payResponse;
} catch (PayException e) {
log.error("[{}] 支付异常: {}", getChannel(), e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("[{}] 支付系统异常", getChannel(), e);
throw new PayException("SYSTEM_ERROR", "系统异常", e);
}
}
/**
* 参数校验 - 通用实现,子类可覆盖
*/
protected void validateParams(PayRequest request) {
if (request == null) {
throw new PayException("PARAM_ERROR", "请求参数不能为空");
}
if (StringUtils.isBlank(request.getOutTradeNo())) {
throw new PayException("PARAM_ERROR", "商户订单号不能为空");
}
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new PayException("PARAM_ERROR", "支付金额必须大于0");
}
}
/**
* 构建请求参数 - 抽象方法,子类必须实现
*/
protected abstract Map<String, String> buildRequestParams(PayRequest request);
/**
* 签名 - 抽象方法,子类必须实现
*/
protected abstract String sign(Map<String, String> params);
/**
* 发送请求 - 通用实现
*/
protected String doRequest(Map<String, String> params) {
// 这里使用HTTP客户端发送请求
// 实际项目中可以使用OkHttp、HttpClient等
return HttpUtils.post(getPayUrl(), params);
}
/**
* 获取支付网关地址 - 抽象方法
*/
protected abstract String getPayUrl();
/**
* 解析响应 - 抽象方法,子类必须实现
*/
protected abstract PayResponse parseResponse(String response);
/**
* 后置处理 - 钩子方法,子类可选覆盖
*/
protected void afterPay(PayRequest request, PayResponse response) {
// 默认空实现,子类可以覆盖实现日志记录、消息通知等
log.info("[{}] 支付完成, outTradeNo={}, success={}",
getChannel(), request.getOutTradeNo(), response.isSuccess());
}
}
支付策略工厂 (工厂模式)
package com.pay.core.factory;
import com.pay.common.enums.PayChannelEnum;
import com.pay.common.exception.PayException;
import com.pay.core.strategy.PaymentStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 支付策略工厂 - 工厂模式核心
* 负责创建和管理所有支付策略实例
*/
@Slf4j
@Component
public class PaymentStrategyFactory {
/** 策略映射表 */
private final Map<PayChannelEnum, PaymentStrategy> strategyMap = new HashMap<>();
/** 自动注入所有PaymentStrategy实现类 */
@Autowired
private List<PaymentStrategy> strategies;
/**
* 初始化策略映射
*/
@PostConstruct
public void init() {
for (PaymentStrategy strategy : strategies) {
strategyMap.put(strategy.getChannel(), strategy);
log.info("注册支付策略: {}", strategy.getChannel());
}
log.info("支付策略工厂初始化完成, 共注册{}个策略", strategyMap.size());
}
/**
* 获取支付策略
*/
public PaymentStrategy getStrategy(PayChannelEnum channel) {
PaymentStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new PayException("CHANNEL_NOT_SUPPORT",
"不支持的支付渠道: " + channel.getDesc());
}
return strategy;
}
/**
* 根据渠道编码获取策略
*/
public PaymentStrategy getStrategy(String channelCode) {
return getStrategy(PayChannelEnum.getByCode(channelCode));
}
/**
* 获取所有支持的渠道
*/
public List<PayChannelEnum> getSupportedChannels() {
return new ArrayList<>(strategyMap.keySet());
}
}
支付上下文 (Context)
package com.pay.core.context;
import com.pay.common.enums.PayChannelEnum;
import com.pay.core.factory.PaymentStrategyFactory;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import com.pay.core.strategy.PaymentStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 支付上下文 - 策略模式的上下文角色
* 对外提供统一的支付接口
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentContext {
private final PaymentStrategyFactory strategyFactory;
/**
* 执行支付
*/
public PayResponse pay(PayChannelEnum channel, PayRequest request) {
log.info("开始支付, channel={}, outTradeNo={}", channel, request.getOutTradeNo());
PaymentStrategy strategy = strategyFactory.getStrategy(channel);
return strategy.pay(request);
}
/**
* 查询订单
*/
public PayResponse query(PayChannelEnum channel, String outTradeNo) {
log.info("查询订单, channel={}, outTradeNo={}", channel, outTradeNo);
PaymentStrategy strategy = strategyFactory.getStrategy(channel);
return strategy.query(outTradeNo);
}
/**
* 关闭订单
*/
public PayResponse close(PayChannelEnum channel, String outTradeNo) {
log.info("关闭订单, channel={}, outTradeNo={}", channel, outTradeNo);
PaymentStrategy strategy = strategyFactory.getStrategy(channel);
return strategy.close(outTradeNo);
}
/**
* 退款
*/
public PayResponse refund(PayChannelEnum channel, String outTradeNo, BigDecimal amount) {
log.info("申请退款, channel={}, outTradeNo={}, amount={}", channel, outTradeNo, amount);
PaymentStrategy strategy = strategyFactory.getStrategy(channel);
return strategy.refund(outTradeNo, amount);
}
}
4.3 支付宝渠道实现 (pay-channel-alipay)
package com.pay.channel.alipay;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.pay.common.enums.PayChannelEnum;
import com.pay.common.enums.PayStatusEnum;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import com.pay.core.strategy.AbstractPaymentStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* 支付宝支付策略实现
*/
@Slf4j
@Component
public class AlipayStrategy extends AbstractPaymentStrategy {
@Value("${pay.alipay.app-id}")
private String appId;
@Value("${pay.alipay.private-key}")
private String privateKey;
@Value("${pay.alipay.gateway-url:https://openapi.alipay.com/gateway.do}")
private String gatewayUrl;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.ALIPAY;
}
@Override
protected Map<String, String> buildRequestParams(PayRequest request) {
// 公共参数
Map<String, String> params = new HashMap<>();
params.put("app_id", appId);
params.put("method", "alipay.trade.page.pay");
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
params.put("version", "1.0");
params.put("notify_url", request.getNotifyUrl());
params.put("return_url", request.getReturnUrl());
// 业务参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", request.getOutTradeNo());
bizContent.put("total_amount", request.getAmount().toString());
bizContent.put("subject", request.getSubject());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
params.put("biz_content", bizContent.toJSONString());
return params;
}
@Override
protected String sign(Map<String, String> params) {
// 按字母顺序排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 构建待签名字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (StringUtils.isNotBlank(entry.getValue()) && !"sign".equals(entry.getKey())) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
String signContent = sb.substring(0, sb.length() - 1);
// RSA2签名 (SHA256WithRSA)
return AlipaySignature.rsaSign(signContent, privateKey, "utf-8", "RSA2");
}
@Override
protected String getPayUrl() {
return gatewayUrl;
}
@Override
protected PayResponse parseResponse(String response) {
JSONObject json = JSON.parseObject(response);
JSONObject result = json.getJSONObject("alipay_trade_page_pay_response");
if ("10000".equals(result.getString("code"))) {
PayResponse payResponse = new PayResponse();
payResponse.setSuccess(true);
payResponse.setTradeNo(result.getString("trade_no"));
payResponse.setStatus(PayStatusEnum.WAITING);
payResponse.setRawResponse(response);
return payResponse;
} else {
return PayResponse.fail(
result.getString("code"),
result.getString("msg")
);
}
}
@Override
public PayResponse query(String outTradeNo) {
// 查询接口实现...
log.info("[支付宝] 查询订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse close(String outTradeNo) {
// 关闭订单实现...
log.info("[支付宝] 关闭订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse refund(String outTradeNo, BigDecimal refundAmount) {
// 退款接口实现...
log.info("[支付宝] 退款: {}, amount={}", outTradeNo, refundAmount);
return PayResponse.success(outTradeNo, null);
}
}
4.4 微信支付渠道实现 (pay-channel-wechat)
package com.pay.channel.wechat;
import com.pay.common.enums.PayChannelEnum;
import com.pay.common.enums.PayStatusEnum;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import com.pay.core.strategy.AbstractPaymentStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 微信支付策略实现
*/
@Slf4j
@Component
public class WechatPayStrategy extends AbstractPaymentStrategy {
@Value("${pay.wechat.app-id}")
private String appId;
@Value("${pay.wechat.mch-id}")
private String mchId;
@Value("${pay.wechat.api-key}")
private String apiKey;
@Value("${pay.wechat.gateway-url:https://api.mch.weixin.qq.com/pay/unifiedorder}")
private String gatewayUrl;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.WECHAT;
}
@Override
protected Map<String, String> buildRequestParams(PayRequest request) {
Map<String, String> params = new HashMap<>();
params.put("appid", appId);
params.put("mch_id", mchId);
params.put("nonce_str", UUID.randomUUID().toString().replace("-", ""));
params.put("body", request.getSubject());
params.put("out_trade_no", request.getOutTradeNo());
// 微信金额单位为分
params.put("total_fee", request.getAmount()
.multiply(new BigDecimal("100")).intValue() + "");
params.put("spbill_create_ip", request.getClientIp());
params.put("notify_url", request.getNotifyUrl());
params.put("trade_type", "NATIVE"); // 扫码支付
params.put("sign_type", "MD5");
return params;
}
@Override
protected String sign(Map<String, String> params) {
// 微信签名: MD5(key1=value1&key2=value2&key=API_KEY)
TreeMap<String, String> sortedParams = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (StringUtils.isNotBlank(entry.getValue()) && !"sign".equals(entry.getKey())) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
sb.append("key=").append(apiKey);
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
}
@Override
protected String getPayUrl() {
return gatewayUrl;
}
@Override
protected PayResponse parseResponse(String response) {
// 解析XML响应
Map<String, String> resultMap = XmlUtils.xmlToMap(response);
if ("SUCCESS".equals(resultMap.get("return_code"))
&& "SUCCESS".equals(resultMap.get("result_code"))) {
PayResponse payResponse = new PayResponse();
payResponse.setSuccess(true);
payResponse.setPayUrl(resultMap.get("code_url")); // 二维码链接
payResponse.setTradeNo(resultMap.get("prepay_id"));
payResponse.setStatus(PayStatusEnum.WAITING);
payResponse.setRawResponse(response);
return payResponse;
} else {
return PayResponse.fail(
resultMap.get("err_code"),
resultMap.get("err_code_des")
);
}
}
@Override
protected void afterPay(PayRequest request, PayResponse response) {
// 微信支付特有的后置处理: 记录prepay_id用于后续操作
super.afterPay(request, response);
if (response.isSuccess()) {
log.info("[微信] 获取二维码链接: {}", response.getPayUrl());
}
}
@Override
public PayResponse query(String outTradeNo) {
log.info("[微信] 查询订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse close(String outTradeNo) {
log.info("[微信] 关闭订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse refund(String outTradeNo, BigDecimal refundAmount) {
log.info("[微信] 退款: {}, amount={}", outTradeNo, refundAmount);
return PayResponse.success(outTradeNo, null);
}
}
4.5 银联支付渠道实现 (pay-channel-unionpay)
package com.pay.channel.unionpay;
import com.pay.common.enums.PayChannelEnum;
import com.pay.common.enums.PayStatusEnum;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import com.pay.core.strategy.AbstractPaymentStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 银联支付策略实现
*/
@Slf4j
@Component
public class UnionPayStrategy extends AbstractPaymentStrategy {
@Value("${pay.unionpay.mer-id}")
private String merId;
@Value("${pay.unionpay.cert-path}")
private String certPath;
@Value("${pay.unionpay.gateway-url:https://gateway.95516.com/gateway/api/frontTransReq.do}")
private String gatewayUrl;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.UNIONPAY;
}
@Override
protected Map<String, String> buildRequestParams(PayRequest request) {
Map<String, String> params = new HashMap<>();
// 版本号
params.put("version", "5.1.0");
// 字符集编码
params.put("encoding", "UTF-8");
// 签名方法
params.put("signMethod", "01");
// 交易类型
params.put("txnType", "01");
// 交易子类型
params.put("txnSubType", "01");
// 业务类型
params.put("bizType", "000201");
// 渠道类型
params.put("channelType", "07");
// 前台通知地址
params.put("frontUrl", request.getReturnUrl());
// 后台通知地址
params.put("backUrl", request.getNotifyUrl());
// 接入类型
params.put("accessType", "0");
// 商户号
params.put("merId", merId);
// 商户订单号
params.put("orderId", request.getOutTradeNo());
// 订单发送时间
params.put("txnTime", LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
// 交易金额 (单位: 分)
params.put("txnAmt", request.getAmount()
.multiply(new BigDecimal("100")).intValue() + "");
// 交易币种
params.put("currencyCode", "156");
// 订单描述
params.put("orderDesc", request.getSubject());
return params;
}
@Override
protected String sign(Map<String, String> params) {
// 银联签名使用证书签名
return UnionPaySignature.signByCert(params, certPath);
}
@Override
protected String getPayUrl() {
return gatewayUrl;
}
@Override
protected PayResponse parseResponse(String response) {
Map<String, String> resultMap = parseQueryString(response);
if ("00".equals(resultMap.get("respCode"))) {
PayResponse payResponse = new PayResponse();
payResponse.setSuccess(true);
payResponse.setPayUrl(resultMap.get("payUrl"));
payResponse.setTradeNo(resultMap.get("queryId"));
payResponse.setStatus(PayStatusEnum.WAITING);
payResponse.setRawResponse(response);
return payResponse;
} else {
return PayResponse.fail(
resultMap.get("respCode"),
resultMap.get("respMsg")
);
}
}
@Override
public PayResponse query(String outTradeNo) {
log.info("[银联] 查询订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse close(String outTradeNo) {
log.info("[银联] 关闭订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse refund(String outTradeNo, BigDecimal refundAmount) {
log.info("[银联] 退款: {}, amount={}", outTradeNo, refundAmount);
return PayResponse.success(outTradeNo, null);
}
}
4.6 京东钱包渠道实现 (pay-channel-jdpay)
package com.pay.channel.jdpay;
import com.pay.common.enums.PayChannelEnum;
import com.pay.common.enums.PayStatusEnum;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import com.pay.core.strategy.AbstractPaymentStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* 京东钱包支付策略实现
*/
@Slf4j
@Component
public class JdPayStrategy extends AbstractPaymentStrategy {
@Value("${pay.jdpay.merchant}")
private String merchant;
@Value("${pay.jdpay.des-key}")
private String desKey;
@Value("${pay.jdpay.private-key}")
private String privateKey;
@Value("${pay.jdpay.gateway-url:https://paygate.jd.com/service/uniorder}")
private String gatewayUrl;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.JDPAY;
}
@Override
protected Map<String, String> buildRequestParams(PayRequest request) {
Map<String, String> params = new HashMap<>();
params.put("version", "V2.0");
params.put("merchant", merchant);
params.put("tradeNum", request.getOutTradeNo());
params.put("tradeName", request.getSubject());
params.put("tradeDesc", request.getBody());
// 金额单位: 分
params.put("amount", request.getAmount()
.multiply(new BigDecimal("100")).longValue() + "");
params.put("currency", "CNY");
params.put("callbackUrl", request.getReturnUrl());
params.put("notifyUrl", request.getNotifyUrl());
params.put("tradeType", "GEN"); // 一般交易
return params;
}
@Override
protected String sign(Map<String, String> params) {
// 京东签名: 先DES加密,再RSA签名
String encrypted = JdPaySignature.encrypt3DES(params, desKey);
return JdPaySignature.rsaSign(encrypted, privateKey);
}
@Override
protected String getPayUrl() {
return gatewayUrl;
}
@Override
protected PayResponse parseResponse(String response) {
// 解析京东响应
Map<String, String> resultMap = JdPaySignature.decrypt3DES(response, desKey);
if ("000000".equals(resultMap.get("code"))) {
PayResponse payResponse = new PayResponse();
payResponse.setSuccess(true);
payResponse.setPayUrl(resultMap.get("payUrl"));
payResponse.setTradeNo(resultMap.get("jdTradeNum"));
payResponse.setStatus(PayStatusEnum.WAITING);
payResponse.setRawResponse(response);
return payResponse;
} else {
return PayResponse.fail(
resultMap.get("code"),
resultMap.get("desc")
);
}
}
@Override
public PayResponse query(String outTradeNo) {
log.info("[京东] 查询订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse close(String outTradeNo) {
log.info("[京东] 关闭订单: {}", outTradeNo);
return PayResponse.success(outTradeNo, null);
}
@Override
public PayResponse refund(String outTradeNo, BigDecimal refundAmount) {
log.info("[京东] 退款: {}, amount={}", outTradeNo, refundAmount);
return PayResponse.success(outTradeNo, null);
}
}
五、Spring Boot Starter集成
5.1 自动配置类
package com.pay.starter.config;
import com.pay.core.context.PaymentContext;
import com.pay.core.factory.PaymentStrategyFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 支付自动配置
*/
@Configuration
@ComponentScan(basePackages = {
"com.pay.core",
"com.pay.channel.alipay",
"com.pay.channel.wechat",
"com.pay.channel.unionpay",
"com.pay.channel.jdpay"
})
public class PaymentAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public PaymentStrategyFactory paymentStrategyFactory() {
return new PaymentStrategyFactory();
}
@Bean
@ConditionalOnMissingBean
public PaymentContext paymentContext(PaymentStrategyFactory factory) {
return new PaymentContext(factory);
}
}
5.2 配置属性类
package com.pay.starter.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 支付配置属性
*/
@Data
@ConfigurationProperties(prefix = "pay")
public class PaymentProperties {
private AlipayConfig alipay = new AlipayConfig();
private WechatConfig wechat = new WechatConfig();
private UnionPayConfig unionpay = new UnionPayConfig();
private JdPayConfig jdpay = new JdPayConfig();
@Data
public static class AlipayConfig {
private String appId;
private String privateKey;
private String publicKey;
private String gatewayUrl = "https://openapi.alipay.com/gateway.do";
}
@Data
public static class WechatConfig {
private String appId;
private String mchId;
private String apiKey;
private String gatewayUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
}
@Data
public static class UnionPayConfig {
private String merId;
private String certPath;
private String gatewayUrl = "https://gateway.95516.com/gateway/api/frontTransReq.do";
}
@Data
public static class JdPayConfig {
private String merchant;
private String desKey;
private String privateKey;
private String gatewayUrl = "https://paygate.jd.com/service/uniorder";
}
}
六、使用示例
6.1 Controller层
package com.example.controller;
import com.pay.common.enums.PayChannelEnum;
import com.pay.core.context.PaymentContext;
import com.pay.core.model.PayRequest;
import com.pay.core.model.PayResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
/**
* 支付控制器
*/
@RestController
@RequestMapping("/api/pay")
@RequiredArgsConstructor
public class PayController {
private final PaymentContext paymentContext;
/**
* 统一下单接口
*/
@PostMapping("/create")
public PayResponse createOrder(@RequestParam String channel,
@RequestParam String orderNo,
@RequestParam BigDecimal amount,
@RequestParam String subject) {
PayRequest request = new PayRequest();
request.setOutTradeNo(orderNo);
request.setAmount(amount);
request.setSubject(subject);
request.setNotifyUrl("https://example.com/notify");
request.setReturnUrl("https://example.com/return");
PayChannelEnum payChannel = PayChannelEnum.getByCode(channel);
return paymentContext.pay(payChannel, request);
}
/**
* 查询订单
*/
@GetMapping("/query")
public PayResponse queryOrder(@RequestParam String channel,
@RequestParam String orderNo) {
PayChannelEnum payChannel = PayChannelEnum.getByCode(channel);
return paymentContext.query(payChannel, orderNo);
}
/**
* 关闭订单
*/
@PostMapping("/close")
public PayResponse closeOrder(@RequestParam String channel,
@RequestParam String orderNo) {
PayChannelEnum payChannel = PayChannelEnum.getByCode(channel);
return paymentContext.close(payChannel, orderNo);
}
/**
* 申请退款
*/
@PostMapping("/refund")
public PayResponse refund(@RequestParam String channel,
@RequestParam String orderNo,
@RequestParam BigDecimal amount) {
PayChannelEnum payChannel = PayChannelEnum.getByCode(channel);
return paymentContext.refund(payChannel, orderNo, amount);
}
}
6.2 application.yml配置
pay:
alipay:
app-id: your_alipay_app_id
private-key: your_alipay_private_key
public-key: your_alipay_public_key
wechat:
app-id: your_wechat_app_id
mch-id: your_wechat_mch_id
api-key: your_wechat_api_key
unionpay:
mer-id: your_unionpay_mer_id
cert-path: /path/to/cert
jdpay:
merchant: your_jd_merchant
des-key: your_jd_des_key
private-key: your_jd_private_key
七、设计模式对比与总结
7.1 三种模式的职责划分
+------------------------------------------------------------------+
| 设计模式职责划分 |
+------------------------------------------------------------------+
| |
| +-------------------+ |
| | 工厂模式 | 负责: 创建和管理策略对象 |
| | StrategyFactory | 解决: 对象创建与使用解耦 |
| +-------------------+ |
| | |
| | 创建 |
| v |
| +-------------------+ |
| | 策略模式 | 负责: 定义统一的策略接口 |
| | PaymentStrategy | 解决: 算法可互换,消除if-else |
| +-------------------+ |
| ^ |
| | 实现 |
| | |
| +-------------------+ |
| | 模板方法模式 | 负责: 定义算法骨架,复用公共流程 |
| |AbstractPayStrategy| 解决: 代码重复,流程标准化 |
| +-------------------+ |
| |
+------------------------------------------------------------------+
7.2 模式组合的优势
| 设计模式 | 解决的问题 | 在支付系统中的应用 |
|---|---|---|
| 策略模式 | 消除if-else,算法可互换 | 不同支付渠道的切换 |
| 模板方法 | 代码复用,流程标准化 | 支付流程的公共步骤 |
| 工厂模式 | 对象创建与使用解耦 | 支付策略的创建管理 |
7.3 扩展新渠道的步骤
新增一个支付渠道(如苏宁支付)只需:
- 创建新模块
pay-channel-suning - 实现
AbstractPaymentStrategy抽象类 - 添加
@Component注解 - 配置相关参数
@Component
public class SuningPayStrategy extends AbstractPaymentStrategy {
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.SUNING; // 需要先在枚举中添加
}
@Override
protected Map<String, String> buildRequestParams(PayRequest request) {
// 实现苏宁特有的参数构建
}
@Override
protected String sign(Map<String, String> params) {
// 实现苏宁特有的签名方式
}
// ... 其他方法实现
}
无需修改任何现有代码,完美符合开闭原则!
八、与开源框架对比
8.1 IJPay
IJPay 是一款优秀的开源支付SDK,我们的设计借鉴了其部分思想:
IJPay特点:
├── 支持多种支付渠道
├── 提供丰富的工具类
├── 简化签名验签流程
└── 社区活跃,文档完善
本文设计的改进:
├── 更清晰的设计模式应用
├── 更好的扩展性(工厂+策略)
├── 更标准化的流程(模板方法)
└── 更灵活的配置方式(Spring Boot Starter)
8.2 Pay-Java
Pay-Java 同样是优秀的支付整合框架:
// Pay-Java的使用方式
PayService payService = new AliPayService(config);
PayOrder payOrder = new PayOrder("订单", "商品", 0.01);
Map<String, Object> result = payService.orderInfo(payOrder);
// 本文设计的使用方式 (更加统一)
PayResponse response = paymentContext.pay(PayChannelEnum.ALIPAY, request);
九、总结
通过本文,我们学习了如何将三种设计模式组合使用,构建一个可扩展的支付系统:
三模式组合优势
|
+----------------+----------------+
| | |
+----v----+ +----v----+ +----v----+
| 策略模式 | | 模板方法 | | 工厂模式 |
+---------+ +---------+ +---------+
| 渠道切换 | | 流程复用 | | 对象管理 |
+---------+ +---------+ +---------+
| | |
+----------------+----------------+
|
高扩展性支付系统
核心要点:
- 策略模式:定义支付策略接口,各渠道实现自己的支付逻辑
- 模板方法:抽取公共流程(校验、签名、请求、解析),子类实现差异部分
- 工厂模式:统一管理策略对象的创建,支持Spring自动注入