十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建企业级OA系统:Spring Boot 3 + Vue 3全栈实战

从零构建企业级OA系统:Spring Boot 3 + Vue 3全栈实战 很多朋友学完 Java 基础、刷完一堆八股文之后最容易陷入一个尴尬境地理论都会但一问“有没有完整做过一个项目”立刻卡壳。OA 系统作为企业内部最常见的管理系统经常出现在简历项目里但真正要自己从零搭一套能用的企业级 OA涉及的链路远比想象中长。这套东西我前后折腾了一个多月从后端接口设计到前端动态菜单再部署上线踩了不少坑。这篇就完整记录我基于Spring Boot 3 MyBatis-Plus Vue 3从零构建 OA 系统的实战过程从选型理由、环境配置、数据库设计到权限系统、审批流程、前后端部署全链路都给你拆开讲透。无论你是准备做毕业设计、简历项目还是公司内部想自研一套轻量 OA都可以照着这条路线走。1. 企业OA系统的真实需求与技术选型考量1.1 一套OA系统到底包含哪些模块先别急着敲代码得先把“OA系统”这四个字拆开看。一个能被企业日常使用的OA系统核心不是在页面上放几张表单而是围绕“人和流程”的关系做文章。典型的模块包括员工管理、部门管理、角色权限、审批流程请假、报销、用章、公告通知、日程安排、会议管理、文档中心。有些企业还会把考勤、绩效、资产管理放进来但本质上都是围绕两类主数据展开组织架构里的“人”以及流转在组织里的“事”。很多新手做OA项目时最容易犯的错误是把页面做得很花哨登录、列表、表单、图表应有尽有但真正让OA区别于普通CRUD应用的那些东西——权限粒度、流程状态、审批人判定、待办已办——反而没做。企业级这个词实际指的不是代码量多少而是系统对组织规则和权限边界的支撑能力。设计时如果心里没有这张地图后期基本是推翻重来。1.2 为什么这套技术栈更适合自己动手做市面上确实有泛微、致远这类成熟的商业OA系统功能覆盖面广直接买来用省心。但商业系统的定制成本很高想改审批规则、想跟内部系统做深度打通很多时候得靠厂商二次开发周期和费用都不友好。对于学习项目、中小型公司内部工具或者想拥有完全可控代码库的技术团队来说基于开源技术栈自己搭一套是更合理的选择。技术栈的选择上我踩过对比过的组合不少最终锁定这套层面选型理由后端框架Spring Boot 3内置自动配置生态成熟JDK 17 GraalVM支持持久层MyBatis-Plus单表CRUD零SQL复杂查询保留手写SQL自由度前端框架Vue 3 Vite组合式API逻辑复用方便Vite冷启动快UI组件库Element Plus后台管理系统生态最完善上手成本低数据库MySQL 8.0稳定、社区资料多OA场景完全够用鉴权方案Sa-Token Redis比Spring Security学习曲线平滑得多功能不缺流程引擎自研状态机轻量OA用引擎大材小用后面业务复杂再迁Flowable这套组合在国内开发者的学习路线里非常主流团队招人好招遇到问题在社区里搜答案也容易。最关键的是每一个组件你都看得懂、控得住这才是自研OA的核心出发点。1.3 动手前必须具备的基础我建议你已经具备以下基础再开始不然容易劝退Java 基础扎实尤其是集合、泛型、Lambda、Stream日常写业务离不开会写 SQL至少会 join 和常见的聚合查询、子查询了解 HTTP 协议和 RESTful 接口设计的基本套路前端至少掌握 Vue 的基础语法、组件通信、路由、Vite 构建工具的使用了解 Linux 基本操作和 Nginx 的简单配置否则最后部署那一步会非常痛苦如果你只想做前端或者只做后端这套项目也能分开学但要真正体会全栈项目为什么会出现各种“联调问题”最好还是整条链路走一遍。2. 项目初始化与分层设计Spring Boot 3 的迁移坑要从第一步就避开2.1 环境准备JDK 17 与 jakarta 命名空间Spring Boot 3 的第一个门槛不是代码而是环境。它强依赖 JDK 17如果还在用 JDK 8连项目都启动不起来。建议先装好 JDK 17并在 IDEA 里把项目 SDK 和 Maven 的 JDK 版本都指到 17。环境变量配置网上的教程很多Java 基础里面也有这里不再展开只提醒一个容易被忽略的点Maven 的JAVA_HOME一定要指向 JDK 17否则 IDEA 里构建和命令行构建结果可能不一致。第二个坑是命名空间迁移。Spring Boot 3 从javax.*迁到jakarta.*这个改动影响面很广。如果你百度到一篇 Spring Boot 2 的教程里头写的是javax.servlet直接粘过来就会看到一片红色报错。涉及文件上传、Servlet 相关代码的时候只要看到javax先想想改成jakarta。初始化 Spring Boot 项目时可以直接用 start.spring.io 选 Java 17依赖先选 Spring Web 和 ValidationMySQL 驱动后面手动加。我习惯手动维护 pom.xml这样每次加依赖都知道干了什么。核心依赖结构大概长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意 MyBatis-Plus 的 starter 必须写mybatis-plus-spring-boot3-starter不能写老版本的mybatis-plus-boot-starter后者在 Spring Boot 3 下启动会直接报错。这个坑我当年栽过网上查半天才反应过来是包名不对。2.2 后端包结构与模块划分单体应用不是不划分模块而是用包结构把边界划出来。我是这样组织的com.company.oa ├── common // 统一返回结果、异常处理、常量、工具类 ├── config // 配置类MyBatis-Plus分页、拦截器、跨域 ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 入参对象 ├── vo // 出参对象视图对象 └── security // 鉴权相关这个分层的核心逻辑是单向依赖controller 只调 serviceservice 调 mapper 和别的 serviceentity 不依赖任何层。dto 和 vo 不要混用一个负责接收前端参数一个负责返回给前端虽然很多字段长得一模一样但后续业务变化时你会发现分开的好处——参数变了不影响返回结构反之亦然。统一返回结果也值得从一开始就做好。我定义了一个RT类包含 code、message、data 三个字段所有接口一律返回它而不是裸返回对象。前端统一在 axios 响应拦截器里处理 code这样接口文档、前端联调、错误处理都有一套固定约定。2.3 Vue 3 工程创建与 Volar 环境前端工程用 Vite 创建最省事一条命令就能搞定npm create vitelatest oa-frontend -- --template vue-ts然后装上路由、状态管理、组件库、请求库cd oa-frontend npm install vue-router4 pinia axios element-plus这里要特别提一下 VS Code 插件的问题。旧项目用 Vetur 的日子已经过去了现在官方推荐的组合是Vue Language Features (Volar)和Vue VSCode Snippets以 TypeScript 的完全支持为准。如果你装了 Vetur 再装 Volar语法提示会互相打架最典型的症状就是.vue文件里所有变量标红但编译能过。建议把 Vetur 禁用只保留 Volar。Vite 创建的项目默认没有别名需要自己配不然后面写 import 全是../../xxx。修改vite.config.tsimport { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })顺便把开发代理配好。这样前后端联调时前端只管写/api/xxx完全不用关心跨域问题。这个代理配置在生产环境不生效生产部署直接用 Nginx 反向代理后面第 6 章会详细讲。3. 权限模型与数据表设计OA系统的基础不能靠后期补3.1 用户-角色-菜单多对多模型与部门树OA系统最核心的领域模型就是权限我采用的是经典 RBAC 模型用户关联角色角色关联菜单权限。这个模型灵活在什么地方一个用户可以有多重身份比如“张三”既是开发工程师又是项目组长那他就同时拥有两个角色的权限集合。企业授权规则变化频繁如果权限直接绑在用户身上每次人员变动都要改代码而通过角色中转只需要调整角色和菜单的关系。核心表分六张表名关键字段说明sys_userid, username, password, real_name, dept_id, status账号密码用BCrypt加密存储sys_roleid, role_name, role_code, status角色编码用于后端鉴权sys_menuid, parent_id, menu_name, path, component, perms, type菜单和权限点都放这张表sys_user_roleid, user_id, role_id用户和角色关联sys_role_menuid, role_id, menu_id角色和菜单关联sys_deptid, parent_id, dept_name, leader_id部门树leader_id指向部门负责人另外建议你不管是用户还是部门都加上deleted字段走逻辑删除。部门树这块如果只用parent_id关联查询子树的时候要递归麻烦。我额外加了一个ancestors字段用逗号分隔所有祖先节点 ID比如1,2,5这样查某个部门及其子部门时直接用ancestors LIKE 1,2,%就能搞定代价是维护部门层级时要更新这个字段但收益远大于成本。3.2 MyBatis-Plus 分页与条件构造器的使用MyBatis-Plus 最香的其实是日常单表 CRUD 几乎不用写 XML 文件复杂查询再用Select注解或者 XML 补充。但有几个配置不能漏分页插件不注册的话Page对象传进去只会返回全量数据不生效。在 config 包里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }日常查询时用 LambdaQueryWrapper 构造条件比老式的 QueryWrapper 更推荐因为用了 lambda 方法引用字段名直接对应实体属性编译期就能发现拼写错误。举一个用户列表分页查询的例子public PageUserVO pageUsers(UserQueryDTO query) { PageUser page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getUsername()), User::getUsername, query.getUsername()) .eq(query.getStatus() ! null, User::getStatus, query.getStatus()) .orderByDesc(User::getCreateTime); PageUser result userMapper.selectPage(page, wrapper); // 转成VO补充部门名称等 return convertToVO(result); }这里用了条件参数重载的第一个参数是布尔值为 false 时该条件直接不拼进 SQL省去一堆 if 判断。注意这种写法里useWrapper参数要传对否则传 null 的时候条件也会拼进去。3.3 逻辑删除、自动填充与乐观锁在数据模型里我认为最应该提前定好的三个基础能力是逻辑删除。企业系统里的数据都是留痕的用户离职、流程单撤回这些场景不能物理删行。实体字段加TableLogic注解TableLogic private Integer deleted;加完后 MyBatis-Plus 会自动把所有查询带上deleted 0删除操作自动转成UPDATE ... SET deleted 1完全不用业务层关心。自动填充。create_time、update_time 这类字段如果每个插入和更新都手动 set很容易漏。定义一个 MetaObjectHandler 实现类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体字段加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)就再也不用手动管时间戳了。乐观锁。审批单这种多人可能同时操作的场景用乐观锁防止并发提交造成的覆盖。实体加Version注解更新时带上 version 条件MyBatis-Plus 会自动在 UPDATE 语句后面拼AND version ?更新成功 version 加一失败则返回 0业务层可以提示“数据已被他人修改请刷新后重试”。4. 登录鉴权与动态权限Sa-Token 比 Security 友好但核心原理要懂4.1 登录流程与 Token 方案选型登录鉴权方案我选了 Sa-Token 而不是 Spring Security。如果你只想把一个OA系统跑起来Security 那套过滤器链、UserDetailsService、AuthenticationManager 给你的认知负担会很重大部分代码不是业务层面的而是在对抗框架本身的复杂性。Sa-Token 的思路是“登录了就给你发个 token之后每次请求带 token 来”代码量少核心概念一两天能掌握而且内置了踢人下线、账号封禁、接口限流这些企业场景真正用得上的功能。登录接口的核心流程就四步参数校验用户名非空、密码非空根据用户名查用户表比对 BCrypt 加密后的密码确认用户状态正常status 10 表示禁用调用 Sa-Token 的StpUtil.login(userId)拿到 token 返回前端需要注意密码比对不要用 MD5。MD5 撞库太容易了必须用 BCrypt因为每次加密结果都带随机盐即使两个用户密码一样密文也不同。Spring Security 自带 BCryptPasswordEncoder我直接拿来用Sa-Token 只管会话密码加密可以自行组合。Token 返回给前端后前端存到 localStorage 或 Pinia 里之后每次请求在 axios 请求拦截器里把 token 塞到 headerservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config })Sa-Token 默认是从 header 的satoken字段取值的你也可以通过配置改成自定义 header 名但我建议直接用它默认的省事且不会有团队成员再发明一套。4.2 后端拦截器与 UserContext 线程变量Sa-Token 本身就是靠拦截器或过滤器实现的但你需要一个业务层的用户上下文。比如审批单创建时要记录“当前登录用户是谁”这些信息如果每次从 token 里解析、再手动传给下一个方法太累了。我做的处理是自定义了一个拦截器Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Sa-Token 校验未登录直接抛异常由全局异常处理器转成401 StpUtil.checkLogin(); // 从 Sa-Token 会话里取出登录用户ID放入 ThreadLocal Long userId StpUtil.getLoginIdAsLong(); UserContext.set(userId); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }UserContext 类就是ThreadLocalLong的封装提供了 set、get、clear 三个静态方法。为什么要 clearThreadLocal 在线程池里如果不清理会内存泄漏这是 Java 八股文里会考但实际代码里很多人不写的问题。注意拦截器只拦截需要登录的接口登录接口、验证码接口这些要放行。注册拦截器时用excludePathPatterns排除/auth/login和/auth/captcha。在需要当前用户信息的业务方法里直接UserContext.get()拿 ID清爽利落。4.3 动态菜单后端返回菜单树前端动态注册路由菜单表 sys_menu 里用 type 区分目录、菜单、按钮。登录成功后前端请求/user/menus后端根据该用户的角色关联的菜单权限生成一棵树返回。树的节点包含菜单名、路由 path、组件路径、图标等信息。前端拿到这棵树后有两个任务第一个任务是渲染侧边栏导航菜单这个直接递归遍历树生成 el-menu 就完事。第二个任务是动态注册路由。Vue Router 3 时代用router.addRoutesVue Router 4 改成addRoute需要遍历后端返回的菜单树把每个菜单项转成一个 RouteRecordRaw并指定对应的.vue组件。这里会遇到一个经典问题路由懒加载时后端返回的 “component” 只是字符串比如system/user/index前端要映射到实际组件。我封装了一个方法function loadView(component: string) { return () import(/views/${component}.vue) }然后用router.addRoute(route)逐个注册。注意一个坑动态路由如果直接router.addRoute(route)默认是注册到根路由下你需要指定父级关系否则刷新页面后动态路由丢失用户被踢到 404 或登录页。解决办法是两个方面都堵菜单树下钻时记录完整路由层级前端在注册时手动构造嵌套关系同时 Pinia 里存一份用户信息和菜单页面刷新后重新拉取再走一遍动态注册逻辑。只有这两步都做了刷新页面才不会闪一下登录页。5. 业务开发实战请假审批这条流程串起前后端全部核心链路5.1 请假单的表设计与状态机理论知识聊完来看一个具体业务怎么落地。我拿请假审批来串全链路因为请假流程短、够典型但又能带出权限、状态、异步通知等复杂点。请假单表 leave_apply 核心字段字段类型说明idbigint主键user_idbigint申请人IDstart_timedatetime开始时间end_timedatetime结束时间reasonvarchar(500)请假原因statustinyint状态1待审批 2已通过 3已驳回 4已撤销approve_user_idbigint当前审批人IDapprove_remarkvarchar(500)审批意见approve_timedatetime审批时间状态机其实很简单创建请假单 - 待审批待审批 - 通过审批人操作待审批 - 驳回审批人操作待审批 - 撤销申请人自己操作且仅限未审批状态只用数字存状态是程序员思维但为了让前端显示友好我在 VO 里把状态翻译成用户能看懂的文本。数字状态的好处是扩展容易以后加“已转交”“已取消”都方便不用改表结构。为什么不直接用字符串字符串状态在数据库里容易写错排错麻烦数字加注释反而更稳。5.2 多级审批的角色判定逻辑中小型OA系统的审批流不需要一上来就上 Flowable 那种重量级流程引擎用一张部门表上的leader_id字段就能解决大部分“一级审批”的需求。申请人提交请假单时后端自动查出申请人所属部门的 leader_id把这条请假单的approve_user_id设置成这个领导。待办列表就是查approve_user_id 当前登录用户 AND status 1已办列表是approve_user_id 当前登录用户 AND status IN (2, 3)。当审批人点击“通过”时需要再判断是否还有下一级审批。比如“请假超过3天要总经理审批”这个规则放在 service 层不要写死在 controller 里public void approve(Long id, String remark) { LeaveApply apply leaveApplyMapper.selectById(id); // 校验审批人 if (!apply.getApproveUserId().equals(UserContext.get())) { throw new BusinessException(你不是该单据的审批人); } // 校验状态 if (apply.getStatus() ! 1) { throw new BusinessException(该单据不在待审批状态); } // 判断是否需要下一级审批 long days calculateDays(apply.getStartTime(), apply.getEndTime()); if (days 3 hasNextApprover(apply)) { // 设置下一个审批人状态保持待审批 apply.setApproveUserId(getNextApproverId(apply)); } else { apply.setStatus(2); // 通过 apply.setApproveTime(LocalDateTime.now()); } leaveApplyMapper.updateById(apply); }这个逻辑里最重要的是两个校验一个是越权校验当前登录人是不是这条单据的审批人另一个是状态校验防止并发下两张单子被重复审批。这两行代码是面试官最喜欢问的“水平越权”场景。5.3 待办、已办查询与异步通知待办已办列表的 SQL 其实不复杂但因为列表要额外展示申请人姓名、部门名称一般不直接拿表数据返回而是写一个 join 查询。MyBatis-Plus 里可以用Select注解写在 mapper 方法上也可以用 XML 文件。个人习惯简单查询用注解SQL 超过三行我直接上 XML可读性好加索引注释也方便。审批完成后还有个衍生需求通知申请人审批结果。这种动作不能阻塞主流程比如用户点一下“通过”如果同步去调短信接口、钉钉机器人接口接口延迟可能从 50ms 变成 2 秒。这种场景用 Spring 的Async注解就能很好解决。单独写一个NotifyService方法上标注Async(taskExecutor)在approve()方法里调用后立即返回真正的通知发放在线程池里异步执行。这也是 Java 线程池和 CompletableFuture 这类知识在企业项目里的真实落地点比单纯背八股文有意思得多。异步任务记得给线程池配一个自定义的拒绝策略否则线程池满了任务直接丢通知就悄悄消失了。我常用CallerRunsPolicy线程池满了就退回调用线程同步执行保证通知不丢。6. 联调、打包与部署我在真实环境里栽过的坑希望你别再栽6.1 开发环境跨域与生产环境 Nginx 反向代理前后端分离项目最常见的坑就是跨域。开发环境其实最简单的方法不是后端开 CORS而是用 Vite 的 proxy。前面配置里已经把/api代理到http://localhost:8080了这样前端代码里所有请求都写相对路径/api/xxx浏览器看到的域名始终是前端自己的不存在跨域。生产环境则是 Nginx 直接接管一段配置搞定server { listen 80; server_name your-domain.com; root /home/oa/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; } location /upload/ { alias /home/oa/upload/; } }这段配置里最不能漏的是try_files $uri $uri/ /index.html;否则 Vue Router 用 history 模式时用户直接访问/system/user刷新页面Nginx 找不到对应文件会返回 404。而location /api/就是把后端接口反向代理出去前端请求/api/user/list最终落在http://127.0.0.1:8080/user/list。6.2 文件上传路径与静态资源映射OA系统几乎逃不掉附件上传用户头像、审批附件、公告配图。这个功能的坑集中在路径问题上。本地开发时大家喜欢把上传文件放到项目的static/upload目录下看起来省事但部署到服务器后项目目录每次发布都可能被覆盖上传的文件跟着没了。正确的做法是配置一个独立的上传目录比如/home/oa/upload这个路径通过application.yml外部化配置oa: upload-path: /home/oa/upload后端接收 MultipartFile 后用 UUID 重命名文件并保存到该目录再把/upload/文件名这个相对路径存到数据库。前端展示图片时直接拼上/upload/...由 Nginx 的location /upload/映射到磁盘目录这样上传和访问都跟后端代码解耦了。6.3 打包部署与 systemd 进程管理部署环节的完整步骤大概是后端执行mvn clean package -DskipTests生成oa-server.jar前端执行npm run build生成dist目录把 jar 放到/home/oa/启动命令用nohup java -jar oa-server.jar --spring.profiles.activeprod 把 dist 里的文件上传到/home/oa/dist配置 Nginx 完成反向代理用nohup启动的方式能跑但进程管理太裸了。推荐用 systemd 托管 Java 进程服务器重启自动拉起日志管理也更方便。在/etc/systemd/system/oa.service写[Unit] DescriptionOA Server Afternetwork.target [Service] Userroot WorkingDirectory/home/oa ExecStart/usr/local/java/bin/java -Xms512m -Xmx1024m -jar /home/oa/oa-server.jar SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.target然后systemctl daemon-reload和systemctl enable oa开机自启。以后管理就是systemctl restart oa一条命令的事。6.4 从能跑到能用安全加固清单也不能省系统跑起来之后还有一关要过。部署到公网或者公司内网前建议按这个清单自查一遍密码全部 BCrypt 加密禁止存储明文或 MD5所有查询接口做水平权限校验用户ID不能由前端参数直接控制SQL 注入风险MyBatis-Plus 的 Wrapper 方式是参数化的但手写${}拼接要坚决避免XSS 攻击前端提交内容做转义富文本场景用白名单过滤越权问题审批人、续费操作必须在 service 层校验当前登录用户与实际操作对象的关系数据库连接、Redis 密码等敏感配置不要提交到代码仓库用环境变量注入这些点也是职业面试里 Java 八股文之外的超高含金量实战经验背一百道面试题不如亲手把一个漏洞堵上面试官问起来你能讲得头头是道。另一个优化方向是列表查询的性能注意给 join 字段建索引比如 leave_apply.user_id、leave_apply.approve_user_id、sys_user_role.user_id 这几个高频查询字段都要加索引。数据量大了之后MyBatis-Plus 的selectPage默认只会查询主表如果你 selectPage 时走了带 join 的自定义方法记住 MySQL 的 limit 在深分页时会越来越慢优化方向是延迟关联或者用游标分页。这些内容等你真正在公司系统里被数据量教育过就会明白今天这篇文章里每个字的价值。我个人实际做下来最大的体会是整个项目最关键的设计决策不是某一个炫技功能而是权限模型和状态机设计的清晰度。把这两块抠明白了OA 系统的骨架就立住了后面填业务内容只是时间问题。你照这篇文章搭完第一版后建议先从一个最细的模块开始用起来比如试试把某个审批流程改成你自己的业务逻辑改一遍之后你对整个系统的掌控感会完全不一样。
返回列表