ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建本科生交流培养管理平台:SpringBoot+Vue前后端分离实战

从零搭建本科生交流培养管理平台:SpringBoot+Vue前后端分离实战 做过的学生项目里交流培养平台这类题目算是毕业设计和课程设计中的常青树。原因很简单——它业务上既有“交流”的社交互动属性又有“培养管理”的过程监控属性技术上前后端分离一套走完SpringBoot、Vue、MyBatis、MySQL这些该碰的组件一个不少从数据库设计到接口开发从前端联调到生产部署整条链路都能完整覆盖。所以我最近完整做了一遍这个“本科生交流培养管理平台”从零写源码到最终部署上线中间踩了不少坑也积累了不少经验。这篇文章就把我的整套思路、选型逻辑、核心代码和部署过程全部分享出来不管是正在做这个题目的同学还是想练手前后端分离项目的开发者都可以直接参考。1. 项目背景与需求拆解一个交流培养管理平台到底要解决什么问题1.1 本科生培养场景中的信息断层现在很多高校在推行本科生导师制或者学业导师制本意是让本科生从大一进校开始就有固定教师带着做科研、参加竞赛、做职业规划。这个制度本身没问题但真实落地的时候我见过不少学院是这样的状态导师和学生之间只有一份Excel名单平时沟通靠微信群临时通知靠口头转达导师想了解学生这周进展如何翻聊天记录都翻不到。学院教学办想统计每个导师带了多少学生、指导频率多高、学生完成了几个阶段任务只能发通知让各班班长收Excel再手动合并。过程资料没有统一归档学生做了什么、老师反馈了什么全凭自觉出了问题也追溯不到。这种场景正是一个交流培养平台要解决的问题。“交流培养管理平台”拆开看其实是两条业务线一条是“交流”偏向内容社区解决师生之间、同学之间信息互通和经验分享的问题另一条是“培养管理”偏向教务过程管理解决培养计划制定、阶段任务下发、材料提交审核、进度追踪的问题。两条线有交集但目标完全不同做需求分析的时候必须先拆清楚否则后面数据库设计和功能开发很容易做成一锅粥。1.2 从标题关键词倒推功能清单“交流”这条线对应的是内容与互动公告发布、经验分享帖、讨论区、评论、回复、点赞、收藏、消息通知。它有明显的社区属性核心要素是用户生成内容的发布和互动。“培养管理”这条线对应的是任务与过程师生结对关系配置、培养计划制定、阶段任务下发、学生提交材料、教师审核评估、进度跟踪、统计报表更像一个轻量级的教务系统。加上平台管理能力例如用户管理、角色权限、内容审核、数据看板就构成了完整的功能矩阵。把这些需求列成功能清单大概是这个样子交流模块公告列表、帖子发布与浏览、评论与回复、点赞收藏、我的消息培养模块培养计划、阶段任务、材料提交、审核反馈、进度跟踪平台管理用户管理、角色分配、专业与年级维护、内容删除、数据统计个人中心基本信息、我的帖子、我的培养进度、消息通知注意我特意没有把“交流”做成很重的社区也没有把“培养”做得很复杂因为这是在校园场景里跑的系统用户是本科生和教师功能太杂反而没人用。先把核心流程跑通比堆功能重要得多。1.3 三类角色与核心业务流程系统的使用者只有三类角色学生、教师、管理员。每个角色的操作边界完全不同。角色核心操作关注点学生登录、浏览公告、发布交流帖、查看培养计划、提交阶段材料、查看教师反馈流程是否顺畅、提醒是否及时教师管理指导学生、发布培养计划与阶段任务、审核学生材料、回复交流区问题批改效率、进度可视化管理员用户导入、角色分配、内容监管、学期配置、统计报表数据准确、操作可追溯核心业务流程可以概括成一条主线管理员初始化学期数据导入学生和教师账号教师创建培养计划拆分成若干阶段任务指派给名下学生学生按阶段提交材料教师在线审核并给出评语学院负责人通过统计报表查看整体完成情况。整个过程中交流区作为非正式的沟通渠道学生可以提问教师可以解答内容沉淀下来也是培养过程中的一部分。2. 技术选型分析SpringBootVueMyBatisMySQL这套组合为什么稳妥这个题目的技术栈几乎已经被固定了缘因不是这套组合多么前端时尚而是在校园项目这种场景下它是公认最稳妥、资源最多、最容易排查的一整套方案。2.1 为什么坚持前后端分离“前后端分离”现在基本成为项目标配核心是把界面展示和业务逻辑解耦。前端通过接口取数渲染后端专注处理数据与业务规则两边可以用不同团队、不同节奏开发部署上也可以各自独立。对我来说最重要的实际收益是调试方便前端报错单独看浏览器控制台后端接口报错单独看服务端日志问题定位比单体JSP项目清晰得多。而且Vue这种SPA应用打包后只是纯静态文件可以扔到Nginx里跑也可以塞进SpringBoot的静态资源目录部署方式很灵活。2.2 SpringBoot让后端开发回归业务本身SpringBoot的核心理念是约定大于配置它对Spring生态做了一层自动装配封装。做一个web项目引入spring-boot-starter-web之后内嵌Tomcat不需要额外装容器一个main方法就能启动服务。相比传统SSH或SSM项目里那一堆XML配置SpringBoot用application.yml就能搞定大部分设置。当然它本质上还是Spring依赖注入、AOP、事务管理这些底层能力一个没少。我的习惯是用SpringBoot做项目骨架Controller负责接收请求Service层放业务逻辑Mapper层对接数据库层次清晰后期加功能也不会乱。我用这个平台项目验证过一件事相同功能量的后端如果用传统Spring MVC加XML配置光配置文件就要写半天SpringBoot只需要一个启动类和很少的配置就能跑起来开发效率差距非常明显。2.3 MyBatis灵活SQL在复杂查询中的价值选MyBatis而不是JPA/Hibernate是我在这个项目里比较坚持的一点。培养管理场景里有大量多条件组合查询例如按年级、专业、导师姓名、阶段状态筛选学生列表这种需求用MyBatis的XML动态SQL写起来非常顺手。 加 的结构可以精确控制SQL拼接条件全选的写法也几乎不会出错。此外SQL由自己掌控跨过框架层面去排查慢查询、优化索引时可以直接定位不用去猜框架帮你生成的SQL是什么样。有人会质疑JPA写代码更简洁但在这个题目的场景下可控制的SQL比省那几行代码更能让我安心。而且MyBatis在国内的普及度极高网上资料和面试题一大堆出了问题搜一下就有答案学习成本也低。2.4 Vue组件化开发带来的前端效率提升Vue在前端这块的优势体现在组件化和响应式上。每个页面可以拆成多个逻辑独立的组件像是帖子卡片组件、评论列表组件、进度条组件写好后直接复用改动一个组件所有引用处同步生效。响应式数据绑定意味着我不需要手动操作DOM数据变化视图自动更新写起来像在操作一个状态层比以前jQuery时代轻松太多。如果配合Vue Router做前端路由整个应用就是单页体验页面切换不刷新感觉像桌面软件。而且Vue的上手曲线在主流框架里算平缓的一个从后端转前端的人花一两天看文档就能上手写页面。这对校园项目来说特别现实因为你的开发周期就那么长没时间从头啃一个重框架。2.5 MySQL在数据量级和运维成本上的现实考量MySQL是这套组合里最没有争议的一块。高校内部系统的数据量级绝大多数场景就是几千用户、几万条帖子、几十万条提交记录MySQL完全撑得住。成本上它免费开源部署简单Linux上一行命令的事生态上Navicat、DataGrip这些客户端工具都很成熟。相比PostgreSQL或者MongoDBMySQL对国内开发者来说熟悉度最高遇到问题找解决方案也最容易。我还专门配合MyBatis做了实践动态SQL在MySQL下的写法、索引优化方式都有成熟路径容错空间很大。3. 数据库设计实战从业务实体到MySQL表结构的完整落地到了数据库设计这一步是最容易决定项目生死的环节。很多同学拿到题目直接建几张表就开始写代码等到功能做起来才发现缺字段、表关系不清再加字段改结构痛不欲生。我的建议是先画实体关系图把用户、帖子、培养计划、提交材料这些核心实体之间的关系理清再落到建表SQL上。3.1 用户-角色-权限表设计我的用户表设计非常直接没有刻意做强权限框架字段类型说明idbigint主键自增usernamevarchar(50)登录名学号或工号passwordvarchar(100)BCrypt加密后的密码real_namevarchar(50)真实姓名roletinyint1学生 2教师 3管理员majorvarchar(50)专业学生用gradevarchar(10)年级如2022级teacher_idbigint学生绑定的指导教师phonevarchar(20)联系方式statustinyint账号状态1正常 0禁用create_timedatetime创建时间这里把教师和学生直接做成一张用户表用role字段区分好处是登录认证逻辑统一不用为不同角色建多张表。学生和教师的结对关系用teacher_id字段直接指向教师的用户ID简单直观。如果你对权限有更复杂的要求可以再加一张角色表和用户角色关联表但在这个平台里三个角色用一个小字段就能覆盖不必过度设计。3.2 交流模块的表设计交流功能我拆成了帖子表、评论表、回复表、点赞记录表四张表post表id、user_id发帖人、category_id、title、content、view_count、like_count、comment_count、status、create_timecomment表id、post_id、user_id、content、create_timereply表id、comment_id、from_user_id、to_user_id、content、create_timelike_record表id、post_id、user_id、type0帖子 1评论、create_time这里面有几个细节要注意。点赞记录表加唯一索引post_id, user_id, type防止一个用户重复点赞这个索引也是高并发下防重的主要手段。评论表和回复表分开是为了在页面上区分二级评论和追评的展示逻辑如果合在一起查询起来会很别扭。帖子表用冗余字段comment_count和like_count来存计数虽然不满足第三范式但查询列表的时候不用连表count性能好很多数据一致性可以通过事务回调维护。3.3 培养管理模块的表结构设计这部分是整个系统业务逻辑最重的区域我设计了四张核心表mentor_student表id、mentor_id教师ID、student_id学生ID、semester学期、status结对状态、create_timetraining_plan表id、mentor_id、semester、title、description、create_time、update_timestage_task表id、plan_id、title、content、deadline、sort_order、create_timestage_submission表id、task_id、student_id、content、file_url、submit_time、score、evaluation、evaluate_time、status阶段任务挂在培养计划下面一份计划拆成若干个阶段任务每个任务学生提交一次材料。status字段在提交表里用来标记待审核、已通过、已打回三种状态。教师在审核的时候可以填评语和打分学生端就能实时看到进度。这里要特别注意外键关系stage_submission的task_id关联stage_taskstage_task的plan_id关联training_plantraining_plan的mentor_id关联用户表层级关系必须清晰删除操作也要按这个层级来约束。3.4 初始化数据与Navicat实操建表我直接用Navicat可视化操作把SQL跑一遍然后把表关系图生成出来检查一遍逻辑没问题再开始写代码。初始化数据这个环节我建议一次到位默认管理员账号、几个测试教师账号、一批学生账号、几条培养计划和阶段任务、若干条测试帖子。有数据的情况下前端页面开发不会对着空表盲写联调时也能直接看到效果。注意学生密码统一用BCrypt加密后的同一串密文方便测试登录上线前再提醒用户修改。明文密码入库是低级错误千万别干。4. 后端实现SpringBootMyBatis的关键代码与核心逻辑4.1 项目分层与初始化配置后端的包结构我用的是经典的controller-service-mapper分层再加上config、entity、dto、common几个包。Controller只做参数接收和响应包装业务逻辑全部下沉到ServiceMapper用MyBatis的XML方式管理SQL。application.yml里的关键配置大概长这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/train_platform?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.train.entity configuration: map-underscore-to-camel-case: true数据库连接串里的三个参数要特别注意useUnicode和characterEncodingutf8保证中文不乱码serverTimezone指定时区否则高版本MySQL驱动会报时区错误。MyBatis开启mapUnderscoreToCamelCase之后数据库字段grade_college自动映射到实体类的gradeCollege属性省去一大堆resultMap。4.2 JWT登录认证与拦截器设计登录接口用的JWT方案用户登录成功后签发一个token之后每次请求前端把token放在Authorization头里后端拦截器解析token确认用户身份。这个方案的优点是不需要在服务端保存会话状态非常适合前后端分离场景。拦截器核心逻辑Component public class LoginInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null jwtUtil.validateToken(token)) { Integer userId jwtUtil.getUserIdFromToken(token); request.setAttribute(userId, userId); return true; } response.setStatus(401); return false; } }注册拦截器时要注意放行登录接口、注册接口和静态资源否则前端没拿到token之前什么都访问不了。我实际开发中在WebMvcConfigurer里通过addPathPatterns和excludePathPatterns配置把这种放行列表集中管理后面加白名单很省事。这个平台里我还加了基于拦截器的角色权限校验比如只有管理员能访问用户管理接口只有教师能调用审核接口。做法是再写一个角色判断的工具方法在需要校验的接口上配合注解使用没有引入Spring Security那种重框架功能上完全够用。4.3 统一返回结果与全局异常处理前后端对接时最怕的就是每个接口返回值结构不一样前端解析起来全靠猜。我设计了统一的返回体类Result包含code、message、data三个字段所有接口都返回这个结构。public class ResultT { private Integer code; private String message; private T data; // 静态方法 success/error 构建返回结果 }对应的全局异常处理器用RestControllerAdvice接管业务异常和系统异常保证后端抛异常时前端拿到的也是标准结构而不是一堆堆栈信息。这样前端axios响应拦截器里只需要判断code是不是200就能统一处理错误弹窗。4.4 MyBatis动态SQL在条件查询里的应用培养进度列表是典型的多条件筛选场景按学期、按专业、按导师、按状态。这种查询用MyBatis动态SQL写起来是真正的舒服select idselectProgressList resultTypecom.example.train.entity.ProgressVO SELECT s.real_name AS studentName, m.real_name AS mentorName, p.title AS planTitle, task.title AS taskTitle, sub.status, sub.score FROM stage_task task LEFT JOIN training_plan p ON task.plan_id p.id LEFT JOIN mentor_student ms ON p.mentor_id ms.mentor_id LEFT JOIN user s ON ms.student_id s.id LEFT JOIN user m ON p.mentor_id m.id LEFT JOIN stage_submission sub ON sub.task_id task.id AND sub.student_id s.id where if testsemester ! null and semester ! AND p.semester #{semester} /if if testmajor ! null and major ! AND s.major #{major} /if if testmentorId ! null AND p.mentor_id #{mentorId} /if if teststatus ! null AND sub.status #{status} /if /where ORDER BY task.sort_order /select这里 标签会自动处理条件前多余的AND 标签根据参数有无动态拼接SQL。我当时在实际操作时遇到过一个问题如果某个学生还没提交材料LEFT JOIN的sub表会出现NULL值此时status筛选的AND条件会把这个学生过滤掉。这个坑的解决办法是在写联查条件时把这个筛选放到HAVING或者子查询里去判断“未提交”这种状态而不是简单地用status IS NULL。4.5 事务与级联删除的坑删除一个培养计划的时候要连带删除计划下的阶段任务和提交记录这个操作必须加事务。我第一版写的时候直接写了三个delete方法然后按顺序调用结果删完计划之后阶段任务删除失败数据库里出现一堆孤儿数据。原因就是没有加Transactional注解三个操作不在一个事务里失败了也不回滚。Transactional(rollbackFor Exception.class) public void deletePlan(Long planId) { ListLong taskIds stageTaskMapper.selectTaskIdsByPlanId(planId); stageSubmissionMapper.deleteByTaskIds(taskIds); stageTaskMapper.deleteByPlanId(planId); trainingPlanMapper.deleteById(planId); }这里最容易被忽略的是删除顺序先删关联的子表数据再删主表数据否则外键约束会直接报错。如果你在设计表时没有加外键约束只靠逻辑关联那你要在代码里保证删除顺序这也是我为什么说数据库设计的逻辑关系必须清晰的原因。5. 前端Vue项目构建与联调细节5.1 项目创建与目录划分前端用Vue 2加Vue CLI为例创建命令就是vue create web-frontend。进入项目后按功能划分目录src/ api/ # 接口请求模块 assets/ # 静态资源 router/ # 路由配置 store/ # Vuex状态管理 views/ # 页面组件 login/ # 登录页 admin/ # 管理后台 mentor/ # 教师端 student/ # 学生端 components/ # 公共组件 utils/ # 工具类axios封装、token存取我习惯把接口请求按模块拆分user.js、post.js、plan.js独立成文件每个文件里只放对应模块的接口调用函数代码量大了以后一眼能找到入口。页面组件里就不会出现一堆axios调用散落各处维护成本低。5.2 路由与权限控制路由我采用静态路由加动态菜单的方案。页面路由本身是固定注册的不需要从后端动态生成因为页面数量有限。但菜单是根据角色动态显示的登录后根据用户role字段控制菜单渲染学生看到的是“我的培养计划-提交材料”这类菜单教师看到的是“我的学生-审核任务”管理员看到的是“用户管理-数据统计”。路由守卫用来处理未登录跳转router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });5.3 axios请求封装与拦截器axios如果不封装每个组件里都要写一遍请求地址、错误处理、loading显示重复代码多且风格不统一。我的封装思路是创建一个实例设置baseURL然后添加请求拦截器和响应拦截器。请求拦截器统一加token响应拦截器里判断result.code401跳登录业务错误统一提示。const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use(response { const res response.data; if (res.code 401) { localStorage.removeItem(token); window.location.href /login; return Promise.reject(new Error(res.message)); } return res; }, error { return Promise.reject(error); });注意axios封装里最常见的坑是响应拦截器return的是res前端拿到的对象结构和实际返回体不匹配所以我在封装里把res.data直接抛出去组件里调用result.code就对了。5.4 跨域代理与联调开发环境联调时最大的问题就是跨域。前端跑在8081端口后端跑在8080端口浏览器直接请求接口会报CORS错误。解决办法有两种一种是后端写CORS过滤器允许跨域另一种是前端用Vue CLI的proxy代理。我推荐用proxy因为生产环境部署时前端的请求本来就会经过Nginx同域转发用proxy模拟这个环境更合理。vue.config.js里的配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这样前端代码里请求地址写/api/user/login开发环境会被代理转发到后端8080端口生产环境交给Nginx做同样的转发。联调的时候还要注意一个问题后端接口如果访问不到不要只盯前端先用浏览器直接访问http://localhost:8080/api/user/login看看能不能返回能排除是后端没启动还是代理配置不对。5.5 前端构建与后端集成开发完打包用npm run build产物在dist目录是纯静态文件。部署时可以扔到Nginx里也可以拷贝到SpringBoot的src/main/resources/static目录下然后通过后端端口直接访问。放到后端的做法省一台Nginx但不太利于前端独立迭代我的建议还是单独用Nginx部署静态资源把后端反向代理出来这样前端后端互不影响发布时不冲突。6. 从零到一完整部署流程与排坑实录6.1 环境准备与关键安装点部署到Linux服务器前本机开发环境至少要装好这几样JDK 1.8以上推荐JDK 8或JDK 11构建和运行都需要Maven 3.6以上用来打后端jar包Node.js 14以上和npm用来构建前端MySQL 5.7或8.0作为数据库Nginx作为前端静态服务器和反向代理MySQL安装这里我多说一句。如果你在Linux上用rpm方式安装MySQL 8.0装完默认root密码只在日志里生成一次很容易错过。我的习惯是用rpm安装完成后用grep temporary password /var/log/mysqld.log找到临时密码然后立刻登录修改root密码。注意MySQL 8.0的密码策略默认要求密码包含大小写字母、数字和特殊符号太简单的密码过不了检查可在init文件中配置降低策略等级。6.2 后端jar包打包与启动在项目根目录执行mvn clean packageMaven会用短短几十秒到几分钟时间完成编译和打包最终在target目录下生成一个train-platform-0.0.1-SNAPSHOT.jar文件。启动命令最简单的方式是java -jar train-platform-0.0.1-SNAPSHOT.jar但如果想要后台运行并且把日志写出来要这样操作nohup java -jar train-platform-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod logs/app.log 21 这里用--spring.profiles.activeprod指定生产环境的配置文件因为我准备了application-dev.yml和application-prod.yml两套配置。生产环境的数据库地址、密码都放到prod配置里避免开发配置泄露。这一步在多人协作项目中非常关键我曾经见过别人把生产库密码写在application.yml里直接提交到代码仓库这是典型的安全事故。6.3 前端Nginx部署配置前端打包后的dist目录上传到服务器的/var/www/train-platform目录下然后配置Nginx。我的Nginx站点配置大概长这样server { listen 80; server_name your-domain.com; root /var/www/train-platform; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }location /里的try_files是最关键的配置vue-router使用history模式时刷新页面如果找不到对应路径就会404这里把请求回退到index.html由前端路由接管。location /api/把前端API请求反向代理到后端8080端口前后端共用一个域名彻底解决跨域。6.4 数据库迁移与初始化部署时数据库这块我踩过一个比较惨的坑第一次上线时直接本地导出的SQL文件往外传结果线上的MySQL版本比本地高导出的默认字符集设置不一样导入后中文全部变成问号。后来养成习惯导出SQL前明确指定导出内容为utf8mb4编码在命令行导入前先执行一句SET NAMES utf8mb4确认客户端编码。初始化流程是登录线上MySQLcreate database train_platform字符集设为utf8mb4然后source导入SQL文件再把默认管理员账号的BCrypt密文写入用户表。跑通之后检查一遍基础数据数量确认没问题再启动后端。6.5 部署中我实际遇到的5个坑第一个坑是端口被占用。服务器上别的服务占了8080SpringBoot应用启动直接报端口冲突排查半天。后来我在启动命令里加了--server.port8081临时换个端口验证确认问题后改回规划端口并腾出占用资源。第二个坑是数据库连接串没加serverTimezone新版MySQL驱动启动时报错Cannot create PoolableConnectionFactory。这个在开发环境可能一直没暴露因为本机MySQL时区设置碰巧没问题部署到服务器就翻车。建议统一在连接串里加上serverTimezoneAsia/Shanghai。第三个坑是Nginx上传文件大小限制。学生提交培养材料时上传附件如果超过Nginx默认的1M限制会直接返回413 Request Entity Too Large。所以需要把client_max_body_size调到合适的值我在这边设置的是100m。第四个坑是前端路由history模式的404。第一次部署时访问首页没问题点击页面内跳转也正常但我刷新了一个子路由页面就404了。排查后发现就是上面说的try_files配置缺失这是vue-router history模式部署时的经典问题。第五个坑是后端日志看不到SQL。排查线上问题时发现MyBatis的SQL执行日志不输出定位问题全靠猜。原因是生产配置文件里没有配置日志级别我加上logging.level.com.example.train.mapperdebug之后SQL和参数都能输出排查效率翻倍。最后分享一点个人体会这个项目从需求梳理到上线我最大的收获不是哪段代码写得漂亮而是把整个流程串起来的经验。数据库表设计时多想一步关系后面写SQL就少改一次前端跨域问题在开发环境靠代理解决生产环境靠Nginx转发解决两条路径都想清楚就不会被卡住。如果你也在做交流培养管理平台这类前后端分离项目我建议你按这个顺序推进先把表结构设计好再做后端接口并保证每个接口能用浏览器直接访问最后再做前端页面。只要这个顺序不乱整个开发过程会顺畅非常多。还有一点部署一定尽早做不要等代码全写完再部署早部署早发现问题很多坑都是上线前才暴露的提前踩总比验收时手忙脚乱强。
返回列表