ARTICLE DETAIL

资讯详情

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

基于ThinkPHP6+Layui的开源企业OA系统:部署、二次开发与避坑指南

基于ThinkPHP6+Layui的开源企业OA系统:部署、二次开发与避坑指南 简介勾股OA是一套基于ThinkPHP6 Layui MySql构建的开源企业办公系统主要面向需要快速搭建OA、CRM、ERP等业务管理平台的中小企业及开发者。系统集成了系统设置、人事管理、消息管理、审批管理、日常办公、客户管理、合同管理、项目管理、财务管理等模块结构清晰、易于扩展便于二次开发时聚焦深度业务需求。压缩包内共1017个文件约10.42MB以330个html页面、276个js脚本、193个php后端文件及84个css样式为主并包含sql数据库脚本、json配置等整体目录组织规范可直接部署或按需改造。目前已有525人学习下载适合作为企业级办公自动化的基础框架或用于学习ThinkPHP6与Layui的整合实践。1. 为什么选 ThinkPHP6 Layui 做企业 OA这套开源系统能帮你省掉什么很多公司审批流程还靠 Excel 传阅、微信群里人确认真正把办公自动化跑起来的中小企业并不多。基于 ThinkPHP6 Layui MySQL 的开源企业办公系统勾股 OA把系统设置、人事、消息、审批、日常办公、客户、合同、项目、财务九个模块完整装进一个源码包。它不是那种只有登录页的半成品——后台和管理员体系齐全拿来就能部署二次开发也顺手。对独立开发者、外包团队和公司内部技术岗来说从零写一套 OA 至少要两个月在这套源码上做业务扩展一周能出可用版本。前端 Layui 组件成熟后端 ThinkPHP6 分层清晰把几张业务表改一改就能扩展成 CRM、ERP 甚至更垂直的业务管理系统。下面按部署、源码解读、二次开发实战、避坑、上线优化一路走完。2. 部署勾股 OA从 PHP 版本到 MySQL 初始化的完整路径2.1 环境版本选择ThinkPHP6 对 PHP 的要求比想象中严格ThinkPHP6 官方要求 PHP 7.2.5这个门槛卡住过不少人。如果你本机还停留在 PHP 5.6 或 7.0前端页面还没看到入口文件就直接parse error了——这是最典型的部署失败信号。我一般建议直接装 PHP 7.4这是 TP6 生态里最稳的版本所有第三方依赖包的兼容性都不用操心。PHP 8.0 / 8.1 也能跑但个别老项目的 Composer 依赖包在 8.x 下会出现函数签名不兼容的报错非必要不折腾。数据库方面MySQL 5.7 和 8.0 都验证过。5.7 内存占用小、sql_mode默认值友好适合 2 核 4G 以下的小服务器8.0 性能和 JSON 支持更好但默认的caching_sha2_password认证插件会让部分 PHP 老驱动直接连不上这个坑在第五章专门讲解决思路。Layui 这边就省心了它是纯静态 UI 组件库不依赖 Node、npm、Webpack 那套编译链路浏览器加载即用这也是它做管理后台比 Vue/React 起步快的核心原因。2.2 数据库初始化SQL 导入、utf8mb4 与 .env 参数拿到源码包后先建数据库。注意字符集一定用 utf8mb4别用 utf8CREATE DATABASE IF NOT EXISTS gougu_oa DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这里不用 utf8 的原因很实际审批表单、合同内容里随时可能输入 emoji 或生僻字utf8 只支持 3 字节编码遇到 emoji 直接报Incorrect string value插入失败。utf8mb4 是 4 字节编码完整覆盖 Unicode加上utf8mb4_general_ci排序规则性能也够用。导入初始数据mysql -uroot -p gougu_oa /path/to/gougu_oa.sql导入完成之后改项目根目录的.env文件这是 ThinkPHP6 的全局配置入口APP_DEBUG true APP_TRACE false [DATABASE] TYPE mysql HOST 127.0.0.1 PORT 3306 DATABASE gougu_oa USERNAME root PASSWORD your_password CHARSET utf8mb4 PREFIX gougu_这里三个参数值得单独说。PREFIX gougu_是表前缀TP6 的Db::name(user)查询会自动拼成gougu_user前缀改了就全查不到。APP_DEBUG true开发期必须开报错页直接显示 SQL 语句和调用堆栈排查问题效率翻倍上线必须关掉否则 SQL 和服务器路径全部暴露给访问者。HOST写127.0.0.1而不是localhost是因为 PHP 在某些 Linux 环境下解析localhost会走 IPv6 的::1而 MySQL 默认不监听 IPv6 端口表现就是连接超时换了地址秒通。2.3 伪静态配置与第一个登录页TP6 默认入口在public/index.php直接访问/index.php/admin/index虽然能跑但 URL 带入口文件名既不专业也容易被批量扫描工具盯上。部署时一定要做伪静态隐藏 index.phpNginx 配置如下server { listen 80; server_name oa.example.com; root /www/wwwroot/oa/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }Apache 环境则在public目录放.htaccessIfModule mod_rewrite.c Options FollowSymlinks -Multiviews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L] /IfModule核心就一句话站点根目录必须指向public不是项目根目录。很多人第一次配 Nginx 把 root 写到项目根访问/admin/login直接 404这就是原因。配置完成后浏览器访问http://oa.example.com/admin/login看到登录页就说明整条链路通了。首次登录用的管理员账号一般在源码里的 README 或安装说明中登录后系统会强制要求改密码这一步别跳过。到这一步环境跑通接下来看这套系统的代码组织方式——这决定了二次开发能不能快速上手。3. 读源码模块划分、数据表设计和 Layui 的对接方式3.1 多应用目录结构新模块为什么不用改路由勾股 OA 源码用的是 ThinkPHP6 的多应用模式app/目录按业务切成若干应用解压后典型结构是这样app/ ├── admin/ │ ├── controller/ # 后台控制器 │ │ ├── System.php │ │ ├── User.php │ │ ├── Contract.php │ │ ├── Project.php │ │ └── Finance.php │ ├── model/ # 数据模型 │ └── view/ # 视图模板Layui 页面 ├── api/ # API 应用对接移动端或外部系统 ├── common/ # 公共模型与公共函数 └── index.php多应用模式带来的直接好处是 URL 三段式映射应用名/控制器/方法比如/admin/contract/index自动对应app\admin\controller\Contract::index()。新增一个业务模块不需要碰全局路由文件只要在admin/controller下加一个控制器写对应方法URL 就能访问到。这和我之前接手过的某些单应用老项目完全不是一个手感——那些项目每加一个页面就要在路由文件里登记一遍漏一条就 404。common/目录放的是跨应用共享的代码比如员工登录状态校验、上传处理、公共模型。如果你要加一个部门管理功能公共的部门模型放这里合适控制器放admin/controller视图放admin/view/。这块边界分清楚后面业务扩展不会越写越乱。3.2 核心数据表设计几个值得抄的字段约定办公系统业务表很多但核心设计思路是统一的。以这套系统里最有代表性的几张表为例表前缀 gougu_表名业务域核心字段gougu_admin / gougu_user系统与账号id, username, password, role_id, depart_id, status, last_login_timegougu_approve审批id, title, type, content, user_id, flow_id, status, approve_uid, approve_timegougu_customer客户管理id, name, phone, level, owner_uid, follow_status, sourcegougu_contract合同管理id, name, customer_id, amount, sign_time, start_time, end_time, statusgougu_project项目管理id, name, customer_id, manager_uid, progress, status, create_time两张表的字段约定值得抄作业。第一是状态字段status统一用tinyint(1)并且 0 永远是待处理正数表示通过/进行中负数表示驳回/作废/关闭。这样筛选列表时where(status, 0)含义稳定统计接口也统一。第二是时间字段用int(11)存 Unix 时间戳不是datetime。虽然datetime可读性好但int在跨时区处理、前后端传参、between范围查询时少很多格式转换的破事TP6 的toDate()格式化函数也够用。这套系统的金额字段我建议保持decimal(10,2)别用 float——合同金额求和时 float 精度误差会让你对不上账。3.3 Layui 表格与 ThinkPHP6 的 JSON 交互这套系统的后台列表页几乎都是同一个套路后端返回固定结构 JSON前端 Layui table 渲染。以合同列表为例layui.use([table, form, layer], function(){ var table layui.table; table.render({ elem: #contractTable, url: /admin/contract/lists, method: get, page: true, cols: [[ {field: id, title: 编号, width: 70}, {field: name, title: 合同名称, minWidth: 160}, {field: amount, title: 金额, width: 120}, {field: sign_time, title: 签订时间, width: 110}, {field: status, title: 状态, templet: #statusTpl}, {field: operate, title: 操作, toolbar: #contractBar} ]] }); });对应后端控制器public function lists() { $page input(page, 1); $limit input(limit, 10); $list Db::name(contract) -page($page, $limit) -select(); $count Db::name(contract)-count(); return json([ code 0, msg , count $count, data $list ]); }这段交互里有两个硬性约定。Layui table 要求返回结构必须是code、msg、count、data四个键code必须是 0 才算成功data键名不能改成rows或list改了表格直接白屏。另外分页参数默认叫page和limit这个由 Layui 自动携带后端取值名和它保持一致即可不需要前端再拼额外参数。搜索条件那边我习惯在lists方法里先做参数清洗再拼查询避免直接拼接where条件$name input(name, , trim); if ($name ! ) { $query-where(name, like, %{$name}%); }TP6 的查询构造器自带参数绑定where传入的%通配符不会被注入这是 ThinkPHP 框架帮我们兜底的第一层安全网。如果直接拼 SQL不传参绑定分分钟出 SQL 注入这条血泪经验后面避坑章节还会提到。4. 二次开发实战新增审批模块和自定义业务表的完整步骤4.1 从数据表开始设计请假审批先建表这是审批类模块的地基。用最常用的请假审批举例CREATE TABLE gougu_leave ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL DEFAULT 0 COMMENT 申请人ID, leave_type tinyint(1) NOT NULL DEFAULT 1 COMMENT 1事假 2病假 3年假, start_time int(11) NOT NULL DEFAULT 0 COMMENT 开始时间戳, end_time int(11) NOT NULL DEFAULT 0 COMMENT 结束时间戳, reason varchar(500) NOT NULL DEFAULT COMMENT 请假原因, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回, approve_uid int(11) NOT NULL DEFAULT 0 COMMENT 审批人ID, approve_time int(11) NOT NULL DEFAULT 0 COMMENT 审批时间戳, create_time int(11) NOT NULL DEFAULT 0 COMMENT 提交时间戳, PRIMARY KEY (id), KEY idx_status (status), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT请假审批表;状态机设计是审批模块的灵魂。status 0永远表示待审批1 和 2 分别是通过、驳回。approve_uid和approve_time留空字段给审批动作落账这样员工可以在列表页看到审批人是谁、什么时候处理的历史追溯不用翻日志。索引方面idx_status和idx_user_id必须建——审批列表页最常见的查询就是我发起的和待我审批的没有这两个索引数据量过十万后一次列表查询能慢到秒级。提交流程的控制器代码public function add() { $data [ user_id session(uid), leave_type input(leave_type, 1, intval), start_time strtotime(input(start_time)), end_time strtotime(input(end_time)), reason input(reason, , trim), create_time time(), ]; if ($data[end_time] $data[start_time]) { return json([code 1, msg 结束时间必须晚于开始时间]); } Db::name(leave)-insert($data); return json([code 0, msg 提交成功]); }strtotime把前端传的时间字符串统一转成时间戳存库查询时再date()格式化输出避免不同浏览器、不同时区导致的格式差异。插入前先校验结束时间晚于开始时间这是一层便宜的防错——表单校验在前端做了一层后端这层才是真正防脏数据的防线。审批动作的控制器public function audit() { $id input(id, 0, intval); $status input(status, 0, intval); if (!in_array($status, [1, 2])) { return json([code 1, msg 非法状态]); } $info Db::name(leave)-find($id); if (!$info || $info[status] ! 0) { return json([code 1, msg 审批状态已变更请刷新后再试]); } Db::name(leave)-where(id, $id)-update([ status $status, approve_uid session(uid), approve_time time(), ]); return json([code 0, msg 审批完成]); }这个接口的关键是幂等控制先查一次当前状态只有待审批status 0的才能更新避免两个人同时审批同一张单子互相覆盖。出现状态已变更提示时前端刷新列表重查即可这是审批系统最基础也最重要的一致性保护。前端用 Layui 表单 laydate 做时间选择form classlayui-form lay-filterleaveForm {:token()} div classlayui-form-item label classlayui-form-label开始时间/label div classlayui-input-block input typetext namestart_time idstart_time placeholder选择开始时间 classlayui-input /div /div div classlayui-form-item label classlayui-form-label结束时间/label div classlayui-input-block input typetext nameend_time idend_time placeholder选择结束时间 classlayui-input /div /div div classlayui-form-item label classlayui-form-label请假事由/label div classlayui-input-block textarea namereason placeholder请输入请假原因 classlayui-textarea/textarea /div /div div classlayui-form-item div classlayui-input-block button typebutton classlayui-btn lay-submit lay-filterleaveSubmit提交审批/button /div /div /form配套 JSlayui.use([form, laydate, jquery], function(){ var form layui.form; var laydate layui.laydate; var $ layui.jquery; laydate.render({elem: #start_time, type: datetime}); laydate.render({elem: #end_time, type: datetime}); form.on(submit(leaveSubmit), function(data){ $.post(/admin/leave/add, data.field, function(res){ if (res.code 0) { layer.msg(提交成功); // 刷新列表 } else { layer.msg(res.msg); } }); return false; }); });{:token()}是 ThinkPHP6 的表单令牌隐藏域配合后端开启的 token 验证可以有效防 CSRF。如果你改了表单结构这个隐藏域别删删了后端验证直接拒掉提交。4.2 新增固定资产登记模块的完整步骤新增业务模块的套路在 TP6 多应用模式里高度统一我拆成四步第一步建表参照 4.1 的表结构规范补上is_del软删除标记ALTER TABLE gougu_asset ADD COLUMN is_del tinyint(1) NOT NULL DEFAULT 0 COMMENT 0正常 1已删除;软删除比物理删除好用的地方在于员工误删的资产记录可以事后恢复审计上也有据可查。TP6 模型里开启软删除namespace app\admin\model; use think\model\concern\SoftDelete; class Asset extends Base { use SoftDelete; protected $deleteTime is_del; }第二步写控制器列表 新增 编辑 删除四个标准方法。第三步视图模板按 4.1 的 Layui 表单套路写。第四步在后台系统设置 → 菜单管理里加菜单项填模块名和方法名角色授权后就能在左侧菜单看到。这套流程跑熟之后一个标准业务模块从建表到能提交数据半天足够。真正花时间的不是 CRUD而是业务校验规则和数据关联设计。4.3 权限校验怎么挂到新控制器上OA 系统的权限不能只做前端隐藏后端每个控制器方法都要校验。这套系统的做法是后台控制器继承一个公共基类比如app\admin\controller\Base基类的初始化方法里做两件事protected function initialize() { parent::initialize(); // 1. 检查登录状态 if (!session(uid)) { return redirect(/admin/login); } // 2. 检查权限当前控制器/方法是否在角色的授权范围内 $request request(); $rule $request-controller() . / . $request-action(); if (!checkAuth($rule)) { throw new \think\exception\HttpException(403, 无权访问该功能); } }checkAuth是系统公共函数核心逻辑就是查角色表关联的权限集合判断当前规则是否在集合内。原理是典型的 RBAC用户 → 角色 → 权限表三层关联角色勾选权限用户挂角色权限变更不需要改代码。新加的Asset控制器只有继承了 Base天然纳入权限体系权限的初始化数据在菜单管理里分配好改角色勾选即可。这里一个容易踩的细节TP6 控制器方法名是驼峰还是下划线直接决定权限规则的写法。如果 URL 是/admin/asset/add_asset那checkAuth里的规则名就要写成add_asset中间别混淆。菜单里填什么权限里就对比什么前后保持一致。5. 避坑记录部署和二次开发中最容易翻车的 5 个问题5.1 PHP 版本太老导致入口直接报错现象浏览器访问站点返回 500 错误查看日志看到PHP Parse error: syntax error, unexpected ...有时候连错误页都是白屏。原因ThinkPHP6 要求 PHP 7.2.5老版本服务器默认装的是 PHP 5.6 / 7.0TP6 源码里的现代语法解析不了直接语法错误。解决把 PHP 升级到 7.4重新安装扩展php-mysql、php-gd、php-bcmath然后重启 PHP-FPM。升级后建议顺手清理一下旧的 opcache 缓存否则新版本代码可能仍然执行旧缓存表现成改了代码没生效。5.2 MySQL 8.0 认证失败端口通但登录不了现象.env配好之后后台能打开但任何查询都报SQLSTATE[HY000] [2054] Authentication plugin caching_sha2_password cannot be loaded或类似的握手失败。原因MySQL 8.0 默认认证插件是caching_sha2_password而 PHP 7.4 的mysqlnd驱动对它的支持不完整握手协议对不上。解决把账号改回旧式认证或者为 OA 单独建一个使用旧认证的账号。单独建账号更干净CREATE USER gougu% IDENTIFIED WITH mysql_native_password BY gougu_123456; GRANT ALL PRIVILEGES ON gougu_oa.* TO gougu%; FLUSH PRIVILEGES;然后把.env的USERNAME和PASSWORD改成这个新账号。除非你要保留 MySQL 8.0 的密码新特性否则这套系统用mysql_native_password最省心。5.3 伪静态没配好路由全部 404现象Nginx 下访问/admin/login报 404但直接访问/index.php/admin/login正常。原因Nginx 配置里root指向了项目根目录而没指向public或者 location 里没加 rewrite 规则请求的/admin/login不匹配任何真实文件被 Nginx 当成静态资源 404 了。解决root改成/www/wwwroot/oa/publiclocation 加 rewrite 规则。配完记得nginx -s reload很多配了没生效其实是没重启服务。5.4 Layui 静态资源加载不出来样式崩掉现象登录页能打开但完全没有样式控制台 Network 面板一堆static/...资源 404。原因绝大多数情况是站点部署在了子目录比如http://ip:8080/oa/而布局模板里的静态资源写的是绝对路径/static/xxx.css浏览器按域名根去请求了。解决要么把站点单独绑定一个域名并让站点根指向public要么在视图模板头部用框架提供的 URL 函数生成静态资源路径写成link relstylesheet href{:asset(layui/css/layui.css)}这样路径会自动带上子目录前缀。血泪经验资源加载问题先看 Network 面板里的完整 URL90% 是路径问题不是权限问题。5.5 表单令牌 Token 校验失败现象后台表单提交后提示表单令牌校验失败或者直接跳到一个错误页刷新页面再提交又正常了。原因TP6 默认开启了表单令牌验证页面里必须有{:token()}隐藏域token 存在 Session 里单次会话有效期比较短如果你把页面挂在浏览器里隔了很久才提交或者前端用了$.post自动提交但没有带上 token 字段后端就校验不过。解决表单里保留{:token()}隐藏域Ajax 提交时从表单序列化数据里带上它var field $(#leaveForm).serialize(); $.post(/admin/leave/add, field, function(res){ // 处理返回 });serialize()会把隐藏域的 token 一并带上比自己手动拼参数省心且不容易漏。6. 上线前做三件事缓存、备份和索引检查6.1 给 TP6 开缓存别让配置和菜单每次都查库后台系统查询频繁的是菜单、配置项、角色权限这几类短小且不常变的数据。TP6 天然支持缓存门面不用动数据库直接用文件缓存就能缓解 90% 的重复查询压力use think\facade\Cache; // 读取菜单缓存 1 小时 $menus Cache::get(menu_ . session(role_id)); if (!$menus) { $menus Db::name(menu)-where(status, 1)-order(sort asc)-select()-toArray(); Cache::set(menu_ . session(role_id), $menus, 3600); }文件缓存适合单机部署Redis 适合以后并发上来再说。菜单这种数据用文件缓存完全够别一上来就上 Redis增加运维负担且收益不明显。6.2 定时备份数据库与上传目录上线后最重要的事不是写新功能是备份。用 mysqldump 做每日凌晨全量备份#!/bin/bash # /root/backup_oa.sh BACKUP_DIR/data/backup/oa mkdir -p ${BACKUP_DIR} mysqldump -uroot -pyour_password gougu_oa | gzip ${BACKUP_DIR}/oa_$(date %F).sql.gz find ${BACKUP_DIR} -type f -name *.sql.gz -mtime 14 -delete最后一行是保留 14 天自动清理防止备份文件把磁盘塞满。crontab 加一行0 2 * * * /bin/bash /root/backup_oa.sh /dev/null 21上传的附件目录合同扫描件、头像、审批附件也要单独 rsync 到异机数据库和文件只要丢一个恢复工作都很痛。6.3 索引复查列表页慢多半是缺索引上线前最后一个习惯是检查大表的索引。审批、合同、客户这几张表是最容易膨胀的业务表用一条 SQL 就能看清哪些查询缺索引SHOW INDEX FROM gougu_contract;然后针对高频查询条件补索引。审批列表最常见的慢查询就是按user_id加create_time排序ALTER TABLE gougu_contract ADD INDEX idx_owner_uid_time (owner_uid, create_time);注意大表上的 ALTER 会锁表生产环境放在凌晨低峰执行别在上班时间直接怼上去。以前我接手过一个外包项目一张审批表三年涨到 400 多万行列表查询要 8 秒加完一个联合索引直接降到 200 毫秒。从那以后我每次部署 OA 都会强制走一遍备份、缓存和索引检查这三件事代码写得再好运营阶段不管数据库迟早要还债。希望帮到你。本文还有配套的精品资源点击获取
返回列表