尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot+Vue校园服务交流平台:前后端分离架构实战解析

Spring Boot+Vue校园服务交流平台:前后端分离架构实战解析 1. 项目到底在做什么先讲场景再谈代码如果你在校园里待过一段时间一定会碰到这些场景食堂丢了一卡通急得到处找毕业季宿舍里堆着带不走的书和电器想找学长问个专业课重点却发现没有渠道社团想办活动但报名和通知全靠群里接龙。这些零散的需求本质上是校园内缺少一个统一的服务交流入口。我做的这个基于Java、Spring Boot和Vue的校园服务交流平台就是要把这些场景整合到一个Web应用里让失物招领、二手交易、活动发布、资料共享、校内问答这些高频需求都有地方可去。这个项目适合谁参考如果你是正在做课程设计或毕业设计的在校生想找一个前后端分离的完整案例或者你是刚入行Java开发、想弄明白一个真实业务系统从前端到后端要经历哪些环节的初学者又或者你是学校信息化部门的技术人员想低成本搭一个校内服务工具——这篇内容都适合你。我把它定位成一个轻量级校内服务中台后端只负责提供稳定的数据接口前端独立成单页应用后续不管要接小程序还是移动端接口都能直接复用。这也是我选择前后端分离架构的根本原因而不是像传统课程设计那样把页面和服务端代码揉在一个工程里。2. 技术选型背后为什么偏偏是Spring Boot加Vue2.1 后端选Spring Boot的三个硬理由后端我选了Java Spring Boot没有去碰SSH或者SSM那一套老组合。原因很直接Spring Boot把Spring家族的配置简化到了极致以前SSM项目里要手写一堆XML配置、配置数据源、配置事务管理器现在一个spring-boot-starter-web依赖加进来内嵌Tomcat一启动就能跑。这对做校园项目来说非常关键因为大部分时间要花在业务逻辑上而不是消耗在环境配置里。第二个理由是生态成熟度。校园类平台绕不开用户权限、文件上传、数据分页、定时任务这些通用需求Spring Boot对这些场景几乎都有现成的starter或第三方整合方案。比如权限这块Spring Security JWT是标准打法文件上传有Spring自带的MultipartFile分页有MyBatis-Plus的IPage接口。生态成熟意味着你遇到问题时能搜到大量可参考的解决方案而不是自己在坑里爬。第三个理由是部署简单。Spring Boot打出来的jar包扔到服务器上用java -jar一条命令就能起来配合Nginx做反向代理整个上线流程可以压缩到十分钟以内。这一点对学校这种不一定有专职运维人员的环境非常友好。2.2 前端选Vue的原因和版本取舍前端技术栈里我选了Vue而不是React核心考虑是学习曲线和团队上手难度。Vue的模板语法贴近原生HTML一个懂HTML和JavaScript的人基本上半天就能写出像样的组件。项目中我用的是Vue 2 Element UI的组合如果你现在从零开始我更推荐Vue 3 Element PlusVue 3组合式API对逻辑复用更友好Element Plus组件库的维护也更活跃。这里补充一点选择经验如果你不是一个人从零写而是接手一个老项目一定要注意Vue 2和Vue 3的根本差异。响应式原理从Object.defineProperty换成了Proxy这意味着Vue 2里那些Vue.set的坑位在Vue 3里不存在了路由从vue-router 3换成vue-router 4动态路由的使用方式有调整。别一上来就把Vue 2的写法搬到Vue 3里否则你会发现数据变了但视图死活不更新。2.3 周边工具链的搭配逻辑整个项目的技术栈可以列成一张清单层面技术选型承担职责后端框架Spring Boot 2.7 / 3.x接口服务、业务逻辑、安全认证ORMMyBatis-Plus数据访问、分页查询、条件构造数据库MySQL 8.x业务数据持久化存储缓存RedisToken存储、热点数据缓存权限认证Spring Security JWT登录认证、接口鉴权前端框架Vue 2/Vue 3页面渲染、状态管理、路由控制UI组件库Element UI / Element Plus表格、表单、弹窗等基础组件构建工具Maven npm后端依赖管理、前端构建打包部署Nginx 云服务器静态资源服务、反向代理这套组合不是我想当然定的而是经过取舍的。比如Redis很多校园项目其实没多大并发压力但校园平台有个特点学期初选课、评教那几天访问量会突然飙升。把登录Token和热门二手商品缓存到Redis能让MySQL少挨不少打。再比如用MyBatis-Plus而不是原生MyBatis是因为校园项目的查询条件经常变今天要多筛个分类明天要多按时间排序用条件构造器QueryWrapper写起来比拼XML快得多。3. 核心模块拆解从数据库到接口的完整设计3.1 表结构怎么设计才能少走弯路校园服务交流平台的核心表我拆成了七张用户表、帖子表、评论表、二手商品表、失物招领表、活动表、私信表。每张表的设计都遵循了一个原则能冗余的字段尽量冗余能不用外键就不用外键。这里说的冗余不是数据冗余而是把关联查询常用的字段直接存进业务表比如帖子表里直接存发布者的昵称和头像就不用每次查一遍用户表。以用户表为例CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, role TINYINT DEFAULT 1 COMMENT 角色1学生 2教职工 3管理员, student_no VARCHAR(20) DEFAULT NULL COMMENT 学号/工号, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, status TINYINT DEFAULT 1 COMMENT 状态1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;说几个我踩过坑的设计细节。第一个密码字段长度给100很多人给50结果BCrypt加密后的字符串是60个字符存进去直接报错。第二个所有表都加上create_time和update_time虽然看起来繁琐但排查数据问题时没有时间戳你会非常被动。第三个MyBatis-Plus的自动填充功能配合TableField(fill FieldFill.INSERT)注解插入和更新时自动维护时间字段不用手写每个INSERT。帖子表的设计是平台的枢纽因为失物招领、二手买卖、活动讨论这些场景最后都要落到帖子上。我用一个type字段来区分业务类型避免拆出多个结构相似的表。CREATE TABLE post ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 帖子ID, user_id BIGINT NOT NULL COMMENT 发布者ID, nickname VARCHAR(50) DEFAULT NULL COMMENT 发布者昵称冗余, avatar VARCHAR(255) DEFAULT NULL COMMENT 发布者头像冗余, type TINYINT NOT NULL COMMENT 类型1失物招领 2二手交易 3校园活动 4学习资料 5问答交流, title VARCHAR(100) NOT NULL COMMENT 标题, content TEXT COMMENT 正文内容, images VARCHAR(1000) DEFAULT NULL COMMENT 图片URL逗号分隔, category VARCHAR(50) DEFAULT NULL COMMENT 分类如数码/图书/生活用品, price DECIMAL(10,2) DEFAULT NULL COMMENT 二手商品价格, location VARCHAR(100) DEFAULT NULL COMMENT 位置信息, contact VARCHAR(100) DEFAULT NULL COMMENT 联系方式, view_count INT DEFAULT 0 COMMENT 浏览量, like_count INT DEFAULT 0 COMMENT 点赞数, comment_count INT DEFAULT 0 COMMENT 评论数, status TINYINT DEFAULT 1 COMMENT 状态1正常 0下架 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type_status (type, status), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT帖子表;这里有一个关键设计考量把nickname和avatar冗余进帖子表。按传统的数据库范式来看这是违规的但在实际应用中帖子列表页每次展示几十条帖子如果不冗余就得做几十次子查询去关联用户表性能差距非常明显。校园平台的数据量虽然不大但这种写法能让你省掉很多联表查询的代码而且只要在发帖时把用户信息带进来以后用户改了昵称也不影响历史帖子展示快照从产品层面看反而更合理。3.2 登录认证与权限控制JWT怎么串起来校园平台虽然有别于商业系统但权限控制依然不能省。我用了Spring Security JWT这套组合整体思路是用户登录成功后后端生成一个带过期时间的JWT字符串返回给前端前端把Token存在localStorage里每次请求通过Authorization: Bearer token头发送后端用拦截器校验。JWT本身由三部分组成Header、Payload和Signature。Header声明加密算法Payload存储用户ID、角色、过期时间等非敏感信息Signature用服务端密钥对前两部分签名。当客户端拿着Token过来服务端只需要用同样的密钥验签就能确认Token有没有被篡改过整个校验过程是无状态的不需要像传统Session那样在服务端存会话记录这对水平扩展非常友好。核心代码如下OncePerRequestFilter是我比较推荐的写法保证过滤器在一次请求中只执行一次Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtTokenProvider tokenProvider; Autowired private UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null tokenProvider.validateToken(token)) { String username tokenProvider.getUsernameFromToken(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }这段代码的逻辑其实很直白先看请求头里有没有Token有就校验校验通过就把用户信息塞进Spring Security的上下文里后面的接口就能通过AuthenticationPrincipal拿到当前登录用户了。JWT的过期时间我设置的是2小时外加一个Redis里存的refresh token7天有效前端发现Token过期后自动用refresh token换新的。校园项目也许用不上这么复杂但这个机制能避免用户正刷着二手市场看商品突然被弹出去重新登录的尴尬。权限这块我用最简单的角色控制。在Controller上加PreAuthorize(hasRole(ADMIN))就能限制管理员接口学生和教职工默认都能访问普通业务接口。管理员后端在用户管理模块可以禁用某个账号被禁用后Redis里的Token会被删除用户下一次请求就校验不通过了这个即时生效的效果是Session方案很难做到的。3.3 发布与互动模块帖子的增删改查实现帖子模块是整个平台接口最密集的部分。发布、列表、详情、删除、点赞、加精、置顶每一个接口都不难难的是把列表查询的效率和代码优雅度兼顾好。我直接用了MyBatis-Plus的分页机制配合QueryWrapper做条件过滤Override public PageResultPostVO getPostPage(PostQuery query) { PagePost page new Page(query.getPageNum(), query.getPageSize()); QueryWrapperPost wrapper new QueryWrapper(); // 按类型筛选 if (query.getType() ! null) { wrapper.eq(type, query.getType()); } // 按分类筛选比如二手交易里的数码分类 if (StringUtils.hasText(query.getCategory())) { wrapper.eq(category, query.getCategory()); } // 关键词模糊搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.and(w - w.like(title, query.getKeyword()) .or().like(content, query.getKeyword())); } // 排序规则置顶优先然后按时间倒序 wrapper.orderByDesc(is_top).orderByDesc(create_time); wrapper.eq(status, 1); // 只查询正常状态下 PagePost result postMapper.selectPage(page, wrapper); // 组装VO补充点赞状态等信息 return convertToPageResult(result, query.getUserId()); }这个接口里有个细节值得说关键词搜索用了like如果平台数据量真的大起来这个写法会全表扫描。但考虑到校园场景的数据量级用MySQL的LIKE完全够用真的要做全文检索那是Elasticsearch的事对课程设计和校内小项目都属于过度设计。发布帖子时要注意XSS攻击问题。用户的输入是纯文本但前端展示时用的是v-html渲染内容等于把控制权交给了用户。我处理的方式是在后端加了一个全局过滤器对所有请求参数里的HTML标签做转义。public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssWrapper((HttpServletRequest) request), response); } } // XssWrapper覆写getParameter和getInputStream // 把 script 等危险标签转义为 lt;scriptgt;评论区用了类似帖子列表的套路只是post_id字段多了一层关联。点赞模块的设计稍微特殊一点我没有在点赞接口里直接更新帖子表的like_count而是建了一张独立的post_like关联表这样用户能不能重复点赞、取消点赞后计数是否准确都有据可查。4. 前后端联调实战从工程初始化到接口对接4.1 后端工程一步步搭建后端项目我习惯用Maven管理第一步在pom.xml里引入核心依赖。如果你是照着做Spring Boot 2.7这个版本更稳因为跟它配套的MyBatis-Plus、Spring Security和JWT都有大量现成案例可以参考。引入依赖时注意版本冲突——尤其Spring Boot 3.x之后javax包名换成了jakarta很多旧教程里的代码会直接编译失败。依赖列表核心部分如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyapplication.yml里的配置我建议把数据库密码放到环境变量里不要直接写在文件里。虽然校园项目不一定会被攻击但这种习惯能让你在面向真实工作场景时不用临时改正。如果你的MySQL密码有特殊字符比如或者#记得在YAML里给URL加引号否则解析时会出问题。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_service?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD} redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true开发阶段打开SQL日志打印很有用能实时看到MyBatis生成的SQL和参数值。map-underscore-to-camel-case设为true后数据库里的create_time能自动映射到Java类的createTime省掉大量XML映射配置。后端分层的包结构我固定成controller / service / mapper / entity / dto / vo / config / common。Controller只做参数接收和结果返回Service层写业务逻辑common包放全局异常处理和统一返回结果类。这样分的好处是团队协作时每个人负责一个包不会冲突更重要的是出问题的时候能快速定位。统一返回结果类ResultT的设计也很关键我定义的格式是{ code: 200, message: 操作成功, data: {} }所有接口都用这个格式返回前端Axios拦截器就能统一处理错误码和成功码不需要每个接口单独写判断逻辑。4.2 Vue前端环境配置和项目初始化前端项目我用Vue CLI初始化然后手动安装配套依赖。如果你跟着做建议Node.js版本选16或18。Vue CLI创建项目时选择Manually select features勾选Router和Vuex然后一路确认。# 安装Vue CLI npm install -g vue/cli # 创建项目 vue create campus-client # 进入项目目录 cd campus-client # 安装UI组件库、Axios和路由 npm install element-ui axios vue-router3这里要单独说一下版本坑。Vue 2对应的是vue-router3Vue 3对应的是vue-router4Element UI对应Vue 2Element Plus对应Vue 3。我见过好几个人没注意这两个对应关系装完发现项目启动直接报错甚至页面白屏。这种问题通常不是代码写错了而是版本不匹配。前端项目按功能模块拆分目录跟后端的包结构形成对应src/ ├── api/ # 接口请求封装每个模块一个文件 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── views/ # 页面组件 ├── utils/ # 工具函数 └── App.vueAxios封装是前端的重点。我建了一个request.js统一设置请求头、超时时间拦截响应统一处理业务错误码和Token过期import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理错误 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { Message.error(error.message || 网络异常) } return Promise.reject(error) } ) export default service这个封装让业务代码里不需要重复处理Token和错误逻辑比如发帖接口里只需要写import request from /utils/request export function publishPost(data) { return request({ url: /post/publish, method: post, data }) }前端的路由守卫也很重要判断用户是否登录没登录就跳转到登录页。我设置了两种路由requiresAuth: true表示需要登录才能访问的页面用beforeEach统一判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })如果用户访问一个需要登录的页面守卫会带上redirect参数把他带到登录页面登录成功后可以自动回到原目标页这个小体验很值。4.3 前后端联调跨域、代理和接口对接前后端分离开发时最头疼的问题就是跨域。前端DevServer跑在localhost:8081后端接口跑在localhost:8080直接请求一定会被浏览器的同源策略拦截。解决方案有后端配CORS和前端配代理两种我推荐开发阶段用前端代理发布阶段用Nginx统一转发。Vue CLI的vue.config.js里这样配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端请求/api/post/list时DevServer会把它转发到后端/post/list完美避开跨域问题。后端就不需要额外加CORS配置了避免重复处理。如果要在后端配CORS我建议只加一个WebMvcConfigurer的过滤器不要用CrossOrigin注解一个一个加在Controller上那样很繁琐。正确的做法是全局配置允许的来源、方法和请求头注意不要用setAllowedOrigins(*)这样的通配符要明确指定http://localhost:8081或者Nginx的域名否则Cookies和认证信息就带不过去了。联调时还有一个小技巧把后端Swagger集成进来。Spring Boot整合Springfox或SpringDoc后进入/swagger-ui/index.html就能看到每个接口的请求参数和返回示例前端可以直接对照Swagger文档调接口比来回翻代码效率高很多。我用过SpringDoc它跟Spring Boot 2.x的兼容性更好生成的文档还支持在线调试。5. 常见问题与排查技巧实录5.1 跨域和CORS问题报错一片红怎么办跨域报错是我在联调阶段遇到最多的问题浏览器控制台会出现类似Access to XMLHttpRequest at http://localhost:8080/... from origin http://localhost:8081 has been blocked by CORS policy的红色错误。排查思路是先确认请求有没有真正到达后端。看后端日志如果什么都没有说明请求被前端代理拦截了如果后端打印出了请求但还是报错说明是响应头里缺少CORS配置。前端的vue.config.js代理配置只在开发环境生效发布时前端请求会被打包成静态文件放到Nginx上Nginx反向代理配置如下server { listen 80; server_name campus.example.edu.cn; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里还有一个前端路由的历史模式问题。Vue Router开启mode: history后URL里不再带#但刷新一个/post/detail/1路径时Nginx会去找这个路径下的物理文件找不到就返回404。解决办法就是上面配置里的try_files $uri $uri/ /index.html让所有路径都回退到index.html交给前端路由处理。如果不想配置Nginx直接改用hash模式也行。5.2 JWT隐性问题Token过期和账号被顶JWT方案有个默认的缺陷因为Token是无状态的签发之后服务端没法主动让它失效。用户改了密码旧Token在过期前依然有效管理员禁用了某个用户只要Token没过期他还能继续调接口。我的解法是把Token和用户状态做了一次绑定校验每次请求时从Redis里查一下这个用户的ID对应的Token是不是当前这个不一致就拒绝。Token过期是另一个容易引发困惑的点。前端的响应拦截器里我写了401跳转逻辑但有一个坑多个请求同时返回401就会触发多次跳转和多次弹窗。解决办法是加一个标志位或者把刷新Token的逻辑串起来让后面的请求等待刷新完成后再重放。下面这段是实际项目里的处理思路let isRefreshing false let pendingQueue [] service.interceptors.response.use( response { /* 正常逻辑省略 */ }, error { const { response } error if (response response.status 401) { const refreshToken localStorage.getItem(refreshToken) if (refreshToken !isRefreshing) { isRefreshing true return request({ url: /auth/refresh, method: post, data: { refreshToken } }) .then(res { localStorage.setItem(token, res.data.token) pendingQueue.forEach(cb cb()) pendingQueue [] return service(error.config) }) .finally(() { isRefreshing false }) } else { // 刷新失败或无refreshToken强制重新登录 router.push(/login) } } return Promise.reject(error) } )这段代码真正落地时还要处理很多边界情况但对于校园平台项目来说能实现静默续期就已经比大多数课程设计高一个档次了。5.3 图片上传文件存哪里、路径怎么配校园服务平台里的二手交易、失物招领、活动海报都离不开图片上传。我在项目里用的是本地存储方案后端接收MultipartFile文件按日期分目录存储返回可访问的URL。具体实现逻辑是先把文件保存到服务器指定目录下然后用Nginx把这个目录映射成静态资源路径。public String uploadFile(MultipartFile file) { // 原始文件名 String originalFilename file.getOriginalFilename(); // 生成唯一文件名避免中文乱码和重名 String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) suffix; // 按日期分目录存储 String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String filePath uploadDir / datePath / newFilename; File dest new File(filePath); // 目录不存在时自动创建 if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 返回可访问的URL return /uploads/ datePath / newFilename; }上传成功返回的URL是相对地址Nginx把/uploads映射到实际目录就能访问。前端上传组件用Element UI的el-upload设置action为后端接口地址headers里带上Authorization Header。这里有几个容易踩的坑。第一个file.transferTo传的目标文件不能放在getServletContext().getRealPath()返回的应用临时目录里因为服务器重启后临时目录会被清空。要单独配置一个持久化目录最好放在应用资源目录之外。第二个文件大小默认限制1MB超出直接报413错误。需要在Spring Boot配置文件里调大spring.servlet.multipart.max-file-size和max-request-size校园活动海报通常2MB到5MB比较合适。第三个上传目录的写权限要设置好不然上传时报Permission denied我当时排查了半天才发现是目录权限问题。5.4 数据库中文乱码和连接池问题中文乱码是校园项目中非常经典的问题涉及三个层面数据库表字符集、JDBC连接字符集、前端页面字符集。我在建库时固定使用utf8mb4而不是utf8因为utf8mb4支持完整的Unicode字符集带Emoji的内容也能存储。连接串里必须加上characterEncodingutf8前端页面在index.html里声明charsetutf-8。三层都对齐之后乱码问题基本就能解决。数据库连接池我用的是HikariCPSpring Boot 2.x默认就集成了它。默认配置在生产环境跑校园项目的访问量完全够用但我调整了连接超时参数避免高峰期连接池被耗尽spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000这些参数不是随便填的。最大连接设20是因为校园平台的高峰并发通常在几十到几百之间按一个请求占用一个连接计算20个连接配合MySQL默认连接数100完全够用设太大反而浪费数据库资源。连接超时设30秒是比较稳妥的既不会让用户等太久也不会误杀慢查询。6. 我踩过的坑和想分享的几句心里话做了这个项目之后我最大的体会是校园类系统看似是个小项目但麻雀虽小五脏俱全从用户认证到文件上传、从消息通知到权限控制一个真实业务系统该有的环节这里全有。把它从零做完一遍你对Spring Boot的理解、对Vue生命周期和各种情况的处理能力都会有非常明显的提升。最后分享一个我自己的开发习惯永远不要在没有任何认证保护的接口里直接信任前端传来的用户ID。所有需要识别当前用户的地方一律从Token里解析出来的身份信息去拿而不是前端传一个userId过来。这个习惯能避免很多越权漏洞。另外写这个项目时一定要学会看日志。后端SQL日志能告诉你数据库到底发生了什么前端控制台的Error能定位到页面哪里崩了。排查问题时先看日志再改代码能少走一大半弯路。这个项目的扩展方向也不少最自然的一步就是把Web端接口直接复用到微信小程序上因为前后端已经分离后端接口是纯API设计小程序只需要写一个独立的UI层就行。我目前也正在把移动端适配提上日程到时候如果再遇到有意思的坑可以再写一篇。
返回列表