支付模块 - 创建订单、查询订单、通知
文章目录
- 支付模块 - 创建订单、查询订单、通知
- 一、生成支付二维码
- 1.1 数据模型
- 1.1.1 订单表
- 1.1.2 订单明细表
- 1.1.3 支付交易记录表
- 1.2 执行流程
- 1.3 Dto
- 1.3.1 AddOrderDto 商品订单
- 1.3.2 PayRecordDto支付交易记录扩展字段
- 1.3.3 雪花算法工具类
- 1.4 生成订单信息
- 1.4.1 OrderController 接口
- 1.4 OrderService
- 1.4.1.1 保存订单
- 1.4.2.2 插入支付记录
- 1.4.3.3 生成支付二维码
- 1.4.4.4 效果图
- 1.5 用户扫码下单
- 1.5.1 OrderController
- 1.5.2 OrderService
- 二、支付结果查询
- 2.1 主动查询支付结果
- 2.1.1 OrderController
- 2.1.2 OrderService
- 2.2 通知
- 2.2.1 测试结果通知
- 2.2.2 支付通知
- 2.2.2.1 需求分析
- 2.2.2.2 技术方案
- 2.2.2.3 订单服务集成MQ
- 2.2.2.4 数据模型
- 2.2.2.5 生产方发送消息 - OrderServiceImpl
- 2.2.2.6 消费方消费消息
一、生成支付二维码
1.1 数据模型
订单支付模式的核心由三张表组成:订单表、订单明细表、支付交易记录表
简单解释:
订单表与订单明细表的含义,假如说一个人同时买了五件不同的商品,那再订单表中就会有一个订单,但订单明细表中就会有五个不同的明细,很好理解的
两个表的关联其实就是订单表的id(订单号)
订单表:记录订单信息
订单明细表记录订单的详细信息
支付交易记录表记录每次支付的交易明细
订单号注意唯一性、安全性、尽量短等特点,生成方案常用的如下:
1、时间戳+随机数
年月日时分秒毫秒+随机数
2、高并发场景
年月日时分秒毫秒+随机数+redis自增序列
3、订单号中加上业务标识
订单号加上业务标识方便客服,比如:第10位是业务类型,第11位是用户类型等。
4、雪花算法
雪花算法是推特内部使用的分布式环境下的唯一ID生成算法,它基于时间戳生成,保证有序递增,加以入计算机硬件等元素,可以满足高并发环境下ID不重复。
本项目订单号生成采用雪花算法。
1.1.1 订单表
订单表:记录订单信息
out_business_id外部系统业务id字段在这里其实指的就是选课表中的主键id
相当于将订单表和选课表关联起来了,能清楚此订单是买的哪门课
并且同一个选课记录只能有一个订单(out_business_id字段值的唯一性)
@Data
@ToString
@TableName("xc_orders")
public class XcOrders implements Serializable {private static final long serialVersionUID = 1L;/*** 订单号*/private Long id;/*** 总价*/private Float totalPrice;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createDate;/*** 交易状态*/private String status;/*** 用户id*/private String userId;/*** 订单类型*/private String orderType;/*** 订单名称*/private String orderName;/*** 订单描述*/private String orderDescrip;/*** 订单明细json*/private String orderDetail;/*** 外部系统业务id*/private String outBusinessId;}
1.1.2 订单明细表
订单明细表记录订单的详细信息
@Data
@ToString
@TableName("xc_orders_goods")
public class XcOrdersGoods implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 订单号*/private Long orderId;/*** 商品id*/private String goodsId;/*** 商品类型*/private String goodsType;/*** 商品名称*/private String goodsName;/*** 商品交易价,单位分*/private Float goodsPrice;/*** 商品详情json,可为空*/private String goodsDetail;}
1.1.3 支付交易记录表
支付交易记录表记录每次支付的交易明细
本系统支付交易号,将来会传给支付宝
@Data
@ToString
@TableName("xc_pay_record")
public class XcPayRecord implements Serializable {private static final long serialVersionUID = 1L;/*** 支付记录号*/private Long id;/*** 本系统支付交易号*/private Long payNo;/*** 第三方支付交易流水号*/private String outPayNo;/*** 第三方支付渠道编号*/private String outPayChannel;/*** 商品订单号*/private Long orderId;/*** 订单名称*/private String orderName;/*** 订单总价单位元*/private Float totalPrice;/*** 币种CNY*/private String currency;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createDate;/*** 支付状态*/private String status;/*** 支付成功时间*/private LocalDateTime paySuccessTime;/*** 用户id*/private String userId;}
1.2 执行流程
点击“支付宝支付”此时打开支付二维码,用户扫码支付。
所以首先需要生成支付二维码,用户扫描二维码开始请求支付宝下单,在向支付宝下单前需要添加选课记录、创建商品订单、生成支付交易记录。
生成二维码执行流程如下:
1、前端调用学习中心服务的添加选课接口。
2、添加选课成功请求订单服务生成支付二维码接口。
前端只要添加选课成功,就会调用订单服务生成支付二维码
3、生成二维码接口:创建商品订单、生成支付交易记录、生成二维码。
4、将二维码返回到前端,用户扫码。
用户扫码支付流程如下
1、用户输入支付密码,支付成功。
2、接收第三方平台通知的支付结果。
3、根据支付结果更新支付交易记录的支付状态为支付成功。
1.3 Dto
1.3.1 AddOrderDto 商品订单
/*** @description 创建商品订单 Dto*/
@Data
@ToString
public class AddOrderDto {/*** 总价*/private Float totalPrice;/*** 订单类型*/private String orderType;/*** 订单名称*/private String orderName;/*** 订单描述*/private String orderDescrip;/*** 订单明细json,不可为空* [{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]*/private String orderDetail;/*** 外部系统业务id*/private String outBusinessId;}
1.3.2 PayRecordDto支付交易记录扩展字段
其实多了一个二维码而已
/*** @author Mr.M* @version 1.0* @description 支付记录dto*/
@Data
@ToString
public class PayRecordDto extends XcPayRecord {private static final long serialVersionUID = -1780473178502369852L;//二维码private String qrcode;}
1.3.3 雪花算法工具类
public final class IdWorkerUtils {private static final Random RANDOM = new Random();private static final long WORKER_ID_BITS = 5L;private static final long DATACENTERIDBITS = 5L;private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTERIDBITS);private static final long SEQUENCE_BITS = 12L;private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTERIDBITS;private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);private static final IdWorkerUtils ID_WORKER_UTILS = new IdWorkerUtils();private long workerId;private long datacenterId;private long idepoch;private long sequence = '0';private long lastTimestamp = -1L;private IdWorkerUtils() {this(RANDOM.nextInt((int) MAX_WORKER_ID), RANDOM.nextInt((int) MAX_DATACENTER_ID), 1288834974657L);}private IdWorkerUtils(final long workerId, final long datacenterId, final long idepoch) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));}if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));}this.workerId = workerId;this.datacenterId = datacenterId;this.idepoch = idepoch;}/*** Gets instance.** @return the instance*/public static IdWorkerUtils getInstance() {return ID_WORKER_UTILS;}public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}if (lastTimestamp == timestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - idepoch) << TIMESTAMP_LEFT_SHIFT)| (datacenterId << DATACENTER_ID_SHIFT)| (workerId << WORKER_ID_SHIFT) | sequence;}private long tilNextMillis(final long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}private long timeGen() {return System.currentTimeMillis();}/*** Build part number string.** @return the string*/public String buildPartNumber() {return String.valueOf(ID_WORKER_UTILS.nextId());}/*** Create uuid string.** @return the string*/public String createUUID() {return String.valueOf(ID_WORKER_UTILS.nextId());}public static void main(String[] args) {System.out.println(IdWorkerUtils.getInstance().nextId());}
}
1.4 生成订单信息
1.4.1 OrderController 接口
/*** 生成支付二维码(主要是创建订单)*/
@ApiOperation("生成支付二维码")
@PostMapping("/generatepaycode")
@ResponseBody
public PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {// 拿到当前用户SecurityUtil.XcUser user = SecurityUtil.getUser();// 调用service,完成插入订单信息、插入支付记录、生成支付二维码return orderService.createOrder(user.getId(), addOrderDto);
}
1.4 OrderService
/*** @description 创建商品订单* @param userId* @param addOrderDto 订单信息* @return PayRecordDto 返回支付记录信息及支付二维码*/
public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto);
1.4.1.1 保存订单
插入订单表(订单表及订单明细表,两个表)
/*** @param userId* @param addOrderDto 订单信息* @return* @description 保存订单信息,插入订单表(订单表及订单明细表,两个表)*/
public XcOrders saveXcOrders(String userId, AddOrderDto addOrderDto) {// 进行幂等性判断,同一个选课记录只能有一个订单XcOrders xcOrders = this.getOrderByBusinessId(addOrderDto.getOutBusinessId());if (xcOrders != null) {// 说明已经创建订单了,我们直接将订单返回即可return xcOrders;}// 1. 插入订单表(订单表及订单明细表,两个表)// 1.1 插入订单表(主表)xcOrders = new XcOrders();//生成订单号long orderId = IdWorkerUtils.getInstance().nextId();xcOrders.setId(orderId); // 雪花算法生成的订单号xcOrders.setTotalPrice(addOrderDto.getTotalPrice()); //总金额xcOrders.setCreateDate(LocalDateTime.now()); //创建时间xcOrders.setStatus("600001");//未支付 交易状态xcOrders.setUserId(userId); //用户idxcOrders.setOrderType(addOrderDto.getOrderType()); //订单类型xcOrders.setOrderName(addOrderDto.getOrderName()); //订单名称xcOrders.setOrderDetail(addOrderDto.getOrderDetail());//订单详情xcOrders.setOrderDescrip(addOrderDto.getOrderDescrip());//订单描述xcOrders.setOutBusinessId(addOrderDto.getOutBusinessId());//选课记录idint insert = ordersMapper.insert(xcOrders);if (insert<=0){XueChengPlusException.cast("添加订单失败");}// 1.2 插入订单明细表// 装订单明细的JSON串转换成List集合形式String orderDetailJson = addOrderDto.getOrderDetail();List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetailJson, XcOrdersGoods.class);xcOrdersGoodsList.forEach(goods->{XcOrdersGoods xcOrdersGoods = new XcOrdersGoods();BeanUtils.copyProperties(goods,xcOrdersGoods);xcOrdersGoods.setOrderId(orderId);//订单号ordersGoodsMapper.insert(xcOrdersGoods);});return xcOrders;
}/*** @param businessId 外部系统业务id* @return 订单信息* @description 根据外部系统业务id获取订单信息*/
public XcOrders getOrderByBusinessId(String businessId) {LambdaQueryWrapper<XcOrders> lqw = new LambdaQueryWrapper<>();lqw.eq(XcOrders::getOutBusinessId, businessId);return ordersMapper.selectOne(lqw);
}
1.4.2.2 插入支付记录
为什么创建支付交易记录?
在请求微信或支付宝下单接口时需要传入 商品订单号,在与第三方支付平台对接时发现,当用户支付失败或因为其它原因最终该订单没有支付成功,此时再次调用第三方支付平台的下单接口发现报错“订单号已存在”,此时如果我们传入一个没有使用过的订单号就可以解决问题,但是商品订单已经创建,因为没有支付成功重新创建一个新订单是不合理的。
解决以上问题的方案是:
1、用户每次发起都创建一个新的支付交易记录 ,此交易记录与商品订单关联。
2、将支付交易记录的流水号传给第三方支付系统下单接口,这样就即使没有支付成功就不会出现上边的问题。
3、需要提醒用户不要重复支付。
/*** @param orders* @return 支付记录* @description 保存支付记录*/
public XcPayRecord createPayRecord(XcOrders orders) {// 订单idLong ordersId = orders.getId();XcOrders xcOrders = ordersMapper.selectById(ordersId);// 如果此订单不存在,则不能添加支付记录if (xcOrders == null) {XueChengPlusException.cast("订单不存在");}// 如果此订单支付结果为成功,也不能添加支付记录(避免重复支付)String status = xcOrders.getStatus();if ("601002".equals(status)) {// 支付成功XueChengPlusException.cast("此订单已支付");}// 添加支付记录XcPayRecord xcPayRecord = new XcPayRecord();long payNo = IdWorkerUtils.getInstance().nextId();xcPayRecord.setPayNo(payNo); //本系统支付交易号,将来会传给支付宝xcPayRecord.setOrderId(ordersId);//商品订单号(在本系统中存储的订单id)xcPayRecord.setOrderName(orders.getOrderName());//订单名称xcPayRecord.setTotalPrice(orders.getTotalPrice());//总价格xcPayRecord.setCurrency("CNY");//币种CNYxcPayRecord.setCreateDate(LocalDateTime.now());//支付记录创建时间xcPayRecord.setStatus("601001");//未支付 支付状态xcPayRecord.setUserId(orders.getUserId());//支付用户int insert = payRecordMapper.insert(xcPayRecord);if (insert<=0){XueChengPlusException.cast("插入支付记录失败");}return xcPayRecord;
}
1.4.3.3 生成支付二维码
其实就是第三部分
/*** @param userId* @param addOrderDto 订单信息* @return PayRecordDto 返回支付记录信息及支付二维码* @description 创建商品订单*/
@Transactional
@Override
public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {// 进行幂等性判断,同一个选课记录只能有一个订单// 1. 插入订单表(订单表及订单明细表,两个表)XcOrders orders = saveXcOrders(userId, addOrderDto);// 2. 插入支付记录表XcPayRecord payRecord = createPayRecord(orders);Long payNo = payRecord.getPayNo();//payRecord 本系统支付交易号,将来会传给支付宝// 3. 生成二维码并返回PayRecordDto payRecordDto = new PayRecordDto();QRCodeUtil qrCodeUtil = new QRCodeUtil();// http://192.168.101.1:63030/orders/requestpay路径就是我们服务的一个接口,当扫描二维码后就会携带参数请求这个接口,并且这个接口会访问支付宝创建支付订单// 这个地方可以配置到nacos上的订单服务里,我这里就不配置了try {String qrCode = qrCodeUtil.createQRCode("http://192.168.101.1:63030/orders/requestpay?payNo=" + payNo, 200, 200);payRecordDto.setQrcode(qrCode);//base64编码的形式} catch (IOException e) {XueChengPlusException.cast("生成二维码支付出错");}BeanUtils.copyProperties(payRecord,payRecordDto);return payRecordDto;
}
1.4.4.4 效果图
1.5 用户扫码下单
生成订单二维码之后用户就需要扫码
1.5.1 OrderController
@Value("${pay.alipay.APP_ID}")String APP_ID;@Value("${pay.alipay.APP_PRIVATE_KEY}")String APP_PRIVATE_KEY;@Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")String ALIPAY_PUBLIC_KEY;@ApiOperation("用户扫码下单接口")@GetMapping("/requestpay")public void requestPay(String payNo, HttpServletResponse httpResponse) throws IOException {//请求支付宝下单//如果payNo不存在则提示重新发起支付XcPayRecord payRecord = orderService.getPayRecordByPayno(payNo);if(payRecord == null){XueChengPlusException.cast("请重新点击支付获取二维码");}//支付状态String status = payRecord.getStatus();if("601002".equals(status)){XueChengPlusException.cast("订单已支付,请勿重复支付。");}//构造sdk的客户端对象AlipayClient client = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE);//获得初始化的AlipayClientAlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
// alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
// alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/paynotify");//在公共参数中设置回跳和通知地址alipayRequest.setBizContent("{" +" \"out_trade_no\":\""+payRecord.getPayNo()+"\"," +" \"total_amount\":\""+payRecord.getTotalPrice()+"\"," +" \"subject\":\""+payRecord.getOrderName()+"\"," +" \"product_code\":\"QUICK_WAP_PAY\"" +" }");//填充业务参数// " \"product_code\":\"QUICK_WAP_PAY\"" 固定写死String form = "";try {//请求支付宝下单接口,发起http请求form = client.pageExecute(alipayRequest).getBody(); //调用SDK生成表单} catch (AlipayApiException e) {e.printStackTrace();}httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面(JS代码)httpResponse.getWriter().flush();httpResponse.getWriter().close();}
1.5.2 OrderService
@Override
public XcPayRecord getPayRecordByPayno(String payNo) {return payRecordMapper.selectOne(new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));
}
二、支付结果查询
用户扫码支付后,我们怎么才能知道用户的支付成功了呢?
两种方式:主动查询支付结果、被动接收支付结果
我们其实不仅仅要完成查询支付结果,我们还要更新支付记录xc_pay_record表以及订单xc_orders表
2.1 主动查询支付结果
2.1.1 OrderController
@ApiOperation("查询支付结果")
@GetMapping("/payresult")
@ResponseBody
public PayRecordDto payresult(String payNo) throws IOException{return orderService.queryPayResult(payNo);
}
2.1.2 OrderService
@Value("${pay.alipay.APP_ID}")
String APP_ID;
@Value("${pay.alipay.APP_PRIVATE_KEY}")
String APP_PRIVATE_KEY;@Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
String ALIPAY_PUBLIC_KEY;@Autowired
OrderServiceImpl currentProxy;/*** 请求支付宝查询支付结果** @param payNo 支付记录id* @return 支付记录信息*/
@Override
public PayRecordDto queryPayResult(String payNo) {//1.查询支付结果PayStatusDto payStatusDto = queryPayResultFromAlipay(payNo);//2.当支付成功后更新支付记录表的支付状态以及订单表的状态// 非事物方法调用事物方法需要使用代理对象currentProxy.saveAliPayStatus(payStatusDto);//3.返回最新的支付记录信息XcPayRecord payRecordByPayno = getPayRecordByPayno(payNo);PayRecordDto payRecordDto = new PayRecordDto();BeanUtils.copyProperties(payRecordByPayno,payRecordDto);return payRecordDto;
}/*** 请求支付宝查询支付结果** @param payNo 支付交易号* @return 支付结果*/
public PayStatusDto queryPayResultFromAlipay(String payNo) {//请求支付宝查询支付结果AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //获得初始化的AlipayClientAlipayTradeQueryRequest request = new AlipayTradeQueryRequest();JSONObject bizContent = new JSONObject();//对于支付宝来说此字段的含义是商户订单号,对我们系统的含义是交易记录号bizContent.put("out_trade_no", payNo);request.setBizContent(bizContent.toString());AlipayTradeQueryResponse response = null;//支付宝返回的信息String body = null;try {response = alipayClient.execute(request);if (!response.isSuccess()) {//交易不成功XueChengPlusException.cast("请求支付查询查询失败");}} catch (AlipayApiException e) {log.error("请求支付宝查询支付结果异常:{}", e.toString(), e);XueChengPlusException.cast("请求支付查询查询失败");}Map<String, String> bodyMap = JSON.parseObject(body, Map.class);//解析支付结果PayStatusDto payStatusDto = new PayStatusDto();payStatusDto.setOut_trade_no(payNo);//商户订单号(支付记录号)payStatusDto.setTrade_no(bodyMap.get("trade_no"));//支付宝交易号payStatusDto.setTrade_status(bodyMap.get("trade_status"));//交易状态payStatusDto.setApp_id(APP_ID);payStatusDto.setTotal_amount(bodyMap.get("total_amount"));//支付总金额return payStatusDto;}/*** @param payStatusDto 支付结果信息* @return void* @description 保存支付宝支付结果* @author Mr.M* @date 2022/10/4 16:52*/
@Transactional
public void saveAliPayStatus(PayStatusDto payStatusDto) {//我们已经拿到了支付结果,我们需要把支付结果保存到数据库String payNo = payStatusDto.getOut_trade_no(); //支付记录号//1.xc_pay_record支付记录表,更新此表中某笔订单的支付状态(如果支付成功,更新支付记录表的状态为已支付XcPayRecord xcPayRecord = payRecordMapper.selectById(payNo);if (xcPayRecord == null) {XueChengPlusException.cast("找不到相关的支付记录");}//相关联的订单idLong orderId = xcPayRecord.getOrderId();XcOrders orders = ordersMapper.selectById(orderId);if (orders == null) {XueChengPlusException.cast("找不到相关联的订单");}//支付状态String status = xcPayRecord.getStatus();if ("601002".equals(status)) {//如果已经成功了,便不再处理return;}//2.xc_orders订单表 更新此表中某笔订单的支付状态(如果支付成功,更新订单表的状态为已支付String trade_status = payStatusDto.getTrade_status();//来自支付宝if ("TRADE_SUCCESS".equals(trade_status)) {//支付表平台返回的字段表示支付成功//更新支付记录xcPayRecord.setStatus("601002");xcPayRecord.setOutPayNo(payStatusDto.getTrade_no());//支付宝交易号xcPayRecord.setOutPayChannel("Alipay");//支付渠道xcPayRecord.setPaySuccessTime(LocalDateTime.now());//通知时间payRecordMapper.updateById(xcPayRecord);//更新订单的状态orders.setStatus("601002");//支付成功ordersMapper.updateById(orders);}}
2.2 通知
被动接收支付结果
对于手机网站支付产生的交易,支付宝会通知商户支付结果,有两种通知方式,通过return_url、notify_url进行通知,使用return_url不能保证通知到位,推荐使用notify_url完成支付结构通知
支付完成后第三方支付系统会主动通知支付结果,要实现主动通知需要在请求支付系统下单时传入NotifyUrl,
这里有两个url:ReturnUrl和NotifyUrl
ReturnUrl是支付完成后支付系统携带支付结果重定向到ReturnUrl地址
NotifyUrl是支付完成后支付系统在后台定时去通知,使用NotifyUrl比使用ReturnUrl有保证
首先在下单这里填写通知地址
2.2.1 测试结果通知
@ApiOperation("接收支付结果通知")
@PostMapping("/receivenotify")
public void receivenotify(HttpServletRequest request, HttpServletResponse response) throws IOException, AlipayApiException {Map<String, String> params = new HashMap<String, String>();Map requestParams = request.getParameterMap();for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {String name = (String) iter.next();String[] values = (String[]) requestParams.get(name);String valueStr = "";for (int i = 0; i < values.length; i++) {valueStr = (i == values.length - 1) ? valueStr + values[i]: valueStr + values[i] + ",";}params.put(name, valueStr);}//验签boolean verify_result = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, "RSA2");if (verify_result) {//验证成功//商户订单号String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");//支付宝交易号String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");//交易状态String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");//appidString app_id = new String(request.getParameter("app_id").getBytes("ISO-8859-1"), "UTF-8");//total_amountString total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");//交易成功处理if (trade_status.equals("TRADE_SUCCESS")) {//更新支付记录表的支付状态为成功,以及订单表的状态为成功PayStatusDto payStatusDto = new PayStatusDto();payStatusDto.setTrade_status(trade_status);payStatusDto.setTrade_no(trade_no);payStatusDto.setOut_trade_no(out_trade_no);payStatusDto.setApp_id(app_id);payStatusDto.setTotal_amount(total_amount);orderService.saveAliPayStatus(payStatusDto);}response.getWriter().write("success");} else {//验证失败response.getWriter().write("fail");}}
2.2.2 支付通知
支付服务要把消息通过消息队列通知到学习中心服务
跨微服务操作
订单服务作为通用服务在订单支付成功后需要将支付结果异步通知给其它微服务
2.2.2.1 需求分析
下图使用了消息队列完成支付结果通知
学习中心服务:对收费课程选课需要支付,与订单服务对接完成支付
学习资源服务:对收费的学习资料需要购买后下载,与订单服务对接完成支付
订单服务完成支付后将支付结果发给每一个与订单服务对接的微服务,订单服务将消息发给交换机,由交换机广播消息,每个订阅消息的微服务都可以接收到支付结果.
微服务收到支付结果根据订单的类型去更新自己的业务数据
2.2.2.2 技术方案
使用消息队列进行异步通知需要保证消息的可靠性,即生产端将消息成功通知到消费端
消息从生产端发送到消费端经历了如下过程:
1、消息发送到交换机
2、消息由交换机发送到队列
3、消息者收到消息进行处理
保证消息的可靠性需要保证以上过程的可靠性,本项目使用RabbitMQ可以通过如下方面保证消息的可靠性。
1、生产者确认机制
发送消息前使用数据库事务将消息保证到数据库表中
成功发送到交换机将消息从数据库中删除
2、mq持久化
mq收到消息进行持久化,当mq重启即使消息没有消费完也不会丢失
需要配置交换机持久化、队列持久化、发送消息时设置持久化
3、消费者确认机制
消费者消费成功自动发送ack,否则重试消费。
2.2.2.3 订单服务集成MQ
订单服务通过消息队列将支付结果发给学习中心服务,消息队列采用发布订阅模式。
1、订单服务创建支付结果通知交换机。
2、学习中心服务绑定队列到交换机。
项目使用RabbitMQ作为消息队列,在课前下发的虚拟上已经安装了RabbitMQ.
执行docker start rabbitmq 启动RabbitMQ。
访问:http://192.168.101.65:15672/
账户密码:guest/guest
交换机为Fanout广播模式。
首先需要在学习中心服务和订单服务工程配置连接消息队列
订单服务添加消息队列依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
生产端(支付服务)和消费端(学习中心服务)都需要加mq的坐标
在nacos配置rabbitmq-dev.yaml为通用配置文件
spring:rabbitmq:host: 192.168.101.65port: 5672username: guestpassword: guestvirtual-host: /publisher-confirm-type: correlated #correlated 异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallbackpublisher-returns: false #开启publish-return功能,同样是基于callback机制,需要定义ReturnCallbacktemplate:mandatory: false #定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息listener:simple:acknowledge-mode: none #出现异常时返回unack,消息回滚到mq;没有异常,返回ack ,manual:手动控制,none:丢弃消息,不回滚到mqretry:enabled: true #开启消费者失败重试initial-interval: 1000ms #初识的失败等待时长为1秒multiplier: 1 #失败的等待时长倍数,下次等待时长 = multiplier * last-intervalmax-attempts: 3 #最大重试次数stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
在订单服务接口工程引入rabbitmq-dev.yaml配置文件
shared-configs:- data-id: rabbitmq-${spring.profiles.active}.yamlgroup: xuecheng-plus-commonrefresh: true
在订单服务service工程编写MQ配置类,配置交换机
同样的代码也要在消费端拷贝一份
为什么生产端和消费端都需要这个配置文件?
消费端要监听队列,假如消费端先启动,但是队列还没有创建完成,那指定会报错,所以我们要做的是不管消费端先启动还是生产端先启动,都不会报错
也就是说消费端先启动的时候,也会在mq中创建交换机、队列并且会绑定
生产端起来也会在mq中创建交换机、队列并且会绑定
假如说一方在创建的时候发现创建了,那就不会再创建了,避免了重复创建
/**
* 消费端不用实现ApplicationContextAware接口
**/
@Slf4j
@Configuration
public class PayNotifyConfig implements ApplicationContextAware {//交换机public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";//支付结果通知消息类型public static final String MESSAGE_TYPE = "payresult_notify";//支付通知队列public static final String PAYNOTIFY_QUEUE = "paynotify_queue";//声明交换机,且持久化@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_fanout() {// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}//支付通知队列,且持久化@Bean(PAYNOTIFY_QUEUE)public Queue course_publish_queue() {return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();}//交换机和支付通知队列绑定@Beanpublic Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {return BindingBuilder.bind(queue).to(exchange);}/*** 生产端的确认,消费端可以没有下面这个方法**/@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {// 获取RabbitTemplateRabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//消息处理serviceMqMessageService mqMessageService = applicationContext.getBean(MqMessageService.class);// 设置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {// 投递失败,记录日志log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());MqMessage mqMessage = JSON.parseObject(message.toString(), MqMessage.class);//将消息再添加到消息表mqMessageService.addMessage(mqMessage.getMessageType(), mqMessage.getBusinessKey1(), mqMessage.getBusinessKey2(), mqMessage.getBusinessKey3());});}
}
2.2.2.4 数据模型
@Data
@ToString
@TableName("mq_message")
public class MqMessage implements Serializable {private static final long serialVersionUID = 1L;/*** 消息id*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 消息类型代码: course_publish , media_test,*/private String messageType;/*** 关联业务信息*/private String businessKey1;/*** 关联业务信息*/private String businessKey2;/*** 关联业务信息*/private String businessKey3;/*** 执行次数*/private Integer executeNum;/*** 处理状态,0:初始,1:成功*/private String state;/*** 回复失败时间*/private LocalDateTime returnfailureDate;/*** 回复成功时间*/private LocalDateTime returnsuccessDate;/*** 回复失败内容*/private String returnfailureMsg;/*** 最近执行时间*/private LocalDateTime executeDate;/*** 阶段1处理状态, 0:初始,1:成功*/private String stageState1;/*** 阶段2处理状态, 0:初始,1:成功*/private String stageState2;/*** 阶段3处理状态, 0:初始,1:成功*/private String stageState3;/*** 阶段4处理状态, 0:初始,1:成功*/private String stageState4;}
2.2.2.5 生产方发送消息 - OrderServiceImpl
生产方发送支付结果,那生产方什么时候发送呢?
支付成功后发送消息
/*** @param payStatusDto 支付结果信息* @return void* @description 保存支付宝支付结果* @author Mr.M* @date 2022/10/4 16:52*/@Transactionalpublic void saveAliPayStatus(PayStatusDto payStatusDto) {//我们已经拿到了支付结果,我们需要把支付结果保存到数据库String payNo = payStatusDto.getOut_trade_no(); //支付记录号//1.xc_pay_record支付记录表,更新此表中某笔订单的支付状态(如果支付成功,更新支付记录表的状态为已支付XcPayRecord xcPayRecord = payRecordMapper.selectById(payNo);if (xcPayRecord == null) {XueChengPlusException.cast("找不到相关的支付记录");}//相关联的订单idLong orderId = xcPayRecord.getOrderId();XcOrders orders = ordersMapper.selectById(orderId);if (orders == null) {XueChengPlusException.cast("找不到相关联的订单");}//支付状态String status = xcPayRecord.getStatus();if ("601002".equals(status)) {//如果已经成功了,便不再处理return;}//2.xc_orders订单表 更新此表中某笔订单的支付状态(如果支付成功,更新订单表的状态为已支付String trade_status = payStatusDto.getTrade_status();//来自支付宝if ("TRADE_SUCCESS".equals(trade_status)) {//支付表平台返回的字段表示支付成功//更新支付记录xcPayRecord.setStatus("601002");xcPayRecord.setOutPayNo(payStatusDto.getTrade_no());//支付宝交易号xcPayRecord.setOutPayChannel("Alipay");//支付渠道xcPayRecord.setPaySuccessTime(LocalDateTime.now());//通知时间payRecordMapper.updateById(xcPayRecord);//更新订单的状态orders.setStatus("601002");//支付成功ordersMapper.updateById(orders);//将消息写到数据库 进行持久化//保存消息记录,参数1:支付结果通知类型,2: 业务id,3:业务类型MqMessage mqMessage = messageService.addMessage("payresult_notify", orders.getOutBusinessId(), orders.getOrderType(), null);//发送消息notifyPayResult(mqMessage);}}// MQ的模板类@AutowiredRabbitTemplate rabbitTemplate;@AutowiredMqMessageService messageService;/*** 发送消息* @param message 消息*/@Overridepublic void notifyPayResult(MqMessage message) {//消息内容String jsonString = JSON.toJSONString(message);//消息本身 指定字符编码是UTF-8Message messageObj = MessageBuilder.withBody(jsonString.getBytes(StandardCharsets.UTF_8))//设置消息的投递类型-持久化.setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();//全局消息idLong id = message.getId();CorrelationData correlationData = new CorrelationData(id.toString());//使用它就可以指定回调方法correlationData.getFuture().addCallback(result -> {if (result.isAck()) {//表示消息已经成功发送到了交换机log.info("发送消息成功:{}", jsonString);//删除数据库消息表中的消息messageService.completed(id);} else {//消息发送到交换机失败log.info("发送消息失败:{}", jsonString);}}, ex -> {//发送消息失败(发生异常了)log.info("发送消息异常:{}", jsonString);});//参数1:交换机名字; 参数2:广播都发空字符串; 参数3:消息本身;消息4:发送消息后的回调rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", messageObj, correlationData);}
2.2.2.6 消费方消费消息
这其实就是学习中心服务中的内容
shared-configs:- data-id: rabbitmq-${spring.profiles.active}.yamlgroup: xuecheng-plus-commonrefresh: true
/*** 接收消息通知*/
@Slf4j
@Service
public class ReceivePayNotifyService {@Autowiredprivate RabbitTemplate rabbitTemplate;@AutowiredMqMessageService mqMessageService;@AutowiredMyCourseTablesService myCourseTablesService;//监听消息队列接收支付结果通知@RabbitListener(queues = PayNotifyConfig.PAYNOTIFY_QUEUE)public void receive(Message message, Channel channel) {try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}//获取消息(解析消息并获取出对象)MqMessage mqMessage = JSON.parseObject(message.getBody(), MqMessage.class);log.debug("学习中心服务接收支付结果:{}", mqMessage);//根据消息内容更新选课记录表//消息类型String messageType = mqMessage.getMessageType();//订单类型,60201表示购买课程String businessKey2 = mqMessage.getBusinessKey2();//这里只处理支付结果通知if (PayNotifyConfig.MESSAGE_TYPE.equals(messageType) && "60201".equals(businessKey2)) {//选课记录idString choosecourseId = mqMessage.getBusinessKey1();//添加选课boolean b = myCourseTablesService.saveChooseCourseStauts(choosecourseId);if (!b) {//添加选课失败,抛出异常,消息重回队列XueChengPlusException.cast("收到支付结果,添加选课失败");}}}
}
MyCourseTablesServiceImpl
/*** 保存选课为成功** @param choosecourseId 选课id* @return*/
@Override
public boolean saveChooseCourseStauts(String choosecourseId) {//根据选课id查询选课表XcChooseCourse chooseCourse = xcChooseCourseMapper.selectById(choosecourseId);if (chooseCourse == null) {log.debug("接收购买课程的消息,根据选课id从数据库中找不到选课记录,选课id{}", choosecourseId);return false;}String status = chooseCourse.getStatus();//只有当未支付时才更新为已支付if ("701002".equals(status)) {//更新选课记录的状态为支付成功chooseCourse.setStatus("701001");xcChooseCourseMapper.updateById(chooseCourse);//向我的课程表插入记录addCourseTables(chooseCourse);}return true;
}
.class);log.debug("学习中心服务接收支付结果:{}", mqMessage);//根据消息内容更新选课记录表//消息类型String messageType = mqMessage.getMessageType();//订单类型,60201表示购买课程String businessKey2 = mqMessage.getBusinessKey2();//这里只处理支付结果通知if (PayNotifyConfig.MESSAGE_TYPE.equals(messageType) && "60201".equals(businessKey2)) {//选课记录idString choosecourseId = mqMessage.getBusinessKey1();//添加选课boolean b = myCourseTablesService.saveChooseCourseStauts(choosecourseId);if (!b) {//添加选课失败,抛出异常,消息重回队列XueChengPlusException.cast("收到支付结果,添加选课失败");}}}
}
MyCourseTablesServiceImpl
/*** 保存选课为成功** @param choosecourseId 选课id* @return*/
@Override
public boolean saveChooseCourseStauts(String choosecourseId) {//根据选课id查询选课表XcChooseCourse chooseCourse = xcChooseCourseMapper.selectById(choosecourseId);if (chooseCourse == null) {log.debug("接收购买课程的消息,根据选课id从数据库中找不到选课记录,选课id{}", choosecourseId);return false;}String status = chooseCourse.getStatus();//只有当未支付时才更新为已支付if ("701002".equals(status)) {//更新选课记录的状态为支付成功chooseCourse.setStatus("701001");xcChooseCourseMapper.updateById(chooseCourse);//向我的课程表插入记录addCourseTables(chooseCourse);}return true;
}