草庐IT

Asp.Net Core6.0中MediatR的应用CQRS

魏杨杨 2023-03-28 原文

1、前言

  对于简单的系统而言模型与数据可以进行直接的映射,比如说三层模型就足够支撑项目的需求了。对于这种简单的系统我们过度设计说白了无异于增加成本,因为对于一般的CRUD来说我们不用特别区分查询和增删改的程序结构。高射炮打蚊子那就有点大材小用了。但是我们的系统具有一定复杂性的时候,可能源于访问频次、数据量或者数据模型这个时候我们的查询跟增删改的需求差距就逐渐变大。所以CQRS(Command Query Responsibility Segregation)命令查询的责任分离就出现了。CQRS本质上是一种读写分离设计思想,这种框架设计模式将命令型业务和查询型业务分开单独处理。我们运用MediatR就可以轻松的实现CQRS。

2、中介者模式

  中介者模式定义了一个中介对象来封装一系列对象之间的交互关系,中介者使各个对象之间不需要显式地相互引用,从而降低耦合性。也符合符合迪米特原则。MediatR本质就是中介者模式,实现命令的构造和命令的处理分开来。

3、MediatR简介

  MediatR是一个跨平台通过一种进程内消息传递机制,进行请求/响应、命令、查询、通知和事件的消息传递,并通过C#泛型来支持消息的智能调度,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。

4、主要的几个对象

  a.IMediator:主要提供Send与Publish方法,需要执行的命令都是通过这两个方法实现

  b.IRequest、IRequest<T>:命令查询 | 处理类所继承的接口,一个有返回类型,一个无返回类型,一个查询对应一个处理类,程序集只认第一个扫描到的类。

  c.IRequestHandler<in TRequest,TResponse>(实现Handle方法) :命令处理接口。命令查询 | 处理类继承它,也可以继承AsyncRequestHandler(实现抽象Handle方法)、RequestHandler(实现抽象Handle方法)接口

  d.INotification:命令查询 | 处理类所继承的接口这个没有返回,与IRequest不通的是可以对于多个处理类。

  e.INotificationHandler<in TNotification>:与IRequestHandler一样的只不过这是INotification的处理接口

5、IRequest栗子

a.IRequest<T>:有返回值的类

  说了那么多干巴巴的直接上代码看。我这里是Core6.0控制台应用程序,安装nuget包 MediatR与扩展包MediatR.Extensions.Microsoft.DependencyInjection。也可以通过命令行添加dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection先看命令的查询处理

  这里我习惯性的将两个类放在一个文件里面方便查看,命名这里做查询就写XXXQuery  处理类的命名也是XXXQueryHandler

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mrdiator.Query
{
    /// <summary>
    /// 查询信息命令类
    /// </summary>
    internal class GetInfoQuery:IRequest<Result>
    {
        /// <summary>
        /// 构造函数--就是查询的条件说白了
        /// </summary>
        /// <param name="age"></param>
        /// <param name="name"></param>
        /// <param name="nowTime"></param>
        internal GetInfoQuery(int age, string name, DateTime nowTime)
        {
            Age = age;
            Name = name;
            NowTime = nowTime;
        }
        
        public  int Age { get; set; }
        public  string Name { get; set; }
        public  DateTime NowTime { get; set; }
    }
    /// <summary>
    /// 查询命令的处理类
    /// </summary>
    internal class GetInfoQueryHandller : IRequestHandler<GetInfoQuery, Result>
    {
        public Task<Result> Handle(GetInfoQuery request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetObjCommandHandller");
            object ret = new
            {
                request.Name,
                request.NowTime,
                request.Age,
            };
            var result = new Result()
            {
                Code = 200,
                Message="Success",
                Data = ret
            };
            return Task.FromResult(result);
        }
    }
}

  来看一下调用

using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Mrdiator.Query;
using Newtonsoft.Json;
using System.Net.Http;
using System.Reflection;

//实例化一个ServiceCollection
IServiceCollection services = new ServiceCollection();
//添加当前的程序集MediatR会扫描当前的程序集
//services.AddMediatR(typeof(Program).Assembly);    
services.AddMediatR(Assembly.GetExecutingAssembly());
//构建一个serviceProvider
var serviceProvider = services.BuildServiceProvider();
//从容器中获取mediator
var mediator = serviceProvider.GetService<IMediator>();
//执行命令
var result =await mediator.Send(new GetInfoQuery(18,"wyy",DateTime.Now));

 

  同样我们启动程序也打印了我们的输出。

   b.IRequest:无返回值的栗子

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mrdiator.Query
{
    /// <summary>
    /// 命令查询类--无返回值
    /// </summary>
    internal class GetInfoQuery2 : IRequest
    {
        public GetInfoQuery2(int age, string name, DateTime nowTime)
        {
            Age = age;
            Name = name;
            NowTime = nowTime;
        }

        public int Age { get; set; }
        public string Name { get; set; }
        public DateTime NowTime { get; set; }
    }
    /// <summary>
    /// 命令处理类1-----继承AsyncRequestHandler 实现抽象方法 Handle
    /// </summary>
    internal class GetInfoQuery2_2Handller : AsyncRequestHandler<GetInfoQuery2>
    {
        protected override Task Handle(GetInfoQuery2 request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetInfoQuery2_2Handller");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    /// 命令处理类2-----IRequestHandler 实现接口方法 Handle
    /// </summary>
    internal class GetInfoQuery2Handller : IRequestHandler<GetInfoQuery2>
    {
        public Task<Unit> Handle(GetInfoQuery2 request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetInfoQuery2Handller");

            return Task.FromResult(new Unit());
        }
    }
   
}
var result2 =await mediator.Send(new GetInfoQuery2(18,"wyy",DateTime.Now));

  我们写了一个GetInfoQuery2,下面有两个类都在泛型里实现了,可以看到程序是只执行了GetInfoQuery2_2Handller就可以看出IRequest命令类跟处理类失忆对一的关系。我们只是通过Mediator的send将GetInfoQuery2 作为参数传进去程序就能执行到GetInfoQuery2_2Handller里面的Handle方法这就是MediatR的好处。

     /// <summary>
    /// 命令处理类-----继承RequestHandler 实现抽象方法 Handle
    /// </summary>
    internal class GetInfoQuery3Handller : RequestHandler<GetInfoQuery3, Result>
    {
        protected override Result Handle(GetInfoQuery3 request)
        {
            Console.WriteLine("GetInfoQuery3Handller");
            return new Result();
        }
    }

  这样写也可以调用到 ,这就是上面写的 继承不同的类或者接口,一般大多数我都是继承IRequestHandler。

6、INotification栗子

  这里我新建了一个Core6.0的WebAPI的工程来演示INotification的运用。同样的nuget安装MediatR与扩展包MediatR.Extensions.Microsoft.DependencyInjection。在Program.cs里添加。这里如果你的命令处理类跟项目在同一个程序集里面就用第二个也可以,如果你是分开的另外建了一个类库写命令查询的直接引用里面随便一个类获取程序集就可以了

//获取该类下的程序集
builder.Services.AddMediatR(typeof(Program).Assembly);
//获取当前程序集
//builder.Services.AddMediatR(Assembly.GetExecutingAssembly());

   这里我们注册了处理多个事件、每个都执行到了。

using MediatApi.Helper;
using MediatApi.Model;
using MediatR;
using Newtonsoft.Json;

namespace MediatApi.Application.Command
{
    /// <summary>
    /// 创建订单
    /// </summary>
    public class OrderCreateCommand:INotification
    {
        /// <summary>
        /// Id
        /// </summary>
        public long Id { get; set; }
        /// <summary>
        /// 订单号
        /// </summary>
        public string? OrderNum { get; set; }
        /// <summary>
        /// 订单类型
        /// </summary>
        public string? OrderType { get; set; }
        /// <summary>
        /// 创建时间
        /// </summary>
        public DateTime? CreatTime { get; set; }

    }
    /// <summary>
    /// 创建订单处理1
    /// </summary>
    public class OrderCreateCommandHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Order model = new(notification.Id, notification.OrderNum, notification.OrderType, notification.CreatTime);
            //数据库操作省略
            Result ret = new()
            {
                Code=200,
                Message="",
                Data=model
            };
            string retJson=JsonConvert.SerializeObject(ret);
            Console.WriteLine("11111——————————————订单创建啦!");
            return Task.FromResult(retJson);
        }
    }
    /// <summary>
    /// 创建订单后处理步骤2
    /// </summary>
    public class OrderCreateTwoHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("22222——————————————扣钱成功了");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  创建订单后处理步骤3
    /// </summary>
    public class OrderCreateThreeHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("333333333——————————————订单入库啦!");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  创建订单后处理步骤4
    /// </summary>
    public class OrderCreateFoureHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("4444444——————————————第四个操作呢!");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  创建订单后处理步骤5
    /// </summary>
    public class OrderCreateFiveHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("55555——————————————接着奏乐接着舞!");
            return Task.CompletedTask;
        }
    }

}

   注意:这里是用mediator的publish方法的实现的,命令查询类继承INotification就要用publish方法,继承IRequest就要用Send方法,项目目录也在左侧这样别人看着也清晰点

7、IPipelineBehavior

  这个接口的作用就是在我们命令处理之前或者之后插入逻辑,类似我们的中间件,我新建一个TransactionBehavior来处理保存数据库之前之后的操作,这里的代码只判断了是否为空之前写的是判断事务是否为空,代码多就随便写了意思意思。

 然后新建一个DBTransactionBehavior这里对操作数据库新增演示一下

using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace MediatApi.Helper
{
    public class TransactionBehavior<TDBContext, TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> where TDBContext : WyyDbContext
    {
        ILogger _logger;
        WyyDbContext _dbContext;

        public TransactionBehavior(ILogger logger, WyyDbContext dbContext)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
        {
            var response = default(TResponse);
            try
            {
                Console.WriteLine("执行前逻辑++++++++++++++++++++++++++++++++++");
                Console.WriteLine();
                if (request != null)
                 return await next();

                Console.WriteLine("逻辑不对处理++++++++++++++++++++++++++++++++");
                Console.WriteLine();
                var strategy = _dbContext.Database.CreateExecutionStrategy();

                await strategy.ExecuteAsync(async () =>
                {
                    Guid transactionId;
                    using (var transaction = await _dbContext.Database.BeginTransactionAsync())
                    using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
                    {
                        _logger.LogInformation("----- 开始事务 {TransactionId} 请求{request}", transaction.TransactionId, request);

                        response = await next();

                        _logger.LogInformation("----- 提交事务 {TransactionId}}", transaction.TransactionId);

                        await _dbContext.CommitTransactionAsync(transaction);

                        transactionId = transaction.TransactionId;
                    }
                });

                return response;

            }
            catch(Exception ex)
            {
                _logger.LogError(ex, "处理事务出错{@Command}", request);

                throw;
            }
        }
    }
}
using MediatApi.Entity;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace MediatApi.Helper
{
    public class WyyDbContext: testContext
    {
        private IDbContextTransaction _currentTransaction;

        public IDbContextTransaction GetCurrentTransaction() => _currentTransaction;
        public bool HasActiveTransaction => _currentTransaction != null;
        
        public async Task CommitTransactionAsync(IDbContextTransaction transaction)
        {
            if (transaction == null) throw new ArgumentNullException(nameof(transaction));
            if (transaction != _currentTransaction) throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
            try
            {
                await SaveChangesAsync();
                transaction.Commit();
            }
            catch
            {
                RollbackTransaction();
                throw;
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }
        public void RollbackTransaction()
        {
            try
            {
                _currentTransaction?.Rollback();
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }
    }
}

   在Program里面注册服务

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DBTransactionBehavior<,>));

  这里链接数据库我们做一个新增Command里面的testContext就是数据库上下文我这里是从数据库直接生成的 WyyDbContext继承testContext

using MediatApi.Entity;
using MediatR;

namespace MediatApi.Application.Command
{
    public class CusCreateCommand:IRequest<int>
    {
        public string? Name { get; set; }
        public int? Age { get; set; }
    }
    public class CusCreateCommandHandler : IRequestHandler<CusCreateCommand, int>
    {
        private readonly testContext _db;

        public CusCreateCommandHandler(testContext db)
        {
            _db = db;
        }

        public async Task<int> Handle(CusCreateCommand request, CancellationToken cancellationToken)
        {
            Cu c = new()
            {
                Name = request.Name,
                Age = request.Age,

            };
            _db.Cus.Add(c);
            Console.WriteLine("执行处理++++++++++++++++++++++++++++++++++");
            return await _db.SaveChangesAsync();
        }
    }
}

   为了增加对比性 我也新建了一个传统的services来新增Cus

using MediatApi.Entity;

namespace MediatApi.services
{
   
    public interface ICusService
    {
        Task<int> AddAsync();
    }
    public class CusService : ICusService
    {
        private readonly testContext _db;

        public CusService(testContext db)
        {
            _db = db;
        }

        public async Task<int> AddAsync()
        {
            Cu c = new()
            {
                Name = "wyy",
                Age = 18

            };
            _db.Cus.Add(c);
      
return await _db.SaveChangesAsync(); } } }

  控制器里面两个新增 一个走MediatRy个走传统的Service

 /// <summary>
        /// 创建用户_mediator
        /// </summary>
        /// <param name="cmd"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<int> CusCreateMediator([FromBody] CusCreateCommand cmd)=> await _mediator.Send(cmd,HttpContext.RequestAborted);
        /// <summary>
        /// 创建用户 Service
        /// </summary>
        /// <returns></returns>

        [HttpPost]
        public async Task<int> CusCreateService() => await _cusService.AddAsync();

   运行可以发现 传统的Service就不会执行。MediatR 中具有与此类似的管线机制,可通过泛型接口 IPipelineBehavior<,>注册,使得我们在 Handle 真正执行前或后可以额外做一些事情:记录日志、对消息做校验、对数据做预处理数据库事务、记录性能较差的Handler 等等。

8、总结

  MediatR的用法

  a.IRequest、IRequest<T> 只有一个单独的Handler执行

  b.Notification,用于多个Handler。

  对于每个 request 类型,都有相应的 handler 接口:

  • IRequestHandler<T, U> 实现该接口并返回 Task<U>
  • RequestHandler<T, U> 继承该类并返回 U
  • IRequestHandler<T> 实现该接口并返回 Task<Unit>
  • AsyncRequestHandler<T> 继承该类并返回 Task
  • RequestHandler<T> 继承该类不返回

  Notification

  Notification 就是通知,调用者发出一次,然后可以有多个处理者参与处理。

  Notification 消息的定义很简单,只需要让你的类继承一个空接口 INotification 即可。而处理程序则实现 INotificationHandler<T> 接口的 Handle 方法

 

  PASS:如果你想成为一个成功的人,那么请为自己加油,让积极打败消极,让高尚打败鄙陋,让真诚打败虚伪,让宽容打败褊狭,让快乐打败忧郁,让勤奋打败懒惰,让坚强打败脆弱,只要你愿意,你完全可以做最好的自己。

有关Asp.Net Core6.0中MediatR的应用CQRS的更多相关文章

  1. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  4. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  5. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  6. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

  7. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  8. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  9. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  10. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

随机推荐