前几天一个同学来找我,说毕业设计分到了一个“学院个人信息管理系统”的题目,问我用什么技术栈做最稳。我跟他捋了一遍需求:学生能登录、查看和修改自己的基本信息、上传头像、改密码;管理员能维护用户列表、查看全院信息、做简单的数据导出。说实话,这类系统的业务模型非常清晰,核心就是“登录鉴权 + 个人信息CRUD + 角色权限”,非常适合用SpringBoot + Vue这套前后端分离方案来做。如果你也正准备做类似的毕设、课程设计,或者刚学完框架想找个完整项目练手,这篇文章正好能帮你把整条链路走通:从数据库设计、后端接口实现、Vue页面开发,到最后的本地联调和部署上线。我会把每一步的选型理由、实现思路和踩坑点都讲清楚,而不是只给一份能跑但看不懂的源码。
1. 这套技术栈凭什么成为管理系统的“标准答案”
1.1 前后端分离带来的开发体验差异
早几年做这种学院管理系统,主流方案还是用JSP + Servlet,或者SSM框架配合Thymeleaf模板引擎。页面和后端逻辑耦合在一个工程里,改个按钮颜色都要重新打包重启,前端想用组件化开发基本没戏。后来慢慢切换到SpringBoot + Vue,等于把“数据接口”和“页面展示”彻底拆开了:后端只负责出JSON接口,前端专注页面交互,两边通过HTTP协议对接,只要接口约定固定下来,前端可以并行开发、后端也可以单独测试接口。
这个项目里,前端用Vue,后端用SpringBoot,开发体验上的提升非常明显。比如用户信息表单要做联动回显,前端拿到JSON之后直接在浏览器里操作DOM状态,不需要刷新整个页面;而后端只需要保证接口返回的数据结构稳定,完全不关心页面长什么样。这样的模式,也和你以后在公司里接触的团队开发方式一致,做完这个项目,等于提前适应了真实的前后端协作流程。
1.2 MyBatis加MySQL:中小型项目里的稳妥选择
很多人会问,现在JPA、MyBatis-Plus都流行,为什么还选MyBatis?我身边不少同学的项目确实用了MyBatis-Plus,因为它省去了手写SQL的麻烦。但“学院个人信息管理系统”这类项目,恰恰是练习MyBatis的最佳场景:表结构不多、查询条件简单,但是你需要理解SQL是怎么映射到Mapper接口的,也需要掌握多表查询在XML里怎么写。
MySQL在这套组合里负责最基础的持久化,它的优势不用多说:免费、轻量、资料多。你随便搜一个问题,基本都能找到解决方案。对于个人开发者或者小团队来说,单机部署一个MySQL实例,建四张表,跑这种量级的数据完全没压力。我见过有人为了体现“技术含量”硬上分布式数据库,结果光环境配置就折腾了两天,最后还得回退到MySQL,属于典型的舍近求远。
1.3 为什么不建议换更“重”的方案
也有同学问过,能不能直接用Spring Cloud、或者把前端搞成微前端架构。我的看法是:项目复杂度要和业务复杂度匹配。一个学院个人信息管理系统,核心用户就是学生、教师和学院管理员,并发量大概率是个位数,单体应用绰绰有余。引入微服务不仅会把分库分表、服务注册、配置中心这些基础设施全部带进来,还会让毕业设计答辩变成“中间件安装大会”。
当然,如果你已经熟练掌握了这套单体方案,也可以预留一下扩展点,比如把文件上传改造成OSS存储、把用户模块拆成独立服务。但在最初版,直接以SpringBoot + Vue + MyBatis + MySQL打底,把核心功能跑通,才是最经济、最稳妥的路线。技术选型不仅要看技术本身好不好,还要看它适不适合你当前阶段的目标和资源。
工程里最终采用的结构就是前面说的这套组合。接下来我从最容易被忽略的数据库设计开始讲,因为很多同学习惯先写代码再想着建表,结果做到一半发现字段对不上,推倒重来非常痛苦。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先把数据模型钉死:表结构、角色与权限设计
2.1 四张核心表的结构设计与字段说明
我强烈建议,写任何一行后端代码之前,先用SQL把表结构设计出来。表格就是项目的“地基”,地基歪了,后面所有接口都要跟着改。这个管理系统我最终设计了四张核心表,下面给出建表思路和关键字段。
第一张是user_account用户账号表,负责登录鉴权。字段包括id、username、password、role、status、create_time、update_time。这里的role用tinyint表示,0代表管理员,1代表学生,2代表教师,避免用字符串直接存角色名,查询效率更高,也方便前端做角色判断。
第二张是student_info学生基本信息表,存储学生详细档案。关键字段有id、user_id、student_no、name、gender、birth_date、id_card、phone、email、politic_status、major、class_name、grade、native_place、address、avatar、deleted、create_time、update_time。注意user_id和user_account.id关联,但学生学号student_no才是业务上的唯一编号,所以该字段也要加唯一索引。deleted是逻辑删除标记,这个非常重要,万一管理员误删了学生信息,还能从数据库里把数据捞回来。
第三张是teacher_info教师信息表,字段相对简单:id、user_id、teacher_no、name、gender、title、phone、email、dept_id、deleted。因为系统以学生为主,教师表可作为扩展,不做太复杂。
第四张是dept_info学院/系部表,字段包括id、dept_name、dept_code、create_time。这张表主要用来做下拉联动,比如学生选择自己所在学院时,前端可以直接从接口拿列表渲染。
四张表之间的关系不复杂:账号表是登录入口,学生表和教师表通过user_id指向账号表,学院表和教师表关联。建表语句里统一使用utf8mb4字符集,别用老的utf8,不然用户填个生僻字或者表情符号就会存不进去,这是我见过很多新手项目里最典型的隐形坑。
2.2 学生、教师、管理员三类角色的最小权限模型
权限设计不需要一上来就搞RBAC全套表结构(用户表、角色表、权限表、角色权限关联表……),一个小型管理系统开局就上五张权限表,只会给自己增加负担。这里的做法是:user_account表里直接加role字段,后端写一个拦截器,在进入需要权限的接口之前判断当前登录用户的角色。
具体的角色划分如下:
- 管理员(role=1):可以查看全部用户列表、编辑用户状态、重置密码、导出数据。
- 教师(role=2):可以查看自己的基本信息,维护所带学生的部分信息(可选功能)。
- 学生(role=0):只能查看和编辑自己的信息、上传头像、修改密码。
后端的拦截器判断逻辑很简单,用HandlerInterceptor实现,在preHandle方法里取当前请求头中的Token,解析出用户ID和角色,然后判断请求路径是否在角色允许的访问列表内。如果当前用户是学生,却去请求/api/admin/users这类接口,直接返回403。
前端的配合方式是路由守卫加菜单权限:登录成功后拿到用户角色,在router.beforeEach里根据路由的meta.role字段判断是否允许进入对应页面。菜单栏也按角色渲染,学生登录只看到“我的信息、修改密码”,管理员才能看到“用户管理、数据导出”。前后端双重校验,既保证用户体验流畅,也保证接口数据安全。
2.3 数据库初始化脚本的编写技巧
脚本里除了建表,我记得当时还写了几条测试数据插入语句。一定要提前插入一个管理员账号,比如admin,密码不能明文保存,项目里用MD5或BCrypt加密。这里提个醒:如果你用Spring Security Crypto里的BCryptPasswordEncoder,同一个密码每次加密结果都不一样,这是正常的,别以为代码写错了。
初始化脚本最好放在项目根目录的doc/sql/init.sql,同时在application.yml里配置spring.sql.init相关参数,让项目启动时自动执行脚本。不过生产环境建议关掉自动初始化,避免每次启动都重复插入数据。脚本的头部加上DROP TABLE IF EXISTS,方便重复执行,但你自己本地调试时要注意,这条命令会把已有数据全部清掉。
数据库设计这块,我实际写的过程中发现,最容易返工的地方不是字段数量不够,而是字段命名不统一。比如有的表用createTime,有的表用create_time,MyBatis的驼峰映射配置一旦没开,查出来的字段就是null。所以从建表第一天起,所有字段统一用下划线命名,Java实体类用驼峰,然后开启map-underscore-to-camel-case=true,能省掉后面无数个低级bug。
3. 后端模块怎么搭:接口约定、统一返回与文件上传
3.1 工程结构、依赖版本与配置文件
后端我用的是SpringBoot工程,包结构按下层划分:
code复制com.example.info
├── controller
├── service
├── mapper
├── entity
├── config
├── common
└── utils
controller只负责接收请求和返回结果,service放业务逻辑,mapper是MyBatis的Mapper接口,entity放实体类,config放跨域、拦截器等配置,common放统一返回结构、异常类,utils放JWT工具、文件上传工具等。
依赖方面,核心就这几个:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、jjwt(用于生成和解析Token)、lombok、hutool(工具库,非必需但很好用)。如果你的JDK是8,SpringBoot版本建议选2.7.x系列;如果你的环境是JDK17及以上,可以直接用SpringBoot 3.x系列。很多同学一上来就装最新的Spring Boot 3.x,结果发现本地JDK还是8,启动直接报错,所以在创建项目之前先确认一下JDK版本。
application.yml里的核心配置是数据源。MySQL 8以上版本记得在连接串后面加时区参数:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/college_info?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
mapper-locations这个配置如果漏了,MyBatis就找不到XML文件,启动时会报“Invalid bound statement”的错误,这应该是新手遇到最多的异常之一,多半就是它造成的。
3.2 从Entity到Mapper再到Service的编码顺序
写后端代码我习惯按“Entity实体类 -> Mapper接口 -> Service接口及实现 -> Controller”的顺序来,因为Controller是入口,先把底层的数据操作写好,业务逻辑再往上一层一层拼。
Entity类直接用Lombok的@Data注解,省去手写getter/setter。字段和数据库表一一对应,注意类型映射:数据库的datetime对应LocalDateTime,tinyint对应Integer,varchar对应String。
Mapper接口很简单,核心方法这样定义:
java复制@Mapper
public interface StudentInfoMapper {
StudentInfo selectByUserId(Long userId);
StudentInfo selectByStudentNo(String studentNo);
int insert(StudentInfo studentInfo);
int updateById(StudentInfo studentInfo);
List<StudentInfo> selectPage(@Param("offset") int offset, @Param("limit") int limit, @Param("keyword") String keyword);
int count(@Param("keyword") String keyword);
}
对应的XML文件放在resources/mapper/目录下,里面写具体的SQL。这里我要多说一句:简单的单表CRUD可以直接用MyBatis注解写在接口上,但我还是建议写成XML,因为一旦查询条件复杂了,比如动态拼接where语句里的多个可选条件,XML的<where>和<if>标签比注解里的字符串拼接清晰太多。而且这个项目后面如果要扩展筛选功能,直接在XML里加条件就行,不用改Java代码。
Service层主要处理业务规则。比如修改个人信息时,要先判断当前登录的用户ID是不是和这条学生信息的所有者匹配;如果学生已经把某个字段填写成了空字符串,是保留原值还是清空,这些逻辑放在Service里最合适。Controller保持精简,只做参数接收和结果包装。
3.3 个人信息管理核心接口的实现细节与上传处理
整个系统的接口数量不多,核心接口大概有这些:
| 接口路径 | 请求方式 | 功能说明 |
|---|---|---|
| /api/auth/login | POST | 登录,返回Token和用户基本信息 |
| /api/auth/logout | POST | 登出,前端清除Token即可 |
| /api/user/info | GET | 获取当前用户的信息 |
| /api/user/info | PUT | 修改当前用户的信息 |
| /api/user/avatar | POST | 上传头像 |
| /api/user/password | PUT | 修改密码 |
| /api/admin/users | GET | 管理员分页查询用户列表 |
| /api/admin/users/{id}/status | PUT | 管理员启用/禁用用户 |
| /api/admin/users/ | DELETE | 管理员删除用户 |
我挑了上传头像这个功能详细说说,因为它是很多初学者容易卡住的地方。前端用multipart/form-data格式,把文件传给后端,后端用MultipartFile接收:
java复制@PostMapping("/avatar")
public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file, @RequestAttribute("userId") Long userId) {
if (file.isEmpty()) {
return Result.error("文件不能为空");
}
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString().replace("-", "") + ext;
String uploadDir = uploadProperties.getPath(); // 如 /data/college-info/avatar/
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
file.transferTo(new File(dir, newFileName));
studentInfoService.updateAvatar(userId, "/avatar/" + newFileName);
return Result.success("/avatar/" + newFileName);
}
这里有几个细节必须强调:第一,文件名不能用用户上传的原始文件名,一是避免中文文件名乱码问题,二是防止两个用户上传了同名文件互相覆盖;第二,目录如果不存在要主动创建,transferTo不会帮你创建父目录;第三,数据库里存的是相对路径而不是完整磁盘路径,完整路径一旦服务器迁移就全废了。
存完路径之后,还要在SpringBoot里配置静态资源映射,把/avatar/**这个URL路径映射到本地的上传目录:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/avatar/**")
.addResourceLocations("file:" + uploadProperties.getPath());
}
}
这样前端访问/avatar/xxxx.jpg就能直接显示图片,不需要再单独写一个读取文件的接口。
3.4 统一返回、全局异常和参数校验
前后端对接最容易扯皮的地方就是接口返回格式不统一。有的接口返回{ code: 200, data: ... },有的接口直接返回裸数据,前端写代码时就要到处判断,非常痛苦。所以我在common包里定义了一个Result<T>泛型类:
java复制@Data
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(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
所有Controller的返回值都统一用Result包装,前端在响应拦截器里只判断code字段,省去了大量重复的错误处理代码。
全局异常用@RestControllerAdvice处理。业务里抛出的自定义异常比如“密码错误”“用户不存在”,统一捕获后返回对应的错误码和提示信息;如果是SQL异常、空指针这类没料到的异常,返回统一提示“系统繁忙”,同时打印日志方便排查。这样前端拿到的永远是结构一致的JSON,不会出现某个接口抛出一堆英文堆栈信息让前端无处下手。
参数校验我用的SpringBoot自带的@Validated,在实体类字段上加上@NotBlank、@Email这类注解,在Controller参数前加@Validated,就能在进入业务逻辑之前先拦截非法参数。比如修改个人信息时手机号格式不对,直接在Controller层就返回错误提示,业务代码干净很多。
后端这一套写完,其实已经能支撑整个系统的核心数据流了。接下来就是前端怎么把这些接口串起来,做成一个像样的页面。
4. 前端Vue页面如何组织:登录、路由守卫、表单联动
4.1 Vite加Vue3的工程划分
前端工程我用Vite + Vue3,而不是老旧的Vue CLI。Vite启动速度快、配置简单,现在的一线项目基本都在往这个方向迁移。工程目录划分我按照模块化思路来:
code复制src
├── api # 所有接口请求函数
├── assets # 静态资源
├── components # 通用组件(头像上传、分页等)
├── router # 路由配置
├── store # Pinia状态管理
├── views # 页面视图(登录页、个人中心、用户管理等)
├── utils # 请求封装、Token工具等
└── App.vue
为什么要把api单独拆出来?因为前端项目一旦页面多了,接口请求如果散落在各个组件里,后端接口一改路径就得全局搜索替换。单独建一个api目录,每个页面模块对应一个JS文件,比如user.js里放getUserInfo、updateUserInfo、uploadAvatar等函数,页面组件里直接引用,接口变动时只改一处。
4.2 axios请求封装与token注入
axios封装是前端工程里最基础也最重要的一环。我在utils/request.js里创建了一个axios实例,配置baseURL,然后在请求拦截器里从localStorage取出Token,放到Authorization请求头里:
javascript复制import axios from 'axios'
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) {
return res.data
}
// 登录过期等特殊状态处理
if (res.code === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(new Error(res.message))
},
error => {
return Promise.reject(error)
}
)
export default request
注意响应拦截器里,当code === 200时直接return res.data,这样页面里调用接口时拿到的就是业务数据本体,不需要再写一层.data.data。如果后端返回的code是401(Token过期或者未登录),自动跳转到登录页。这个统一处理能帮你省掉几乎所有页面里的重复登录判断。
4.3 登录页和路由守卫的实现思路
登录页的交互逻辑看起来很普通,但做的时候有几个点容易被忽略。表单校验要覆盖:用户名为空、密码为空、密码长度不足;登录成功后接口会返回一个Token字符串,需要同时把用户基本信息也存下来,因为个人中心页面打开后第一件事就是显示当前用户名和角色。后端登录接口我设计成返回{ token, userInfo },前端一次性处理,不用登录后再发一次请求拿信息。
路由守卫是控制页面访问的关键:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
next()
return
}
if (!token) {
next('/login')
return
}
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
if (to.meta.roles && !to.meta.roles.includes(userInfo.role)) {
next('/403')
return
}
next()
})
个人中心页面只有登录后才能访问,用户管理页面只有管理员才能访问。如果学生手动输入/admin/users的地址,路由守卫会拦截并跳到403页面。这里我要强调一个Web开发常识:路由守卫只是用户体验层面的控制,真正的安全校验必须依赖后端接口的权限拦截,因为懂技术的人完全可以绕过前端路由,直接拿Token去请求后端接口。前后端的权限判断必须是各自独立的,不能因为前端做了控制就放松后端的校验。
4.4 个人信息表单的联动回显与头像上传组件
个人中心页的核心是一个大表单,包含学生的基本信息。页面打开时调用getUserInfo接口,把返回的数据填充到表单里。这里有个经验:后端返回的字段名尽量和前端表单绑定字段保持一致,比如数据库里是studentNo,接口返回也是studentNo,前端form.studentNo直接可以回显,不用再做字段映射。
学院下拉框用dept_info表的数据渲染,前端用el-select组件绑定deptId,选项列表由/api/dept/list接口提供。当用户选择了新的学院,可以继续联动专业、班级等字段,不过这个系统里专业和班级是文本字段,不需要做成级联,直接用el-input填写即可。
头像上传这里,我用的是el-upload组件,但它默认的action方式不太好携带请求头Token,所以需要自定义上传方法。核心是配上:http-request属性,用我们封装好的request实例发送请求:
javascript复制const handleUpload = async (options) => {
try {
const formData = new FormData()
formData.append('file', options.file)
const url = await uploadAvatar(formData)
form.userAvatar = url
// 全局提示上传成功
} catch (e) {
// 提示上传失败
}
}
上传成功后,要同时做两件事:把返回的图片URL回显到当前表单的avatar字段上,以及调用一次updateUserInfo把头像字段持久化到数据库。很多新手容易漏掉第二步,导致图片刷新之后就丢了,实际只是存到了内存里。
用户管理页面就比较常规了:一个搜索框、一个el-table列表、一个分页器。管理员可以按学号或姓名搜索学生,搜索结果通过分页接口返回。列表的每一行提供“编辑”“禁用”“删除”操作按钮,删除走的是逻辑删除,所以数据库里的记录还在,只是deleted字段变成了1。
前端这块我实际写下来,发现最花时间的反而不是页面本身,而是各种交互细节的打磨:表单校验规则、加载状态、空数据展示、按钮权限控制。这些细节直接决定了这个系统拿去演示时给人的整体印象,所以不建议只追求“能跑就行”。
5. 从本地联调到服务器部署:跨域、端口和配置文件这些坑
5.1 前后端联调时的跨域与代理配置
前后端分离之后,第一个迎面而来的问题就是跨域。开发环境下,Vue工程跑在localhost:5173,SpringBoot跑在localhost:8080,端口不同,浏览器默认会拦截跨域请求。有两套解决方案,我用的是第二套,先说第一套方便你理解。
第一套在后端配置跨域,写个配置类或者直接加@CrossOrigin注解,告诉浏览器允许哪些来源访问。这套简单,但生产环境一般不推荐直接全开,因为那等于允许任意站点访问你的接口,除非配合完整的安全验证。
第二套是前端Vite代理,开发时把请求转发到后端,浏览器视角看所有请求都指向同一个源,自然不存在跨域问题:
javascript复制// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
这样前端请求/api/user/info,Vite会把请求转发到http://localhost:8080/api/user/info。前端代码里的baseURL直接写/api,后续部署时也只要改代理配置指向真实的后端地址就行,前端代码本身不用动。我强烈建议开发阶段用代理方案,因为后端不需要写任何跨域代码,生产环境也更容易统一配置。
5.2 环境切换与生产部署要点
代码写完之后,部署上线又是一个容易出问题的环节。application.yml里我习惯拆成三个文件:application.yml放公共配置,application-dev.yml放本地开发配置,application-prod.yml放生产环境配置。启动时加参数--spring.profiles.active=prod切换环境,避免每次部署都去改数据库连接串。
生产部署步骤很简单,但每一步都有坑:
- 后端执行
mvn clean package,打出jar包。 - 上传到服务器,执行
nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &。 - 前端执行
npm run build,生成dist目录。 - 把
dist目录上传到服务器的Nginx站点目录。
然后配置Nginx,将前端请求分发到SpringBoot端口:
nginx复制server {
listen 80;
server_name your-domain.com;
root /data/college-info/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /avatar/ {
proxy_pass http://127.0.0.1:8080;
}
}
try_files $uri $uri/ /index.html这一行特别关键,它解决了Vue Router的history模式刷新404问题。如果你用的是hash模式(URL里带#),就不会有这个困扰,但页面地址不好看。现在主流还是history模式,所以Nginx这行配置必须写上。
5.3 部署上线后遇到过的几个真实雷区
我记得第一次部署这个系统的时候,信心满满地访问线上地址,结果一连踩了三个雷,每个都花了不少时间排查。
第一个是MySQL的时区问题。本地数据库连不上,报错内容里带着The server time zone value...,原因就是MySQL 8的默认时区和驱动不一致。解决方法是连接串上加serverTimezone=Asia/Shanghai,这个前面配置里已经提到过,但部署时如果数据库是用Docker起的,还要保证宿主机和容器的时区设置正确。
第二个是上传头像404。本地测试一切正常,上了服务器之后,前端能上传成功,但访问图片地址一直404。排查下来发现是上传目录权限问题——应用在Linux服务器上运行时,写的目录是/data/college-info/avatar/,但这个目录不存在,而且启动用户也没有权限创建。后来我在部署文档里明确加了一步:先手动创建好目录并赋权,再启动jar包。
第三个是最隐蔽的:前端修改密码之后再次登录,提示密码正确但登录失败。查了半天发现是数据库里存的是BCrypt,但后端比较密码时用的却是普通equals。后来统一在Service层加了passwordEncoder.matches(rawPassword, encodedPassword)才解决。这个小问题提醒我,任何涉及到密码加密的方案,一定要在写入和校验两个环节用同一套算法,别混用。
部署完成之后,整个系统才算真正“闭环”了。从本地开发的便利性到生产环境的稳定性,每个环节都有可以优化的地方,但先把基础链路跑通,才是这类项目最务实的目标。
如果你也想做类似的系统,我的建议是不要一上来就追求把所有功能都堆上去。先完成“登录 -> 查看个人信息 -> 修改个人信息 -> 管理员维护用户”这条最小闭环,确认整条链路没有bug,再逐步加Excel导出、通知公告这些附加功能。我在实际调试中就发现,功能越多,模块之间的耦合越隐蔽,比如用户状态被禁用之后,他的Token还能不能用、已经上传的头像要不要清理,这些边界逻辑如果不在初期想清楚,后面改起来特别痛苦。最后再分享一个小技巧:开发过程中把数据库的初始化脚本和部署文档跟着代码一起维护,版本更新时顺手更新,等你要交付或者部署到新环境的时候,会省下大量沟通成本。
