1. 项目概述:这套系统解决的是什么问题
前后端分离的企业内管信息化系统,对很多开发者和企业来说并不陌生。我在实际接触过的项目里,十套内部管理系统有八套都是这个技术组合:SpringBoot 做后端服务,Vue 做前端页面,MyBatis 管数据库交互,MySQL 存业务数据。这套组合之所以成为事实标准,不是因为它最前沿,而是因为它最稳——生态成熟、资料多、招人容易、出问题能搜到答案。
这个项目标题里提到的"企业内管",指的就是企业内部管理,典型场景是人事管理、部门管理、员工信息维护、考勤记录、薪资核算、公告通知这类日常运营功能。它不是面向外部用户的电商或门户站点,而是给企业员工和管理层使用的内部工具。
我做过的内部管理系统项目不算少,从早期单体 JSP 到后来的前后端分离,踩过的坑基本都集中在几个地方:权限控制怎么做才不漏、数据库表结构怎么设计才不返工、跨域问题怎么处理才不折腾、部署上线时有哪些细节容易被忽略。这篇博文会把整个系统的设计思路、核心模块、实操代码和部署流程完整拆一遍,还会把我在真实项目中遇到的高频问题整理成排查清单,方便你直接参考。
适合读这篇内容的人有三类:一是正在做毕业设计的学生,需要一个完整可演示的项目;二是刚入行一到两年的 Java 开发,想看看一个标准的企业级项目内部到底是怎么组织的;三是小团队的技术负责人,想快速搭一套内部管理系统,需要现成的架构参考和部署方案。
2. 核心架构拆解:为什么选这套技术组合
2.1 前后端分离到底"分离"了什么
很多初学者对前后端分离的理解停留在"前端用一个项目,后端用另一个项目",这没错,但不够深入。真正的分离包含三个层面:代码工程分离、开发职责分离、部署运行分离。
代码工程分离指的是前端工程和后端工程各自独立维护,前端用 Vue CLI 或 Vite 创建项目,后端用 Spring Initializr 创建 Maven 工程,两边通过 HTTP 接口通信,而不是像传统 JSP 那样把页面塞进后端工程里。职责分离意味着前端工程师只管页面交互和数据展示,后端工程师只管业务逻辑和数据处理,两边靠接口文档对齐。部署运行分离则更加关键——前端构建出的静态文件可以部署在 Nginx,后端打包成 Jar 包独立运行,两者可以分开扩容,互不干扰。
这套系统的实用性就体现在这里:如果公司规模小,前端和后端可以部署在同一台服务器上;如果访问量上来,后端可以横向扩展多实例,前端只需要通过 Nginx 做反向代理即可。这种架构的弹性是单体应用很难做到的。
2.2 后端选 SpringBoot 的理由
SpringBoot 在 Java 后端领域的主导地位短期内不会动摇。它最大的价值是"约定大于配置",把 Spring 生态里繁琐的 XML 配置大幅简化,开发者在大多数场景下只需要写业务代码。
具体到这套内管系统,SpringBoot 提供的核心能力包括:
- 内嵌 Tomcat,打成一个可执行的 Jar 包,服务器上不需要单独安装 Tomcat
- starter 机制,引入一个依赖就能获得一组自动配置,比如引入 spring-boot-starter-web 就自动配好 SpringMVC
- 天然的依赖管理,父 POM 统一管控版本号,避免 jar 包冲突
- 配合 Spring Security 或 JWT 过滤器,在框架层就解决了接口鉴权的骨架问题
在实际开发中,SpringBoot 的启动类和配置文件结构非常清晰。主启动类用 @SpringBootApplication 注解标注,配置文件 application.yml 统一管理数据源、端口、日志等参数。整个工程按 Controller 层、Service 层、Mapper 层、Entity 层、Config 层分包,维护起来一目了然。
2.3 前端为什么用 Vue
Vue 在国内中小型项目里的渗透率非常高,原因很简单:入门曲线平缓,中文资料丰富,组件化开发思路直观。
这套系统的前端用 Vue + Element UI 组合。Element UI 是饿了么团队开源的一套桌面端组件库,表格、表单、弹窗、树形控件、分页器这些管理系统的"标配配件"全都现成,稍微改改样式就能用,能省掉大量重复的 CSS 和交互代码。
单页面应用的优势在这个系统里体现得很充分:员工信息维护、考勤管理、公告发布这些模块切换时不需要刷新页面,路由跳转由前端 router 控制,用户体验接近原生应用。配合 Vuex 做状态管理,登录用户的 token、用户信息、权限标识在页面刷新后也能快速恢复。
2.4 数据持久化:MyBatis 的适用边界
MyBatis 在 Java 面试里被问到"和 JPA 有什么区别"几乎是保留题目。落到实际项目,我的经验是:对于这种表结构清晰、查询条件多变的内部管理系统,MyBatis 的灵活性优势明显。
MyBatis 让你直接用 SQL 控制数据查询,优化空间完全掌握在自己手里。比如员工列表的模糊搜索、多条件组合过滤,在 Mapper XML 里写动态 SQL 是件很自然的事情。JPA 虽然能靠方法名推导查询,但复杂查询要么写 JPQL 要么写原生 SQL,反而多了一层转换成本。
当然,MyBatis 也有自己的问题,其中最典型的是"胶水代码"多。每个实体类都要写对应的 Mapper 接口和 XML 文件,表多了以后文件数量膨胀明显。解决思路是引入通用 Mapper 或 MyBatis-Plus,但这套系统为了保持技术栈纯粹,用的还是原生 MyBatis,后面会展示一个完整的 Mapper 写法。
2.5 数据库选型 MySQL 的必然性
企业内管系统的数据量通常不会特别大,日增记录以千级计算已经算活跃了。MySQL 在这种量级下性能完全够用,加上 InnoDB 引擎对事务的支持,保证了数据操作的一致性。
数据库设计上需要重点考虑的字段包括:员工表的主键、工号(唯一索引)、姓名、部门 ID 等;部门表通过 parent_id 自关联实现树形结构;考勤表用员工 ID 和日期做联合业务键,避免重复打卡记录。字符集统一用 utf8mb4,排序规则用 utf8mb4_general_ci,这能避免中文乱码以及 emoji 存储失败的尴尬。
3. 系统功能全景:企业内管信息系统的模块设计与数据库结构
3.1 功能模块总览
一套合格的企业内管系统,功能模块通常围绕"人"和"事"两条线展开。人是基础数据,事是流程操作。这个系统的模块设计如下表所示:
| 模块 | 核心功能 | 涉及表 |
|---|---|---|
| 登录认证 | 用户名密码校验、令牌签发验证、退出登录 | 用户表、角色表 |
| 用户管理 | 用户增删改查、角色分配、密码重置 | 用户表、角色表、用户角色关联表 |
| 部门管理 | 部门树展示、新增子部门、调整上级部门 | 部门表 |
| 员工管理 | 员工信息录入、条件查询、Excel 导入导出 | 员工表、部门表 |
| 考勤管理 | 上下班打卡、考勤记录查询、异常标记 | 考勤表 |
| 薪资管理 | 薪资项配置、月度薪资生成、发放记录 | 薪资表、薪资明细表 |
| 公告通知 | 公告发布、置顶、已读标记 | 公告表、已读记录表 |
这七个模块基本覆盖了中小型企业内部管理的日常需求。如果后续要扩展,比如增加 OA 审批流、会议室预订、物资领用,这套架构里加表和加接口的路径也很清晰。
3.2 数据库表结构设计思路
我按实际经验拆一张最核心的员工表,字段设计如下:
CREATE TABLE `employee` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `emp_no` varchar(32) NOT NULL COMMENT '工号', `name` varchar(64) NOT NULL COMMENT '姓名', `gender` tinyint(4) DEFAULT '1' COMMENT '性别 1男 2女', `dept_id` bigint(20) DEFAULT NULL COMMENT '所属部门ID', `position` varchar(64) DEFAULT NULL COMMENT '岗位', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(128) DEFAULT NULL COMMENT '邮箱', `entry_date` date DEFAULT NULL COMMENT '入职日期', `status` tinyint(4) DEFAULT '1' COMMENT '状态 1在职 2离职', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`), KEY `idx_dept_id` (`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';几个设计要点值得说明。第一,工号 emp_no 设唯一索引,既防止重复,也方便线下场景里用工号快速定位员工。第二,dept_id 只存部门主键,不冗余部门名称,避免部门改名后还要同步改员工表的麻烦。第三,create_time 和 update_time 由数据库自动维护,业务层不需要手动塞值。
这种表结构设计看起来简单,但恰恰是很多新手容易忽略的地方。我在评审别人项目时经常看到把部门名称直接存进员工表的做法,短时间确实省了一次关联查询,但后面部门调整、数据统计、报表输出的时候就会处处别扭。
3.3 权限模型:RBAC 的落地实现
权限控制是内部管理系统绕不开的重点。这个系统采用经典的 RBAC(基于角色的访问控制)模型,核心是"用户-角色-权限"三层结构。
用户表存储登录账号信息,角色表定义角色名称(如管理员、人事专员、普通员工),权限表定义具体的操作权限(如"员工导出"、"薪资修改")。用户和角色是多对多关系,角色和权限也是多对多关系,通过中间表关联。
前端控制体现在路由层面:用户登录后根据角色标识动态生成可访问的路由表,没权限的页面直接不注册。后端的控制落在拦截器和注解上,每个接口标注所需权限码,请求进来先校验 token 解析出的角色是否包含对应权限。前端控制提升体验,后端控制保证安全,两者缺一不可。
4. 后端核心实现:从登录认证到业务接口的完整串联
4.1 JWT 登录认证机制
内部管理系统的接口不能裸奔,每个请求都要能识别"谁在调用"。这个系统的做法是 JWT + 拦截器的组合。
用户提交用户名密码后,后端校验通过则签发一个 JWT token。token 的有效期设置为 2 小时,过期后前端携带 token 请求会收到 401 状态码,前端据此跳转登录页重新认证。JWT 的最大好处是服务端无状态,不需要在 Redis 里存会话记录,水平扩展时不需要考虑会话同步问题。
签发的核心代码使用 JJWT 库实现,逻辑并不复杂:
public String createToken(Long userId, String username, List<String> roles) { Date now = new Date(); Date expireDate = new Date(now.getTime() + 2 * 60 * 60 * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("roles", roles) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }注意:SECRET_KEY 在实际项目中必须放到配置文件里,通过环境变量注入,绝对不能硬编码在源码中。这个系统的源码为了方便演示写在了常量类里,部署到生产环境前一定要改掉。
拦截器负责解析请求头中的 token,校验签名和有效期,然后把解析出的用户信息放入 ThreadLocal,方便后续业务代码获取当前操作人。
4.2 统一返回结构与全局异常处理
前后端分离模式下,接口返回的数据结构必须统一,否则前端每次请求都要做兼容处理。这个系统定义了一个 Result 类:
public class Result<T> { private Integer code; private String message; private T data; }code 为 200 表示成功,500 表示业务异常,401 表示未认证,403 表示无权限。前端根据 code 做统一拦截处理,而不是每次请求都写一遍判断逻辑。
全局异常处理用 @RestControllerAdvice 注解实现,核心逻辑是捕获所有异常,返回统一的错误格式。这里有个我自己踩过的坑:异常处理里如果把堆栈信息直接返给前端,既不安全又把内部实现暴露了。正确的做法是异常信息记录到日志文件,前端只拿 message 字段展示。
4.3 MyBatis 动态 SQL 的典型写法
员工管理模块的列表查询是最典型的"多条件组合查询"场景:按姓名模糊搜索、按部门精确过滤、按状态下拉选择。这些条件可能全有、可能有部分、可能一个都没有,用 MyBatis 的动态 SQL 处理非常顺手。
Mapper XML 里的核心片段:
<select id="selectEmployeeList" resultType="com.example.entity.Employee"> SELECT * FROM employee <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND dept_id = #{deptId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY emp_no LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理第一个条件前的 AND 关键字,这是 MyBatis 很实用的细节。如果不用<where>而是直接写 WHERE 1=1 也能实现,但后者在 SQL 日志里看起来不够干净,而且多了一次无意义的条件判断,所以我更推荐前者。
分页这里用的是手动 LMIT 传参方式,没有引入 PageHelper 插件。对于这个系统的数据量来说完全够用,而且 SQL 的执行计划一眼就能看清,排查慢查询时少绕弯子。
4.4 文件导入导出:Excel 工具的实际运用
员工信息的批量导入和导出,是内部管理系统里一个看似简单却很容易翻车的功能。这个系统用 EasyExcel 实现,核心思路是先用注解定义 Excel 列和实体字段的映射关系,然后用几行代码完成读写。
导出时的样式控制容易出问题,比如表头加粗、列宽自适应、日期格式等。EasyExcel 提供了一些注解来配置,但更复杂的样式需要自定义拦截器。我的建议是:第一版先保证功能可用,样式问题放到后面迭代,不要在导出功能上花太多时间雕花。
导入的难点在于数据校验。Excel 里的工号如果和数据库已有数据重复,是跳过还是覆盖?手机号格式不对是拒绝整行还是只标记错误?这个系统的处理策略是逐行校验,将正确的数据写入数据库,错误的数据单独生成一个错误报告下载,告诉用户哪一行哪一列出错了。这个方案用户体验比"全部拒绝"友好得多。
4.5 定时任务:月度薪资自动生成的实现
薪资管理模块有一个很好展示 Spring 生态能力的点:定时任务。每月 1 日凌晨自动生成上个月的薪资记录,不需要人工点击生成按钮。
实现方式是在一个配置类上添加 @EnableScheduling 注解,然后在业务方法上使用 @Scheduled(cron = "0 0 1 1 * ?") 表达式。方法的逻辑是:查询上个月所有在职员工,按薪资配置项计算应发工资、扣除社保公积金、得出实发工资,然后批量插入薪资表。
这个功能看起来简单,但有一个细节必须处理好:定时任务不能重复执行。如果服务器重启或者任务被手动触发,重复生成会导致薪资数据翻倍。解决方案是加一个业务唯一约束,比如员工 ID + 薪资月份做联合唯一索引,插入时捕获 DuplicateKeyException 并跳过。
5. 前端工程实现:Vue 单页应用的模块化开发
5.1 前端工程结构与路由设计
前端工程的结构直接影响后续维护成本。这个系统的 frontend 目录划分如下:
src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex 状态管理 ├── utils/ # 工具函数(request.js 等) ├── views/ # 页面视图 │ ├── login/ │ ├── dashboard/ │ ├── employee/ │ ├── attendance/ │ ├── salary/ │ └── notice/ ├── App.vue └── main.jsrouter 里有一个核心处理逻辑:全局前置守卫。每次路由跳转前检查本地是否有 token,如果没有且目标页面不是登录页,就强制跳转到登录页。有 token 的情况下去服务端获取用户信息,动态注册路由,实现基于权限的菜单渲染和页面准入控制。
5.2 Axios 请求封装与拦截器
前端接口请求统一封装在 utils/request.js 里。核心配置包括:基础地址指向后端接口前缀、请求超时时间设置为 10 秒、请求拦截器自动在 header 里添加 token。
响应拦截器是整段封装里最关键的部分。处理逻辑如下:若响应体的 code 为 200,直接返回 data 给业务代码;若为 401,清除本地 token 并跳转登录页;若为 500 或业务错误码,弹出错误提示。这样业务代码里只需要关心数据获取和渲染,不需要处理鉴权失败和网络异常。
需要注意的一个细节是文件下载的处理。如果接口返回的是二进制流而不是 JSON,响应拦截器如果统一按 JSON 解析就会报错。实际做法是:在请求配置里单独标注 responseType: 'blob' 的请求不经过 JSON 解析逻辑,直接返回二进制数据给后续代码处理下载。
5.3 核心页面:员工列表的条件查询与表格渲染
员工管理页面是这套系统里最典型的"表格 + 查询"页面。页面上方是查询表单,包括姓名输入框、部门下拉树、状态选择器、查询和重置按钮;下方是数据表格,展示员工基本信息,每行提供编辑和删除操作;底部是分页器。
这里有一个非常实用的技巧:查询条件对象和分页对象作为响应式数据维护在 data 里,查询按钮触发查询方法时带上这些参数请求接口,重置按钮则把查询条件全部初始化并恢复第一页。表格的加载状态用 loading 变量控制,数据量小的时候体验差异不大,但数据量上来后这个细节能明显提升交互流畅度。
部门下拉树用的是 Element UI 的 el-cascader 组件,数据源是后端返回的部门树形结构。这里要处理的一个坑是:部门树的 value 值(即 id)类型必须与后端一致,否则回显选中时会出现类型不匹配的怪异问题。Java 返回的 Long 类型到前端 JSON 解析后如果是字符串,就要在组件绑定值处做好转换。
5.4 状态管理:Vuex 存储用户信息与权限
Vuex 在本项目里承担的是全局状态中心角色。用户登录成功后,后端返回 token 和用户基本信息。token 存入 localStorage(刷新不丢失),用户信息放入 Vuex。
权限控制的另一面在菜单渲染上。菜单不是静态写死的,而是根据用户角色动态生成路由表里的可访问项,再从路由信息推导出侧边栏菜单。这个方案实现起来并不复杂,但体验比"所有菜单都显示、点击后提示无权限"好上很多。
需要说明的是 Vuex 里存用户信息要注意刷新问题:localStorage 中的 token 还在,但内存中的 Vuex 状态已经清空了。解决方式是写一个初始化方法,在应用启动时检查 localStorage 是否有 token,有则重新请求用户信息接口回填状态。
6. 部署上线全流程:从本地环境到服务器运行
6.1 本地环境准备与参数配置
在开始部署之前,需要先准备好本地开发环境。JDK 建议使用 1.8 或 11,这两个版本对 SpringBoot 的兼容性最稳定。MySQL 使用 5.7 或 8.0 均可,需要注意 8.0 版本的驱动类名和连接 URL 与 5.7 有差异。
后端配置文件 application.yml 里最关键的两个部分是数据源信息和 MyBatis 配置。数据源的 URL 需要指定 useSSL=false、characterEncoding=utf8 等参数,否则可能出现连接警告或中文乱码。MyBatis 配置里要指定 mapper-locations 指向 XML 文件路径,同时开启驼峰命名映射,这样数据库的下划线字段可以自动映射到 Java 的驼峰属性。
前端本地开发时通过 Vite 或 Vue CLI 启动开发服务器,同时需要在 vite.config.js 里配置开发代理,把 /api 开头的请求转发到后端地址,这也是解决开发环境跨域问题的最常用方案。
6.2 前端构建与 Nginx 部署
前端的线上部署不是直接把源码扔到服务器上,而是需要先构建出静态文件。执行 npm run build 命令后,产物输出到 dist 目录。这个目录里的 HTML、JS、CSS 文件就是需要部署到服务器的全部内容。
Nginx 配置的核心块如下:
server { listen 80; server_name your-domain.com; root /opt/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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files 指令的作用是处理前端路由的 history 模式:当用户直接访问某个子路由地址时,Nginx 找不到对应的物理文件,就回退到 index.html,由前端路由接管页面渲染。如果不配置这一行,刷新页面就会报 404,这是部署前端项目最常踩的坑。
6.3 后端打包与进程启动
后端打包执行 mvn clean package 命令,产物是 target 目录下的 Jar 包。启动命令有两种方式:直接 java -jar 前台运行,或者用 nohup 后台运行并输出日志到指定文件。
我建议的生产部署方式是使用 nohup 配合重定向日志:
nohup java -jar inner-manage-system.jar --spring.profiles.active=prod > logs/app.log 2>&1 &--spring.profiles.active=prod 参数指定加载 application-prod.yml 配置文件,这样可以把生产环境的数据源密码、日志级别等参数与开发环境隔离。生产环境配置文件里的敏感信息,建议通过环境变量的方式覆盖,而不是直接写在文件里。
6.4 前后端联调与接口地址适配
部署完成后最容易出的问题是前端页面能打开,但数据加载不出来。排查的第一步就是打开浏览器开发者工具看网络请求:请求地址是相对路径 /api/login 还是绝对路径 http://ip:8080/api/login?
生产环境推荐使用相对路径,由 Nginx 把 /api 前缀的请求反向代理到后端服务。这样前后端只要部署在同一台服务器上就自动通了。如果前端代码里写死了开发环境的 localhost 地址,部署到生产环境必然请求失败。这个系统的前端接口基础地址就配置成了 /api,开发时靠 vite 代理转发,生产时靠 Nginx 代理转发,全流程不需要改代码。
7. 常见问题排查与避坑实录
7.1 数据库连接中文乱码
我几乎在每一个部署现场都会遇到一次乱码问题,原因五花八门:MySQL 数据库字符集不是 utf8mb4、连接 URL 没指定 characterEncoding=utf8、前端页面 meta 标签没声明 UTF-8,三者中任何一个没做到都会出现问题。
排查顺序建议是:先看数据库表的 charset,再检查连接 URL,最后查前端源码文件编码。按照这个顺序基本能在五分钟内定位问题。
7.2 跨域请求被拦截
开发环境中前后端分离,必然遇到跨域问题。处理方式有两种:后端写一个 CORS 配置类允许跨域请求,或者前端使用开发代理。生产环境因为有了 Nginx 反向代理,跨域问题自然消除。
后端 CORS 配置的代码非常简单,定义一个 WebMvcConfigurer 的 Bean 并重写 addCorsMappings 方法即可。需要说明的是,配置中的 allowedOriginPatterns 不要使用 *,因为部分浏览器对携带凭证的跨域请求不允许通配来源,实际开发中我通常会写明确的前端地址或者用环境变量控制。
7.3 刷新页面 404
这个问题的原因已经在前面的 Nginx 配置小节里详细说过。如果你的 Vue 项目使用了 History 路由模式(地址栏里没有 # 号),Nginx 必须配置 try_files 指令。如果不想改 Nginx 配置,也可以改用 Hash 模式路由(地址栏带 # 号),但我不推荐,因为 URL 不够美观,分享时还会多出无效字符。
7.4 Jar 包运行后端口被占用
启动 Jar 包时如果提示端口被占用,常见原因有两个:上一次启动的进程没有完全退出,或者服务器上其他服务占用了 8080 端口。排查命令是 netstat -tlnp | grep 8080 查看端口占用情况,确认是残留进程则 kill 掉,是其他服务占用则修改后端端口或者让运维协调。
7.5 定时任务重复执行
这个系统里的定时任务是单机部署方案。如果后续要把后端水平扩展到多个节点,那么 @Scheduled 注解的定时任务会在每个节点上都执行一遍,导致数据重复生成。解决方案有几种:引入分布式任务调度框架,或者使用数据库分布式锁。考虑到内管系统的数据量,一个基于 MySQL 唯一约束的幂等方案其实就能解决问题,不必上太重的框架。
8. 源码交付与二次开发建议
这套系统交付时除了前后端完整源码,还包含了数据库初始化脚本和部署文档。数据库脚本里不仅建了表,还预置了初始数据,包括一个管理员账号和几个演示角色,启动系统后可以直接登录体验完整流程。
如果你打算在这套源码的基础上做二次开发,我有几个具体的建议。第一,先跑通整个流程再改代码,不要上来就改权限逻辑,否则出了问题很难判断是环境问题还是代码问题。第二,扩展新业务模块时,复制一个现有模块的完整结构(Controller 到 Mapper 全套)再改,比从零开始写快得多,也不容易漏掉配置。第三,注意在新增表的时候,同步把数据库脚本更新到项目里的 sql 目录,避免团队成员各用各的数据库结构。
我个人在实际操作中的体会是,这种内管系统的代码质量是否过关,不取决于用了多新的技术,而是取决于基础的工程习惯是否到位。一个合格的开发者在交付这种项目时,至少要做到日志清晰到能顺着排查问题、异常处理不裸奔、数据库脚本和代码保持一致。这三点本系统在开发过程中都在刻意落实。
如果你打算拿这个项目作为面试作品,我建议你重点准备两个地方的讲解:一是 RBAC 权限模型从数据库到接口鉴权再到前端路由的完整链路,二是员工列表分页查询从 SQL 到前端表格渲染的完整链路。把这两条链路口述清楚,比背十个设计模式管用得多。
最后再分享一个小技巧:部署前把数据库脚本的初始化数据里,除了默认账号外再加几条真实感的演示数据,比如不同部门的员工各两条、考勤记录覆盖最近一周、公告有三条不同的状态。这样不管你是自己演示还是给面试官展示,系统打开就有内容可看,而不是一个空荡荡的界面。这个细节虽然不起眼,但给人的第一印象差异非常明显。