简介:一套2022更新修复版的逸轩小微支付系统全开源源码,面向需要搭建微信/支付宝服务商小微商户进件通道的技术开发者与支付系统运维人员,解决支付接口对接、商户进件管理等场景需求。压缩包约195.6MB,已适配PHP 7.2与MySQL 5.7及以上环境,内含完整项目代码、数据库脚本install.sql及环境配置指引,并针对宝塔面板的禁用函数、运行目录、伪静态与目录权限等关键项给出明确设置说明,可帮助读者减少部署踩坑。资源已有376人学习,适合具备一定PHP/Laravel基础、希望快速获得一套可运行支付源码用于二次开发或学习支付体系结构的读者。由于系统涉及支付资金安全,建议部署时重点检查加密扩展与商户进件参数配置,自行做好代码审计与风险评估。
1. 做服务商最头疼的进件环节,这套逸轩小微支付系统源码值不值得下
做支付服务商这行,最磨人的不是费率谈判,而是小微商户进件。一套资料在微信服务商后台填完,再去支付宝服务商后台填一遍,字段对不上、审核被退回,来回折腾一整天是常事。我拆这套逸轩小微支付系统源码的原因很简单:它把微信服务商进件、支付宝服务商进件、商户资料管理全部收进一个后台,一次采集、双渠道提交,页面字段和官方进件接口一一对应。适合做支付服务商的技术、给小微商户做代收系统的外包团队,也适合想研究官方进件API怎么落地的开发者。这篇笔记会把链路、参数、坑一次性讲清楚。
这套源码是2022年更新修复过的全开源版本,比早期流传的版本多补了微信进件接口的字段变动和支付宝异步回调查询逻辑。拿到手第一步不是急着部署,而是先看明白它在你整个支付体系里的位置——它是一个进件中台,不是支付收单程序本身。
2. 逸轩小微进件系统在中间层做的事:进件链路与源码结构
2.1 一条进件请求在系统里绕了哪些环节
先看角色。微信/支付宝服务商模式里四个角色:服务商(也就是你)、小微商户、官方支付平台、以及这套系统。系统唯一职责是代替你在两个官方后台之间做搬运和翻译。
一条进件记录在系统里按这个顺序走:
- 操作员在后台录入商户资料,包括营业执照照片、经营者身份证正反面、结算银行卡、门店照片。
- 提交后系统先把资料存进本地数据库,状态为“草稿”,这一步保证了渠道审核被拒时能改字段重新提交,不用重新录入。
- 按渠道配置组装参数,微信把这套参数POST到
applyment4sub/applyment,支付宝走alipay.merchant.indirect.indirect.sync。 - 渠道受理后返回进件单号,系统把单号和本地商户绑定,状态变成“进件中”。
- 后续靠两条线更新状态:一条是微信的异步回调,一条是系统里设置的定时轮询任务。
- 审核通过后,商户出现在后台“已进件”列表里,可以继续做绑定收款配置。
我拆的时候特别注意第三步。不同渠道的字段命名差异很大,比如同一个经营者姓名,微信叫contact_name,支付宝叫legal_name,而同一个结算卡号,微信在bank_account,支付宝在account_no。这套源码核心价值就是维护了这套字段映射表,让录入页面只保留一套中文标签,底层各自翻译。
2.2 源码结构与核心文件说明
解开压缩包后目录结构是常见PHP项目布局,入口在public/,业务代码在app/下按控制器、模型、视图分层。核心文件和职责如下表。
| 路径 | 职责 |
|---|---|
app/controller/MchApply.php | 商户进件录入与提交入口 |
app/controller/WechatPay.php | 微信服务商进件API封装与签名 |
app/controller/AlipayIsv.php | 支付宝ISV进件API封装与签名 |
app/controller/Notify.php | 渠道异步回调接收与状态更新 |
app/service/ApplyStatus.php | 进件状态机与审核驳回处理 |
config/config.php | 数据库、渠道证书、密钥配置 |
public/jobs/checkedApply.php | 定时轮询进件状态的任务脚本 |
读代码的顺序按“录入→提交→回调→轮询”四条线走。先读MchApply.php知道页面字段落到哪些表,再读WechatPay.php和AlipayIsv.php看两边参数怎么组织,最后读Notify.php。
// app/service/ApplyStatus.php 里状态机核心代码(节选) public function transit($applyId, $event) { $apply = $this->find($applyId); $statusMap = [ 'draft' => ['submit' => 'submitting'], 'submitting'=> ['pass' => 'approved', 'reject' => 'rejected'], 'rejected' => ['resubmit'=> 'submitting'], ]; if (isset($statusMap[$apply['status']][$event])) { return $this->saveStatus($applyId, $statusMap[$apply['status']][$event]); } throw new \RuntimeException('非法的状态流转:' . $apply['status'] . ' -> ' . $event); }这段把进件状态流转收敛成一张表:草稿只能提交,提交中只能被审核通过或驳回,驳回后只能重新提交。好处是回调、轮询、用户手动操作共用一个状态入口,不会出现数据库里一个商户反反复复在“审核中”和“已驳回”之间横跳。改动状态逻辑只改$statusMap这一个数组就行。
数据库里核心表是xw_mch_apply,里面form_data字段以JSON存全部表单资料,这样微信支付宝两渠道参数差异不会散落到几十个字段里。这个设计有利有弊:好处是渠道接口升级时只需要改控制器组装逻辑;坏处是SQL里没法直接按“手机号”查商户,二开做列表搜索时得用JSON_EXTRACT。我一般会在需要按手机号检索时,把contact_mobile冗余到单独字段。
实际操作建议先跑通“草稿”链路。不要一上来就提交官方正式进件。这套系统数据库里有一个config表存渠道开关,开发环境把开关关掉,这样录进去的资料只落库不请求官方,避免测试数据污染真实商户。这个习惯帮我避了很多麻烦。另外,目录里可能有Runtime或logs目录要赋写权限;public/下.htaccess和Nginx配置对应着伪静态规则。常见做法是用宝塔或LNMP一键环境,把站点根目录指向public/,否则路由全乱。
3. 本地部署逸轩小微支付系统:PHP环境、数据库初始化与服务商参数接入
3.1 环境要求与部署前检查
这套源码是PHP+MySQL的结构,部署前先确认环境。我一般用PHP 7.1以上、MySQL 5.7以上,Nginx首选,扩展里必须有openssl、curl、fileinfo、gd。前两个管HTTPS请求和证书,fileinfo管上传文件类型识别,gd用在图片压缩处理。
先跑一组命令确认:
php -v php -m | grep -Ei 'openssl|curl|fileinfo|gd' mysql --versionphp -m输出里如果少了fileinfo或gd,后台上传营业执照和身份证照片时会直接白屏或返回“上传失败”,这是最典型的部署期翻车点。缺扩展的补救:
# CentOS 上安装扩展(以PHP 7.4、remi源为例) yum install php74-php-gd php74-php-fileinfo systemctl restart php-fpm别小看这步。我见过两回部署到一半卡住,都是因为环境里gd没装,页面能打开、进件通道建不了。检查完扩展之后看伪静态配置。Nginx站点配置:
server { listen 80; server_name pay.example.com; root /www/wwwroot/xiaowei/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }关键是把root指到public/,同时用if (!-e $request_filename)做路由重写。如果直接指到项目根目录,访问后台会出现404或者控制器方法名暴露在URL里的现象。
改完配置记得重启PHP-FPM并清掉Runtime缓存,因为源码会把config缓存下来,不清理的话界面改了配置实际请求还是旧值。这个坑后面配置服务商参数时会再次遇到。
3.2 数据库初始化与系统配置文件
源码包里带一份SQL文件,常见命名是install.sql或xiaowei_pay.sql。我建议用命令行导入而不是用phpMyAdmin导入,因为文件里可能带存储过程或触发器,phpMyAdmin容易卡。
mysql -uroot -p --default-character-set=utf8mb4 < xiaowei_pay.sql导入后进config/config.php,把数据库连接、系统URL、加密密钥改掉:
// config/config.php 关键配置节选 return [ 'database' => [ 'host' => '127.0.0.1', 'port' => 3306, 'name' => 'xiaowei_pay', 'user' => 'pay_user', 'password' => '改成一个够长的密码', 'charset' => 'utf8mb4', ], 'app_url' => 'https://pay.example.com', 'app_key' => '一串随机字符串,至少32位', ];数据库字符集必须用utf8mb4。小微商户联系人姓名和门头照审核备注里可能出现生僻字和Emoji,utf8的库存这些会变问号,提交到微信服务商接口后反被驳回。
导入完先别急着配支付参数,先把后台登录跑通。这套源码默认有一个安装账号,登录后第一件事修改管理员密码,然后把平台名称换掉,避免被扫描到默认标识。
3.3 微信/支付宝服务商参数接入
进件要能提交成功,服务商参数必须完整。在后台“系统设置”里填,但源码里真正起作用的其实是config里这段:
// config/config.php 渠道配置节选 'channel' => [ 'wechat' => [ 'enabled' => true, 'mch_id' => '你的微信服务商商户号', 'app_id' => '服务商公众账号APPID', 'api_v3_key' => '32位APIv3密钥', 'serial_no' => '商户API证书序列号', 'private_key' => '/www/wwwroot/xiaowei/cert/apiclient_key.pem', 'notify_url' => 'https://pay.example.com/index.php/notify/wechat', ], 'alipay' => [ 'enabled' => true, 'app_id' => '支付宝开放平台应用APPID', 'private_key' => '/www/wwwroot/xiaowei/cert/alipay_app_private.pem', 'public_key' => '支付宝公钥内容', 'out_biz_id_prefix' => 'XW', 'notify_url' => 'https://pay.example.com/index.php/notify/alipay', ], ],微信侧最容易填错的是private_key路径。源码加载的是apiclient_key.pem原始私钥文件,不是证书压缩包里的apiclient_cert.pem,更不是p12。我见过有人把.p12传上去,结果签名一直报“证书序列号或私钥不对”。
支付宝侧填的public_key是支付宝开放平台“应用信息”里看到的支付宝公钥,不是自己生成的RSA公钥。这两个东西混了,签名验证永远过不去。
配置完成后可以做一个连通性测试:在后台建一个测试商户,只填必填资料,提交一次进件。如果返回官方受理单号,说明服务商参数链路是通的,后面再去调字段细节。
4. 微信与支付宝服务商进件对接:参数映射、状态轮询与回调处理
4.1 微信服务商小微进件的参数映射
系统后台录入字段是中文标签,但提交到微信的接口参数是按官方文档命名的。源码里微信这条线的参数映射关系整理如下。
| 系统表单字段 | 微信API参数 | 类型 |
|---|---|---|
| 业务申请编号 | business_code | string |
| 外部请求号 | out_request_no | string |
| 联系人姓名 | contact_name | string |
| 联系人身份证号 | contact_id_card_number | string |
| 联系人手机号 | contact_mobile | string |
| 身份证人像面 | contact_id_card_front | media_id |
| 身份证国徽面 | contact_id_card_back | media_id |
| 营业执照照片 | business_license_copy | media_id |
| 经营者姓名 | legal_person_name | string |
| 结算银行卡号 | bank_account | string |
| 开户行总行 | account_bank | string |
| 开户省市 | bank_province/bank_city | string |
| 商户简称 | merchant_shortname | string |
组装参数时要注意media_id不是文件路径。源码里WechatPay.php先调图片上传接口拿media_id,再组装进件参数。
// app/controller/WechatPay.php 提交进件核心流程(节选) public function submitApply($applyData) { $mediaIds = []; foreach (['business_license_copy', 'contact_id_card_front', 'contact_id_card_back'] as $field) { if (empty($applyData[$field])) { continue; } $mediaIds[$field] = $this->uploadMedia( $applyData['mch_id'], realpath($applyData[$field]) ); } $params = [ 'business_code' => $applyData['apply_sn'], 'out_request_no' => $applyData['out_request_no'], 'contact_name' => $applyData['contact_name'], 'contact_mobile' => $applyData['contact_mobile'], 'contact_id_card_number' => $applyData['contact_id_card_number'], 'business_license_copy' => $mediaIds['business_license_copy'] ?? '', 'contact_id_card_front' => $mediaIds['contact_id_card_front'] ?? '', 'contact_id_card_back' => $mediaIds['contact_id_card_back'] ?? '', // 结算银行信息 'account_bank' => $applyData['account_bank'], 'bank_account' => $applyData['bank_account'], 'account_name' => $applyData['legal_person_name'], 'account_type' => 'ACCOUNT_TYPE_PRIVATE', ]; return $this->post('/applyment4sub/applyment', $params); }第一段循环是要把三张图片先传成media_id,第二段才是正式的进件参数。这里有个隐藏约束:微信要求图片必须是jpg格式、尺寸要大于1000像素,源码里对上传文件做了压缩转码,压缩质量默认80。如果你后台上传的图片是png超大图,压缩后还是超过2MB,微信会返回“图片大小超过限制”。
business_code和out_request_no两个字段不要混。business_code是服务商自己为这次进件生成的业务编号,out_request_no是后续查询进件结果用的外部请求号。源码里两者都由系统生成,但一个用于对账展示,一个用于状态查询,顺序错了查询接口会一直报单号不存在。
4.2 支付宝服务商进件的参数与同步逻辑
支付宝ISV进件走的是带sync语义的接口,含义是把商户信息同步给支付宝,异步返回审核结果。源码里AlipayIsv.php主要调两个接口:进件同步alipay.merchant.indirect.indirect.sync和查询alipay.merchant.indirect.indirect.query。
支付宝参数映射相对简单,核心参数就几个。
// app/controller/AlipayIsv.php 组装进件参数(节选) public function buildParams($applyData) { return [ 'out_biz_id' => $applyData['out_biz_id'], 'alias_name' => $applyData['merchant_shortname'], 'cert_type' => 'IDENT_CARD', 'cert_no' => $applyData['legal_person_id_card_number'], 'legal_name' => $applyData['legal_person_name'], 'sub_mch_id' => $applyData['sub_mch_id'] ?? '', 'shop_name' => $applyData['shop_name'], 'isv_outer_id' => $this->config['alipay']['out_biz_id_prefix'] . $applyData['apply_sn'], ]; }alias_name是商户的对外简称,这个字段一旦被支付宝审核通过后再次修改会触发重新进件。所以二开时如果要做“商户资料变更”,不能直接改原记录,而是生成一条新的进件申请单,带新的isv_outer_id提交。源码里这个逻辑写在ApplyStatus.php的resubmit事件里。
支付宝不像微信那样强依赖图片media_id,但在商户主体和结算账户信息上要求准确。源码里支付宝进件的银行卡信息是从同一个表单字段取的,只改参数名映射过去。要注意支付宝的account_no是卡号,account_name是开户名,这两个字段在中文后台表单里都叫“结算账户”,很多二开者在这里把卡号和户名填反。
4.3 进件状态轮询与异步回调
微信进件审核结果会通过异步回调推送到notify_url,支付宝不会推送进件结果,必须主动查询。所以这套源码用了两条互补链路:回调负责微信,定时任务负责支付宝。
回调控制器核心逻辑:
// app/controller/Notify.php 微信进件结果回调(节选) public function wechat() { $body = file_get_contents('php://input'); $headers = $this->getRequestHeaders(); $signStr = $headers['Wechatpay-Serial'] . "\n" . $headers['Wechatpay-Timestamp'] . "\n" . $headers['Wechatpay-Nonce'] . "\n" . $body . "\n"; $verify = openssl_verify($signStr, base64_decode($headers['Wechatpay-Signature']), $this->wechatPublicKey(), OPENSSL_ALGO_SHA256); if ($verify !== 1) { return 'FAIL'; } $event = json_decode($body, true); // 微信推送的事件:审核通过、驳回、补充材料 $applyService = new ApplyStatus(); $applyService->transit($event['out_request_no'], $this->mapEvent($event['event_type'])); return 'SUCCESS'; }这里最容易被忽略的是验签串的换行符。微信要求serial + "\n" + timestamp + "\n" + nonce + "\n" + body,中间少一个\n验签必失败。源码里特意把拼接写在一个变量里,二开时不要为了“优化”去改成数组拼接。
支付宝查询靠定时任务public/jobs/checkedApply.php,核心就是一个循环加限流:
// public/jobs/checkedApply.php 轮询任务(节选) while (true) { $list = $db->query("SELECT * FROM xw_mch_apply WHERE channel='alipay' AND status='submitting' LIMIT 10"); foreach ($list as $apply) { $result = $alipay->query($apply['out_biz_id']); if ($result['status'] === 'SUCCESS') { $applyService->transit($apply['id'], 'pass'); } elseif ($result['status'] === 'FAIL') { $applyService->transit($apply['id'], 'reject'); } } sleep(60); }上线时用crontab每分钟启动一次这个脚本,注意加flock锁防止上一轮没跑完下一轮又进来:
*/1 * * * * flock -xn /tmp/xw_checked_apply.lock php /www/wwwroot/xiaowei/public/jobs/checkedApply.php >> /www/wwwroot/xiaowei/logs/checked.log 2>&1flock -xn是拿不到锁就退出,保证同一时间只有一个轮询进程在跑。不加锁的话,进件单量上来后会出现重复查询,虽然transit有状态机保护不至于把数据改乱,但支付宝接口频率限制可能会被触发。
5. 进件接入避坑实录:五个高频翻车点的定位与修复
5.1 微信进件报“图片上传失败”不是网络问题
现象:后台上传营业执照,前端提示“图片上传失败”,看日志HTTP调用本身是通的。
原因:这套源码的上传接口把图片转码成jpg后,没有检查是否超过了微信2MB限制。手机上拍的门头照传到后台,压缩质量虽然降低了,但分辨率极高,转出来的jpg仍然超过2MB。
解决:上传类目里加一层尺寸判断和二次压缩。我看代码发现原来转码用的是imagejpeg,没有按微信要求的宽高上限做缩放。改法是先getimagesize判断最长边,超过1200像素就等比缩放再压缩,这样能把门头照压到300KB左右。实测微信端通过率明显提升。
5.2 银行卡四要素校验不过,卡得我怀疑人生
现象:进件被拒,微信驳回理由“结算账户信息有误,请核实开户行信息”。
原因:小微进件的结算卡要求与经营者本人身份信息一致,而且开户行字段不能只填总行名称。很多小微个体户拿的是农村信用社、村镇银行卡,这些卡在微信的account_bank枚举里用简称,源码下拉框里如果还是老枚举值,提交过去就校验不过。
解决:先看微信服务商文档里最新account_bank枚举值,再改后台数据字典。还有一个隐形坑:对私结算卡account_type是ACCOUNT_TYPE_PRIVATE,这是源码里写死的,小微商户默认只能对私,运营方如果改成对公,微信直接驳回。
5.3 支付宝进件提示“商户已存在”进不了下一步
现象:录完资料点提交,支付宝返回“该商户已存在”,本地看却是新记录。
原因:支付宝ISV进件以身份证号为主键识别商户。同一个经营者身份证,被另一个ISV进过件,或者之前渠道切换时在老ISV那里注册过,再同步就会撞车。源码的buildParams里cert_no就是从表单取身份证号,没有先查重。
解决:提交前先调alipay.merchant.indirect.indirect.query查询该证件是否已存在渠道商户号。如果已存在,要么走商户绑定流程而不是进件流程,要么在参数里带上已存在的sub_mch_id做关联。源码里这部分在AlipayIsv.php有注释半成品,我二开时把它补完整了。
5.4 异步回调收不到通知,这是最玄学的一个
现象:微信进件审核通过,后台状态卡在“进件中”一直不变。
原因:三个可能。一是notify_url用的是http,微信服务商回调解密要求HTTPS;二是notify_url里带了index.php/notify/wechat,但Nginx伪静态配置没生效,微信请求打到404;三是回调处理返回了FAIL,微信会重试三次后放弃,但日志里没有打印具体验签失败原因。
解决:先看Nginx access.log里有没有微信回调IP的POST记录。有记录但业务库不变,再看logs/notify.log里验签错误;连记录都没有,直接确认外网能否访问notify_url,并检查防火墙是否放行了443。我在本地联调时一般是把微信官方回调报文存文件,然后用curl重放。
5.5 进件被拒提示信息不完整
现象:微信驳回理由笼统,后台看不到具体是哪个字段的问题。
原因:这不算微信的问题,是源码的坑。微信驳回时带audit_detail数组,里面的字段名是官方参数名,比如legal_person_id_card_number,但后台详情页只渲染了驳回文本,没有把字段名映射回中文表单标签,看着就是“联系人不完整”,查半天不知道哪个不完整。
解决:在详情页做一层映射,把官方参数名翻译成后台中文标签,同时保留原始值方便对比。我改完后台后,五百个商户进件的驳回沟通成本明显下降,客服截图发给商户,商户自己就知道该改哪张照片。
提示:部署时先确认
logs/目录可写,否则回调验签失败和进件提交异常都无处可查。这个目录在源码里默认是禁止外部访问的,配Nginx时不需要额外处理。
6. 上线前走一遍这组状态机测试,二开才敢提交生产
6.1 进件状态自测清单
部署完不要直接拿真实商户试。我一般先在后台关掉微信和支付宝渠道开关,用测试商户跑一遍状态流转,对照下表检查。
| 操作 | 预期状态 | 检查点 |
|---|---|---|
| 新建商户资料 | 草稿 | 数据落库,渠道开关关闭时不发起请求 |
| 提交进件 | 进件中 | 渠道开关打开时生成out_request_no |
| 模拟微信回调pass | 已通过 | 状态机只允许从“进件中”流转 |
| 模拟微信回调reject | 已驳回 | 驳回原因写入明细表 |
| 驳回后修改资料再提交 | 进件中 | 生成新的进件单,不覆盖原单 |
| 对已通过商户再次提交 | 报错 | 状态机拦截重复提交 |
6.2 一个临时调试接口
我二开时会临时加一个调试接口,打印最后一次组装好的进件参数,用来对比官方文档。位置放在app/controller/Debug.php,上线前删掉。代码很短:
// app/controller/Debug.php 临时调试接口,上线前务必删除 public function dumpWechatParams($applyId) { $apply = db('mch_apply')->where('id', $applyId)->find(); $wechat = new WechatPay(); $raw = $wechat->buildParams(json_decode($apply['form_data'], true)); echo '<pre>' . htmlspecialchars(json_encode($raw, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre>'; exit; }这个接口的价值在于能看到PHP数组最终长什么样。很多官方文档里的参数名和源码变量名差一个单词,肉眼检查数组比逐行读代码快得多。我在对接微信费率变更时就靠它确认business_code没有被意外填成out_request_no。
6.3 二次开发衔接点
如果下一步要加“批量导入进件”,改动点集中在MchApply.php的导入方法里,状态机不用动,因为批量导入只是把多张草稿表记录变成待提交状态,提交动作仍然走transit。如果要加“商户资料变更”,注意不要复用pass事件,需要先走submit生成新进件单,否则会把原单状态覆盖掉。
这套源码真正值钱的地方是把进件链路做成了可审计的本地数据流,但状态机表是写死在ApplyStatus.php里的,业务扩展时优先扩事件类型而不是改状态数组。我后来上批量导入功能时,只加了import事件,其余行的代码没动过一行。
最后说个教训:我最早部署这套系统时,为了省事把回调验签临时改成直接返回SUCCESS,结果微信推送真实进件结果时数据全乱了,那批商户我人工在微信后台核对了整整两天才恢复。从那以后我每次部署这套源码,都强制走一遍上面这组状态机自测,回调验签永远不跳过。希望帮到你。
本文还有配套的精品资源,点击获取