Asp .Net Core 系列:Asp .Net Core 集成 Panda.DynamicWebApi

文章目录

    • 简介
    • Asp .Net Core 集成 Panda.DynamicWebApi
    • 配置
    • 原理
      • 什么是POCO Controller?
      • POCO控制器原理
      • ControllerFeatureProvider
      • 实现自定义判断规则
      • IApplicationModelConvention
      • Panda.DynamicWebApi中的实现
        • ConfigureApiExplorer()
        • ConfigureSelector()
        • ConfigureParameters()

简介

Panda.DynamicWebApi 是一个动态生成WebApi的组件,生成的API符合Restful风格,受启发于ABP。它可以根据符合条件的类来生成WebApi,由MVC框架直接调用逻辑,无性能问题,完美兼容Swagger来构建API说明文档,与手动编写Controller相比并无区别。

应用场景:DDD架构中的应用逻辑层,可使用本组件来直接生成WebApi,而无需再用Controller来调用。

Asp .Net Core 集成 Panda.DynamicWebApi

(1)新建一个 ASP.NET Core WebApi(或MVC) 项目

(2)通过Nuget安装组件

Install-Package Panda.DynamicWebApi

(3)创建一个类命名为 AppleAppService,实现 IDynamicWebApi 接口,并加入特性 [DynamicWebApi]

[DynamicWebApi]
public class AppleAppService: IDynamicWebApi
{
    private static readonly Dictionary<int,string> Apples=new Dictionary<int, string>()
    {
        [1]="Big Apple",
        [2]="Small Apple"
    };

    /// <summary>
    /// Get An Apple.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet("{id:int}")]
    public string Get(int id)
    {
        if (Apples.ContainsKey(id))
        {
            return Apples[id];
        }
        else
        {
            return "No Apple!";
        }
    }

    /// <summary>
    /// Get All Apple.
    /// </summary>
    /// <returns></returns>
    public IEnumerable<string> Get()
    {
        return Apples.Values;
    }

    public void Update(UpdateAppleDto dto)
    {
        if (Apples.ContainsKey(dto.Id))
        {
            Apples[dto.Id] =dto.Name;
        }
    }

    /// <summary>
    /// Delete Apple
    /// </summary>
    /// <param name="id">Apple Id</param>
    [HttpDelete("{id:int}")]
    public void Delete(int id)
    {
        if (Apples.ContainsKey(id))
        {
            Apples.Remove(id);
        }
    }

}

(4)在 Startup 中注册 DynamicWebApi

public void ConfigureServices(IServiceCollection services)
{
    // 默认配置
    services.AddDynamicWebApi();

    // 自定义配置
    services.AddDynamicWebApi((options) =>
    {
        // 指定全局默认的 api 前缀
        options.DefaultApiPrefix = "apis";
    
        /**
         * 清空API结尾,不删除API结尾;
         * 若不清空 CreatUserAsync 将变为 CreateUser
         */
        options.RemoveActionPostfixes.Clear();
    
        /**
         * 自定义 ActionName 处理函数;
         */
        options.GetRestFulActionName = (actionName) => actionName;
    
        /**
         * 指定程序集 配置 url 前缀为 apis
         * 如: http://localhost:8080/apis/User/CreateUser
         */
        options.AddAssemblyOptions(this.GetType().Assembly, apiPreFix: "apis");
    
        /**
         * 指定程序集 配置所有的api请求方式都为 POST
         */
        options.AddAssemblyOptions(this.GetType().Assembly, httpVerb: "POST");
    
        /**
         * 指定程序集 配置 url 前缀为 apis, 且所有请求方式都为POST
         * 如: http://localhost:8080/apis/User/CreateUser
         */
        options.AddAssemblyOptions(this.GetType().Assembly, apiPreFix: "apis", httpVerb: "POST");
    });
}

(5)添加 Swagger

            builder.Services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new OpenApiInfo() { Title = "Panda Dynamic WebApi", Version = "v1" });

                // TODO:一定要返回true!
                options.DocInclusionPredicate((docName, description) => true);

                var baseDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
                var xmlFile = System.AppDomain.CurrentDomain.FriendlyName + ".xml";
                var xmlPath = Path.Combine(baseDirectory, xmlFile);
                options.IncludeXmlComments(xmlPath);
            });

配置

所有的配置均在对象 DynamicWebApiOptions 中,说明如下:

属性名是否必须说明
DefaultHttpVerb默认值:POST。默认HTTP动词
DefaultAreaName默认值:空。Area 路由名称
DefaultApiPrefix默认值:api。API路由前缀
RemoveControllerPostfixes默认值:AppService/ApplicationService。类名需要移除的后缀
RemoveActionPostfixes默认值:Async。方法名需要移除的后缀
FormBodyBindingIgnoredTypes默认值:IFormFile。不通过MVC绑定到参数列表的类型。

原理

什么是POCO Controller?

POCO Controller是 ASP.NET Core 中的一个特性,虽然在2015年刚发布的时候就有这个特性了,可是大多数开发者都只是按原有的方式去写,而没有用到这个特性。其实,如果利用这个特性进行稍微封装后,用在SOA架构中Service层的场景中是极其便利的。这篇文章主要就是说我最近在学习使用开源AOP库AspectCore写WebApi动态代理客户端的时候,实现为普通类无添加WebApi服务的过程。

POCO控制器就是ASP.NET Core项目中所有带有Controller后缀的类、或者标记了[Controller]特性的类,虽然没有像模版项目中那样继承自Controller类,也会被识别为控制器,拥有跟普通控制器一样的功能,像下面这段代码中,两个类都会被识别成控制器:

public class PocoController
{
    public IActionResult Index()
    {
        return new ContentResult() { Content = “Hello from POCO controller!” };
    }
}
[Controller]
public class Poco
{
    public IActionResult Index()
    {
        return new ContentResult() { Content = “Hello from POCO controller!” };
    }
}

POCO控制器原理

其实,在ASP.NET Core中,已经不像旧版本的 ASP.NET WebApi 那样,通过ControllerFactory来创建Controller,多亏于ASP.NET Core一脉相承的IoC框架 Microsoft.Extensions.DependencyInjection,ASP.NET Core中的内部实现变得更优雅。其中POCO控制器的核心原理就在IApplicationFeatureProvider<ControllerFeature>这个接口的实现ControllerFeatureProvider。

通过aspnet/Mvc项目的Github源码仓库中查询得知,Mvc里把Controller、ViewComponent、TagHelper、Views等组件定义为特性(Feature),如ControllerFeature,特性里就存放了应用中被识别为相组件的类型的集合,如如ControllerFeature中就存放了所有Controller类型。IApplicationFeatureProvider<ControllerFeature>这个接口是用来给MVC框架提供控制器类型识别的接口,当把这个接口的实现注册到服务配置中,就能为其中识别的类型提供控制器功能。

ControllerFeatureProvider是这个接口的默认实现,其中有一个方法IsController(TypeInfo typeInfo)的功能就是判断某类型是否为控制器的。而接口方法PopulateFeature(IEnumerable<ApplicationPart> parts,ControllerFeature feature)则为把传入的 “Mvc应用部分(ApplicationPart,大概是指Mvc的作用程序集)”中的类型都一一判断,如果是控制器,那么就加入控制器特性对象中。

ControllerFeatureProvider

ControllerFeatureProvider 是 ASP.NET Core MVC 框架中的一个类,它实现了 IApplicationFeatureProvider<ControllerFeature> 接口。这个类的主要作用是提供控制器类型的识别功能。

在 ASP.NET Core MVC 中,控制器是用来处理 HTTP 请求的类。传统的控制器类需要继承自 Controller 基类,但 ASP.NET Core 引入了一个新特性,即 POCO(Plain Old CLR Object)控制器。POCO 控制器允许你创建没有继承自 Controller 基类的类,但仍然可以将其识别为控制器,并赋予其处理 HTTP 请求的能力。

ControllerFeatureProvider 就是负责识别这些 POCO 控制器的类。它实现了 IApplicationFeatureProvider<ControllerFeature> 接口的 PopulateFeature 方法,该方法会在 MVC 框架构建应用模型时被调用。在这个方法中,ControllerFeatureProvider 会扫描应用程序中的类型,并根据一定的规则判断哪些类型应该被识别为控制器。

默认情况下,ControllerFeatureProvider 会将所有带有 “Controller” 后缀的类,或者使用了 [Controller] 特性的类识别为控制器。但你也可以通过自定义 ControllerFeatureProvider 的子类来提供自己的识别规则,以满足特定的需求。

https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/ControllerFeatureProvider.cs

实现自定义判断规则

通过上面的剖析,我们就知道要实现自定义的控制器判断规则,只需要重写ControllerFeature类或者重新实现IApplicationFeatureProvider接口,但是由于PopulateFeature不是虚方法或抽象方法,所以不能被重写,那么只能重新写一个类来实现IApplicationFeatureProvider接口了:

 public class MyDynamicControllerFeatureProvider : ControllerFeatureProvider
 {
     protected override bool IsController(TypeInfo typeInfo)
     {
            var typeInfo = type.GetTypeInfo();
         
            if (!typeof(IDynamicWebApi).IsAssignableFrom(type) ||
                !typeInfo.IsPublic || typeInfo.IsAbstract || typeInfo.IsGenericType)
            {
                return false;
            }


            var attr = ReflectionHelper.GetSingleAttributeOrDefaultByFullSearch<DynamicWebApiAttribute>(typeInfo);

            if (attr == null)
            {
                return false;
            }

            if (ReflectionHelper.GetSingleAttributeOrDefaultByFullSearch<NonDynamicWebApiAttribute>(typeInfo) != null)
            {
                return false;
            }

            return true;
     }
 }

IApplicationModelConvention

IApplicationModelConvention 是ASP.NET Core中的一个接口,它允许开发者在应用模型构建过程中应用自定义约定。ASP.NET Core 的应用模型是描述如何构建 HTTP 请求处理管道的一组组件和服务。

通过实现 IApplicationModelConvention 接口,开发者可以注册中间件、修改路由、添加模型绑定器、配置控制器和服务等。这些约定在应用的启动过程中被应用,通常在 Startup.ConfigureServices 方法中通过调用 AddApplicationPartApplyApplicationPartManager 方法来注册。

https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelconvention?view=aspnetcore-8.0

下面是一个简单的 IApplicationModelConvention 实现示例,该示例演示了如何为所有控制器添加一个自定义操作筛选器:

using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.ApplicationModels;  
  
public class CustomConvention : IApplicationModelConvention  
{  
    public void Apply(ApplicationModel application)  
    {  
        foreach (var controller in application.Controllers)  
        {  
            // 为每个控制器添加自定义操作筛选器  
            controller.Filters.Add(new CustomActionFilter());  
        }  
    }  
}  
  
public class CustomActionFilter : IActionFilter  
{  
    public void OnActionExecuting(ActionExecutingContext context)  
    {  
        // 在操作执行前执行的代码  
    }  
  
    public void OnActionExecuted(ActionExecutedContext context)  
    {  
        // 在操作执行后执行的代码  
    }  
}

然后,在 Startup.ConfigureServices 方法中注册这个约定:

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddControllers();  
  
    services.AddApplicationPart(typeof(Startup).Assembly)  
            .ApplyApplicationPartManager(manager =>  
            {  
                manager.Conventions.Add(new CustomConvention());  
            });  
}

在这个例子中,CustomConvention 被添加到了 ApplicationModel 的约定集合中。每当 ASP.NET Core 构建应用模型时,Apply 方法就会被调用,并且所有的控制器都会被添加 CustomActionFilter 筛选器。

Panda.DynamicWebApi中的实现

ConfigureApiExplorer()

首先,是对ApiExplorer进行配置。通过ApiExplorer,我们可以控制Controller级别和Action级别的Web API的可见性。一般情况下的用法是在Controller或者Action上添加ApiExplorerSettings标记,而在这里,我们只需要给ControllerModel和ActionModel的ApiExplorer属性赋值即可。

 private void ConfigureApiExplorer(ControllerModel controller)
 {
     if (controller.ApiExplorer.GroupName.IsNullOrEmpty())
     {
         controller.ApiExplorer.GroupName = controller.ControllerName;
     }

     if (controller.ApiExplorer.IsVisible == null)
     {
         controller.ApiExplorer.IsVisible = true;
     }

     foreach (var action in controller.Actions)
     {
         if (!CheckNoMapMethod(action))
             ConfigureApiExplorer(action);
     }
 }

 private void ConfigureApiExplorer(ActionModel action)
 {
     if (action.ApiExplorer.IsVisible == null)
     {
         action.ApiExplorer.IsVisible = true;
     }
 }
ConfigureSelector()

接下来,是对路由进行配置。这部分的核心其实就是根据AreaName、ControllerName、ActionName来生成路由信息,我们会为没有配置过特性路由的Action生成默认的路由,这其实就是MVC里约定大于配置的一种体现啦。在这里会涉及到对ControllerName和ActionName的优化调整,主要体现在两个方面,其一是对类似XXXService、XXXController等这样的后缀进行去除,使其构造出的Api路由更加短小精简;其二是对ActionName里的Get/Save/Update等动词进行替换,使其构造出的Api路由更加符合RESTful风格。

     private void ConfigureSelector(ControllerModel controller, DynamicWebApiAttribute controllerAttr)
     {

         if (controller.Selectors.Any(selector => selector.AttributeRouteModel != null))
         {
             return;
         }

         var areaName = string.Empty;

         if (controllerAttr != null)
         {
             areaName = controllerAttr.Module;
         }

         foreach (var action in controller.Actions)
         {
             if (!CheckNoMapMethod(action))
                 ConfigureSelector(areaName, controller.ControllerName, action);
         }
     }

     private void ConfigureSelector(string areaName, string controllerName, ActionModel action)
     {

         var nonAttr = ReflectionHelper.GetSingleAttributeOrDefault<NonDynamicWebApiAttribute>(action.ActionMethod);

         if (nonAttr != null)
         {
             return;
         }

         if (action.Selectors.IsNullOrEmpty() || action.Selectors.Any(a => a.ActionConstraints.IsNullOrEmpty()))
         {
             if (!CheckNoMapMethod(action))
                 AddAppServiceSelector(areaName, controllerName, action);
         }
         else
         {
             NormalizeSelectorRoutes(areaName, controllerName, action);
         }
     }

     private void AddAppServiceSelector(string areaName, string controllerName, ActionModel action)
     {

         var verb = GetHttpVerb(action);

         action.ActionName = GetRestFulActionName(action.ActionName);

         var appServiceSelectorModel = action.Selectors[0];

         if (appServiceSelectorModel.AttributeRouteModel == null)
         {
             appServiceSelectorModel.AttributeRouteModel = CreateActionRouteModel(areaName, controllerName, action);
         }

         if (!appServiceSelectorModel.ActionConstraints.Any())
         {
             appServiceSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb }));
             switch (verb)
             {
                 case "GET":
                     appServiceSelectorModel.EndpointMetadata.Add(new HttpGetAttribute());
                     break;
                 case "POST":
                     appServiceSelectorModel.EndpointMetadata.Add(new HttpPostAttribute());
                     break;
                 case "PUT":
                     appServiceSelectorModel.EndpointMetadata.Add(new HttpPutAttribute());
                     break;
                 case "DELETE":
                     appServiceSelectorModel.EndpointMetadata.Add(new HttpDeleteAttribute());
                     break;
                 default:
                     throw new Exception($"Unsupported http verb: {verb}.");
             }
         }


     }
ConfigureParameters()

接下来参数绑定相对简单,因为简单类型MVC自己就能完成绑定,所以,我们只需要关注复杂类型的绑定即可,最常见的一种绑定方式是FromBody:

        private void ConfigureParameters(ControllerModel controller)
        {
            foreach (var action in controller.Actions)
            {
                if (!CheckNoMapMethod(action))
                    foreach (var para in action.Parameters)
                    {
                        if (para.BindingInfo != null)
                        {
                            continue;
                        }

                        if (!TypeHelper.IsPrimitiveExtendedIncludingNullable(para.ParameterInfo.ParameterType))
                        {
                            if (CanUseFormBodyBinding(action, para))
                            {
                                para.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
                            }
                        }
                    }
            }
        }

image

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深度学习入门笔记(八)可以不断思考的模型:RNN与LSTM

8.1 循环神经网络RNN 之前学到的 CNN 和全连接&#xff0c;模型的输入数据之间是没有关联的&#xff0c;比如图像分类&#xff0c;每次输入的图片与图片之间就没有任何关系&#xff0c;上一张图片的内容不会影响到下一张图片的结果。但在自然语言处理领域&#xff0c;这就成了…

【Unity】QFramework通用背包系统优化:TipPanel优化

前言 在学习凉鞋老师的课程《QFramework系统设计&#xff1a;通用背包系统》第五章时&#xff0c;笔者对物品提示TipPanel界面进行了一些优化。 优化内容包括&#xff1a; 解决闪烁问题跟随鼠标移动自适应界面大小生成位置优化 效果还是蛮丝滑的&#xff1a; 解决闪烁问题 …

JQuery学习一

JQuery学习一 文章目录 前言一、JQuery是什么&#xff1f;二、属性和内容2.1. jQuery选择器2.2. attr() 方法2.3. text()方法2.4. val()方法2.5. 小结2.6. 添加内容 三、操作CSS3.1. addClass() 方法3.2. ​removeClass() ​方法3.3. toggleClass() 方法3.4. CSS()方法3.5. jQu…

REvil/Sodinokibi勒索病毒通用解密工具

前言 REvil/Sodinokibi勒索病毒相信关注我公众号的朋友&#xff0c;应该都不会陌生了&#xff0c;如果不清楚的可以去翻看之前的文章吧&#xff0c;如果你见过类似下面这样的勒索病毒攻击之后的电脑桌面&#xff0c;如下所示&#xff1a; 或者你见过这样的勒索提示界面&#x…

“金龙送礼,昂首贺春”—— Anzo Capital给您送五粮液、茅台啦!

“迎龙年&#xff0c;贺新春”—— 值此龙年将至之际&#xff0c;为答谢新老客户一直以来对Anzo Capital昂首资本的信赖和支持&#xff0c;Anzo Capital昂首资本2月入金送礼活动重磅升级&#xff0c;除了京东卡、天猫超市卡、奔富红酒、SKG健康产品、白酒礼盒以外&#xff0c…

ncc匹配(五,匹配提速的思考)

感觉ncc&#xff08;相关系数匹配&#xff09;与bpnet&#xff08;bp神经网络&#xff09;相似&#xff0c;但ncc简洁方便快速&#xff0c;计算量小&#xff0c;问题点也少。 都有归一化的动作&#xff0c;都是相关性的学习&#xff0c;不过bpnet可以学习多种类型&#xff0c;…

【Unity】重力场中的路径预测方法

前言 笔者前些天参加完了一场72小时的GameJam游戏开发比赛。这次比赛的主题是“探索”&#xff0c;笔者做了一个名为《探索者号》的探索宇宙的游戏&#xff08;游戏名一开始叫做《星际拾荒者》&#xff0c;但这不重要&#xff09;。 在开发过程中&#xff0c;笔者遇到了一些问…

Django的配置文件setting.py

BASE_DIR 项目路径&#xff1a;默认是已经打开的主项目路径 ​​​​​​​BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY 密钥 SECRET_KEY (dh&_fm2hfn9y)35!_6#$a7q%%^onoy#-a8x18r4(6*8f(aniDEBUG 帮助调试&#xff0c;默认…

【pip】本地和Anaconda的pip冲突时如何指定安装位置

输入指令&#xff1a; where pip 显示如下&#xff1a; D:\LenovoSoftstore\Anaconda\Scripts\pip.exe C:\python\python3.8\Scripts\pip.exe 可以看到有两个位置的pip&#xff0c;一个Anaconda下的pip&#xff0c;一个是本地的pip。 当我们使用pip安装的时候&#xff0c;系…

JavaScript综合练习3

JavaScript 综合练习 3 1. 案例演示 2. 代码实现 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewpor…

C语言操作符超详细总结

文章目录 1. 操作符的分类2. 二进制和进制转换2.1 2进制转10进制2.1.1 10进制转2进制数字 2.2 2进制转8进制和16进制2.2.1 2进制转8进制2.2.2 2进制转16进制 3. 原码、反码、补码4.移位操作符4.1 左移操作符4.2 右移操作符 5. 位操作符&#xff1a;&、|、^、~6. 逗号表达式…

【数据结构】堆(创建,调整,插入,删除,运用)

目录 堆的概念&#xff1a; 堆的性质&#xff1a; 堆的存储方式&#xff1a; 堆的创建 &#xff1a; 堆的调整&#xff1a; 向下调整&#xff1a; 向上调整&#xff1a; 堆的创建&#xff1a; 建堆的时间复杂度&#xff1a; 向下调整&#xff1a; 向上调整&#xff…

电商小程序05用户注册

目录 1 搭建页面2 设置默认跳转总结 我们上一篇拆解了登录功能&#xff0c;如果用户没有账号就需要注册了。本篇我们介绍一下注册功能的实现。 1 搭建页面 打开应用&#xff0c;点击左上角的新建页面 输入页面的名称&#xff0c;用户注册 删掉网格布局&#xff0c;添加表单容…

Cisco firepower2100系列使用FDM管理FTD

Cisco firepower2100系列使用FDM管理FTD 啥是FDM Firepower Device Manager 当思科Firepower系列运行的FTD镜像时&#xff0c;可以通过2种方式进行管理 第1种方式&#xff1a; FMC (Firepower management Center) 可以进行统一管理&#xff0c;一台FMC可以管理多个FTD&…

RK3568笔记十二:Zlmedia拉流显示测试

若该文为原创文章&#xff0c;转载请注明原文出处。 Zlmediakit功能很强大&#xff0c;测试一下拉流&#xff0c;在通过解码显示。 一、环境 1、平台&#xff1a;rk3568 2、开发板:ATK-RK3568正点原子板子 3、环境&#xff1a;buildroot 测试的代码在GitHub - airockchip/…

Stable Diffusion教程——使用TensorRT GPU加速提升Stable Diffusion出图速度

概述 Diffusion 模型在生成图像时最大的瓶颈是速度过慢的问题。为了解决这个问题&#xff0c;Stable Diffusion 采用了多种方式来加速图像生成&#xff0c;使得实时图像生成成为可能。最核心的加速是Stable Diffusion 使用了编码器将图像从原始的 3512512 大小转换为更小的 46…

91 xxl-job executor 还存在 并且 job 正在执行, 但是 job 被标记为 “任务结果丢失,标记失败“

前言 最近出现了一个这样的问题 我们生产环境中的一个 xxl-job 任务, 很大一部分执行记录被标记为 "任务结果丢失&#xff0c;标记失败", 几乎是 98% 吧 然后 调试的时候 存在几个令人疑惑的地方 1. 通过 xxl-job 点击查看任务的执行记录的日志, 日志为空, …

异步编程(JS)

前言 想要学习Promise&#xff0c;我们首先要了解异步编程、回调函数、回调地狱三方面知识&#xff1a; 异步编程 异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。 与此同时&#xff0c;你的程序也将在任务完成后显示…

《剑指 Offer》专项突破版 - 面试题 37 : 小行星碰撞(C++ 实现)

题目链接&#xff1a;LCR 037. 行星碰撞 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 输入一个表示小行星的数组&#xff0c;数组中每个数字的绝对值表示小行星的大小&#xff0c;数字的正负号表示小行星运动的方向&#xff0c;正号表示向右飞行&#xff0c;负…

【开源】SpringBoot框架开发医院门诊预约挂号系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 功能性需求2.1.1 数据中心模块2.1.2 科室医生档案模块2.1.3 预约挂号模块2.1.4 医院时政模块 2.2 可行性分析2.2.1 可靠性2.2.2 易用性2.2.3 维护性 三、数据库设计3.1 用户表3.2 科室档案表3.3 医生档案表3.4 医生放号…
最新文章