小蔡学Java

项目二总结:(十二)支付总结(1)

2024-03-20 20:01 1416 0 项目 支付消息消费幂等性

分析

支付业务与其他业务相比,相对独立,所以比较适合将支付业务划分为一个微服务,而支付业务并不关系物流业务中运输、取派件等业务,只关心付款金额、付款平台、所支付的订单等。 支付微服务在整个系统架构中的业务时序图:

支付渠道管理

支付是对接支付平台完成的,例如支付宝、微信、京东支付等,一般在这些平台上需要申请账号信息,通过这些账号信息完成与支付平台的交互,在我们的支付微服务中,将这些数据称之为【支付渠道】,并且将其存储到数据库中,通过程序可以支付渠道进行管理。

表结构

支付微服务的数据是:sl_trade,支付渠道的表为:sl_pay_channel,表结构如下:

其中表中已经包含了2条数据,分别是支付宝和微信的账号信息,可以直接与支付平台对接。

代码

PayChannelEntity类是对sl_pay_channel表的映射,Entity类继承BaseEntity,在BaseEntity中统一定义了id、created、updated,其中created、updated是使用MybatisPlus自动填充的。

/**
* @Description:交易渠道表
*/
@Data
	@NoArgsConstructor
	@AllArgsConstructor
	@EqualsAndHashCode(callSuper = true)
	@TableName("sl_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;

	}

PayChannelMapper

PayChannelMapper继承了MP的BaseMapper,并且加了@Mapper注解。

/**
 * 交易渠道表Mapper接口
 */
@Mapper
public interface PayChannelMapper extends BaseMapper<PayChannelEntity> {

}

PayChannelService

该Service中定义了6个方法,可以对支付渠道的数据进行CRUD的管理,其中findByEnterpriseId()方法将是我们常用的一个方法,根据业务商户id查询和通道唯一标记符查询支付渠道。该方法是需要对数据做缓存的,目前并没有实现缓存,这个需要由你来实现。

package com.sl.ms.trade.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sl.ms.trade.domain.PayChannelDTO;
import com.sl.ms.trade.entity.PayChannelEntity;

import java.util.List;

/**
 * @Description: 支付通道服务类
 */
public interface PayChannelService extends IService<PayChannelEntity> {

    /**
     * @param payChannelDTO 查询条件
     * @param pageNum       当前页
     * @param pageSize      当前页
     * @return Page<PayChannel> 分页对象
     * @Description 支付通道列表
     */
    Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize);

    /**
     * 根据商户id查询渠道配置,该配置会被缓存10分钟
     *
     * @param enterpriseId 商户id
     * @param channelLabel 通道唯一标记
     * @return PayChannelEntity 交易渠道对象
     */
    PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel);

    /**
     * @param payChannelDTO 对象信息
     * @return PayChannelEntity 交易渠道对象
     * @Description 创建支付通道
     */
    PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO);

    /**
     * @param payChannelDTO 对象信息
     * @return Boolean 是否成功
     * @Description 修改支付通道
     */
    Boolean updatePayChannel(PayChannelDTO payChannelDTO);

    /**
     * @param checkedIds 选择的支付通道ID
     * @return Boolean 是否成功
     * @Description 删除支付通道
     */
    Boolean deletePayChannel(String[] checkedIds);

    /**
     * @param channelLabel 支付通道标识
     * @return 支付通道列表
     * @Description 查找渠道标识
     */
    List<PayChannelEntity> findPayChannelList(String channelLabel);
}

PayChannelServiceImpl

● 该类继承了MP的ServiceImpl,可以实现基本的CRUD方法 ● findByEnterpriseId()方法中的TODO需要在实战中完成

/**
 * @Description: 服务实现类
 */
@Service
public class PayChannelServiceImpl extends ServiceImpl<PayChannelMapper, PayChannelEntity> implements PayChannelService {

    @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);
    }

    @Override
    public PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel) {
        LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PayChannelEntity::getEnterpriseId, enterpriseId)
                .eq(PayChannelEntity::getChannelLabel, channelLabel)
                .eq(PayChannelEntity::getEnableFlag, Constants.YES);
        //TODO 缓存
        return super.getOne(queryWrapper);
    }

    @Override
    public PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO) {
        PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
        boolean flag = super.save(payChannel);
        if (flag) {
            return payChannel;
        }
        return null;
    }

    @Override
    public Boolean updatePayChannel(PayChannelDTO payChannelDTO) {
        PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
        return super.updateById(payChannel);
    }

    @Override
    public Boolean deletePayChannel(String[] checkedIds) {
        List<String> ids = Arrays.asList(checkedIds);
        return super.removeByIds(ids);
    }

    @Override
    public List<PayChannelEntity> findPayChannelList(String channelLabel) {
        LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PayChannelEntity::getChannelLabel, channelLabel)
                .eq(PayChannelEntity::getEnableFlag, Constants.YES);
        return list(queryWrapper);
    }
}

PayChannelController

该类中对于支付渠道维护的各种方法的维护,确保可以对外提供服务。

@Slf4j
@RestController
@Api(tags = "支付通道")
@RequestMapping("payChannel")
public class PayChannelController {

    @Resource
    private PayChannelService payChannelService;

    /**
     * 支付通道列表
     *
     * @param payChannelDTO 查询条件
     * @return 分页数据对象
     */
    @PostMapping("page/{pageNum}/{pageSize}")
    @ApiOperation(value = "查询支付通道分页", notes = "查询支付通道分页")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true),
            @ApiImplicitParam(name = "pageNum", value = "页码"),
            @ApiImplicitParam(name = "pageSize", value = "每页条数")
    })
    public PageResponse<PayChannelDTO> findPayChannelPage(
            @RequestBody PayChannelDTO payChannelDTO,
            @PathVariable("pageNum") int pageNum,
            @PathVariable("pageSize") int pageSize) {
        Page<PayChannelEntity> payChannelVoPage = payChannelService.findPayChannelPage(payChannelDTO, pageNum, pageSize);
        return new PageResponse<>(payChannelVoPage, PayChannelDTO.class);
    }

    /**
     * 添加支付通道
     *
     * @param payChannelDTO 对象信息
     */
    @PostMapping
    @ApiOperation(value = "添加支付通道", notes = "添加支付通道")
    @ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
    public void createPayChannel(@RequestBody PayChannelDTO payChannelDTO) {
        PayChannelEntity payChannel = this.payChannelService.createPayChannel(payChannelDTO);
        if (null != payChannel) {
            return;
        }
        throw new SLException("添加支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

    /**
     * 修改支付通道
     *
     * @param payChannelDTO 对象信息
     */
    @PutMapping
    @ApiOperation(value = "修改支付通道", notes = "修改支付通道")
    @ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
    public void updatePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
        Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
        if (flag) {
            return;
        }
        throw new SLException("修改支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

    /**
     * 删除支付通道
     *
     * @param payChannelDTO 查询对象
     */
    @DeleteMapping
    @ApiOperation(value = "删除支付通道", notes = "删除支付通道")
    @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
    public void deletePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
        String[] checkedIds = payChannelDTO.getCheckedIds();
        Boolean flag = this.payChannelService.deletePayChannel(checkedIds);
        if (flag) {
            return;
        }
        throw new SLException("删除支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

    @PutMapping("update-payChannel-enableFlag")
    @ApiOperation(value = "修改支付通道状态", notes = "修改支付通道状态")
    @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
    public void updatePayChannelEnableFlag(@RequestBody PayChannelDTO payChannelDTO) {
        Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
        if (flag) {
            return;
        }
        throw new SLException("修改支付通道状态失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
    }
}

扫码支付

交易单表结构

【交易单表 sl_trading】是指,针对于订单进行支付的记录表,其中记录了订单号,支付状态、支付平台、金额、是否有退款等信息。具体表结构如下:

代码流程

下面展现了整体的扫描支付代码调用流程,我们将按照下面的流程进行代码的阅读。

幂等性处理

在向支付平台申请支付之前对交易单对象做幂等性处理,主要是防止重复的生成交易单以及一些业务逻辑的处理

    @Override
    public void idempotentCreateTrading(TradingEntity tradingEntity) throws SLException {
        TradingEntity trading = tradingService.findTradByProductOrderNo(tradingEntity.getProductOrderNo());
        if (ObjectUtil.isEmpty(trading)) {
            //新交易单,生成交易号
            Long id = Convert.toLong(identifierGenerator.nextId(tradingEntity));
            tradingEntity.setId(id);
            tradingEntity.setTradingOrderNo(id);
            return;
        }

        TradingStateEnum tradingState = trading.getTradingState();
        if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.YJS, TradingStateEnum.MD)) {
            //已结算、免单:直接抛出重复支付异常
            throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
        } else if (ObjectUtil.equals(TradingStateEnum.FKZ, tradingState)) {
            //付款中,如果支付渠道一致,说明是重复,抛出支付中异常,否则需要更换支付渠道
            //举例:第一次通过支付宝付款,付款中用户取消,改换了微信支付
            if (StrUtil.equals(trading.getTradingChannel(), tradingEntity.getTradingChannel())) {
                throw new SLException(TradingEnum.TRADING_STATE_PAYING);
            } else {
                tradingEntity.setId(trading.getId()); // id设置为原订单的id
                //重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
                tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
            }
        } else if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.QXDD, TradingStateEnum.GZ)) {
            //取消订单,挂账:创建交易号,对原交易单发起支付
            tradingEntity.setId(trading.getId()); // id设置为原订单的id
            //重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
            tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
        } else {
            //其他情况:直接交易失败
            throw new SLException(TradingEnum.PAYING_TRADING_FAIL);
        }
    }

在此代码中,主要是逻辑是: ● 如果根据订单号查询交易单数据,如果不存在说明新交易单,生成交易单号后直接返回,这里的交易单号也是使用雪花id。

● 如果支付状态是已经【支付成功】或是【免单 - 不需要支付】,直接抛出异常。

● 如果支付状态是【付款中】,此时有两种情况

○ 如果支付渠道相同(此前使用支付宝付款,本次也是使用支付宝付款),这种情况抛出异常

○ 如果支付渠道不同,我们是允许在生成二维码后更换支付渠道,此时需要重新生成交易单号,此时交易单号与id将不同。

● 如果支付状态是【取消订单】或【挂账】,将id设置为原交易号,交易号重新生成,这样做的目的是既保留了原订单的交易号,又可以生成新的交易号(不重新生成的话,没有办法在支付平台进行支付申请),与之前不会有影响。

评论( 0 )

  • 博主 Mr Cai
  • 坐标 河南 信阳
  • 标签 Java、SpringBoot、消息中间件、Web、Code爱好者

文章目录