简介这是一套面向公益互助场景的筹款系统源码定位为大病互助与众筹平台适合需要快速搭建或二次开发同类系统的PHP开发者。源码覆盖用户注册、项目创建、捐赠流程、资金管理、一级/二级代理推广、支付接口集成及基础权限控制等核心模块结构上以PHP后端逻辑配合HTML前端页面实现。资源包共2000个文件约21.6MB以1045个HTML页面、638个PHP脚本、293个JS交互脚本和170个CSS样式为主另有PNG/GIF等图片素材及日志、配置文件整体目录清晰便于按功能模块定位代码。已有267人学习下载。拿到源码后可直接部署测试也可参考其代理层级、众筹流程与透明度报告设计用于学习完整业务链路或扩展个人项目。1. 互助筹款源码的业务链路众筹、互助与两级代理大病互助筹款系统在很多团队手上跑过典型形态用户注册后可以发起筹款项目也可以转发别人的项目链接平台靠支付手续费和两级代理的推广佣金维持运转。这套源码把众筹、大病互助、两级代理三件事揉在同一个 PHP 工程里前端资源锁定在 jQuery 2.1.4 与 Bootstrap 3.3.5包内还附了 php_xxtea.c 和 xxtea.c——这是 XXTEA 算法的 PHP 扩展源码通常用来给支付跳转参数、推广来源 Cookie 和接口签名做防篡改处理。跟纯捐赠型众筹源码相比这套东西最值得拆的是两级代理的分佣逻辑一级代理由平台指定并发展二级代理二级代理带来的每笔捐助两个层级按规则分佣。适合做公益互助、社群众筹或者想复用“邀请链路 分佣结算”骨架的团队参考。2. 文件清单透视PHP 运行底座与 php_xxtea 加密扩展编译2.1 从资源文件反推技术栈与部署年代拿到源码包先别急着配数据库把文件清单过一遍信息量很大。fontawesome-webfont.{eot,svg,ttf,woff,woff2} 全部带v4.3.0后缀这是 2015 年前后 FontAwesome 的打包命名习惯jquery.min.js 2.1.4、bootstrap.min.js 3.3.5 也都指向那个时期的前端工程。这意味着后端 PHP 代码大概率带着老写法短标签?、mysql_*函数族、$_GET/$_POST直接进 SQL 的拼接方式。你在 PHP 7.4 以上环境部署时第一轮报错会集中在这些地方后面第 5 章我会给排查顺序。真正值得注意的是php_xxtea.c和xxtea.c。原开发方没有用纯 PHP 的 XXTEA 实现而是把 C 源码放在包里说明他们的部署环境是 Linux php-fpm可以走phpize编译安装扩展。比起纯 PHP 逐字节循环C 扩展在处理支付参数加密时快一个量级而且算法实现对业务代码不可见顺带起到一点反向保护作用。CHANGELOG 文件里通常写了每次改版动了哪些表、修了哪些回调问题部署前务必先读一遍。2.2 编译安装 php_xxtea 扩展假设 PHP 装在/usr/local/php解包后进到php_xxtea目录执行cd /usr/local/src/php_xxtea /usr/local/php/bin/phpize ./configure --with-php-config/usr/local/php/bin/php-config make -j4 make installphpize的作用是为当前这个 PHP 版本生成编译配置所以必须用绝对路径指定到实际运行的 SAPI--with-php-config指向的是 php-config 工具位置而不是 php 可执行文件本身这两个参数写错最常见的结果是configure: error: Cannot find php-config。编译完成后make install会打印一行Installing shared extensions: /usr/local/php/lib/php/extensions/...把输出的 .so 路径记下来写进 php.iniecho extensionxxtea.so /usr/local/php/etc/php.ini systemctl reload php-fpm php -m | grep -i xxteaphp -m输出里能看到xxtea就说明加载成功。如果你拿到的是老版本 php_xxtea.c在 PHP 7.4 以上很可能编译失败常见报错是zend_parse_parameters参数个数不匹配或 TSRM 宏缺失需要手动把函数声明改成 PHP 7 的写法例如把ZEND_NUM_ARGS()换成ZEND_PARSE_PARAMETERS_NONE()。这一步卡住的话先用php -v确认 SAPI 版本再动手改 C 代码。2.3 用 XXTEA 给推广来源做防篡改票据这套系统里 XXTEA 的典型用途是保护“邀请来源”不被伪造。玩法是二级代理的推广链接带上自己的agent_code访客第一次进入时后端把代理 ID 和过期时间加密写进 Cookie用户注册时再解密写入member.agent_parent_id。如果不加密别人直接改 Cookie 里的代理 ID 就能把佣金挂到自己头上。$xxteaKey 8f93c2a4e1b7d506; // 从配置中心读取不要写死在业务代码里 // 生成推广来源票据 $payload json_encode([ agent_id 10086, expire time() 86400 * 7 ]); $ticket rawurlencode(xxtea_encrypt($payload, $xxteaKey)); setcookie(agent_ref, $ticket, time() 86400 * 7, /, , false, true);这里rawurlencode是必须的XXTEA 加密结果是二进制串直接塞进 Cookie 会被浏览器按 Latin-1 截断或转义导致解密失败。setcookie最后一个参数开 HttpOnly防止 XSS 脚本读取推广票据。取用时注意先rawurldecode再解密function parseAgentRef($xxteaKey) { $raw $_COOKIE[agent_ref] ?? ; if ($raw ) return 0; $dec xxtea_decrypt(rawurldecode($raw), $xxteaKey); if ($dec false) return 0; $info json_decode($dec, true); if (!$info || $info[expire] time()) return 0; return (int)$info[agent_id]; }xxtea_decrypt返回 false 有两种情况密钥不对或者密文被篡改。这里不直接抛异常而是返回 0 走“无推荐人”分支用户体验上就是佣金归平台。方案密钥长度典型场景注意事项php_xxtea 扩展128 bit 定长推广票据、短链接参数加密非标准算法密钥泄露需整体换 keyopenssl_encrypt(AES-128-ECB)16/24/32 字节支付回调敏感字段ECB 模式看不出明文模式但缺 IVpassword_hash verify自动加盐用户密码存储不可逆绝不能换成 XXTEA一个容易踩的误区把用户密码也用 XXTEA 加密存库。XXTEA 是可逆的、无盐的两个相同密码会得到相同密文一旦密钥丢失等于全体密码脱库。密码只走password_hashXXTEA 只负责短时效、可校验的票据类数据。3. 两级代理体系member 表结构、佣金流水与结算脚本3.1 一级代理和二级代理到底怎么关联先把关系模型讲清楚再写代码。一级代理由平台运营在后台直接指定系统给每个代理生成唯一邀请码用户通过邀请链接或邀请码注册后agent_parent_id指向这个一级代理。一级代理可以把名下用户升级为二级代理二级代理再发展自己的用户。关键点在于用户表中只存一个agent_parent_id两级关系靠“向上再查一次”得到用户 A 的上级是一级代理 L1L1 再往上查到的上级就是二级来源。也就是说表的组织是“每个节点只存父节点”层级深度靠查询步数控制天然限制了两级。这种设计在业务上用来控制结算层级、降低运营复杂度也避免层级无限延伸带来的分佣漏洞。3.2 建表用户、代理邀请码、佣金流水CREATE TABLE member ( id int(11) unsigned NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL, agent_level tinyint(4) NOT NULL DEFAULT 0 COMMENT 0普通用户 1一级代理 2二级代理, agent_parent_id int(11) NOT NULL DEFAULT 0 COMMENT 上级代理ID, agent_code varchar(16) NOT NULL DEFAULT COMMENT 专属邀请码, reg_time int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_parent (agent_parent_id), KEY idx_code (agent_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE commission_log ( id int(11) unsigned NOT NULL AUTO_INCREMENT, donation_id int(11) NOT NULL COMMENT 关联捐助订单, member_id int(11) NOT NULL COMMENT 获得佣金的代理ID, level tinyint(4) NOT NULL COMMENT 1一级代理 2二级代理, amount decimal(10,2) NOT NULL COMMENT 佣金金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待结算 1已入账 2已提现, create_time int(11) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_donation_level_member (donation_id, level, member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;agent_code用随机 6 位大小写字母生成时先查重uniqid()或md5截断都不是好方案短码冲突概率在数据量上去后很可观。commission_log的联合唯一键是整个分佣系统的命根子它保证“同一笔捐助、同一个层级、同一个代理”最多只结算一次支付回调重复到达时不会重复发钱。3.3 支付成功后的佣金结算脚本结算逻辑挂在支付回调里和订单收款、项目金额累加放在同一个数据库事务中。下面是一段兼容这套表结构的核心脚本function settleCommission($donationId, $donorId, $amount) { $amount round((float)$amount, 2); if ($amount 0) return; $level1Rate 0.05; // 一级代理分 5% $level2Rate 0.02; // 二级代理分 2% db()-begin(); try { $donor db()-row(SELECT agent_parent_id FROM member WHERE id ?, [$donorId]); if (empty($donor[agent_parent_id])) { db()-commit(); return; } grantCommission($donationId, $donor[agent_parent_id], 1, round($amount * $level1Rate, 2)); $l1 db()-row(SELECT agent_parent_id FROM member WHERE id ?, [$donor[agent_parent_id]]); if (!empty($l1[agent_parent_id])) { grantCommission($donationId, $l1[agent_parent_id], 2, round($amount * $level2Rate, 2)); } db()-commit(); } catch (Throwable $e) { db()-rollback(); error_log(settle_fail donation{$donationId} err{$e-getMessage()}); } } function grantCommission($donationId, $memberId, $level, $amount) { if ($amount 0) return; db()-exec( INSERT INTO commission_log (donation_id, member_id, level, amount, status, create_time) VALUES (?, ?, ?, ?, 1, ?) ON DUPLICATE KEY UPDATE id id, [$donationId, $memberId, $level, $amount, time()] ); }ON DUPLICATE KEY UPDATE id id是典型的幂等写法唯一键冲突时不报错、也不更新金额直接跳过。这样即使支付渠道把异步通知重发三次佣金也只有一条。两个利率 5% 和 2% 放在函数顶部是为了让人一眼看到参数实际项目里应该挪到setting表后台运营可调。结算失败时事务回滚订单金额、项目已筹金额、佣金流水三件事一起回滚不会出现收了钱但代理没分到的情况。4. 筹款项目状态机、支付回调验签与幂等结算4.1 先定义状态机再写发起表单筹款项目不是一张表加一个 status 字段那么简单。常见的状态集合如下状态含义可进入的操作draft用户草稿未提交编辑、删除、提交审核pending已提交待审核平台通过 / 驳回funding筹款中下架、提前结束、产生捐助completed已达目标或到期申请放款closed平台强制关闭退款处理refunded已退款完成无状态切换必须在 Service 层收敛不能允许控制器直接 UPDATE status。一个反例是“已关闭的项目还能被支付回调加上钱”问题根源就是回调逻辑只判了 status ! 1没判项目是否在 funding 状态。4.2 发起项目的字段校验这套系统的项目表围绕“发起人 目标金额 证明材料 筹款天数”四个维度。后端校验比前端校验严格得多$title trim($_POST[title] ?? ); $target (float)($_POST[target] ?? 0); $days (int)($_POST[days] ?? 0); if (mb_strlen($title) 6 || mb_strlen($title) 50) { throw new BizException(标题长度需在 6-50 字之间); } if ($target 100 || $target 1000000) { throw new BizException(目标金额需在 100 到 1000000 元之间); } if ($days 7 || $days 90) { throw new BizException(筹款天数需在 7-90 天之间); }(float)强转会把abc变成 0.0所以金额下限 100 元能顺手挡掉非数字输入天数限制 90 天是为了避免项目长期挂网后材料过期引发纠纷。图片材料建议在服务器端校验文件头而不是扩展名getimagesize()比pathinfo的扩展名判断可靠。4.3 支付下单、异步回调验签与幂等写入支付流程分三步用户下单生成平台订单号、跳转支付渠道、支付渠道异步通知回写。平台订单号不要用time().rand()这种可预测格式容易被撞单。推荐带业务前缀和随机因子$orderNo C . date(YmdHis) . str_pad((string)mt_rand(1, 99999), 5, 0, STR_PAD_LEFT); // 示例C2025062114301599999落库后作为唯一业务单号异步回调处理是整条资金链上最容易被攻击的一环。以通用支付渠道通知格式为例function onPayNotify($input) { // 1. 验签不通过直接返回失败触发渠道重试 if (!verifySign($input)) { http_response_code(400); exit(sign_error); } $orderNo $input[out_trade_no]; $paidAmount (float)$input[total_amount]; // 2. 幂等判断已支付订单直接返回成功 $order db()-row(SELECT * FROM donation_order WHERE order_no ?, [$orderNo]); if (!$order) { exit(fail); } if ($order[status] 1) { echo success; return; } // 3. 金额校验订单金额必须和渠道回调金额一致 if (abs($order[amount] - $paidAmount) 0.01) { error_log(amount_mismatch order{$orderNo}); exit(fail); } db()-begin(); try { db()-exec(UPDATE donation_order SET status1, pay_time? WHERE order_no? AND status0, [time(), $orderNo]); db()-exec(UPDATE project SET raised_amount raised_amount ? WHERE id ?, [$order[amount], $order[project_id]]); db()-exec(INSERT INTO donation_log (order_no, project_id, member_id, amount, create_time) VALUES (?,?,?,?,?), [$orderNo, $order[project_id], $order[member_id], $order[amount], time()]); settleCommission($order[id], $order[member_id], $order[amount]); db()-commit(); echo success; } catch (Throwable $e) { db()-rollback(); error_log(notify_fail {$e-getMessage()}); exit(fail); } }回调处理有三条纪律第一验签失败绝不能返回 success否则渠道会认为已送达而停止重试造成单边账第二更新订单用WHERE status0条件防止并发回调双重入账第三金额比对用abs() 0.01而不是浮点误差在支付场景是常态。settleCommission和订单更新在同一事务佣金结算保证跟随订单状态不会出现“订单未支付但代理已有佣金”的脏数据。4.4 放款与透明公示项目结束后运营审核发起人的材料、银行卡信息人工确认后放款。放款动作必须生成一条资金流水而不是直接改raised_amount否则后续对账无从谈起INSERT INTO fund_flow (project_id, type, amount, memo, create_time) VALUES (?, pay_out, ?, ?, ?); SELECT f.amount, f.type, f.memo, f.create_time, p.title FROM fund_flow f JOIN project p ON p.id f.project_id WHERE f.project_id ? AND f.type pay_out ORDER BY f.id DESC;fund_flow表同时承担“项目已筹多少、平台手续费收了多少、放款放了多少”三条流水线前端公示页直接查这张表即可不用把运营内部备注暴露出去。5. 上线自检清单XXTEA 加载、回调对账与佣金唯一性验证5.1 环境与扩展验证上线前先把运行环境钉死在一条命令里php -v php -m | grep -E pdo_mysql|curl|openssl php -m | grep -i xxtea php -r var_dump(function_exists(xxtea_encrypt));function_exists返回 true 才说明 PHP 进程真的加载了 xxtea而不是只改了 php.ini 没重载。老源码在 PHP 8.0 上最常见的三个报错是mysql_*函数不存在、each()函数被移除、count(NULL)抛 TypeError按报错顺序逐项替换成mysqli/PDO和foreach即可。5.2 业务链路验证表验证点操作预期结果失败时排查方向推广票据访问带 agent_code 的链接后注册member.agent_parent_id 正确写入检查 Cookie 是否被宽松的同域名覆盖项目审核后台通过一个 pending 项目状态变 funding前台可见状态机 Service 层是否允许该迁移支付回调用测试金额支付一笔order、project、donation_log 三表同步看 fpm 日志里notify_fail佣金唯一性同一订单连发三次回调commission_log 只有一条查联合唯一键是否建上提现代理提交提现后冻结余额待审核列表可见余额冻结检查提现事务是否提交5.3 最容易翻车的三处细节第一佣金重复入账。即使代码里写了幂等历史脏数据仍然可能存在。上线后跑一次体检 SQLSELECT donation_id, level, member_id, COUNT(*) AS cnt FROM commission_log GROUP BY donation_id, level, member_id HAVING cnt 1;查出任何cnt 1的记录都要人工核对原始订单金额后删除多余流水并同步扣减代理余额。第二金额类型统一用decimal(10,2)float只存在于 PHP 计算层落到数据库必须转 decimal。否则累积到一万笔订单后SUM 结果会和每笔累加的余额差出几分钱对账时非常被动。第三回滚必须是整条链路。调试时很多人把订单表 status 改回 0 就以为“模拟退单成功”但project.raised_amount和commission_log不会自动跟着回退。正确的做法是把三张表放进同一个事务或者用订单号作为唯一基准做二次对账SELECT o.order_no, o.amount, c.commission_sum FROM donation_order o LEFT JOIN ( SELECT donation_id, SUM(amount) AS commission_sum FROM commission_log GROUP BY donation_id ) c ON c.donation_id o.id WHERE o.status 1 AND o.amount c.commission_sum;不管回调重试、退款还是并发刷单只要这条 SQL 查不出“佣金超过订单金额”的记录资金链路就还是干净的。本文还有配套的精品资源点击获取