☰
开源OA系统二次开发实战:基于ThinkPHP6+Layui的部署与优化
2026/10/22 4:58:08 网站建设 项目流程

简介:基于ThinkPHP6+Layui+MySql构建的开源企业办公系统,面向中小企业和开发者,提供开箱即用的OA解决方案,覆盖日常办公、审批、人事、客户、合同、项目、财务等核心模块,可用于快速搭建企业级自动化办公平台,也适合作为二次开发基底扩展为CRM、ERP等业务管理系统。包体共997个文件,zip压缩包约10.3MB,以HTML页面、JavaScript交互脚本、PHP后端逻辑、CSS样式为主,另含SQL数据库脚本、配置文件及少量图标字体资源,目录划分清晰,便于定位前后端代码和初始化数据。已有319人学习/下载,适合需要了解ThinkPHP6框架实践、企业内部系统整包部署或在此基础上做功能定制的开发者。通过资源可获取完整可运行的项目代码、数据库结构及基础配置说明,若在此基础上继续扩展,可在较短时间内搭建起适合自身业务的办公或管理系统。

1. 这套企业办公系统到底解决了什么痛点

一家几十人的公司要上办公自动化,不是钱的问题,是没有合适的系统。买一套商业OA,按账号收费,一年下来预算不低,想改个审批流程还得提工单等排期;用钉钉或企微的免费版,考勤和审批是够用了,可数据都在别人平台上,想接自己内部的客户系统、想自定义报表,处处受限。我见过不少团队最后就靠微信群+Excel滚动排期,审批靠口头,考勤靠自觉,公告靠@全体成员——实际上全员都在为流程混乱买单。

勾股OA正是冲着这个缝隙来的:开源、基于ThinkPHP6+Layui+MySQL,部署到你自己的服务器,数据自己掌握,审批流程、组织架构、考勤规则都能改代码改表,不依赖任何外部平台。它覆盖常用审批、工作汇报、任务协作、公告通知、日程管理等企业日常所需的办公模块,拿来就能用,二次开发的门槛也不高。这篇笔记先把这套系统的技术结构和落地步骤讲清楚,再把我实际部署和改动过程中的踩坑记录写出来,最后给出一套生产环境可以参考的优化与备份习惯。适合准备自建OA、正在选型或已经在用勾股OA但想深度改造的开发者。

2. 技术选型与核心架构:为什么是ThinkPHP6+Layui+MySQL这套组合

2.1 选型逻辑:ThinkPHP6、Layui、MySQL各自承担了什么角色

一套OA系统本质上围绕三类对象转——人、流程、数据。人靠组织架构和权限管理,流程靠审批引擎和状态流转,数据靠业务表的读写与统计。勾股OA把这三块分别托付给了三个成熟组件:ThinkPHP6负责后端逻辑与接口,Layui负责后台页面的UI渲染,MySQL负责所有业务数据落地。

ThinkPHP6是国内的PHP开发框架,社区活跃,中文文档完整,Composer管理依赖,ORM、验证器、中间件这些日常功能开箱即用。选它做后端,对国内开发团队来说维护成本低——招人容易,遇到问题搜得到答案,不用自己封装底层。Layui是另一款国产前端框架,主打后端友好的理念,不用学Vue那一套响应式思维,页面结构直接用HTML+模板引擎就能拼出来,表格、表单、弹层这些OA场景的高频组件都有现成封装。MySQL更不用多说,企业场景里最常见的关系型数据库,稳、省资源、生态成熟。

这套组合的核心价值在于匹配OA系统的开发形态。办公系统页面数量多但交互深度不高,大量页面是「表格+表单+详情」的重复模式。Layui的表格组件配合ThinkPHP6的分页接口,十几行代码就能出一个能筛选、能排序、能批量操作的管理列表页。如果你选的是前后端分离架构+Vue+SpringBoot,技术上是更现代,但同样的页面要写两套接口、处理跨域、维护构建产物,初期成本明显更高。

2.2 核心模块拆解:一套OA系统里必须有哪些零件

一个能真正用于企业办公的OA系统,至少要覆盖组织、流程、协作、行政四类场景。勾股OA的模块设计也是按这条线展开的。

组织层面是部门管理和用户管理。部门表负责维护树形结构,用户表挂在部门下,角色表做权限分组,再通过用户-角色关联表把权限分配给具体的人。这四张表构成了权限体系的地基。

流程层面的核心是审批。包括请假、出差、报销、用章、采购申请等常见审批类型,每类审批都是一套独立的业务表单加上一条独立的状态流转记录。它的设计思路是:业务数据存在对应的业务表里,而状态流转(待审、通过、驳回、撤回)由通用的审批记录表统一管理。这样新增一种审批类型时,只需要建一张业务表加一套对应的控制器逻辑,不需要动审批引擎本身。

协作场景对应工作汇报和任务管理。日报、周报、月报是汇报模块的三种粒度,任务则围绕项目或部门展开,支持派发、截止日期、进度更新。行政模块里则包含公告通知、日程安排、会议管理、物资领用、车辆申请等功能。

2.3 数据库表设计的基本盘

我在部署前先梳理了它的核心数据表,这个动作对后续二次开发非常重要。用户表、部门表、角色表是基础三件套,关联关系清晰;审批相关的表按业务类型分表,比如请假申请表、报销申请表、出差申请表,各自独立,互不干扰。

以请假申请为例,核心字段包括申请人ID、部门ID、请假类型、开始时间、结束时间、请假事由、状态字段、当前审批人ID。状态字段是整条审批流程的灵魂,代码里通常用数字表示:0待审、1通过、2驳回。当流程涉及多级审批时,还会有一个当前审批层级字段,每次通过后自动加一,直到达到设定阈值才算审批完成。

这套表设计整体是够用的,结构简单,理解成本低。但有一个明显的问题:报销、采购这类业务数据表之间没有做统一的关联设计,未来如果要跨模块统计数据,比如按部门和月份汇总所有人的报销金额和出差费用,需要自己写SQL把多张表union起来,这是后续二次开发中需要重点处理的地方。

3. 本地部署与初始化:先把系统在电脑上跑起来

3.1 环境准备:PHP版本与扩展检查清单

部署前先确认环境,避免装到一半被PHP版本卡住。我这边建议的本地环境是PHP 7.4或8.0,MySQL 5.7及以上,Web服务器Apache或Nginx均可。PHP需要启用的扩展包括PDO、pdo_mysql、mbstring、curl、openssl、fileinfo,这几个缺一不可。

# 查看PHP版本 php -v # 检查关键扩展是否已启用 php -m | grep -E '^(PDO|pdo_mysql|mbstring|curl|openssl|fileinfo)$'

PDO和pdo_mysql是数据库驱动,没启用的话系统一连接数据库就报错。mbstring负责多字节字符串处理,涉及中文截断和字符编码判断。fileinfo用于上传附件时的MIME类型检测,不启用会导致文件上传功能异常。如果用的是宝塔面板,PHP扩展可以直接在面板的软件商店里一键安装,省去编译的麻烦。

3.2 获取项目代码并安装依赖

代码获取和依赖安装是整个部署流程里最标准化的步骤。勾股OA本身是一个完整的Web项目,需要先下载代码放到Web目录,然后通过Composer安装ThinkPHP6框架依赖,再配置数据库连接和伪静态规则。

# 以某宝塔面板环境为例,回到网站根目录 cd /www/wwwroot # 拉取项目代码到本地目录(项目名为模拟项目X) git clone https://gitee.com/example/gougu-oa.git gougu_oa # 进入项目目录,用国内镜像安装PHP依赖 cd gougu_oa composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ composer install

Composer安装依赖这一步最容易出问题。网络超时、内存不足、vendor目录权限不对,都会导致安装失败。如果你没有配置过Composer国内镜像,直接执行composer install大概率会因为网络原因卡住。设置镜像源后速度会明显提升。如果VPS内存低于1G,还需要临时调高PHP的内存限制,否则安装过程中可能报内存耗尽。

3.3 环境配置与数据库导入

依赖装完后,需要复制一份环境配置模板,写入数据库连接信息。ThinkPHP6的配置方式是.env文件和config目录下的database.php,勾股OA在.env里存放数据库账密、调试开关、日志路径等基础配置。把模板文件复制一份再改就行,不要直接改原模板。

# 复制环境配置模板 cp .env.example .env # 编辑.env,填入你的数据库信息 vim .env

.env文件里需要改动的核心项包括数据库主机地址、端口、库名、用户名、密码,以及一个app_debug项。app_debug在本地调试阶段建议设为true,这样页面会展示详细的错误堆栈,定位问题非常方便。但正式上生产环境时一定要改回false,否则会把数据库字段名和代码路径暴露出去。

数据库初始化是在phpMyAdmin或命令行里创建数据库,再把项目根目录下的SQL文件导入。勾股OA的SQL文件包含全部数据表和初始数据,导入后会自动生成一个默认的管理员账号,账号密码在SQL文件头部注释里有标注。登录后台后第一时间把默认密码改掉,这个习惯能避免很多不必要的麻烦。

3.4 伪静态配置与首次登录

最后一步是配置伪静态。ThinkPHP6默认的URL模式是入口文件加参数的形式,比如/index.php/admin/login/index。通过伪静态可以去掉index.php,把URL变成/admin/login/index,观感和分享链接的友好度都会好很多。Nginx的配置方式和Apache略有差异,需要按实际使用的Web服务器来写。

# Nginx伪静态规则,location段内添加 location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }

配置完重载Nginx,浏览器访问http://你的域名/admin,应该能看到登录页。用SQL里默认的管理员账号登录后,第一件事是去系统设置里检查站点名称、时区、附件存储路径。附件存储路径如果默认是相对路径,在后续使用中容易出现文件访问不到的情况,建议直接改成绝对路径或者购买单独的存储空间来做对象存储。

4. 二次开发实战:新增一个「采购申请」审批类型

4.1 开发前先看清现有逻辑:审批流程的三层结构

二次开发的第一步不是写代码,而是先搞清楚现有审批的运转方式。勾股OA的审批体系可以拆成三层,理解了这三层,新增一个审批类型就变成一个填模板的工作。

第一层是流程定义层。系统后台有一个审批类型管理页面,管理员可以在里面新增、编辑、停用某个审批类型,并设置名称、图标、是否启用。这层操作走的是通用配置表,不需要动代码。第二层是业务数据层。每个具体的审批(比如某员工填写了一张请假单)对应一张业务表记录,存储申请内容、申请人、时间字段。第三层是审批流层层。一条通用审批记录,负责承接状态流转。当用户在请假单上点击提交,会生成一条审批记录,当前状态为待审;审批人通过或驳回时,更新这条记录的状态。

新增「采购申请」审批类型,需要做的就是:建一张采购申请表、建一个控制器、写一套列表和表单视图、在审批类型管理里注册一个新条目。前四步靠代码,最后一步靠后台界面,整套逻辑完全可以复用请假审批的思路。

4.2 建表与模型:采购申请的数据结构

先建数据表。字段设计参照请假表的风格,但根据采购业务的特点做了调整。核心字段包括:申请人ID、部门ID、采购事由、物品清单(用JSON或长文本存储)、预估金额、期望到货日期、状态、当前审批层级。

CREATE TABLE `gou_ask_purchase` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `uid` int(11) NOT NULL COMMENT '申请人ID', `dept_id` int(11) NOT NULL COMMENT '部门ID', `purchase_reason` varchar(500) NOT NULL COMMENT '采购事由', `items` text COMMENT '物品清单,JSON格式', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '预估总金额', `expect_date` date DEFAULT NULL COMMENT '期望到货日期', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0待审,1通过,2驳回', `audit_level` tinyint(1) NOT NULL DEFAULT '1' COMMENT '当前审批层级', `create_time` int(11) NOT NULL COMMENT '创建时间', `update_time` int(11) NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购申请表';

金额字段用decimal而不是float,因为浮点数在精度上存在误差,涉及金额的数据字段必须用定点数存储。物品清单用text类型存JSON字符串,方便前端一次性展示结构化数据,但要注意:查询时不能对items字段做条件筛选,需要单独抽取物品名称、分类字段再做过滤。

表建完后,在应用目录下新增对应的模型文件。ThinkPHP6的模型继承baseModel即可。如果希望在写入时间字段时自动填充,可以在模型里打开自动时间戳功能,数据库字段名create_time和update_time正好是框架默认识别的格式。

<?php namespace app\common\model; class AskPurchase extends Base { protected $name = 'ask_purchase'; protected $autoWriteTimestamp = true; }

4.3 控制器与提交逻辑:最核心的审批提交动作

控制器文件是采购申请的后端逻辑入口。在app目录下的admin模块中新建一个AskPurchase控制器,继承后台基础控制器。我们需要实现三个方法:index列表页、add新增页、doAdd处理提交。列表页负责渲染表格数据,新增页负责渲染表单,doAdd接收前端POST数据后落库。

提交逻辑里需要做一个关键动作:写采购申请表数据的同时,向审批记录表插入一条待审记录。这两步必须放在同一个数据库事务里,保证原子性。如果只写了采购表却忘了写审批记录,这条采购申请就成了「不可见」的脏数据,任何人也审不了。

public function doAdd() { if (!$this->request->isPost()) { return json(['code' => 0, 'msg' => '请求方式错误']); } $data = $this->request->post(); $validate = new \app\common\validate\AskPurchase(); if (!$validate->check($data)) { return json(['code' => 0, 'msg' => $validate->getError()]); } // 组装采购数据 $purchaseData = [ 'uid' => session('uid'), 'dept_id' => session('dept_id'), 'purchase_reason' => $data['purchase_reason'], 'items' => json_encode($data['items'], JSON_UNESCAPED_UNICODE), 'total_amount' => $data['total_amount'], 'expect_date' => $data['expect_date'], 'status' => 0, 'audit_level' => 1 ]; Db::startTrans(); try { $purchaseId = Db::name('ask_purchase')->insertGetId($purchaseData); // 向审批记录表插入待审记录 Db::name('approval_record')->insert([ 'type' => 'purchase', 'business_id' => $purchaseId, 'uid' => $purchaseData['uid'], 'status' => 0, 'create_time' => time() ]); Db::commit(); return json(['code' => 1, 'msg' => '提交成功']); } catch (\Exception $e) { Db::rollback(); return json(['code' => 0, 'msg' => '提交失败:' . $e->getMessage()]); } }

事务的意义在采购场景里体现得很具体:如果提交采购时同时要做库存预占或者预算锁定,任何一步失败都必须回滚全部操作。这里我用Db::startTrans和Db::rollback做了最基础的保护。真正的大型系统还会引入最终一致性方案,但对OA这种轻量场景,数据库事务已经足够可靠。

4.4 Layui表格与表单页面:快速拼出管理界面

后端接口写好后,前端页面用Layui来渲染。列表页的核心是一个数据表格,它通过接口获取数据,自动处理分页、排序和状态列的显示。采购申请列表包含申请人、部门、采购事由、金额、状态、操作列,典型的Layui表格配置如下。

<table id="purchaseTable" lay-filter="purchaseTable"></table> <script> layui.use(['table', 'form'], function(){ var table = layui.table; table.render({ elem: '#purchaseTable', url: '/admin/ask_purchase/index', cols: [[ {field: 'id', title: 'ID', width: 60}, {field: 'username', title: '申请人', width: 100}, {field: 'dept_name', title: '部门', width: 100}, {field: 'purchase_reason', title: '采购事由', minWidth: 200}, {field: 'total_amount', title: '金额', width: 100}, {field: 'status_text', title: '状态', width: 80}, {field: 'create_time_text', title: '申请时间', width: 120}, {fixed: 'right', title: '操作', toolbar: '#barDemo', width: 150} ]], page: true, limit: 20 }); }); </script>

这里有个细节需要注意:status字段在数据库里是数字,如果直接把0和1渲染到页面上,用户根本看不懂。我在列表接口里已经把status翻译成了「待审」「通过」「驳回」等中文文本,表格直接用status_text这一个字段就好。控制器里拼装额外字段,是后端做数据处理最常用的方式,能最大程度减少前端的判断逻辑。

表单页面同样简单,用Layui的form表单,字段和数据库一一对应。物品清单部分可以做成动态增行,一行填物品名、数量、单价,提交前由JS汇总自动填入total_amount。在展现形式上,这类表单与请假表单一模一样,只是字段不同。

4.5 审批类型注册与联动设置

代码写完后,最后一步不是刷新页面看效果,而是去后台「审批类型管理」中把这个新的采购申请注册进去。审批类型表里需要配置三样东西:类型标识(purchase,和前面代码里type字段对应)、显示名称(采购申请)、可用的审批流(比如一级审批还是二级审批)。

这一步做完,采购申请才算真正接入整个审批体系。用户在新建审批界面能看到「采购申请」入口,提交的申请自动进入待审列表,审批人打开后能看到与请假单类似的审批操作按钮。从建表到接入,整个流程走下来大约需要半天时间,这半天时间已经把勾股OA的二开套路摸透了七成。

5. 部署与开发中的常见问题排查:五条踩坑记录

5.1 伪静态没配好:登录页能打开但登录后跳回登录页

现象:浏览器访问登录页正常,输入账号密码后页面刷新一遍又回到登录页,后台登录不进去。

原因:登录接口本身是通的,但登录成功后跳转的目标地址因为没有伪静态支持,URL是带index.php的长地址,与当前站点URL规则不匹配。或者反过来,当前环境不支持pathinfo格式,框架生成的跳转地址解析不到对应的控制器。这类问题在Nginx上尤其常见,Apache环境下通常不会有。

解决:检查Nginx配置文件,确认location段内已经加入了对应站点的伪静态规则。如果规则没问题,再检查一下站点配置里的root路径是否指到了public目录。ThinkPHP6的入口文件在public下,如果不把站点根目录指到public,控制器就无法正确路由。另外查看.env里是否开了调试模式,开着的话登录后看浏览器地址栏的跳转路径,直接能看出URL格式是否异常。

5.2 上传附件提示IO错误:目录权限与路径的双重坑

现象:附件上传失败,返回IO错误或者找不到文件路径的报错。

原因:大部分情况是附件目录没有写权限。勾股OA默认把附件存在项目根目录下的uploads文件夹里,Web运行用户(通常是www)如果没有该目录的写权限,文件就写不进去。还有一部分情况是PHP配置里upload_tmp_dir指向的临时目录不可写,导致文件在服务端落盘之前就被拒了。

解决:先给uploads目录设置写权限,命令为chown -R www:www uploads和chmod -R 755 uploads。如果还不行,打印PHP的upload_tmp_dir配置,确认临时目录存在且可写。还有一种隐蔽情况是磁盘满了,当时我怎么查都查不到原因,最后df -h一看根分区用了100%,清理日志后才恢复正常。

5.3 定时任务不执行:命令行访问路径与web端不一致

现象:工作汇报的提醒通知一直发不出来,计划任务里配置的定时脚本在命令行手动执行正常,但到了设定时间就是不动。

原因:计划任务脚本内部使用了相对路径定位应用目录,从crontab启动时的工作路径与手动执行时不同,导致脚本找不到入口文件。这类问题在LNMP环境下很典型,根因是路径写法不够严谨。

解决:在crontab里统一使用绝对路径,切换到项目根目录后再执行PHP命令。例如cd /www/wwwroot/gougu_oa && php think cron。写成一行命令,不要省略cd这一步。同时确认脚本执行时使用的PHP版本与Web环境一致,我遇到过命令行默认PHP是5.6而项目需要7.4的情况,执行直接报语法错误。

5.4 中文内容变成乱码:字符集设置不一致

现象:插入数据库的中文数据变成了问号。

原因:数据库表、数据库连接、页面请求三个环节的字符集不一致。最常见的情况是数据库表设置的是utf8mb4,但数据库连接参数里写的是utf8,连接层发生了字符集转换,中文被截断。另一种情况是页面请求没有带charset参数,系统按ISO-8859-1处理了请求体。

解决:统一使用utf8mb4,包括表结构、数据库连接DSN、Nginx配置里的default_charset三项。修改后需要清掉已有的乱码数据,重新插入测试。注意utf8和utf8mb4在MySQL里是两个东西,utf8mb4才真正完整支持四字节emoji和生僻字。

5.5 修改代码后页面没变化:框架路由缓存或浏览器缓存

现象:改完控制器方法,刷新页面还是旧逻辑,看代码明明已经改了。

原因:ThinkPHP6的路由缓存机制。应用在调试模式关闭后可能已生成了路由缓存文件,如果新增了控制器方法或改了路由定义,旧缓存还在生效。浏览器端则可能是静态资源(尤其js和css)被本地缓存,Layui的模块加载机制里,修改js文件后经常出现新代码不生效的情况。

解决:清理runtime目录下的cache和route缓存文件,只保留.gitignore文件占位。浏览器端则通过修改静态资源的版本号参数强制刷新,比如在HTML里给js链接加上?v=20250115这样带日期后缀的版本号,线上部署时直接改这个时间戳就能让用户拉到新资源。

6. 进阶技巧:上线前必须做的三件事

系统功能开发完、测试通过,不代表可以上线。还有三件事不在功能开发范围内,但是如果你跳过它们,大概率会在上线后的某个深夜被钉钉消息吵醒。

第一件事是备份策略落地。我见过有人上线后一次备份都没做过,直到某天误操作把数据表删了,才意识到备份的重要性。针对勾股OA这种规模的MySQL库,最简单的方案是每天凌晨用mysqldump导出全量SQL,保留最近7天或者30天的备份文件,再定期把备份文件传送到另一台机器或对象存储。一条crontab加一行shell脚本就能搞定,成本极低,收益极高。

第二件事是Nginx层面的性能调优。OA系统的主要使用者是公司内部员工,并发量通常不高,但需要关注的是慢查询和图片附件加载。Nginx开启Gzip压缩能明显减小HTML和JS的传输体积,静态资源设置合理的浏览器缓存过期时间能减少重复请求。数据库的slow_query_log建议打开,日志文件定期分析,把执行时间超过1秒的SQL单独捞出来,看是不是缺索引或者有不必要的大表全扫描。

第三件事是二次开发规范的建立。团队的加班往往不是因为需求多,而是因为代码不可维护。在勾股OA上做二次开发时,我习惯把每个新增模块的控制器、模型、视图放在不同命名的目录中,并且不改动原系统的基础表结构,宁可新增关联表也不在原表里乱加字段。这样上游如果发布了新版本,升级代码时不会因为本地改动过多而冲突。我自己后来做过一次版本升级,因为当时没有遵守这个原则,结果迁移脚本写了整整两天。

这套系统的真实门槛并不高。只要你能跑通一个带事务的控制器方法,理解审批记录与业务表之间的关系,再结合Layui的表格和表单组件,就能在几天内从零搭建出一套完全贴合公司流程的办公系统。过程中后续会遇到权限控制的细枝末节,也会遇到个把隐藏的坑,但这些都是每个自建OA项目都会经历的成长路径。希望这篇笔记能帮你少走几步弯路,也希望你在自己的部署过程中找到更合适的解法。

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

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

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

立即咨询