1.引言
1.1编写目的
本文档是微服务插件框架(英文名称Micro service plugin framework,以下简称:Mspf)的用户手册。该手册用于讲解我们为什么要研发这样一个框架,以及讲解Mspf的产品定义和产品功能,用户如何使用它开发和发布自己的插件。本文档的读者群是具有开发能力的软件开发人员,开发人员具备前端开发能力,如熟悉HTML,JavaScript,CSS,DOM,Ajax等,以及VUE,Jquery,前端插件技术等,后端开发能力如Python语言,RESTful技术,Django技术,数据库技术等。
1.2研发背景
当前云计算、大数据以及AI的发展,是软件信息化革命后的又一次软件智能化升级,是一次深度革命。未来,大数据应用以及AI应用不再是完整的信息化系统,而是多个相对独立的微服务应用,将智能化数据处理结果展示给用户。因此,微服务架构将是软件领域的主流技术架构的判断。
Python语言已经是大数据应用以及AI的主流开发语言,当前还没有一个成熟的支持Python语言的微服务插件的平台或框架。大量Python开发的应用均是独立的应用,移植性不好。
我们开发微服务框架,基于Django框架开发,每个微服务均是Django的一个APP,以RESTful形式接口提供JSON数据访问服务。在可预见的未来十年,AI和大数据微服务将遍布于全球互联网。而微服务框架将是支撑微服务运行的基础设施和平台。
1.3产品定义
微服务插件框架,这个概念分为两个层面,首先是微服务,其次是插件框架。
微服务是从软件组织架构上来构建软件,插件是从软件技术框架上来构建软件,是从不同层面的构建软件。
- 解耦:系统内的软件各模块基于功能最小化原则被解耦。解耦后的系统将可轻易地通过组合被重构、修改和扩展。
- 组合编排:系统解耦成微服务后,可以根据业务系统功能需求,实现灵活的组合和组装,快速搭建成符合特定需求的应用系统。
- 组件化:微服务可以被看成相互独立的组件,可以通过独立的技术栈实现,而组件间通信基于REST API访问和通过JSON作为数据交换格式(这是当前事实上的软件访问标准和数据交换标准),组件作为整体的替换和内部功能的升级对其他组件是透明的。
- 业务能力:微服务因功能最小化原则而构建,它们可专注于某种单一的功能,业务和功能边界清晰。
- 自治:开发者和团队独立地、并行地工作,提高开发速度。
- 持续交付:允许持续发布软件新版本,通过系统化的自动手段来创建、测试和批准新版本。
- 去中心化管理:微服务的关注于使用正确的工具来完成正确的工作。这也就是说,没有标准化的方式或者技术模式。开发者们有权选择最好的工具来解决问题。
- 敏捷性:微服务支持小型独立团队敏捷开发自己专注的服务,以自己熟悉的环境更迅速和快速的工作,以缩短开发周期。任何新功能都可以被快速开发或丢弃。
微服务的优势
- 独立开发:适合于小团队的快速独立开发出内聚性更高的独有的功能。
- 独立部署:基于它们所提供的服务,它们可以被独立地部署到应用中。
- 错误隔离:即便其中某个服务发生了故障,整个系统还可以继续工作。同时由于便于迅速定位错误原因,降低修复故障成本。
- 混合技术栈:可以使用不同技术栈为同一个应用构建不同的服务。队可以自由选择**工具来解决他们的具体问题。
- 按粒度扩展:可以根据需求扩展某一个组件,不需要将所有组件全部扩展。
- 重用性增强:将软件划分为小型且明确定义的微服务,可用于多种组合以满足不同的功能需求,开发人员无需从头开始编写代码。
插件,英文,Plug-in,又称addin、add-in、addon或add-on,又译外挂,是一种遵循一定规范的应用程序接口编写出来的一个或一组程序或程序包。其不能脱离指定的平台单独运行,只能运行在程序规定的系统平台下(可能同时支持多个平台)。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。
插件的基本特点:
- 热插拔:支持动态的安装、启动、停止、更新和卸载,而整个系统无需重启。
- 适配性:动态的注册、获取和监听服务,使得系统能够在微服务插件环境调整自己的功能。
- 透明:提供了接口来监控插件内部状态,能够通过命令行进行调试。
- 版本化隔离:插件可以版本化,多版本能够共存而不会影响系统功能,解决了JAR hell的问题。
- 快速:插件类的加载速度快。
- 懒加载:本框架采用了很多懒加载机制。比如服务可以被注册,但是直到被使用时才创建,用即加载。
插件设计的优势:
- 结构清晰、易于理解:由于各个插件之间是相互独立的,所以结构非常清晰也更容易理解。良好地体现了微服务的解耦思想。
- 易修改、可维护性强:由于插件与宿主程序之间具有热插拔特点,软件结构很灵活,容易修改,方便软件的升级和维护。
- 可移植性强、重用力度大:插件本身也是由一系列小的功能结构组成,通过接口向外部提供自己的服务,可复用程度高,易于移植。
- 结构容易调整:系统功能的增加或减少,只需相应的增删插件,而不影响整个体系结构,能方便的实现结构调整,具有超强的组合能力。
- 插件之间的耦合度较低:插件通过与宿主程序通信来实现插件与插件,插件与宿主程序间的通信,插件之间的耦合度更低。
可以在软件开发的过程中修改应用程序:插件的结构支持在软件的开发过程中随时修改插件,也可在应用程序发行之后,通过补丁包的形式增删插件,通过这种形式达到修改应用程序的目的。
插件化思想已经在多个框架或者设计模式中得以实现,应用不同语言的插件框架也很多。基于Java语言开发的开放服务网关协议(Open Services Gateway initiative,简称OSGi)的插件框架,如Apache Felix,Equinox,均是优秀的插件框架。近年利用Springboot的注入原理开发的国产插件框架,也在成长。而基于Python语言的插件框架有PluginBase。
微服务的软件架构思想与插件的技术框架思想的结合,是未来软件开发的一个重要方向,特别是智能手机的普及和移动互联网的兴起,促使软件发展趋向为功能单一化,操作简单化,业务边界明确。Python语言近年的迅速普及,让软件开发的简单化成为新的趋势。一个功能只要从原来Java开发要数十行代码,到Python的数行代码。更促使软件开发团队小型化,甚至大量的个人软件开发极客,他们能够开发出大量具有专业性质的小软件服务,服务特定的行业和客户。这一趋势也孕育了微服务插件框架的市场要求。
2.Mspf介绍
Mspf是一个开源的能够支持面向Python语言的微服务插件的插件框架。Mspf基于Django开发。每个微服务插件都是一个Django的App。
这个插件框架具有以下特点:
- 可直接热插拔到框架:用户可以上传和下载Django的App的zip文件到框架上,框架能够自动注册和加载;插件可以直接从框架删除,并自动注销;在框架上可创建一个Django的App插件,并注册到框架;
- 框架提供App中的文件代码快速编辑功能,以支持快速开发Python微服务的前后端代码,前端代码可以是VUE,Django的template,也可以是HTML和DOM等,后端是Python代码,提供REST API接口注册。
当前微服务架构基本以Docker为主实现,但是Docker对于一般的小型化或者微型化的应用,如一个python计算算法,也是过于重量级,不够轻量。插件是一个超轻量级的技术模型,适宜于轻量级Python程序部署和发布,是一种比Docker更为实用的微服务技术框架。
当前Mspf是一个微服务插件框架的初级版本,对于微服务的特点以及插件框架的特点都还没有充分支持,随着深入的产品开发,上述特征将逐渐支持。
我们欢迎任何感兴趣的Python工程加入团队,为开源的框架做出贡献。
开发设计规划:
(1)设计实现一个面向Python的插件框架,可部署到Linux服务器,支持在线前后端编程开发,这个工作已经完成
(2)设计实现能支持debug的开发。
(3)设计实现微服务的开发。
(4)设计支持框架的高级服务开发。
3.运行环境搭建
Mspf支持跨平台运行,支持Windows和Linux,需要在平台上安装Python环境。
- Python 3.9
安装Python 3.9,在Windows上安装和Linux上安装,网上有很多培训教材,此处不再赘述。
- Django-3.2.4:
命令:pip install django
- Watchdog 2.1.3:
命令:pip install watchdog
- Configparser-5.0.2:
命令:pip install configparser
- Python-socketio:
命令:pip install python-socketio
4.下载代码
Mspf的完整代码在https://github.com/swinflowcloud/mspf,如下图所示,下载Download ZIP,就可以把完整代码下载下来。
在bin目录下面,运行./startup.bat(Windows)或者./startup.sh(Linux),就可以运行来整个框架。

讯享网
在bin目录下面,运行./startup.bat(Windows)或者./startup.sh(Linux),就可以运行来整个框架。
5.项目目录结构
打开项目,通过tree,来展示目录,每个目录的解释如下图所示:

6.软件功能
6.1主界面
Mspf框架启动起来后,在浏览器的地址栏中输入http://localhost:8899/

如上图所示,界面上有几个按钮:
快速入门按钮:点击按钮可以下载本文件。这个文档就是本文档。
下载Mspf按钮:点击按钮下载框架所有的代码。(这个功能,如果下载了项目代码,就不需要在这里下载了)
创建微服务插件按钮:点击按钮可以创建插件,参见6.3节。
单机或将文件拖拽到此处按钮:将插件压缩文件拖动带此区域,直接上传并解压和注册插件。注册完成的插件,就可以直接访问了。
搜索框:输入一个字符串,可以模糊匹配出符合自己需要的插件列表。
下面是插件列表,这个插件列表默认列出来所有的已经注册的插件。插件基本信息卡片如下图所示:

如上图所示,卡片最上方是图标,未来能够显示服务插件的前端页面。中间是插件的名称、版本和开发商。下面是插件的描述以及最后更新时间。
下面的一排小图标是操作按钮,第一个是预览图标,点击此图标是可以预览插件的主页。如下图所示:

上面的插件的访问地址是http://ip:port/plugins.PluginID/,访问地址的结构,将在6.2节说明。
点击编辑插件信息图标,可以编辑插件的基本信息。详见6.5节。
点击编辑代码图标,可以编辑插件的源代码。详见6.6节。
点击删除插件图标,可以删除插件,详见6.4节。
点击下载插件代码图标,可以下载插件的源代码,详见6.7节。
6.2上传插件
点击上传单机或将文件拖拽到此处按钮,打开文件选择器。如下图所示,选择器中仅显示zip文件。

下面讲解下插件的文件目录结构:

上图中,static目录中,css保存CSS文件,img目录主要保存各种图片文件,jsplugins目录保存JavaScript的脚本文件。templates目录保存HTML页面,也保存Django的template文件,或者VUE文件。
如果要能成功上传一个插件文件包,文件包中需要包含如下图的文件:

__pycache__目录是Django的生成的,这里不深入讲解。其余的文件均是Django生成的APP的文件。我们将在6.3节,详细介绍插件中所包含文件。
在上传的插件的apps.py文件中,需要特别指出的是apps.py文件的name属性。一般地通过Django的startapp命令生成的App的apps.py文件中,都是直接将app的名字放在class名和name属性。举例来说,一个生成的App的名字叫mytest,那么apps.py文件中的格式是:
from django.apps import AppConfig # Secondly run:apps.py # This file is used to configure current App's conf. class MytestConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'Mytest'
讯享网
如果要把mytest这个App作为一个插件上传到Mspf,那么需要修改name属性为:
讯享网name = 'plugins.Mytest'
然后将App打包后成ZIP文件后,就可以上传到Mspf框架。访问插件App需要在浏览器中输入:http://ip:port/plugins.Mytest/…
6.3创建插件
创建插件功能是,Mspf框架提供的在线创建一个插件的功能。点击创建微服务插件,将打开如下窗口:

如上图所示,输入插件名称等信息,尽管每个输入框都可以为空,但是尽量每个输入框都填写上信息。状态是未禁和已禁两种状态,这个目前初级版本未支持的。点击添加按钮,框架将创建一个插件,并在插件列表中展示出来。如下图所示,我们创建一个mytest插件:

点击添加按钮后,如下图所示:

插件已经创建好,并且能够显示在插件列表中。点击预览,可以看到如下插件的页面:

这意味着我们的插件已经创建好了。新创建好的插件中包括__init__.py,admin.py,apps.py,models.py,plugin.conf,tests.py,urls.py,views.py等文件,除plugin.conf之外。
一个新创建的插件中的文件如下,init.py内容:

这个文件是App运行起来后,第一个执行的文件。
admin.py文件的内容:

admin.py文件是App启动起来后,第四个执行的文件。
apps.py文件的内容:

apps.py文件是App执行起来后,第二个执行的文件。
models.py文件的内容是:

from django.db import models # Thirdly run:models.py # Create your models here.
该文件是标准的Django的App生成的models.py。tests.py也是标准的Django的App生成文件。
urls.py文件中,有两个URL
讯享网from django.urls import path from . import views # This file is used to defined RESTFul APIs: # path('hello/', views.hello),that is http://localhost:port/hello, # direct to views.hello method,views.hello defined in views.py。 # Fifthly run:urls.py urlpatterns = [ path('', views.pluginIndex), path('hello/', views.hello), ]
这个文件中,包含两个URL路径。第一个是默认路径,也就是说,在浏览器中输入http://ip:port/plugins.PluginID/就可以访问到views.pluginIndex方法,输入http://ip:port/plugins.PluginID/hello/,就可以访问到views.hello方法。例如,在浏览器中输入http://localhost:8899/plugins.SP_000000000000A4M0/,会出现如下图所示:

同样,输入http://localhost:8899/plugins.SP_000000000000A4M0/hello,会出现如下图所示:

上述两个链接访问的URL,都是views.py的方法,或者叫函数。具体如下:
from django.shortcuts import render from django.http import HttpResponse,JsonResponse from django.views.decorators.csrf import csrf_exempt # When getting requests from browers using ajax :views.py # this file is similar to controllers in srpingmvc or springboot # Create your views here. def pluginIndex(request): return HttpResponse('Hi, this is a plugin!') def hello(request): return HttpResponse('Hello, This is Mspf!')
这个文件中,第一个方法就是在urls.py中定义的path(’’, views.pluginIndex),第二个方法就是path(‘hello/’, views.hello)。
用户可以在urls.py增加新的访问URL定义,并相应的在views.py中添加方法即可。
如在urls.py中添加,path(‘mytest/’, views.test),在views.py中添加
讯享网def test(request): return HttpResponse('Hello, This is mytest!')
6.4删除插件
插件是可以被删除的。点击插件卡片下面的图标,将会弹出对话框,以确认是否要删除插件。点击“是”,框架将删除插件。列表中也将删除掉插件掉卡片信息。

注:在初级版本中,一旦打开了插件代码编辑器,删除插件是无法控制代码编辑框的关闭的,也就是说,删除了插件,代码编辑框应该伴随关闭。但是初级版本和没有实现。
6.5编辑插件信息
创建插件时候输入的信息的编辑是点击插件信息卡片底部的图标,点击后打开信息编辑对话框。如下图所示:

编辑完插件信息后,点击保存,插件信息将自动保存并刷新。
6.6编辑插件源程序
插件中的文件以及源程序可以通过点击图标来编辑,点击该图标,将打开源代码编辑器。如下图所示:

点击左上角的红框中的图标,可以看到插件下所有的文件。如下所示:

上图可以看到,最顶端有工具栏。如下图所示:

工具栏实现可以文件管理,需要注意的是,在操作文件或目录之前,需要选中一个目录,选中的这个目录作为父目录。点击新建按钮,选择新建目录选项,可以打开新目录对话框:

如上图所示,新建目录一定在一个目录之下,或者根目录,或者是根目录下边的一个目录。我们现在创建的目录就是在一个根目录下的static目录下面。输入目录名dddd,点击创建。将在SP_\static下面创建一个目录dddd。如下图所示:

如上图所示,鼠标移动上去,将显示目录名和最后修改时间戳。
选中dddd目录,再点击新建按钮,选择新建文件选项,打开新文件对话框,输入一个文件名称(注意一定要带扩展名)如下图所示:

输入文件名,一个新的文件将在dddd目录下创建。如下图所示:

点击刷新按钮,是刷新所有插件文件目录。
拷贝功能可以拷贝文件,也可以拷贝目录,无论拷贝文件还是拷贝目录,都需要指定目标目录。点击拷贝按钮,打开拷贝对话框,如下图所示:

如上图所示,选择了拷贝文件SP_\static\dddd\djdjd.js,选择一个目标目录,如下图所示:

选择SP_\logs后,点击确定。文件将被拷贝到SP_\logs下面,如下图所示:

拷贝功能也可以支持拷贝目录,拷贝目录实际上拷贝目录树,即该目录下所有文件及目录都拷贝到新目录下。选择一个目录,点击拷贝按钮,选择一个目标目录,如下图所示:

点击确定按钮,源目录将被拷贝到SP_\logs,如下图所示:

移动功能是移动文件或目录到新的目录下面,移动功能和拷贝功能类似,也是要选择一个目标目录。点击移动按钮,打开移动对话框,如下图所示:

选择一个目标,目录点击确定。目录将被移动到指定目录下。如下图所示:

移动文件操作类似于拷贝文件,此处不再赘述。
重命名功能能够重命名文件或目录。以重命名文件为例,选择一个文件,点击重命名,打开重命名对话框,如下图所示:


输入一个新的名称test.js,点击确定,此文件的名称被更新,如下图所示:

重命名目录操作类似重命名文件名,此处不再赘述。
注:搜索文件的功能,在初级版本中未实现。待后续版本实现。
单击或将文件托拽到此处,是上传文件功能,该功能是支持将自定义的文件上传到插件空间中,上传仅支持文件上传,不支持目录上传。上传默认上传到根目录下,如果想上传到特定目录或者某文件所在目录下下,选中一个上传的目标目录或者一个文件也可以,然后点击上传区域,打开一个文件选择对话框。如下图所示:

选择文件test.log,点击打开。该文件将被上传到dddd目录下。如下图所示:

另一种方法是要上传的文件用鼠标拖动到此区域。也可以达到同样的效果。此处不再赘述。
下载功能在每个目录或者文件旁边的小图标,点击这个图标,将让文件或者目录,以压缩包ZIP文件的形式,下载到本地。此功能很简单,不再赘述。
删除功能是点击每个目录或者文件旁边的小图标,将弹出来一个删除确认对话框,如下图所示:

点击“是”按钮,将删除该文件或目录。
点击任何文件,都可以打开一个编辑器进行编辑,如下图所示:

编辑完文件自动保存,无需点击保存。
6.7下载插件源程序
插件源程序可以打成ZIP包下载。点击下载图标,如下图所示:

插件源程序就能打包下载。此功能很简单,不再赘述。
7.实战开发指南
用Mspf开发微服务插件,本质上就是开发Django的App,但现在Django开发主要是本地开发,完成开发后,直接部署到服务器。而基于Mspf开发,是一种在线开发模式。Mspf为插件创建插件空间,插件的所有文件均在插件空间中。Mspf提供在线前端后端程序编辑器,前端编辑器设计界面和脚本,后端Python编辑器设计后端访问TEST API接口。开发完成即可完成部署和加载,是一种新的敏捷开发模式。
下面我们创建一个测试插件,并试着开发前端界面和后端接口。前端展示基于Bootstrap 5.1.0(https://getbootstrap.com/)的HTML页面,后端开发一个基于Python的REST API接口,共前端页面访问。
7.1前端界面设计
第一步:
我们创建一个插件。点击创建微服务插件。如下图所示:

点击添加按钮,新插件创建好。如下图所示:

第二步:
点击编辑代码图标,打开代码编辑界面。

第三步:
在templates目录下面创建一个home.html页面。如下图所示:

点击创建,创建一个新的空页面。
第四步:
用编辑器打开这个页面。

在该页面中,输入如下代码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate"> <meta name="keywords" content="Mspf,Micro service,plugin framework" /> <meta http-equiv="expires" content="0"> <!-- Bootstrap CSS CDN --> <link rel="stylesheet" href="../../static/base-plugins/bootstrap-5.1.0-dist/css/bootstrap.min.css"> <link rel="stylesheet" href="../../static/base-plugins/font-awesome-4.7.0/css/font-awesome.css"> <link rel="stylesheet" href="../../static/base-plugins/bootstrap-icons-1.5.0/bootstrap-icons.css"> <title>主界面</title> </head> <body> <button type="button" name="getdata" class="btn btn-primary bi bi-play-fill" onclick="getdata()">获得数据</button> <div id="label"></div> <!-- jQuery CDN - Slim version (=without AJAX) --> <script src="../../static/base-plugins/jquery-3.5.1.min.js"></script> <!-- Popper.JS --> <script src="../../static/base-plugins/popper.min.js"></script> <!-- Bootstrap JS --> <script src="../../static/base-plugins/bootstrap-5.1.0-dist/js/bootstrap.bundle.js"></script> <script> function getdata(){
$.ajax({
url:"../getdata", data:{
a:1, }, type:'GET', dataType:'JSON', complete:function(data) {
data = data.responseJSON; var el = document.getElementById("label"); el.innerHTML = data.msg; }, }); } </script> </body> </html>
7.2后端接口设计
第五步:
后端接口设计,打开views.py。添加
讯享网from django.shortcuts import render from django.http import HttpResponse,JsonResponse from django.views.decorators.csrf import csrf_exempt # When getting requests from browers using ajax :views.py # this file is similar to controllers in srpingmvc or springboot # Create your views here. def pluginIndex(request): return HttpResponse('Hi, this is a plugin!') def hello(request): return HttpResponse('Hi, This is Mspf!') # add codes here: def home(request): return render(request, 'home.html') def api1(request): a = request.GET s = a.get('a') return JsonResponse({
'msg': 'Hi, This is '+s+' rubby!'})
第六步:
修改urls.py文件,注册一个REST API,让前端可以访问。添加如下代码到urls.py中:
from django.urls import path from . import views # This file is used to defined RESTFul APIs: # path('hello/', views.hello),that is http://localhost:port/hello, # direct to views.hello method,views.hello defined in views.py。 # Fifthly run:urls.py urlpatterns = [ path('', views.pluginIndex), path('hello/', views.hello), # add codes here path('home/', views.home), path('getdata/', views.api1), # add codes here ]
注意:用户要是想注册多个插件,每个path的url均要唯一。也就是path(param1, method)中的param1在所有插件中都是唯一的。
第七步:

8.Mspf的数据库设计
数据库,不需要用户创建,框架中已经创建好了。就是plugin_repository.sqlite3。
这里只是讲解下数据库结构。修改PluginMaster中的models.py:
讯享网class Plugin(models.Model): id = models.CharField('系统标识',max_length = 22, primary_key=True) name = models.CharField('插件名称',max_length = 512) developer = models.CharField('开发者',max_length = 512) pluginVersion = models.CharField('版本',max_length = 20) pluginDescription = models.CharField('描述',max_length = 1024) useCounting = models.IntegerField('引用') likeCounting = models.IntegerField('引用') copyright = models.CharField('版权所有',max_length = 512) keywords = models.CharField('关键词',max_length = 512) pluginType = models.CharField('插件类型',max_length = 32) logo = models.CharField('商标',max_length = 512) defaultOptions = models.CharField('默认参数',max_length = 512) createDatetime = models.DateTimeField('创建时间', auto_now_add=True) lastupdate = models.DateTimeField('最后更新',auto_now =True) license = models.CharField('许可证',max_length = 512) isFree = models.SmallIntegerField('免费') pricing = models.DecimalField('价格',max_digits = 5, decimal_places = 2) banned = models.SmallIntegerField('是否封禁') parent = models.CharField('父节点',max_length = 22) currOwner = models.CharField('当前所有人',max_length = 22) owner = models.CharField('组织所有人',max_length = 22) class Meta: db_table = 'Plugin'
这是Mspf的初级版本的文档。希望这个框架对大家有所帮助。我们会继续开发完善功能。
如果有问题,欢迎留言,我会及时回复。或者发我的邮箱也可以dhcao2003 at 163.com
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/41439.html