SpringBoot+Vue+MyBatis+MySQL构建OA管理系统:从架构设计到部署实战
2026/9/14 21:52:38 网站建设 项目流程

接手过企业内部管理系统的人都有体会,不管规模大小,这类项目最麻烦的不是技术多高深,而是业务逻辑绕、人员关系复杂、权限规则细碎。我这次做的这套OA管理系统,选型就是SpringBoot+Vue+MyBatis+MySQL,前后端完全分离,从零搭建到部署上线,整个过程踩了不少坑,也沉淀了一些复用的经验。这篇文章把这套系统的核心设计、数据库结构、认证授权链路、业务模块实现以及打包部署细节全部整理出来,给正在做类似项目或准备上手前后端分离项目的朋友一个完整参考。

1. 从一开始就想清楚:这套OA系统为什么采用前后端分离

1.1 传统单体结构开发OA,日常哪里让人崩溃

早期OA系统最常见的形态是JSP+Servlet,或者SpringMVC直接渲染Thymeleaf模板。页面是服务端拼出来的,前端代码和后端Java代码混在一个工程里。表面上开发起来"省事",一个请求从Controller直接对应到一个HTML页面,但实际维护一段时间后问题就暴露了。

最直观的痛点在于改页面要动后端。OA系统里审批单的样式、表单校验规则、列表展示字段,这些调整是高频需求。今天人事说请假单要加一个"紧急程度"字段,明天财务说报销单的金额要用大写显示,每次都要去改Java代码里的ModelAndView,然后重新编译、打包、重启Tomcat。整个流程走下来,一次小改动花大半天很正常。

另一个问题是分工混乱。前端页面和后端逻辑绑在一起,前端工程师要迁就后端的模板语法,后端工程师要处理一堆CSS和JavaScript,两边都做得不舒服。尤其遇到页面白屏、脚本报错,排查的时候要从前端HTML一路追到后端源码,效率极低。

还有部署上的麻烦。一套传统单体OA,前端页面更新也要跟着整个WAR包一起发版。如果只是改了一个按钮的颜色,也得走一次完整的发版流程,风险高,牵涉面大。

1.2 这套架构重点解决的四个核心问题

这次重做OA系统,目标很明确,就是要解决上面提到的痛点,同时为后续迭代留出空间。前后端分离这个方向从一开始就确定了,具体来说,要解决四个问题。

第一个是开发和维护效率。前后端分离之后,前端工程和后端工程是独立的代码仓库,可以并行开发。前端用Mock数据先行开发页面,后端专注接口实现,两边对上接口文档即可。改版时互不影响,各自发版。

第二个是权限控制的灵活性。OA系统的权限要求比普通网站严格得多,不同角色的员工登录后看到的菜单、能点的按钮都不一样。前后端分离下,后端通过接口返回菜单和权限标识,前端根据权限动态渲染路由和按钮,权限模型更加清晰可控。

第三个是接口复用性。企业OA未来大概率要出移动端,或者对接钉钉、企业微信这类办公平台。只要接口是标准的RESTful API,移动端可以直接复用,不用重新写一套后端逻辑。这个考量在选型时就定下了基调。

第四个是多环境部署的灵活性。开发环境、测试环境、生产环境的数据库地址、文件存储路径、日志级别都不同。前后端分离后,前端通过环境变量切换API地址,后端通过Spring Profile切换配置,互不干扰,部署时只需要替换配置文件,代码不用改。

2. 技术选型复盘:SpringBoot+Vue+MyBatis+MySQL各自解决的问题

2.1 SpringBoot版本选择,以及版本太高带来的实际问题

SpringBoot目前主流的稳定版本是2.7.x系列和3.x系列。很多新手在创建项目的时候,会习惯性选择最新版本,但这一步往往给后续部署埋下隐患。SpringBoot 3.0以上强制要求JDK 17,而很多企业服务器的生产环境还停留在JDK 8。如果本地开发电脑用的是JDK 17,代码跑得欢,一放到服务器上就报UnsupportedClassVersionError,折腾半天才反应过来是版本不匹配。

这套OA系统我选用的是SpringBoot 2.7.18,兼容JDK 8和JDK 11,是目前企业项目里用的最多的版本序列。依赖相对稳定,网上资料丰富,遇到问题容易搜到解决方案。在选择SpringBoot版本时,不要只看功能,要结合目标部署环境来决定。如果服务器JDK是8,就用2.7.x;如果确定全链路都是JDK 17以上,才考虑3.x。

还有一个实际问题是SpringBoot版本对依赖版本的影响。SpringBoot 2.7默认管理的MyBatis Stater版本、MySQL Connector版本都是经过官方测试的,直接继承父POM的依赖管理即可,不需要手动指定版本。如果换成SpringBoot 3.x,部分老版本的依赖会因为Javax命名空间改成Jakarta而无法兼容,改起来很痛苦。

2.2 Vue侧选型:Vue3+Element Plus,还是Vue2+Element UI

前端框架的选择,在这套OA系统里其实经历过摇摆。Vue2的生态成熟,Element UI组件库全面,网上案例多;Vue3的响应式原理更高效,组合式API写起来更清爽,组件库方面Element Plus也在持续完善。

最终确定Vue3 + Element Plus,原因有几个。第一,OA系统里的表单和表格交互非常频繁,Vue3的组合式API对逻辑复用很友好,比如查询条件、分页逻辑、表单校验这些代码可以抽成可复用的函数,减少重复代码。第二,Element Plus对Vue3的支持是官方维护的,组件覆盖了OA需要的几乎所有场景——表格、树形控件、上传、日期选择、对话框,不用额外找第三方库。第三,新项目用新框架,社区在持续投入,后续遇到问题更容易获得支持。

不过如果你手里的项目要兼容IE浏览器,或者团队里没人用过Vue3,那退回到Vue2 + Element UI也完全没问题。这套系统的接口设计是标准RESTful风格,前端用什么框架都能对接,不会锁死。

2.3 MyBatis的价值,以及和MyBatis-Plus的取舍

持久层选MyBatis而不是JPA/Hibernate,是考虑到OA系统的业务特点。OA里的查询条件非常灵活,比如可以根据部门、时间范围、状态、关键字等多个维度筛选用户或单据,这种动态多条件查询如果用JPA来写,要么拼Specification,要么用QueryDSL,学习成本和维护成本都不低。MyBatis的XML里写动态SQL是最直观的,if、where、foreach这些标签几乎是为这种场景量身定做的。

另一个原因是SQL的可控性。OA系统里资金相关的报表,SQL可能有几十行,关联多张表、嵌套子查询,用MyBatis可以在XML里直接优化SQL执行计划,定位慢查询也方便。

至于MyBatis-Plus,它提供了内置的CRUD方法和分页插件,确实能省不少基础代码。这套系统在部分基础模块使用了MyBatis-Plus的单表操作能力,但在复杂查询场景依然保留XML手写SQL。如果团队对SQL掌控力一般,用MyBatis-Plus做基础CRUD是很好的选择,但不要完全依赖它,复杂的多表关联还是得自己写SQL。

2.4 MySQL安装版本,以及连接串里的几个关键参数

数据库选的MySQL,版本是8.0。相比5.7,8.0的默认字符集是utf8mb4,原生支持窗口函数和CTE,性能也有提升。不过8.0默认的密码加密方式是caching_sha2_password,一些老版本的客户端工具连接不上,需要在创建用户时指定mysql_native_password,或者升级客户端连接驱动。这个细节在部署教程里通常不会重点标注,但真的很常见。

数据库连接串里有几个参数值得注意。第一个是serverTimezone=Asia/Shanghai,如果不设置,JDBC驱动默认时区和服务器不一致,日期查询会出现8小时偏差。第二个是useUnicode=true&characterEncoding=utf8mb4,确保中文和特殊字符存储正常。第三个是useSSL=false,本地开发和内网环境不需要SSL加密,省去证书配置的麻烦。

spring.datasource.url=jdbc:mysql://localhost:3306/oa_system?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

3. 数据库设计与后端骨架搭建,这些细节决定后续开发顺不顺畅

3.1 一张表清单看懂OA系统的核心表结构

OA系统的数据模型围绕"用户—角色—权限—业务单据"这条主线展开。下面是这套系统中几张核心表的结构设计,基本覆盖了权限认证和审批流程的主链路。

系统基础表:

表名用途关键字段
sys_user用户表id, username, password, real_name, dept_id, status
sys_dept部门表id, parent_id, dept_name, order_num
sys_role角色表id, role_name, role_key, status
sys_menu菜单/权限表id, parent_id, menu_name, path, perms, menu_type
sys_user_role用户角色关联表user_id, role_id
sys_role_menu角色菜单关联表role_id, menu_id
sys_notice公告表id, title, content, status, create_time

业务单据表:

表名用途关键字段
oa_leave请假申请单id, user_id, leave_type, start_time, end_time, reason, status, approver_id
oa_expense报销申请单id, user_id, amount, expense_type, detail, status, approver_id
oa_approval审批记录表id, business_type, business_id, approver_id, approval_result, comment, create_time

设计时要特别注意几点。第一,用户表和部门表是多对一关系,部门删除前要校验是否还有员工挂在这个部门下,否则会产生孤儿数据。第二,菜单表通过type字段区分目录、菜单和按钮,按钮类型的权限用于前端按钮级控制。第三,业务单据表里冗余一个approver_id作为当前审批人,查询待办事项时直接根据approver_id和status就能查出当前用户需要处理的任务,不用每次去关联审批流配置表。

3.2 后端工程分包结构与启动流程

后端项目按照常见的分层架构分包,每个包职责清晰,后续维护时能快速定位代码位置。

com.oa.system ├── common // 通用工具类、常量、统一返回结果封装 ├── config // Spring配置类,Redis、跨域、安全等配置 ├── controller // 接口层,接收请求,参数校验 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis接口 ├── entity // 数据库实体 ├── dto // 前端交互的数据传输对象 ├── vo // 视图对象,用于返回给前端的组装数据 └── security // 认证授权相关,JWT过滤器、登录处理

启动流程里有一个非常容易忽略的细节:数据库初始化。很多新手拿到项目后,直接运行Application,报了一堆表不存在的错误,才发现忘了导入SQL脚本。建议在项目根目录放一个sql文件夹,里面按顺序编号存放建库建表脚本和初始化数据脚本,配合README说明,其他人接手时直接按说明执行即可。

3.3 MyBatis配置文件里最容易翻车的两个点

第一个是mapper-locations路径问题。如果Mapper接口和XML文件没有放在同一个包下,必须在application.yml里显式声明XML文件的位置,否则启动时会报Invalid bound statement错误。常见做法是在resources目录下建和mapper接口相同包名路径的文件夹,比如com/oa/system/mapper,配置如下:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.oa.system.entity configuration: map-underscore-to-camel-case: true

第二个就是那个经典的单字符比较问题。MyBatis的XML里,如果用if test="status == '1'"这种写法,框架会把'1'当成char类型,和String类型的status比较会报ODI错误或者比较结果不正确。正确写法用双引号包裹:

<if test='status == "1"'> AND status = 1 </if>

这个坑在热词里也被大量搜索,可见中招的人非常多。我最初也被这个坑卡了一下午,后来养成习惯,MyBatis动态SQL里只要有单字符比较,一律用双引号包外层、单引号包内层或者反过来。

这里整理的数据库连接、分包结构、MyBatis配置是搭建阶段最核心的三件事,这三处做好了,后面业务模块的编码效率会高很多。

4. 登录认证与token处理,前后端分离系统最核心的一环

4.1 JWT认证流程:从登录到接口放行的完整链路

前后端分离系统的认证方案,主流是JWT。相比Session方案,JWT天然适合无状态接口,后端不需要维护会话状态,前端拿到token存在本地,后续请求在Header里带上即可。

这套OA系统的JWT认证流程如下:

  1. 用户提交用户名和密码到/api/login接口。
  2. 后端查询用户表,校验密码(BCrypt加密后的密文比对)。
  3. 校验通过后,用JWT工具类生成token,token里包含用户ID和用户名,并设置过期时间(一般2小时)。
  4. 后端返回token和一个refreshToken(用于过期后刷新token)。
  5. 前端收到token后存入localStorage和Vuex/Pinia状态管理。
  6. 后续每次请求,前端在axios请求拦截器里把token放到Authorization请求头。
  7. 后端拦截器拦截所有非白名单接口,解析token,解析成功则放行,失败则返回401状态码。

JWT工具类的核心代码大致长这样:

public String generateToken(Long userId, String username) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRE_TIME); return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); }

这里特别提醒一点:JWT的SECRET_KEY一定不能硬编码在代码里,也不要选取太简单的字符串。生产环境应该通过配置文件或者环境变量注入,并且定期更换。如果密钥泄露,攻击者可以直接伪造任意用户的token,这比密码泄露还要可怕。

还有一个实际设计经验:不要把密码、手机号等敏感信息放进token的payload里。JWT的payload是Base64编码,不是加密的,任何人拿到token都能解码看到内容。

4.2 Vue侧token处理:axios拦截器和路由守卫的完整方案

前端这块,token处理是前后端分离项目的重中之重。热词里专门有"vue前后端分离请求token处理"这一条,说明这个问题讨论度非常高。

axios拦截器的封装,我分成三步来做。

第一步,请求拦截器。在发送请求前,从localStorage获取token,如果有就设置到请求头的Authorization字段。注意判断是否存在token以及token是否过期。

service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error) )

第二步,响应拦截器。在这里统一处理HTTP响应,后端返回的数据一般会封装成{ code, message, data }结构,拦截器里直接把data取出来,页面代码不用每次解包。同时要处理一个关键情况:token失效。后端返回401时,说明token过期或非法,此时应该清除本地token,跳转到登录页面。这里要注意防止多个请求同时401时重复跳转,得加个标记位控制。

第三步,创建共享的请求方法。按业务模块封装get/post/put/delete方法,每个方法带Loading状态处理,接口层面对前端组件完全透明。

路由守卫主要负责页面访问控制。在Vue Router的beforeEach钩子里,判断目标路由是否需要登录权限,需要的话就检查是否有token。同时根据用户登录后缓存的菜单权限数据,动态添加路由,实现菜单级权限控制。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } if (to.matched.length === 0) { next('/404') return } next() })

4.3 权限控制的具体落地:菜单权限和按钮权限

OA系统的权限不是简单登录后就能访问所有模块,不同角色的用户看到的菜单、能操作的按钮必须做区分。这套系统采用RBAC模型,用户→角色→权限三层结构。

后端在用户登录成功后,根据用户ID查出所有角色,再根据角色查出所有菜单权限,返回给前端一个菜单树和权限字符集合。前端拿到菜单数据后动态生成侧边栏菜单,权限字符集合存在状态管理里,用于按钮级别的判断。

菜单权限的实现方式有两种,一种是前端写死全部路由,登录后根据返回的菜单数据过滤出可见的菜单;另一种是前端只写基础路由,动态添加有权限的路由。

按钮权限的实现,主流是通过自定义指令,比如v-permission="'sys:user:add'",指令内部检查用户权限集合里是否有这个权限标识,没有就移除该DOM。后端接口层面还要做二次校验,防止有人绕过前端直接调用接口。比如删除用户接口,在Service层先检查当前操作人是否有sys:user:delete权限,没有就直接抛出权限不足异常。

这里有一个容易被忽略的问题:不要只根据登录接口返回的角色或权限做判断,有些用户角色信息在登录到使用期间可能被管理员修改。稳妥的做法是每次进入某个模块时,或者定期刷新用户权限信息。这套系统的做法是,路由切换时如果路由配置里声明了需要的权限标识,前端会从状态管理里检查,同时后端接口对关键操作也会用自定义注解做权限校验,双保险。

5. OA核心业务模块的实现思路,从审批流到公告管理

5.1 审批流模块:状态流转和待办事项的设计思路

审批流是OA系统里最复杂、也最有代表性的模块。请假、报销、采购这些场景本质上都是"提交申请→上级审批→结果通知"的流程。这套系统没有引入专门的流程引擎,而是用简单的状态机来支撑,对中小企业的OA系统来说是性价比最高的方案。

审批状态的定义:

状态值状态名称说明
0草稿用户保存但未提交
1待审批已提交,等待审批人处理
2已通过审批通过,流程结束
3已驳回审批被驳回,可修改后重新提交
4已撤回申请人撤回已提交的申请

核心表就两张:业务单据表(请假、报销)和审批记录表。业务单据表里有一个current_approver_id字段,指向当前待处理的审批人。查询某人的待办事项就是查所有status=1且current_approver_id=当前用户ID的单据,一条SQL就能搞定。

审批动作的实现逻辑:

  • 提交申请:生成业务单据,status=1,approver_id设置为审批人
  • 审批通过:如果有多级审批,approver_id指向下一级审批人;如果已到终审,status更新为2
  • 审批驳回:status更新为3,在审批记录表里写入驳回原因
  • 撤回审批:只有在前一个审批人尚未处理的情况下才允许撤回,否则撤回失败

审批记录表的每条数据记录了谁在什么时间对哪张单据做了什么操作、审批意见是什么,方便后续追溯和审计。

如果以后要升级成复杂的会签、或签、条件分支等流程,可以引入Flowable或Activiti,但中小企业OA一般用不到那种复杂度。状态机方案的优势是简单、透明、可维护,出问题容易排查。

5.2 部门与用户管理:树形结构怎么处理

部门组织结构天然是树形结构,这套系统用parent_id字段表示层级关系,根部门的parent_id为0。

后端查询时,有两种方式。一种是递归查询,每次根据parent_id查子部门,优点是SQL简单,缺点是数据库查询次数多。另一种是一次性查出所有部门,在内存中组装成树,一次查询搞定,效率高,缺点是当部门数据特别多时内存占用稍大。对一套OA系统来说,部门数量一般不会超过几百个,完全可以用第二种方式,而且实现起来也不复杂。

public List<DeptVO> buildDeptTree(List<SysDept> deptList) { Map<Long, DeptVO> deptMap = new HashMap<>(); for (SysDept dept : deptList) { DeptVO vo = new DeptVO(); BeanUtils.copyProperties(dept, vo); deptMap.put(dept.getId(), vo); } List<DeptVO> tree = new ArrayList<>(); for (DeptVO vo : deptMap.values()) { if (vo.getParentId() == 0) { tree.add(vo); } else { DeptVO parent = deptMap.get(vo.getParentId()); if (parent != null) { parent.getChildren().add(vo); } } } return tree; }

部门模块有个经验值得分享:删除部门前,除了检查有没有子部门,还要检查有没有用户挂在这个部门下。很多OA系统上线第一周就出现员工找不到部门的情况,不是数据丢了,就是删部门的时候把关联用户给一起带崩了。

用户管理模块,查询条件一般包含关键字(用户名、姓名、手机号)、部门、状态。这里用MyBatis动态SQL做多条件分页查询,配合PageHelper分页插件,逻辑清晰,接口响应快。

5.3 公告与文件模块:上传下载的实现细节

公告模块的目的是发布公司通知、规章制度、内部动态。表结构里有title、content、status(草稿/已发布)、pinned(是否置顶)、create_time、publish_time。查询时置顶公告排前面,其余按发布时间倒序。已发布的公告对所有人可见,草稿公告只有管理员可查看。

文件管理这块,考虑到OA系统的实际使用场景,文档上传、图片展示、下载是最基本的需求。存储方案有几种:本地磁盘存储、FastDFS分布式存储、阿里云OSS。对中小企业的OA系统来说,本地磁盘存储最简单,部署时挂载一个专门的目录作为文件存储盘,上传时把文件写到磁盘,数据库保存文件访问的相对路径,前端通过后端接口访问。

上传接口的设计有一个关键点:文件大小限制和类型校验。SpringBoot默认上传大小是1MB,需要手动调到10MB甚至更大,配置文件里这样设置:

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

文件类型校验,不能只判断扩展名,要同时校验文件的Content-Type,否则一个恶意用户改后缀名就能上传可执行文件,这会成为安全隐患。

文件访问权限也比较重要。OA系统里的文件不应该直接放在nginx静态目录下公开访问,而是通过后端接口做权限校验后返回。前端下载文件时,带上token请求文件接口,后端校验无误再用流的方式返回文件内容。这样权限控制和文件访问是统一的。

6. 从开发机到服务器:打包部署全流程与坑点实录

6.1 SpringBoot后端打JAR包的两个关键操作

后端部署,标准做法是用Maven打成可执行JAR包,放到服务器上通过java -jar运行。

打JAR包之前,注意两个关键配置。第一个是跳过测试,如果项目里写了单元测试,但测试类依赖数据库连接,打包时不跳过测试就会因为连不上测试库而失败。用下面这个命令:

mvn clean package -DskipTests

第二个是生产环境配置文件要独立出来。项目里至少要有application-dev.yml、application-prod.yml,或者通过application.yml配合spring.profiles.active=prod来切换环境。打包时不用改任何代码,启动命令指定profile即可:

java -jar oa-system.jar --spring.profiles.active=prod --server.port=8080

JAR包启动还有一个细节:内存参数。默认的JVM堆内存可能不够用,尤其是同时运行前端构建工具和后端服务的时候。建议显式指定堆内存,比如-Xms256m -Xmx512m,根据服务器配置调整。

6.2 Vue前端打包和部署路径的坑

前端打包,核心命令是npm run build,完成后生成dist目录。但dist目录里的资源引用路径,默认是绝对路径/。如果nginx把前端部署在域名根路径下,没有问题;如果部署在子路径下,比如http://ip:8081/oa/,直接访问就是白屏,资源全部404。

解决办法是修改vue.config.js里的publicPath,改成相对路径或者绝对路径:

module.exports = { publicPath: './' }

改成相对路径后,dist目录里的js/css资源会使用相对路径加载,不管部署在哪个子路径都能正常工作。这种做法在多数场景下是最省心的。

还有一个常见的坑是打包后布局异常。热词里专门有"vue 打包后 布局异常"这条,我实际也遇到一次。排查下来发现是Element Plus组件的样式没被完整引入,或者是因为使用了按需引入但是有组件被遗漏。解决方案是检查main.js里的样式导入方式,建议在开发阶段先全量引入Element Plus样式,打包发布前再优化为按需引入,避免样式缺失。

6.3 Nginx反向代理配置模板

前端打包后的dist目录,通常用Nginx托管。Nginx的核心配置有两块:静态文件服务,以及API反向代理。

静态文件服务的配置就是root指向dist目录,index指定index.html。关键点是history路由模式下的重写规则。Vue Router如果用history模式,刷新一个子页面时,Nginx会去磁盘上找对应的路径,找不到就返回404。解决办法是配置try_files,让所有路径都回退到index.html,由前端路由接管:

server { listen 8081; server_name localhost; root /data/www/oa-web; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

反向代理这一块,实质上是把前端请求的/api开头的接口转发到后端SpringBoot服务。这样前后端的域名完全统一,绕开了跨域问题。前端的axios请求baseURL配置为/api,和Nginx的location规则对应即可。

这里还有个细节:代理配置里proxy_set_header带上Host和真实IP。后端Log里打印的是真实客户端IP地址,方便排查问题。如果没有这段配置,所有的日志IP都是127.0.0.1,出了问题很难定位具体是哪个用户在操作。

6.4 部署上线后客户反馈的真实问题

部署完成后,客户试用阶段暴露了很多在开发环境不会出现的问题,这里挑几个典型的复盘。

第一个是日期显示问题。客户反馈审批单上的时间比实际时间少了8小时。排查发现两个原因叠加:前端没有做时区转换,后端返回的时间是带时区的Date对象,序列化后显示的是UTC时间。解决方法是后在配置里指定Jackson序列化格式:

spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss

第二个是文件上传后中文文件名乱码。问题出在nginx代理没有设置header大小和编码。上传文件的接口返回的文件名如果是中文,浏览器可能把UTF-8编码当成ISO-8859-1解读。解决方法是后端设置Content-Disposition时显式指定UTF-8编码。

第三个是首次访问页面加载慢。OA首页要加载大量菜单权限数据、公告数据、待办数据,多个接口串行请求导致白屏时间长。优化方案是首页改为并行请求,并且后端对角色菜单数据做Redis缓存,二次访问明显加速。

第四个是客户在办公室内网访问系统,图片加载正常,但上传大文件时经常超时。排查下来是nginx默认的客户端请求体大小限制是1MB,大文件直接返回413错误。调整nginx配置:

client_max_body_size 50m;

部署上线后遇到的这些问题,开发环境几乎无法提前暴露,只有放到真实网络环境里才会触发。这也是为什么我一直强调:部署前一定要在测试环境完整跑一遍,尤其是文件上传、日期展示、路由刷新这些高频操作。

这套OA系统从开发到上线,前后用了差不多一个半月时间,核心代码量不算夸张,真正耗时的是各种边界情况的处理和联调。如果你也要做类似的企业管理系统,架构上直接参考这套前后端分离的方案,技术上把认证授权链路、审批状态流、部署配置这三块啃透,基本就把握住了这类项目的骨架。实际动手时如果卡在哪个环节,多看看日志,多想想数据在前后端之间怎么流转,很多问题其实自己就能定位出来。

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

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

立即咨询