前阵子帮一个学弟调后端,他的前后端分离精准扶贫管理系统跑不起来,SpringBoot 后端一启动就报数据库连接超时,Vue 前端登录接口直接 404。折腾了半个下午,问题出在 MySQL 8.0 的驱动配置和前端代理没配对。我当时就想,这类课设项目真的是年年都有、年年踩同样的坑。索性把整个项目从数据库设计、代码结构,到部署步骤和常见问题完整梳理一遍,给后面做 SpringBoot + Vue + MyBatis + MySQL 前后端分离管理系统的人留一份可直接对照的实战参考。
这个系统本身解决的是一个很实在的管理问题:困难群众信息、帮扶责任人、走访记录、帮扶计划、脱贫进度,这些数据如果停留在纸质档案和 Excel 里,查一个人要翻半天,统计一个片区更是灾难。系统化之后,管理员管权限,帮扶干部在线建档、填走访记录,领导看统计图表,整个流程就闭环了。对于想做课设、毕设,或者刚接触前后端分离开发模式想练手的人来说,这个项目是很典型的小而全案例,麻雀虽小,该有的知识点一个不缺。
1. 项目核心需求拆解:精准扶贫管理系统到底要管理什么
1.1 业务痛点:纸质档案与 Excel 管理模式的死穴
先聊业务,因为技术选型必须为业务服务。以往帮扶管理工作最大的问题是信息割裂。一个困难家庭的档案可能存在村委会的柜子里,帮扶干部的走访记录在自己的笔记本上,乡里的统计报表在另一个人的 Excel 里。等到需要汇总某个片区的脱贫情况时,要一层一层打电话要数据,回来还要手工核对身份证号、收入数字,错一处整个报表就废了。
另一个隐性痛点是过程无法追踪。谁去走访过、走访发现了什么问题、问题有没有跟进解决,这些信息如果不落到系统里,时间一长就变成一笔糊涂账。所以系统设计的第一目标不是“做一个漂亮的页面”,而是把“建档—帮扶—走访—退出”这条工作链路完整地数字化。
1.2 角色梳理:三类使用者的操作边界
设计权限之前先理清楚有哪些人用系统,这是管理系统的起点。这个系统里我划分了三类角色:
- 系统管理员:负责用户管理、角色权限分配、数据字典维护、菜单配置,相当于整个系统的运维者,原则上不参与具体的帮扶业务操作。
- 帮扶干部:负责困难家庭建档、填写走访记录、制定帮扶计划、上传证明材料,是系统的日常高频使用者。
- 部门领导:主要看各类统计报表、脱贫进度、区域分布数据,拥有大量的查询和统计权限,但不需要维护业务数据。
这个角色划分直接决定后端的接口权限设计、前端的菜单渲染和路由守卫拦截逻辑。你如果做课设,哪怕功能砍掉一半,这三个角色也得保住,因为评委基本都会问“不同角色登录后能看到什么、不能看到什么”。
1.3 核心业务流程闭环
把业务链路往前推一步,系统的主干流程是:管理员创建账号并分配角色 → 帮扶干部登录 → 为困难家庭建档(录入基本信息、致贫原因、家庭成员、收入情况)→ 制定帮扶计划 → 按计划开展走访并填写走访记录 → 定期更新收入数据 → 达到脱贫标准后提交脱贫评估申请 → 领导审批后完成“已脱贫”状态流转。
这个闭环后端实现时有几个关键点:建档时的行政区划联动选择(省市区三级)、走访记录与困难家庭表的关联写入、脱贫状态变更时的记录留痕。这些功能拆到技术上,就是多表关联查询、事务控制、分页列表、条件检索和简单的统计聚合。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型不是拍脑袋:为什么这套组合最适合课设与小型管理系统
2.1 SpringBoot:把打地基的时间省下来盖楼
在 SpringBoot 之前,搞一个 SSM 项目要先配置 web.xml、Spring 配置文件、MyBatis 配置文件、数据源、事务管理器,光是让项目成功启动就能劝退一半新手。SpringBoot 的核心价值是自动配置和内置容器,它把大量约定好的默认配置直接给你准备好,你要做的事情只剩在 application.yml 里填自己需要覆盖的配置项。
用一句生活化的话来说:Spring 是给你一套做菜的食材和配方,SpringBoot 是连炉灶、锅具、抽油烟机都帮你装好了,你直接开火就行。对于课设这种时间紧张的项目,省下来的配置时间可以用来打磨业务功能和写文档。
2.2 Vue 2 而不是 Vue 3?这里有一个经验主义的选择
我知道现在 Vue 3 已经是主流,但从找一个“能快速跑通、遇到问题能快速搜到答案”的课设项目角度,我仍然推荐 Vue 2 + Element UI 的组合,原因很实在:
- Vue 2 的教程、博客、CSDN 问答存量是 Vue 3 的好几倍,课设阶段你遇到 90% 的问题都能直接搜到现成的解决方案。
- Element UI 是专门为 Vue 2 设计的组件库,Element Plus 虽然功能更新,但和 Vue 2 不兼容,新手混装最容易出问题。
- 很多高校的教材和实验指导还停留在 Vue 2,用 Vue 2 写代码至少有人能指点。
当然如果你有 Vue 3 基础,用 Vue 3 + Vite + Element Plus 也完全可以,架构思路是一样的。核心要点是前后端通过 JSON 交互、通过 JWT 做身份认证、通过路由守卫控制页面访问权限,这些与 Vue 版本无关。
2.3 MyBatis:手写 SQL 带来的掌控感
MyBatis 被很多人吐槽 SQL 要自己写,但这种“笨办法”在管理系统里恰恰是优势。贫困家庭列表的多条件查询、贫困户信息和走访记录的多表联查、脱贫率的聚合统计,这些 SQL 手写能让你精确控制每一行查询逻辑,排查问题时也很清晰。
我见过不少用 JPA 的课设项目,简单 CRUD 确实爽,但一旦遇到复杂查询就得写 @Query 注解原生的 JPQL,很多人写不熟练就开始乱。而 MyBatis-Plus 又跳了一层,虽然它用起来很香,但如果你是做课设,答辩时被问到底层 SQL 是什么,用 MyBatis 手写 SQL 的你至少能讲清楚每一条查询是怎么回事。
2.4 最终版本组合参考
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定且大部分学校机器都能跑 |
| SpringBoot | 2.7.x | 兼容 JDK8,配置方式成熟,教程多 |
| MyBatis | 2.3.x | Spring Boot 官方 starter,支持 XML 映射 |
| MySQL | 8.0.x | 免费、稳定,注意驱动类名与 5.7 不同 |
| Vue | 2.6.x | 与 Element UI 配套 |
| Element UI | 2.15.x | Vue 2 生态最成熟的 UI 库 |
| Node.js | 14.21.x | 对应 webpack4,避免高版本 Node 兼容问题 |
这套组合最大的特点是:每一层都有海量的踩坑记录。你在部署时遇到的任何报错,基本都能在网上找到同款。
3. 数据库设计:这套系统的表结构是管理系统的标准答案
3.1 RBAC 权限模型:用户、角色、菜单三件套
权限管理是所有管理系统的地基,我这里的做法是经典 RBAC 模型。最核心的是五张表:用户表、角色表、菜单表、用户-角色关联表、角色-菜单关联表。
用户表设计时有几个细节需要注意:
- password 字段长度要预留,如果使用 BCrypt 加密,密文长度是 60 位,varchar(64) 是安全的。
- 建议加 status 字段,用来禁用账号,而不是直接删除用户。
- create_time、update_time 用 datetime 类型,不要用 varchar,排序和查询性能都会更好。
菜单表的设计是前端动态路由的依据,常见的字段是 menu_name、parent_id、url、icon、order_num、type(目录/菜单/按钮),前端根据当前用户的角色查询可访问的菜单列表,再动态生成侧边栏。
3.2 核心业务表:困难家庭档案、帮扶计划、走访记录
业务主表是困难家庭表,我命名为 poor_household,字段设计如下:
- id:主键
- region_id:关联行政区划表,方便按区域统计
- householder_name:户主姓名
- id_card:身份证号,建索引,因为检索和去重都依赖它
- family_members:家庭成员数
- annual_income:年收入
- poverty_reason:致贫原因
- status:档案状态,建档、帮扶中、已脱贫、已返贫,用 tinyint 存储,用数字而不是中文,避免后期枚举值变更的麻烦
- is_deleted:逻辑删除标记,默认 0
走访记录表 help_record 是操作频繁的表,关键字段包括 household_id(关联困难家庭)、user_id(走访人)、record_content(走访内容)、visit_time(走访时间)、photo(图片路径)。这个表的查询场景通常是“某个困难家庭的走访历史”,所以 household_id 一定要建索引。
帮扶计划表 help_plan 则记录计划的目标、时间范围、当前进度,与困难家庭表是一对多关系。
3.3 核心建表 SQL 与关键设计说明
下面给出一段核心表结构的建表 SQL,你可以直接复制到 Navicat 或命令行执行:
sql复制CREATE DATABASE poverty_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE poverty_db;
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
phone VARCHAR(20),
role_id BIGINT,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50),
role_code VARCHAR(50)
) ENGINE=InnoDB;
CREATE TABLE poor_household (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
region_id BIGINT,
householder_name VARCHAR(50),
id_card VARCHAR(18) UNIQUE,
family_members INT,
annual_income DECIMAL(10,2),
poverty_reason VARCHAR(255),
status TINYINT DEFAULT 0,
is_deleted TINYINT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE help_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
household_id BIGINT,
user_id BIGINT,
record_content TEXT,
visit_time DATETIME,
photo VARCHAR(255),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_household (household_id)
) ENGINE=InnoDB;
设计表时有一个容易忽略的点:凡是跟金额相关的字段,用 DECIMAL 而不是 FLOAT,浮点数在计算时会出现精度丢失,年收入一旦带小数,统计求和时就容易出现 0.01 的误差,虽然不大,但答辩时被问到会很难看。
逻辑删除字段 is_deleted 我单独拎出来说一下。管理系统的数据是有审计价值的,物理删除会让历史记录彻底丢失,所以我一般统一用逻辑删除。查询列表时在 SQL 里加上 WHERE is_deleted = 0 的条件,这是最底线的数据保全策略。
3.4 行政区划表与数据字典设计
行政区划表 region 的设计很常规:id、parent_id、name、level。parent_id 为 0 表示顶级节点(省),通过 parent_id 可以无限级展开。前端用 el-cascader 级联选择器,数据源就是从这个表查出来的树形 JSON。
数据字典表 sys_dict 则是另一种提升开发效率的通用设计。比如“致贫原因”的下拉选项有“因病、因学、因灾、缺技术、缺劳力”等多种,如果写死在业务表里,后面要加一个选项就得改表结构。拆一个字典表出来,按 type 和 code 两个字段维护,前端通过接口拉取字典项渲染下拉框,后端无需频繁改代码。这在管理系统里是一个很加分的亮点设计。
4. 后端落地:从配置文件到核心业务接口的完整实现
4.1 项目初始化与统一返回体设计
后端工程建议按功能分包,而不是按技术分包。我常用的结构是:
code复制com.example.poverty
├── controller
├── service
│ └── impl
├── mapper
├── entity
├── config
├── common
│ ├── Result.java
│ ├── ResultCode.java
│ └── exception
└── utils
统一返回体 Result 是前后端联调的基础。我在公司写代码时习惯让所有接口返回一个统一的 JSON 结构,前端 Axios 拦截器拿到后能统一处理错误码,不用每个接口单独做判断,节省大量重复代码。
java复制public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
常见的错误码约定是:200 成功、401 未登录或 token 过期、403 无权限、500 服务器异常,这个约定在后面前端封装 Axios 时会用到。
4.2 application.yml 配置文件中的两个关键坑
后端能不能一次启动成功,配置文件是重灾区。先看一份完整的 application.yml:
yaml复制server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/poverty_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.poverty.entity
configuration:
map-underscore-to-camel-case: true
两个关键坑分别说一下:
第一个是 driver-class-name。MySQL 5.x 用的是 com.mysql.jdbc.Driver,但 MySQL 8.x 必须要用 com.mysql.cj.jdbc.Driver。如果这里配错,启动日志会报 ClassNotFoundException 或者 Loading class com.mysql.jdbc.Driver. This is deprecated. 我帮学弟调的那个项目就是把旧的驱动类名抄过来了。
第二个是 serverTimezone=Asia/Shanghai。MySQL 8.0 的默认时区如果不对,连接时容易报 The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这个报错本质是数据库时区与中国本地时区不一致,在连接串后面加上 serverTimezone 参数就能解决,踩过一次就忘不掉。
另外,map-underscore-to-camel-case 配置很重要。数据库字段是下划线风格(household_name),Java 实体类是驼峰风格(householdName),开启这个配置后 MyBatis 会自动映射,否则你每次查询出来的对象属性都是 null,而且没有任何报错提示,排查起来非常迷惑。
4.3 JWT 登录鉴权与拦截器的实现思路
登录模块是前后端分离项目最核心的鉴权环节。传统的 Session 方式需要服务端保存会话状态,而前后端分离架构中,前端可能部署在 Nginx,后端是独立进程,不适合共享 Session。JWT 的无状态特性正好适合这个场景:用户登录成功后,后端用 JWT 生成一个带过期时间的令牌返回给前端,前端后续每次请求都在 Header 里带上这个令牌,后端通过拦截器验证令牌的合法性。
后端登录接口的核心逻辑:
java复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String login(String username, String password) {
User user = userMapper.selectByUsername(username);
if (user == null || !user.getPassword().equals(MD5Utils.md5(password))) {
throw new BizException("用户名或密码错误");
}
if (user.getStatus() == 0) {
throw new BizException("账号已被禁用");
}
// 生成JWT,过期时间设置为2小时
String token = JwtUtils.createToken(user.getId(), user.getUsername());
return token;
}
}
JWT 工具类是基于 jjwt 库实现的,生成 token 时把用户 ID 和用户名写进 payload,再用密钥签名。拦截器要做的事情是从请求头 Authorization 字段拿到 token,解析通过则放行,解析失败则返回 401 状态码。
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || !JwtUtils.verify(token)) {
response.setStatus(401);
response.getWriter().write("未登录或token已过期");
return false;
}
return true;
}
}
注意这里一定要放行 OPTIONS 预检请求,这是前后端分离联调时期一个超级隐蔽的坑。浏览器跨域请求前会自动发一个 OPTIONS 请求做预检,如果拦截器直接拦截掉,前端就永远拿不到真实响应,而且控制台报错比较隐晦,容易被误判为跨域问题。
4.4 建档管理接口:分页与多条件查询怎么写才清晰
贫困家庭列表页是最典型的管理系统功能,后端接口需要支持分页,同时根据户主姓名、状态、区域 ID 进行条件过滤。用 PageHelper 做分页是我认为最方便的方式,在查询前调用 PageHelper.startPage(pageNum, pageSize),它会给后面的第一条查询语句自动拼接 LIMIT。
java复制@GetMapping("/list")
public Result<PageInfo<PoorHouseholdVO>> list(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
String householderName,
Integer status,
Long regionId) {
PageHelper.startPage(pageNum, pageSize);
List<PoorHouseholdVO> list = poorHouseholdMapper.selectList(householderName, status, regionId);
return Result.success(new PageInfo<>(list));
}
Mapper 层的 XML 文件负责写动态 SQL,这里用到了 MyBatis 的 <if> 标签:
xml复制<select id="selectList" resultType="com.example.poverty.vo.PoorHouseholdVO">
SELECT ph.*, r.name AS regionName
FROM poor_household ph
LEFT JOIN region r ON ph.region_id = r.id
WHERE ph.is_deleted = 0
<if test="householderName != null and householderName != ''">
AND ph.householder_name LIKE CONCAT('%', #{householderName}, '%')
</if>
<if test="status != null">
AND ph.status = #{status}
</if>
<if test="regionId != null">
AND ph.region_id = #{regionId}
</if>
ORDER BY ph.create_time DESC
</select>
动态 SQL 的好处是三个查询条件自由组合,任何一个为空就自动跳过。这种写法在管理系统里太常用了,值得单独练熟。
4.5 走访记录新增:一次事务控制下的完整链路
新增走访记录不能只往 help_record 表插一条数据,还需要同步更新困难家庭表的最近走访时间和状态,两个表的数据变更要么都成功、要么都失败,所以必须加 @Transactional 注解。
java复制@Transactional(rollbackFor = Exception.class)
public void addRecord(HelpRecord record) {
PoorHousehold household = poorHouseholdMapper.selectById(record.getHouseholdId());
if (household == null) {
throw new BizException("困难家庭不存在");
}
helpRecordMapper.insert(record);
// 更新最近走访时间和状态
poorHouseholdMapper.updateVisitInfo(record.getHouseholdId(), new Date());
}
这个例子虽然简单,但它展示了事务管理的典型应用场景。不光是走访记录,帮扶计划、脱贫评估审批等凡是涉及多表写入的操作,都应该考虑是否需要用事务包裹。
4.6 统计接口:一条 SQL 跑通的脱贫进度
统计功能是领导角色最关注的部分,实现难度不大,但 SQL 的聚合思想要过关。统计各区域困难家庭数量和已脱贫户数,可以使用 CASE WHEN 搭配 GROUP BY:
sql复制SELECT
r.name AS regionName,
COUNT(ph.id) AS totalCount,
SUM(CASE WHEN ph.status = 2 THEN 1 ELSE 0 END) AS deCount
FROM poor_household ph
LEFT JOIN region r ON ph.region_id = r.id
WHERE ph.is_deleted = 0
GROUP BY ph.region_id
这里的 status = 2 表示已脱贫,SUM 配合 CASE WHEN 相当于条件计数。前端拿到这个结果后渲染 ECharts 饼图或柱状图,就是一个完整的统计报表模块。
5. 前端工程化:Vue 项目的结构与页面实战
5.1 项目初始化与目录规划
前端工程推荐用 Vue CLI 来创建,命令很简单:vue create poverty-web。创建完之后按下面的目录整理:
code复制src
├── api # 接口请求统一管理
│ ├── login.js
│ ├── household.js
│ └── record.js
├── router
│ └── index.js
├── store # Vuex,主要存用户信息和权限信息
├── utils
│ ├── request.js # Axios 封装
│ └── auth.js
├── views
│ ├── login
│ ├── dashboard
│ ├── household
│ ├── record
│ └── system
│ ├── user
│ └── role
├── App.vue
└── main.js
这个目录规划的原则是:API 专门放一个目录集中管理,页面组件放 views 目录,公共请求逻辑封装在 utils/request.js。很多新手习惯在组件里直接写 axios 请求,后期接口一多,代码就难以维护。你把它拆出来之后,前端结构就跟后端的 controller-service 一样清晰。
5.2 路由守卫:把未登录用户挡在门外
前端路由守卫是安全防线的第一关。虽然后端有 JWT 拦截器,但前端的体验也要做好:用户访问需要登录的页面时,如果 token 不存在,直接帮他跳转到登录页。
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.path === '/login') {
next();
} else {
if (!token) {
next('/login');
} else {
next();
}
}
});
这个逻辑简单直接,适合大多数课设项目。更进阶的做法是根据用户角色动态生成可访问的路由表,用 router.addRoutes() 动态挂载,这样不同角色登录后看到的侧边栏菜单完全不同,是答辩时的加分项。
5.3 Axios 请求封装与 JWT 令牌注入
Axios 封装的目标是让每个页面组件里不需要关心 token 怎么带、错误怎么提示,我只保留最常见且最实用的方案:
javascript复制import axios from 'axios';
import { Message } from 'element-ui';
import router from '@/router';
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_URL || '/api',
timeout: 15000
});
// 请求拦截器:注入token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = token;
}
return config;
});
// 响应拦截器:统一处理错误码
request.interceptors.response.use(
response => {
const res = response.data;
if (res.code === 401) {
localStorage.removeItem('token');
router.push('/login');
Message.error('登录已过期,请重新登录');
return Promise.reject(new Error('unauthorized'));
}
if (res.code !== 200) {
Message.error(res.message || '请求失败');
return Promise.reject(new Error(res.message));
}
return res.data;
},
error => {
Message.error('网络异常,请稍后重试');
return Promise.reject(error);
}
);
export default request;
这里有一个容易被忽视的细节:baseURL 的开发环境和生产环境应该分开配置。开发环境建议用 /api 加代理方式解决跨域,生产环境则由 Nginx 转发到后端服务。在项目根目录创建 .env.development 文件,写入 VUE_APP_BASE_URL=/api,用环境变量区分,换环境部署时不需要改代码。
5.4 前端开发服务器代理:解决跨域最优雅的方式
开发环境下前后端分离最省心的跨域处理方式,不是在后端写 CORS 过滤器,而是用 Vue CLI 的 devServer 代理。在 vue.config.js 中配置:
javascript复制module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
这样配置之后,前端页面里请求 /api/household/list 会被 devServer 转发到 http://localhost:8081/household/list,浏览器地址栏里始终访问的是前端自己的地址,不存在跨域问题。如果后端没有做跨域配置,而这个代理又没有配好,前端就会一直报 No 'Access-Control-Allow-Origin' header is present,箭头直指后端,但根因其实在前端代理。
5.5 Element UI 页面实战:表格、弹窗、表单三件套
困难家庭列表页是典型的管理系统页面结构,用 Element UI 组合起来就是表格 + 查询表单 + 分页 + 新增弹窗。
表格部分用 el-table 绑定数据,分页用 el-pagination,配套的逻辑是:查询条件变化时重置 pageNum 为 1,点击分页时重新拉取接口数据。
vue复制<el-table :data="tableData" v-loading="loading">
<el-table-column prop="householderName" label="户主姓名" width="120"></el-table-column>
<el-table-column prop="idCard" label="身份证号" width="180"></el-table-column>
<el-table-column prop="annualIncome" label="年收入"></el-table-column>
<el-table-column prop="regionName" label="所属区域"></el-table-column>
<el-table-column label="状态">
<template slot-scope="scope">
<el-tag :type="statusMap[scope.row.status].type">
{{ statusMap[scope.row.status].label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
新增和编辑功能共用同一个 el-dialog,表单数据用 el-form 的 rules 做基础校验,提交成功后刷新列表。这是管理系统前端最标准的三件套组合,熟练之后写任何管理页面都是这套逻辑的复制与微调。
6. 完整部署教程:从克隆代码到浏览器访问全流程
6.1 环境准备:用一张表对齐版本
部署环节是翻车重灾区,我强烈建议先把本机环境和下面的表格对齐,尤其是 Node 的版本,Vue 2 项目用 Node 14 最稳妥。我自己实测过,Node 17+ 跑 Vue 2 老项目时经常报 OpenSSL 相关的 hash 错误,那个报错一出来,新手基本手足无措。
| 软件 | 版本要求 | 主要用途 |
|---|---|---|
| JDK | 1.8 | 编译运行 SpringBoot |
| Maven | 3.6+ | 后端依赖管理 |
| MySQL | 5.7 或 8.0 | 数据库,8.0 注意驱动配置 |
| Node.js | 14.21.x | 前端依赖安装和构建 |
| IDEA | 2020+ | 后端开发调试 |
| VSCode | 最新版 | 前端开发调试(可选) |
6.2 后端启动步骤
第一步是导入数据库。打开 Navicat 或命令行,执行你本地准备好的 poverty_db.sql 脚本,它会自动创建数据库和所有表。不要用记事本手动复制 SQL 里的中文,一定用数据库管理工具执行,避免字符编码问题。
第二步是用 IDEA 打开后端项目,等 Maven 下载完依赖后,修改 application.yml 中的数据库用户名和密码,改成你本机 MySQL 的实际账号。
第三步是直接运行启动类 PovertyApplication.java。启动成功会在控制台看到 Spring Boot 的启动日志,默认端口是 8081。如果启动失败,优先排查数据库连接配置,其次看端口是否被占用,被占用的话改 server.port。
6.3 前端启动步骤
第一步,用 VSCode 打开前端项目目录,在终端执行 npm install 安装依赖。国内网络环境下载慢的话,换成淘宝镜像源安装会快很多:
bash复制npm config set registry https://registry.npmmirror.com
npm install
第二步,执行 npm run serve 启动开发服务器。看到 App running at localhost:8080 的输出说明启动成功。
第三步,浏览器访问 http://localhost:8080,就能看到登录页面。用管理员账号登录后,正常情况下就能进入系统主页,后续的所有接口请求都会经过 devServer 代理转发到后端服务。
6.4 常见部署报错对照表
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| Access denied for user 'root' | 数据库用户名或密码错误 | 检查 application.yml 中的账号密码 |
| Unknown database 'poverty_db' | 数据库没有创建成功 | 重新执行 SQL 脚本 |
| The server time zone value is unrecognized | MySQL 时区问题 | 连接串加上 serverTimezone=Asia/Shanghai |
| Cannot load driver class: com.mysql.jdbc.Driver | MySQL 8 使用了旧驱动类名 | 改为 com.mysql.cj.jdbc.Driver |
| Invalid bound statement (not found) | Mapper 接口与 XML 未绑定 | 检查 mapper 接口的 namespace 和 XML 中的 statement id |
| npm ERR! ERESOLVE unable to resolve dependency tree | Node 版本过高 | 切换 Node 14 后重新 npm install |
| Failed to resolve module 'vue' | 依赖没装全或版本冲突 | 删除 node_modules 和 package-lock.json 后重装 |
这张表的每一行都是我真实遇到过的,尤其是 Invalid bound statement,新手一看到这个报错就慌。它本质是 MyBatis 找不到 Mapper 接口对应的 SQL 语句,排查时先看 XML 文件的 namespace 是否对应正确的 Mapper 接口全限定名,再看 mapper-locations 配置是否扫描到了 XML 文件路径。
6.5 生产环境部署思路(可选做加分项)
如果你想把项目部署到服务器上,或者想在答辩时展示一下“线上运行”,基本的思路是:前端 npm run build 会生成 dist 静态文件,交给 Nginx 托管;后端 mvn package 打成 jar 包,用 java -jar 启动。
Nginx 配置里最关键的是把 /api 的请求反向代理到后端的 8081 端口,同时配置前端路由的 history 模式重写。你不需要在服务器上真正搭这套环境,只需要在答辩 PPT 里画清楚这个架构图,评委就会觉得你的项目有工程化思维。
7. 开发完两套课设后总结的坑与值得复用的经验
7.1 跨域问题为什么总是第一关
前后端分离的项目,跨域问题几乎是每个新手都会遇到的第一道坎。根因是浏览器的同源策略,但很多人的解决方式很粗暴——直接在后端加一个允许所有来源跨域的过滤器。
我个人的建议是分场景处理:开发环境优先用 Vue devServer 代理,简单干净,不污染后端代码;生产环境用 Nginx 反向代理。后端保留 CORS 配置不是不可以,但要把允许的域名写具体,allowedOriginPatterns("*") 这种写法用于开发调试可以,上线前一定要收口。
7.2 MyBatis 的 N+1 查询问题
在管理系统里,列表页最容易出现 N+1 查询。比如查困难家庭列表时,如果要显示帮扶责任人的姓名,新手的第一反应是在循环里根据 user_id 再去查一遍用户表,100 条数据就会多执行 100 条 SQL。
正确的做法是用一条 SQL 把关联表的数据 JOIN 进来,或者先查出列表,再把所有 user_id 收集起来用 IN 查询一次,然后内存中组装。前者简单直接,后者适合关联数据更复杂的场景。面试或答辩时能主动提到这个优化点,会显得你有性能意识。
7.3 PageHelper 分页插件的使用边界
PageHelper 的机制是基于 ThreadLocal 的,它会把分页参数保存下来,拦截并修改紧接着的下一条 SQL。因此有两个使用禁忌:第一,startPage 方法后面必须紧跟 Mapper 查询,中间不要穿插其他 SQL 操作;第二,不要在循环里调用 startPage,否则分页参数会错乱。
遇到分页数据总数不对的 bug,优先检查这两条。
7.4 答辩论:几个安全与性能的进阶方向
如果课设想拿高分,下面这几个点不需要全部实现,但至少要能讲出思路:
- 密码存储用 BCrypt 而不是 MD5,MD5 加盐也不如 BCrypt 的哈希算法安全。我在登录模块演示代码里用了 MD5 是为了突出业务主流程,但如果你想让项目上一个档次,换成
BCryptPasswordEncoder更合适。 - 文件上传不要直接存数据库,把文件保存在服务器磁盘或云存储,数据库里只存路径。
- 常见查询字段加索引,比如 id_card、household_id、create_time。
- 敏感操作记录日志,改造一个 AOP 切面,统一记录操作人、操作时间、操作内容,这也是管理系统审计功能的雏形。
- 用 Redis 缓存字典数据和热点统计结果,减轻数据库压力。
我在实际做课设指导时发现一个规律:很多项目之所以出问题,不是代码能力不行,而是环境层面的细节没处理好。版本不统一、配置文件抄错、依赖不兼容,这些小问题会消耗掉大量本该用于打磨功能的时间。如果你正在做同类项目,把部署时每一处的细节都记录好,这些经验才是比功能代码更值钱的东西。
最后分享一个我个人的操作习惯:写完一个模块,先不急着做下一个,立刻启动前后端把接口联调一遍,宁可多花十分钟,也不要最后集中联调时冒出二十个错误,那时候连错误从哪来都理不清楚。前后端分离系统的复杂度比单体高得多,但只要你把架构逻辑理顺,它就是一套非常清晰的生产力工具。
