基于PHP与ThinkPHP构建开源OA系统:架构设计与核心模块实现
2026/9/5 23:16:07 网站建设 项目流程

简介:这是一套面向中小企业及PHP开发者免费开源的办公自动化(OA)系统源码,基于信呼项目实现,旨在解决日常审批、任务协同、即时通信与多端统一管理等核心办公需求。资源包共1460个文件,含733个PHP后端逻辑文件、190个HTML页面模板、178个JavaScript交互脚本、90个PNG与231个GIF图像资源、19个CSS样式表及配套字体(woff/ttf/eot/svg)、SQL数据库脚本等,整体压缩后仅7.21MB,结构清晰,含webimcss.css、weui.min.css、rui.css等主流UI框架样式,便于二次开发与主题定制。已有1015人学习下载,适合PHP中级开发者快速搭建私有化OA环境,掌握前后端协同架构、权限控制模块、REIM通信集成及APP/PC双端适配实践。

1. 项目概述:为什么选择PHP来构建开源OA系统?

在数字化办公浪潮下,一个高效、灵活且成本可控的办公自动化(OA)系统,对于许多中小型团队和企业来说,已经从“锦上添花”变成了“雪中送炭”。市面上成熟的商业OA产品固然功能强大,但往往伴随着高昂的授权费用、僵化的业务流程和深度的定制壁垒。这恰恰是开源OA系统大显身手的地方。而当我们谈论快速开发、广泛部署和社区生态时,PHP语言总是绕不开的一个选择。

我选择基于PHP来设计和实现这套开源OA系统,并非一时兴起。PHP作为一门久经沙场的服务器端脚本语言,其最大的优势在于“接地气”。它学习曲线平缓,一个有一定编程基础的开发者,能在短时间内上手并贡献代码;它部署极其简单,从共享虚拟主机到独立服务器,几乎无处不在的支持让系统落地几乎没有环境障碍;更重要的是,它拥有一个庞大而活跃的开源生态。从ThinkPHP、Laravel、Yii这些成熟的开发框架,到无数经过实战检验的类库和组件,都意味着我们在构建OA系统时,不必重复发明轮子,可以将精力聚焦于业务逻辑本身。

这套系统的设计目标很明确:它不是一个追求大而全的“巨无霸”,而是一个核心功能扎实、架构清晰、易于二次开发的“种子项目”。我们希望它能够开箱即用,解决日常办公中的审批、沟通、文档管理等基本痛点;同时,它的代码结构应该是模块化的,数据库设计应该是规范的,方便其他开发者根据自己公司的独特制度,进行功能增删或流程改造。换句话说,我们提供的不仅是一个可用的系统,更是一个可供学习和演进的“设计源码”,这也是项目标题所强调的核心价值。

2. 系统核心架构设计与技术选型

2.1 分层架构与模块化设计思路

一个可维护性高的系统,必须始于一个清晰的架构。在本OA系统的设计中,我采用了经典的三层分层架构,并在此基础上强化了模块化的思想。

表现层(UI Layer):负责与用户交互。这里我没有选择重量级的前端框架(如Vue、React),而是采用了主流的“服务端渲染 + jQuery/Bootstrap”的组合。这样做的考量主要有两点:一是降低技术栈复杂度,让专注于后端业务的PHP开发者也能轻松维护前端界面;二是提升首屏加载速度和SEO友好性。所有页面由PHP脚本动态生成,通过Bootstrap保证响应式布局,再辅以jQuery处理简单的动态交互(如表单验证、Ajax提交),在开发效率和用户体验之间取得了很好的平衡。

业务逻辑层(BLL):这是系统的“大脑”,包含了所有的核心业务规则和流程。我将其设计为独立的服务类(Service Class)。例如,LeaveApplicationService(请假申请服务)、DocumentService(文档服务)。这些服务类不直接操作数据库,也不处理HTTP请求,它们只关心业务逻辑,比如“检查请假天数是否超过年假余额”、“判断文档流转的下一个审批人是谁”。这种设计使得业务逻辑高度内聚,易于单元测试,并且当业务流程需要变更时,修改点非常集中。

数据访问层(DAL):负责与数据库打交道。为了安全性和便捷性,我使用了PDO(PHP Data Objects)扩展进行数据库操作,并对其进行了简单的封装。封装类提供了常用的findsavedelete等方法,并统一处理参数绑定,从根本上杜绝SQL注入的风险。同时,我们遵循Active Record模式或Data Mapper模式(根据复杂度选择),让每个业务实体(如User、Department)都对应一个模型类(Model),模型类包含了该实体的数据结构和基本的CURD方法。

模块化设计:整个系统按功能划分为独立的模块,例如“组织架构模块”、“流程审批模块”、“知识库模块”、“消息中心模块”。每个模块拥有自己的控制器、视图、服务和模型目录。这种设计的好处是功能边界清晰,模块之间通过定义良好的接口进行通信,未来可以像搭积木一样,轻松启用、禁用甚至替换某个模块。

2.2 关键技术组件选型解析

技术选型决定了项目的开发体验和长期生命力。以下是本系统核心组件的选型与理由:

  1. PHP框架:ThinkPHP 6.x

    • 理由:ThinkPHP在国内拥有极高的普及率和丰富的学习资源,其文档完善,中文社区活跃,对于团队协作和后续开发者接入非常友好。6.x版本相比5.x进行了彻底的重构,引入了更符合PSR规范的容器、依赖注入等现代PHP特性,在保持易用性的同时,提升了代码的优雅性和可测试性。它的路由、中间件、验证器等功能,能极大加速OA系统中权限验证、请求过滤等通用功能的开发。
  2. 数据库:MySQL 5.7+

    • 理由:依然是关系型数据库中的绝对主流。对于OA系统涉及的大量结构化数据(用户、部门、审批流、文档元数据),关系模型能提供最清晰、最稳定的数据关系表达。事务支持能确保如“提交申请”和“生成审批任务”这样的操作原子性。我们利用InnoDB存储引擎的外键约束(谨慎使用)和索引优化,来保证数据的一致性和查询性能。
  3. 前端UI框架:Bootstrap 5

    • 理由:提供了一整套响应式、移动设备优先的UI组件。OA系统需要在PC、平板、手机等多种设备上保持良好的可用性,Bootstrap的栅格系统和组件类能让我们快速构建出风格统一、适配多端的界面,将视觉设计的精力更多地投入到交互逻辑上。
  4. 缓存与会话:Redis

    • 理由:虽然小型部署可以用文件或数据库存储会话,但为了更高的性能和分布式部署能力,我们引入Redis作为缓存和会话存储后端。用户的登录会话、频繁访问但更新不频繁的数据(如部门树、系统配置)、消息队列(可选)都可以放在Redis中,能显著降低数据库压力,提升系统响应速度。
  5. 开发与部署辅助:Docker

    • 理由:为了给开发者提供一致的开发环境,并简化生产部署,项目提供了docker-compose.yml文件。一键即可启动包含PHP-FPM、Nginx、MySQL、Redis的完整服务栈。这解决了“在我机器上能跑”的经典难题,也让后续的持续集成和自动化部署成为可能。

注意:关于“PHP经典程序”与“CTF题目”的思考:在搜索热词中,我注意到“php语言经典程序100题”和“[极客大挑战 2019]php”这类CTF题目关键词。这提醒我们,安全必须是OA系统的基石。这些“经典程序”和CTF题目常常暴露的是历史遗留的安全反模式,如SQL注入、文件上传漏洞、反序列化漏洞、会话固定等。在设计之初,我们就必须采用PDO参数化查询、严格的白名单控制文件上传类型与路径、对用户输入进行彻底的过滤和转义、使用框架内置的安全机制等,主动规避这些经典陷阱,而不是让我们的系统源码成为下一道CTF题。

3. 核心功能模块详细设计与实现

3.1 组织架构与权限管理(RBAC)实现

这是OA系统的基石,直接决定了系统的安全性和灵活性。我们采用基于角色的访问控制(RBAC)模型,并进行了适合中国企事业组织特点的扩展。

数据库设计核心表

  • user:用户表,存储账号、密码(加盐哈希存储)、姓名、所属部门ID等。
  • department:部门表,采用parent_id实现无限级树形结构,存储部门名称、编码、负责人等。
  • role:角色表,如“管理员”、“部门经理”、“普通员工”、“人事专员”。
  • permission:权限节点表,细化到“请假申请:查看”、“公文发布:审核”这样的操作级别。通常与后端路由或前端菜单项关联。
  • user_role:用户-角色关联表。
  • role_permission:角色-权限关联表。

权限校验流程

  1. 用户登录后,系统根据其user_iduser_rolerole_permission表中查询出该用户拥有的所有权限节点标识符(permission code),存入Redis或Session。
  2. 当用户发起一个请求(如访问/document/approve),系统在统一的中间件(Middleware)或控制器基类中,拦截该请求。
  3. 提取请求对应的权限节点标识符(可通过路由映射),与用户拥有的权限列表进行比对。
  4. 如果匹配,则放行;否则,返回“无权访问”的提示或页面。

实现细节与技巧

  • 数据权限:除了功能权限,OA系统经常需要数据权限。例如,部门经理只能查看本部门的请假申请。这需要在业务逻辑层进行额外过滤。我们的做法是在查询数据时,自动注入基于用户部门ID的查询条件。这可以通过重写模型查询的scope(作用域)或是在Service层封装查询方法来实现。
  • 岗位与角色分离:在实际中,角色更偏向功能集合,而“岗位”可能更贴近实际职位。我们可以增加position表,用户属于某个岗位,岗位再关联角色。这样,当员工岗位变动时,只需修改岗位的角色绑定,而无需调整大量用户的个人权限。
  • 权限缓存:用户的权限列表在登录后加载到Redis,并设置合理的过期时间(如2小时)。每次权限校验都从缓存读取,避免频繁查询数据库。

3.2 工作流引擎(审批流)的设计

审批流是OA系统的核心价值所在。我们设计了一个轻量级、可配置的工作流引擎。

核心实体

  • workflow:流程定义表。存储流程名称、唯一标识、描述、所属模块(如“请假”、“报销”)。
  • workflow_node:流程节点表。每个节点代表一个步骤,如“提交申请”、“部门经理审批”、“人事备案”。节点类型包括“开始节点”、“结束节点”、“审批节点”、“知会节点”。
  • workflow_link:节点流转线表。定义节点之间的流转关系,可以附带条件(如“请假天数>3天则流转到总经理审批”)。
  • workflow_instance:流程实例表。记录每一次具体的流程发起,关联一个workflow,并包含当前状态、发起人、发起时间等。
  • workflow_task:审批任务表。这是最活跃的表,记录每个需要被处理的审批项,关联一个instance和一个node,包含处理人、状态(待处理、已同意、已拒绝)、处理意见、处理时间。

流程运转逻辑

  1. 发起:用户填写请假单,点击提交。系统根据“请假”类型找到对应的workflow,创建一条workflow_instance记录,状态为“进行中”。
  2. 创建任务:系统根据流程定义,找到开始节点的下一个“审批节点”,根据节点配置的规则(如“指定角色:部门经理”或“指定人员:张三”),计算出具体的处理人,生成一条workflow_task记录,状态为“待处理”。
  3. 审批与流转:部门经理登录系统,在待办列表中看到此任务。他可以选择“同意”或“拒绝”。
    • 若同意,系统根据workflow_link和条件,找到下一个节点,并创建新的workflow_task给下一个处理人(如人事)。
    • 若拒绝,流程可以直接跳转到结束节点(驳回),或跳转到指定的修正节点。
  4. 结束:当流程运行到“结束节点”,更新workflow_instance状态为“已完成”或“已驳回”,并可能触发后续动作(如发送通知、更新请假状态)。

关键实现技巧

  • 审批人动态计算:这是最灵活的部分。我们设计了一个“审批人解析器”接口。可以实现多种解析器:RoleResolver(按角色)、DepartmentHeadResolver(按部门负责人)、UserSpecifiedResolver(指定具体用户)、PreviousStepApproverResolver(上一步审批人)。在节点配置时,选择并配置相应的解析器。当流程到达该节点时,引擎调用对应的解析器计算审批人列表。
  • 条件分支:在workflow_link中,可以存储一个简单的条件表达式,如${days} > 3。当流程需要判断走向时,引擎会从流程实例的上下文数据(存储为JSON格式)中提取days变量的值,并用一个轻量级的表达式求值器(如symfony/expression-language)进行计算,决定走哪条分支。
  • 状态持久化与恢复:所有流程状态都持久化在数据库中。即使服务器重启,流程也能从中断点继续。这对于长时间运行的复杂流程至关重要。

3.3 知识库与文档管理

知识库旨在实现团队知识的沉淀、共享与快速检索。

核心功能设计

  1. 文档模型:文档除了标题、内容、创建者、分类等基本属性,还应支持版本管理。每次编辑保存时,并非直接覆盖,而是创建新版本,旧版本历史可查、可回滚。这通过document表(存储最新版本元数据)和document_version表(存储所有版本内容)来实现。
  2. 富文本编辑与附件:集成开源的富文本编辑器(如WangEditor、TinyMCE),支持图文混排、格式设置。同时,文档应能关联多个附件。附件上传需严格安全控制:检查文件MIME类型、重命名文件(避免原始文件名冲突和脚本执行风险)、存储在Web根目录之外的非可执行路径,并通过PHP脚本进行鉴权后输出。
  3. 权限体系:知识库的权限比审批流更复杂,需要支持“读/写/管理”三级权限,并能精确到单个文档或目录。我们可以在RBAC的基础上,增加一个document_permission表,记录document_iduser_id/role_idpermission_level。查询时,需要合并系统角色权限和此处的个性化权限。
  4. 全文检索:这是提升知识库可用性的关键。对于中小规模,可以使用MySQL的全文索引(FULLTEXT INDEX)。对于更大规模或更复杂需求,可以集成Elasticsearch或国产的Tantivy。实现原理是:在文档创建或更新时,异步任务将其标题、纯文本内容提取出来,索引到搜索引擎中。前端提供搜索框,后端将查询词提交给搜索引擎,返回匹配的文档ID列表,再回数据库查询详细信息。

实现注意事项

  • 文档内容存储:富文本内容建议以HTML格式直接存入数据库的TEXT字段。对于超大型文档,可以考虑存储为文件,数据库中只存路径。但数据库存储更利于备份和事务管理。
  • 图片处理:富文本编辑器中的图片上传,应采用单独的上传接口,上传后返回一个可访问的URL地址插入编辑器。务必对图片进行安全检查(如图片二次渲染),防止Webshell攻击。
  • 树形目录:分类目录同样使用parent_id实现无限层级。在展示时,通过递归或一次查询后用PHP组装成树形结构,方便用户浏览。

4. 数据库设计与核心表结构详解

良好的数据库设计是系统稳定、高效运行的前提。以下是部分核心表的字段设计思路。

用户表 (user)

CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '登录账号', `password_hash` varchar(255) NOT NULL COMMENT '密码哈希值', `realname` varchar(50) NOT NULL COMMENT '真实姓名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `mobile` varchar(20) DEFAULT NULL COMMENT '手机号', `department_id` int(11) NOT NULL COMMENT '所属部门ID', `position_id` int(11) DEFAULT NULL COMMENT '岗位ID', `avatar` varchar(500) DEFAULT NULL COMMENT '头像路径', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `last_login_ip` varchar(45) DEFAULT NULL COMMENT '最后登录IP', `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_department` (`department_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
  • 设计要点password_hash字段存储的是通过password_hash()函数生成的、带盐值的BCrypt哈希值,绝对禁止明文存储密码。status字段用于软删除或禁用账号。department_id是外键,关联部门表。

流程实例表 (workflow_instance)

CREATE TABLE `workflow_instance` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '实例ID', `sn` varchar(50) NOT NULL COMMENT '流水号,如LEAVE-20231027-001', `workflow_id` int(11) NOT NULL COMMENT '流程定义ID', `title` varchar(255) NOT NULL COMMENT '实例标题(如请假事由)', `applicant_id` int(11) NOT NULL COMMENT '发起人ID', `status` tinyint(4) NOT NULL COMMENT '状态:1-进行中,2-已完成,3-已驳回,4-已撤销', `form_data` json DEFAULT NULL COMMENT '表单数据(JSON格式)', `created_at` datetime NOT NULL COMMENT '发起时间', `finished_at` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_sn` (`sn`), KEY `idx_workflow_applicant` (`workflow_id`,`applicant_id`), KEY `idx_status_created` (`status`,`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程实例表';
  • 设计要点sn字段生成规则可定制,通常包含类型、日期、序号,便于追踪。form_data字段使用JSON类型,灵活存储流程表单中填写的动态数据(如请假开始结束时间、事由、附件ID等)。这种“半结构化”存储避免了为每种流程创建单独的表,极大增强了扩展性。索引的建立要考虑到按流程类型、发起人、状态和时间的查询频率。

审批任务表 (workflow_task)

CREATE TABLE `workflow_task` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '任务ID', `instance_id` int(11) NOT NULL COMMENT '所属实例ID', `node_id` int(11) NOT NULL COMMENT '当前节点ID', `node_name` varchar(100) NOT NULL COMMENT '节点名称(快照)', `assignee_id` int(11) NOT NULL COMMENT '处理人ID', `status` tinyint(4) NOT NULL COMMENT '状态:0-待处理,1-已同意,2-已拒绝,3-已转交', `comment` text COMMENT '处理意见', `action_time` datetime DEFAULT NULL COMMENT '处理时间', `created_at` datetime NOT NULL COMMENT '任务创建时间', `due_at` datetime DEFAULT NULL COMMENT '任务截止时间', PRIMARY KEY (`id`), KEY `idx_assignee_status` (`assignee_id`,`status`), -- 用于查询个人待办/已办 KEY `idx_instance_node` (`instance_id`,`node_id`), -- 用于查询实例当前任务 KEY `idx_due_at` (`due_at`) -- 用于查询即将超时的任务 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批任务表';
  • 设计要点node_name是节点名称的快照,即使流程定义后续修改了节点名称,历史任务显示的名称也不会变,保证历史记录的准确性。assignee_idstatus的联合索引是核心索引,用于高效加载用户的待办任务列表。due_at字段支持设置任务处理时限,可以结合定时任务,实现超时提醒或自动处理。

5. 典型业务场景的代码实现与解析

5.1 用户登录与会话管理

登录是系统的入口,安全至关重要。

// UserService.php 中的登录方法 public function login($username, $password, $rememberMe = false) { // 1. 基础验证 if (empty($username) || empty($password)) { throw new \Exception('用户名和密码不能为空'); } // 2. 查找用户 $user = UserModel::where('username', $username)->find(); if (!$user) { // 使用相同的模糊提示,防止用户名枚举攻击 throw new \Exception('用户名或密码错误'); } // 3. 验证密码 if (!password_verify($password, $user->password_hash)) { // 记录登录失败次数,达到阈值可锁定账号 $this->recordLoginFailure($user->id); throw new \Exception('用户名或密码错误'); } // 4. 检查用户状态 if ($user->status != 1) { throw new \Exception('账号已被禁用,请联系管理员'); } // 5. 登录成功,更新信息 $user->last_login_ip = request()->ip(); $user->last_login_time = date('Y-m-d H:i:s'); $user->save(); // 6. 设置会话 session('user_id', $user->id); session('user_info', [ 'id' => $user->id, 'username' => $user->username, 'realname' => $user->realname, 'department_id' => $user->department_id ]); // 7. 加载用户权限到缓存(如Redis) $permissions = $this->getUserPermissions($user->id); cache('user_permissions_' . $user->id, $permissions, 7200); // 缓存2小时 // 8. “记住我”功能 if ($rememberMe) { $token = bin2hex(random_bytes(32)); // 生成安全的随机令牌 // 将令牌哈希后与用户ID一起存入数据库和Cookie $this->setRememberToken($user->id, hash('sha256', $token)); cookie('remember_token', $token, 86400 * 30); // 30天有效期 } return $user; }
  • 安全要点:使用password_verify()验证密码;登录失败提示信息统一,防止攻击者判断用户名是否存在;成功登录后更新IP和时间;会话中不存储敏感信息;remember me令牌需随机、哈希存储,并关联用户ID和过期时间。

5.2 一个完整的请假申请与审批流程

我们以最经典的请假流程为例,串联起前文提到的各个模块。

1. 前端提交申请 (AJAX请求)

// 前端使用jQuery提交表单 $('#leaveForm').submit(function(e) { e.preventDefault(); $.ajax({ url: '/leave/apply', type: 'POST', data: $(this).serialize(), // 包含请假类型、起止时间、事由等 dataType: 'json', success: function(res) { if (res.code === 200) { alert('提交成功!流程号:' + res.data.sn); window.location.href = '/my/apply'; } else { alert('提交失败:' + res.msg); } } }); });

2. 后端控制器接收并创建流程实例

// LeaveController.php public function apply() { $data = request()->post(); // 1. 数据验证(使用框架验证器或自定义) $validate = new \app\common\validate\LeaveApply(); if (!$validate->check($data)) { return json(['code' => 400, 'msg' => $validate->getError()]); } // 2. 调用服务层 try { $result = app('LeaveApplicationService')->createApplication( session('user_info.id'), $data ); return json(['code' => 200, 'msg' => '提交成功', 'data' => ['sn' => $result['sn']]]); } catch (\Exception $e) { return json(['code' => 500, 'msg' => $e->getMessage()]); } }

3. 服务层核心逻辑 (LeaveApplicationService)

public function createApplication($applicantId, $formData) { // 1. 业务校验(如假期余额检查) $leaveDays = $this->calculateLeaveDays($formData['start_time'], $formData['end_time']); if (!$this->hasEnoughLeaveBalance($applicantId, $formData['type'], $leaveDays)) { throw new \Exception('可用假期余额不足'); } // 2. 获取对应的流程定义 $workflow = WorkflowModel::where('code', 'LEAVE_APPLY')->find(); if (!$workflow) { throw new \Exception('未找到请假流程定义'); } // 3. 创建流程实例 $instanceSn = 'LEAVE-' . date('Ymd') . '-' . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT); $instanceData = [ 'sn' => $instanceSn, 'workflow_id' => $workflow->id, 'title' => $formData['reason'], // 或用更规范的标题生成规则 'applicant_id' => $applicantId, 'status' => 1, // 进行中 'form_data' => json_encode($formData), // 存储原始表单数据 ]; $instance = WorkflowInstanceModel::create($instanceData); // 4. 调用工作流引擎,启动流程 $workflowEngine = app('WorkflowEngine'); $workflowEngine->startInstance($instance->id, $applicantId); // 5. 扣减假期余额(可选,或等审批完成再扣) // $this->deductLeaveBalance($applicantId, $formData['type'], $leaveDays); // 6. 发送通知(如邮件、站内信、钉钉/企业微信机器人) $this->sendNewApplicationNotification($instance); return ['sn' => $instanceSn, 'instance_id' => $instance->id]; }

4. 工作流引擎启动流程 (WorkflowEngine::startInstance)

public function startInstance($instanceId, $starterId) { $instance = WorkflowInstanceModel::find($instanceId); $workflow = WorkflowModel::find($instance->workflow_id); // 1. 找到开始节点 $startNode = WorkflowNodeModel::where('workflow_id', $workflow->id) ->where('type', 'start') ->find(); // 2. 处理开始节点的出线,找到第一个审批节点 $firstLink = WorkflowLinkModel::where('source_node_id', $startNode->id)->find(); if (!$firstLink) { throw new \Exception('流程定义错误,开始节点无出线'); } $firstTaskNode = WorkflowNodeModel::find($firstLink->target_node_id); // 3. 为第一个审批节点创建任务 $this->createTaskForNode($instanceId, $firstTaskNode, $starterId); }

5. 创建审批任务 (WorkflowEngine::createTaskForNode)

private function createTaskForNode($instanceId, $node, $starterId) { // 1. 根据节点配置的“审批人规则”,解析出具体的处理人ID列表 $assigneeIds = $this->resolveAssignees($node->assignee_rule, $instanceId, $starterId); foreach ($assigneeIds as $assigneeId) { // 2. 创建任务记录 WorkflowTaskModel::create([ 'instance_id' => $instanceId, 'node_id' => $node->id, 'node_name' => $node->name, // 快照节点名称 'assignee_id' => $assigneeId, 'status' => 0, // 待处理 'created_at' => date('Y-m-d H:i:s'), 'due_at' => $node->due_hours ? date('Y-m-d H:i:s', time() + $node->due_hours * 3600) : null, ]); // 3. 发送任务通知 $this->sendTaskNotification($assigneeId, $instanceId); } }

至此,一个请假申请成功提交,并在数据库中创建了流程实例和待办任务。部门经理登录后,即可在待办列表看到此任务并进行审批。审批的逻辑类似,服务层会调用工作流引擎的completeTask方法,更新任务状态,并根据流程定义驱动流程走向下一个节点或结束。

6. 部署、优化与常见问题排查

6.1 基于Docker的一键部署实践

为了让任何开发者都能快速搭建环境,项目根目录提供了docker-compose.yml

version: '3.8' services: nginx: image: nginx:alpine container_name: oa-nginx ports: - "8080:80" volumes: - ./:/var/www/html - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - php networks: - oa-network php: build: ./docker/php container_name: oa-php volumes: - ./:/var/www/html environment: - TZ=Asia/Shanghai networks: - oa-network mysql: image: mysql:8.0 container_name: oa-mysql environment: MYSQL_ROOT_PASSWORD: rootpassword123 MYSQL_DATABASE: oa_system MYSQL_USER: oa_user MYSQL_PASSWORD: oa_password123 ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql networks: - oa-network redis: image: redis:alpine container_name: oa-redis ports: - "6379:6379" volumes: - redis-data:/data networks: - oa-network volumes: mysql-data: redis-data: networks: oa-network: driver: bridge
  • ./docker/php/Dockerfile:基于官方PHP-FPM镜像,安装项目所需的扩展(如pdo_mysql, redis, gd等)。
  • ./docker/nginx.conf:配置Nginx,将PHP请求转发给php:9000容器处理,并设置正确的根目录和重写规则。
  • 操作步骤
    1. 确保服务器已安装Docker和Docker Compose。
    2. 将项目代码克隆到服务器。
    3. 进入项目根目录,执行docker-compose up -d
    4. 访问http://服务器IP:8080,按照安装向导初始化数据库。
    5. 如果需要修改端口或密码,直接编辑docker-compose.yml文件并重启服务。

6.2 性能优化与安全加固要点

性能优化

  1. OPCache:在生产环境的PHP配置中务必启用并优化OPCache,它能将PHP脚本编译后的字节码缓存到内存,极大提升执行速度。
  2. 数据库优化
    • 索引:为所有高频查询条件(如where,order by,join的字段)添加合适的索引。使用EXPLAIN分析慢查询。
    • 查询优化:避免SELECT *,只取需要的字段;合理使用关联查询,避免N+1查询问题(ThinkPHP的with预加载可以解决);对大数据表进行分页查询。
    • 主从分离:当单库压力大时,考虑MySQL主从复制,将读请求分流到从库。
  3. 缓存策略
    • 页面片段缓存:对首页、公告栏等变化不频繁的页面部分进行缓存。
    • 数据查询缓存:使用Redis缓存复杂的查询结果(如组织架构树、系统配置项)。
    • 会话存储:将会话(Session)存储到Redis,比文件存储更快且支持分布式。
  4. 前端资源优化:合并和压缩CSS/JS文件,开启Nginx的Gzip压缩,使用CDN分发静态资源。

安全加固

  1. 输入验证与过滤:对所有用户输入(GET, POST, COOKIE)进行严格的验证和过滤。使用框架的验证器,或filter_var()函数。对于富文本内容,使用HTMLPurifier等库进行白名单过滤,防止XSS。
  2. SQL注入防护:坚持使用参数化查询(PDO预处理),这是最根本的防护手段。
  3. 文件上传安全
    • 验证文件扩展名和MIME类型。
    • 将上传文件重命名为随机字符串(如UUID),并存储在Web目录外的非可执行文件夹。
    • 对图片进行二次渲染处理。
    • 设置文件大小限制。
  4. 会话安全:设置Session Cookie为HttpOnly和Secure(如果使用HTTPS),防止XSS窃取。可以定期更换Session ID。
  5. CSRF防护:为所有状态修改的请求(POST, PUT, DELETE)添加CSRF Token验证。
  6. 密码安全:强制使用强密码策略,并使用password_hash()(PASSWORD_BCRYPT)存储密码。

6.3 常见问题与排查实录

在实际开发和部署中,你可能会遇到以下典型问题:

问题1:流程走到某个节点后卡住,没有生成新的审批任务。

  • 排查思路
    1. 检查节点配置:登录数据库,查看workflow_node表中该节点的typeassignee_rule是否正确。确认是否是“结束节点”。
    2. 检查流转线:查看workflow_link表中,该节点作为源节点的出线。检查condition条件字段是否设置了复杂的表达式,而实例数据不满足条件。
    3. 查看日志:在流程引擎的关键步骤(如completeTaskcreateTaskForNode)中加入详细日志,记录实例ID、节点ID、计算出的审批人等信息。查看日志文件,看流程执行到哪一步报错或中断。
    4. 审批人解析失败:检查assignee_rule解析器。例如,规则是“部门负责人”,但发起人的部门没有设置leader_id,导致解析出的审批人列表为空,引擎可能因此静默失败。应在解析失败时抛出明确异常。

问题2:用户登录后,权限时好时坏,有时提示无权访问。

  • 排查思路
    1. 检查缓存:确认权限数据是否成功写入Redis。检查Redis连接是否稳定,键名是否正确。可以尝试在登录后直接读取缓存中的权限数据,看是否完整。
    2. 检查权限节点标识符:确认中间件或控制器中提取的权限节点标识符(通常对应路由),与permission表中存储的code是否完全一致(包括大小写)。
    3. 并发登录问题:检查Session存储机制。如果使用文件存储,在负载均衡环境下会出问题,必须使用Redis等集中式存储。检查是否有其他地方(如修改用户角色后)清除了用户的权限缓存,导致新旧会话数据不一致。

问题3:知识库全文搜索速度慢,或者搜不到结果。

  • 排查思路
    1. 确认索引是否建立:如果使用MySQL全文索引,用SHOW INDEX FROM document;命令确认在目标字段(如title,content)上是否建立了FULLTEXT索引。
    2. 检查搜索词:MySQL全文索引有最小词长度限制(ft_min_word_len,默认4),太短的词(如“的”、“OA”)不会被索引。需要调整配置或使用其他方案。
    3. 分词问题:中文搜索需要分词。MySQL的全文索引对中文支持不友好。如果使用Elasticsearch,检查其中文分词插件(如ik)是否安装并配置正确。测试分词器是否能将“办公自动化系统”正确拆分为“办公”、“自动化”、“系统”。
    4. 数据同步延迟:检查文档创建/更新后,索引到搜索引擎的异步任务是否成功执行。查看消息队列或定时任务的日志。

问题4:Docker部署后,上传大文件失败。

  • 排查思路
    1. PHP配置:检查PHP容器内的php.ini,确认upload_max_filesizepost_max_size的值是否足够大(如100M)。
    2. Nginx配置:检查Nginx容器的配置文件,确认client_max_body_size也设置了足够大的值(如100M)。
    3. 执行时间:大文件上传可能需要更长时间,检查PHP的max_execution_time和Nginx的proxy_read_timeout是否足够。
    4. 存储权限:确认PHP-FPM进程(通常是www-data用户)对上传目录有写入权限。在Docker中,需通过volumes映射确保宿主机目录权限正确。

这套基于PHP的开源OA系统设计源码,从架构设计到代码实现,都力求在实用性、可扩展性和安全性之间找到平衡。它可能不是功能最全面的,但希望其清晰的设计和可读的代码,能成为一个可靠的起点,帮助开发者快速构建出符合自身需求的办公自动化平台,或者作为一个深入理解PHP企业级应用开发的优秀学习样本。在实际使用中,最重要的是根据团队的实际情况,对流程、权限和界面进行持续的迭代和打磨。

本文还有配套的精品资源,点击获取

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

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

立即咨询