asp.net 网站开发 pdf,建设通同类网站,室内设计效果图平面图,汪峰做的音乐网站支付模块 - 创建订单、查询订单、通知 文章目录 支付模块 - 创建订单、查询订单、通知一、生成支付二维码1.1 数据模型1.1.1 订单表1.1.2 订单明细表1.1.3 支付交易记录表 1.2 执行流程1.3 Dto1.3.1 AddOrderDto 商品订单1.3.2 PayRecordDto支付交易记录扩展字段1.3.3 雪花算法…支付模块 - 创建订单、查询订单、通知 文章目录 支付模块 - 创建订单、查询订单、通知一、生成支付二维码1.1 数据模型1.1.1 订单表1.1.2 订单明细表1.1.3 支付交易记录表 1.2 执行流程1.3 Dto1.3.1 AddOrderDto 商品订单1.3.2 PayRecordDto支付交易记录扩展字段1.3.3 雪花算法工具类 1.4 生成订单信息1.4.1 OrderController 接口1.4 OrderService1.4.1.1 保存订单1.4.2.2 插入支付记录1.4.3.3 生成支付二维码1.4.4.4 效果图 1.5 用户扫码下单1.5.1 OrderController1.5.2 OrderService 二、支付结果查询2.1 主动查询支付结果2.1.1 OrderController2.1.2 OrderService 2.2 通知2.2.1 测试结果通知2.2.2 支付通知2.2.2.1 需求分析2.2.2.2 技术方案2.2.2.3 订单服务集成MQ2.2.2.4 数据模型2.2.2.5 生产方发送消息 - OrderServiceImpl2.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 cant 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 cant 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 (insert0){XueChengPlusException.cast(添加订单失败);}// 1.2 插入订单明细表// 装订单明细的JSON串转换成List集合形式String orderDetailJson addOrderDto.getOrderDetail();ListXcOrdersGoods 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) {LambdaQueryWrapperXcOrders 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);//商品订单号在本系统中存储的订单idxcPayRecord.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 (insert0){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 LambdaQueryWrapperXcPayRecord().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(请求支付查询查询失败);}MapString, 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
这里有两个urlReturnUrl和NotifyUrl
ReturnUrl是支付完成后支付系统携带支付结果重定向到ReturnUrl地址
NotifyUrl是支付完成后支付系统在后台定时去通知使用NotifyUrl比使用ReturnUrl有保证 首先在下单这里填写通知地址 2.2.1 测试结果通知
ApiOperation(接收支付结果通知)
PostMapping(/receivenotify)
public void receivenotify(HttpServletRequest request, HttpServletResponse response) throws IOException, AlipayApiException {MapString, String params new HashMapString, 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广播模式。
首先需要在学习中心服务和订单服务工程配置连接消息队列
订单服务添加消息队列依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-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 异步回调定义ConfirmCallbackMQ返回结果时会回调这个ConfirmCallbackpublisher-returns: false #开启publish-return功能同样是基于callback机制需要定义ReturnCallbacktemplate:mandatory: false #定义消息路由失败时的策略。true则调用ReturnCallbackfalse则直接丢弃消息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: 业务id3:业务类型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;
}