资讯动态

ThinkPHP6+Layui企业OA系统实战:从环境搭建到二次开发全流程

发布时间:2026/10/22 4:02:01 来源:尧图企业网站定制
简介这是一套面向中小企业开发者与信息化管理人员的开源企业办公自动化系统源码基于ThinkPHP6、Layui与MySQL构建集成系统设置、人事管理、消息管理、审批管理、日常办公、客户管理、合同管理、项目管理、财务管理等核心模块结构简约、易于扩展适合二次开发为CRM、ERP或业务管理系统。压缩包共1017个文件约10.42MB以330个html页面模板、276个js脚本、193个php后端逻辑、84个css样式为主另含sql建表脚本、字体图标、图片资源及项目说明文档前后端与数据库脚本齐备。已有526人学习下载。借助这套源码读者可快速搭建企业级办公自动化平台理解模块化目录组织与权限审批流程并在此基础上按业务深度需求进行定制开发节省数字化办公成本。1. 从一堆 CSS 文件说起这套 OA 系统到底能干什么如果你拿到一份源码解压后第一眼看到的是layui.css、editormd.min.css、skin.min.css、ambiance.css这一串样式文件大概率会以为这只是某个后台模板的换皮包。但把这套基于 ThinkPHP6 Layui MySQL 的企业办公系统跑起来之后你会发现它把人事、审批、客户、合同、项目、财务这些模块都做进了同一套权限体系里前端用 Layui 做交互后端用 TP6 的中间件和模型关联撑业务逻辑。它解决的不是“有没有后台”的问题而是“中小企业想快速搭一套能落地的 OA又不想从零写权限和流程”的问题。适合谁一类是接私活需要快速交付业务管理系统的开发者另一类是公司内部想自建办公平台、又养不起大团队的 IT 负责人。这套东西的价值在于模块已经拆好二次开发时你只需要盯着业务表加字段不用再折腾登录、菜单、角色这套地基。2. 环境搭起来TP6 的入口绑定与数据库初始化2.1 运行环境与依赖版本对齐这套系统对版本比较敏感ThinkPHP6 的小版本差异会导致中间件执行顺序变化Layui 的模块加载方式也跟 2.x 和 3.x 有区别。我一般会先把环境锁死再动手避免后面出现“本地能跑、服务器 500”的玄学问题。组件建议版本说明PHP7.4 / 8.0TP6 对 8.1 部分语法兼容不稳生产建议 7.4MySQL5.7 / 8.0注意 8.0 默认字符集与旧 SQL 的排序规则冲突Composer2.x用于拉取 TP6 核心与扩展包Nginx1.18需要配置伪静态否则路由 404Layui2.6前端框架源码里已内置装依赖之前先确认php -m里有没有fileinfo、pdo_mysql、mbstring、openssl这几个是 TP6 启动的硬门槛。缺fileinfo会在上传模块直接抛异常缺openssl则 JWT 或加密相关功能会挂。# 进入项目根目录安装 TP6 依赖 composer install --no-dev # 如果 composer 卡住换国内镜像再试 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/--no-dev是生产环境习惯跳过 phpunit 这类开发包减少体积。镜像配置只影响拉包速度不改业务代码。2.2 数据库导入与 .env 配置源码包里一般会带一个.sql文件导入之前先建库字符集用utf8mb4排序规则用utf8mb4_general_ci。MySQL 8.0 默认是utf8mb4_0900_ai_ci跟旧 SQL 里的utf8mb4_general_ci混用会在联表查询时报“排序规则不一致”的错这个坑我踩过不止一次。# 建库显式指定字符集和排序规则 mysql -u root -p -e CREATE DATABASE oa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入数据 mysql -u root -p oa_db ./database/oa_init.sql接着改.env文件TP6 默认读根目录下的.env没有的话从.env.example复制一份。APP_DEBUG true [APP] DEFAULT_TIMEZONE Asia/Shanghai [DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE oa_db USERNAME root PASSWORD your_password HOSTPORT 3306 CHARSET utf8mb4 DEBUG true [LANG] default_lang zh-cnAPP_DEBUG在调试阶段开true上线必须关否则异常页会暴露数据库账号和目录结构。DEFAULT_TIMEZONE不设的话审批流的时间戳会差 8 小时这个在考勤模块里是致命的。2.3 Nginx 伪静态与入口绑定TP6 默认入口在public/index.phpNginx 的 root 必须指向public目录不能指到项目根否则.env和vendor会被直接下载。伪静态规则写错的话除了首页其他路由全是 404。server { listen 80; server_name oa.local; root /www/oa_project/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; } # 禁止访问敏感目录 location ~ ^/(\.env|vendor|runtime|database) { deny all; } }rewrite那行把 PATH_INFO 交给 TP6 处理?s是兼容模式写法比try_files在某些 PHP-FPM 配置下更稳。deny all那几条是保命用的线上环境不配的话别人直接访问/vendor/autoload.php就能看到依赖结构。3. 权限与模块拆解从登录态到审批流的代码落点3.1 登录态与 RBAC 的中间件实现这套系统的权限控制走的是典型的 RBAC用户绑角色角色绑菜单和操作节点。TP6 的中间件在app/middleware.php里注册登录校验和权限校验分两层先验 token 再验节点顺序反了会出现“未登录却提示无权限”的怪现象。// app/middleware.php return [ // 全局中间件处理跨域和日志 \app\middleware\Cors::class, // 登录校验白名单在中间件内部判断 \app\middleware\CheckLogin::class, // 权限节点校验依赖登录态 \app\middleware\CheckAuth::class, ];CheckLogin里一般会读 session 或 JWT白名单放行登录页、验证码接口。CheckAuth则拿当前请求的控制器和方法去比对角色权限表比对逻辑通常是role_id - auth_ids的字符串包含判断。// app/middleware/CheckAuth.php 核心片段 public function handle($request, \Closure $next) { $controller $request-controller(); $action $request-action(); $node strtolower($controller . / . $action); // 超管直接放行 if (session(admin.is_super) 1) { return $next($request); } $authIds session(admin.auth_ids); // 形如 1,2,5,9 $current Db::name(auth)-where(node, $node)-value(id); if (!$current || !in_array($current, explode(,, $authIds))) { return json([code 403, msg 无权限访问]); } return $next($request); }$node用控制器和方法拼所以新增模块时必须在auth表里补节点记录否则超管之外的人一律 403。in_array这里没做严格类型因为auth_ids存的是字符串$current是整型松散比较反而省事但如果你开了严格模式记得手动转一下。3.2 审批流的表结构与状态机审批模块是这套 OA 里逻辑最重的部分。表结构一般拆成三张approval审批单主表、approval_process流程节点、approval_record审批记录。状态机用数字表示常见的是 0 待审、1 通过、2 驳回、3 撤回。-- 审批单主表关键字段 CREATE TABLE oa_approval ( id int unsigned NOT NULL AUTO_INCREMENT, type_id int NOT NULL COMMENT 审批类型, user_id int NOT NULL COMMENT 发起人, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审 1通过 2驳回 3撤回, current_step int NOT NULL DEFAULT 1 COMMENT 当前节点, create_time int NOT NULL, PRIMARY KEY (id), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_user_status这个联合索引是给“我的待办”列表用的不加的话数据量上到几万条列表页会明显卡顿。current_step记录走到第几个节点每次审批通过后加一直到等于流程总节点数才把主表status置为 1。// 审批通过的核心逻辑 public function approve($id, $remark) { $info Db::name(approval)-where(id, $id)-find(); if ($info[status] ! 0) { throw new \Exception(该单已处理); } Db::startTrans(); try { // 写审批记录 Db::name(approval_record)-insert([ approval_id $id, step $info[current_step], user_id session(admin.id), remark $remark, create_time time(), ]); // 判断是否还有下一节点 $total Db::name(approval_process) -where(type_id, $info[type_id]) -count(); if ($info[current_step] $total) { Db::name(approval)-where(id, $id)-update([status 1]); } else { Db::name(approval)-where(id, $id) -update([current_step $info[current_step] 1]); } Db::commit(); } catch (\Exception $e) { Db::rollback(); throw $e; } }事务包住“写记录”和“改状态”两步避免记录写了但状态没改的脏数据。$total每次实时查流程节点改了也能立刻生效代价是多一次查询但审批场景并发不高可以接受。3.3 客户与合同模块的关联查询客户管理和合同管理是联动的一个客户对应多份合同列表页要显示客户名称和合同金额汇总。TP6 的模型关联用hasMany就能搞定但要注意with预加载和join的区别预加载适合展示关联字段join适合用关联字段做筛选。// 客户列表带合同数量 $list Db::name(customer) -alias(c) -leftJoin(contract ct, ct.customer_id c.id) -field(c.*, COUNT(ct.id) as contract_count, IFNULL(SUM(ct.amount),0) as total_amount) -group(c.id) -order(c.id desc) -paginate(15);leftJoin保证没有合同的客户也能显示IFNULL防止SUM返回 null 导致前端渲染出错。group(c.id)是必须的不然聚合结果会串行。分页用 TP6 自带的paginate前端 Layui 的 table 组件直接对接返回的data和total字段。4. 避坑排查从 500 到样式错乱的五个血泪记录4.1 现象首页能开点任何菜单都 404原因Nginx 伪静态没配或者root指到了项目根目录而不是public。TP6 的入口文件在public/index.php所有路由都靠index.php转发直接访问/admin/user/index这种路径Nginx 找不到对应文件就报 404。解决确认root指向public并加上rewrite ^(.*)$ /index.php?s$1 last;。改完nginx -t测试语法再nginx -s reload。如果还不行检查fastcgi_param SCRIPT_FILENAME有没有写对路径拼错也会 404。4.2 现象登录后跳转回登录页session 存不住原因runtime目录没有写权限TP6 的 session 默认存文件目录不可写就写不进去。另一种可能是.env里APP_DEBUG开了但session配置的path指向了不存在的目录。解决chmod -R 755 runtime并确保www用户有写权限。如果是 Linux 下 SELinux 开着还得chcon -R -t httpd_sys_rw_content_t runtime。session 配置在config/session.php默认type是file改成cache走 Redis 也能绕开文件权限问题。4.3 现象Layui 表格样式错乱按钮变成纯文本原因layui.css和skin.min.css的加载顺序不对或者editormd.css在layui.css之后加载把部分基础样式覆盖了。源码里AUTHORS文件列出的那几个 CSS 是有依赖顺序的layui.css必须最先。解决检查view/layout/head.html里的 link 顺序确保layui.css在editormd.css之前。如果用了 CDN确认 CDN 上的 Layui 版本和本地一致2.6 和 2.8 的 class 命名有差异混用必乱。4.4 现象审批提交后状态没变但记录里多了一条原因事务没包住或者Db::startTrans()和Db::commit()不在同一个数据库连接上。TP6 里如果中间切换了Db::connect()事务会失效。解决确保事务内的所有操作都用同一个连接不要在中途Db::connect(other)。另外检查 MySQL 引擎是不是 InnoDBMyISAM 不支持事务startTrans会静默失效。4.5 现象上传附件报“文件类型不允许”但明明是 jpg原因fileinfo扩展没装TP6 的上传验证拿不到真实 MIME 类型只能靠后缀判断而后缀判断又被config/filesystem.php里的白名单卡住。解决php -m | grep fileinfo确认扩展存在没有就装。然后检查config/filesystem.php的mime_type白名单把image/jpeg、image/png加进去。如果用的是对象存储还要确认 SDK 的Content-Type有没有被改写。5. 二次开发提速从加一张业务表到接进菜单的完整链路这套系统最值钱的地方不是现成模块而是它把“加模块”的路径铺平了。我一般会拿一个“设备借用登记”当练手走一遍从建表到菜单显示的完整流程跑通之后再加复杂业务就有底了。第一步建业务表。字段按需加但create_time、update_time、status这三个建议保留列表页和软删除都用得上。CREATE TABLE oa_device_borrow ( id int unsigned NOT NULL AUTO_INCREMENT, device_name varchar(100) NOT NULL, borrower varchar(50) NOT NULL, borrow_time int NOT NULL, return_time int DEFAULT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0借用中 1已归还, create_time int NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第二步用 TP6 的命令行生成控制器和模型省得手写命名空间。php think make:controller admin/DeviceBorrow php think make:model DeviceBorrow生成的文件在app/admin/controller/DeviceBorrow.php和app/model/DeviceBorrow.php。控制器里补index、add、edit、delete四个方法模型里定义$name和软删除字段。第三步在auth表里插入菜单和节点记录。菜单是给侧边栏渲染用的节点是给权限校验用的两条记录都要有。INSERT INTO oa_auth (pid, title, node, type, status) VALUES (0, 设备借用, deviceborrow/index, 1, 1), (0, 设备借用-新增, deviceborrow/add, 2, 1), (0, 设备借用-编辑, deviceborrow/edit, 2, 1), (0, 设备借用-删除, deviceborrow/delete, 2, 1);type为 1 是菜单2 是操作节点。pid为 0 表示顶级菜单要挂到某个父菜单下就填父菜单的 id。第四步前端模板放到view/device_borrow/index.html用 Layui 的 table 组件渲染数据接口指向/admin/deviceborrow/index。模板里引用的 CSS 和 JS 路径要跟现有模块保持一致不然样式会脱节。第五步清缓存。TP6 的路由和配置有缓存改完菜单不显示先php think clear再刷新。php think clear # 如果用了路由缓存 php think route:clear这套流程跑下来大概二十分钟比从零搭一套后台省掉的是权限、菜单、分页、上传这些通用能力的重复建设。从那以后我每次接新的业务模块都强制先走一遍“建表 → 生成控制器 → 插 auth 记录 → 写模板 → 清缓存”这五步漏掉任何一步都会在某个环节卡住。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑