ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

工地记账系统PHP源码全开源:从流水记录到报表实现

工地记账系统PHP源码全开源:从流水记录到报表实现 简介一套面向中小型建筑公司、包工头及施工队的完整开源工地记账系统PHP源码围绕“人、钱、项目”三大核心要素提供工人档案与日工资标准管理、项目全周期跟踪、收入支出双轨记账、实时净收益计算及自定义时间段统计支持超支标红提醒与操作留痕覆盖工程款入账到材料人工结算场景。压缩包仅15KB、共15个文件含9个PHP业务逻辑文件、1个SQL数据库脚本以及JS/CSS前端交互、HTML入口与运行配置文件结构清晰便于直接部署或扩展工资结算、考勤打卡等模块。整套源码采用PHPMySQL经典组合前端以Chart.js绘制收入条形图与支出饼状图登录认证、会话管理及双端表单校验保障安全适合PHP学习者研究完整业务流也适合工地现场快速搭建轻量记账工具。目前已有68人浏览学习内置默认登录账号导入数据库后即可体验收/支、报表等核心功能。1. 工地记账系统这套 PHP 源码在解决谁的什么账先说一个常见场面十来个工人的工地材料进场靠白条工时靠手写考勤月底老板把一摞票据拿回家用 Excel 敲到半夜最后发现水泥的吨数对不上库存包工头说的人工天数也和打卡对不上。工地记账系统 PHP 源码全开源解决的正是这个问题——用一套 PHP MySQL 的轻量系统把“材料、人工、机械、分包、借款”四类流水统一记下来让老板每天看的汇总和库管记的流水是同一套数据。这套东西适合三类人不想买几十万 SaaS 的小施工队、做劳务分包的包工头、以及接单做网站源码二次开发的 PHP 工程师。它的价值不是记账而是把“事后对账”变成“事中监控”。2. 拆开一套工地记账系统从建材流水到老板日报2.1 为什么这个场景用 PHP 源码而不是 Python/Java工地记账这个需求有个特点数据量不大、并发极低、使用人数就那么十来个但部署环境往往很糟糕——可能是一台 2C4G 的云服务器甚至一个旧电脑装的 Linux。这种场景下PHP 8 加 MySQL 8 的组合几乎是最省心的。不用编译 JVM 应用不用配 Nginx 反向代理给 Python WSGIPHP 自带 FastCGI 进程管理phpstudy 或宝塔面板点几下就能跑。很多人问网上免费 Python 源码大全里也有记账系统为什么不用 Python真要落地说Python 方案在依赖管理和部署上的门槛比 PHP 高一个量级尤其给施工队这种没有专职运维的客户交付后续更新维护会把你拖垮。PHP 的优势是“改完直接生效”没有编译构建步骤改一个文件刷一下页面就能验证这对工地现场快速改需求非常重要。至于 Java那是对着几十上百万项目去的一个小工地记账上 Spring Boot光是 JVM 调优就够喝一壶。这套源码常见的形态是原生 PHP 8 PDO MySQL或者套一个轻量的 ThinkPHP 风格框架。为什么不用 Laravel不是不行而是工地上很多源码包为了兼容老服务器写得比较土目录里直接是admin/、api/、install.sql这种结构好处是没有黑匣子任何一层逻辑翻开源码都能看懂。2.2 记账核心模块与数据库表设计材料、人工、机械、借款不管源码长什么样一套能用的工地记账系统至少要有六个模块项目档案、材料入库、人工工时、机械台班、分包结算、借款报销。这六个模块对应六张核心业务表外加用户表和项目成员关联表。以材料入库为例最保守的表结构是这样的-- 材料入库流水表每一行代表一次进场记录 CREATE TABLE material_records ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, project_id INT UNSIGNED NOT NULL COMMENT 所属项目必须强制传, item_name VARCHAR(64) NOT NULL COMMENT 材料名称如水泥、沙子, quantity DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT 数量支持小数, unit VARCHAR(10) NOT NULL DEFAULT 件 COMMENT 单位吨/方/件/袋, unit_price DECIMAL(12,2) NOT NULL COMMENT 单价单位为元, total_price DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED COMMENT 总价数据库自动算, supplier_id INT UNSIGNED DEFAULT NULL COMMENT 供应商ID可空表示现金采购, happen_date DATE NOT NULL COMMENT 业务发生日期不是录入日期, voucher_path VARCHAR(255) DEFAULT NULL COMMENT 单据照片路径白条变电子凭证, operator_id INT UNSIGNED NOT NULL COMMENT 录入人ID用来追责, remark VARCHAR(255) DEFAULT COMMENT 备注, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_project_happen (project_id, happen_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT材料入库流水;三个地方值得注意。第一金额字段全部用DECIMAL而不是FLOAT/DOUBLE浮点数的精度问题会在第五章单独说记账系统里一分钱都不能差。第二total_price用生成列而不是应用层算出后再 INSERTPython 或 PHP 端少传一个参数就少一个出错机会。第三happen_date和created_at分开存很多二次开发翻车就是因为在“录入时间”上做了月结遇到补录上个月单据的情况整个报表全错。人工工时表类似但要多一个work_type字段区分大工、小工、加班工时机械台班表则要有machine_name和operating_hours。这三张表是报表的数据源头也是后面做多项目隔离时最容易漏加project_id的地方。2.3 全开源不等于没有边界License、二次开发和数据导出说“源码全开源”接手之前一定先看清楚两件事目录里有没有 LICENSE 文件以及数据库脚本是不是完整的。常见开源协议里MIT/Apache 2.0 可以随意改、可以商用GPL 的话改了代码再分发就必须继续开源。工地记账系统这类源码包大多是作者自己写的很多时候干脆没写 License口头“全开源”意思是给你看全部代码不代表你可以拿去二次售卖这个边界心里要有数。我一般收到这类源码会先按三步评估它能不能用第一步看目录结构有没有install.sql或schema.sql没有的话让作者补第二步看数据库连接配置是不是写死在代码里的写死的直接扣分第三步看导出功能能不能把每个月账单导成 Excel 或 CSV。最后这个是硬需求工地老板看不懂系统报表但谁都看得懂 Excel给不出导出功能的记账系统等于废了一半。另外建议把“数据导出”做成一个独立模块不要依赖某个后台管理页面的按钮。因为很多开源源码的后台是 PHP 纯手写表格数据一多点击导出就超时正确做法是写一个 PHP CLI 脚本后台异步跑完导出发送下载链接这个思路在第六章会给出具体做法。3. 把源码在本地跑通从 PHP 8 环境到第一笔材料账3.1 环境准备PHP 8.x MySQL 8 phpstorm 调试开工之前先把环境统一到 PHP 8 系。网上很多工地记账源码还是 PHP 5/7 风格函数像mysql_connect()、ereg()早就删了所以本地直接用 PHP 8.3 跑一遍能快速暴露兼容性问题这比部署到服务器上再发现强得多。本地跑最省事的组合是 WSL 或 Linux 上装 PHP 8.3 和 MySQL 8# Ubuntu / Debian 系安装 PHP 8.3 和扩展 sudo apt update sudo apt install -y php8.3-cli php8.3-mysql php8.3-gd php8.3-mbstring php8.3-curl # 用 PHP 内置服务器直接起项目不需要 Nginx php -S 0.0.0.0:8080 -t /path/to/your/ledger-t指定项目根目录0.0.0.0:8080表示局域网内其他设备也能访问方便测试手机端。注意这只是本地验证手段生产环境还是建议 Nginx PHP-FPM因为内置服务器是单进程的并发一高就卡住。调代码推荐用 PhpStorm把 Deployment 配成本地目录映射浏览器装 Xdebug helper 插件断点一打就能看到$_POST里到底接了哪些参数。这套调试方式在工地现场远程改 bug 时特别管用省去反复var_dump()再删代码的体力活。3.2 导入建表 SQL 并修改数据库连接配置把第二章的 SQL 保存成install.sql在 MySQL 里建库导入mysql -uroot -p -e CREATE DATABASE ledger DEFAULT CHARSET utf8mb4; mysql -uroot -p ledger install.sql然后找到源码里的配置文件常见叫法是config.php或config.example.php。复制一份成config.php把连接参数改掉?php // config.php 数据库连接配置 define(DB_HOST, 127.0.0.1); define(DB_PORT, 3306); define(DB_NAME, ledger); define(DB_USER, ledger_user); define(DB_PASS, your_strong_password); // 统一字符集和时区乱码和8小时问题一半出在这 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); function db(): PDO { static $pdo null; if ($pdo null) { $dsn sprintf( mysql:host%s;port%s;dbname%s;charsetutf8mb4, DB_HOST, DB_PORT, DB_NAME ); $pdo new PDO($dsn, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); $pdo-exec(SET time_zone 08:00); } return $pdo; }这里用的是 PDO 而不是老式mysqli_query()好处是预处理语句天然防 SQL 注入ATTR_EMULATE_PREPARES false让 MySQL 端做真正的预编译。static $pdo保证一次请求内只建立一次连接不会每个函数都新建连接把 MySQL 拖垮。3.3 用 PHP CLI 或 curl 录入第一笔人工工时账配置文件改好之后先别打开浏览器点界面用命令行直接调接口验证明白能不能写进库。大部分这套源码里会有一个api/目录没有的话自己加一个统一入口api.php?php // api.php 极简JSON接口入口 require config.php; $action $_GET[action] ?? ; if ($action add_labor) { $data json_decode(file_get_contents(php://input), true); $stmt db()-prepare( INSERT INTO labor_records (project_id, worker_name, work_type, work_date, work_hours, unit_price, operator_id) VALUES (?, ?, ?, ?, ?, ?, ?) ); $stmt-execute([ $data[project_id], $data[worker_name], $data[work_type], $data[work_date], $data[work_hours], $data[unit_price], $_SESSION[user_id] ?? 1, ]); echo json_encode([code 0, message ok]); exit; }然后终端里用 curl 模拟一次录入curl -X POST http://127.0.0.1:8080/api.php?actionadd_labor \ -H Content-Type: application/json \ -d {project_id:1,worker_name:老李,work_type:大工,work_date:2025-04-01,work_hours:8,unit_price:350}注意work_date是业务日期不要传当前时间否则下个月补录时报表会乱。返回{code:0,message:ok}后用mysql -e SELECT * FROM labor_records;查一下数据在不在。这一步过了说明环境和数据库没有问题后面接前端页面只是体力活。4. 贴合工地现场的四个改造点照片、隔离、权限、离线4.1 让白条变电子单据照片上传与 EXIF 清理工地上的原始凭证是送货单、过磅单让库管再去电脑上敲一遍明细不现实。常见做法是给材料流水表加一个照片上传字段让库管拍张照文字信息按需补这张照片就是以后对账的证据。PHP 处理上传的代码框架大概是这样的?php function save_voucher(array $file, int $project_id): string { $allowed [image/jpeg, image/png, image/webp]; $mime (new finfo(FILEINFO_MIME_TYPE))-file($file[tmp_name]); if (!in_array($mime, $allowed, true)) { throw new RuntimeException(只支持 jpg/png/webp); } if ($file[size] 5 * 1024 * 1024) { throw new RuntimeException(单张不能超过5MB); } $ext [image/jpeg jpg, image/png png, image/webp webp][$mime]; // 文件名用随机串绝不使用用户提供的原始文件名防止路径穿越和重名覆盖 $new_name date(YmdHis) . _ . bin2hex(random_bytes(6)) . . . $ext; $target_dir __DIR__ . /uploads/project_ . $project_id . /; if (!is_dir($target_dir)) { mkdir($target_dir, 0755, true); } move_uploaded_file($file[tmp_name], $target_dir . $new_name); // 去掉 EXIF 信息手机照片里可能带 GPS 位置这里直接剥离掉 $image imagecreatefromstring(file_get_contents($target_dir . $new_name)); if ($image ! false) { imagejpeg($image, $target_dir . $new_name, 85); imagedestroy($image); } return uploads/project_ . $project_id . / . $new_name; }这里有两个关键点。第一MIME 类型必须用finfo去读文件内容不能用$_FILES[voucher][type]那个值来自浏览器请求头随便就能伪造。第二重新用 GD 写一次图片能顺带剥掉 EXIF很多手机照片里带 GPS 坐标属于位置隐私不该留在服务器上。4.2 多工地数据隔离project_id 不能只靠前端同时管两个工地的老板最怕的是 A 工地的库管录单据时选错项目把水泥记到 B 项目头上。很多简易源码的漏洞在于project_id是前端页面下拉框传上来的接口层没有校验这个项目到底是不是这个用户负责的。后端强制隔离的做法是建立“用户-项目”关联?php // 获取当前登录用户有权限的项目列表 function user_project_ids(int $user_id): array { static $cache []; if (isset($cache[$user_id])) { return $cache[$user_id]; } $stmt db()-prepare( SELECT project_id FROM user_projects WHERE user_id ? ); $stmt-execute([$user_id]); $cache[$user_id] $stmt-fetchAll(PDO::FETCH_COLUMN); return $cache[$user_id]; }每次写操作之前先做一次归属判断?php $projectId (int)$data[project_id]; $allowed user_project_ids($_SESSION[user_id]); if (!in_array($projectId, $allowed, true)) { http_response_code(403); echo json_encode([code 403, message 无权操作该项目]); exit; }注意不要用前端隐藏域存project_id因为那个值可以随请求任意篡改。只要后端执行 INSERT 和 SELECT 前都过一遍user_project_ids就能把多项目隔离做成后端硬约束。4.3 权限分清楚老板看汇总、库管记流水工地记账系统的角色一般就三种老板、库管、会计。老板要能看到所有项目的汇总数字但不应让他看到每个工人的私密备注库管能录入材料但不能删历史记录会计能改单据但不能导出发工资表。用最简化的 RBAC 实现不引入复杂组件?php // 登录时把角色写入 session $_SESSION[user_id] $user[id]; $_SESSION[role] $user[role]; // 枚举admin / keeper / accountant // 页面或接口做前置拦截 function require_role(string ...$roles): void { if (!isset($_SESSION[user_id])) { http_response_code(401); exit(请先登录); } if (!in_array($_SESSION[role], $roles, true)) { http_response_code(403); exit(当前角色无权访问); } }调用示例材料入库接口要求require_role(admin, keeper)删除单据接口要求require_role(admin)老板报表接口要求require_role(admin)同时查所有项目。权限判断一定要放在接口入口不能只在页面上把按钮隐藏否则有人直接 curl 接口就能删数据。4.4 弱网离线先把流水存本地有网再同步工地现场经常没信号库管填了一半的单子断网刷新页面全没了这会让他干脆放弃系统改回纸质。最实用的补救方案是给前端加一个 localStorage 草稿队列// 前端保存待同步流水 function queueVoucher(payload) { const list JSON.parse(localStorage.getItem(pending_vouchers) || []); payload.client_request_id crypto.randomUUID(); // 幂等键防止重复提交 list.push(payload); localStorage.setItem(pending_vouchers, JSON.stringify(list)); } // 网络恢复后同步失败保留继续等下次 async function flushQueue() { const list JSON.parse(localStorage.getItem(pending_vouchers) || []); for (const item of list) { try { const res await fetch(/api.php?actionadd_material, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(item) }); const data await res.json(); if (data.code 0) { // 服务端应检查 client_request_id 是否已存在已存在则直接返回成功 list.splice(list.indexOf(item), 1); } } catch (e) { break; // 网络还是不通退出下次再试 } } localStorage.setItem(pending_vouchers, JSON.stringify(list)); }对应的后端接口要做幂等处理在流水表加一个client_request_id唯一索引INSERT 前先查一次查到了就直接返回成功。这样库管在信号不好的地方连点两次提交也不会产生两条重复记录。5. 部署避坑PHP 8 兼容与账目对不上的 5 个排查点5.1 页面白屏 500老源码直接用了已删除的 mysql_* 函数现象源码在旧服务器上跑得好好的换到 PHP 8 环境打开页面一片空白error_log里报Call to undefined function mysql_connect()。原因PHP 7.0 已经把mysql_*系列函数移除只有mysqli_*或 PDO。很多工地记账源码是十年前写的接缝处全是mysql_query()。解决全局替换成 PDO 是正路临时保命可以用一个兼容层垫一下。先grep -rn mysql_ admin/ api/看波及范围少的话就手工改成 PDO多的话写一个compat.php把mysql_connect、mysql_query、mysql_fetch_array包一层但注意这只是争取改造时间不能当长期方案用。5.2 中文乱码数据库能查页面显示全是问号现象后台录入了“搅拌站”刷新页面变成“”但用 Navicat 直接打开表看数据是正常的。反过来数据库里是乱码页面上反而正常。原因PHP 连接 MySQL 时没指定字符集会话默认是latin1或者建库建表时用的utf8而不是utf8mb4存不了 emoji 和生僻字。解决统一三处字符集。CREATE DATABASE ... CHARSET utf8mb4PHP 连接成功后执行一次SET NAMES utf8mb4HTML 页面输出meta charsetutf-8。如果存量数据已经乱码先用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4转表结构再用UPDATE把历史脏数据按二进制方式重解。不要指望只改一处就能好三处必须一致。5.3 月底汇总对不上单价和数量相乘差了三分钱现象材料表里每条记录的总价用数据库算出来是对的但用 PHP 把unit_price * quantity累加后比 SUM(total_price) 多了几分钱。原因写代码的人当年用了DOUBLE或FLOAT存单价浮点数本质上不精确0.1 0.2 在二进制下是 0.30000000000000004。SQL 里 SUM 和单行乘法结果在舍入时走了不同路径。解决第一把字段改成DECIMAL(12,2)这是钱的正解第二PHP 端对金额计算一律使用bcmath扩展不要写$a * $b直接乘?php // 用 bcmath 做高精度乘法记住先转字符串 function money_mul(string $a, string $b): string { return bcadd(bcmul($a, $b, 4), 0, 2); }5.4 上传单据照片一直失败目录没权限、php.ini 限制现象点击上传后提示“上传失败”或“文件过大”服务器上uploads目录是 755 权限PHP 进程没有写权限。原因PHP-FPM 的工作用户是www-data不是你的登录用户另外upload_max_filesize和post_max_size默认只有 2M/8M。解决命令行先验证哪个环节挂了php -i | grep -E upload_max_filesize|post_max_size chown -R www-data:www-data /path/to/ledger/uploads注意不要图省事把整个目录设成 777安全性太差。正确姿势是目录属主改成 PHP 运行用户并禁止执行 PHP 文件否则攻击者可能传一个 webshell 上去。在 Nginx 里对 uploads 目录加上location ~* \.php$ { deny all; }。5.5 日报表差 8 个小时夜里 9 点录的账在报表里变成第二天现象工人晚上 21:00 下班后库管补录当天单据月报里这笔账日期不对正好差 8 小时。原因PHP 默认date.timezone是 UTCMySQL 的time_zone也是 SYSTEM 对应的 UTC导致NOW()和CURRENT_TIMESTAMP出来的都是 UTC 时间。解决PHP 里初始化时设置时区MySQL 连接后也执行一次。改完记得把已经入库的错误时间刷一遍UPDATE用CONVERT_TZ()或直接UPDATE tab SET created_at DATE_ADD(created_at, INTERVAL 8 HOUR) WHERE created_at 2025-01-01。这属于典型的“改完配置等于没改”的坑统一时区之后新建的库记录才安全。6. 验证与进阶用一条对账 SQL 加 Cron 月结报表记了一堆流水怎么知道系统没记漏我常用的验证手法是“三栏对账”期初库存 本期入库 - 本期出库 期末结存。这套逻辑不需要专门的盘点模块一条 SQL 就能拉出来SELECT -- 材料表中本期入库总金额 (SELECT COALESCE(SUM(total_price),0) FROM material_records WHERE project_id 1 AND happen_date BETWEEN 2025-04-01 AND 2025-04-30) AS in_total, -- 材料出库表中本期出库总金额 (SELECT COALESCE(SUM(total_price),0) FROM material_out_records WHERE project_id 1 AND happen_date BETWEEN 2025-04-01 AND 2025-04-30) AS out_total, -- 期间转入、转出合并后应等于库存余额变化 (SELECT balance FROM inventory_snapshots WHERE project_id 1 AND snap_date 2025-03-31) AS opening_balance;如果opening_balance in_total - out_total和月末盘点的系统库存对不上不是漏了入库单就是漏了出库单直接按材料名和日期下钻就能查到哪笔不对劲。把这条 SQL 写成 PHP CLI 脚本用 Cron 每天早上 8 点跑一次把月结摘要送到负责人那里就能把人工月结从两天压缩到十分钟# crontab -e 每天 08:00 执行上月月结并推送 0 8 * * * /usr/bin/php /var/www/ledger/cli/monthly_report.php --monthlast /dev/null 21脚本内部逻辑很简单查流水、算汇总、组装消息、POST 到企业微信或钉钉群机器人。要注意 CLI 脚本里没有$_SESSION所以登录校验要跳过但必须通过命令行参数传一个密钥比如--tokenxxxx否则任何人都能定时触发脚本把内部数据推出去。我最早做工地记账源码时只想着把账记进去月和月之间怎么校验完全没管结果第二个月老板拿着 Excel 跑来问我仓库里还剩的 20 吨水泥去和总账对不上。后来把上面这条对账 SQL 写进每天的任务里不管谁漏记第二天一早就现形。后来再交付这种全开源的记账系统我必做三件事统一字符集、检查金额字段类型、把对账脚本加进部署文档里。希望这些经验能帮你少走点弯路。本文还有配套的精品资源点击获取
返回列表