大家好,我是此林。

此后,我们将进入支付微服务落地设计系列,今天要聊的是支付微服务的部分之一:支付渠道开发。

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,也不用关心具体实现类(AliPayHandlerWeChatPayHandler)。

  • 对象的获取逻辑(遍历 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 {
}

调用时,根据 channelLabelPayChannelEnum 动态选择某个策略:

PayHandler handler = HandlerFactory.get("ALI_PAY", BasicPayHandler.class);
handler.pay();

这就是策略模式的体现:支付逻辑有很多种(策略),它们都实现了同一个接口,系统在运行时选择某一个来执行。

所以我们可以发现,在实际业务里,工厂模式和策略模式的边界并不是那么明显,很多时候是结合在一起用的。

支付微服务-支付渠道管理就讲到这里了。

我是此林,关注我吧,带你看不一样的世界!

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐