前言
上篇文章讲了我们应该怎么设计支付系统,构建了一个支付系统的蓝图,这篇文章我来讲一下怎么用代码来设计支付系统。当然,我肯定不是手把手的教你哈哈哈,我只是把支付系统的核心的部分展示给大家看,让大家了解一下原来用代码是这样子做的!
项目目录简述
整个支付系统都放在一个叫pay的大文件夹里,其中pay大文件夹中有很多小文件夹。
config文件夹是用来对微信和支付宝账户进行配置的;
Dao层是用来与数据库打交道的;
service层是用来实现支付的业务逻辑的;
controller层是用来对数据进行组装;
pojo文件夹里面的PayInfo 是一个用于表示支付信息的类,它包含了一系列的属性;
enums文件夹中的定义了一个枚举类PayPlatformEnum,用于表示支付平台的类型。其中包含两个枚举值 ALIPAY 和 WX,分别代表支付宝和微信支付平台;
mappers文件夹是将一个数据模型或对象转换为另一个数据模型或对象。它们通常用于处理不同层或组件之间的数据映射和转换;
templates文件夹里面的两个文件的作用是将支付链接转成对应的二维码页面。
关键文件夹
Dao层
我们在代码中建立一个Dao文件夹,用于存放Dao层的所有内容,这个Dao文件夹就一个文件:PayInfoMapper,文件的内容如下所示:
package com.imooc.pay.dao;
import com.imooc.pay.pojo.PayInfo;
/**
*这行代码是用于导入 com.imooc.pay.pojo.PayInfo 类。
*通过这个导入语句,您可以在当前的代码文件中使用 PayInfo 类。
* PayInfo 类 其实就是一些参数数据而已
*/
public interface PayInfoMapper {
//以下这些方法提供了对支付信息的常见数据库操作,包括插入、查询、更新和删除。您可以根据具体的业务需求选择适合的方法来操作支付信息数据
int deleteByPrimaryKey(Integer id);
// 根据主键删除支付信息。该方法根据传入的主键值,在数据库中找到对应的支付信息,并将其删除
int insert(PayInfo record);
// 插入支付信息。该方法将传入的支付信息对象插入到数据库中
int insertSelective(PayInfo record);
// 有选择地插入支付信息。该方法只插入支付信息对象中非空的属性到数据库中,对于空属性的字段不进行插入
PayInfo selectByPrimaryKey(Integer id);
// 根据主键查询支付信息。该方法根据传入的主键值,在数据库中找到对应的支付信息,并返回查询到的支付信息对象
int updateByPrimaryKeySelective(PayInfo record);
// 有选择地更新支付信息。该方法根据传入的支付信息对象中非空的属性值,对数据库中对应的支付信息进行更新
int updateByPrimaryKey(PayInfo record);
// 更新支付信息。该方法将传入的支付信息对象的所有属性值更新到数据库中对应的支付信息
PayInfo selectByOrderNo(Long orderNo);
// 根据订单号查询支付信息。该方法根据传入的订单号,在数据库中找到对应的支付信息,并返回查询到的支付信息对象
}
通过定义这些方法,我们就可以使用PayInfoMapper接口来执行与支付信息相关的数据库操作。
关于接口的具体实现,通常是由持久化框架(比如Mybatis)来完成。
service层
有一个接口,这个接口定义了支付服务的核心功能(业务逻辑):创建支付,处理异步通知和查询支付记录
//在这段代码中,IPayService 是一个接口,用于定义支付服务相关的方法
package com.imooc.pay.service;
import com.imooc.pay.pojo.PayInfo;
import com.lly835.bestpay.enums.BestPayTypeEnum;
import com.lly835.bestpay.model.PayResponse;
import java.math.BigDecimal;
public interface IPayService {
//以下的这些方法定义了支付服务的核心功能,包括创建支付、处理异步通知和查询支付记录。通过实现这些方法,您可以具体实现支付服务的业务逻辑
/**
创建/发起支付:该方法用于根据订单号、支付金额和支付类型发起支付请求,并返回支付的响应结果
*/
PayResponse create(String orderId, BigDecimal amount, BestPayTypeEnum bestPayTypeEnum);
/**
* 异步通知处理:
* 该方法用于处理支付异步通知的数据,通常在支付平台支付成功后,支付平台会发送异步通知,该方法负责处理这个异步通知的数据
* @param notifyData
*/
String asyncNotify(String notifyData);
/**
* 查询支付记录(通过订单号):该方法根据订单号查询支付记录,返回支付信息对象
* @param orderId
* @return
*/
PayInfo queryByOrderId(String orderId);
}
接下来是实现接口中的功能:
package com.imooc.pay.service.impl;
//import导入包这些就省掉了,太长不写进来
@Slf4j
@Service // @Service 注解表示该类是一个服务类
public class PayServiceImpl implements IPayService {
private final static String QUEUE_PAY_NOTIFY = "payNotify";
//@Autowired 注解用于依赖注入
@Autowired
private BestPayService bestPayService;
@Autowired
private PayInfoMapper payInfoMapper;
@Autowired
private AmqpTemplate amqpTemplate;
/**
* 创建/发起支付:
* 在该方法中,首先将支付信息写入数据库,
* 然后使用 BestPayService 发起支付请求,
* 并返回支付的响应结果
*
* @param orderId
* @param amount
*/
@Override
public PayResponse create(String orderId, BigDecimal amount, BestPayTypeEnum bestPayTypeEnum) {
//写入数据库
PayInfo payInfo = new PayInfo(Long.parseLong(orderId),
PayPlatformEnum.getByBestPayTypeEnum(bestPayTypeEnum).getCode(),
OrderStatusEnum.NOTPAY.name(),
amount);
payInfoMapper.insertSelective(payInfo);
PayRequest request = new PayRequest();
request.setOrderName("4559066-最好的支付sdk");
request.setOrderId(orderId);
request.setOrderAmount(amount.doubleValue());
request.setPayTypeEnum(bestPayTypeEnum);
PayResponse response = bestPayService.pay(request);
log.info("发起支付 response={}", response);
return response;
}
/**
* 异步通知处理:在方法内部,首先进行签名检验,
* 然后查询数据库中的订单信息,并校验订单金额。
* 如果校验通过,将修改订单支付状态,并通过消息队列发送支付信息。
* 最后,根据支付平台的不同,返回相应的响应结果
*
* @param notifyData
*/
@Override
public String asyncNotify(String notifyData) {
//1. 签名检验
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("异步通知 response={}", payResponse);
//2. 金额校验(从数据库查订单)
//比较严重(正常情况下是不会发生的)发出告警:钉钉、短信
PayInfo payInfo = payInfoMapper.selectByOrderNo(Long.parseLong(payResponse.getOrderId()));
if (payInfo == null) {
//告警
throw new RuntimeException("通过orderNo查询到的结果是null");
}
//如果订单支付状态不是"已支付"
if (!payInfo.getPlatformStatus().equals(OrderStatusEnum.SUCCESS.name())) {
//Double类型比较大小,精度。1.00 1.0
if (payInfo.getPayAmount().compareTo(BigDecimal.valueOf(payResponse.getOrderAmount())) != 0) {
//告警
throw new RuntimeException("异步通知中的金额和数据库里的不一致,orderNo=" + payResponse.getOrderId());
}
//3. 修改订单支付状态
payInfo.setPlatformStatus(OrderStatusEnum.SUCCESS.name());
payInfo.setPlatformNumber(payResponse.getOutTradeNo());
payInfoMapper.updateByPrimaryKeySelective(payInfo);
}
//TODO pay发送MQ消息,mall接受MQ消息
amqpTemplate.convertAndSend(QUEUE_PAY_NOTIFY, new Gson().toJson(payInfo));
if (payResponse.getPayPlatformEnum() == BestPayPlatformEnum.WX) {
//4. 告诉微信不要再通知了
return "<xml>\n" +
" <return_code><![CDATA[SUCCESS]]></return_code>\n" +
" <return_msg><![CDATA[OK]]></return_msg>\n" +
"</xml>";
}else if (payResponse.getPayPlatformEnum() == BestPayPlatformEnum.ALIPAY) {
return "success";
}
throw new RuntimeException("异步通知中错误的支付平台");
}
@Override
public PayInfo queryByOrderId(String orderId) {
return payInfoMapper.selectByOrderNo(Long.parseLong(orderId));
}
}
我会详细的讲讲创建/发起支付,处理异步通知和查询支付记录的意思。
创建/发起支付
首先支付平台会接受到用户的订单信息,然后将订单信息集成为一个对象,之后后端服务器会调用支付接口(调用接口其实就是让支付平台给用户一个二维码,让用户扫码支付,二维码中的内容就是一个链接,那要生成二维码需要哪个接口呢?其实这个接口名字叫:统一下单接口。在调用这个API时,要传递一些参数给这个API,所以说之前要将订单信息 集成为一个对象,原因就是这样!)
异步通知处理
首先,签名检验。对异步通知数据进行签名验证,确保通知的合法性;
其次,金额校验。从数据库中查询订单信息,并与异步通知中的订单金额进行比较校验。如果订单不存在,会触发告警并抛出异常。如果订单支付状态不是"已支付",还会比较订单金额与异步通知中的金额是否一致。如果金额不一致,同样会触发告警并抛出异常;
再而,修改订单支付状态。如果经过校验后,订单支付状态为非"已支付",则将订单的支付状态修改为"已支付",并更新支付平台的交易号;
接着,发送MQ消息。向消息队列(MQ)发送支付信息,以便其他模块(如mall接受MQ消息)可以进行相应的处理;
最后,返回不同支付平台的响应。如果是微信支付(BestPayPlatformEnum.WX
),返回一个XML格式的成功响应。如果是支付宝支付(BestPayPlatformEnum.ALIPAY
),返回字符串"success"。如果支付平台枚举不匹配任何已知支付平台,则抛出异常。
查询支付记录(通过订单号)
根据订单号查询数据库中的支付信息,并将查询结果(支付信息对象,比如支付状态、支付金额、订单号、支付时间等)返回
后记
这篇文章讲了支付系统的内部代码的实现,重点讲了关于service层业务逻辑。我们这个项目叫:电商+支付双系统项目,那么支付系统讲完了,接下来下一篇文章讲一下电商系统需要怎么设计!