ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue学生宿舍管理系统开发实战:从架构到部署

SpringBoot+Vue学生宿舍管理系统开发实战:从架构到部署 1. 项目定位与整体设计思路1.1 为什么选用SpringBoot Vue这套组合做学生宿舍管理系统技术选型其实是个需要认真权衡的事情。我见过不少同类项目还在用JSP Servlet的老一套也见过用纯Vue mock数据糊弄交差的但真正要落地到实际使用场景SpringBoot Vue前后端分离几乎是目前最稳妥的组合没有之一。先聊后端。SpringBoot的优势在于它把Spring生态里那些繁琐的XML配置全部干掉通过自动装配机制让你用极少的配置就能跑起来一个可用的Web服务。拿宿舍管理这个场景来说学生信息管理、宿舍分配、报修工单、卫生检查记录这些都是典型的CRUD操作加上一点业务规则校验SpringBoot MyBatis Plus可以非常高效地完成这些事。我实测过从零开始搭一个包含JWT鉴权、统一异常处理、分页查询的后端骨架用SpringBoot大概只需要半小时。再聊前端。Vue的响应式数据绑定和组件化开发天然适合管理后台这类页面。宿舍管理系统的前端无非就是登录页、Dashboard统计页、学生管理页、宿舍管理页、报修管理页这些每一个页面都可以拆成可复用的组件比如表格组件、弹窗组件、表单组件。Vue Router帮我们管理页面跳转和路由守卫Vuex或Pinia管全局登录状态这套组合在中小型管理系统的开发效率上非常能打。前后端分离的架构还有一个隐藏福利将来如果想给这个系统加一个移动端适配或者接入微信公众号查询宿舍信息后端接口可以直接复用不需要重写业务逻辑。这也是我为什么坚持推荐这套架构的根本原因——它不是最炫的技术栈但它是性价比最高、最不容易翻车的方案。1.2 系统功能模块怎么划分学生宿舍管理系统的核心业务场景我在实际调研了几个高校的后勤管理部门之后把需求归成了六个核心模块模块核心功能涉及角色用户认证登录、登出、修改密码、Token刷新学生、宿管、管理员学生管理学生信息录入、导入导出、入住登记管理员、宿管宿舍管理宿舍楼栋、房间、床位信息维护管理员分配管理入住分配、调宿、退宿、空床位查询宿管、管理员报修管理报修提交、派单、处理状态流转学生、宿管日常管理卫生检查打分、访客登记、晚归记录宿管、管理员这套模块划分是我在实际项目中反复调整后沉淀下来的。原本还想过加一个公告通知模块后来跟使用方聊完发现学校已经有企业微信和QQ群发通知了没必要在系统里再造一个轮子。做项目最忌讳的就是需求堆砌每个功能都要问一句“真的有人会用吗”这样砍完之后系统反而更好用。角色权限方面我采用的是RBAC模型也就是基于角色的访问控制。学生只能看到自己的入住信息和发起报修宿管能管理一栋楼的日常事务管理员拥有全部权限。这个权限模型在SpringBoot的后端用拦截器 自定义注解就能实现不用引入Spring Security那么重的东西对学生项目或者中小型系统来说足够用了。2. 后端SpringBoot核心实现细节2.1 数据库设计与MyBatis Plus实战数据库设计是整个系统的地基我见过太多项目在后期因为表结构设计不合理而返工。宿舍管理系统我建议设计这几张核心表-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码(BCrypt加密), real_name VARCHAR(50) COMMENT 真实姓名, role TINYINT NOT NULL COMMENT 角色:1学生 2宿管 3管理员, student_no VARCHAR(20) COMMENT 学号, phone VARCHAR(20) COMMENT 手机号, status TINYINT DEFAULT 1 COMMENT 状态:1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 宿舍楼栋表 CREATE TABLE dorm_building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_name VARCHAR(50) NOT NULL COMMENT 楼栋名称如A栋, floor_count INT COMMENT 楼层数, manager_id BIGINT COMMENT 宿管ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 宿舍房间表 CREATE TABLE dorm_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL COMMENT 所属楼栋, room_no VARCHAR(20) NOT NULL COMMENT 房间号, bed_count INT DEFAULT 4 COMMENT 床位数, beds_used INT DEFAULT 0 COMMENT 已用床位, status TINYINT DEFAULT 1 COMMENT 状态:1正常 0维修中, UNIQUE KEY uk_building_room (building_id, room_no) ); -- 入住记录表 CREATE TABLE dorm_assignment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID, room_id BIGINT NOT NULL COMMENT 房间ID, bed_no INT COMMENT 床位编号, check_in_time DATETIME COMMENT 入住时间, check_out_time DATETIME COMMENT 退宿时间, status TINYINT DEFAULT 1 COMMENT 状态:1在住 0已退宿 ); -- 报修工单表 CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 报修人, room_id BIGINT NOT NULL COMMENT 报修房间, description VARCHAR(500) COMMENT 问题描述, image_url VARCHAR(200) COMMENT 图片URL, status TINYINT DEFAULT 0 COMMENT 状态:0待处理 1处理中 2已完成 3已关闭, assignee VARCHAR(50) COMMENT 处理人, handle_time DATETIME COMMENT 处理时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );MySQL版本建议直接用8.0字符集统一用utf8mb4这个编码能存emoji和生僻字学生姓名里有生僻字的情况在北方高校还挺常见的我踩过这个坑。MyBatis Plus的使用上我强烈建议开启逻辑删除和自动填充。逻辑删除就是加一个deleted字段删除操作变成更新操作防止误删数据自动填充就是让create_time和update_time这类字段在插入和更新时自动赋值。MyBatis Plus提供MetaObjectHandler接口实现自动填充写一次全局生效不用每个实体都手动set时间节省大量重复代码。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }分页查询是管理系统的刚需MyBatis Plus的PaginationInnerInterceptor配置一下就能用需要传入数据库类型。我见过有人不用分页插件自己写limit拼SQL一旦遇到动态查询条件就变得非常难维护完全没有必要。2.2 JWT鉴权与统一响应格式前后端分离架构下Session的天然劣势在多个部署节点时会暴露出来而JWT无状态的设计更契合当前的主流方案。JWT全称是JSON Web Token简单说就是后端在用户登录成功后签发一个加密的Token前端把Token存下来之后每次请求都带上后端验证Token合法就认为请求可信。在实际实现中我用的方案是jjwt库加自定义拦截器。用户在登录接口提交用户名密码校验通过后生成Token返回前端存储到localStorage或Pinia里。后端的拦截器统一从请求头中取Token解析成功就把用户信息放入ThreadLocal中后续业务代码里直接通过UserContext.getUserId()获取当前用户非常方便。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); UserContext.setUserId(claims.get(userId, Long.class)); UserContext.setRole(claims.get(role, Integer.class)); return true; } catch (Exception e) { // Token无效 } } // 返回JSON格式未认证信息 response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSON.toJSONString(Result.error(401, 未登录或Token已过期))); return false; } }这里有个细节需要注意Token的过期时间建议设成2小时前端在拿到后端返回的401错误后跳转到登录页并清理本地存储。如果用户的Token有效期设置太长安全风险会明显上升太短又会导致用户频繁重新登录影响体验。这个值是实际使用中的权衡结果我试过24小时和30分钟体验都不如2小时来得平衡。统一响应格式也是后端开发中容易被忽略但实际上很重要的习惯。我这边定义了一个Result类状态码、消息、数据三个字段成功返回Result.success(data)失败返回Result.error(code, msg)。这样前端在axios的拦截器里统一处理不需要每个接口各自判断不同结构联调效率提升非常明显。很多初学者喜欢后端直接返回Map或原始对象当时确实省事一旦前端需要统一处理错误提示或加解密就会非常后悔。2.3 宿舍分配的核心业务逻辑宿舍分配是整个系统里最有业务含量的一块。基本规则是优先满足同班同学住一起、按入住时间顺序分配空床位、支持手动调整。我设计了一个DormAssignService来处理这块逻辑避免直接在Controller里堆代码。关键点是空床位的查询。一个房间有bed_count个床位已经住了beds_used个人直观的做法是遍历所有房间判断beds_used bed_count。但这有个坑退宿之后beds_used要减一减到多少该把房间状态置为检修这些联动逻辑如果散落在各个调用方很容易出现数据不一致。我的做法是把这些操作收敛到Service层对外只暴露assignRoom、changeRoom、checkOut三个方法内部加事务管理保证数据一致性。Transactional(rollbackFor Exception.class) public Result assignRoom(Long studentId, Long buildingId, Long roomId) { // 1. 查询房间信息 DormRoom room roomMapper.selectById(roomId); if (room null || room.getStatus() ! 1) { return Result.error(房间不存在或不可入住); } // 2. 检查床位是否已满 if (room.getBedsUsed() room.getBedCount()) { return Result.error(该房间已住满); } // 3. 检查学生是否已在住 LambdaQueryWrapperDormAssignment wrapper new LambdaQueryWrapper(); wrapper.eq(DormAssignment::getStudentId, studentId) .eq(DormAssignment::getStatus, 1); if (assignmentMapper.selectCount(wrapper) 0) { return Result.error(该学生已在住请先办理退宿); } // 4. 分配入住 DormAssignment assignment new DormAssignment(); assignment.setStudentId(studentId); assignment.setRoomId(roomId); assignment.setBedNo(room.getBedsUsed() 1); // 床位号简单从1开始排 assignment.setCheckInTime(LocalDateTime.now()); assignment.setStatus(1); assignmentMapper.insert(assignment); // 5. 更新房间已用床位 room.setBedsUsed(room.getBedsUsed() 1); roomMapper.updateById(room); return Result.success(); }Transactional注解一定要加在业务方法上它的作用是让这个方法的多个数据库操作要么全部成功、要么全部回滚。如果只调用了多个Mapper方法而不加事务插入入住记录成功但更新床位失败就会出现数据不一致这种Bug查起来非常痛苦。我建议所有包含两次以上写操作的方法都加上事务。3. 前端Vue开发实战3.1 项目初始化与Element Plus集成前端我推荐直接用Vite搭建Vue3项目而不是Vue CLI。Vite的启动速度在开发环境下优势太明显了热更新几乎是秒级响应对于Vue Router、Element Plus这类依赖较多的项目来说体验上的差距非常明显。创建一个新项目的命令npm create vitelatest dormitory-web -- --template vue cd dormitory-web npm install npm install element-plus axios pinia vue-routerElement Plus是Vue3生态里最成熟的中后台组件库表格、表单、弹窗、分页这些现成的组件都有做管理系统省了非常多的事。全局引入Element Plus的方式很简单import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)如果是项目体积比较敏感的场景可以考虑按需自动导入用unplugin-vue-components和unplugin-auto-import这两个插件只打包用到的组件。但对于宿舍管理系统这种后台应用我实测全量引入和按需引入的打包体积差距大约在300KB左右Gzip之后差距更小所以直接用全量引入最省心。真做到线上部署体积特别敏感再换不迟。3.2 用户认证状态管理登录状态管理是前端架构的核心。我用Pinia来管理用户信息和Token页面刷新后从localStorage重新加载用户信息。这里有个常见的坑localStorage存在XSS攻击风险Token如果被恶意脚本拿到就能冒充用户操作。简单处理方式是在前端做好输入过滤不渲染未经处理的HTML片段高安全要求场景可以改用httpOnly Cookie存Token但会引入CSRF防护的复杂度。宿舍管理系统属于校园内网应用用localStorage 登录过期时间基本够用。// stores/user.js import { defineStore } from pinia import { loginApi, getUserInfoApi } from /api/auth export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }), actions: { async login(username, password) { const res await loginApi({ username, password }) this.token res.data.token this.userInfo res.data.userInfo localStorage.setItem(token, this.token) localStorage.setItem(userInfo, JSON.stringify(this.userInfo)) }, async fetchUserInfo() { const res await getUserInfoApi() this.userInfo res.data localStorage.setItem(userInfo, JSON.stringify(this.userInfo)) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })路由守卫是另一个必须做好的事情。Vue Router的路由守卫可以根据用户角色控制访问权限。比如学生角色不能访问宿舍管理页面不是管理员不能访问用户管理页面。实现方式是给路由的meta字段加上角色要求然后在beforeEach中判断。3.3 宿舍管理页面组件拆分拿宿舍管理页面举例这个页面的UI结构包括左侧楼栋列表、右侧房间表格、顶部筛选条件、右下角的分页器。按照Vue组件化的思路可以拆成DormBuildingList.vue楼栋选择列表点击切换当前楼栋DormRoomTable.vue房间表格展示房间号、床位使用情况、状态RoomDetailDialog.vue房间详情弹窗展示入住学生列表支持分配调整每个组件通过props接收父组件传的数据通过emit向父组件发送事件。这样拆分的好处是每个组件的职责单一修改楼栋列表的样式不会影响到房间表格排查问题的时候也不用来回跳转代码找起来特别快。我见过有人把整个页面所有逻辑代码都写在一个vue文件里两千行代码自上而下堆下来测试的时候改一个变量要滚动半天后期维护成本实在太高了。前端调接口时axios的封装我也提一下。基础配置里设置baseURL请求拦截器里加上Token响应拦截器里统一处理后端返回的状态码。特别是401这种状态响应拦截器里统一跳转登录页省得每个接口都写一遍错误判断。// api/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.clear() router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request4. 前后端联调与权限控制4.1 跨域问题处理与开发代理前后端分离开发最常遇到的就是跨域问题。前端在localhost:5173后端在localhost:8080两个端口不同就产生了跨域。解决方案有两种开发环境用Vite代理生产环境用Nginx或者后端加CrossOrigin。开发环境最好用Vite的代理配置而不是在代码里开启CORS。修改vite.config.jsexport default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样前端请求/api/user/list会被代理到http://localhost:8080/user/list前后端联调完全无感。生产环境我建议用Nginx统一处理静态资源和反向代理server { listen 80; server_name your.domain.com; # 前端静态资源 location / { root /opt/dormitory-web/dist; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }Nginx配置里有一个关键点前端路由如果用的是history模式而非hash模式必须加上try_files $uri $uri/ /index.html这行配置否则刷新页面时Nginx会在磁盘上寻找对应路径的文件找不到就会返回404。这个坑我踩过好多次用hash模式没有这个问题但地址栏会有个#看起来不够正式。4.2 角色权限控制的双端配合权限控制要前后端配合缺一不可。前端做路由守卫和按钮级控制是提升用户体验用的后端做接口鉴权才是真正的安全防线。只做前端控制等于没控制因为任何人打开浏览器的开发者工具就能绕过前端直接调用接口这点我反复强调过。后端我刚提过JWT拦截器负责登录校验业务层的权限需要配合自定义注解来实现。写一个RequireRole注解在需要限制角色的Controller方法上标注Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int value() default 3; }然后在JWT拦截器中解析出当前用户的角色如果角色值小于注解要求的值直接返回403。注意我这里约定的是学生角色值为1宿管为2管理员为3数字越大权限越高。判断逻辑就是当前角色 注解要求角色才有资格访问接口简单明了。前端按钮级的控制用的是自定义指令v-permission。比如学生管理页面有个“新增学生”按钮只有管理员能看到宿管看不到app.directive(permission, { mounted(el, binding) { const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) const requiredRole binding.value if (userInfo.role requiredRole) { el.parentNode el.parentNode.removeChild(el) } } })5. 常见问题与排查技巧实录5.1 SpringBoot启动失败与端口占用问题开发中遇到最多的问题是启动失败。自己排查的时候先看启动日志的最后几行有没有APPLICATION FAILED TO START这样的标题有的话下面会有Description说明具体原因。最常见的几个原因现象原因解决方案Port 8080 was already in use端口被占用换端口或杀掉占用进程Consider defining a bean of type XxxMapper启动类没扫描到Mapper启动类加MapperScan(com.xxx.mapper)Failed to configure a DataSource数据库连接配置错误检查application.yml的连接地址和账号密码Cannot resolve symbolMaven依赖没下载全执行mvn clean install或检查网络环境端口占用的情况Windows下用netstat -ano | findstr 8080找到PID然后taskkill /PID 进程号 /F杀掉。Mac和Linux用lsof -i:8080找到进程号再用kill -9处理。新手容易在这里卡住直接删掉或者重启电脑都是不可取的做法会浪费很多不必要的时间。5.2 Vue前端白屏与路由跳转失败Vue项目启动后页面白屏常见原因包括组件的name写错导致路由匹配不上、Element Plus没正确注册、JS报错被浏览器拦截。我建议遇到白屏第一步先打开浏览器F12看Console面板有没有红色报错信息根据报错信息来定位。大多数情况都是component is undefined或者xxx is not defined这种可以直观定位的问题。还有一种是npm run dev能启动但访问时接口全部报404。这个大概率是Vite代理配置写错检查vite.config.js里proxy的target是否指向了正确的后端地址以及前端request.js里的baseURL是否以/api开头。路由跳转失败的情况最常见的是登录成功后跳转到首页失败。原因是Vue Router 4中push方法返回一个Promise如果跳转时抛出NavigationDuplicated错误控制台会报一个不影响功能的错误看起来很吓人但系统其实正常。有两种方案在push前catch掉这个错误或者用router.replace代替router.push。另外查一下路由表定义确保/redirect或/home这些路径已经正确注册不然跳到一个不存在的路由也会白屏。5.3 部署上线前的配置注意点本地开发一切正常打包部署后出问题也是家常便饭。我总结几个高频坑位前端请求后端的地址开发时用相对路径/api部署后如果前后端不在同一个域名下需要改成完整地址或通过Nginx代理。如果硬编码了localhost:8080部署到服务器上肯定连不上。后端的数据库地址application.yml里如果写的是localhost部署到服务器后也要改成服务器的数据库地址。注意不用把数据库密码明文写在配置里用环境变量占位比如${DB_PASSWORD}这样更安全。静态资源路径学生上传的报修图片如果存到了本地磁盘Nginx需要额外配置一个/upload/路径指向该目录否则前端加载图片会404。Docker部署如果用Docker记得把后端容器的端口映射到宿主机的端口配置-p 8080:8080。数据库如果用Docker容器跑两个容器默认网络不通需要使用docker network或者通过宿主机IP访问。这些坑本质原因都一样开发环境和生产环境的差异没有在配置上做好隔离。SpringBoot可以用application-dev.yml和application-prod.yml分别放不同环境的配置前端也可以用环境变量区分这条路走通之后部署会顺手很多。5.4 数据库连接池和性能初优化宿舍管理系统的高峰场景大概是开学季几百个学生同时访问。MySQL默认的连接数是151如果不做任何配置同时在线人数一多就大概率报Too many connections。SpringBoot默认使用HikariCP连接池需要在配置里调一下最大连接数spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000maximum-pool-size设20对单体应用来说够用了没必要贪多太大会增加数据库的负担。HikariCP的另一项配置也值得注意connection-timeout设置的是获取连接的超时时间单位毫秒如果数据库慢查询多这个值设太小会导致应用频繁报连接超时需要配合慢SQL排查一起优化。MyBatis Plus的查询也建议加上索引。拿报修工单表举例student_id和status都是高频查询字段加个复合索引能明显加快查询速度特别是“我的报修记录”这种按学生维度查的场景效果会很显著。SQL写法上避免在WHERE条件里对索引字段做函数运算比如WHERE DATE(create_time) 2025-01-01这种写法会让索引失效改成create_time 2025-01-01 AND create_time 2025-01-02就能正常走索引。6. 项目扩展与个人经验总结6.1 后续可以考虑增强的方向基础版本的宿舍管理系统做完之后还有几个方向值得扩展。一是数据可视化把宿舍入住率、报修量趋势、各楼栋卫生平均分等数据做成图表后端用定时任务聚合成统计数据前端用ECharts来展示这样就有一个像样的可视化数据大屏让管理层直观掌握整体情况。二是消息通知比如报修状态变化时可以通过邮件或企业微信机器人通知学生不需要自己写推送系统用现成的webhook就能实现。三是移动端适配不考虑原生App成本最低的方案是把现有Vue项目做成响应式布局或者直接用H5页面配合微信公众号菜单入口。6.2 几个值得分享的心得做这类管理系统项目我最大的体会是“业务规则比技术栈更值得花时间”。宿舍分配什么时候允许调宿、退宿之后床位的保留时间、卫生检查的评分标准这些业务规则如果前期跟使用方沟通不清晰后期改起来牵连的表和接口非常多比换个技术栈还痛苦。所以做方案设计的时候我会先画出业务流程图把所有业务规则一条条列清楚再进入编码阶段。另一点是对前后端分离的完整链路要有敬畏之心。从数据库设计到后端接口再到前端页面联调、部署上线每一环都有它自己的坑。我第一次独立做这个项目时跨域、端口占用、Maven依赖冲突、路由打包后404这些问题几乎全踩了一遍但正是因为每一个坑都亲自踩过并找到解决方案现在再做同类项目时架构设计和问题排查的效率都高了很多。最后分享一个实用小工具SpringBoot项目的banner可以通过在线生成器定制把默认的Spring banner换成自己设计的样式代码仓库看起来会更有辨识度。这类看似细节的东西在团队协作或课程设计展示时其实能体现一个人的工程意识。整个项目从数据库建表到部署上线需要掌握的技能点是密集且完整的把每一步都吃透你收获的不只是一个管理系统而是整套前后端分离开发的方法论。
返回列表