7.3 支付模块 - 创建订单、查询订单、通知

支付模块 - 创建订单、查询订单、通知

文章目录

  • 支付模块 - 创建订单、查询订单、通知
  • 一、生成支付二维码
    • 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 数据模型

订单支付模式的核心由三张表组成:订单表、订单明细表、支付交易记录表

image-20240222225814857

简单解释:

订单表与订单明细表的含义,假如说一个人同时买了五件不同的商品,那再订单表中就会有一个订单,但订单明细表中就会有五个不同的明细,很好理解的

两个表的关联其实就是订单表的id(订单号)

订单表:记录订单信息

image-20240222225830823

订单明细表记录订单的详细信息

image-20240222225845972

支付交易记录表记录每次支付的交易明细

image-20240222225859647

订单号注意唯一性、安全性、尽量短等特点,生成方案常用的如下

1、时间戳+随机数

年月日时分秒毫秒+随机数

2、高并发场景

年月日时分秒毫秒+随机数+redis自增序列

3、订单号中加上业务标识

订单号加上业务标识方便客服,比如:第10位是业务类型,第11位是用户类型等。

4、雪花算法

雪花算法是推特内部使用的分布式环境下的唯一ID生成算法,它基于时间戳生成,保证有序递增,加以入计算机硬件等元素,可以满足高并发环境下ID不重复。

本项目订单号生成采用雪花算法

1.1.1 订单表

订单表:记录订单信息

image-20240222225830823

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 订单明细表

订单明细表记录订单的详细信息

image-20240222225845972

@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 支付交易记录表

支付交易记录表记录每次支付的交易明细

本系统支付交易号,将来会传给支付宝

image-20240222225859647

@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 执行流程

image-20240222225315579

点击“支付宝支付”此时打开支付二维码,用户扫码支付。

所以首先需要生成支付二维码,用户扫描二维码开始请求支付宝下单,在向支付宝下单前需要添加选课记录、创建商品订单、生成支付交易记录。

生成二维码执行流程如下

image-20240222225349868

1、前端调用学习中心服务的添加选课接口。

2、添加选课成功请求订单服务生成支付二维码接口。

前端只要添加选课成功,就会调用订单服务生成支付二维码

3、生成二维码接口:创建商品订单、生成支付交易记录、生成二维码。

4、将二维码返回到前端,用户扫码。

用户扫码支付流程如下

image-20240222225445869

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、需要提醒用户不要重复支付。

image-20240227222055256

/*** @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 效果图

image-20240227230151644

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

二、支付结果查询

用户扫码支付后,我们怎么才能知道用户的支付成功了呢?

image-20240228213946063

两种方式:主动查询支付结果、被动接收支付结果

我们其实不仅仅要完成查询支付结果,我们还要更新支付记录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完成支付结构通知

image-20240229223542689

支付完成后第三方支付系统会主动通知支付结果,要实现主动通知需要在请求支付系统下单时传入NotifyUrl,

这里有两个url:ReturnUrl和NotifyUrl

ReturnUrl是支付完成后支付系统携带支付结果重定向到ReturnUrl地址

NotifyUrl是支付完成后支付系统在后台定时去通知,使用NotifyUrl比使用ReturnUrl有保证

首先在下单这里填写通知地址

image-20240229225333471

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的坐标

image-20240304222045181

在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

image-20240304222145461

在订单服务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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/727593.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习——感知机模型

机器学习系列文章 入门必读&#xff1a;机器学习介绍 文章目录 机器学习系列文章前言1. 感知机1.1 感知机定义1.2 感知机学习策略 2. 代码实现2.1 构建数据2.2 编写函数2.3 迭代 3. 总结 前言 大家好&#xff0c;大家好✨&#xff0c;这里是bio&#x1f996;。这次为大家带来…

基于springboot+vue的在线远程考试系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

亚马逊使用什么国外代理IP?跨境电商代理IP推荐

代理IP作为网络活动的有力工具&#xff0c;同时也是跨境电商的必备神器。亚马逊作为跨境电商的头部平台&#xff0c;吸引了大量的跨境电商玩家入驻&#xff0c;想要做好亚马逊&#xff0c;养号、测评都需要代理IP的帮助。那么应该使用什么代理IP呢&#xff1f;如何使用&#xf…

钡铼技术R40工业路由器稳定可靠支持环境检测应用

在现代工业化进程中&#xff0c;环境监测已经成为确保生产安全、提升环保效能的关键环节。而在这个领域中&#xff0c;钡铼技术的R40工业路由器以其卓越的稳定性和可靠性&#xff0c;在环境检测应用中发挥着至关重要的作用。 首先&#xff0c;钡铼技术R40工业路由器采用了先进…

水下蓝牙耳机有哪些?绝对物有所值的4大游泳耳机分享!

随着科技的不断进步&#xff0c;运动爱好者们对于耳机的需求也在不断提升。在众多运动场景中&#xff0c;游泳无疑是最为特别的一个。水下蓝牙耳机的出现&#xff0c;不仅解决了传统耳机无法防水的问题&#xff0c;更让游泳者可以在享受音乐的同时进行锻炼。然而&#xff0c;在…

【Spring底层原理高级进阶】Spring Batch清洗和转换数据,一键处理繁杂数据!Spring Batch是如何实现IO流优化的?本文详解!

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

论文阅读_世界模型

1 2 3 4 5 6 7 8英文名称: World Models 中文名称: 世界模型 链接: https://arxiv.org/abs/1803.10122 示例: https://worldmodels.github.io/ 作者: David Ha, Jurgen Schmidhuber 机构: Google Brain, NNAISENSE, Swiss AI Lab, IDSIA (USI & SUPSI) 日期: 27 Mar 2018 引…

【MetaGPT】多智能体协作——你画我猜(文字版)

多智能体协作 本篇将学习 MetaGPT中的 Environment 、 Team 组件。 1. Muti Agent 概念概述 多智能体系统 (Multi-Agent System, MAS) 是由一群具有一定自主性、协同性和学习能力的智能体组成的系统。智能体在环境中相互协作&#xff0c;以达到某种目标或完成特定任务。 2. 多…

阿珊解说Vue中`$route`和`$router`的区别

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

ResponseStatusException

目录 概述&#xff1a; 综合实例&#xff1a; 继承 ResponseStatusException-自定义异常类 继承 ResponseStatusException-自定义响应头信息 继承 ResponseStatusException-定制更多异常处理逻辑 继承 ResponseStatusException-根据异常发生的上下文动态改变 HTTP 状态码…

C++之类(一)

1&#xff0c;封装 1.1 封装的引用 封装是C面向对象三大特性之一 封装的意义&#xff1a; 将属性和行为作为一个整体&#xff0c;表现生活中的事物 将属性和行为加以权限控制 1.1.1 封装意义一&#xff1a; 在设计类的时候&#xff0c;属性和行为写在一起&#xff0c;表…

事务失效的八种情况!!!!

一、非publi修饰的方法。 /*** 私有方法上的注解&#xff0c;不生效&#xff08;因私有方法Spring扫描不到该方法&#xff0c;所以无法生成代理&#xff09;*/ Transactional private boolean test() {//test code }二、类内部访问。 类内部非直接访问带注解标记的方法 B&…

类初步认识与对象

一&#xff0c;对于面向对象的认识 Java是一门面向对象的语言&#xff0c;一切都可以称为对象。将一个大象装进冰箱&#xff0c;甭管步骤多复杂&#xff0c;大象便是对象&#xff1b;将牛奶放进冰箱&#xff0c;牛奶便是对象&#xff1b;你我均是对像。 再比如洗一个衣服&…

如何在Linux中安装ARM交叉环境编译链

安装ARM交叉环境编译链过程如下&#xff1a; 首先创建一个文件夹如下&#xff1a; mkdir -p Linux_ALPHA/toolcahin然后将arm交叉编译工具链安装包拖到Linux中如下&#xff1a; 先输入mv 拖入的安装包即可 mv /var/run/vmblock-fuse/blockdir/pXeysK/gcc-4.6.4.tar.xz .直接…

进程:守护进程

一、守护进程的概念 守护进程是脱离于终端控制&#xff0c;且运行在后端的进程。&#xff08;孤儿进程&#xff09;守护进程不会将信息显示在任何终端上影响前端的操作&#xff0c;也不会被终端产生的任何信息打断&#xff0c;例如&#xff08;ctrlc&#xff09;.守护进程独立…

【数据结构】哈希

在一个数据序列中查找某一个数据元素&#xff0c;是数据管理时经常涉及的&#xff0c;通常以比较的方式来完成&#xff0c;典型的案例有无序序列的暴力查找&#xff08;O(N)&#xff09;、有序序列的二分查找&#xff08;O(logN)&#xff09;、平衡搜索树&#xff08;O(logN)&a…

融合软硬件串流多媒体技术的远程控制方案

远程技术已经发展得有相当水平了&#xff0c;在远程办公&#xff0c;云游戏&#xff0c;云渲染等领域有相当多的应用场景&#xff0c;以向日葵&#xff0c;todesk rustdesk等优秀产品攻城略地&#xff0c;估值越来越高。占据了通用应用的方方面面。 但是细分市场&#xff0c;还…

试用Claude3

1 简介 好消息是&#xff0c;2024 年 3 月 4 日发布了 Claude3&#xff0c;据传比 GPT-4 更好&#xff0c;snooet 版本可以免费试用&#xff0c;坏消息是我们这儿不能用。 在官网注册时&#xff0c;需要选择国家并使用手机接收短信验证码。而在选项中没有中国这个选项。即使成…

IT外包怎样帮助企业控制成本?

在当今激烈的商业竞争中&#xff0c;企业不仅需要保持创新&#xff0c;还需要有效控制成本。IT外包作为一种管理模式&#xff0c;成为许多企业降低成本的得力工具。究竟IT外包如何帮助企业控制成本呢&#xff1f; 首先&#xff0c;IT外包在减少人力资源成本方面发挥了至关重要的…

【微服务生态】Nginx

文章目录 一、概述二、Nginx 的安装三、常用命令四、Nginx 配置4.1 反向代理配置&#xff08;1&#xff09;反向代理实例1&#xff08;2&#xff09;反向代理实例2 4.2 负载均衡配置4.3 动静分离4.4 集群配置 五、nginx 原理与优化参数配置 一、概述 本次为简易版&#xff0c;…