一文让你了解微服务契约测试

谈到微服务,大家都想到契约测试,到底什么是契约测试呢,为什么要使用契约测试呢,关于这样的文章很多,本文将结合Spring Boot让你了解微服务契约测试。

首先我们来看一下微服务,微服务是一种分布式结构,对于一种服务一方为服务的提供者,另一方为服务的消费者。我们用一个虚拟的阿里产品体系做个对比,比如登录这个接口,对于许多电商模块(我们称为服务)要使用,比如:天猫、淘宝、飞猪、盒马。这些登录接口可以由用户服务中台来提供,大家使用统一的登录接口,防止重复开发。

在没有契约测试之前,当用户接口没有提供天猫、淘宝、飞猪、盒马登录使用用户服务中台提供的Stub,用户服务中台提供的Stub由用户服务中台开发团队来维护,当用户服务中台开发完毕,天猫、淘宝、飞猪、盒马登录才使用真正的登录模块。由此可以看到每一个服务提供者模块在开发自身的业务模块之前,还要维护服务Stub模块,这样大大增加了开发成本。    

有了契约测试,只要服务提供者提供契约文件及基于契约文件自动产生的stub模块给服务消费者,服务消费者利用契约文件也产生同样的Stub模块,在服务提供者没有开发可用的真正的程序之前,利用Stub模块进行调试。以上是契约测试的一个优点,把维护stub模块变为了维护契约文件,这样大大地节约开发成本;另外还可以发现接口变动问题。

这是最初服务生产者提供的接口body接口

  1. {

  2. "年龄":"37"

  3. "性别":"男"

  4. "姓名":"王睿"

  5. }

服务消费者A提供的接口是

  1. {

  2. "年龄":"37"

  3. "性别":"男"

  4. }

没有“姓名”,这个是允许的。

服务消费者B提供的接口是

  1. {

  2. "年龄":"37"

  3. "性别":"男"

  4. "姓名":"王睿"

  5. }

这个与提供者完全一致,当然是允许的。

服务消费者C提供的接口是

  1. {

  2. "性别":"男"

  3. "姓名":"王睿"

  4. }

没有“年龄”,这个是允许的。

某一天,服务消费者C由于业务要求,需要把姓名中的姓与名拆成两部分,修改了body格式

  1. {

  2. "性别":"男"

  3. "姓":"王"

  4. "名":"睿"

  5. }

并提给服务生产者这个接口需求变更,生产者接受了这个请求,将契约文件改为

  1. {

  2. "性别":"男"

  3. "姓":"王"

  4. "名":"睿"

  5. }

当这个契约文件分发给各个服务消费者,由于服务消费者A提供的接口是

  1. {

  2. "年龄":"37"

  3. "性别":"男"

  4. }

由于没有“姓名”变更不受影响,而服务消费者B提供的接口是

  1. {

  2. “年龄”:"37"

  3. “性别”:"男"

  4. “姓名”:"王睿"

  5. }

姓名没有拆分,所以测试失败,告知大家,线下协商策略,决定 “姓名”是否修改。

在这里知道相关的服务消费者只有3个,而在实际的产品中服务多达成百上千个,有的是服务生产者。有的是服务消费者,大部分既是服务生产者又是服务消费者。当某一个接口发生变化,不运行契约测试不知道哪些模块会受到变动的影响,另外最后决定接口是否修改,也是根据fail接口的数量及fail接口的优先等级来决定的。

下面我们用一个具体的Spring Boot的案例来进行介绍,在这个案例中,流程是这样的。

  1. 服务生产者开发契约文件程序,自动形成契约文件。

  2. 将形成的契约文件打包上传到GitHub中。

  3. 服务消费者开发之前从GitHub中下载本地契约文件到本地目录下。

  4. 运行测试文件,验证测试是否满足现在的契约文件。

在这里我使用Spring Boot+cucumber+契约测试文章中的案例

服务生产者Spring Boot pom.xml文件如下:

 

<?xml version="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.3</version><relativePath/><!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>ATMService</artifactId><version>0.0.1-SNAPSHOT</version><name>ATMService</name><description>Demo project for Spring Boot</description><properties><java.version>11</java.version><cucumber.version>6.8.1</cucumber.version><spring-cloud.version>4.1.0</spring-cloud.version><spring-cloud.version>2023.0.0</spring-cloud.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-contract-stub-runner</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-contract-wiremock</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-contract-verifier</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.restdocs</groupId><artifactId>spring-restdocs-mockmvc</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type></dependency><dependency><groupId>io.cucumber</groupId><artifactId>cucumber-java</artifactId><version>${cucumber.version}</version><scope>test</scope></dependency><dependency><groupId>io.cucumber</groupId><artifactId>cucumber-spring</artifactId><version>${cucumber.version}</version><scope>test</scope></dependency><dependency><groupId>io.cucumber</groupId><artifactId>cucumber-junit-platform-engine</artifactId><version>${cucumber.version}</version><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-contract-maven-plugin</artifactId><version>4.1.0</version><extensions>true</extensions><configuration><testFramework>JUNIT5</testFramework></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

 在test下建立StubsGenerator.java

 

package com.example.ATMService;import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;import org.springframework.cloud.contract.wiremock.restdocs.SpringCloudContractRestDocs;import org.junit.jupiter.api.Test;import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import org.springframework.beans.factory.annotation.Autowired;@AutoConfigureRestDocs(outputDir = "target/stubs/META-INF/com.example")@AutoConfigureMockMvc@SpringBootTestpublic class StubsGenerator {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate MockMvc mockMvc;@Testpublic void verify_pin_ok()throws Exception {mockMvc.perform(MockMvcRequestBuilders.get("/verify_pin/1111222233?pin=123456").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andDo(document("verify_pin",SpringCloudContractRestDocs.dslContract()));}@Testpublic void verify_pin_fail()throws Exception {mockMvc.perform(MockMvcRequestBuilders.get("/verify_pin/1111222233?pin=654321").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andDo(document("verify_pin_fail",SpringCloudContractRestDocs.dslContract()));}}

程序将模拟一个HTTP请求,并且建立契约文件

  • @AutoConfigureRestDocs(outputDir = "target/stubs/META-INF/com.example")//定义契约文件位置

  • 当/verify_pin/1111222233?pin=123456为Get请求时.andExpect(status().isOk()) //返回状态码为200;返回内容在Controller程序中定义。

  • .andDo(document("verify_pin",SpringCloudContractRestDocs.dslContract()));//建立名为verify_pin的契约文件

  • 当/verify_pin/1111222233?pin=654321为Get请求时

  • .andExpect(status().isOk()) //返回状态码为200;返回内容在Controller程序中定义。

  • .andDo(document("verify_pin_fail",SpringCloudContractRestDocs.dslContract()));//建立名为verify_pin_fail的契约文件

接下来我们看一下main中建立的pageController.java。

 

package com.example.ATMService.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class pageController {@GetMapping("/verify_pin/1111222233")public String list(String pin){String list = "";if (!pin.equals("123456")){list = list + "{\"result\":\"Your PlN is apnalnd\"}";}else {list = list + "{\"result\":\"OK\"}";}return list;}}

@GetMapping("/verify_pin/1111222233")获得路径为"/verify_pin/1111222233"

当参数pin!="123456",返回body体为"{\"result\":\"Your PlN is apnalnd\"}"

否则返回body体为"{\"result\":\"OK\"}"。

用JUnit运行StubsGenerator.java,测试通过    

在@AutoConfigureRestDocs(outputDir = "target/stubs/META-INF/com.example")生成契约文件(如果pageController存在错误,系统将产生不了契约文件)。

verify_pin_fail.groovy与verify_pin.groovy是产生的契约文件。

 

import org.springframework.cloud.contract.spec.ContractContract.make {request {method 'GET'urlPath('/verify_pin/1111222233') {queryParameters {parameter('''pin''', '''123456''')}}headers {header('''Accept''', '''application/json''')header('''Content-Type''', '''application/json;charset=UTF-8''')}}response {status 200body('''{"result":"OK"}''')headers {header('''Content-Type''', '''application/json''')}}}

verify_pin_fail.groovy

 

import org.springframework.cloud.contract.spec.ContractContract.make {request {method 'GET'urlPath('/verify_pin/1111222233') {queryParameters {parameter('''pin''', '''654321''')}}headers {header('''Accept''', '''application/json''')header('''Content-Type''', '''application/json;charset=UTF-8''')}}response {status 200body('''{"result":"Your PlN is apnalnd"}''')headers {header('''Content-Type''', '''application/json''')}}}

verify_pin.json与verify_pin_fail.json是verify_pin_fail.groovy与verify_pin.groovy目标文件,也是真正起作用的文件。
verify_pin.json

 

{"id" : "a8c7b023-e2dc-4105-956c-f6b46e65d447","request" : {"urlPath" : "/verify_pin/1111222233","method" : "GET","headers" : {"Content-Type" : {"equalTo" : "application/json;charset=UTF-8"},"Accept" : {"equalTo" : "application/json"}},"queryParameters" : {"pin" : {"equalTo" : "123456"}}},"response" : {"status" : 200,"body" : "{\"result\":\"OK\"}","headers" : {"Content-Type" : "application/json"}},"uuid" : "a8c7b023-e2dc-4105-956c-f6b46e65d447"}

verify_pin_fail.json

 

{"id" : "ccf996bb-0388-4a3b-83d7-d2891a303c80","request" : {"urlPath" : "/verify_pin/1111222233","method" : "GET","headers" : {"Content-Type" : {"equalTo" : "application/json;charset=UTF-8"},"Accept" : {"equalTo" : "application/json"}},"queryParameters" : {"pin" : {"equalTo" : "654321"}}},"response" : {"status" : 200,"body" : "{\"result\":\"Your PlN is apnalnd\"}","headers" : {"Content-Type" : "application/json"}},"uuid" : "ccf996bb-0388-4a3b-83d7-d2891a303c80"}

将产生的verify_pin_fail.groovy与verify_pin.groovy契约文件(注意:不是verify_pin_fail.json与verify_pin.json)拷贝到src/test/resources/contracts下,运行mvn spring-cloud-contract:convert&&mvn spring-cloud-contract:run命令,当出现:

 

Host: []Content-Length: [538]Content-Type: [text/plain; charset=UTF-8]Connection: [keep-alive]User-Agent: [Apache-HttpClient/5.1.3 (Java/17.0.10)]{"id" : "16d3118b-27c6-48fb-b020-83dfd89ef7db","request" : {"urlPath" : "/verify_pin/1111222233","method" : "GET","queryParameters" : {"pin" : {"equalTo" : "654321"}}},"response" : {"status" : 200,"body" : "{\"result\":\"Your PlN is apnalnd\"}","headers" : {"Content-Type" : "application/json;charset=UTF-8"},"transformers" : [ "response-template", "spring-cloud-contract" ]},"uuid" : "16d3118b-27c6-48fb-b020-83dfd89ef7db"}[INFO] Started stub server for project [C:\Code\MyJava\javawork\card\target\stubs:+:stubs] on port 8080 with [2] mappings[INFO] All stubs are now running RunningStubs [namesAndPorts={C:\Code\MyJava\javawork\card\target\stubs:+:stubs=8080}][INFO] Press ENTER to continue...

在浏览器中输入:http://127.0.0.1:8080/verify_pin/1111222233?pin=123456

输入:http://127.0.0.1:8080/verify_pin/1111222233?pin=654321

接下来将契约文件上传到GitHub中( 由于我没有GitHub Server,所以没有实现)。

第一次服务消费者从GitHub下载契约文件到本地,在target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests自动形成ContractVerifierTest.java文件。

 

package org.springframework.cloud.contract.verifier.tests;import com.jayway.jsonpath.DocumentContext;import com.jayway.jsonpath.JsonPath;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;import io.restassured.response.ResponseOptions;import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;@SuppressWarnings("rawtypes")public class ContractVerifierTest {@Testpublic void validate_verify_pin() throws Exception {// given:MockMvcRequestSpecification request = given();// when:ResponseOptions response = given().spec(request).queryParam("pin","123456").get("/verify_pin/1111222233");// then:assertThat(response.statusCode()).isEqualTo(200);assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");// and:DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());assertThatJson(parsedJson).field("['result']").isEqualTo("OK");}@Testpublic void validate_verify_pin_fail() throws Exception {// given:MockMvcRequestSpecification request = given();// when:ResponseOptions response = given().spec(request).queryParam("pin","654321").get("/verify_pin/1111222233");// then:assertThat(response.statusCode()).isEqualTo(200);assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");// and:DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());assertThatJson(parsedJson).field("['result']").isEqualTo("Your PlN is apnalnd");}}

启动本地服务,编写然后运行测试文件CardApplicationTests.java

 

import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.web.client.RestTemplate;@SpringBootTestclass CardApplicationTests {private RestTemplate restTemplate = new RestTemplate();@Testvoid pass() throws Exception{String result = restTemplate.getForObject("http://localhost:8080/verify_pin/1111222233?pin=123456", String.class);Assertions.assertEquals(result, "{\"result\":\"OK\"}");}@Testvoid fail() throws Exception{String result = restTemplate.getForObject("http://localhost:8080/verify_pin/1111222233?pin=654321", String.class);Assertions.assertEquals(result,"{\"result\":\"Your PlN is apnalnd\"}");}}

保证测试通过

以后每次运行CardApplicationTests.java之前,都从GitHub上下载契约文件,启动本地服务。如果测试fail,说明接口发生了变化,通过团队线下解决。 

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:【文末自行领取】

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

详细介绍MES系统的生产监控模块

MES系统的监控模块是系统的重要组成部分&#xff0c;它负责实时监控生产过程中的各项关键指标和数据&#xff0c;以确保生产活动的顺利进行和高效管理。以下是MES系统监控模块的详细介绍&#xff1a; 一、MES系统监控模块的主要功能 实时数据采集&#xff1a; 监控模块通过传…

解决uniapp开发的app,手机预览,上下滑动页面,页面出现拉伸,抖动的效果问题,

在pages.json文件里“globalStyle”下面的"app-plus"里加入"bounce": "none"即可 "app-plus": { "bounce": "none", //关闭窗口回弹效果 }

MFC -文件类控件

前言 各位师傅大家好&#xff0c;我是qmx_07&#xff0c;今天给大家讲解MFC中的文件类 MFC文件类 在MFC中&#xff0c;CFILE 是基本的文件操作类&#xff0c;提供了读取、写入、打开、关闭等操作方法主要成员函数:Open(用于打开文件&#xff0c;设置模式 例如 只读 只写 读…

(done) 声音信号处理基础知识(1)

来源&#xff1a;https://www.youtube.com/watch?viCwMQJnKk2c 声学处理应用场景如下 这个系列的内容包括如下&#xff1a; 作者的 slack 频道 油管主的 github repo: https://github.com/musikalkemist/AudioSignalProcessingForML

滑动条QSlider

可以在一个范围内拖动。 常用属性和方法 值 包括当前值、最大值、最小值 // 获取和设置当前值 int value() const; void setValue(int);// 获取和设置最大值 int maximum() const; void setMaximum(int);// 获取和设置最小值 int minimum() const; void setMinimum(int);//…

【深度学习】聊一聊正则化

在机器学习中&#xff0c;正则化是一种常用的技术&#xff0c;用于控制模型的复杂度&#xff0c;减少过拟合的风险。它通过在损失函数中引入额外的项来对模型的参数进行约束或惩罚&#xff0c;使模型更加简单、平滑或稀疏。我们在实际应用中&#xff0c;经常使用的是L1和L2正则…

内网渗透之中间人欺骗攻击-ARP攻击

ARP攻击 ARP协议简介 ARP全称为Address Resolution Protocol&#xff0c;即地址解析协议&#xff0c;它是一个根据IP地址获取物理地址的TCP/IP协议&#xff0c;主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机&#xff0c;并接收返回消息&#xff0c;以此确定…

李宏毅2023机器学习作业HW07解析和代码分享

ML2023Spring - HW7 相关信息&#xff1a; 课程主页 课程视频 Kaggle link 回来了 : ) Sample code HW07 视频 HW07 PDF 个人完整代码分享: GitHub | Gitee | GitCode P.S. HW7 的代码都很易懂&#xff0c;可以和 2024 年的新课&#xff1a;生成式AI导论做一个很好的衔接&#…

高性能string库-stringzilla

这段时间在优化服务耗时问题&#xff0c;其中perf打出来的热点显示&#xff0c;有一部分热点集中在string find. 由于之前看到sonic-cpp在使用simd加速string的一些操作&#xff0c;所以当时我想使用AVX2实现一版strstr来加速这个过程。但是在实现过程中&#xff0c;碰到一些问…

【速成Redis】03 Redis 五大高级数据结构介绍及其常用命令 | 消息队列、地理空间、HyperLogLog、BitMap、BitField

前言&#xff1a; 上篇博客我们讲到redis五大基本数据类型&#xff08;也是就下图的第一列&#xff09;。 【速成Redis】02 Redis 五大基本数据类型常用命令-CSDN博客文章浏览阅读1k次&#xff0c;点赞24次&#xff0c;收藏10次。该篇适用于速成redis。本篇我们将讲解&#…

Innodb存储架构

Innodb整体存储架构 Innodb是一款兼顾性能及可靠性的存储引擎&#xff0c;主要分为内存存储结构和磁盘存储结构&#xff0c;二者分别扮演着提高性能和数据持久化的工作 内存结构中定义了缓冲池、变更缓冲区、日志缓冲区、自适应哈希四个缓冲区&#xff0c;它们均是为提升查询…

linux网络-----传输层

前言 一.传输层&#xff1a; 数据要交接应用层先通过传输层&#xff08;给哪个程序发数据&#xff09; 传输层作用&#xff1a;负责数据能够从发送端传输接收端。对于应用层来说有许多服务&#xff0c;传输层怎么知道把数据发给那个应用服务&#xff1f; 这时就有了端口号&am…

kubernetes中的认证授权

目录 一、kubernetes API访问控制 1、UserAccount与ServiceAccount &#xff08;1&#xff09;ServiceAccount &#xff08;2&#xff09;示例 二、认证&#xff08;在k8s中建立认证用户&#xff09; 1、创建UserAccount 2、RBAC&#xff08;Role Based Access Control&…

Redis——redispluspls库——通用命令以及String类型相关接口使用

文章目录 通用命令get&#xff0c;setkeys插入迭代器 expire和ttltype string 类型接口set和getset NX和XXmset 和 mgetgetrange 和 setrangeincr 和 decr 通用命令 get&#xff0c;set void get_set_test(sw::redis::Redis& redis){//bool set(const sw::redis::StringV…

Iterative Regularized Policy Optimization with Imperfect Demonstrations

ICML 2024 paper code Intro 利用基于次优专家数据的专家策略&#xff0c;通过policy constraint的形式引导智能体的在线优化&#xff0c;同时通过利用在线高质量数据扩展专家数据&#xff0c;并有监督得对专家策略进行矫正。二者交替优化实现目标策略的迭代更新 Method 上述…

51单片机-红外遥控器(NEC标准)-实验(红外遥控及调速电机)

作者&#xff1a;Whappy 时间&#xff1a;2024.9.20 总结一下&#xff01;基础实验到这儿里就圆满结束&#xff0c;历经25天&#xff0c;将51单片机学完并亲自手敲代码近5000行&#xff0c;在手敲代码过程中&#xff0c;明显感觉的看和敲&#xff0c;明显就是不同的感觉&…

STM32 通过 SPI 驱动 W25Q128

目录 一、STM32 SPI 框图1、通讯引脚2、时钟控制3、数据控制逻辑4、整体控制逻辑5、主模式收发流程及事件说明如下&#xff1a; 二、程序编写1、SPI 初始化2、W25Q128 驱动代码2.1 读写厂商 ID 和设备 ID2.2 读数据2.3 写使能/写禁止2.4 读/写状态寄存器2.5 擦除扇区2.6 擦除整…

基于SpringBoot的在线点餐系统【附源码】

​基于SpringBoot的高校社团管理系统&#xff08;源码L文说明文档&#xff09; 4 系统设计 4.1 系统概述 网上点餐系统的结构图4-1所示&#xff1a; 图4-1 系统结构 模块包括主界面&#xff0c;首页、个人中心、用户管理、美食店管理、美食分类管理、美食…

前端开发者必学:mo.js动画库

前端开发者必学&#xff1a;mo.js动画库 前言 在当今的网页设计中&#xff0c;动态效果和交互性是提升用户体验的关键因素。 mo.js&#xff0c;一个轻量级的 JavaScript 动画库&#xff0c;为前端开发者提供了一种简单而强大的方法来创建引人注目的动画效果。 本文将向您介…

Nature|PathChat:病理学多模态生成性AI助手的创新与应用|顶刊精析·24-09-21

小罗碎碎念 今日顶刊&#xff1a;Nature 这篇文章今年6月就发表了&#xff0c;当时我分析的时候&#xff0c;还是预印本&#xff0c;没有排版。今天第一篇推文介绍的是Faisal Mahmood &#xff0c;所以又把这篇文章拉出来详细分析一下。 作者角色作者姓名单位名称单位英文名称第…