爱折腾 JavaWeb 的朋友应该都见过这类项目标题基于 javaweb 和 mysql 的 springboot 办公 erp 管理系统后面还挂着一串 javaspringbooteasyuimavenmysqlhtml。我第一次看到这种命名的时候第一反应是好家伙技术栈全写在脸上了。但仔细拆开看这个项目其实是很多做毕业设计、课程设计或者刚转 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 或 11Spring Boot 版本选 2.x 即可不需要追新到 3.x因为 3.x 要求 Jakarta EE 规范一些老教程和老依赖会有兼容问题。下面是我习惯的初始化步骤用 Maven 的mvn archetype:generate生成一个最基础的 Java 工程或者直接在你的 IDE 里选择 Spring Initializr 生成Group 填com.xxxArtifact 填erp-system。在pom.xml里加入 Spring Boot 父依赖、web 启动器、MyBatis 启动器、MySQL 驱动、Druid 连接池、Lombok、PageHelper 分页插件。按controller / service / mapper / entity / common五层建包。我最终使用的pom.xml核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies这里有一个我在教学中反复强调的点不要在一开始就引入太多依赖比如 Spring Security、Redis、Quartz。能跑通一个最小闭环再逐步加否则新手遇到启动了但界面打不开的问题时根本分不清是依赖冲突还是业务代码有问题。3.2 配置文件里的三个隐形地雷application.yml是这个项目最先可能坑到你的地方。我把高频踩坑列成了一张表配置项容易写错的地方正确示例后果数据库地址漏掉serverTimezone参数jdbc:mysql://localhost:3306/erp_db?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8时区报错或中文乱码静态资源路径误配了spring.mvc.static-path-pattern导致 404保持默认/ **或显式配/static/**页面样式和 JS 加载失败MyBatis mapper 扫描漏写mapper-locationsmybatis.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;charsetutf-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 MapString, Object list(SysUserQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListSysUser users sysUserService.queryList(query); PageInfoSysUser pageInfo new PageInfo(users); MapString, Object result new HashMap(); result.put(total, pageInfo.getTotal()); result.put(rows, pageInfo.getList()); return result; }前端页面里对应的是table iddg classeasyui-datagrid url/sysUser/list toolbar#toolbar paginationtrue fitColumnstrue rownumberstrue singleSelecttrue thead tr th fieldid width50ID/th th fielduserName width100用户名/th th fielddeptName width120部门/th th fieldstatus width60 formatterformatStatus状态/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意味着你不需要在服务器上额外装 Tomcatjava -jar erp-system.jar一条命令就能启动。如果你一定要部署到外部 Tomcat 的 webapps 目录下则需要在启动类继承SpringBootServletInitializer并重写configure方法同时把pom.xml的打包方式改为 war。我在部署时常用的启动命令是nohup java -Xms256m -Xmx512m -jar erp-system.jar --spring.profiles.activeprod erp.log 21 nohup和是为了让进程在 SSH 断开后继续运行--spring.profiles.activeprod是为了切换到生产环境的配置。生产环境的application-prod.yml里数据库密码不要写明文可以用环境变量注入比如password: ${ERP_DB_PASSWORD}5.2 时区问题、连接超时和内存溢出这些都算部署期高频问题。时区问题前面提过在 JDBC URL 上加serverTimezoneAsia/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 函数后续每增加一个功能模块主页面代码能减少一半——这种造轮子积累才是做完一个项目后最实在的收获。