☰
SpringBoot+Vue学院个人信息管理系统开发实践全解
2026/10/12 2:46:47 网站建设 项目流程

最近把手上这套基于SpringBoot+Vue的学院个人信息管理系统完整梳理了一遍,正好把踩过的坑和沉淀下来的方案整理成一篇长文。很多朋友在做这类系统时,第一反应都是“不就是个信息维护页面嘛”,但真正开发时才发现,角色划分、数据审核状态、字段可变范围、报表导出格式,随便哪一项都能让你改上好几轮。这套系统的目标用户是学院里的学生、辅导员和院系管理员:学生在系统里维护自己的基础信息、学习经历、证书和奖惩材料;辅导员负责审核和统计;院系管理员主导角色分配、数据导出和异常数据修复。后端用SpringBoot+MyBatis提供接口,MySQL负责存储,Vue做前端交互界面。想拿来做课程设计、毕业设计,或者准备进入Java开发岗位练手的同学,这篇内容应该都有参考价值。

全套流程走下来,我的一个整体感受是:技术本身不复杂,但“业务边界”比想象中要麻烦。下面我从需求拆解、技术选型、数据库设计、后端接口、前端页面、部署排错六个方面,尽量把细节讲到能直接复用的程度。

1. 功能需求与页面结构拆解

1.1 需求不是“做一张表”这么简单

刚开始接触这个项目时,最容易被表面需求带偏,以为只要实现几个常规的增删改查操作就能交付。真实情况是,不同角色的使用者对“信息管理”的理解完全不一样,如果只用一套逻辑去覆盖所有场景,上线之后一定会被用户吐槽。

学生端关心的只有“我的数据”能不能改、改起来方不方便。比如手机号变更、紧急联系人调整、证书图片补充,这些高频操作要是藏在三级菜单后面,基本不会有学生愿意用。辅导员端关心的是能不能快速筛选出某个班级里信息不完整的同学,以及学生提交的证书、经历是否属实,审核操作不能太繁琐。院系管理员则更关心数据整体质量,比如年级人数统计、导出报表字段是否齐全、账号是否能批量导入、异常数据如何追踪。

所以在动手写代码之前,我先把角色和权限矩阵画了出来。学生能修改的字段集中在联系电话、住址、紧急联系人、证书图片、个人简介等;辅导员能修改的是审核状态、班级备注;管理员拥有全量数据的管理和导出权限。这个过程看似繁杂,实际上决定了后面数据库字段能不能撑住业务变化。前期取舍做得越清楚,后期返工的概率就越低。

1.2 功能模块按“流程”而不是按“页面”拆分

我习惯把功能拆成四个主模块:基础信息、学习经历、证书材料、奖惩记录,再外加一个系统管理模块。表面上看都是信息的增删改查,但内部有一条审核流程在贯穿。

我做了一个简单的模块对照表:

模块学生端功能辅导员/教师端功能院系管理员功能
基础信息查看和修改本人资料查看班级成员资料、核对全量查询、导出、修复数据
学习经历新增课程、补录经历审核经历状态汇总统计、异常清理
证书材料上传图片/附件在线预览、核验删除异常材料
奖惩记录查看本人记录录入本班记录复核、批量导入
系统管理修改登录密码查看操作日志分配账号、重置密码

看到这个表你可能会发现,它不只是简单的单表CRUD,而是带状态流转的迷你工作流。比如学生提交一条证书记录后,默认状态是“待审核”,辅导员审核通过后变成“已通过”,进入“已通过”状态后,学生就不能再随意修改,如果材料有问题则会被驳回并重新编辑。

状态流转的实现方式不算复杂,数据库里用一个status字段控制,前端根据状态值控制按钮显隐,后端在Service层再做一次校验。这个设计带来的实际体验提升非常明显:过去改信息靠口头通知,改完老师也不知道;现在一切数据变更都有“待审核”缓冲,责任边界很清晰,辅导员每天处理待审核项也就十来分钟。

1.3 非功能性需求同样值得提前定

这类系统虽然规模不大,但并发情况还是要去想的。开学季、选课周、毕业审核这几个节点会出现集中访问,虽然不至于打到高并发级别,但数据库连接池、接口超时这些参数最好提前调优,避免页面转圈。另外,数据导出是一个容易被忽略的高频功能,学生名单、获奖名单、毕业审核名单都会需要导出Excel,前端点击导出后如果后端接口处理时间偏长,会出现请求超时的错觉,需要配合异步任务或者调整超时时间。

2. 技术选型与整体架构

2.1 技术栈定型和理由

技术选型这块我要承认,确实没有太多花活,甚至可以称得上“标准答案”。SpringBoot负责接口层和自动化配置,相比传统SSM繁琐的XML配置体验好太多;MyBatis做SQL操控非常灵活,尤其遇到连表查询、条件筛选、分组统计时,直接写SQL比JPA这类全自动ORM更容易掌控;Vue做后台管理页面够轻,配合成熟的组件库能省掉大量时间;MySQL则是团队里最不需要解释的选项,免费、稳定、部署简单。

为什么不用Spring Cloud或者微服务那套?因为这套应用的服务规模根本撑不起来微服务的成本。引入注册中心、配置中心、网关这些组件反而把问题复杂化。杀鸡用牛刀在某些场景是炫技,在交付型的项目里就是给自己找麻烦。

MyBatis可能是这套技术栈中争议比较大的一个选择。现在很多新项目更愿意用MyBatis-Plus或者Spring Data JPA,但我在这个项目里坚持用原生MyBatis,原因是当前的表结构不算复杂,但筛选逻辑比较细碎。原生MyBatis的动态SQL能让条件筛选逻辑一目了然,排查问题也更直接,换个框架反而多一层封装要学习。当然,如果项目里单表操作量极大,用MyBatis-Plus确实能提高开发效率,这需要根据团队习惯来权衡,没有绝对的对错。

2.2 项目目录结构与请求链路

后端我采用标准的Controller-Service-Mapper三层结构,再加上entity、dto、vo、common、config几个包。common里放统一返回结果、异常处理、工具类,config里放跨域配置、拦截器配置。

src/main/java/com/xx/system ├── controller ├── service │ ├── impl ├── mapper ├── entity ├── dto ├── vo ├── common ├── config └── utils src/main/resources ├── mapper └── application.yml

前端Vue工程使用Vite作为构建工具,比Webpack的启动速度快了不少。页面放在views目录,组件放在components目录,api目录统一管理接口请求,router管理路由,store管理登录状态。

请求链路其实很清晰:浏览器访问页面后,前端路由匹配对应组件,组件在生命周期里调用API模块封装好的Axios请求,请求到达后端Controller,Controller校验入参后交给Service层处理业务逻辑,Service调用Mapper,Mapper通过XML或注解执行SQL,最终数据以JSON格式返回前端渲染。

前后端分离架构带来的最大好处是联调和部署都解耦了。后端只需要保证接口稳定,前端可以按页面进度独立开发;部署时也可以将后端打包成JAR,前端构建成静态文件用Nginx托管,再通过代理把接口请求转发给后端服务。

2.3 为什么把异常处理放到架构层面

不少初学者写代码时习惯在每个Controller方法里做try-catch,每个方法返回不同结构的JSON,结果是前端调用每个接口都要写不同的解析逻辑,维护成本极高。我把异常处理统一到全局层,Controller只关注业务逻辑,业务异常通过抛出RuntimeException处理,全局异常处理器统一捕获并封装成固定格式返回。

这样做的核心原因很简单:接口调用方最怕接口返回结构不稳定。统一出参格式后,前端封装Axios全局处理异常,后端排查日志也方便。这个习惯值得从学生阶段就养成,因为后面做任何跨端接口对接都会受益。

3. 数据库设计与核心表结构

3.1 数据库设计先从使用场景出发

刚开始设计表结构时,最容易出现的问题是“单表包打天下”,把用户账号、学生详细信息、教师信息全部塞进一张大表。这种设计不是不能用,但面对不同角色有不同扩展字段时会异常痛苦。比如学生有学号、班级、宿舍、辅导员ID,教师有工号、职称、研究方向,这些字段塞在同一张表里,非相关字段全是NULL,查询和索引效率都不理想。

我最终拆成了这样几类表:系统用户表存放登录账号和角色类型;学生信息表存放学生扩展信息;教师信息表存放教师扩展信息;业务信息表分别存放学习经历、证书材料、奖惩记录;操作日志表记录重要操作。各表之间用user_id做关联,既能保证数据一致性,又不会让单表字段过多。

设计数据库时还要注意软删除的问题。信息管理系统里难免有管理员误删数据的场景,直接物理删除会让数据和日志都失去痕迹。我在业务表上都加了deleted字段,删除操作实际是逻辑删除,默认查询条件里过滤掉已经标记删除的记录。这样处理起来增加不了多少代码量,但数据安全性提升明显。

3.2 核心建表语句与字段说明

用户表是整套系统的基础,字段不需要多,但约束要清晰。密码存的是加密后的值,绝不允许明文落库。

CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_type` tinyint NOT NULL DEFAULT 3 COMMENT '1管理员 2教师 3学生', `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT 'BCrypt加密后的密码', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` tinyint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

用户表的索引设计上,账号做了唯一索引,因为登录时第一步就是按账号查询,这个索引能大幅提升查询速度。角色类型status在这种量级的数据下不需要单独建索引,查询频率不高。

学生信息表是业务查询最频繁的表,字段相对丰富。重点关注的是班级名和学生编号,这两个字段在未来报表统计和筛选导出中会经常作为查询条件。

CREATE TABLE `student_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '关联sys_user', `student_no` varchar(30) NOT NULL COMMENT '学号', `real_name` varchar(50) NOT NULL COMMENT '姓名', `class_name` varchar(50) DEFAULT NULL COMMENT '班级', `gender` tinyint DEFAULT 0 COMMENT '0未知 1男 2女', `phone` varchar(20) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `id_card` varchar(20) DEFAULT NULL COMMENT '证件号', `political_status` varchar(20) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, `emergency_contact` varchar(50) DEFAULT NULL, `emergency_phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` tinyint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`), KEY `idx_class_name` (`class_name`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息表';

这里有个容易被忽略的细节:证件号这类敏感字段虽然需要存,但前端列表页面没有必要展示完整号码,尤其是导出Excel给学生时更要做脱敏处理。可以在查询SQL里直接用函数截断显示,也可以在VO层做处理,总之不要太随意地展示全部敏感信息。

业务表的逻辑以证书材料表为例。这张表要管理文件路径、证书名称、获得时间、审核人、审核状态,并且要能和学生关联。

CREATE TABLE `certificate_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '所属学生', `cert_name` varchar(100) NOT NULL COMMENT '证书名称', `cert_level` varchar(50) DEFAULT NULL COMMENT '级别', `cert_date` date DEFAULT NULL COMMENT '获得日期', `file_path` varchar(255) DEFAULT NULL COMMENT '附件路径', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待审核 1已通过 2已驳回', `audit_remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` tinyint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='证书材料表';

很多人在设计审核功能时会忽略audit_remark字段,但实际操作过程中,辅导员一定会有驳回操作,如果不记录驳回原因,学生根本不知道哪里有问题。加上这个字段,在驳回邮件或站内通知里可以直接展示,能省掉大量反复沟通的时间。

数据库中的外键约束我基本没有使用,原因是大数据量场景外键会影响写入性能,而且业务层已经做了关联校验。但关联字段的索引一定要建,尤其是user_id这种高频关联字段,否则连表查询到后期就是灾难。

3.3 时间字段统一与数据规范

时间字段统一使用datetime类型,Java端使用LocalDateTime对应。别用timestamp,因为timestamp只支持到2038年的时间范围,看着用不上,但一旦涉及历史归档数据就会有隐患。所有表统一带create_time和update_time字段,更新时由后端统一填充,不在业务代码里到处手动set。字符串字段全部使用utf8mb4字符集,因为某些生僻字、emoji符号在utf8mb4下才能正常存储,等前端表单里出现输入特殊字符乱码时再改,就很被动了。

4. 后端核心实现与接口设计

4.1 统一返回结果与全局异常处理

后端接口设计是所有前端交互的基础。我定义了一个通用的响应结构,所有接口都返回统一的对象。

public class ApiResponse<T> { private int code; private String message; private T data; public static <T> ApiResponse<T> ok(T data) { ApiResponse<T> resp = new ApiResponse<>(); resp.setCode(200); resp.setMessage("success"); resp.setData(data); return resp; } public static <T> ApiResponse<T> error(int code, String message) { ApiResponse<T> resp = new ApiResponse<>(); resp.setCode(code); resp.setMessage(message); return resp; } }

前端拿到这个结构后,只需要判断code === 200然后再处理数据。业务异常则通过自定义异常类统一抛出,全局异常处理器捕获并转换。

这个设计的好处体现在两个地方:第一,Controller代码非常干净,不需要每个方法重复写异常捕获;第二,前端可以全局处理会话过期、权限不足这类通用异常,不用在每次请求逻辑里到处判断。我是强烈建议从项目一开始就定好这套结构,不然接口写多了之后你再想改,成本会陡增。

4.2 登录鉴权与权限控制

系统里有三种角色,每个接口都要确认访问者身份及权限。常用的方案是JWT配合拦截器实现。

登录成功后,后端生成Token并返回给前端,前端把Token存到localStorage里,之后每次请求都在请求头携带。拦截器负责校验Token的有效性,并把当前用户信息放到ThreadLocal中,后续业务代码可以直接获取当前登录用户。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); } }

拦截器里做的事情不复杂:解析请求头里的Token,如果为空或过期,直接返回401状态码。解析成功则继续放行。

权限控制这里容易犯的错误,是只在前端做了按钮级“伪隐藏”,后端接口没有做拦截。实际上后端永远不能信任前端传来的任何字段,尤其是权限必须依赖后端判断。比如学生调用删除证书接口,如果只靠传参来决定删除哪一条记录,攻击者完全可以把自己user_id换成别人的user_id,把别人的记录删掉。正确做法是从Token中获取当前登录用户ID,所有操作都校验该ID与数据归属ID是否一致。

基于注解的权限校验在这个项目里属于锦上添花,我实现了一个简单的@RequireRole注解,在Controller方法上标注允许访问的角色,通过AOP解析并校验。校区级项目使用这种轻量方案就足够了,没必要引入专门的权限框架,省得学习成本和配置成本反而比业务代码更高。

4.3 MyBatis Mapper 与动态SQL

MyBatis最打动我的地方是动态SQL。学生列表页的筛选条件极其常见:按学号模糊搜、按班级精确选、按姓名模糊搜、按状态过滤。写动态SQL比在Java代码里拼字符串优雅得多。Mapper接口我通常写成这样:

public interface StudentInfoMapper { List<StudentInfoVO> selectStudentList(@Param("query") StudentQuery query); }

对应的XML里用<where>配合<if>动态拼接条件:

<select id="selectStudentList" resultType="com.xx.vo.StudentInfoVO"> SELECT si.*, u.user_type FROM student_info si LEFT JOIN sys_user u ON si.user_id = u.id <where> <if test="query.studentNo != null and query.studentNo != ''"> AND si.student_no LIKE CONCAT('%', #{query.studentNo}, '%') </if> <if test="query.className != null and query.className != ''"> AND si.class_name = #{query.className} </if> <if test="query.realName != null and query.realName != ''"> AND si.real_name LIKE CONCAT('%', #{query.realName}, '%') </if> </where> ORDER BY si.update_time DESC </select>

这里有三个细节值得说。第一,条件判断尽量用CONCAT('%', #{query.studentNo}, '%'),不要写%${query.studentNo}%,前者用预编译占位符防SQL注入,后者直接字符串拼接有注入风险。第二,<where>标签会自动去掉第一个多余的AND,比手动写WHERE 1=1再加条件更规范。第三,连表查询字段多时,不要用SELECT *,最好用VO明确列出返回字段,省得把不必要的大字段也查出来影响性能。

关联查询里的LEFT JOIN sys_user经常被忽略,其实很多页面需要在学生信息旁边展示账号状态。如果提前就在VO里设计了账号状态字段,一次查询就能拿全,不用二次查库,体验差别很大。

4.4 文件上传与数据导出实现

证书材料会涉及图片和附件上传。上传功能看似简单,实际需要考虑文件大小限制、文件类型校验、存储路径规划、静态资源映射等问题。

我在application.yml里设置了允许上传的文件大小上限:

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB

文件上传接口接收MultipartFile,校验扩展名和大小后,将文件保存到服务器指定目录,文件名用UUID重命名,避免中文文件名和重名问题。数据库里只存相对路径,访问时通过静态资源映射暴露出来。存储路径一定不要放到项目classpath内部,因为打包成JAR后每次重启可能找不到写的文件,或者文件会随部署版本丢失,这是一个很隐蔽的坑。

数据导出我采用了简单的POI实现。导出学生名单、证书名单到Excel,在后端生成临时文件,返回给前端下载链接。导出逻辑要留意数据量,如果一次导出全量的几万条数据,建议加一个异步处理的接口,前端轮询导出状态,而不是让请求一直挂着。

5. Vue前端实现要点

5.1 工程搭建与路由权限控制

前端Vue工程选择Vite作为构建工具,页面组件使用Vue3的组合式API。搭配Element-Plus作为UI组件库,开发后台管理类的界面效率非常高,尤其是表格、表单、弹窗这些高频组件,基本都是现成的,只需要做数据绑定和事件处理。

路由设计上,我按模块划分,每个模块对应一个懒加载页面。路由元信息meta里配置了requiresAuth和role,全局前置守卫里判断登录状态和角色权限。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') return } if (to.meta.role) { const userType = Number(localStorage.getItem('userType')) if (to.meta.role.includes(userType)) { next() } else { next('/403') } return } next() })

页面跳转时如果路由权限不匹配,直接引导到403页面,比页面加载完再去判断更顺滑。登录状态我用localStorage存Token和用户角色,刷新页面后状态不丢失。当然,更安全的做法是用Pinia管理用户状态,初始化时从后端获取一遍用户信息,能避免本地存储的角色信息被篡改,但学院内部系统通常用简化方案也能接受。

5.2 Axios请求封装与响应拦截

Axios封装看起来平凡,实际体验影响很大。我在utils/request.js里创建了一个Axios实例,设置基础路径和请求超时时间。请求拦截器里统一在header中带上Token,响应拦截器里统一处理返回码。

const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('请求失败,请稍后重试') return Promise.reject(error) } )

所有接口请求统一从src/api目录导出,页面组件只负责调用函数,不关心请求细节。例如学生信息接口:

export function getStudentList(params) { return service.get('/student/page', { params }) }

好处很明显:后端接口路径一旦调整,只需要修改API模块一处,页面组件完全不受影响。对于这种全校通用的管理系统,API统一封装后,前端维护成本降得很低。

5.3 关键页面实现细节

学生信息编辑页是最常被吐槽的页面。很多表单让用户填写一堆字段,实际上手机号、邮箱这种字段当然可以开放编辑,但学号、姓名这类核心字段应该默认只读,如果确需变更,应该在系统管理模块走申请流程,而不是让学生自助修改。前端做字段级只读不仅可以通过表单禁用实现,还可以根据角色ID动态渲染,比如学生角色看到的姓名输入框是禁用的,管理员看到的姓名输入框是可编辑的。

证书材料上传页需要处理图片回显。后端返回文件相对路径后,前端拼接下载或预览地址。一个容易被忽视的点是,使用Element-Plus的upload组件时,文件回显需要配置file-list属性,并且编辑时不能直接把数据库里的路径塞给组件,要转换成组件要求的对象结构。

列表页的分页和查询条件联动,我习惯把查询参数和分页参数合并到一个响应式对象里,每次点击查询按钮时重置page为1,再加载数据。这样可以避免停留在第10页时筛选数据,筛选结果只有67条,但列表还显示在第10页造成空白。

6. 联调部署与高频问题排查

6.1 跨域和代理配置问题

前后端分离开发时,跨域是最先遇到的问题。开发阶段可以通过后端配置跨域支持解决,但更推荐方式是通过Vite的代理配置,前端访问/api开头接口时把请求转发到后端地址。

// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样开发环境下前端页面和服务端接口都在同一域名下,不会产生跨域问题。部署到生产环境后,用Nginx代理同样处理:

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

如果后端直接开启跨域配置,需要注意允许的来源不能写*,因为跨域携带Cookie时,浏览器会拒绝*,必须指定明确来源域名。

6.2 数据库和MyBatis高频踩坑

MyBatis配置和MySQL之间最容易出问题的资料点,集中在Java类型和数据库类型的映射上。使用MyBatis时,Java的LocalDateTime跟MySQL的datetime类型一般没问题,但需要注意LocalDate要映射到date类型。如果Java字段定义为LocalDateTime,查询结果里对应数据库字段却是date,虽然不会报错,但时间会变成零点,比较隐患。

另一个高频坑是#{字段}和${字段}混用。动态排序时比如ORDER BY ${orderBy},因为排序字段不能预编译,只能使用字符串拼接,但需要严格控制传入的值。我把排序字段写成一个白名单Map,前端传入排序key,后端根据key映射到真实的数据库字段名,绝不直接拼接用户传值。

数据库层面还有一个问题是SQL模式。MySQL的ONLY_FULL_GROUP_BY模式默认开启后,使用GROUP BY查询时,SELECT字段必须都在聚合函数里或包含在GROUP BY中。做统计报表时很容易踩到这个报错,需要把字段加入GROUP BY,或者使用ANY_VALUE函数处理。

6.3 时间格式和JSON序列化问题

接口返回的时间字段格式如果不统一,前端显示会出现各种奇怪格式。我习惯在后端全局统一配置Jackson的日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

但要注意,date-format对LocalDateTime并不总是生效。更靠谱的做法是在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解,或者统一在VO层做格式转换。

前端拿到时间字符串直接展示,没有时区问题。如果直接返回LocalDateTime默认序列化的数组形式或者带T的ISO格式,前端就必须自己转换,业务代码里就会到处写处理逻辑。

6.4 打包部署与运维注意

后端使用Maven打JAR包,打包时测试配置和正式配置分离,通过profile指定环境。前端执行构建命令产出dist目录,再用Nginx托管静态文件。

SpringBoot的JAR包启动命令、MySQL连接配置、文件上传目录这些都是部署后最先要检查的点。我在部署时踩过一个大坑:上传目录没有从默认的相对路径改成绝对路径,结果每次重新部署,历史上传的证书文件就变成无法访问的死链。文件存储路径尽量配置成外部独立目录,数据库里只存相对路径,这样做迁移或升级部署时不会丢失文件。

JVM参数也需要留意,学院服务器通常配置不高,我一般设置-Xms256m -Xmx512m控制内存占用。MySQL连接池配置使用HikariCP,默认参数还算合理,但如果并发查询统计报表频繁,可以适当调大maximum-pool-size。

6.5 关于这套项目,我个人的几点体会

做完这个系统,我对“管理系统”这类项目有了更实际的认知。技术层面积累的东西其实有限,真正值钱的是对业务场景的洞察。比如学生信息管理,表面是数据库表设计,内里是“谁有权限改”、“改了之后谁负责审核”、“数据不一致时怎么处理”这些管理问题。代码只是把这些规则落地了而已。

第一次做这类系统时,我尽量减少接口的数量,一个接口能返回全量数据就直接返回。但实习一段时间后我发现,接口返回字段过多会让前端处理变得复杂,也会让数据传输变得臃肿。正确做法是尽量按页面需要裁剪字段,用VO或DTO隔离数据库实体与前后端交互结构,而不是让数据库实体直接暴露给前端。

最后想分享一个实用小技巧:在开发阶段可以给后端接口统一加一个简单的日志切面,打印每个请求的路径、耗时、操作人。这套系统维护的时候,排查操作日志、数据变更来源会非常依赖这个日志切面。日志也别只在本地打印,服务器上搞一个轮转日志文件,保证排错时有据可查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询