爱折腾 JavaWeb 的朋友应该都见过这类项目标题:基于 javaweb 和 mysql 的 springboot 办公 erp 管理系统,后面还挂着一串 java+springboot+easyui+maven+mysql+html。我第一次看到这种命名的时候,第一反应是"好家伙,技术栈全写在脸上了"。但仔细拆开看,这个项目其实是很多做毕业设计、课程设计,或者刚转 Spring Boot 的开发者非常典型的一条学习路径:用一套成熟稳妥的技术组合,去实现一个真实业务场景下的管理系统,而不是停留在单纯的增删改查 Demo。
这个项目本质上是一个办公自动化 + ERP 核心流程的整合系统,覆盖了企业里最常见的几类业务场景:部门人员管理、审批流转、公告通知、客户/供应商信息维护,可能还包含简单的进销存或合同台账。它能解决的问题是:让一个中小型公司把日常办公中散落在 Excel、微信、纸质单据里的信息,收敛到一个统一的 Web 系统里进行维护和查询。适合谁参考?我觉得是三类人:一是在校学生需要完成一个能答辩、能演示、能讲清楚设计思路的完整项目;二是刚入行不久、想搞明白 Spring Boot 整合传统前端框架(EasyUI)和 MySQL 全流程的开发者;三是确实有内部管理工具需求、但不想引入过于笨重的成熟 ERP 产品的小团队。
这个项目最有意思的地方在于,它正好踩在"前后端分离"和"传统服务端渲染"两代技术风格的交界处。EasyUI 这种老牌 jQuery UI 框架在今天看来交互不算时髦,但对一个管理系统来说,它的 datagrid、tree、layout 组件开箱即用、文档齐全、后端对接逻辑极其直白,学习曲线比 Vue + Element 那一套要平缓得多。当别人还在纠结跨域、token 刷新、路由守卫的时候,这个项目里只需要后端返回 JSON、前端用 EasyUI 组件消费数据就够了。对初学者来说,这种"少绕弯子"的体验,价值远高于技术栈本身的新旧。
下面我把这个项目从业务边界、数据库设计、工程搭建、核心功能实现到部署上线的完整链路,按照我自己做项目时的习惯拆开讲一遍,包括我在实际开发和教学过程中踩过的坑。
1. 业务边界与技术选型:为什么"办公 ERP"值得当做一个独立项目来做
1.1 办公 ERP 和管理系统到底差在哪
很多人觉得 ERP 是 SAP、Oracle 那种动辄几百张表、实施周期按年算的庞然大物,于是看到"办公 ERP 管理系统"这个名字就发怵。实际上,这个项目标题里的"办公 ERP",要求的是"覆盖企业核心办公与资源管理场景的轻量级系统",它不是让你去实现复杂的 MRP 物料需求计划、排产算法,而是把企业里高频、通用、可标准化流程的信息化需求落地。
我在给别人讲这个项目时,习惯先画一个业务象限:纵轴是"频率",横轴是"标准化程度"。办公自动化里最高频且最标准化的三件事是:组织架构管理(部门/员工)、事务审批(请假/报销/用章)、信息发布(公告/通知)。这三件事几乎是所有企业都有的,做完它们,系统就已经能"跑起来给人用"了。再往下扩展,就进入 ERP 的资源类模块:客户信息、供应商信息、商品/库存台账,以及围绕这些主数据产生的简单单据流。关键点是:“办公 ERP”的模块之间要有主从关系和数据流转,比如员工属于部门,报销单归属于员工,审批通过后影响某项业务状态——这种关联逻辑,正是它区别于"十个独立的 CRUD 页面拼凑成的管理后台"的核心。
提示:如果答辩或者向业务方介绍系统时,不用强调"我做了多少张表",而要强调"我的业务流程闭环了"。一张审批单从提交走到归档,中间涉及角色、状态、时间线,这才是 ERP 味道。
1.2 技术选型的选与不选:EasyUI 赢在"低摩擦"
我见过不少人在看到标题里的 EasyUI 时会有疑问:为什么不用 Vue?为什么不用 Element UI?这里我直接说结论:对于单人开发、工期紧张、以 Java 后端逻辑为核心(而非交互花样)的管理系统,EasyUI 的综合开发效率依然不低,尤其适合后端思维主导的开发者。
先看数据对接方式。Spring Boot 的 Controller 返回一个 Map 或自定义 Result 对象,里面放 total 和 rows 两个字段,EasyUI 的 datagrid 直接消费,不需要任何额外适配。如果你熟悉 MyBatis 的 PageHelper,一条PageHelper.startPage(pageNum, pageSize)语句之后,把 PageInfo 里取到的 total 和 list 塞进 Map,前端表格、分页条就全有了。这里的"无缝感"对新手极其友好——你不必理解 axios 封装、拦截器、v-model 双向绑定这些抽象概念,只需记住"我请求什么 URL,拿到什么 JSON,组件怎么渲染"。
再对比一下"前后端分离"的开发链路:前端工程 npm install、代理配置、构建部署,每个环节都可能拦住初学者一整天。而这个项目的 HTML 页面放在src/main/resources/static下,Spring Boot 启动后直接通过http://localhost:8080/index.html访问,页面里引 jQuery 和 EasyUI 的 CSS、JS 也是本地静态资源,整个前后端共享一个端口、一个进程、一套部署包,逻辑链路短、排错范围小。
当然,我不是说 EasyUI 就应该被永远使用。如果你已经在团队里用的是 Vue 3 + Element Plus,那自然不用为了这个项目倒退。但如果你是作为个人学习项目、需要快速交出完整成果,EasyUI 这套方案确实能帮你把精力集中在"业务逻辑"而不是"工程基建"上。
2. 数据库设计:把 ERP 的表关系理顺,项目就成了一半
2.1 核心表结构拆解:从主数据到单据流
这个项目的数据库通常叫erp_db,字符集用utf8mb4,排序规则utf8mb4_general_ci。表不多,一般控制在 15~25 张之间,但表与表之间的外键逻辑必须干净。我按信息的重要程度把表分成三层:
- 第一层是组织与人员主数据:
sys_department(部门表)、sys_user(用户表)、sys_role(角色表)、sys_user_role(用户角色关联表)、sys_menu(菜单表)、sys_role_menu(角色菜单关联表)。这几张表构成了系统的权限骨架。 - 第二层是办公业务数据:
oa_leave(请假单)、oa_reimbursement(报销单)、oa_notice(公告)、oa_audit_record(审批记录)。它们共同特点是都有一个状态字段,比如status从 0(待审批)到 1(通过)到 2(驳回),并有applicant_id、approver_id这类外键指向用户表。 - 第三层是 ERP 资源数据:
erp_customer(客户)、erp_supplier(供应商)、erp_product(商品/物料)、erp_stock_record(出入库流水)。到这一层时,建议考虑引入create_time、update_time、deleted这三个通用字段,后面查问题、做统计都方便。
在设计时有一个我特别想强调的惯例:不要为了追求纯粹的"第三范式"把所有表都拆到不能再拆。比如sys_user表里保留一个dept_name冗余字段,虽然不符合严格的范式设计,但你在列表页展示用户所属部门时,能少做一次表关联,对这个小项目来说性能差别不大、代码还能简化不少。这类"适度冗余"在 ERP 系统里很常见,是业务经验而不是设计缺陷。
2.2 逻辑删除、状态字段和时间字段的约定
ERP 系统和普通博客系统的最大区别在于:数据不允许物理删除。员工的离职记录、订单的作废记录,都需要保留在库里以便追溯。所以在每张业务表里,我建议都要有:
ALTER TABLE oa_leave ADD COLUMN deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-正常,1-删除';所有查询语句都默认带上AND deleted = 0。如果你用的是 MyBatis,可以把这条公共条件放到 XML 里的<sql>标签中统一引用,避免每个查询都手写一遍导致遗漏。
时间字段方面,统一用DATETIME类型,Java 侧用LocalDateTime映射,mybatis配置里加上jdbcType的映射:
mybatis: configuration: jdbc-type-for-null: 'NULL' map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置务必打开,否则create_time字段无法自动映射到createTime属性,你会被迫写一堆 resultMap,纯属浪费时间。
审批单的状态字段,我推荐用TINYINT而不是字符串,比如 0=待提交,1=审批中,2=通过,3=驳回,4=已撤销。状态不要超过 6 个,一旦超过,维护状态机转换逻辑时会比较痛苦。这个项目中审批链路是最有"系统感"的部分,状态设计得清楚,后面的列表筛选、统计报表都好写。
3. Maven 工程搭建与 Spring Boot 配置:环境对了,开发才不闹心
3.1 从零初始化工程的步骤和目录规划
这个项目用的是 Maven 做依赖管理和构建,Java 版本建议 1.8 或 11,Spring Boot 版本选 2.x 即可,不需要追新到 3.x,因为 3.x 要求 Jakarta EE 规范,一些老教程和老依赖会有兼容问题。下面是我习惯的初始化步骤:
- 用 Maven 的
mvn archetype:generate生成一个最基础的 Java 工程,或者直接在你的 IDE 里选择 Spring Initializr 生成,Group 填com.xxx,Artifact 填erp-system。 - 在
pom.xml里加入 Spring Boot 父依赖、web 启动器、MyBatis 启动器、MySQL 驱动、Druid 连接池、Lombok、PageHelper 分页插件。 - 按"controller / service / mapper / entity / common"五层建包。
我最终使用的pom.xml核心依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>这里有一个我在教学中反复强调的点:不要在一开始就引入太多依赖,比如 Spring Security、Redis、Quartz。能跑通一个最小闭环,再逐步加,否则新手遇到"启动了但界面打不开"的问题时,根本分不清是依赖冲突还是业务代码有问题。
3.2 配置文件里的三个"隐形地雷"
application.yml是这个项目最先可能坑到你的地方。我把高频踩坑列成了一张表:
| 配置项 | 容易写错的地方 | 正确示例 | 后果 |
|---|---|---|---|
| 数据库地址 | 漏掉serverTimezone参数 | jdbc:mysql://localhost:3306/erp_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8 | 时区报错或中文乱码 |
| 静态资源路径 | 误配了spring.mvc.static-path-pattern导致 404 | 保持默认/ **,或显式配/static/** | 页面样式和 JS 加载失败 |
| MyBatis mapper 扫描 | 漏写mapper-locations | mybatis.mapper-locations: classpath:mapper/*.xml | 报 Invalid bound statement 异常 |
时区问题的本质是 MySQL 驱动 8.x 默认要求明确时区,而服务器和数据库可能处于不同时区。我建议项目里强制统一使用Asia/Shanghai,并且数据库连接池 Druid 的initialSize设置为 5、maxActive设置为 20,避免并发测试时连接不够用。
静态资源这块多说一句。Spring Boot 默认将/static、/public、/resources、/META-INF/resources映射为根路径下的静态资源,所以你应该把index.html、easyui/、jquery/这些文件放在src/main/resources/static下。如果你硬要把页面放在WEB-INF/views那种传统 JavaWeb 目录下,反而要额外配置视图解析器,徒增复杂度。
4. 核心功能实现:从登录鉴权到 EasyUI 表格数据交互
4.1 登录、角色与菜单权限:用最务实的方式做权限控制
权限控制是所有管理系统的门面。这个项目不需要做到像 Spring Security 那么重,用拦截器 + 注解的轻量方案完全够用。具体做法是定义一个@RequirePermission("sys:user:list")注解,写一个拦截器实现HandlerInterceptor,在preHandle方法中:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; RequirePermission annotation = hm.getMethodAnnotation(RequirePermission.class); if (annotation != null) { String perm = annotation.value(); SysUser user = (SysUser) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } if (!user.getPermSet().contains(perm)) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}"); return false; } } } return true; }用户登录后,把该用户拥有的权限字符串集合(比如从sys_role_menu关联查出的菜单按钮权限)放进 user 对象里,缓存到 Session。以后在 Controller 方法上加注解,就实现了接口级别的权限控制。
菜单这块的数据加载也很套路:登录成功跳转到主页面时,发起一个请求getUserMenus,后端根据用户角色查询sys_menu表,按父子关系组装成 JSON 数组,前端用 EasyUI 的tree组件渲染左侧导航。需要注意的一个细节是:递归组装菜单的时候要检查children列表是否为空,不要返回null,否则前端遍历时会报错。
4.2 EasyUI datagrid 与后端的标准数据协议
EasyUI 的 datagrid 是所有列表页的核心组件。后端接口只要返回一个固定结构的 JSON 就能驱动表格展示。我最常用的返回结构是:
{ "total": 35, "rows": [ { "id": 1, "userName": "张三", "deptName": "技术部", "status": 1 } ] }Controller 的完整写法可以参考:
@RequestMapping("/list") @RequirePermission("sys:user:list") public Map<String, Object> list(SysUserQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<SysUser> users = sysUserService.queryList(query); PageInfo<SysUser> pageInfo = new PageInfo<>(users); Map<String, Object> result = new HashMap<>(); result.put("total", pageInfo.getTotal()); result.put("rows", pageInfo.getList()); return result; }前端页面里对应的是:
<table id="dg" class="easyui-datagrid" url="/sysUser/list" toolbar="#toolbar" pagination="true" fitColumns="true" rownumbers="true" singleSelect="true"> <thead> <tr> <th field="id" width="50">ID</th> <th field="userName" width="100">用户名</th> <th field="deptName" width="120">部门</th> <th field="status" width="60" formatter="formatStatus">状态</th> </tr> </thead> </table>这里我最想提醒的是:formatter 函数返回的 HTML 会被 EasyUI 直接插入单元格,如果你返回的内容里包含用户输入的字符串,必须做 HTML 转义,否则就埋下了存储型 XSS 的隐患。比如用户把部门名称改成<script>...</script>,列表加载时脚本就会执行。防御方法非常简单,写一个 escapeHtml 工具函数,在所有 formatter 里先转义再拼标签。
4.3 审批流:让"办公"名副其实的关键模块
审批流是办公 ERP 的加分项。实现一个"请假审批"的完整链路,既能体现业务深度,又不会复杂到难以收尾。我建议这样设计:
oa_leave表存储请假单主数据:applicant_id、leave_type、start_date、end_date、reason、status。oa_audit_record表存储每次审批操作:biz_type(如 leave)、biz_id、auditor_id、action(通过/驳回)、comment、audit_time。
后端接口分四个:提交申请、审批通过、审批驳回、查询我的审批记录。要点在于:
- 状态校验:只有当前状态是"审批中"的单子,审批通过接口才能执行,否则直接抛出业务异常。
- 数据一致性:通过审批时,开启事务,同时更新主表状态、插入审批记录。
- 归属校验:普通用户只能查到自己提交的单子,部门主管可以查本部门,管理员可以查全部。这个"数据权限"概念比按钮权限更能体现系统设计的层次感。
我第一次实现这个模块时,最大的失误是忘记处理"驳回后再次提交"的场景。后来我在 status 里加了一个状态 3(已驳回),并允许从 3 重新进入 1(审批中),同时把oa_audit_record里的历史记录完整保留,界面展示成一条时间线,业务逻辑才算闭环。
4.4 前端布局与弹窗表单:EasyUI 的日常操作流
主界面用 EasyUI 的layout组件,划分成 north(顶部标题栏 + 用户信息)、west(菜单树)、center(标签页/数据表格)。这是一个非常经典的后台布局,代码量不大但效果专业。
弹窗表单是另一个高频操作。点击"新增"按钮,打开一个dialog,内部放一个form,提交时走 AJAX:
$('#dlg').dialog('open').dialog('setTitle', '新增用户'); $('#fm').form('clear'); $('#fm').form('load', row); // 编辑时回显保存时:
$.ajax({ url: '/sysUser/save', type: 'POST', data: $('#fm').serialize(), dataType: 'json', success: function(res) { if (res.code === 200) { $('#dlg').dialog('close'); $('#dg').datagrid('reload'); } else { $.messager.alert('提示', res.msg, 'error'); } } });这套写法非常"传统",但它简单、直接、可预期。我需要提醒的是:表单提交前先做后端校验,不要只依赖前端。EasyUI 的validatebox只是体验层,真正的非空、长度、唯一性校验必须在 Controller/Service 里做。我在某次开发中由于懒惰,只在前端校验了用户名非空,结果有人绕过页面直接 POST 请求,创建了一堆空用户名数据,清库清了半天。
5. 打包部署与常见报错:我把每个人都会碰到的坑提前踩给你看
5.1 jar 包还是 war 包
这个项目推荐直接打包成可执行的 jar 包运行。Spring Boot 内置 Tomcat,意味着你不需要在服务器上额外装 Tomcat,java -jar erp-system.jar一条命令就能启动。如果你一定要部署到外部 Tomcat 的 webapps 目录下,则需要在启动类继承SpringBootServletInitializer并重写configure方法,同时把pom.xml的打包方式改为 war。
我在部署时常用的启动命令是:
nohup java -Xms256m -Xmx512m -jar erp-system.jar --spring.profiles.active=prod > erp.log 2>&1 &nohup和&是为了让进程在 SSH 断开后继续运行,--spring.profiles.active=prod是为了切换到生产环境的配置。生产环境的application-prod.yml里,数据库密码不要写明文,可以用环境变量注入,比如:
password: ${ERP_DB_PASSWORD}5.2 时区问题、连接超时和内存溢出
这些都算部署期高频问题。时区问题前面提过,在 JDBC URL 上加serverTimezone=Asia/Shanghai就好。连接超时则要留意 Druid 连接池的validationQuery配置:
spring: datasource: druid: validation-query: SELECT 1 test-while-idle: true如果不配置,数据库空闲一段时间后回收连接时可能报Communications link failure,表现就是页面偶尔白屏、后端日志有异常。内存溢出的问题,大多是查询列表时没有分页、一次性把全表数据加载到内存导致的。所以坚持使用 PageHelper、坚持列表页强制传 pageNum 和 pageSize,是对自己好,也是对服务器好。
5.3 MockMvc 和接口自测:不要等前端联调才发现问题
最后补一个开发习惯层面的建议。因为 EasyUI 前端代码比较"直给",你很可能在写完 Controller 后自我感觉良好,但真正点击页面时才发现字段名对不上、日期格式渲染不对。我建议在开发过程中就用spring-boot-starter-test里的 MockMvc 做接口自测,比如验证/sysUser/list返回的 JSON 里total字段是否大于 0、status格式化是否符合预期。哪怕只是最基本的status().isOk()、jsonPath("$.total").value(10),都能在十分钟内发现一半以上的低级错误,减少反复刷新浏览器调试的挫败感。
提示:如果接口没报错但页面表格空白,先用浏览器直接访问该 HTTP 接口看看 JSON 返回,再查看浏览器控制台有没有 JS 报错。90% 的情况是字段名拼写不一致,后端的
userId和前端 field 写的user_id,永远匹配不上。
写到这里,我个人的体会是:这个项目真正值钱的地方并不在于某一项技术用得多深,而在于它把"工程化落地"这件事完整走通了。从数据库建模到权限控制,从列表分页到审批流转,每个环节都是实际企业系统中会被反复使用的套路。你在完成它的过程中积累下来的表设计思路、接口约定意识、前端组件对接方法,换到别的业务场景(比如图书管理系统、设备管理系统)也能直接平移。最后再分享一个小技巧:把 EasyUI 的通用操作(如弹窗新增、编辑回显、删除刷新)封装成一组公共 JS 函数,后续每增加一个功能模块,主页面代码能减少一半——这种"造轮子"积累,才是做完一个项目后最实在的收获。