支付系统-支付微服务落地设计1:支付渠道开发
大家好,我是此林。
此后,我们将进入支付微服务落地设计系列,今天要聊的是支付微服务的部分之一:支付渠道开发。
1. 支付渠道概述
在我们的系统里,支付功能是通过对接第三方支付平台来实现的,比如支付宝、微信支付、京东支付等。
要使用这些平台的服务,通常需要先在对应的平台上申请账号,并获取相关的配置信息。有了这些账号和参数,系统才能与支付平台进行交互,完成支付、退款等操作。
为了方便管理和使用,我们在支付微服务中,把这些 账号及配置信息 统一称为【支付渠道】。
支付渠道就像是系统和支付平台之间的 “桥梁” ,所有与支付平台相关的动作都会依赖它。
为了保证后续能够灵活调用和统一维护,我们会把这些支付渠道的信息保存到数据库中。
这样一来,不同的支付方式都能通过程序进行集中管理,比如新增、修改或停用某个渠道。
2. 通用实体类设计
一般项目中,我们的 common 包会写一个 BaseEntity 实体,比如:
@Data
public abstract class BaseEntity implements Serializable {
@TableId
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updated;
}
这里我们结合了 Mybatis-plus 的依赖:
<properties>
<mybatis.mybatis-plus>3.5.2</mybatis.mybatis-plus>
</properties>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
字段 id 是主键,默认使用雪花算法填充
字段 created 是创建时间,@TableField(fill = FieldFill.INSERT) 代表 SQL插入的时候会自动填充。
字段 updates 是更新时间,@TableField(fill = FieldFill.INSERT_UPDATE) 代表 SQL插入或者更新的时候会自动填充。
当然,这个配合 MyBatis-Plus 的 MetaObjectHandler 使用,否则不会自动赋值。
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Object created = getFieldValByName("created", metaObject);
if (null == created) {
//字段为空,可以进行填充
setFieldValByName("created", LocalDateTime.now(), metaObject);
}
Object updated = getFieldValByName("updated", metaObject);
if (null == updated) {
//字段为空,可以进行填充
setFieldValByName("updated", LocalDateTime.now(), metaObject);
}
}
@Override
public void updateFill(MetaObject metaObject) {
//更新数据时,直接更新字段
setFieldValByName("updated", LocalDateTime.now(), metaObject);
}
}
这个配置不难,如果直白点,直接在数据库的表字段里设置触发器也是可以的。不过我们设计统一的实体类、后续实体直接继承会更加方便点。
除了这个 BaseEntity,我们还会设计一个 BaseEnum,如下:
public interface BaseEnum {
/**
* 业务状态码
*/
Integer getCode();
/**
* 业务说明
*/
String getValue();
}
3. 支付渠道实体类设计
因为我们是第一次,所以会讲的细一点,后续文章的话就不多赘述了。
刚才我们设计了 BaseEnum,现在我们来设计 PayChannelEnum,支付渠道主流的也就两种:微信、支付宝。
public enum PayChannelEnum implements BaseEnum {
ALI_PAY(1, "支付宝"),
WECHAT_PAY(2, "微信支付");
private Integer code;
private String value;
PayChannelEnum(Integer code, String value) {
this.code = code;
this.value = value;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getValue() {
return this.value;
}
public static PayChannelEnum codeOf(Integer code) {
return EnumUtil.getBy(PayChannelEnum::getCode, code);
}
}
之前看了 BaseEntity,现在我们来设计 PayChannelEntity。
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("wy_pay_channel")
public class PayChannelEntity extends BaseEntity {
private static final long serialVersionUID = -1452774366739615656L;
@ApiModelProperty(value = "通道名称")
private String channelName;
@ApiModelProperty(value = "通道唯一标记")
private String channelLabel;
@ApiModelProperty(value = "域名")
private String domain;
@ApiModelProperty(value = "商户appid")
private String appId;
@ApiModelProperty(value = "支付公钥")
private String publicKey;
@ApiModelProperty(value = "商户私钥")
private String merchantPrivateKey;
@ApiModelProperty(value = "其他配置")
private String otherConfig;
@ApiModelProperty(value = "AES混淆密钥")
private String encryptKey;
@ApiModelProperty(value = "说明")
private String remark;
@ApiModelProperty(value = "回调地址")
private String notifyUrl;
@ApiModelProperty(value = "是否有效")
protected String enableFlag;
@ApiModelProperty(value = "商户号")
private Long enterpriseId;
}
这里类上有很多注解,除了最后一个 @TableName 是 Mybatis-Plus 的,其他的其实都是 lombok 的,我们逐个来解释。
@Data
自动生成 getter/setter、toString()、equals()、hashCode() 方法。
相当于省掉很多样板代码。
@NoArgsConstructor
生成一个 无参构造函数。
@AllArgsConstructor
生成一个包含所有字段的构造函数。
@EqualsAndHashCode(callSuper = true)
生成 equals() 和 hashCode() 方法。
callSuper = true 会把父类(BaseEntity)的字段也一起比较。
如果不写,默认 false,只比较子类字段。
@TableName("wy_pay_channel")
指定这个实体类对应的数据库表名 wy_pay_channel。
不写的话,MyBatis-Plus 会根据驼峰规则推断(可能不准确)。
理解了这些注解的含义,我们再具体看重要的字段。看到这么多字段,是不是感觉有点晕?
举个例子就知道了:
channel_name (通道名称):比如 “微信支付”、“支付宝支付”。
channel_label (通道唯一标记):比如 “WECHAT_PAY”、“ALI_PAY”。
这两个字段都是我们业务系统自己设定的。
domain (域名):也就是支付平台的域名,比如支付宝的 “openapi.alipay.com”。
app_id (商户 appid):在 小程序支付 场景下,它指的是 商户实际经营主体的小程序应用的 AppID。也就是说,最终用户在小程序里唤起支付时,支付平台需要知道“这是在哪个小程序里发起的支付”。
public_key (支付公钥)、merchant_private_key (支付私钥):鉴权机制,public_key 用来加密,merchant_private_key 用于解密。
other_config (其他配置)、encrypt_key (AES混淆密钥)、remark (说明):这三个字段在数据库表里我们把它们设置为允许 NULL,encrypt_key 是微信独有的,后续会说到。
notify_url (回调接口):这个是三方支付平台在完成支付后,异步回调这个地址来通知我们支付状态(成功还是失败),所以这个地址是我们服务器的公网 IP 接口。如果没有公网 IP,需要使用内网穿透。
enable_flag (是否有效):即当前这个渠道配置是否有效。
enterprise_id (商户 id):支付平台分配给商户的唯一标识。就类似于我们业务系统要给每个用户分配唯一 userId 一样,三方支付也要给我们使用它支付服务的商户分配 enterprise_id。
3. PayChannel 的 CRUD
前面说了这么多,其实支付渠道写入数据库,无非就是为了方便系统内部进行 CRUD,所以支付渠道这一部分也是不对外提供 feign 接口的。
当然,要是觉得麻烦,你也可以把各个支付平台的配置写死在 yml 里。
这样的话,只不过后续需要动态修改麻烦点。
比如根据 channelLabel、channelName、enableFlag 分页查询支付渠道,并根据 created 正序排序。
@Override
public Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize) {
Page<PayChannelEntity> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
//设置条件
queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getChannelLabel()), PayChannelEntity::getChannelLabel, payChannelDTO.getChannelLabel());
queryWrapper.likeRight(StrUtil.isNotEmpty(payChannelDTO.getChannelName()), PayChannelEntity::getChannelName, payChannelDTO.getChannelName());
queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getEnableFlag()), PayChannelEntity::getEnableFlag, payChannelDTO.getEnableFlag());
//设置排序
queryWrapper.orderByAsc(PayChannelEntity::getCreated);
return super.page(page, queryWrapper);
}
注意这个 Mybatis-Plus 分页需要配置分页插件。
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", value = "url")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//设置分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//防全表更新与删除插件
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return mybatisPlusInterceptor;
}
}
这个 @ConditionalOnProperty(prefix = "spring.datasource", value = "url") 为条件注解,只有在配置了 spring.datasource 且设置了数据库 url 的 springboot 应用才会进行自动装配。
当查询出 PayChannelEntity 后,我们可以选择把它缓存到 redis 里 10 分钟,MySQL 和 Redis 一致性策略采用:先更新 MySQL,再删除 Redis 的策略。
问:为什么不先删除 Redis,再更新 MySQL?
答:若先删除 Redis 缓存,在更新后的 PayChannelEntity 还没保存到 MySQL 时,这个时候又有读请求过来,MySQL 里旧的值又被重新缓存到了 Redis,造成了缓存不一致。所以如果要先删除 Redis,通常会采用 延迟双删 的策略。(即在更新完 MySQL 之后再删除一遍 Redis)。不过,延迟双删这个策略在生产一般不怎么用。
4. 策略模式 + 工厂模式选择支付渠道
对于支付逻辑,我们通常会使用 策略模式 或 工厂模式 根据 channelLabel 选择具体实现,这样新增渠道时无需修改已有逻辑,系统易扩展、低耦合、安全可靠。
/**
* Handler工厂,用于获取指定类型的具体渠道的实例对象
*/
public class HandlerFactory {
private HandlerFactory() {
}
public static <T> T get(PayChannelEnum payChannel, Class<T> handler) {
Map<String, T> beans = SpringUtil.getBeansOfType(handler);
for (Map.Entry<String, T> entry : beans.entrySet()) {
PayChannel payChannelAnnotation = entry.getValue().getClass().getAnnotation(PayChannel.class);
if (ObjectUtil.isNotEmpty(payChannelAnnotation) && ObjectUtil.equal(payChannel, payChannelAnnotation.type())) {
return entry.getValue();
}
}
return null;
}
public static <T> T get(String payChannel, Class<T> handler) {
return get(PayChannelEnum.valueOf(payChannel), handler);
}
}
这段 HandlerFactory 就是刚才提到的 策略模式 + 工厂模式 的具体实现方式。
这里采用了私有化构造方法,表示这个 HandlerFactory 不能被实例化,只能被静态化调用。
我们逐行来看代码,先看 第一个 get() 方法。
入参为:
- payChannel 支付渠道枚举,比如 PayChannelEnum.ALIPAY。
- handler 接口类型,比如 BasicPayHandler.class
从 Spring 容器里获取所有该接口类型的 Bean,key = Bean 名称,value = Bean 实例。用的是 hutool 的 SpringUtil 工具类。
Map<String, T> beans = SpringUtil.getBeansOfType(handler);
遍历所有的 Bean,取出每个 Bean 上的 @PayChannel 注解,如果注解存在,并且注解里的渠道类型 == 入参的 payChannel,返回当前 Bean(就是对应渠道的处理器)。
// 遍历所有的 Bean
for (Map.Entry<String, T> entry : beans.entrySet()) {
// 取出每个 Bean 上的 @PayChannel 注解
PayChannel payChannelAnnotation = entry.getValue().getClass().getAnnotation(PayChannel.class);
// 如果注解存在,并且注解里的渠道类型 == 入参的 payChannel
if (ObjectUtil.isNotEmpty(payChannelAnnotation)
&& ObjectUtil.equal(payChannel, payChannelAnnotation.type())) {
// 返回当前 Bean(就是对应渠道的处理器)
return entry.getValue();
}
}
没找到匹配的 Bean,返回 null。
return null;
再看第二个 get() 方法。其实也就入参和第一个 get() 有点不一样。
入参为:
- 支付渠道标识,比如 "ALIPAY"。
- handler 接口类型,比如 BasicPayHandler.class。
把字符串转成枚举,然后调用上面那个 get 方法。
return get(PayChannelEnum.valueOf(payChannel), handler);
问:所以这个 HandlerFactory 哪里体现了工厂模式和策略模式呢?
工厂模式(Factory Pattern) 的核心思想是:把创建对象的逻辑集中在工厂里,而不是分散在调用方。
PayHandler handler = HandlerFactory.get("ALI_PAY", BasicPayHandler.class);
-
调用方只要传入渠道标识(
ALIPAY/WECHAT),就能得到正确的PayHandler实例。 -
调用方不用自己 new,也不用关心具体实现类(
AliPayHandler、WeChatPayHandler)。 -
对象的获取逻辑(遍历 Spring Bean、解析
@PayChannel注解、匹配枚举)都集中在HandlerFactory。
这就是工厂模式的体现:我要一个支付渠道处理器,工厂帮我挑对的返回给我。
策略模式(Strategy Pattern) 的核心思想是:把一组算法或行为独立封装起来,通过统一接口,在运行时选择使用哪一个。
我们代码里有一个统一接口:
public interface BasicPayHandler {
}
不同渠道有不同实现:
@PayChannel(type = PayChannelEnum.ALI_PAY)
public class AliBasicPayHandler implements BasicPayHandler {
}
@PayChannel(type = PayChannelEnum.WECHAT_PAY)
public class WeChatBasicPayHandler implements BasicPayHandler {
}
调用时,根据 channelLabel 或 PayChannelEnum 动态选择某个策略:
PayHandler handler = HandlerFactory.get("ALI_PAY", BasicPayHandler.class);
handler.pay();
这就是策略模式的体现:支付逻辑有很多种(策略),它们都实现了同一个接口,系统在运行时选择某一个来执行。
所以我们可以发现,在实际业务里,工厂模式和策略模式的边界并不是那么明显,很多时候是结合在一起用的。
支付微服务-支付渠道管理就讲到这里了。
我是此林,关注我吧,带你看不一样的世界!
更多推荐



所有评论(0)