简介这是一份以中小型建筑公司、包工头及施工队为目标用户的工地记账系统 PHP 全开源源码围绕资金、人员、项目三个维度设计收入支出双轨记账、精确到分支持工程款关联项目支出细分材料、人工、设备等8余类工人管理含姓名、电话、身份证、日工资标准等档案项目模块支持起止日期、地点描述、进度可视化和收益分析。包体共15个文件以9个PHP页面为主附带SQL数据库脚本、Chart.js图表脚本、CSS样式、配置文件及HTML引导页压缩包约15KB导入jz.sql后即可运行。目前已有68人学习下载。源码内置登录验证、会话管理、操作时间戳追踪可实时过滤收支并生成条形图、饼状图等动态报表适合开发者二次接入工资结算、考勤打卡、材料库存也便于现场人员快速上手。1. 工地上的流水账为什么必须用系统管工地记账和一般的企业财务软件完全不是一回事。工地上没有统一的科目表一笔钱支出后要用在哪个班组、算不算借支、要不要盯回款都是工头脑子里瞬间的事。用云端个人记账App或者企业财务套件录入慢、分类对不上用 Excel 表汇总月底光对账就要熬两个通宵还经常出现两个版本的表格互相矛盾的尴尬局面。我的经验是团队要的是一套能装在自己服务器上、按工地习惯记账的 PHP 系统要把收支、班组、材料商账、预支工资和结算绑在同一个漏斗里。工地记账系统 PHP 源码正是为这种场景准备的全开源方案管理端做财务审核现场端做快速录入数据全部落在自己的 MySQL 里不依赖第三方平台。2. 为什么我推荐 PHP轻量、不挑服务器、改起来不疼2.1 对比 Java 和 .NETPHP 在工地场景的优势照先说结论工地记账系统选 PHP不是因为 PHP 多先进而是因为它和工地的网络环境、硬件条件、维护人员水平是匹配的。工地的网线经常是临时拉的服务器往往就是一台旧电脑或者最低配的云主机很多时候宽带还是共享的。Java 系方案冷启动吃内存.NET 对 Windows 服务器有依赖这两样在工地现场都容易翻车。PHP 是进程级的请求来了就执行执行完就释放256MB 内存的机器跑一个小型记账系统绰绰有余。第二个优势是部署链路短。一个 PHP 项目丢进 Nginx 就能跑不需要像 Java 那样配 Tomcat、配 JVM 参数、处理 jar 包冲突。这一点对分包队伍的临时技术员特别友好出了问题远程改两个文件就能修不用背着电脑到机房折腾。第三个优势是代码可读性。这套源码如果按原生 PHP 的 MVC 风格写变量命名和表结构设计都贴近业务直译二次开发的成本低。想要加一个“按楼栋过滤流水”的功能改一个控制器方法加一个查询条件就行不用在抽象工厂和依赖注入的迷宫里绕路。2.2 系统架构入口、路由和会话状态我拆过的这套工地记账系统大框架是经典的 Web 单入口模式。所有请求都走 index.php由路由解析 URI 的前缀分发到对应的 controller。记账模块的请求路径类似 index.php?crecordaaddc 是控制器名a 是方法名。这样做的价值是统一了权限拦截的位置判断登录态、判断操作权限、写操作日志都集中在入口文件里完成不会出现某个控制器漏掉鉴权的问题。会话管理用的是 PHP 原生 Session。这里我建议你在部署后改两个参数一是把 Session 有效期从默认的 1440 秒调长到 8 小时因为工地现场经常半天不看后台填到一半的表单提交时会话过期直接跳登录页二是把 Session 的存储路径改成独立目录防止和系统其他应用的 session 混淆导致相互踢下线。会话数据不要放进 Redis因为这套系统的定位是低成本部署。MySQL 存 Session 也是一个选择但考虑到多工地同时记账时并发量并不高文件存储足够。关键在于 php.ini 里的 session.gc_maxlifetime 必须和你要调的有效期一致否则会出现“后台还没退出但是操作又提示重新登录”的玄学问题。2.3 数据库设计六张核心业务表这套记账系统的数据库设计是这个资源最有价值的部分。它没有按照会计学的复式记账来建表而是按照工地上真实的资金流向来建模。一进系统先建工地档案site 表每个工地绑定一批班组和供应商。核心业务表的关系如下表名作用关键字段site工地档案site_id, site_name, status(在建/完工)gang班组信息gang_id, site_id, leader_name, phonesupplier材料供应商supplier_id, name, contact, phonerecord流水明细in_out_type, amount, category, site_id, gang_id, supplier_idadvance预支工资/借支单worker_name, amount, gang_id, settle_statusadmin后台管理员username, password_hash, role, site_scoperecord 表是整个系统的主动脉。每一笔收入或支出都记在这里用 in_out_type 区分方向用 category 区分大类。category 的设计逻辑是从项目现场总结出来的列了一级分类材料费、人工费、机械费、运输费、生活开支、预支借支、工程款回款往下还有几十个二级项。材料费下分钢筋、水泥、模板、方木等人工费下分点工、包工、加班等。这个分类层次决定了对账时能按哪个维度筛数据建议你不要随意改动一级分类。design to 业务逻辑里统计报表全部依赖这些基础分类新增二级分类可以动一级分类会引起历史数据对不上账。3. 把记账分类做成工地习惯而不是会计科目3.1 收入侧工程款与保证金的分账处理工地记账最容易错乱的就是收入侧。进账不只有工程款还有投标保证金退回、甲方代付水电费、材料商押金退回几种情况。如果只用一张 Excel 表记总收入月底一看数字对不上因为押金以后还要退不能算作实际收入。这套系统在收入分类里单独设计了“往来款”这个类目进出的押金、保证金都走这里等退回时再冲销。这样操作利润计算只统计工程款回款那一类不会被押金虚增的部分干扰。实际操作时录入一笔收入要带三个附加信息回款对应的楼栋或标段、回款方式转账/现金/承兑、票据编号。这个场景对应到表单上就多出三个字段但绝对是值得的。因为工地对账经常要回答“一号楼的钱还差多少没回来”这种问题没有楼栋维度就得拿 Excel 再去筛一遍。代码层面对应的 SQL 是带 site_id 和楼栋字段的复合条件查询效率上完全没问题。3.2 支出侧材料费和人工费的双通道校验支出记录的坑在于“同一笔钱可能既属于材料费又涉及人工费”。比如搅拌站送混凝土车费是包在单价里的但供应商开了两张单子一张是混凝土货款一张是泵送费。如果录入时图省事合并成一条后续做成本分析时泵送费就消失了。这套系统的做法是流水明细禁止合并录入拆开记两条金额各自归位。录入表单里还做了数据联动校验选了供应商且金额大于 5000必须填写工地项目名称防止大额支出漏归属。人工费有个独立的录入入口因为工人的日薪、出勤天数和借支记录是联动的。这里核心逻辑是每个工人的借支总额不能超过已确认工资的 80%。系统在保存借支单时会自动比对 advance 表和工资结算表的差值超过了就弹窗提示拒绝保存。这个控制规则对现场来说很节省精力也堵住了部分班组长夸大预支的口子。3.3 班组结算从流水到结算单的自动汇总班组结算是整个系统里最具备算法色彩的部分。它的逻辑可以一句话说清每月固定日期系统把该班组当月的所有点工记录、包工合同金额、借支单按人汇总自动生成一张结算单。结算单上的应发金额 费用合计 - 借支合计 - 罚款金额。需要注意包工合同金额不会自动平摊到每个月只有该班组确认完成某个施工节点、项目经理验收后在系统里点击确认节点完成这笔钱才会进入本月的结算池。这是这套系统比较有亮点的地方它模拟了工地“按节点拨付”的真实节奏。另一个细节是罚款项。罚款来源要关联到具体的违章记录没有来源说明的罚款单会被系统标记为“待补充依据”不参与最终的结算金额计算避免了乱罚款导致的纠纷。3.4 录入一笔记账的表单逻辑与参数说明以下是一段录入支出记录的表单提交接口的核心逻辑。这套系统的代码风格偏过程化适合新手读懂每一步在干什么也方便改造成自己的命名习惯public function addRecord() { $in_out_type $_POST[in_out_type]; // 1收入 2支出 $category $_POST[category]; $amount floatval($_POST[amount]); $site_id intval($_POST[site_id]); $gang_id intval($_POST[gang_id] ?? 0); $supplier_id intval($_POST[supplier_id] ?? 0); $pay_method $_POST[pay_method]; // 1转账 2现金 3承兑 $description trim($_POST[description]); // 核心校验1支出金额不能为负且必须大于0 if ($amount 0) { exit(json_encode([code 1, msg 金额必须大于0])); } // 核心校验2材料类支出必须选择供应商 if ($category 101 $category 199 $supplier_id 0) { exit(json_encode([code 1, msg 材料费必须挂接供应商])); } // 核心校验3大额支出需要填写工地项目名 if ($amount 5000 $site_id 0) { exit(json_encode([code 1, msg 超过5000元的支出必须关联工地])); } $sql INSERT INTO record (in_out_type, category, amount, site_id, gang_id, supplier_id, pay_method, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()); $stmt $this-db-prepare($sql); $stmt-bind_param( iidiiiss, $in_out_type, $category, $amount, $site_id, $gang_id, $supplier_id, $pay_method, $description ); $stmt-execute(); exit(json_encode([code 0, msg 记账成功])); }这段逻辑里值得注意的有三个参数category 用范围区间 101-199 约定为材料费这个设计比数字精确匹配更灵活后续新增二级分类不用改判断条件bind_param 的类型串 iidiiiss 分别是整数、整数、小数、整数、整数、整数、整数、字符串顺序必须与 SQL 里的占位符一一对应写错一位就会导致数据写入失真description 字段做了 trim 处理防止录入时误带空格导致后期模糊搜索匹配不到记录。这个接口是比较典型的场景它把工地记账中最容易漏的关联关系都卡住了。新手常犯的错误是省略 supplier_id 的判断逻辑认为“先记上单子以后还能补”结果月底对账时一堆支出找不到供应商一笔一笔问班组长又费时间又得罪人。宁可在录入时多一道拦截也不要事后补账。4. 从源码到上线部署步骤和参数配置4.1 源码目录结构看清每个文件夹的职责这套源码拿到手后先厘清目录结构再动手改配置。通常一个 PHP 记账系统的目录划分如下project/ ├── index.php # 单入口文件路由与权限判断 ├── config/ │ └── database.php # 数据库连接配置 ├── controller/ # 业务控制器 │ ├── RecordController.php │ ├── GangController.php │ └── ReportController.php ├── model/ # 数据表操作类 ├── view/ # 页面模板 │ ├── record/ │ ├── report/ │ └── layout/ ├── static/ # js/css/图片 └── runtime/ # 日志和缓存这套结构是标准的轻量 MVC。看代码时先读 config/database.php 了解数据库账号再从 index.php 路由跟踪一条请求路径最后进 controller 看业务逻辑。不需要把 model 层全部读完就能上手改功能这也是我推荐这种源码结构的原因。以独立入口文件开头的方式让整个系统的运行逻辑清晰可追踪。4.2 配置 MySQL 与 Nginx新手机器快速跑起来环境要求方面PHP 版本建议 7.2 以上启用了 PDO 和 mysqli 扩展MySQL 5.7 或 8.0 均可。以下是两步关键的初始化操作。先建库建账号并给权限CREATE DATABASE site_account DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER site_userlocalhost IDENTIFIED BY your_password_here; GRANT ALL PRIVILEGES ON site_account.* TO site_userlocalhost; FLUSH PRIVILEGES;这里必须用 utf8mb4 而不是 utf8因为工地材料商的备注信息里经常有生僻字、表情符号或者繁体字。utf8mb4 才能完整覆盖这些字符不然写入时容易变成乱码或直接报错。colation 用 general_ci 足够不需要用 unicode_ci排序场景对中文不敏感。权限只授了 site_account 一个库没有授全局权限这是安全习惯即使代码被注入攻击者也没有权限去动服务器上的其他库。接着配置 Nginx 站点server { listen 80; server_name your_domain_or_ip; root /var/www/site_account; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }try_files 指令是将所有不存在的文件请求都交给 index.php 处理这样控制器路由才能正常工作。fastcgi_pass 后面的 socket 路径必须与实际安装的 PHP 版本一致常见错误是 php8.1-fpm.sock 写成了 php7.4-fpm.sock导致 502 Bad Gateway。修改了 php.ini 或 nginx 配置后记得重载服务systemctl reload nginx 和 systemctl restart php7.4-fpm。4.3 第一次打开系统六步初始化清单源码部署完成后首次进入系统需要走一遍初始化流程否则后面记账会乱。第一步用默认账号登录后台默认账号前缀是 admin密码在 install.sql 里有提示。登录后立即修改密码这步不要跳过因为源码托管平台上这个密码是公开的不修改等于把系统敞开给所有知道源码的人。第二步在“工地管理”中创建第一个工地档案填好工地名称和负责人确认状态为“在建”。此时系统会将 site_id 生成出来后续录流水时都要依赖它。第三步创建班组。至少录入一个班组名称和班组长手机号手机号会用于后面接收借支确认的短信通知接口这一步是影响后续体验的关键。注意同一工地下的班组名称不要重复系统在保存时会做唯一校验校验逻辑是查询 gang 表里 site_id 与 gang_name 的组合是否存在。第四步创建供应商。把长期供货的钢材商、水泥商、五金店各建一个档案供应商名称和联系人分开填写方便后期搜索、对账时按联系人核对。第五步测试录入一笔“收入”一笔“支出”。确认收入记录在报表中显示为正数支出显示为负数验证 in_out_type 字段的取值逻辑与报表统计口径一致。第六步跑一次“日报表”功能核对今天录入的数据是否完整呈现尤其是分类和金额。如果日报表和流水列表不一致大概率是时间过滤条件写错检查 created_at 的时间边界判断是 当天 00:00:00 还是 当天 00:00:00。4.4 伪静态与低配置服务器下的性能调优这套系统默认使用 index.php?cxxxaxxx 形式的 URL不启用伪静态也可以跑。但如果后期想部署在公网供多家分包队伍使用URL 好看只是一方面关键是减少路由解析层的负担。常见做法是在 Nginx 里加上一条 rewrite 规则把 /record/add 这种路径重写到 index.php?crecordaadd。配置方式如下location / { rewrite ^/(\w)/(\w)$ /index.php?c$1a$2 last; }这个正则只匹配两层路径。如果以后要增加第三层比如按工地筛选就要改成 ^/(\w)/(\w)/(\w)$ 对应 c、a、param 三个参数。低配置服务器下还可以调整 PHP-FPM 的进程管理参数把 pm.max_children 从默认的 5 改成 8同时确认 pm.start_servers 为 2pm.min_spare_servers 为 1pm.max_spare_servers 为 3。这个组合适合内存只有 1GB 的机器同时能扛住现场端大约 10 个人的并发录入。max_children 不建议盲目调高每个 PHP-FPM 进程默认占用约 30MB 内存调成 20 的话在内存小的机器上反而会触发 swap 导致响应变慢。5. 避坑指南工地记账系统最常见的五个故障5.1 时间显示少了 8 小时现象后台列表记录的 created_at 时间比实际时间慢了 8 小时报表里当天的流水总数对不上。原因PHP 默认时区是 UTCMySQL 连接也默认用系统时区。服务器在中国但时区配置没有修改时所有时间字段都以 UTC 存储并展示。解决在 config/database.php 或入口文件中加入 date_default_timezone_set(Asia/Shanghai);并在建立 MySQL 连接后执行 set time_zone 8:00;。改完配置之后重启 PHP-FPM 并重新登入系统新录入的数据时间才会正确。但历史数据的 created_at 字段仍然差 8 小时需要跑一段 UPDATE 语句加上 8 小时。如果你在月末对账之后才发现这个问题不要慌把上个月的数据用一个时间参数统一调整即可。5.2 材料商无法上传大图现象在票据附件上传里选择超过 2MB 的图片页面提示上传失败但小于 2MB 的图片正常。原因PHP 配置 upload_max_filesize 默认值是 2Mpost_max_size 默认值是 8M。施工人员用手机拍的材料送货单原图往往在 3-5MB所以超过限制就被拦截。解决修改 php.ini 中的 upload_max_filesize 为 10Mpost_max_size 为 12M同时确认 memory_limit 不低于 128M。上传目录如果有执行权限问题会导致文件写入失败检查 runtime/upload 目录权限是否为 www 用户可写。改完 php.ini 后执行 php-fpm -t 先检查语法再用 systemctl reload php7.4-fpm 重载此时再去上传大图就不会再报错了。5.3 修改管理员密码后旧会话仍然有效现象管理员在后台修改了自己的登录密码但用旧密码登录过的浏览器还能继续操作一段时间系统没有强制下线。原因密码修改只更新了 admin 表的 password_hash 字段没有同步销毁该用户的 Session 数据。PHP Session 的默认行为是只要客户端 Cookie 不失效会话就一直有效。解决在修改密码的处理逻辑中加入 session_unset() 和 session_destroy()并同时删除该用户相关的 session 文件。更稳妥的做法是在会话表里加一个 login_token 字段每次修改密码时更新 token然后在入口文件的鉴权校验中比对当前 session 里的 token 与数据库中的 token不一致就强制跳转到登录页。这个做法虽然多一条数据库查询但解决了强密码策略与旧会话之间的矛盾。5.4 多人同时录入同一笔借支形成重复单现象某班组一天内出现多条相同的借支记录金额、日期、工人姓名完全相同月底结算时被重复扣除。原因现场多个管理员同时录入没有在数据库层面做唯一约束。代码逻辑只判断了金额上限没有判断同一天内同一工人是否已经有过一笔待结算的借支单。解决在 advance 表上加唯一索引设置字段组合为 site_id, worker_name, DATE(created_at)禁止同一天内同一工地同一工人重复创建“待结算”状态的借支单。注意这个唯一约束要限制状态为待结算因为如果同一天该工人的借支已经被标记为“已还款”再借一笔是允许的。具体实现是在插入前先查一遍该工人当天的待结算借支数量代码里加一个条件判断杜绝数据库层面的并发写入。5.5 按条件导出的 Excel 金额合计与页面显示不一致现象导出某工地本月的支出明细到 Excel 后用 Excel 求和得到一个数和系统页面上显示的合计金额不一致误差恰好是某个供应商所有订单的金额之和。原因导出逻辑中过滤条件漏掉了供应商的软删除标记。系统删除供应商时不会真的删行而是打上 is_deleted 1 做逻辑删除但导出 SQL 没有加上这个条件把已删除供应商的历史流水也统计进去了。解决检查导出模块的 SQL在 where 条件里明确加上 AND supplier.is_deleted 0 或对应的字段条件。同时流水明细页面和导出报表最好统一使用同一个汇总函数避免两处分别写统计逻辑导致口径分叉。我一般会把这个汇总逻辑抽成一个公共方法页面预览和导出都调用同一个方法从源头上保证两个数字一致。6. 进阶技巧定时备份与二次开发的两个习惯工地记账系统的数据量虽然不大但一旦跨年使用流水记录很容易超过几万条。定时备份是唯一能让你晚上睡得着觉的习惯。MySQL 自带 mysqldump 工具配合 crontab 就能实现每晚自动备份。以下是我常用的备份脚本#!/bin/bash BACKUP_DIR/data/backup/site_account DATE$(date %Y%m%d_%H%M%S) DB_NAMEsite_account DB_USERsite_user DB_PASSyour_password_here # 导出并压缩保留最近30天的备份 mysqldump -u$DB_USER -p$DB_PASS $DB_NAME $BACKUP_DIR/db_$DATE.sql gzip $BACKUP_DIR/db_$DATE.sql find $BACKUP_DIR -name *.sql.gz -mtime 30 -deletecrontab 配置每天凌晨 3 点执行0 3 * * * /usr/local/bin/backup_site.sh /dev/null 21这个方案的关键点在于 mysqldump 的参数。没有加 --single-transaction 时 InnoDB 表会锁表如果备份时间恰好赶上有人记账就会报超时。建议在 mysqldump 后追加 --single-transaction --quick --skip-lock-tables 三个参数。还有一个小细节备份文件里含密码一定不要把脚本所在目录设为 www 用户可读否则网站被人拿源码后 MySQL 密码也一起泄漏了。关于二次开发我最想强调的习惯是改动前先跑一次完整备份然后只改 controller 层和 view 层不要去动数据库表结构。这套源码的报表统计都是写死在 SQL 里的修改表字段意味着要同步改至少五个查询文件工作量翻倍还容易产生遗漏。如果确需增加字段比如记录材料单价对应的单位建议新增一张扩展表 material_extra 关联主流水 ID而不是直接往 record 表里加列。这样既保留了原始数据结构的完整性也让新增字段的维护边界收得很小。我还养成了一个习惯每次上线新功能前先在本地模拟环境里把流程完整走一遍录一笔支出、做一次班组结算、导一次报表三项都通过才推送到生产服务器。因为 PHP 是解释型语言很多错误只有运行到特定分支才会暴露语法检查工具只能识别出少了一行分号识别不出路由写错的问题。从那以后我每次更新记账系统都强制走一遍冒烟测试流程这个习惯帮我避开了好几次现场对账时的尴尬。希望这些拆解内容对你有帮助落地部署时少走一些弯路。本文还有配套的精品资源点击获取