RabbitMQ学习模拟超时支付场景代码(ASP.NET Core)

一.项目结构

    

二 类库Infrastructure代码

添加nuget引用:RabbitMQ.Client,引用项目Rabbit.Entities

1.RabbitOption.cs代码

using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Config
{
    public class RabbitOption
    {
        /// <summary>
        /// 主机名称
        /// </summary>
        public string HostName { get; set; }
        public string Address { get; set; }
        /// <summary>
        /// 端口号
        /// </summary>
        public int Port { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }

        public string VirtualHost { get; set; }
    }
}

2.RabbitConnection.cs代码

using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Config
{
    public class RabbitConnection
    {
        private readonly RabbitOption _config;
        private IConnection _connection = null;
        public RabbitConnection(RabbitOption rabbitOption)
        {
            _config = rabbitOption;
        }

        public IConnection GetConnection()
        {
            if (_connection == null)
            {
                if (string.IsNullOrEmpty(_config.Address))
                {
                    ConnectionFactory factory = new ConnectionFactory();
                    factory.HostName = _config.HostName;
                    factory.Port = _config.Port;
                    factory.UserName = _config.UserName;
                    factory.Password = _config.Password;
                    factory.VirtualHost = _config.VirtualHost;
                    _connection = factory.CreateConnection();
                }
                else
                {
                    ConnectionFactory factory = new ConnectionFactory();
                    factory.UserName = _config.UserName;
                    factory.Password = _config.Password;
                    factory.VirtualHost = _config.VirtualHost;

                    var address = _config.Address;
                    List<AmqpTcpEndpoint> endpoints = new List<AmqpTcpEndpoint>();
                    foreach (var endpoint in address.Split(","))
                    {
                        endpoints.Add(new AmqpTcpEndpoint(endpoint.Split(":")[0],int.Parse(endpoint.Split(":")[1])));
                    }
                    _connection = factory.CreateConnection(endpoints);
                }
            }
            return _connection;
        }
    }
}

3.RabbitConstant.cs代码

using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Config
{
    public class RabbitConstant
    {
        public const string TEST_EXCHANGE = "test.exchange";
        public const string TEST_QUEUE = "test.queue";

        public const string DELAY_EXCHANGE = "delay.exchange";
        public const string DELAY_ROUTING_KEY = "delay.routing.key";
        public const string DELAY_QUEUE = "delay.queue";

        public const string DEAD_LETTER_EXCHANGE = "dead.letter.exchange";
        public const string DEAD_LETTER_QUEUE = "dead.letter.queue";
        public const string DEAD_LETTER_ROUTING_KEY = "dead.letter.routing.key";
    }
}

4.QueueInfo.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Consumer
{
    public class QueueInfo
    {
        /// <summary>
        /// 队列名称
        /// </summary>
        public string Queue { get; set; }
        /// <summary>
        /// 路由名称
        /// </summary>
        public string RoutingKey { get; set; }
        /// <summary>
        /// 交换机类型
        /// </summary>
        public string ExchangeType { get; set; }
        /// <summary>
        /// 交换机名称
        /// </summary>
        public string Exchange { get; set; }
        public IDictionary<string, object> props { get; set; } = null;
        public Action<RabbitMessageEntity> OnRecevied { get; set; }
    }
}

5.RabbitChannelConfig.cs

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Consumer
{
    public class RabbitChannelConfig
    {
        public string ExchangeTypeName { get; set; }
        public string ExchangeName { get; set; }
        public string QueueName { get; set; }
        public string RoutingKeyName { get; set; }
        public IConnection Connection { get; set; }
        public EventingBasicConsumer Consumer { get; set; }

        /// <summary>
        /// 外部订阅消费者通知委托
        /// </summary>
        public Action<RabbitMessageEntity> OnReceivedCallback { get; set; }

        public RabbitChannelConfig(string exchangeType,string exchange,string queue,string routingKey)
        {
            this.ExchangeTypeName = exchangeType;
            this.ExchangeName = exchange;
            this.QueueName = queue;
            this.RoutingKeyName = routingKey;
        }

        public void Receive(object sender,BasicDeliverEventArgs args)
        {
            RabbitMessageEntity body = new RabbitMessageEntity();
            try
            {
                string content = Encoding.UTF8.GetString(args.Body.ToArray());
                body.Content = content;
                body.Consumer =(EventingBasicConsumer)sender;
                body.BasicDeliver = args;
            }
            catch (Exception e)
            {
                body.ErrorMessage = $"订阅出错{e.Message}";
                body.Exception = e;
                body.Error = true;
                body.Code = 500;
            }
            OnReceivedCallback?.Invoke(body);
        }
    }
}

6.RabbitChannelManager.cs

using Infrastructure.Config;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Consumer
{
    public class RabbitChannelManager
    {
        public RabbitConnection Connection { get; set; }

        public RabbitChannelManager(RabbitConnection connection)
        {
            this.Connection = connection;
        }

        /// <summary>
        /// 创建接收消息的通道
        /// </summary>
        /// <param name="exchangeType"></param>
        /// <param name="exchange"></param>
        /// <param name="queue"></param>
        /// <param name="routingKey"></param>
        /// <param name="arguments"></param>
        /// <returns></returns>
        public RabbitChannelConfig CreateReceiveChannel(string exchangeType,string exchange,string queue,string routingKey,
            IDictionary<string,object>arguments = null)
        {
            IModel model = this.CreateModel(exchangeType,exchange,queue,routingKey,arguments);
            EventingBasicConsumer consumer = this.CreateConsumer(model,queue);
            RabbitChannelConfig channel = new RabbitChannelConfig(exchangeType,exchange,queue,routingKey);
            consumer.Received += channel.Receive;
            return channel;
        }

      
        /// <summary>
        /// 创建一个通道,包含交换机/队列/路由,并建立绑定关系
        /// </summary>
        /// <param name="exchangeType">交换机类型:Topic,Direct,Fanout</param>
        /// <param name="exchange">交换机名称</param>
        /// <param name="queue">队列名称</param>
        /// <param name="routingKey">路由名称</param>
        /// <param name="arguments"></param>
        /// <returns></returns>
        private IModel CreateModel(string exchangeType, string exchange, string queue, string routingKey, IDictionary<string, object> arguments)
        {
            exchangeType = string.IsNullOrEmpty(exchangeType) ? "default" : exchangeType;
            IModel model = this.Connection.GetConnection().CreateModel();
            model.BasicQos(0,1,false);
            model.QueueDeclare(queue,true,false,false,arguments);
            model.ExchangeDeclare(exchange, exchangeType);
            model.QueueBind(queue, exchange, routingKey);
            return model;
        }

        /// <summary>
        /// 创建消费者
        /// </summary>
        /// <param name="model"></param>
        /// <param name="queue"></param>
        /// <returns></returns>
        private EventingBasicConsumer CreateConsumer(IModel model, string queue)
        {
            EventingBasicConsumer consumer = new EventingBasicConsumer(model);
            model.BasicConsume(queue, false, consumer);
            return consumer;
        }
    }
}

7.RabbitMessageEntity.cs

using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Consumer
{
    public class RabbitMessageEntity
    {
        public string Content { get; set; }
        public EventingBasicConsumer Consumer { get; set; }
        public BasicDeliverEventArgs BasicDeliver { get; set; }
        public string ErrorMessage { get; set; }
        public Exception Exception { get; set; }
        public bool Error { get; set; }
        public int Code { get; set; }
    }
}

8.OrderMessage.cs

using RabbitMQ.Entities;

namespace Infrastructure.Message
{
    public class OrderMessage
    {
        public Account Account { get; set; }
        public OrderInfo OrderInfo { get; set; }
    }
}

9.IRabbitProducer.cs

using System;
using System.Collections.Generic;

namespace Infrastructure.Producer
{
    public interface IRabbitProducer
    {
        public void Publish(string exchange,string routingKey,IDictionary<string,object> props,string content);
    }
}

10.RabbitProducer.cs

using Infrastructure.Config;
using System;
using System.Collections.Generic;
using System.Text;

namespace Infrastructure.Producer
{
    public class RabbitProducer : IRabbitProducer
    {
        private readonly RabbitConnection _connection;
        public RabbitProducer(RabbitConnection connection)
        {
            _connection = connection;
        }
        public void Publish(string exchange, string routingKey, IDictionary<string, object> props, string content)
        {
            var channel = _connection.GetConnection().CreateModel();
     
            var prop = channel.CreateBasicProperties();
            if (props.Count > 0)
            {
                var delay = props["x-delay"];
                prop.Expiration = delay.ToString();
            }
            channel.BasicPublish(exchange, routingKey, false, prop, Encoding.UTF8.GetBytes(content));
        }
    }
}

三 类库RabbitMQ.Entities代码

1.Account.cs

using System;

namespace RabbitMQ.Entities
{
    public class Account
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
    }
}

2.OrderInfo.cs

using System;

namespace RabbitMQ.Entities
{
    public class OrderInfo
    {
        public int GoodsCount { get; set; }
        public int GoodsId { get; set; }
        public string GoodsName { get; set; }
        public int Status { get; set; }
        public int UserId { get; set; }
    }
}

四 类库RabbitMQ.Services代码

引用:nuget添加Newtonsoft.Json,添加项目Infrastructure、RabbitMQ.Entities引用

1.IOrderService.cs

using System;

namespace RabbitMQ.Services
{
    public interface IOrderService
    {
        void SendOrderMessage();
        void SendTestMessage(string message);
    }
}

2.OrderService.cs

using Infrastructure.Config;
using Infrastructure.Producer;
using System;
using System.Collections.Generic;
using System.Text;
using RabbitMQ.Entities;
using Newtonsoft.Json;
using Infrastructure.Message;

namespace RabbitMQ.Services
{
    public class OrderService : IOrderService
    {
        private readonly IRabbitProducer _rabbitProducer;
        public OrderService(IRabbitProducer rabbitProducer)
        {
            _rabbitProducer = rabbitProducer;
        }
        public void SendOrderMessage()
        {
            OrderInfo orderInfo = new OrderInfo();
            orderInfo.GoodsCount = 1;
            orderInfo.GoodsId = 1;
            orderInfo.GoodsName = "大话设计模式";
            orderInfo.Status = 0;
            orderInfo.UserId = 1;
            Account account = new Account();
            account.UserName = "Hobelee";
            account.Password = "password007";
            account.Email = "hobelee@163.com";
            account.Phone = "13964836342";
            OrderMessage orderMessage = new OrderMessage();
            orderMessage.Account = account;
            orderMessage.OrderInfo = orderInfo;
            string message = JsonConvert.SerializeObject(orderMessage);
            Console.WriteLine("短信/邮件异步通知");
            Console.WriteLine($"send message:{message}");
            //支付服务
            _rabbitProducer.Publish(RabbitConstant.DELAY_EXCHANGE, RabbitConstant.DELAY_ROUTING_KEY,
                new Dictionary<string, object>()
                {
                    { "x-delay",1000*20}
                },message);
        }

        public void SendTestMessage(string message)
        {
            Console.WriteLine($"send message:{message}");
            _rabbitProducer.Publish(RabbitConstant.TEST_EXCHANGE,"",new Dictionary<string,object>(),message);
        }
    }
}

3.IPayService.cs

using RabbitMQ.Entities;

namespace RabbitMQ.Services
{
    public interface IPayService
    {
        void UpdateOrderPayState(OrderInfo orderInfo);
    }
}

4.PayService.cs

using RabbitMQ.Entities;
using System;

namespace RabbitMQ.Services
{
    public class PayService : IPayService
    {
        public void UpdateOrderPayState(OrderInfo orderInfo)
        {
            Console.WriteLine($"修改订单状态:{orderInfo.Status}");
        }
    }
}

五 ASP.NET Core Web API项目RabbitMQ.WebApi.Order代码

引用:添加项目引用Infrastructure、RabbitMQ.Services

1.appsettins.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "RabbitMQ": {
    "HostName": "127.0.0.1",
    "Address": "",
    "Port": 5672,
    "UserName": "guest",
    "Password": "guest",
    "VirtualHost": "/"
  },

  "AllowedHosts": "*"
}

2.ServiceExtensions.cs

using Infrastructure.Config;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace RabbitMQ.WebApi.Order.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services)
        {
            services.AddCors(options=>
            {
                options.AddPolicy("AnyPolicy",
                    builder=>builder.AllowAnyOrigin()
                                    .AllowAnyMethod()
                                    .AllowAnyHeader());
            });
        }
        public static void ConfigureRabbitContext(this IServiceCollection services,IConfiguration config)
        {
            var section = config.GetSection("RabbitMQ");
            services.AddSingleton(
                  new RabbitConnection(section.Get<RabbitOption>())); 
        }
    }
}

3.Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using RabbitMQ.WebApi.Order.Extensions;
using Microsoft.OpenApi.Models;
using RabbitMQ.Services;
using Infrastructure.Producer;

namespace RabbitMQ.WebApi.Order
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerGen(c=>
            {
                c.SwaggerDoc("v1",new OpenApiInfo { Title="RabbitMQ.WebApi.Order",Version="v1"});
            });
            services.AddScoped<IOrderService, OrderService>();
            services.AddScoped<IRabbitProducer,RabbitProducer>();
            services.ConfigureCors();
            services.ConfigureRabbitContext(Configuration);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c=>c.SwaggerEndpoint("/swagger/v1/swagger.json","RabbitMQ.WebApi.Order v1"));
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

六 控制台项目RabbitMQ.Pay代码

添加nuget引用:Microsoft.Extensions.Hosting,Newtonsoft.Json

添加项目引用:Infrastructure,RabbitMQ.Services

将项目RabbitMQ.WebApi.Order中的appsettings.json文件复制到该项目

1.ProcessPay.cs

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Infrastructure.Config;
using Infrastructure.Consumer;
using Infrastructure.Message;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using RabbitMQ.Client;
using RabbitMQ.Entities;
using RabbitMQ.Services;

namespace RabbitMQ.Pay
{
    public class ProcessPay : IHostedService
    {
        private readonly RabbitConnection _connection;
        private readonly IPayService _payService;
        public List<RabbitChannelConfig> Channels { get; set; }
        public List<QueueInfo> Queues { get; } = new List<QueueInfo>();
        public ProcessPay(RabbitConnection connection, IPayService payService)
        {
            _connection = connection;
            _payService = payService;
            Queues.Add(new QueueInfo()
            { 
               ExchangeType = ExchangeType.Direct,
               Exchange = RabbitConstant.DELAY_EXCHANGE,
               Queue = RabbitConstant.DELAY_QUEUE,
               RoutingKey = RabbitConstant.DELAY_ROUTING_KEY,
               props = new Dictionary<string, object>()
               {
                   { "x-dead-letter-exchange",RabbitConstant.DEAD_LETTER_EXCHANGE},
                   { "x-dead-letter-routing-key",RabbitConstant.DEAD_LETTER_ROUTING_KEY}
               },
               OnRecevied = this.Receive
            });
        }

        private void Receive(RabbitMessageEntity message)
        {
            Console.WriteLine($"Pay Receive Message:{message.Content}");
            OrderMessage orderMessage = JsonConvert.DeserializeObject<OrderMessage>(message.Content);

            //超时未支付
            string money = "";
            //支付处理
            Console.WriteLine("请输入:");
            //超时未支付进行处理
            Task.Run(()=>
            {
                money = Console.ReadLine();
            }).Wait(20*1000);
            if (string.Equals(money, "100"))
            {
                //设置状态为支付成功(同时设置消息的状态和数据库订单的状态)
                orderMessage.OrderInfo.Status = 1;
                _payService.UpdateOrderPayState(orderMessage.OrderInfo);
                Console.WriteLine("支付完成");
                message.Consumer.Model.BasicAck(deliveryTag:message.BasicDeliver.DeliveryTag,multiple:true);
            }
            else
            {
                //重试几次依然失败
                Console.WriteLine("等待一定时间失效超时未支付的订单");
                //消息进入到死信队列
                message.Consumer.Model.BasicNack(deliveryTag: message.BasicDeliver.DeliveryTag,
                                                  multiple: false,
                                                  requeue: false);
            }
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("RabbitMQ支付通知处理服务已启动");
            RabbitChannelManager channelManager = new RabbitChannelManager(_connection);
            foreach (var queueInfo in Queues)
            {
                RabbitChannelConfig channel = channelManager.CreateReceiveChannel(queueInfo.ExchangeType,
                    queueInfo.Exchange,queueInfo.Queue,queueInfo.RoutingKey,queueInfo.props);
                channel.OnReceivedCallback = queueInfo.OnRecevied;
            }
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

2.Program.cs

using Infrastructure.Config;
using Infrastructure.Producer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RabbitMQ.Services;
using System;

namespace RabbitMQ.Pay
{
    class Program
    {
        static void Main(string[] args)
        {
            var configRabbit = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build()
                .GetSection("RabbitMQ");
            var host = new HostBuilder()
                .ConfigureServices(services=>
                services.AddSingleton(new RabbitConnection(configRabbit.Get<RabbitOption>()))
                .AddSingleton<IHostedService,ProcessPay>()
                .AddScoped<IRabbitProducer,RabbitProducer>()
                .AddScoped<IPayService,PayService>()).Build();
            host.Run();
        }
    }
}

七 控制台RabbitMQ.Pay.Timeout项目代码

添加nuget引用:Microsoft.Extensions.Hosting,Newtonsoft.Json

添加项目引用:Infrastructure,RabbitMQ.Services

将项目RabbitMQ.WebApi.Order中的appsettings.json文件复制到该项目

1.ProcessPayTimeout.cs

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Infrastructure.Config;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using RabbitMQ.Services;
using Infrastructure.Consumer;
using RabbitMQ.Client;
using RabbitMQ.Entities;
using Infrastructure.Message;

namespace RabbitMQ.Pay.Timeout
{
    public class ProcessPayTimeout : IHostedService
    {
        private readonly RabbitConnection _connection;
        private readonly IPayService _payService;
        public List<RabbitChannelConfig> Channels { get; set; } = new List<RabbitChannelConfig>();
        public List<QueueInfo> Queues { get; } = new List<QueueInfo>();
        public ProcessPayTimeout(RabbitConnection connection,IPayService payService)
        {
            _connection = connection;
            _payService = payService;
            Queues.Add(new QueueInfo
            { 
               ExchangeType = ExchangeType.Direct,
               Exchange = RabbitConstant.DEAD_LETTER_EXCHANGE,
               Queue = RabbitConstant.DEAD_LETTER_QUEUE,
               RoutingKey = RabbitConstant.DEAD_LETTER_ROUTING_KEY,
               OnRecevied = this.Receive
            });
        }

        private void Receive(RabbitMessageEntity messgae)
        {
            Console.WriteLine($"Pay Timeout Receive Message:{messgae.Content}");
            OrderMessage orderMessage = JsonConvert.DeserializeObject<OrderMessage>(messgae.Content);
            //获取到消息后,修改消息的状态为超时未支付 2
            orderMessage.OrderInfo.Status = 2;
            Console.WriteLine("超时未支付");
            _payService.UpdateOrderPayState(orderMessage.OrderInfo);
            messgae.Consumer.Model.BasicAck(messgae.BasicDeliver.DeliveryTag,true);
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("RabbitMQ超时支付处理程序启动");
            RabbitChannelManager channelManager = new RabbitChannelManager(_connection);
            foreach (var queueInfo in Queues)
            {
                RabbitChannelConfig channel = channelManager.CreateReceiveChannel(queueInfo.ExchangeType,
                    queueInfo.Exchange,queueInfo.Queue,queueInfo.RoutingKey);
                channel.OnReceivedCallback = queueInfo.OnRecevied;
            }
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

2.Program.cs

using Infrastructure.Config;
using Infrastructure.Producer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RabbitMQ.Services;
using System;

namespace RabbitMQ.Pay.Timeout
{
    class Program
    {
        static void Main(string[] args)
        {
            var configRabbit = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build()
                .GetSection("RabbitMQ");
            var host = new HostBuilder()
                .ConfigureServices(services =>
                services.AddSingleton(new RabbitConnection(configRabbit.Get<RabbitOption>()))
                .AddSingleton<IHostedService, ProcessPayTimeout>()
                .AddScoped<IRabbitProducer, RabbitProducer>()
                .AddScoped<IPayService, PayService>()).Build();
            host.Run();
        }
    }
}

八 运行

1.分别使用powershell运行三个项目

PS D:\DotNetProject\RabbitMQ.WebApiDemo\RabbitMQ.Pay> dotnet run
PS D:\DotNetProject\RabbitMQ.WebApiDemo\RabbitMQ.Pay.Timeout> dotnet run
PS D:\DotNetProject\RabbitMQ.WebApiDemo\RabbitMQ.WebApi.Order> dotnet run

2.使用浏览器打开http://localhost:5000/swagger/index.html

  

3.模拟正常支付

 

 4.模拟超时支付

首先执行3步骤,发送请求

然后powershell控制台不做任何操作 

执行结果如下:

5.模拟支付失败

支付金额不够 

先发送请求,然后请输入的金额输入一个不是100的值 

执行结果:

原文地址:https://www.cnblogs.com/hobelee/p/15781524.html