ToB企服应用市场:ToB评测及商务社交产业平台
标题:
十七:Spring Boot依靠 (2)-- spring-boot-starter-web 依靠详解
[打印本页]
作者:
宁睿
时间:
2025-1-7 20:02
标题:
十七:Spring Boot依靠 (2)-- spring-boot-starter-web 依靠详解
目录
1. spring-boot-starter-web 简介
1.1 作用与功能:
1.2 引入方式:
1.3 包罗的核心依靠:
2. 自动配置原理
3. 内嵌 Servlet 容器
3.1 默认 Tomcat 配置:
3.2 更换容器(Jetty 或 Undertow):
4. 构建 RESTful Web 服务:
4.1?什么是 RESTful Web 服务
4.2 创建 REST 控制器
5. 自动处理 JSON:
6. 静态资源支持
7. Web 配置定制(通过 WebMvcConfigurer)
7.1 注册拦截器(Interceptor)
7.2 配置静态资源处理
7.2.1 addResourceHandlers(ResourceHandlerRegistry registry) 方法
7.2.2 registry.addResourceHandler(“/assets/**”)
7.2.3 addResourceLocations(“classpath:/static/assets/”)
7.3 配置视图解析器(ViewResolver)
7.3.1 pom.xml 加两个引用 支持jsp 的
7.3.2 配置视图解析器
7.3.3 创建jsp页面
7.3.4 写controller 一定用@Controller
7.3.4 浏览器访问 乱码 无所谓 只要能请求到 就没大题目
7.3.5 工作原理
7.4 CORS 配置(跨域资源共享)
7.4.1 什么是跨域?
7.4.2 为什么会有跨域题目?
7.4.3 同源计谋(Same-Origin Policy)
7.4.4 跨域的场景???
7.4.5 浏览器的跨域限定
7.4.6 跨域的办理方案
???7.4.6.1 CORS(跨域资源共享)
7.4.6.2 JSONP(仅限 GET 请求)
7.4.6.3 服务器端代理(推荐)
7.5 消息转换器(Message Converters)
7.8 定制非常处理(@ExceptionHandler)(不推荐)
8. 支持文件上传与下载
???8.1?文件上传
8.1.1 配置文件上传的基本设置
8.1.2 实现文件上传接口
8.1.3 上传目录配置
8.1.4 上传多个文件
8.2 文件下载
8.2.1 实现文件下载接口
8.2.2 设置响应头部以下载文件
1. spring-boot-starter-web 简介
1.1 作用与功能
:
spring-boot-starter-web 是 Spring Boot 的一个启动器(starter),用于构建 Web 应用,它自动配置了多种常见的 Web 组件,尤其得当构建 RESTful Web 服务。
1.2 引入方式
:
Maven:在 pom.xml 中添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
复制代码
Gradle
:在 build.gradle 中添加:
implementation 'org.springframework.boot:spring-boot-starter-web'
复制代码
1.3 包罗的核心依靠
:
Spring MVC
:构建 Web 应用的基础框架,提供了控制器、视图解析等功能。
内嵌 Servlet 容器(Tomcat)
:提供一个内嵌的默认 Servlet 容器,简化部署。
Jackson
:用于 JSON 数据的序列化和反序列化。
Spring Boot 自动配置
:自动配置 DispatcherServlet,自动配置 Spring MVC 相干的功能。
2. 自动配置原理
DispatcherServlet 自动配置
:
Spring Boot 会自动配置 DispatcherServlet,它是 Spring MVC 的核心,用于路由请求到适当的控制器方法。
Spring MVC 相干的自动配置
:
spring-boot-starter-web 自动配置了 Spring MVC 所需的 MessageConverter、视图解析器等,简化了手动配置。
Tomcat 自动配置
:
默认情况下,spring-boot-starter-web 利用 Tomcat 作为嵌入式容器,你可以通过配置改变容器(如利用 Jetty 或 Undertow)。
3. 内嵌 Servlet 容器
3.1 默认 Tomcat 配置
:
默认内嵌容器是
Tomcat
,并且默认端口是 8080。
可以通过 application.properties 或 application.yml 修改端口:
server.port=8081
复制代码
3.2 更换容器(Jetty 或 Undertow)
:
假如你不想利用 Tomcat,可以排除它并利用 Jetty 或 Undertow:
排除 Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
复制代码
添加 Jetty 或 Undertow:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
复制代码
4. 构建 RESTful Web 服务:
4.1
什么是 RESTful Web 服务
REST(Representational State Transfer)是一种通过 HTTP 协议与 Web 服务交互的架构风格。RESTful Web 服务遵照一系列约定,通常利用 HTTP 方法(如 GET、POST、PUT、DELETE)来举行资源的创建、查询、更新和删除操作。每个资源通常由一个 URL 唯一标识,且资源的数据通常以 JSON 返回。
4.2 创建 REST 控制器
在 Spring Boot 中,构建 RESTful 服务的核心是 @RestController 注解。@RestController 是一个结合了 @Controller 和 @ResponseBody 注解的注解,体现该类是一个控制器,且返回的内容会自动以 JSON 或 XML 格式返回(根据客户端请求的 Accept 头)。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
复制代码
@RestController
:体现该类是一个 REST 控制器,返回数据会被自动序列化为 JSON 格式。
@RequestMapping("/api")
:为所有请求添加一个基础路径 /api。
@GetMapping("/hello")
:处理 GET 请求,当客户端访问 /api/hello 时,返回 "Hello, World!"。
@PostMapping
:处理 HTTP POST 请求,用于创建新用户。
@PutMapping
:处理 HTTP PUT 请求,用于更新用户信息。
@DeleteMapping
:处理 HTTP DELETE 请求,用于删除用户。
5. 自动处理 JSON:
spring-boot-starter-web 默认集成了 Jackson 序列化和反序列化,自动将 Java 对象与 JSON 数据举行转换。
你可以利用 @RequestBody 注解接收请求体中的 JSON 数据,利用 @ResponseBody 返回 JSON 数据。
6. 静态资源支持
默认情况下,Spring Boot 会从 /static、/public、/resources 和 /META-INF/resources 目录提供静态资源。
可以将静态文件(如 HTML、CSS、JavaScript、图片等)放入这些目录中,Spring Boot 会自动提供访问。
自定义静态资源路径
:
可以通过配置文件修改静态资源的根目录
spring.resources.static-locations=classpath:/custom-static/
复制代码
7. Web 配置定制(通过 WebMvcConfigurer)
在开发 Spring Web 应用时,WebMvcConfigurer 是一个非常常用的接口,它提供了一种灵活的方式来定制 Spring MVC 的行为。
7.1 注册拦截器(Interceptor)
package com.lirui.springbootmoduledemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册一个拦截器
registry.addInterceptor(new WebInterceptor())
// 设置拦截路径
.addPathPatterns("/api/**")
// 排除不需要拦截的路径
.excludePathPatterns("/api/login", "/api/register");
}
}
package com.lirui.springbootmoduledemo.config;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class WebInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 这里可以做一些拦截前的校验或操作
System.out.println("preHandle: 请求即将到达Controller");
// 返回true表示继续处理请求,false表示请求被拦截,不会继续执行
return true; // 如果返回false,请求会被拦截,后续不会执行
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 请求处理之后,视图渲染之前调用
System.out.println("postHandle: 请求已处理,视图渲染之前");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求处理完后,视图渲染完毕后调用(通常用于清理资源)
System.out.println("afterCompletion: 请求完成后,视图渲染之后");
}
}
复制代码
7.2 配置静态资源处理
package com.lirui.springbootmoduledemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册一个拦截器
registry.addInterceptor(new WebInterceptor())
// 设置拦截路径
.addPathPatterns("/api/**")
// 排除不需要拦截的路径
.excludePathPatterns("/api/login", "/api/register");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置 /assets/** 请求,映射到 classpath:/static/assets/ 文件夹
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/");
// 配置 /images/** 请求,映射到 classpath:/static/images/ 文件夹
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/static/images/");
}
}
复制代码
7.2.1 addResourceHandlers(ResourceHandlerRegistry registry) 方法
addResourceHandlers 是 Spring MVC 提供的一个方法,用于配置静态资源的处理方式。ResourceHandlerRegistry 用来注册静态资源的访问路径和实际的资源位置。
addResourceHandlers 方法答应你定义一组
资源处理器
(Resource Handlers),这些处理器负责处理静态资源请求并返回相应的资源。通过配置,Spring MVC 可以大概自动映射 URL 路径到实际的文件存放位置,从而提供静态文件的访问功能。
7.2.2 registry.addResourceHandler(“/assets/**”)
registry.addResourceHandler("/assets/**")
addResourceHandler("/assets/**") 指定了一个
资源请求的映射路径
,即用户访问 /assets/ 路径下的任何 URL(匹配 /assets/**)时,都会交给 Spring MVC 来处理。
** 是一种通配符,体现匹配 /assets/ 后面所有的路径(包括子目录)。
例如,访问 http://localhost:8080/assets/img/logo.png 或者 http://localhost:8080/assets/css/style.css 都会被映射到对应的静态资源文件。
7.2.3 addResourceLocations(“classpath:/static/assets/”)
addResourceLocations("classpath:/static/assets/") 指定了
静态资源文件的实际存储路径
。classpath:/static/assets/ 是资源文件所在的路径。
classpath: 体现资源位置在类路径中。Spring Boot 默认会将静态资源放在 src/main/resources/static 目录下。以是假如你的静态文件在 src/main/resources/static/assets/ 目录下,路径应该设置为 classpath:/static/assets/。
这样,当用户请求 /assets/** 路径时,Spring 会在 classpath:/static/assets/ 目录中查找对应的文件。
7.3 配置视图解析器(ViewResolver)
感觉很少会用到来
7.3.1 pom.xml 加两个引用 支持jsp 的
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
复制代码
7.3.2 配置视图解析器
package com.lirui.springbootmoduledemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册一个拦截器
registry.addInterceptor(new WebInterceptor())
// 设置拦截路径
.addPathPatterns("/api/**")
// 排除不需要拦截的路径
.excludePathPatterns("/api/login", "/api/register");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置 /assets/** 请求,映射到 classpath:/static/assets/ 文件夹
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/");
// 配置 /images/** 请求,映射到 classpath:/static/images/ 文件夹
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/static/images/");
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// 配置 JSP 视图解析器
registry.jsp().prefix("/WEB-INF/views1/").suffix(".jsp");
}
}
复制代码
7.3.3 创建jsp页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello</title>
</head>
<body>
<h1>欢迎</h1>
<h1>欢迎</h1>
<h1>欢迎</h1>
<h1>欢迎</h1>
</body>
</html>
复制代码
7.3.4 写controller 一定用@Controller
package com.lirui.springbootmoduledemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Controller
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
@GetMapping("/helloJSP")
public String home() {
// 返回 index.jsp 视图
return "hello";
}
}
复制代码
7.3.4 浏览器访问 乱码 无所谓 只要能请求到 就没大题目
7.3.5 工作原理
当 Spring MVC 中的控制器(Controller)返回一个视图名称时(好比 "hello"),视图解析器会根据配置的 prefix 和 suffix 来构造终极的 JSP 文件路径。
控制器返回视图名称:hello
视图解析器会去 WEB-INF/views/ 目录下查找名为hello.jsp的文件。
假如文件存在,Spring 会将该 JSP 文件渲染到响应中,返回给客户端。
7.4 CORS 配置(跨域资源共享)
7.4.1 什么是跨域?
**跨域(Cross-Origin)**是指浏览器在不同的域、协议、端口之间举行资源请求的行为。简单来说,当一个网页试图从不同的域名、端口号或协议(如 http://example.com 和 https://example.com)加载资源时,就涉及到“跨域”题目。
7.4.2 为什么会有跨域题目?
浏览器出于安全考虑,接纳了
同源计谋(Same-Origin Policy)
,即一个网页只能访问同一来源(同协议、同域名、同端口)的资源。这是为了防止恶意网站通过脚本获取用户的敏感数据或做其他恶意操作。
7.4.3 同源计谋(Same-Origin Policy)
协议
:http、https。
域名
:如 example.com。
端口
:如 8080。
同源计谋要求这三者必须完全相同才气举行资源的访问和交互。例如,https://example.com 和 http://example.com 是不同的源,因为协议不同;http://example.com:8080 和 http://example.com:9090 是不同的源,因为端口不同。
7.4.4 跨域的场景
不同的域
:
网站 A(https://site-a.com)想要访问网站 B(https://site-b.com)的资源。
不同的协议
:
网站 A(http://site.com)想要访问网站 B(https://site.com)的资源,协议不同。
不同的端口
:
网站 A(http://site.com:8080)想要访问网站 B(http://site.com:9090)的资源,端口不同。
7.4.5 浏览器的跨域限定
浏览器出于安全原因,限定了网页脚本对跨域资源的访问。好比,假如你的网页在 http://localhost:3000 上,试图去请求 http://api.example.com 上的资源,浏览器会默认阻止这种请求,称为
跨域请求(Cross-Origin Request)
。
跨域请求的范例
跨域请求可以分为两种范例:
简单请求(Simple Request)
:
请求方法是 GET、POST 或 HEAD。
请求头只包括浏览器内置的标准头部(如 Content-Type 设置为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)。
复杂请求(Preflighted Request)
:
请求方法是 PUT、DELETE 或自定义的方法,或者请求头包罗了非标准的自定义头部(例如 Authorization,X-Custom-Header 等)。
在这种情况下,浏览器会先发送一个
预检请求(Preflight Request)
,以确认服务器是否答应实际的请求。
7.4.6 跨域的办理方案
7.4.6.1 CORS(跨域资源共享)
CORS(Cross-Origin Resource Sharing)
是一种浏览器和服务器之间的协议,它答应服务器声明哪些源(域、协议、端口)可以访问其资源。
当浏览器发起跨域请求时,浏览器会自动添加一些 CORS 相干的头部信息:
Origin
:体现请求发起的源(域、协议、端口)。
Access-Control-Allow-Origin
:服务器响应的头部,体现答应哪些源可以访问资源。
CORS 预检请求
: 当浏览器发起一个复杂的跨域请求时(例如,方法是 PUT、DELETE 或请求头包罗自定义头),浏览器会先发起一个
OPTIONS
请求,即
预检请求(Preflight Request)
,询问服务器是否答应跨域请求。假如服务器返回适当的响应头,则答应实际请求的发送。
CORS 响应头
:
Access-Control-Allow-Origin
:答应访问的源,* 体现所有域都可以访问,或者可以指定特定的域(如 http://example.com)。
Access-Control-Allow-Methods
:答应的 HTTP 方法(如 GET, POST, PUT, DELETE)。
Access-Control-Allow-Headers
:答应的请求头。
Access-Control-Allow-Credentials
:是否答应带上身份凭证(如 Cookies)。
Access-Control-Max-Age
:预检请求的有效时间,体现浏览器在多长时间内不需要再次发送预检请求。
@Override
public void addCorsMappings(CorsRegistry registry) {
// 配置全局跨域
registry.addMapping("/**")
.allowedOrigins("http://example.com") // 允许来自 example.com 的跨域请求
.allowedMethods("GET", "POST") // 允许的请求方法
.allowedHeaders("*"); // 允许所有请求头
}
复制代码
7.4.6.2 JSONP(仅限 GET 请求)
在早期,浏览器没有支持 CORS 机制时,开发者常用
JSONP
来绕过跨域限定。JSONP 是通过 <script> 标签的跨域特性来发送请求的,但它只支持 GET 请求,不支持发送其他范例的请求,如 POST。
7.4.6.3 服务器端代理(推荐)
一种常见的办理跨域题目的方式是通过设置
代理服务器
。前端应用发送请求到同源的代理服务器,由代理服务器转发请求到实际的跨域资源服务器。代理服务器和跨域服务器之间的请求不受浏览器的同源计谋限定。
7.5 消息转换器(Message Converters)
Spring MVC 利用 HttpMessageConverter 来将请求和响应的主体内容转换成 Java 对象或从 Java 对象转换为响应的格式(如 JSON、XML)。你可以定制消息转换器来支持自定义的序列化方式。
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建 Jackson 转换器
MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
// 创建 ObjectMapper 并进行配置
ObjectMapper objectMapper = new ObjectMapper();
// 排除 null 字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 设置日期格式
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 可以通过 Jackson 的 MixIn 来定制序列化规则
objectMapper.addMixIn(SomeClass.class, SomeClassMixIn.class);
// 设置 ObjectMapper 到 Jackson 转换器
jacksonConverter.setObjectMapper(objectMapper);
// 添加到转换器列表
converters.add(jacksonConverter);
}
复制代码
7.8 定制非常处理(@ExceptionHandler)(不推荐)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
// 定制异常处理
resolvers.add(new MyCustomExceptionResolver());
}
}
复制代码
8. 支持文件上传与下载
8.1
文件上传
8.1.1 配置文件上传的基本设置
在 Spring Boot 中,文件上传的功能默认已经启用。但是,你可以在 application.properties 或 application.yml 中配置一些上传限定(如文件巨细)。
# 最大上传文件大小
spring.servlet.multipart.max-file-size=10MB
# 最大请求数据大小
spring.servlet.multipart.max-request-size=10MB
复制代码
8.1.2 实现文件上传接口
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
@Value("${file.upload-dir}")
private String uploadDir; // 用于存储文件的目录
// 文件上传接口
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 获取文件名
String fileName = file.getOriginalFilename();
// 将文件存储到指定目录
Path path = Paths.get(uploadDir, fileName);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return ResponseEntity.ok("文件上传成功:" + fileName);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文件上传失败:" + e.getMessage());
}
}
}
复制代码
8.1.3 上传目录配置
你可以在 application.properties 文件中指定文件上传的目录。例如:
# 文件存储路径
file.upload-dir=./uploads
复制代码
8.1.4 上传多个文件
@PostMapping("/uploadMultiple")
public ResponseEntity<String> uploadMultipleFiles(@RequestParam("files") List<MultipartFile> files) {
for (MultipartFile file : files) {
// 保存每个文件
try {
String fileName = file.getOriginalFilename();
Path path = Paths.get(uploadDir, fileName);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("部分文件上传失败:" + e.getMessage());
}
}
return ResponseEntity.ok("所有文件上传成功");
}
复制代码
8.2 文件下载
8.2.1 实现文件下载接口
下载文件的实现比较简单,通常利用 HttpServletResponse 来将文件内容写入响应流中,浏览器会自动处理并触发文件下载。
@RestController
@RequestMapping("/api/files")
public class FileDownloadController {
@Value("${file.upload-dir}")
private String uploadDir; // 文件存储目录
// 文件下载接口
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
// 获取文件路径
Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
// 如果文件不存在,抛出异常
Resource resource = new FileSystemResource(filePath);
if (!resource.exists()) {
throw new FileNotFoundException("文件未找到:" + fileName);
}
// 返回文件资源并设置下载的 Content-Disposition
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + resource.getFilename() + """)
.body(resource);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
}
复制代码
8.2.2 设置响应头部以下载文件
通过 Content-Disposition 响应头可以让浏览器以下载的情势处理文件而不是直接表现内容。上述代码中已经设置了这个头部:
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + resource.getFilename() + """)
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4