数据库课程设计:用户登录系统从表设计到前后端联调完整指南
简介一份适用于数据库课程设计的高校实践源码项目围绕用户登录功能整合前端界面、后端逻辑与数据库存储适合计算机相关专业学生用于期末设计或项目练手。压缩包共49个文件以23个Java文件和8个JSP页面构成后端业务与动态页面前端含4个HTML、3个CSS和2个JS实现登录页结构与交互另有5个XML及properties配置文件支撑项目运行Maven构建文件和项目说明文档则便于导入与理解。整个源码包仅64KB轻量且便于研读。这份源码在CSDN已有3956人浏览学习属于关注度较高的课程设计案例。项目完整覆盖用户登录链路包括表单非空/复杂度校验、密码哈希存储、数据库用户表查询与会话状态维护等通过研读源码可清晰看到前端如何发起请求、后端如何调用数据库并返回结果并可直接在IDE中运行调试进一步扩展注册、权限管理等模块。对于希望掌握前后端整合与数据库应用的初学者这是一个很好的参考。1. 数据库课程设计里的用户登录为什么最简单的功能最容易翻车答辩现场最常见的翻车场景是这样的老师走到你机器前你打开浏览器输入账号密码点击登录然后页面一直转圈——前端报跨域后端接口 404数据库连不上三个环节各坏一个几分钟的演示全砸了。用户登录在数据库课程设计里向来是标配功能但它从来不是写个接口对一下用户名密码这么简单真正难的是让前端页面、后端接口、数据库表结构三件事在别人的机器上也能一次跑通。这篇文章从表结构设计讲到前后端联调把登录、注册、会话保持、增删改查这条链路里最容易被扣分、最容易演示失败的细节全部摊开。适合正在做课程设计、需要交付一份能跑能答辩源码的同学也想帮刚接触前后端分离项目的开发者少踩几个公开的坑。2. 数据库设计先行把用户表、角色表和登录日志画在写代码之前2.1 三张基础表用户、角色、操作日志怎么拆很多课程设计的第一版数据库只有一张 user 表字段就是 id、username、password然后往死里堆需求。等做到一系列操作的时候发现每个功能都要判断当前用户有没有权限登录状态不知道存在哪操作记录全都丢了。所以我的习惯是开工先拆三张表分别是用户表、角色表或者权限表、操作日志表。用户表存静态信息比如用户名、密码哈希、盐值、昵称、邮箱、注册时间角色表存身份信息比如学生、管理员、普通用户日志表存动态记录比如登录时间、IP、操作类型、操作对象。拆分逻辑不复杂用户和角色是多对一关系用户表里存 role_id 外键操作日志表通过 user_id 关联用户。三元组设计的好处在于后续做管理员才能删除数据普通用户只能查看自己的记录这类权限控制时不需要改表结构只需在查询条件里加角色判断。课程设计答辩经常被问到你的系统怎么控制权限能答出通过角色表关联登录时把角色写进 Token后端接口用拦截器校验就已经超过大多数只会用 if 判断的作业了。再补充一个容易被忽略的字段status。用户状态字段建议加上可以表示正常、禁用、未激活。演示的时候可以当场把某个账号禁用再尝试登录展示系统对异常账号的拦截能力这是一个成本极低但观感很好的加分项。2.2 建库建表 SQL从 MySQL 到 SQLite 的落地差异课程设计最常见的数据库选型是 MySQL因为环境好装、资料多、答辩老师也认。但如果你在实验室机器上演示MySQL 未必装了我一般建议准备两个版本主用 MySQL备用 SQLite。SQLite 的建表语句和 MySQL 大体相近只需要注意自增字段写法不同再改一下 JDBC 驱动和连接 URL 就能切换。下面是课程设计里最常用的一套 MySQL 建表脚本。-- 创建数据库字符集务必用 utf8mb4否则中文用户名可能乱码 CREATE DATABASE IF NOT EXISTS course_design DEFAULT CHARACTER SET utf8mb4; USE course_design; -- 用户表核心字段是 username、password_hash、salt、role_id、status CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名登录凭证, password_hash VARCHAR(255) NOT NULL COMMENT 加盐哈希后的密码绝不允许存明文, salt VARCHAR(32) NOT NULL COMMENT 每个用户独立的盐值, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称可空, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱用于找回密码等场景, role_id INT NOT NULL DEFAULT 2 COMMENT 角色ID1管理员2普通用户, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意几个细节。password_hash 给定 VARCHAR(255) 而不是最常见的 VARCHAR(20)原因是 BCrypt 这类哈希算法产生的字符串长度约 60 位MD5 加盐后也可能超过 32 位字段太短会直接插入失败。salt 字段的长度取决于你生成盐的方式UUID 去掉横线是 32 位如果用了更长格式可以放宽。username 上建唯一索引防止注册时并发插入重复账号。接着是角色表和日志表。角色表非常轻量管理员和普通用户两条数据就够了。日志表主要用于登录记录的追踪也承载最近登录时间这类展示数据。-- 角色表一般只需要管理员和普通用户两个角色 CREATE TABLE t_role ( id INT NOT NULL AUTO_INCREMENT, role_name VARCHAR(20) NOT NULL COMMENT 角色名称, role_desc VARCHAR(100) DEFAULT NULL COMMENT 角色说明, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 登录日志表记录每次登录行为的来源IP、时间和结果 CREATE TABLE t_login_log ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 关联t_user.id, login_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 登录时间, login_ip VARCHAR(50) DEFAULT NULL COMMENT 登录来源IP, login_result TINYINT NOT NULL DEFAULT 1 COMMENT 登录结果1成功0失败, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT登录日志表;日志表里不要存冗余的用户名字段直接存 user_id查询时 JOIN 用户表拿用户名。如果还需要记录用户做了哪些操作可以再加一张 t_oper_log字段设计思路完全相同无非是加上操作类型和操作描述。这里要提一个比较常见的数据库设计教训不要为了图省事把所有日志都塞进一张表登录日志和操作日志的查询频率不同、清理策略也不同拆开写更清晰。2.3 密码存储别再往数据库里写明文了课程设计代码里最常见的安全翻车就是密码明文存储。很多现成源码为了演示方便直接在数据库里存 123456答辩时老师问一句如果数据库泄露怎么办就答不上来。正确的做法是加盐哈希主流方案是 BCrypt。BCrypt 会自动生成随机盐并拼接进结果串校验时直接拿密文和明文比对即可无需你手动管理 salt 字段。如果你用的是 Spring Boot 全家桶Spring Security 自带 BCryptPasswordEncoder如果手写 JDBC可以用 jBCrypt 库。// 注册时生成密码哈希salt自动包含在hash结果中 import org.mindrot.jbcrypt.BCrypt; public class PasswordUtil { // 生成密文每次调用salt都不同所以同一个密码两次生成的hash也不同 public static String hashPassword(String plainPassword) { return BCrypt.hashpw(plainPassword, BCrypt.gensalt(10)); } // 校验密码用明文和数据库中取出的密文做比对 public static boolean checkPassword(String plainPassword, String hashedPassword) { return BCrypt.checkpw(plainPassword, hashedPassword); } }gensalt(10) 是计算强度参数取值范围 4 到 3110 在课程设计规模下足够安全且响应时间可接受。如果调成 14 以上单次校验可能耗时几百毫秒演示时登录按钮点了没反应容易被误判为卡死。另外提醒一句不要把 BCrypt.result 截断后存入数据库前面已经把 password_hash 设计成 VARCHAR(255) 的原因就在这里。注册流程的完整逻辑是接收用户名和密码 → 校验用户名是否重复 → hashPassword 生成密文 → 插入用户表。登录流程是按用户名查用户 → 不存在则提示用户不存在 → checkPassword 比对 → 记录登录日志 → 签发 Token。2.4 数据库连接池课程设计也要认真配置直接在 DAO 里用 DriverManager.getConnection 是初学写法课程设计里如果要体现工程素养建议用连接池。常见做法是 HikariCPSpring Boot 默认或者 Druid国内用得很多有监控页面。连接池的核心参数是 maximumPoolSize 和 connectionTimeout课程设计系统并发量很低最大连接数给 10 就够了但连接池的意义是演示不卡死。最典型的症状是连续登录登出几次之后下一次点击登录就长时间无响应原因就是连接没释放连接池耗尽。如果用了连接池还翻车优先检查 DAO 层有没有在 finally 里关闭 Connection、Statement、ResultSet。3. 后端接口与会话保持登录不只是查一次数据库3.1 后端选型Spring Boot 和 Node.js Express 怎么选课程设计后端技术栈的主流选择是 Spring Boot 和 Node.js Express各有各的利弊。Spring Boot 适合 Java 课程配套使用结构严谨、资料最多但写起来琐碎一个登录接口涉及 Controller、Service、Mapper 三个类。Express 适合快速出成果JavaScript 全栈可以前后端共用一种语言代码量少但项目的工程感不如 Java 强。我做课程设计指导时一般建议如果学校明确要求 Java 或者你以后想走 Java 方向选 Spring Boot如果只要求实现功能Node.js Express 或 Python Flask 更快。下面的接口示例用 Express 写原因是代码更紧凑方便理解登录接口的核心流程。Spring Boot 的思路完全一致只是换成注解和 Mapper。3.2 登录接口的最小实现参数校验、密码校验、Token 签发先理清一个概念登录接口不是查询数据库判断密码正确就结束了。课程设计要求通常包括注册、登录、退出、修改密码、用户信息查询这几个接口其中登录接口是最复杂的因为它要承担两个职责验证身份、发放凭证。凭证用来让后续的增删改查接口识别你是谁。下面是一段 Express 登录接口的核心实现。// routes/auth.js —— 用户认证相关路由 const express require(express); const router express.Router(); const db require(../db); // 数据库连接池模块 const bcrypt require(bcrypt); const jwt require(jsonwebtoken); const { v4: uuidv4 } require(uuid); // POST /api/auth/login 登录接口 router.post(/login, async (req, res) { const { username, password } req.body; // 第1步基础参数校验避免空值打到数据库 if (!username || !password) { return res.status(400).json({ code: 400, message: 用户名和密码不能为空 }); } try { // 第2步按用户名查用户信息SQL中的字段要和表结构对齐 const [rows] await db.query( SELECT id, username, password_hash, nickname, role_id, status FROM t_user WHERE username ?, [username] ); // 第3步区分“用户不存在”和“密码错误”避免泄露用户是否注册 if (rows.length 0) { return res.status(401).json({ code: 401, message: 用户名或密码错误 }); } const user rows[0]; // 第4步检查账号状态禁用账号虽然密码正确也不允许登录 if (user.status 0) { return res.status(403).json({ code: 403, message: 账号已被禁用请联系管理员 }); } // 第5步用bcrypt比对明文密码和数据库中的哈希值 const match await bcrypt.compare(password, user.password_hash); if (!match) { return res.status(401).json({ code: 401, message: 用户名或密码错误 }); } // 第6步签发JWTpayload中只放必要信息不放密码的哈希 const token jwt.sign( { uid: user.id, username: user.username, roleId: user.role_id, // jwtid字段生成唯一ID便于服务端做登出失效处理 jwtid: uuidv4() }, process.env.JWT_SECRET || course_design_secret, { expiresIn: 8h } ); // 第7步返回用户信息与token前端保存token并存入localStorage res.json({ code: 200, message: 登录成功, data: { token: token, userInfo: { id: user.id, username: user.username, nickname: user.nickname, roleId: user.role_id } } }); } catch (err) { // 统一异常返回避免把数据库错误详情直接抛给前端 console.error(登录接口异常:, err); res.status(500).json({ code: 500, message: 服务器内部错误 }); } }); module.exports router;这段代码把登录接口拆成了七个步骤每一步都有明确的目的。参数校验放在最早防止恶意请求携带空值打到数据库增加无谓开销。查询用户时只 select 需要的字段不要取 password_hash 之外的敏感信息。密码错误和用户不存在返回同样的提示信息避免攻击者通过接口确认用户名是否注册。JWT 的 payload 里只放 uid、username、roleId不放 email、手机号这类不必要的信息因为 JWT 只是 Base64 编码而非加密任何人解码都能看到 payload 内容。secret 不要写死到代码仓库里用环境变量注入。3.3 会话保持与 Token前端要的登录态到底是什么登录成功后前后端之间通过 Token 维持会话。JWT 的优势是无状态服务端不存 session每个请求带上 Token服务端验签即可。但课程设计里经常出现两个问题一是 Token 过期策略缺失前端永远不退出二是把 Token 放进了 localStorage 但请求时忘记带。前端每次请求需要将 Token 放入 HTTP 请求头。后端写一个全局中间件统一校验 Authorization 请求头这样增删改查接口不需要每个都写一遍判断逻辑。除了 JWT 还有一个更传统的方式Session Cookie把 sessionId 写在 Cookie 里浏览器自动携带后端用内存或 Redis 存储会话数据。这种方案对前端更省事但浏览器跨域时 Cookie 处理容易出问题课程设计的演示机经常前后端分离跑CORS 配置稍有不慎就掉链子。以下是 JWT 方案的中间件示例。// middleware/auth.js —— 登录校验中间件 const jwt require(jsonwebtoken); module.exports function authMiddleware(req, res, next) { // 从请求头中取token规范做法是 Bearer 空格 token const authHeader req.headers.authorization; if (!authHeader || !authHeader.startsWith(Bearer )) { return res.status(401).json({ code: 401, message: 未登录或登录已过期 }); } const token authHeader.split( )[1]; try { // 验证token并解析payload验证失败会抛出异常 const payload jwt.verify(token, process.env.JWT_SECRET || course_design_secret); req.user payload; // 后续接口可以从req.user中取用户ID和角色 next(); } catch (err) { // 后端不要直接返回err.message避免把JWT报错信息暴露给前端 return res.status(401).json({ code: 401, message: 登录已过期请重新登录 }); } };需要配套一个接口清单来把等一系列操作落实到位。最少的完整闭环是注册、登录、获取当前用户信息、退出登录、修改密码、用户管理增删改查用户。其中用户管理接口要区分角色比如只有 roleId 为 1 的管理员才能访问删除用户接口。在 Express 里可以用一个中间件组合先走 authMiddleware 确保已登录再检查 req.user.roleId。注意当 Token 过期时页面要跳回登录页这一步由前端路由守卫来处理下一章展开。3.4 后端跨域配置前后端分离项目一个经典的坑课程设计把前端和后端拆成两个端口运行是非常普遍的情况前端跑 5173后端跑 8080浏览器发请求时就会出现跨域问题。跨域的本质是浏览器同源策略拦截了不同端口之间的请求不是后端拒绝了你是浏览器的安全策略不让前端读取响应。解决跨域的常见做法是后端启用 CORS 中间件在 Express 中是使用 cors 包。允许的前端地址应该明确指定而不是直接使用通配符这样更规范。// app.js —— Express应用入口 const express require(express); const cors require(cors); const authRoutes require(./routes/auth); const userRoutes require(./routes/user); const authMiddleware require(./middleware/auth); const app express(); app.use(express.json()); // 解析JSON格式的请求体 // 跨域配置明确允许前端地址credentials配合前端withCredentials使用 app.use(cors({ origin: [http://localhost:5173, http://127.0.0.1:5173], credentials: true, methods: [GET, POST, PUT, DELETE, OPTIONS] })); // 公开接口注册和登录不需要token app.use(/api/auth, authRoutes); // 受保护接口登录之后才能访问的用户模块 app.use(/api/user, authMiddleware, userRoutes); app.listen(8080, () { console.log(后端服务已启动: http://localhost:8080); });这里注意 cors 的 origin 要写全并且如果前端用 localhost 访问后端 origin 里要有 localhost用 127.0.0.1 访问后端也要有 127.0.0.1。这是一个很容易在演示前忽略的细节前端明明打的是 127.0.0.1:5173后端只配了 localhost:5173跨域照样报错。解决了跨域之后前后端才能正常联调接下来的前端部分就是围绕这一套接口做页面。4. 前端页面与请求链路登录表单背后藏着的三件事4.1 登录页、注册页与 Axios 请求封装前端技术栈在课程设计中最常见的是 Vue 3 Element Plus因为组件全、不用手写样式、演示效果好。登录页面的表单本身不复杂两个输入框加一个按钮但提交时的逻辑要前后端匹配请求地址要正确、请求体字段名要和后端一致、密码不能明文出现在 URL 上。我经常看到源码里用 GET 传密码的写法这是比明文存库还要离谱的操作GET 请求会留在浏览器历史记录和服务器访问日志里。正确的登录提交方式用 POST字段名和请求头要跟后端接口文档对齐。// src/api/request.js —— axios统一封装 import axios from axios; import { ElMessage } from element-plus; import router from ../router; // 创建axios实例baseURL指向后端服务地址 const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 // 10秒超时避免接口异常时页面一直转圈 }); // 请求拦截器每次请求自动带上token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { // 后端中间件要求请求头格式为 Bearer token config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error)); // 响应拦截器统一处理后端返回的code和http状态码 request.interceptors.response.use( response { // 约定后端返回格式{ code: 200, message: , data: {} } const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; // 直接返回业务数据省去每处res.data.data }, error { // 处理HTTP层错误401跳转登录页其他弹错误提示 if (error.response error.response.status 401) { localStorage.removeItem(token); localStorage.removeItem(userInfo); router.push(/login); ElMessage.warning(登录已过期请重新登录); } else { ElMessage.error(error.response?.data?.message || 网络请求失败); } return Promise.reject(error); } ); export default request;封装 axios 的主要收益是避免在每一个页面组件里重复写 token 携带逻辑。请求拦截器统一从 localStorage 读取 token加到 Authorization 头后端中间件要求的 Bearer 前缀在拦截器里拼好。响应拦截器统一处理 401 状态码和业务错误码前端代码中只需要关心业务数据的处理。注意 baseURL 不要写成相对路径课程设计演示环境前后端分离部署、同一个机器的两个端口用绝对地址最省心。4.2 登录页组件表单校验与提交逻辑Vue 组件中登录页的逻辑要点有三个表单校验规则、请求提交成功后的跳转、失败后的提示。Element Plus 的 el-form 组件自带校验能力rules 里设置必填和格式校验。提交流程是先校验表单合法性再调用登录接口成功后把 token 和用户信息写入 localStorage最后跳转到首页。下面是一个精简版的登录组件。template el-form :modelloginForm :rulesrules refloginRef label-width80px el-form-item label用户名 propusername el-input v-modelloginForm.username placeholder请输入用户名 / /el-form-item el-form-item label密码 proppassword el-input v-modelloginForm.password typepassword show-password placeholder请输入密码 / /el-form-item el-form-item el-button typeprimary :loadingloading clickhandleLogin登 录/el-button /el-form-item /el-form /template script setup import { ref, reactive } from vue; import { useRouter } from vue-router; import { ElMessage } from element-plus; import { login } from ../api/auth; // 登录接口封装模块 const router useRouter(); const loginRef ref(); const loading ref(false); const loginForm reactive({ username: , password: }); // 表单校验规则前端校验只是体验优化后端接口仍要做参数校验 const rules { username: [ { required: true, message: 请输入用户名, trigger: blur }, { min: 3, max: 20, message: 用户名长度为3到20位, trigger: blur } ], password: [ { required: true, message: 请输入密码, trigger: blur }, { min: 6, max: 20, message: 密码长度为6到20位, trigger: blur } ] }; // 提交登录先校验再发请求 const handleLogin () { loginRef.value.validate(async (valid) { if (!valid) return; loading.value true; try { const data await login(loginForm); // 登录成功保存凭据和基本信息到localStorage localStorage.setItem(token, data.token); localStorage.setItem(userInfo, JSON.stringify(data.userInfo)); ElMessage.success(登录成功); router.push(/home); } finally { loading.value false; } }); }; /script登录接口函数 login 的实现本身只有一行request.post(/auth/login, params)。前端校验规则和后端的参数校验是两套逻辑各做各的千万不要以为前端校验了就万事大吉。密码框的 show-password 属性在演示时很有用老师可能会让你现场证明输入的密码不会被明文保存点一下小眼睛就能确认。4.3 路由守卫与登录态持久化刷新页面不掉线课程设计另一个高频翻车点是登录成功跳转首页一切正常按 F5 刷新页面直接跳回登录页。原因是前端路由没有做守卫或者登录态只保存在 Vuex 内存中没有持久化。正确做法有两条路由守卫拦截跳转 localStorage 持久化登录态。前面 axios 封装时已经把 token 存入 localStorage现在只需要在路由配置里检查。// src/router/index.js —— 路由守卫与登录态检查 import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /home, component: () import(../views/Home.vue) }, { path: /user, component: () import(../views/UserManage.vue) } ]; const router createRouter({ history: createWebHistory(), routes }); // 全局前置守卫每次路由跳转前检查token是否存在 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { // 未登录一律赶去登录页并把目标地址记录下来便于登录后跳回 next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } }); export default router;这段守卫的逻辑很直白目标页面不是登录页且没有 token就强制跳转登录页。redirect 参数是可选的优化适合用户手动输入了某个深层链接、未登录被跳走后希望登录完回到原页面的场景。路由守卫只负责跳转拦截真正的接口鉴权还是靠后端中间件前端守卫只是提升用户体验。4.4 从登录到增删改查把一系列操作串成一条完整链路登录只是入口课程设计要求的一系列操作通常落点在用户管理、数据录入、信息查询这些模块。这里给出一个标准的链路设计登录成功后跳转首页首页左侧菜单显示个人中心用户管理等功能入口进入用户管理页后调用后端接口展示用户列表新增和编辑弹窗表单删除按钮加二次确认。每个接口都要走 axios 封装自动携带 token。下面以用户管理页的分页查询为例展示带参请求和数据渲染的完整套路。// src/api/user.js —— 用户管理相关接口 import request from ./request; // 分页查询用户列表pageNum从1开始pageSize每页数量 export function getUserList(params) { return request({ url: /user/list, method: get, params: { pageNum: params.pageNum, pageSize: params.pageSize, keyword: params.keyword || } }); } // 新增用户密码由前端生成或者不设置重置密码单独走一个接口 export function addUser(data) { return request({ url: /user/add, method: post, data: { username: data.username, nickname: data.nickname, roleId: data.roleId } }); } // 删除用户根据ID删除注意这个接口需要管理员权限 export function deleteUser(userId) { return request({ url: /user/delete/${userId}, method: delete }); }这些接口和后端 Controller 一一对应前端调用方在页面组件里先调 getUserList 拿到列表数据再用 el-table 渲染。分页组件 el-pagination 的 current-change 事件触发重新查询。新增删除操作完成后刷新列表。到这里用户登录等一系列操作的功能链路才算闭合从注册登录到增删改查全部跑通的前提是前后端接口的字段命名完全对齐。5. 登录系统避坑手册5 个课程设计里最容易卡住演示的故障5.1 BLOB/Text 字段截断导致注册失败现象注册接口调用直接 500 错误后端日志提示 Data truncation: Data too long for column password_hash。原因建表时 password_hash 字段设成了 VARCHAR(50) 或更短而 BCrypt 生成的哈希长度为 60 位插入超长被拒绝。解决把字段扩到 VARCHAR(255)或者用 TEXT 类型。这个坑特别隐蔽因为本地测试时如果密码恰好短、生成的 hash 也短可能侥幸通过换个密码就报错。建议建表时就按 255 预留。5.2 登录接口提示 404 或 405现象前端点击登录Network 面板显示 POST 请求状态 404 Not Found 或 405 Method Not Allowed。原因前端请求的 URL 和后端定义的路由不一致例如前端请求 /api/auth/login后端映射在 /api/user/login还有一个常见原因是后端用 GET 接收了 POST 请求。解决打开浏览器开发者工具 Network 面板对比请求 URL和后端路由定义逐一核对路径和请求方法。后端接口的路径定义不要带前后缀混乱统一风格如 /api/auth/login、/api/user/list。5.3 跨域配置只写了 localhost 漏掉 127.0.0.1现象前端页面能打开点击登录后浏览器 Console 报 CORS errorAccess to XMLHttpRequest has been blocked by CORS policy。原因后端 cors 中间件 origin 配置写了 http://localhost:5173但前端实际访问地址是 http://127.0.0.1:5173。两者在浏览器眼里是不同源。解决origin 数组里同时写 localhost 和 127.0.0.1。如果想彻底省事可以在开发环境用 vite 的 proxy 转发前端请求写相对路径 /api由 vite 代理到后端 8080这样就没有跨域问题了。5.4 刷新页面登录态丢失现象登录成功进入首页刷新浏览器后跳回登录页或者首页数据加载失败。原因前端路由守卫检查的 token 存在内存变量里比如 Pinia刷新后内存清空localStorage 是空的。解决登录成功后把 token 写入 localStorage路由守卫从 localStorage 读取。注意为了防止 XSS 攻击真正生产环境倾向于把 token 放内存配合 HttpOnly Cookie但课程设计项目用 localStorage 足够答辩时能解释清楚区别就行。5.5 数据库连接没有被释放系统越用越卡现象刚开始系统运行正常连续操作几次登录、查询后页面响应越来越慢最终卡死不响应后端日志报连接池超时或 Too many connections。原因DAO 层获取了 Connection 后没有在 finally 里关闭或者使用了 Connection 池但连接耗尽。解决排查所有数据库访问代码保证 Connection、PreparedStatement、ResultSet 都在 finally 中关闭。Java 中可以用 try-with-resources 语法这样即使中间抛异常连接也能自动归还连接池。6. 把源码变成作品初始化脚本、测试账号与验收自检源码交付和现场演示是两回事前者要求代码能移植后者要求演示流畅。我吃过亏的地方是到答辩前才发现初始化数据全靠手动往数据库里 insert换一台机器跑起来连个测试账号都没有。所以项目里应该带一个 init.sql内容包含建表语句、初始角色数据、一个管理员账号和一个普通用户账号密码都用 BCrypt 哈希后的密文。这样拿到源码的人只要执行一条 SQL再启动后端和前端就能立刻登录演示。# 项目根目录下的演示环境启动脚本 # 1. 初始化数据库执行init.sql插入角色和管理员账号 mysql -u root -p sql/init.sql # 2. 启动后端Spring Boot示例 mvn spring-boot:run # 3. 启动前端Vue3示例 cd frontend npm install npm run dev验收自检我习惯按一条主线走注册新用户 → 用新账号登录 → 修改密码 → 退出登录 → 用旧密码登录预期失败 → 用新密码登录预期成功 → 用管理员账号登录 → 进入用户管理页 → 查看注册的新用户 → 删除一个用户 → 退出时用被删除的账号登录预期失败。这条链路走完核心功能就是闭环的。技术验证方面打开后端日志确认每次登录都写入了 t_login_log查询数据库确认密码字段存储的是长哈希字符串而非明文在浏览器开发者工具中确认每个受保护接口的请求头都携带了 Authorization Bearer Token。给课程设计作品收尾我最想提醒的是别把精力全花在加功能上把登录这条主链路打磨到在任意一台机器上复制过去就能跑通比多加一个花哨页面更能稳住答辩分数。常见做法是写一个 README把环境要求、数据库初始化、启动步骤、测试账号四件事写清楚这份文档在答辩时就是你的提词器。希望这篇文章里的表设计、接口代码和五个避坑点能帮到你照着这些细节把项目跑通演示时你就知道每一步操作背后为什么不会翻车了。本文还有配套的精品资源点击获取