资讯动态

SSM+Vue社团管理系统实战:验证码防刷与部署全流程解析

发布时间:2026/10/21 5:24:56 来源:尧图企业网站定制
做这类大学生社团管理系统的人真的太多了每年课程设计、毕业设计里都能见到。很多人交上来的东西功能列表看着挺全但一问细节就露馅尤其是登录页那个验证码——大多数人是直接从别的项目复制过来的问它怎么生成、怎么校验、为什么刷新之后还是显示旧图答不上来。我最近刚好把一个“SSM Vue 带验证码”的社团管理项目从零到上线完整跑了一遍包括数据库设计、后端接口、前端对接、验证码防刷、部署到服务器踩坑今天就把整条线捋出来讲清楚。这篇东西适合三类人看正在做类似课设或毕设的学生、刚接触前后端分离开发的新手、以及想系统复习一遍 SSM 技术栈的开发者。1. 项目概述一个常见的课设题目到底有多少坑1.1 核心需求解析先把这个系统到底要做什么拆清楚。标题上写着“大学生社团管理系统”那么围绕这个业务域最核心的功能无非是几块用户注册与登录、社团信息展示与检索、社团成员管理、活动发布与报名审核、公告通知以及一个超级管理员后台来做整体数据维护。实际开发中如果不先做需求梳理就直接建表写代码后面一定会乱。我拿到这个项目后的第一件事是列了一个功能清单按照角色视角去划分普通学生能做什么社团管理员能做什么超级管理员能做什么。学生端注册登录、浏览社团、申请加入社团、查看活动列表、报名活动、查看自己的报名状态、维护个人资料。社团管理员管理自己社团的基本信息、审核入社申请、发布活动、审核活动报名、发布公告。超级管理员管理全部社团数据、管理用户账号、查看系统统计数据、拥有所有社团的管理权限。这套需求模型很典型几乎涵盖了大部分中小型管理系统的通用模块。重点不在于功能多少而在于每个功能的边界怎么划、权限怎么控、数据流转怎么设计。1.2 技术选型背后的考虑为什么这类项目仍然用 SSM 而不是直接上 Spring Boot原因很现实很多高校的课程大纲还在用 SSM 作为教学主线Spring 的 IOC 和 AOP、SpringMVC 的请求映射、MyBatis 的 SQL 映射这三样东西拆开学是理解 Java Web 后端原理的最好路径。Spring Boot 固然上手快、配置少但它把太多细节封装掉了初学者根本不知道请求是怎么进来的、事务是怎么生效的。而前端选 Vue 也很好理解。传统的 JSP JSTL 那种服务端渲染方式页面交互僵硬维护起来也痛苦。Vue 负责做单页应用后端只提供 JSON 接口前后端职责分离联调方便这也是目前大多数公司在用的协作模式。所以这套项目虽然表面上是“老技术 新技术”的混搭但它非常贴近真实开发场景。验证码这个需求之所以被我单独拎出来讲是因为它在整个系统里非常典型——它不算核心业务但实现细节极多。你需要在后端画图、生成随机字符串、存 Session、设置过期时间、画干扰线前端还要处理缓存刷新、点击更换、校验失败后的联动重置。很多人在这个环节上直接抄现成代码导致做成的东西漏洞百出这恰恰是拉开完成度的地方。1.3 这套系统练的是什么说完技术选型再说说这套项目真正的训练价值。它不像那种“图书管理系统”一样只做几张 CRUD 页面也不像高并发秒杀项目那样上来就是分布式、消息队列。它的难度恰好在中间你要处理好三种角色的权限关系要设计合理的数据表结构要把验证码这种独立模块做得严谨还要让 Vue 前端和 SSM 后端在跨域、会话、Token 这些环节上顺利协作。做完这个项目你学的不是某个框架的某个 API而是一整套从需求到部署的工程思维。后面哪怕转到 Spring Boot转到微服务这套思维方式都能直接平移。2. 业务模型与数据库设计2.1 角色与权限体系在动任何代码之前先把权限模型定清楚因为权限直接决定接口能不能被随便调用、数据能不能被越权访问。我采用的是最简单的 RBAC 模型延伸版——不引入复杂的菜单权限表和角色权限关联表而是给用户表加一个 role 字段取值分别是 1超级管理员、2社团管理员、3普通学生。社团管理员再通过社团成员表关联到具体的某个社团也就是说“这个人是管理员”和“他管的是哪个社团”是两回事。必须靠 member 表里的 club_id 和 role 共同判断。后端拦截器在进入 Controller 之前先通过请求头里携带的 Token 解析出用户 ID然后查用户表拿到角色。如果是需要社团管理员权限的接口再额外校验该用户是否属于目标社团。这种方案对当前系统的复杂度来说完全够用性能开销也可以忽略不计。2.2 核心数据表结构数据库我用的 MySQL字符集统一 utf8mb4排序规则 utf8mb4_general_ci。表结构经过两次调整才最终定型一开始设计得太多冗余字段后面又删掉了不少下面是最终版的核心表结构。表名核心字段说明userid, username, password, real_name, student_no, role, avatar, create_time用户表username 唯一索引clubid, club_name, category, description, president_id, status, create_time社团表president_id 指向 user 表memberid, user_id, club_id, member_role, join_time, status成员关系表user_id 和 club_id 加唯一索引activityid, club_id, title, content, location, start_time, end_time, max_people, status活动表status 表示未开始/报名中/已结束enrollid, activity_id, user_id, apply_time, audit_status报名表activity_id 和 user_id 加唯一索引noticeid, club_id, title, content, publish_time公告表这六张表基本覆盖了全部业务。有人会问社团和用户的关联为什么不用一张表直接记录原因是 member 表还要承担“申请入社”这个临时状态的存储。学生申请加入社团时先插入一条 status 为待审核的记录等管理员审核通过后把 status 改掉。这样申请、加入、踢出、退社这些操作都只需要操作同一张表逻辑非常集中。活动报名表同理。一个人对同一个活动只能报名一次这个约束在数据库层面就用唯一索引堵死而不是只靠 Java 代码判断因为并发情况下代码判断容易出漏洞。2.3 接口设计与统一返回结构前端要对接的接口我全部走 REST 风格设计。列表类的用 GET新增用 POST修改用 PUT 或 POST删除用 DELETE。实际开发中受前端框架限制虚拟机上不支持 DELETE 方法所以删除接口最后也改成了 POST 路径传参这种妥协在真实项目里很常见。所有接口统一返回一个 Result 对象结构如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }分页接口统一接收 pageNum 和 pageSize 两个参数返回一个 PageResult 对象里面包含总记录数、总页数、当前页数据列表。这样前端的分页组件可以直接绑定不用每次都对数据做二次加工。3. 验证码功能的完整实现3.1 后端动态生成验证码图片验证码是登录安全的第一道门槛核心作用是防止自动化脚本暴力破解密码。实现方案上我用的不是引入第三方验证码组件而是纯 Java 代码基于 BufferedImage 手绘生成这样整个流程完全可控也方便自定义字体、颜色和干扰线。生成验证码的步骤拆开看就是四步创建画布、写随机字符、画干扰线、加噪点。字符我选了数字加字母的组合去掉容易混淆的 0 和 O、1 和 l这样用户体验会好很多。public class CaptchaUtil { private static final int WIDTH 120; private static final int HEIGHT 40; private static final int CODE_COUNT 4; private static final char[] CHARS { A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z, 2, 3, 4, 5, 6, 7, 8, 9 }; public static String generateCaptcha(HttpSession session) { BufferedImage image new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, WIDTH, HEIGHT); StringBuilder code new StringBuilder(); Random random new Random(); // 写入字符 for (int i 0; i CODE_COUNT; i) { char c CHARS[random.nextInt(CHARS.length)]; code.append(c); g.setFont(new Font(Arial, Font.BOLD, 24 random.nextInt(6))); g.setColor(new Color(random.nextInt(130) 60, random.nextInt(130) 60, random.nextInt(130) 60)); // 对每个字符做轻微旋转增加识别难度 double angle random.nextInt(30) - 15; g.rotate(Math.toRadians(angle), 15 i * 28, 20); g.drawString(String.valueOf(c), 10 i * 28, 30); g.rotate(-Math.toRadians(angle), 15 i * 28, 20); } // 画干扰线 for (int i 0; i 6; i) { g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200))); g.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT), random.nextInt(WIDTH), random.nextInt(HEIGHT)); } // 加噪点 for (int i 0; i 80; i) { g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200))); g.drawOval(random.nextInt(WIDTH), random.nextInt(HEIGHT), 1, 1); } g.dispose(); // 将验证码文本存入 Session同时记录生成时间 session.setAttribute(captchaCode, code.toString()); session.setAttribute(captchaTime, System.currentTimeMillis()); // 写入输出流 try { ImageIO.write(image, JPEG, response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } return code.toString(); } }上面的代码把验证码文本放在了返回值的 code 字符串里但这只为了本地测试用正式环境返回给前端的是图片流。另外代码中的字体用了 Arial它在大多数操作系统上都存在如果你要画中文验证码就得换成系统中文字体而且中文识别难度高、显示效果不稳定做项目时尽量避开中文验证码。3.2 存储与校验逻辑验证码存哪里是个关键选择。单机部署或者小规模场景直接存 Session 是最简单的毕竟 HttpSession 本身就是服务端的状态存储。但我加了两个细节一是给验证码设置过期时间5 分钟未使用就失效二是校验成功之后立即清除 Session 中的验证码防止同一个验证码被反复使用。校验时机上我建议把验证码校验放在用户名密码校验之前。如果验证码都不对根本不需要去查数据库比对密码这既减轻了数据库压力也避免了密码爆破工具在验证码环节就被拦下来。PostMapping(/login) public Result? login(RequestBody LoginDTO loginDTO, HttpSession session) { // 先校验验证码 String captchaCode (String) session.getAttribute(captchaCode); if (captchaCode null || !captchaCode.equalsIgnoreCase(loginDTO.getCaptcha())) { return Result.error(验证码错误或已过期); } // 清除验证码防止重复使用 session.removeAttribute(captchaCode); session.removeAttribute(captchaTime); // 再校验用户名密码 User user userService.login(loginDTO.getUsername(), loginDTO.getPassword()); if (user null) { return Result.error(用户名或密码错误); } // 登录成功生成 Token 返回 return Result.success(tokenService.generateToken(user)); }校验的时候我用的是 equalsIgnoreCase这样用户在输入时不区分大小写友好度高很多。很多人在验证码这里踩了坑把验证码用 Base64 塞进 JSON 返回给前端然后再由前端画到 canvas 上。这样绕了一圈既增加了接口数据量又容易把 Session 和前端状态搞混完全没有必要。3.3 前端接入与自动刷新前端部分用 Vue 实现验证码图片本质上就是一张普通图片img 标签的 src 指向后端验证码接口即可。这里最容易犯的错是浏览器缓存问题——同一个 src 地址第二次加载时浏览器可能直接返回缓存的旧图片这就导致验证码刷新无效。解决办法是在图片地址后面拼一个随机参数。template div classcaptcha-wrapper el-form-item propcaptcha el-input v-modelloginForm.captcha placeholder请输入验证码 maxlength4 / img :srccaptchaUrl classcaptcha-img alt验证码 title点击刷新 clickrefreshCaptcha / /el-form-item /div /template script export default { data() { return { loginForm: { username: , password: , captcha: }, captchaUrl: }; }, created() { this.refreshCaptcha(); }, methods: { refreshCaptcha() { // 加时间戳参数绕过浏览器缓存 this.captchaUrl /api/captcha?t new Date().getTime(); } } }; /script同时我加了一个行为细节在验证码输入框失焦并且内容为空的时候不触发任何校验但一旦用户提交了登录请求并且后端返回验证码错误前端会自动调用一次 refreshCaptcha把验证码换成新的。这个交互虽然小但对用户体验提升很明显用户不需要手动去点图片刷新。3.4 验证码的踩坑清单验证码模块是我在这个项目里踩坑最多的地方集中说几个最典型的。第一图片请求跨域导致验证码拿不到 Session。如果你的前端部署在 8080 端口后端部署在 8181 端口那么前端请求后端接口时如果不做跨域配置并且没有在 axios 和 SpringMVC 里正确设置 Cookie 携带策略后端的 Session 每次都是新建的验证码永远校验失败。这里关键在于 axios 要配置 withCredentials: true后端跨域配置要设置 allowCredentials(true)。第二验证码图片不显示但接口不报错。很可能是因为图片写入流之后没有 flush或者 Controller 方法上加了 ResponseBody 导致返回被转成了 JSON而不是作为图片流输出。解决方法是让接口返回 void直接使用 HttpServletResponse 写输出流。第三验证码刷新后还是旧图。这个问题我刚说过就是浏览器缓存。多个项目经验告诉你时间戳参数一定不能忘。第四Session 过期导致验证码失效提示不明确。用户在页面停留太久再登录会提示验证码错误但实际上是 Session 超时。建议在捕获 Exception 时对验证码为 null 的情况返回“验证码已过期请刷新重试”而不是笼统地返回“验证码错误”这样用户更容易理解。4. 业务模块的实操细节4.1 社团列表与条件检索社团列表是整个系统访问量最大的接口学生登录后第一眼看到的就是它。这个接口我做了分页和关键词搜索支持按社团名称模糊查询、按分类筛选。MyBatis 里的动态 SQL 是这类查询最舒服的写法不需要手动拼接字符串也不容易出 SQL 注入问题。select idselectClubPage resultTypemap SELECT c.id, c.club_name, c.category, c.description, u.real_name AS president_name, (SELECT COUNT(*) FROM member m WHERE m.club_id c.id AND m.status 1) AS member_count FROM club c LEFT JOIN user u ON c.president_id u.id where if testclubName ! null and clubName ! AND c.club_name LIKE CONCAT(%, #{clubName}, %) /if if testcategory ! null and category ! AND c.category #{category} /if /where ORDER BY c.create_time DESC LIMIT #{offset}, #{pageSize} /select这里有个值得注意的点member_count 这个字段我是用子查询求出来的而不是在 club 表里维护一个冗余字段。一开始我图省事在 club 表加了 member_count 字段每次有人加入或退出都要去更新结果经常出现数据不一致。后来改成子查询虽然多一次扫描但在百万级以下数据量下性能完全没问题而且永远不会有脏数据。4.2 活动报名与审核流程活动报名的核心难点在于并发控制。学生报名活动时先要判断活动是否还存在名额再插入报名记录。如果两个学生同时提交报名而活动只剩最后一个名额代码层面的判断可能会出现超卖。要从根上解决问题不需要引入分布式锁。对于单体应用直接把数据库约束做好就够了。enroll 表上 activity_id 和 user_id 的唯一索引保证一个用户不可能重复报名然后活动名额的判断放在事务里用悲观锁查询活动记录。Transactional public Result? enroll(Long activityId, Long userId) { // 悲观锁锁定活动记录防止并发超卖 Activity activity activityMapper.selectByIdForUpdate(activityId); if (activity null) { return Result.error(活动不存在); } if (activity.getStatus() ! 1) { return Result.error(活动不在报名时间内); } int enrolledCount enrollMapper.countByActivityId(activityId); if (enrolledCount activity.getMaxPeople()) { return Result.error(活动名额已满); } enrollMapper.insert(activityId, userId); return Result.success(报名成功); }selectByIdForUpdate 对应的 SQL 是 SELECT ... FOR UPDATE它会把这条记录锁住直到当前事务提交。这样当多个请求同时进来时第二个请求必须等第一个请求提交后才能读取活动记录天然避免了超卖。代价是并发性能有影响但社团活动系统的并发量极低这种牺牲是值得的。审核环节的逻辑也不复杂。管理员查看报名列表对每条报名记录执行通过或拒绝操作。审核通过时还要顺带把该用户插入到 member 表成为社团成员。这里要注意把两个操作放到同一个事务里否则会出现“报名通过了但不是社团成员”的数据不一致。我用运行时异常触发事务回滚确保两步操作要么全部成功要么全部失败。4.3 Vue 端的路由守卫与 axios 拦截器Vue 前端有两个地方必须做权限控制一个在请求层一个在视图层。请求层的拦截器管 Token。登录成功之后后端会返回一个 Token前端存储在 localStorage 里之后所有请求都在 header 中携带。如果请求返回 401 状态码说明 Token 失效这时自动清除本地 Token 并跳转到登录页。import axios from axios; const service axios.create({ baseURL: /api, timeout: 10000, withCredentials: true }); 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 ! 200) { this.$message.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } this.$message.error(网络异常请稍后重试); return Promise.reject(error); } );视图层的路由守卫管页面跳转。未登录用户只能访问登录页普通学生不能访问管理后台社团管理员不能访问超级管理员专属页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { const role parseInt(localStorage.getItem(role)); if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); } else { next(); } } });这套双重控制非常必要。路由守卫只是用户体验层面的限制真正的安全边界永远在后端。如果一个用户直接通过 curl 伪造请求去调用后台接口前端路由是不会拦截的所以后端每个需要权限的接口都必须做校验。4.4 前后端权限控制的协同后端权限的核心实现是基于 HandlerInterceptor 的拦截器。我在 SpringMVC 配置文件里注册了自定义拦截器并配置了哪些路径需要拦截哪些路径放行。比如登录接口、验证码接口、注册接口都不拦截而管理端接口全部拦截。拦截器里先解析 Token 获取用户信息再根据请求路径判断所需的权限等级。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } User user tokenService.parseToken(token); if (user null) { response.setStatus(401); return false; } // 将用户信息存入 request供 Controller 使用 request.setAttribute(currentUser, user); // 检查路径对应的权限 if (request.getRequestURI().contains(/admin/) user.getRole() ! 1) { response.setStatus(403); return false; } return true; }前端菜单也是根据角色动态渲染的。登录后后端返回用户信息包含 role前端根据 role 判断要不要渲染“系统管理”菜单项。这样不同角色登录后看到的界面是不同的超级管理员可以看到全部功能社团管理员只能看到自己社团相关的内容普通学生看到的就是社团浏览和个人中心。5. 部署上线与高频问题排查5.1 从本地到云端的构建部署项目开发完成后部署又是一个新的小关卡。SSM 项目通常打包成 war 包放在 Tomcat 的 webapps 目录下运行而 Vue 项目打包后生成 dist 目录。这里有两个方案。方案一前端 dist 目录里的文件直接拷贝到 war 包或者 Tomcat 的 webapp 目录下让 SpringMVC 同时负责静态资源和接口这样前后端在同一个端口下运行。好处是没有跨域问题部署简单。坏处是前后端耦合后续如果想把前端拆出来单独部署就比较麻烦。方案二前端独立部署在 Nginx后端部署在 Tomcat前端通过 Nginx 的反向代理把 /api 路径的请求转给后端 Tomcat。这种方式前后端完全分离也更贴近生产环境。我实际部署时用的是方案二。Nginx 配置中关键的一段是这样server { listen 80; server_name your-domain.com; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8181; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个关键点proxy_pass 后面的地址不能写 http://127.0.0.1:8181/api/否则请求路径会多重复一层 api。我一开始在这里折腾了很久改来改去总是 404最后发现就是 proxy_pass 转发时路径拼接的问题。部署前还有几个准备工作要做数据库脚本要在服务器上执行一遍MySQL 的账号密码要用环境变量或者配置文件管理而不是写死在代码里Tomcat 的端口和 UTF-8 编码要确认无误。尤其 Tomcat 的 server.xml 里一定要配置 URIEncodingUTF-8否则中文参数会出现乱码。5.2 高频问题速查表我把实战中遇到的高频问题整理成了一张表每个问题都对应了具体的现象和解决方案排查时照着顺序看就行。现象原因解决方案前端请求接口 404前后端路径没对上或 Nginx 代理配置错误检查前端 baseURL 是否带 /api、检查 proxy_pass 路径拼接前端请求接口跨域报错后端未配置跨域或前端未携带 Cookie后端配置 CORSaxios 开启 withCredentials登录时验证码一直错误Session 跨域丢失或验证码过期检查 allowCredentials检查 Session 存储与清除逻辑日期时间显示多 8 小时后端返回 UTC 时间前端未做本地转换后端返回时间戳或使用北京时间格式字符串MyBatis 查询结果字段为 null数据库字段下划线和 Java 属性驼峰未映射MyBatis 开启 mapUnderscoreToCamelCase 配置上传文件提示文件过大SpringMVC 默认上传限制 1MB在配置文件调整 maxFileSize 和 maxRequestSize列表接口响应慢查询没有走索引或 MyBatis 循环查询了 N1 次检查 explain 执行计划使用联表查询替代循环服务器重启后图片验证码失效图片存在本地磁盘导致路径丢失使用对象存储或限制部署环境一致5.3 几个值得细说的部署坑第一个坑是 MySQL 8 的时区问题。新版 MySQL 连接驱动对时区要求很严格如果 JDBC 连接字符串里不加上 serverTimezoneAsia/Shanghai连接时会直接报错。很多人在本地开发没事一部署到服务器就崩溃多半是这个原因。第二个坑是 MyBatis 的驼峰映射。我建表时字段名用的是下划线风格比如 create_time、user_id而 Java 实体类是 createTime、userId。如果 MyBatis 没有开启映射查出来的对象这些字段就全是 null。解决方式是在 mybatis-config.xml 里加一行配置settings setting namemapUnderscoreToCamelCase valuetrue/ /settings第三个坑是 LocalDateTime 的 JSON 序列化。SSM 项目里如果引入了 Jackson 来处理 JSON默认序列化 LocalDateTime 会输出一长串数字前端完全没法用。需要在 ObjectMapper 里注册 JavaTimeModule并指定日期格式ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss));这个坑不报错但表现出来的现象很隐蔽前端时间字段显示成一串数字用户看着莫名其妙。第四个坑是文件上传问题。社团管理中管理员会传社团 logo 图片如果直接存在本地磁盘跨机器部署时就丢失了。我做了一个妥协方案把图片转成 Base64 存到数据库的一个字段里。这个方案在图片很小时完全可用省去了搭建文件服务的成本。如果图片量大了再迁移到对象存储也不复杂。6. 复盘与改进方向整个项目做完我最大的感受是这个系统的复杂度刚刚好做一遍能学到的东西比想象中多得多。但如果让我重新再写一遍我会在几个地方做调整。第一技术栈上我会直接切换到 Spring Boot 3 Vue 3 TypeScript因为 Spring Boot 内置了 Tomcat省掉配置 SSM 的大段 XML开发效率和可维护性都更高。不过对于没接触过 SSM 的同学我还是建议先手写一次传统 SSM不然直接上用 Spring Boot 会像背答案一样不知道它帮你解决了什么。第二验证码的存储我会从 Session 改成 Redis。Session 有个天然的局限如果以后系统扩展到多台服务器Session 是存在单台服务器内存里的负载均衡转发到另一台机器时验证码就校验不过去了。用 Redis 存验证码key 用随机 UUIDvalue 存验证码文本和过期时间Redis 又是独立部署天然跨机器共享。第三权限模型可以做得更通用一些。目前的角色字段是写死的但真实系统里角色和权限经常需要调整。我会做成 user_role、role、role_menu 三张表把权限管理做成一个可配置的功能而不是在代码里硬编码。第四日志系统一定要从一开始就加。这个项目前期我为了省事没怎么打日志后期排查线上问题非常痛苦只能靠猜。后来在关键业务节点加了操作日志比如谁在什么时间创建了社团、谁审核了哪个报名、谁修改了密码这样出问题时有据可查。第五前端组件封装可以更细。活动报名状态、社团成员角色这些字段我一开始直接写了多重 if else 判断状态值和文字的对应关系代码又长又乱。后来封装了枚举映射再加一个通用字典组件代码量减少三分之一可读性也好很多。如果你正在做类似的社团管理系统我的建议是别急着写代码先把数据表设计好、把权限模型画出来、把验证码流程想清楚。这三个环节只要有一环没理顺后面大概率要返工。特别是验证码别看它只是一个登录页的小功能从后端生成、Session 存储、前端刷新、跨域处理到防刷设计一整套走下来你对 Web 应用请求生命周期的理解会深很多。这套系统做完已经有一段时间了我现在回头在看觉得当时踩过的每一个坑都值回了票价。项目本身是课设也好是练手也罢重要的是通过它把前后端之间的请求链路彻底搞明白了。后面再做别的项目不管技术栈怎么变这一套从需求拆解到数据建模、从接口设计到安全校验、从本地联调到服务器部署的完整思路都是完全通用的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑