资讯详情

资讯详情

ThinkPHP6+Layui+MySQL:中小团队自建办公系统实战指南

简介这是一套面向中小企业开发者与信息化管理人员的开源企业办公自动化系统源码基于ThinkPHP6、Layui与MySQL构建集成系统设置、人事管理、消息管理、审批管理、日常办公、客户管理、合同管理、项目管理、财务管理等核心模块。系统结构简约、易于功能扩展方便二次开发可在此基础上延伸出CRM、ERP及各类业务管理系统帮助团队快速搭建企业级OA平台降低数字化办公成本。资源包共1017个文件约10.42MB以html页面模板、js脚本、php后端逻辑与css样式为主另含sql建表脚本、字体图标、图片素材及项目说明文档前后端与数据库结构完整。目前已有526人学习下载适合希望研究企业级应用架构、进行OA二次开发或搭建内部办公系统的开发者参考可借此理解模块划分、权限与审批流程设计并作为业务系统快速起步的工程基础。1. 为什么很多团队用 ThinkPHP6 Layui MySQL 重写办公系统后反而比买现成 SaaS 更省心不少中小团队都经历过这样的场景用着某款在线协作工具审批流想加一个“金额超过 5000 元自动转财务总监”的节点结果发现要么加钱升级套餐要么根本改不了。数据存在别人服务器上导出还要申请权限。这时候一套能自己部署、自己改代码的开源企业办公系统就成了刚需。而 ThinkPHP6 Layui MySQL 这个组合恰恰是国内中小团队落地这类系统时最稳、最省人力的一条路。ThinkPHP6 负责后端路由、ORM 和权限控制Layui 负责后台界面快速搭建MySQL 存组织架构、审批流和考勤数据。这套方案不追求技术炫技追求的是“三天能跑起来一周能改出自己想要的审批节点”。适合谁适合有一到两名 PHP 开发、想拥有自己数据主权的团队。下面我把这套系统从环境搭建到审批流改造的完整路径拆开讲中间会重点说清楚参数怎么配、坑在哪。2. 环境搭建与项目初始化从零把骨架跑起来2.1 为什么选 ThinkPHP6 而不是 ThinkPHP5 或 LaravelThinkPHP6 相比 TP5 最大的变化是全面拥抱 Composer 和 PSR 标准中间件机制更清晰对于办公系统里常见的“登录态校验 权限节点检查 操作日志记录”这三层拦截用中间件串起来非常顺手。Laravel 当然更强但学习曲线和服务器要求都更高中小团队维护成本划不来。MySQL 选 5.7 或 8.0 都行5.7 在低配服务器上更稳8.0 的窗口函数对考勤统计类查询有帮助。我一般会建议如果服务器内存小于 2G用 MySQL 5.7否则直接上 8.0。2.2 用 Composer 创建项目并配置多应用模式办公系统通常分“后台管理”和“员工自助”两个入口ThinkPHP6 的多应用模式正好匹配。先执行创建命令# 创建 ThinkPHP6 项目项目名按自己习惯来 composer create-project topthink/think office-system # 进入目录 cd office-system # 安装多应用模式扩展 composer require topthink/think-multi-app安装完成后在app目录下建立admin和home两个应用目录。admin放后台管理控制器home放员工端控制器。多应用模式下访问/admin/index/index就会自动路由到app/admin/controller/Index.php的index方法。这里有个容易翻车的点多应用模式安装后必须删除原来的app/controller目录否则路由会冲突报“控制器不存在”的错。2.3 数据库连接与 .env 配置的四个关键参数ThinkPHP6 推荐用.env文件管理数据库配置不要硬编码在config/database.php里。在项目根目录新建.envAPP_DEBUG true [DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE office_system USERNAME office_user PASSWORD your_strong_password HOSTPORT 3306 CHARSET utf8mb4 DEBUG true [LANG] default_lang zh-cn四个关键参数说明CHARSET必须用utf8mb4否则员工姓名里的生僻字会变成问号DEBUG在开发阶段设为true上线后必须改false否则 SQL 报错会直接暴露表结构HOSTNAME如果 MySQL 和 PHP 不在同一台机器要改成内网 IPPASSWORD不要用 root 密码单独建一个只对office_system库有权限的账号。建库语句如下CREATE DATABASE office_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER office_user% IDENTIFIED BY your_strong_password; GRANT ALL PRIVILEGES ON office_system.* TO office_user%; FLUSH PRIVILEGES;2.4 引入 Layui 并搭建第一个后台页面Layui 不需要 Composer 安装直接下载后放到public/static/layui目录即可。然后在app/admin/view/index/index.html里引入!DOCTYPE html html head meta charsetutf-8 title办公系统后台/title link relstylesheet href/static/layui/css/layui.css /head body div classlayui-layout layui-layout-admin div classlayui-header div classlayui-logo企业办公系统/div /div div classlayui-side layui-bg-black ul classlayui-nav layui-nav-tree li classlayui-nav-itema href/admin/approval/list审批管理/a/li li classlayui-nav-itema href/admin/attendance/list考勤管理/a/li /ul /div div classlayui-body iframe src/admin/welcome frameborder0 stylewidth:100%;height:100%;/iframe /div /div script src/static/layui/layui.js/script script layui.use([element], function(){ var element layui.element; }); /script /body /html这段代码搭出了经典的后台三栏布局顶部标题、左侧菜单、右侧 iframe 内容区。Layui 的element模块负责菜单折叠和导航高亮。注意 iframe 的src指向/admin/welcome这个路由需要在app/admin/controller/Welcome.php里实现一个空方法返回欢迎页。很多新手会忘记注册这个路由导致右侧一直白屏。3. 审批流与权限模型办公系统最核心的两块硬骨头3.1 审批流的数据库表设计三张表搞定多级审批办公系统里最复杂的就是审批流。我见过有人用一张表存所有审批记录结果查询“当前待我审批”时 SQL 写得像天书。稳妥的做法是三张表approval_flow存流程模板approval_node存节点定义approval_record存实际审批记录。建表 SQLCREATE TABLE approval_flow ( id int unsigned NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 流程名称, type varchar(32) NOT NULL COMMENT 流程类型leave/expense/purchase, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE approval_node ( id int unsigned NOT NULL AUTO_INCREMENT, flow_id int unsigned NOT NULL, sort int NOT NULL DEFAULT 0 COMMENT 节点顺序, approver_type tinyint NOT NULL COMMENT 1指定人 2角色 3直属上级, approver_value varchar(128) NOT NULL COMMENT 用户ID或角色ID, PRIMARY KEY (id), KEY flow_id (flow_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE approval_record ( id int unsigned NOT NULL AUTO_INCREMENT, flow_id int unsigned NOT NULL, applicant_id int unsigned NOT NULL, current_node int NOT NULL DEFAULT 0 COMMENT 当前节点sort值, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审 1通过 2驳回, data json DEFAULT NULL COMMENT 表单数据, create_time int unsigned NOT NULL, PRIMARY KEY (id), KEY applicant_id (applicant_id), KEY status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;approval_node里的approver_type是关键设计指定人适合财务审批角色适合部门经理审批直属上级适合请假审批。approval_record的data字段用 JSON 存表单内容这样不同流程可以共用一张记录表不用为每种审批建独立表。查询“待我审批”的 SQL 大致是SELECT r.* FROM approval_record r JOIN approval_node n ON n.flow_id r.flow_id AND n.sort r.current_node WHERE r.status 0 AND ( (n.approver_type 1 AND n.approver_value :user_id) OR (n.approver_type 2 AND n.approver_value IN (SELECT role_id FROM user_role WHERE user_id :user_id)) OR (n.approver_type 3 AND n.approver_value (SELECT leader_id FROM user WHERE id r.applicant_id)) )这条 SQL 把三种审批人类型统一在一个查询里避免了在 PHP 层做多次查询再合并。注意approver_value存的是字符串比较时 MySQL 会做隐式类型转换如果用户 ID 是整数建议在 PHP 层就转成字符串再存入避免索引失效。3.2 用 ThinkPHP6 中间件实现权限节点校验权限控制不要写在每个控制器方法里用中间件统一处理。先建一个app/admin/middleware/Auth.php?php namespace app\admin\middleware; use think\facade\Session; use think\Response; class Auth { public function handle($request, \Closure $next) { $user Session::get(admin_user); if (!$user) { return redirect(/admin/login/index); } // 获取当前路由节点如 admin/approval/list $node strtolower($request-controller() . / . $request-action()); $allowList [login/index, login/check, index/index]; if (!in_array($node, $allowList)) { $hasAuth \app\admin\model\Auth::checkNode($user[role_id], $node); if (!$hasAuth) { return Response::create(无权限访问, html, 403); } } return $next($request); } }然后在app/admin/middleware.php里注册?php return [ \app\admin\middleware\Auth::class, ];checkNode方法去查角色权限表判断当前角色是否拥有该节点。这里有个血泪经验节点名统一用小写因为 ThinkPHP6 的controller()返回的是驼峰格式不转小写会导致权限表里存的approval/list匹配不上Approval/list。另外allowList里必须放登录页和首页否则会死循环重定向。3.3 角色与权限的关联表设计及查询优化权限表用经典的 RBAC 模型role、node、role_node三张表。node表存所有可访问的控制器/方法节点role_node存角色和节点的多对多关系。查询某个角色的所有权限节点时SELECT n.node FROM node n JOIN role_node rn ON rn.node_id n.id WHERE rn.role_id :role_id如果角色多、节点多这张查询会频繁执行。优化手段是在 ThinkPHP6 里用缓存$nodes cache(role_nodes_ . $role_id); if (!$nodes) { $nodes Db::name(role_node) -alias(rn) -join(node n, n.id rn.node_id) -where(rn.role_id, $role_id) -column(n.node); cache(role_nodes_ . $role_id, $nodes, 3600); }缓存 1 小时角色权限变更时手动删缓存。注意column(n.node)返回的是一维数组in_array判断效率很高。不要用select()返回二维数组再循环那样白白浪费内存。4. 考勤与数据导出把日常高频操作做顺4.1 考勤打卡的防重复与时间窗口控制考勤模块最怕员工用脚本刷打卡。常见做法是在attendance_record表上加唯一索引ALTER TABLE attendance_record ADD UNIQUE KEY uk_user_date (user_id, attendance_date);这样同一个人同一天只能插一条记录。但上班和下班是两条记录所以实际设计是user_id attendance_date type唯一type区分上班卡和下班卡。打卡时间窗口用 ThinkPHP6 的验证器控制$rule [ type require|in:1,2, time require|date ]; $msg [ type.in 打卡类型错误, time.date 时间格式错误 ]; $validate new \think\Validate($rule, $msg); if (!$validate-check($data)) { return json([code 1, msg $validate-getError()]); }然后在业务逻辑里判断上班卡只能在 6:00 到 12:00 之间打下班卡在 17:00 到 23:59 之间。超出窗口返回“不在打卡时段”。注意服务器时区要设成Asia/Shanghai在config/app.php里改default_timezone否则时间差 8 小时员工会炸锅。4.2 用 Layui table 模块做分页与搜索Layui 的 table 模块自带分页后端只需要返回固定格式的 JSON。ThinkPHP6 控制器里public function list() { $page input(page, 1); $limit input(limit, 10); $keyword input(keyword, ); $where []; if ($keyword) { $where[] [name, like, % . $keyword . %]; } $total Db::name(attendance_record)-where($where)-count(); $list Db::name(attendance_record) -where($where) -page($page, $limit) -order(id desc) -select(); return json([code 0, msg , count $total, data $list]); }前端 Layui table 配置layui.use([table], function(){ var table layui.table; table.render({ elem: #attendanceTable, url: /admin/attendance/list, page: true, limit: 10, cols: [[ {field: user_name, title: 姓名, width: 120}, {field: attendance_date, title: 日期, width: 120}, {field: check_in_time, title: 上班打卡, width: 160}, {field: check_out_time, title: 下班打卡, width: 160} ]] }); });关键参数code必须为 0 表示成功count是总记录数data是当前页数据。Layui 会自动根据count和limit计算分页条。搜索时用table.reload带上where参数即可。注意page和limit参数名不能改改了 Layui 就认不出来。4.3 大数据量导出 Excel 的内存控制导出考勤报表时如果一个月几千条记录用 PHPExcel 直接load会爆内存。稳妥做法是用 ThinkPHP6 的分块查询 CSV 流式输出public function export() { $filename attendance_ . date(Ymd) . .csv; header(Content-Type: text/csv); header(Content-Disposition: attachment; filename . $filename . ); $fp fopen(php://output, w); fputcsv($fp, [姓名, 日期, 上班打卡, 下班打卡]); $query Db::name(attendance_record)-order(id asc); $query-chunk(500, function ($items) use ($fp) { foreach ($items as $item) { fputcsv($fp, [ $item[user_name], $item[attendance_date], $item[check_in_time], $item[check_out_time] ]); } }); fclose($fp); exit; }chunk(500)每次只取 500 条内存占用稳定在几 MB。fputcsv直接写入输出流不生成临时文件。注意 CSV 里的中文要用utf8mb4编码Excel 打开如果乱码在文件开头加 BOMfwrite($fp, \xEF\xBB\xBF);这一行不加Windows 用户打开就是乱码然后你会收到一堆“导出功能坏了”的反馈。5. 避坑与排查那些让我加班到凌晨的坑5.1 坑一Session 在多个应用间不共享现象后台登录后员工端页面还是未登录状态。原因ThinkPHP6 多应用模式下每个应用默认使用独立的 Session 前缀。解决在config/session.php里把prefix设为空字符串或者统一用admin前缀确保两个应用读同一个 Session。更彻底的做法是用 Redis 存 Session在.env里配SESSION_TYPE redis。5.2 坑二Layui 表格请求返回 200 但页面不显示数据现象Network 面板看到请求成功返回 JSON 也正常但表格一直显示“暂无数据”。原因Layui table 要求返回的code必须是数字 0如果后端返回的是字符串0Layui 会认为失败。解决在 PHP 里用return json([code 0, ...])确保0是整数。另外count字段不能少少了分页条不显示。5.3 坑三MySQL 的 utf8mb4 连接排序规则不一致导致查询报错现象执行带WHERE name 张三的查询时报“Illegal mix of collations”。原因建库时用了utf8mb4_unicode_ci但连接时用了utf8mb4_general_ci。解决在.env的[DATABASE]段加一行COLLATION utf8mb4_unicode_ci或者在config/database.php里把collation显式设成和建库一致。这个坑在迁移数据库时特别常见。5.4 坑四审批流节点顺序调整后历史记录错乱现象管理员把审批流的节点顺序从“经理→总监”改成“总监→经理”结果之前提交的待审记录全部卡住。原因approval_record的current_node存的是sort值节点顺序一变原来的sort对应的审批人变了。解决节点顺序调整时不要改已有节点的sort而是新建节点并停用旧节点。或者用节点 ID 而不是sort来关联记录但那样查询会复杂一些。我一般建议前者简单可靠。5.5 坑五定时任务重复执行导致考勤数据重复插入现象每天凌晨生成考勤汇总时偶尔出现同一天两条汇总记录。原因ThinkPHP6 的定时任务如果没加锁多台服务器同时跑就会重复。解决用 Redis 原子锁$lock cache(attendance_lock_ . date(Ymd)); if ($lock) { return; } cache(attendance_lock_ . date(Ymd), 1, 3600); // 执行汇总逻辑cache的第三个参数是过期时间1 小时后自动释放。这样即使多台机器同时触发也只有一台能拿到锁。6. 进阶技巧把办公系统做成团队真正愿意用的工具6.1 用消息队列解耦审批通知审批通过后要发邮件、发站内信、发企业微信通知如果同步执行用户点“通过”后会卡好几秒。用 Redis 队列异步处理// 审批通过时推入队列 \think\facade\Queue::push(\app\job\ApprovalNotify::class, [record_id $id], approval); // 消费端 php think queue:work --queue approval --daemonApprovalNotify类里实现fire方法依次发邮件和站内信。这样审批操作瞬间返回通知在后台慢慢发。注意--daemon模式要用 Supervisor 守护否则进程挂了通知就丢了。6.2 用数据库迁移管理表结构变更团队协作时每个人本地数据库结构不一致是灾难。用 ThinkPHP6 的迁移工具composer require topthink/think-migration php think migrate:create CreateApprovalFlow生成的迁移文件里写up和down方法提交到 Git。其他人拉代码后执行php think migrate:run就能同步表结构。这比手动导 SQL 文件靠谱得多尤其是字段类型调整时。6.3 一个让我省下大量沟通成本的技巧操作日志自动记录在Auth中间件里顺手记录操作日志Db::name(operation_log)-insert([ user_id $user[id], node $node, method $request-method(), params json_encode($request-param(), JSON_UNESCAPED_UNICODE), ip $request-ip(), create_time time() ]);这样谁在什么时候改了哪个审批流、导出了哪份报表全部有据可查。有一次财务说考勤数据被改了我直接查日志定位到是某管理员在后台手动调整了打卡时间。没有这张表扯皮能扯一整天。注意params里不要记录密码字段在插入前unset($params[password])。这套 ThinkPHP6 Layui MySQL 的办公系统方案我前后在三个团队落地过最深的体会是不要一上来就追求功能大而全先把审批流和考勤这两个最高频的场景跑通让团队用起来再根据反馈迭代。我自己的习惯是每加一个功能先问“这个功能能不能用现有表结构扩展”能就不建新表。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →