首页 / 资讯中心 / 文章详情

SpringBoot+Uniapp前后端分离校园项目实战:从源码到部署避坑指南

SpringBoot+Uniapp前后端分离校园项目实战:从源码到部署避坑指南 ★ FEATURED ARTICLE
简介基于 SpringBoot 与 Uniapp 做前后端分离的校园社区项目覆盖校园集市、表白墙、论坛、失物招领、校园墙和跳蚤市场等日常场景适合高校课程设计、毕业设计以及校园信息化实践参考。技术实现上采用 MyBatisPlus 管理数据层SpringSecurity 保证接口安全Quartz 处理定时任务MinIO 提供文件存储Redis 负责缓存MongoDB 存储文档类信息前端配合 uView 组件库完成跨端页面展示了权限控制、缓存、对象存储、定时任务等企业级开发中的常见问题与解决方案。压缩包共 564 个文件约 1.85MB包括 113 个 Java 后端类、253 个 JS 与 127 个 Vue 前端页面以及 SCSS 样式、SQL 数据库脚本、YML 配置和说明文档。目录结构按业务模块划分便于学习时快速找到对应代码邮件模板、工具类、认证与发帖服务等文件也一并提供能帮助读者了解完整的工程化组织方式。目前已有 75 人学习。下载后可直接获得完整源代码和数据库脚本适合想完整跑通项目、理解前后端分离架构以及中间件整合方式的开发者也可作为二次开发或课程报告的基础材料。1. 校园圈项目到底是什么从一套源码看清 SpringBoot Uniapp 的前后端分离套路校园圈这类全栈项目几乎每个做毕设或接私活的开发者都绕不开。它的名字很多有人叫校园集市有人叫校园墙本质都是用 SpringBoot 提供接口用 Uniapp 做用户端把校园里的信息流、商品流和社交需求放进一个应用。你拿到的“源代码数据库”不是两个文件夹那么简单而是一条完整的数据链路MySQL 存业务数据SpringBoot 处理增删改查Uniapp 在 H5、微信小程序、App 三端共用一套代码。很多第一次接触前后端分离的读者习惯把后端 Controller 当页面用把前端当成静态文件丢进 SpringBoot 的 static 目录。分离的核心是接口约定、Token 鉴权和跨域配置我后面所有步骤也围绕这三个点展开。如果你是计算机专业学生、转行做全栈的开发或者想接校园类私单这套方案能让你少走很多弯路。2. 项目拆解SpringBoot 后端服务、Uniapp 前端模块与 MySQL 数据库的映射关系2.1 五个业务模块如何对应后端接口和核心表先看后端结构。常见做法是 controller/service/mapper 三层再加上 config、interceptor、common 几个包。校园圈虽然有五个功能入口但后端接口并不需要各自为政很多逻辑可以复用。我拆解过这类项目核心表一般是这样业务模块核心表关键字段复用点校园集市/跳蚤市场goodsid, user_id, title, description, price, images, status商品上下架、搜索、分类表白墙/校园墙wall_postid, user_id, content, is_anonymous, status, audit_reason匿名逻辑、审核状态机论坛forum_topic / forum_replyid, user_id, title, content, view_count评论回复、点赞失物招领lost_foundid, type(0丢/1捡), title, place, contact, status与商品共用图片上传用户userid, openid, nickname, avatar, role登录鉴权所有模块共用一张 user 表贯穿所有业务status 字段在四张业务表里都表示“是否审核通过”只是字段名可能叫 status 或 audit_status。数据库里真正需要重点设计的是 goods 和 wall_post因为前者有图片、有价格后者有匿名和审核两个特殊需求。接口上每个模块基本是标准的增删改查列表带分页、详情、新增、修改、删除。但要注意表白墙和失物招领的删除权限通常要给管理员前端按钮要通过 role 字段控制。不能简单复用同一个删除接口。运营后台是另一个常见的隐藏需求。很多源码会在 admin 包里单独放一套 Controller比如审核表白墙、下架违规商品。这套接口和用户端接口共用同一个 service只是通过注解校验管理员身份。后面讲 Token 鉴权时会具体说。SpringBoot 项目结构上我一般建议把“通用返回体”“分页请求参数”“异常处理”这三个东西放到 common 包。像 Result 类统一返回 code、msg、data避免每个 Controller 自己拼 Map。项目里常见的翻车是登录接口返回的字段名和前端 request.js 里解构的字段对不上所以定好返回体结构后再写业务能省很多联调时间。2.2 前后端分离的鉴权方案用拦截器统一校验 Token前后端分离最常见的问题是接口裸奔。如果所有接口都能直接访问那数据库里的表白墙投稿、商品信息随便就能被爬走。所以一般项目里会有一个全局拦截器校验 Token。看一段典型的 Java 代码Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 放行登录接口和静态资源 if (request.getRequestURI().contains(/user/login) || request.getRequestURI().contains(/images/)) { return true; } if (token null || token.isEmpty()) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录请先登录\}); return false; } // 用 Redis 或 JWT 解析 token这里以 JWT 为例 try { Claims claims Jwts.parser().setSigningKey(yourSecretKey).parseClaimsJws(token.replace(Bearer , )).getBody(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\登录已过期请重新登录\}); return false; } } }逻辑说明拦截器先放行登录和图片访问剩下的接口统一取 Authorization 请求头里的 Token用 JWT 解析 userId最后放进 request 属性Controller 里直接用。这样各业务接口就不需要自己重复写登录判断。参数说明yourSecretKey是 JWT 的签名密钥实际项目里要放到 application.yml 里用Value读取不要写死在代码里。Bearer前缀是可选的但前端在请求拦截器里统一加上后端保持一致即可。如果有 Redis 作单点登录可以把 JWT 的 jti 作为 key 存一份退出登录时删掉实现真正踢人。配置拦截器时还需要一个 WebMvcConfig 注册它并设置 excludePathPatternsConfiguration public class WebConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/goods/list); } }注意这里 excludePathPatterns 是把列表接口放行了因为游客也要看商品列表。如果是表白墙投稿就必须登录才能发。这需要你在 controller 层用 userId 参数进一步判断拦截器只保证“有人登录”不保证“这个人能不能访问某个数据”。我在实际项目里还会加一层角色判断把管理员接口路径放在一个独立前缀下比如/admin/**单独拦截。2.3 数据库初始化SQL 脚本里必须改的 3 个地方拿到源码后第一步不是启动项目而是建库。大多数源码会带一个 .sql 文件或者 db 文件夹。直接导入很可能会出问题我一般会打开脚本改三个地方。第一字符集。脚本里如果写的是DEFAULT CHARSETutf8建议改成utf8mb4否则表白墙里用户发个 emoji 表情MySQL 直接报错不写入。一条核心建表语句大概是CREATE TABLE wall_post ( id int NOT NULL AUTO_INCREMENT, user_id int DEFAULT NULL, content varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, is_anonymous tinyint(1) DEFAULT 0, status tinyint(1) DEFAULT 0, audit_reason varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;第二自增起始值。如果源码附带的 SQL 是从开发者的测试库导出的表里会带着一堆测试数据而且 AUTO_INCREMENT 已经被推进到几百甚至几千。你在本地演示没问题但是交作业或者部署上线时想让 id 从 1 开始就得把每张表的 AUTO_INCREMENT 改回 1。一条命令重置所有表比较麻烦我通常是直接在建表语句里把 AUTO_INCREMENT 改成 1或者导入后用ALTER TABLE wall_post AUTO_INCREMENT 1;逐表改。第三管理员账号。很多脚本里的 admin 密码是明文或者 MD5 加密而且 openid 是开发者测试时的小程序 appid。你本地登录不了后台往往不是代码 bug而是种子数据里的 openid 和你的微信小程序 appid 对不上。解决方案是改 SQL 里的种子数据或者在登录接口里临时打日志看实际查出的 openid 是什么。数据库连接串也要注意。application.yml 里的 url 至少是这种标准写法spring: datasource: url: jdbc:mysql://127.0.0.1:3306/campus-circle?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver参数说明useUnicodetruecharacterEncodingutf8mb4是解决中文和 emoji 乱码的关键serverTimezoneAsia/Shanghai是 MySQL 8.x 的必填参数不写会直接报时区异常。username 和 password 一定要换成自己本地的数据库账号。改完配置后我建议先执行一个最简单的查询接口看数据库通没通。如果接口能返回用户列表说明连接没问题如果报Communications link failure优先检查 MySQL 服务是否启动、端口是不是 3306、账号有没有 IP 访问权限。3. 本地跑通最小闭环后端启动、Uniapp 编译到 H5 和微信小程序3.1 后端启动前的配置application.yml 里端口、数据源和上传路径在跑后端之前先确认三个配置端口、数据源、文件上传路径。很多源码自带的配置是开发者的本机环境你直接启动大概率会报错。server: port: 8080 servlet: context-path: /api spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB file: upload-dir: /var/www/campus-circle/images这段配置里context-path: /api意味着所有接口都被加了一层前缀访问地址是http://localhost:8080/api/user/login。Uniapp 前端请求的时候baseURL 必须写成http://localhost:8080/api。很多人卡在启动后白屏就是因为这里前缀不一致。multipart配置是给图片上传用的。表白墙配图、集市商品图都走这个配置max-file-size控制单张大小一般 5MB 够用。file.upload-dir是自定义属性Controller 里通过Value(${file.upload-dir})注入上传的文件就写到这个目录。Windows 本地上传路径建议用D:/upload这种盘符路径不要写C:/因为项目放到别的机器会迁移。启动 SpringBoot 时我习惯用 IDE 里的 Application 主类右键启动而不是mvn spring-boot:run后者在 Windows 控制台如果编码是 GBKMaven 日志里的中文会乱码容易干扰判断。启动成功后看到Tomcat started on port(s): 8080再继续配前端。3.2 用 HBuilderX 导入 Uniapp 前端并编译到 H5拿到 Uniapp 前端源码后最常见的打开方式是用 HBuilderX直接“文件 - 导入 - 从本地目录导入”。导入后先别急着运行先打开 manifest.json 看三个配置H5 配置的 router base 和 devServer 端口默认是 8080如果和后端接口端口冲突要改成比如 8081。微信小程序配置的 appid如果是空或者别人的换成你自己的测试号 appid否则开发者工具里会报错。要检查 request 工具里的 baseURL。大多数源码会在utils/request.js里统一处理。// utils/request.js const BASE_URL http://localhost:8080/api export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) ? Bearer uni.getStorageSync(token) : }, success: (res) { if (res.data.code 401) { uni.navigateTo({ url: /pages/login/login }) } else { resolve(res.data) } }, fail: reject }) }) }逻辑说明request 封装把 token 从本地存储取出统一加到请求头里服务端 401 时跳到登录页。这套逻辑是前后端分离的关键后端拦截器能拿到 Authorization全靠这行 header。参数说明BASE_URL需要根据你的编译环境动态改。在 H5 开发时浏览器访问的是http://localhost:8080在微信小程序里本地开发可以填你电脑的局域网 IP比如http://192.168.1.100:8080/api并勾选开发者工具里的“不校验合法域名”。真机预览时必须保证手机和电脑在同一局域网。编译到 H5 很简单HBuilderX 菜单栏 “运行 - 运行到浏览器 - Chrome”如果一切正常你会看到登录页。如果 network 里显示请求失败优先排查跨域和端口冲突。3.3 微信小程序打包时2MB 超限和不打印日志怎么处理Uniapp 编译到微信小程序最常遇到的是“source size 2612kb exceed max limit 2mb”。这不是项目跑不起来而是包体积超了微信的限制。常见处理思路有三个把图片和静态资源改成网络路径不要放本地 static。开启分包pages 里按模块分成 main 包和 subpackages比如集市、论坛、失物招领各自一个分包。在 manifest.json 里勾选“运行时的压缩代码”并在微信开发者工具里勾选“上传代码时压缩样式文件”。还有一个让新手头疼的现象是Uniapp 编译到小程序后 console.log 不打印。这不是代码问题而是开发者工具的“调试器”里默认过滤了 info 级别日志。把日志级别调整为“All”或者“Verbose”就能看到 uni.request 的日志。很多老手排查接口问题前会先确认这一步不然以为代码没执行。打包上架安卓应用市场是另一个方向。Uniapp 项目在 HBuilderX 里可以发行“原生 App-云打包”前提是你的 DCloud 账号有打包证书。注意云打包时manifest.json 里的 App 模块配置勾选定位、地图等模块如果代码里用了uni.getLocation但没配权限打包后功能会静默失败。到这里你可以成功跑通前后端分离的最小闭环。但离做完还差一件事把核心业务逻辑的边界看清楚。4. 核心业务实现匿名发布、审核状态机和图片上传的落地方式4.1 表白墙匿名发布表里存 user_id接口返回时抹掉身份校园表白墙最核心的诉求是匿名。但匿名不等于不记录。为了后续审核和管理员能追溯数据库里必须存真实的 user_id只是在接口返回给前台时把 user 表关联查询出来的昵称和头像换成默认值。后端实现上查询列表的 SQL 要小心。很多新手直接联表查 user然后select *导致匿名贴把昵称带出来了。正确做法是SELECT wp.id, wp.content, wp.create_time, CASE WHEN wp.is_anonymous 1 THEN 匿名 ELSE u.nickname END AS nickname, CASE WHEN wp.is_anonymous 1 THEN https://example.com/default-avatar.png ELSE u.avatar END AS avatar FROM wall_post wp LEFT JOIN user u ON wp.user_id u.id WHERE wp.status 1 ORDER BY wp.create_time DESC LIMIT #{offset}, #{pageSize}逻辑说明用 CASE WHEN 在 SQL 层判断is_anonymous 为 1 时昵称显示“匿名”头像用默认占位图。这样即使前端直接联表展示也拿不到真实昵称。注意这里默认头像的 URL 不要写死最好放到配置表或常量类里。我在项目里习惯把这类常量放一个CommonConstants类避免每个 mapper 里各写各的。4.2 审核状态机待审、通过、拒绝三态怎么闭环表白墙和失物招领都有审核。通常库里的 status 字段是 0 待审、1 通过、2 拒绝。前端用户端只查 status1管理端能查所有状态并修改。审核接口的 Java 实现可以在 Service 层写一个方法public boolean audit(Integer id, Integer status, String reason) { if (status ! 1 status ! 2) { throw new RuntimeException(审核状态不合法); } WallPost post wallPostMapper.selectById(id); if (post null) { throw new RuntimeException(记录不存在); } WallPost update new WallPost(); update.setId(id); update.setStatus(status); if (status 2) { update.setAuditReason(reason); } return wallPostMapper.updateById(update) 0; }逻辑说明这个 service 方法至少包含了三个防御点非法状态值、数据不存在、拒绝时必须填原因。实际项目里还会加一个“审核记录表”存操作人、操作时间、前后状态但学校项目里一张 status 字段就够用了。参数说明reason只有拒绝时才有意义通过时传 null。前端审核页面要根据返回的 reason 展示拒绝原因所以接口返回值里的 auditReason 字段在 status0 时要给空字符串避免小程序端 json 解析 null 报错。这里要特别注意审核操作必须写进事务里。如果 update 前还要给用户发通知比如插入一条站内信发送失败会导致审核状态已改但用户没收到通知。我给这类方法都会加Transactional(rollbackFor Exception.class)让两步操作要么都成功要么都回滚。4.3 图片上传本地存储目录和 Nginx 访问映射图片上传是校园集市里最容易出的 bug。后端接收上传后存到本地磁盘同时返回一个可访问的 URL。先看 Controller 代码RestController RequestMapping(/api/upload) public class UploadController { Value(${file.upload-dir}) private String uploadDir; PostMapping(/image) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty() || file.getSize() 5 * 1024 * 1024) { return Result.error(文件不能为空且大小不能超过5MB); } String ext file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.)); String fileName System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadDir / fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); } catch (IOException e) { throw new RuntimeException(文件上传失败, e); } return Result.success(http://localhost:8080/images/ fileName); } }逻辑说明用时间戳UUID 生成文件名防止重名和路径穿越然后转移到 upload-dir 完成落盘。参数说明这里返回的 URL 是给前端直接用于 标签的。要让这个 URL 能被访问SpringBoot 必须增加一个静态资源映射Configuration public class FileConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadDir /); } }这个映射的意思是浏览器访问/images/abc.jpg时从本地磁盘的 uploadDir 目录下找文件。如果你部署到服务器通常会用 Nginx 直接代理这个目录而不再经过 SpringBoot这样静态资源请求不会打到 Java 进程内存占用更低。Nginx 配置片段是location /images/ { alias /var/www/campus-circle/images/; expires 7d; }注意如果后端配了context-path: /api那么上传接口是/api/upload/image但图片访问地址不要带/api前缀因为 Nginx 里的 location /images/ 是独立于 SpringBoot 的。我在这上面栽过一次上传成功、返回 URL 也正确但 Nginx 一直 404最后发现是 location 的路径和 alias 最后的斜杠不匹配。5. 避坑前后端分离校园项目最容易翻车的 5 个现场5.1 现象接口请求返回 404前端一直白屏现象前端页面能打开但请求/api/user/login返回 404控制台显示 Failed to load resource。原因后端配置了server.servlet.context-path/api但 Uniapp 的 BASE_URL 也写了/api两个前缀叠加实际请求变成了/api/api/user/login。或者反过来后端没配 context-path但前端 BASE_URL 写成了/api也会 404。解决统一约定一个标准。我习惯后端不配context-path所有 Controller 的 RequestMapping 都带/api前缀前端 BASE_URL 只写协议IP端口即http://localhost:8080。这样就算之后拆成独立网关改动也更小。5.2 现象H5 请求跨域浏览器报 CORS error现象用 HBuilderX 运行到浏览器登录接口返回 200但浏览器控制台报 No Access-Control-Allow-Origin header is present。原因H5 页面跑在http://localhost:8081接口跑在 8080 端口浏览器同源策略拦截。很多源码只在后端加了CrossOrigin注解但这个注解默认允许的源不包含 8081 端口或者没加allowedHeaders导致携带 Authorization 头的请求被拒。解决后端加一个全局 CORS 配置而不是在每个 Controller 上零散加注解。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意.allowedOriginPatterns(*)是 SpringBoot 2.4 的写法低版本用.allowedOrigins(*)但后者和allowCredentials(true)一起用可能报错所以用 Patterns 更省事。如果你不想在代码里放开所有源也可以在开发阶段用 HBuilderX 的 devServer 代理正式上线后同域部署或走 Nginx 反代。5.3 现象数据库写入中文变成问号小程序里发个表情直接报错现象表白墙投稿内容保存到 MySQL 后是???或者插入 emoji 时抛Incorrect string value。原因表结构、连接串、前端请求三处字符集不一致。最常见的是表是 utf8但 emoji 需要 utf8mb4或者 SpringBoot 连接串没写 characterEncoding。解决把表字符集改成 utf8mb4连接串里加characterEncodingutf8mb4前端请求头保持 utf-8。如果数据已经变成问号只能删掉重插。这个问题的隐蔽点在于后端代码里明明写了content new String(content.getBytes(ISO-8859-1), UTF-8)这种错误转码代码应该删掉。这种转码在本地偶尔碰巧能跑换到 Linux 服务器上就是灾难。5.4 现象图片上传成功但前端图片裂开现象上传接口返回 200服务器上也能看到文件但小程序或 H5 里image标签显示不了。原因返回的图片地址写成了localhost手机真机访问不到或者静态资源映射没加或者文件权限不对Nginx 启动用户读不了/var/www下的文件。解决本地调试用局域网 IP 替换 localhost部署到服务器后用绝对路径访问验证。getOriginalFilename()拿到完整路径时要先用File.separator处理反斜杠否则在 Windows 上能跑、Linux 上找不到目录。我自己的习惯是上传文件名里只保留扩展名时间戳UUID 自己生成这样完全避免路径分隔符问题。5.5 现象微信开发者工具提示包体积超过 2MB无法预览现象编译成功但点击预览或上传时提示source size 2612kb exceed max limit 2mb。原因Uniapp 默认把整个 vue 组件和静态资源打进主包。很多源码会把 default 头像、封面图、tabBar 图标放在 static 里一张图几百 KB两个循环图就超了。解决按页面拆分分包tabBar 页面必须在主包其他模块如论坛、失物招领放 subPackages。再把静态图片压缩后传到 OSS 或用外链。一条经验本地图片总大小控制在 200KB 以内。关闭 HBuilderX 自带的“代码混淆压缩”开关有时能省一点但真正治本的是分包。以上 5 个现象基本都是我接手这类源码时逐个踩过的。每次排查我都会先把请求的完整 URL 复制到浏览器直接访问再用 Postman 测试接口最后才怀疑前端。这套排查顺序能帮你从黑匣子里跳出来。6. 再进一步从能跑到能交付验证校园圈项目的四个指标如果你已经跑通下面四个点决定这个项目能不能拿去答辩或上线。先用一份接口自测表过一遍登录后拿 Token、未登录访问需要鉴权的接口应返回 401、发布表白墙后管理员能审核、审核拒绝后前端不展示。这些是业务闭环的硬指标最好写进自己的测试清单里。然后是异常数据验证。给wall_post.is_anonymous传 0 和 1 各测一次确认匿名用户的昵称字段能正确替换。给商品价格传负数看后端有没有校验后端通常在goodsServiceImpl里写了if (price.compareTo(BigDecimal.ZERO) 0) throw new RuntimeException(价格不能为负数)。如果没有你可以自己补上这是很多人会忽略的健壮性问题。还有一个很实用的验证方式把数据库的 SQL 打开手动执行一遍核心查询看返回行数和前端页面是否一致。如果列表页和数据库行数对不上多半是查询条件里status 1的过滤漏了或者分页 SQL 的LIMIT参数写错。我会习惯在 MyBatis 的 mapper XML 里给每个查询加order by create_time desc避免列表顺序在多次分页时跳跃。拿我自己交付过的项目来说最后一步一定是把项目里的测试数据清掉、admin 密码改成强密码、把 application.yml 里的环境配置确认一遍。很多答辩时老师最在意的是你能不能说清楚“匿名是真的匿名吗”“审核状态怎么流转”“图片存在哪里”。你把这三个问题答清楚比会背 SpringBoot 原理更管用。这个方向是否值得投入我的判断是值得。SpringBoot Uniapp 的组合覆盖了后端接口、前端多端打包、数据库设计、上线部署一整条链路你只要完整跟通一遍再去看其他前后端分离项目会发现结构都类似。但别贪多先把最核心的登录、发布、审核、图片上传四个流程做到不出纰漏再谈加功能。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站