ARTICLE DETAIL

资讯详情

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

代付源码深度解析:ThinkPHP5+FastAdmin+Workerman打造多通道支付后台

代付源码深度解析:ThinkPHP5+FastAdmin+Workerman打造多通道支付后台 简介一套支持多模板、多支付通道的美团代付系统源码基于ThinkPHP5Workerman构建适配nginx、PHP7.3、MySQL5.6环境面向需要快速搭建代付平台或希望深入二次开发的PHP开发者与网站站长。资源包内共含2000个文件以JavaScript脚本、HTML页面、CSS样式表为主辅以JSON数据、Markdown文档及说明文本压缩包45.2MB目录结构清晰便于按模块部署和学习。已有1004人参与学习/下载适合具有PHP基础并想获得全开源代付方案的技术人员。这套源码全开源、无加密代码注释与目录结构适合快速上手源码具备强力安全加固能有效防止黑客入侵内置二十种语言一键翻译覆盖中英越俄泰日韩等常见外贸语种还提供多商户客服、不限坐席、独立部署支持SSL加密与离线对话所有数据存储在自己服务器上可直接服务于外贸代付或多语言支付业务的落地。1. 这套源码到底解决什么问题第一眼看到“美团代付源码”这个名字很容易以为是仿美团App的Demo拆开压缩包才发现完全不是。它是一套面向代付业务场景的支付后台源码商户在后台发起代付请求系统自动路由到多个支付通道再通过Workerman长连接把支付结果实时推给商户端和客服端。核心是“代付”而不是个人收款也不是C端扫码。骨架是ThinkPHP5的FastAdmin后台静态资源里backend.css、frontend.css、fastadmin.css这些命名暴露了它继承自FastAdmin后台框架。适合谁看要接代付通道的支付外包或者研究多商户客服和长连接的PHP工程师。源码的价值不在UI而在多模板、多通道、多语言这三件事怎么耦合成一个可运营的系统。2. 源码包结构与多模板机制从backend.css到FastAdmin的扩展逻辑2.1 静态资源清单里藏着什么压缩包里有一堆cssindex.css、bootstrap.css、bootstrap.min.css、main.css、backend.min.css、frontend.min.css、fastadmin.css。表面看是常规前端资源实际分类有讲究bootstrap.css是基础栅格和组件主要给登录页和公共布局用fastadmin.css是FastAdmin框架自带的后台样式定义了左侧菜单、Tab、表格按钮等backend.min.css是后台业务模块的统一入口压缩版frontend.min.css是前台商户端样式也就是商户登录后看到的操作台index.css是首页落地页的自定义样式main.css是覆盖层用于修正前面几个框架样式之间的冲突。提示看到min.css和普通css同时存在说明源码在开发模式下用的是未压缩文件生产环境用压缩文件。部署时如果改了普通css记得同步生成min版本否则线上加载的永远是旧样式。2.2 多模板切换的配置方式模板机制的落脚点在FastAdmin的template配置。FastAdmin基于ThinkPHP5默认视图路径是application/index/view但多模板场景下应该把路径切换到入口目录下的template文件夹再通过参数选择主题。框架的模板引擎配置写在application/config.php里常见做法是这样template [ type Think, view_path ./template/, view_suffix html, tpl_cache true, tpl_deny_php false, ],view_path指到template目录后还需要在公共控制器里动态拼接模板的二级目录。比如有“默认模板”和“海外模板”两套可以在基类控制器中这样写class Base extends Controller { protected function initTheme($theme ) { $theme $theme ?: cookie(theme) ?: default; $this-view-engine-config(view_path, ./template/ . $theme . /); $this-assign(theme, $theme); $this-assign(site, $this-site); } }$theme是从cookie里读出来的模板标识比如default、global。这样控制器render时ThinkPHP5会去template/default/index/index.html或template/global/index/index.html找文件。参数说明type必须是ThinkFastAdmin内置模板引擎基于ThinkPHP5模板view_suffix是模板文件后缀html为默认tpl_cache调试阶段要设为false否则改了模板不生效tpl_deny_php建议始终为false禁止在模板里直接执行php函数降低注入风险。模板目录结构一般这样组织template/ default/ index/ index.html order/ create.html global/ index/ index.html2.3 模板继承与语言变量注入多模板不仅仅是不同HTML文件还需要一套可复用的布局。FastAdmin支持ThinkPHP5的模板继承常用写法是在template/default/public/base.html中定义公共区块html head link relstylesheet href/static/{$theme}/css/main.css /head body {block namecontent}{/block} script src/static/js/common.js?v{$version}/script /body /html然后在业务页面里继承它{extend namepublic/base /} {block namecontent} div classpay-box h2{:lang(order_title)}/h2 /div {/block}{:lang(order_title)}直接调用ThinkPHP5的语言函数语言文件把每个模板里的文案抽掉。如果一套模板要在越南、泰国、印尼同时上线语言变量必须放到语言包里不能硬编码到HTML里。模板文件里还可以用{$lang}之类的前缀做条件渲染比如阿拉伯语时给html增加dirrtl属性。2.4 模板切换时的静态资源陷阱多模板切换最容易踩的坑不是HTML而是CSS/JS的路径。很多源码把静态资源写在public/assets下但每套模板的theme参数没有拼到链接上导致切换模板后页面样式还是旧的一套。正确做法是给静态资源加上theme参数和版本号script src/static/{$theme}/js/main.js?v{$version}/script其中{$version}是发布时的版本号比如20250718。加上版本号是为了绕过浏览器缓存每次发版改一个v值用户重新拉取资源。下表是资源文件定位文件模块修改后注意backend.min.css后台框架压缩文件需重新生成frontend.min.css前台商户端与template中主题联动fastadmin.cssFastAdmin核心样式不建议直接改用main.css覆盖main.css自定义覆盖层挂载在模板入口文件末尾代付系统的商户端往往要支持多币种、多语言的收银台模板里不能把文案写死。比如越南语、印尼语的文案长度差异很大用main.css去调整按钮宽度和文案溢出比改框架CSS更安全因为框架升级时不会覆盖main.css。3. 支付通道抽象与代付网关接入3.1 代付通道的数据模型设计代付系统跟普通支付的最大区别是普通支付是“收款”代付是“出钱”。所以通道侧关心的是余额、手续费、单笔限额、到账时间。源码里通常用一张channel表来保存这些参数。建议这样建表CREATE TABLE pay_channel ( id int(11) NOT NULL AUTO_INCREMENT, code varchar(32) NOT NULL COMMENT 通道标识如alipay_bank, name varchar(64) NOT NULL COMMENT 通道名称, mode tinyint(1) NOT NULL DEFAULT 0 COMMENT 0-代付 1-代收回调 2-余额查询, config text COMMENT json格式的通道配置包括appid、私钥、公钥、网关地址, weight int(11) NOT NULL DEFAULT 100 COMMENT 轮询权重, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1启用 0停用, fail_times int(11) NOT NULL DEFAULT 0 COMMENT 连续失败次数, limit_amount_min decimal(12,2) NOT NULL DEFAULT 0.00, limit_amount_max decimal(12,2) NOT NULL DEFAULT 0.00, create_time int(11) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT代付通道表;字段说明config字段用JSON存因为不同通道的密钥类型不一样有的用RSA有的用HMAC不可能给每个通道建一套字段weight控制通道被随机选中的概率数值越大越容易被选中fail_times用于熔断连续失败超过阈值会自动停用。注意mode字段代付通道和代收回调通道可能共用一张表通过mode区分。3.2 通道轮询与自动熔断实现代付请求进来后不能只调一个通道否则单个通道余额不足或接口报错整笔代付就卡住了。常见做法是把启用的通道按权重排序逐个尝试直到成功或全部失败。核心逻辑public function channelDispatch($amount, $bankCode) { $channels Db::name(pay_channel) -where(status, 1) -where(mode, 0) -where(limit_amount_min, , $amount) -where(limit_amount_max, , $amount) -order(weight desc) -select() -toArray(); foreach ($channels as $ch) { if ($ch[fail_times] 3) { continue; } $result $this-sendToChannel($ch, $amount, $bankCode); if ($result[code] 0) { Db::name(pay_channel)-where(id, $ch[id])-update([fail_times 0]); return $result[data]; } Db::name(pay_channel)-where(id, $ch[id])-setInc(fail_times); } throw new \Exception(所有代付通道均不可用); }这里有两个细节第一fail_times是“连续失败”而不是“累计失败”所以每次代付成功后要清零第二sendToChannel内部要做异常捕获不能让一个通道的超时把整个进程拖死建议给HTTP请求设置timeout 5超过5秒就视为失败。自动恢复的逻辑放到定时任务里每5分钟执行一次把所有fail_times在1到3之间的通道减1等于3的直接置回0这样通道临时性故障不会被永久熔断。3.3 回调验签与幂等控制代付通道的回调是通知“这笔钱有没有打出去”如果处理不好会出现用户收到钱但系统显示失败或者重复入账。验签的通用模式是把回调里的业务参数按字典序拼接加上通道密钥做MD5或RSA验签。以MD5为例public function callback() { $data $this-request-post(); $channel Db::name(pay_channel)-where(code, $data[channel_code])-find(); $config json_decode($channel[config], true); $signStr $data[order_no] . $data[status] . $data[amount] . $config[secret]; if (md5($signStr) ! strtolower($data[sign])) { exit(sign_error); } $order Db::name(pay_order)-where(order_no, $data[order_no])-lock(true)-find(); if ($order[status] 1) { exit(success); } Db::startTrans(); try { Db::name(pay_order)-where(order_no, $data[order_no])-update([ status $data[status], channel_order_no $data[channel_order_no], callback_time time(), ]); Db::commit(); } catch (\Exception $e) { Db::rollback(); exit(failed); } exit(success); }参数说明lock(true)是ThinkPHP5的悲观锁写法防止两个回调请求同时读到旧状态status建议用0待处理 1成功 2失败回调里不是成功就是失败不要既允许改成功又允许改失败否则通道误回调会把成功订单改成失败。验签失败时输出sign_error通道那边看到这个返回值会知道是签名问题不会一直重发。3.4 代付状态机与异常订单处理代付订单不能只依赖回调还要有主动查询补偿。建议状态流转为待处理→处理中→成功/失败。每次发起代付后把订单状态置为“处理中”并记录最后查询时间。定时任务每3分钟扫描一次处理中超过5分钟的订单主动调用通道查询接口。这个查询接口要复用channelDispatch里同一个sendToChannel的通道配置但请求的action不同。为了避免后续重复查询每次查询都要更新query_time超过5次仍未明确的订单标记为“异常需人工介入”。人工介入的操作要记录在order_log表保留操作人、时间、备注这也是支付资金来源的审计要求。4. Workerman在代付回调与多商户客服中的长连接实践4.1 为什么选Workerman而不是Swoole摘要里明确写了“框架Thinkphp5workerman”在廉价的nginxphp7.3环境里Workerman比Swoole更容易部署它是纯PHP实现不需要编译扩展php7.3跑起来完全没问题。这套源码把Workerman用在两个地方代付回调的实时通知和商户客服系统。普通HTTP请求是“用户问一次服务器答一次”但客服对话要求服务端主动推送消息客服回复了商户端要立刻弹出新消息不能靠轮询。Workerman开一个WebSocket服务让商户端和客服端都保持长连接订单状态变更或客服回复时服务端直接用$connection-send()推送。4.2 多商户客服的在线状态与消息推送Workerman服务端代码通常长这样use Workerman\Worker; $ws new Worker(websocket://0.0.0.0:2346); $ws-count 4; $ws-onConnect function ($conn) { }; $ws-onMessage function ($conn, $msg) { $data json_decode($msg, true); if ($data[type] login) { $conn-uid $data[uid]; Worker::$connections[$data[uid]] $conn; } elseif ($data[type] chat) { $to $data[to]; if (isset(Worker::$connections[$to])) { Worker::$connections[$to]-send(json_encode([ type chat, from $conn-uid, content $data[content], time time(), ])); } else { Db::name(offline_msg)-insert([ from_uid $conn-uid, to_uid $to, content $data[content], create_time time(), ]); } } };这里有一个关键点Worker::$connections保存的是当前进程的连接对象但在count4的多进程模式下每个进程是独立的login消息可能落在进程1chat消息落在进程2进程2的$connections里就没有目标用户。解决方法是使用GlobalData组件或Redis Pub/Sub。用Redis订阅发布更通用进程1收到login把uid映射写到Redis进程2发消息时从Redis查uid在哪个进程再通过Workerman的Channel组件转发给那个进程。这套源码在连接数不大时可以简化成单进程但上线超过200个坐席一定要改成多进程Redis广播。4.3 离线对话的存储与拉取策略离线消息不能无限存通常保留最近30天。拉取接口可以这样写public function pullOfflineMessages($uid) { $list Db::name(offline_msg) -where(to_uid, $uid) -where(create_time, , time() - 30 * 86400) -order(create_time asc) -limit(100) -select() -toArray(); if (!empty($list)) { Db::name(offline_msg)-where(to_uid, $uid)-delete(); } return $list; }说明每次拉取后直接删除避免重复拉取如果是同一个商户账号在PC和手机同时登录的多端场景建议改成is_read字段标记而不是物理删除。消息中要带上msg_id前端收到后做去重因为WebSocket重连后可能从Redis重新推送一次。离线消息在用户上线时一次性推给客户端再由客户端逐条确认收到也就是ACK机制。4.4 心跳与断线重连WebSocket连接会因防火墙空闲断开、网络切换等原因掉线必须做心跳。前端可以每30秒发一个ping服务端回pong。Workerman的定时器写法$ws-onWorkerStart function ($worker) { Timer::add(10, function () use ($worker) { foreach ($worker-connections as $conn) { if ($conn-lastTime time() - $conn-lastTime 90) { $conn-close(); } } }); };在onMessage里记住$conn-lastTime time()超过90秒没收到任何消息的连接直接关闭。前端在onclose事件里做重连建议采用指数退避重连间隔从1秒开始5次后固定30秒一次。nginx转发WebSocket的配置location /ws { proxy_pass http://127.0.0.1:2346; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 120s; }proxy_set_header Connection upgrade是WebSocket代理的核心少了这行浏览器会一直报WebSocket connection failed。proxy_read_timeout设120秒比心跳间隔30秒长避免nginx提前断开空闲连接。5. 国际化多语言与数据安全加固5.1 语言包与一键翻译摘要提到二十种语言ThinkPHP5的语言包放在application/lang目录每种语言一个PHP文件返回数组映射比如越南语// application/lang/vi.php return [ order Đơn hàng, amount Số tiền, pay_now Thanh toán ngay, ];后台“一键翻译”就是遍历中文文案调用翻译接口生成对应语言包。注意阿拉伯语要设置html dirrtlCSS也要切换对齐方向。语言切换用cookie记录短代码如vi、id控制器入口读取后加载对应文件。5.2 防注入二次校验FastAdmin自带input过滤但代付涉及资金关键参数建议再做正则校验public function formatRequest($data) { $filter [ order_no [format /^[A-Za-z0-9_-]{6,64}$/], amount [format /^\d(\.\d{1,2})?$/], bank_no [format /^\d{12,19}$/], ]; foreach ($filter as $key $rule) { if (empty($data[$key]) || !preg_match($rule[format], $data[$key])) { throw new \Exception(参数格式错误: . $key); } } }银行账号必须纯数字金额不能有逗号和币种符号。API接口再加一层签名校验商户独立api_secret参数按字典序拼接MD5防止伪造代付请求。5.3 上线自查代付系统上线前重点检查四项检查项判断标准验证命令后台目录权限运行用户不可写ls -l /data/wwwroot/pay/applicationWorkerman端口仅内网或nginx反代可达netstat -antlp | grep 2346PHP危险函数禁用eval、systemphp --ini看disable_functions数据库备份每日至少一次crontab -l查看备份任务Workerman是常驻内存进程启动命令建议加php -d memory_limit256M server.php start避免长时间运行被内存限制杀掉。后台入口public/admin.php应在nginx里加IP白名单。二次开发时把通道适配抽成ChannelInterface验签、报文转换、余额查询三个方法分开新增通道只加实现类不动控制器逻辑。本文还有配套的精品资源点击获取
返回列表