分析
支付业务与其他业务相比,相对独立,所以比较适合将支付业务划分为一个微服务,而支付业务并不关系物流业务中运输、取派件等业务,只关心付款金额、付款平台、所支付的订单等。 支付微服务在整个系统架构中的业务时序图:
支付渠道管理
支付是对接支付平台完成的,例如支付宝、微信、京东支付等,一般在这些平台上需要申请账号信息,通过这些账号信息完成与支付平台的交互,在我们的支付微服务中,将这些数据称之为【支付渠道】,并且将其存储到数据库中,通过程序可以支付渠道进行管理。
表结构
支付微服务的数据是: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 )