当前位置: 首页 > news >正文

Hyperf对接对账

基于搜索结果,yansongda/hyperf-pay 是 Hyperf 生态中最优雅的支付 SDK,支持支付宝、微信、银联,并内置对账(query/reconcile)能力。下面我写一个完整的对账案例。 --- Hyperf 支付对账案例 基于 https://github.com/yansongda/hyperf-pay,覆盖微信和支付宝的对账下载与差异核对。 目录结构 app/ ├── Service/ │ └── ReconciliationService.php# 核心对账逻辑├── Job/ │ └── ReconciliationJob.php# 异步对账任务├── Controller/ │ └── ReconciliationController.php └── Model/ └── Order.php config/ └── autoload/ └── pay.php ---1. 安装依赖composerrequire yansongda/hyperf-pay ---2. 配置文件 config/autoload/pay.php<?phpreturn['alipay'=>['default'=>['app_id'=>env('ALIPAY_APP_ID'),'app_secret_cert'=>env('ALIPAY_APP_SECRET_CERT'), // 应用私钥'app_public_cert_path'=>BASE_PATH.'/cert/appCertPublicKey.crt','alipay_public_cert_path'=>BASE_PATH.'/cert/alipayCertPublicKey_RSA2.crt','alipay_root_cert_path'=>BASE_PATH.'/cert/alipayRootCert.crt','return_url'=>env('ALIPAY_RETURN_URL'),'notify_url'=>env('ALIPAY_NOTIFY_URL'),'mode'=>env('PAY_MODE','normal'), // sandbox / normal],],'wechat'=>['default'=>['mch_id'=>env('WECHAT_MCH_ID'),'mch_secret_key'=>env('WECHAT_MCH_SECRET_KEY'),'mch_secret_cert'=>env('WECHAT_MCH_SECRET_CERT'),'mch_public_cert_path'=>BASE_PATH.'/cert/wechat_public.pem','notify_url'=>env('WECHAT_NOTIFY_URL'),'mode'=>env('PAY_MODE','normal'),],],];---3. 核心对账服务 app/Service/ReconciliationService.php<?php declare(strict_types=1);namespace App\Service;use App\Model\Order;use Hyperf\Logger\LoggerFactory;use Psr\Log\LoggerInterface;use Yansongda\Pay\Pay;class ReconciliationService{private LoggerInterface$logger;publicfunction__construct(LoggerFactory$loggerFactory){$this->logger=$loggerFactory->get('reconciliation');}/** * 支付宝对账:下载账单并与本地订单比对 *$date格式:2026-04-21 */ publicfunctionreconcileAlipay(string$date): array{$this->logger->info("开始支付宝对账",['date'=>$date]);//1. 拉取支付宝账单(trade=交易账单,signcustomer=签约账单)$result=Pay::alipay()->download(['bill_type'=>'trade','bill_date'=>$date,]);// 返回的是 CSV 内容字符串$billRows=$this->parseCsv($result->getBody()->getContents());//2. 拉取本地当天订单$localOrders=Order::query()->whereDate('paid_at',$date)->where('channel','alipay')->where('status','paid')->get()->keyBy('out_trade_no');// 以商户订单号为 keyreturn$this->diff($billRows,$localOrders,'alipay');}/** * 微信对账:下载账单并与本地订单比对 *$date格式:20260421*/ publicfunctionreconcileWechat(string$date): array{$this->logger->info("开始微信对账",['date'=>$date]);//1. 拉取微信账单$result=Pay::wechat()->download(['bill_date'=>$date,'bill_type'=>'ALL', // ALL / SUCCESS / REFUND]);$billRows=$this->parseCsv($result->getBody()->getContents());//2. 拉取本地当天订单$localOrders=Order::query()->whereDate('paid_at', str_replace('-','',$date))->where('channel','wechat')->where('status','paid')->get()->keyBy('out_trade_no');return$this->diff($billRows,$localOrders,'wechat');}/** * 单笔订单查询(实时核对) */ publicfunctionqueryOrder(string$outTradeNo, string$channel): array{$order=Order::where('out_trade_no',$outTradeNo)->firstOrFail();if($channel==='alipay'){$remote=Pay::alipay()->query(['out_trade_no'=>$outTradeNo]);$remoteStatus=$remote->trade_status ??'';$remoteAmount=(float)($remote->total_amount ??0);}else{$remote=Pay::wechat()->query(['out_trade_no'=>$outTradeNo]);$remoteStatus=$remote->trade_state ??'';$remoteAmount=(float)($remote->amount->total ??0)/100;// 微信单位是分}$matched=$this->statusMatch($remoteStatus,$order->status,$channel)&&abs($remoteAmount-(float)$order->amount)<0.01;if(!$matched){$this->logger->warning("订单核对不一致",['out_trade_no'=>$outTradeNo,'local_status'=>$order->status,'remote_status'=>$remoteStatus,'local_amount'=>$order->amount,'remote_amount'=>$remoteAmount,]);}return['matched'=>$matched,'out_trade_no'=>$outTradeNo,'local_status'=>$order->status,'remote_status'=>$remoteStatus,'local_amount'=>$order->amount,'remote_amount'=>$remoteAmount,];}// ------------------------------------------------------------------------- privatefunctiondiff(array$billRows,$localOrders, string$channel): array{$result=['matched'=>[],'only_in_bill'=>[], // 渠道有,本地无(漏单)'only_in_local'=>[], // 本地有,渠道无(幽灵单)'amount_diff'=>[], // 金额不一致];$billMap=[];foreach($billRowsas$row){$no=$channel==='alipay'?($row['商户订单号']??''):($row['商户订单号']??'');if(!$no)continue;$billMap[$no]=$row;}// 渠道账单 vs 本地 foreach($billMapas$no=>$billRow){if(!isset($localOrders[$no])){$result['only_in_bill'][]=$no;continue;}$localAmount=(float)$localOrders[$no]->amount;$remoteAmount=$this->extractAmount($billRow,$channel);if(abs($localAmount-$remoteAmount)>0.01){$result['amount_diff'][]=['out_trade_no'=>$no,'local_amount'=>$localAmount,'remote_amount'=>$remoteAmount,];}else{$result['matched'][]=$no;}}// 本地 vs 渠道账单(找幽灵单) foreach($localOrdersas$no=>$order){if(!isset($billMap[$no])){$result['only_in_local'][]=$no;}}$this->logger->info("对账完成",['channel'=>$channel,'matched'=>count($result['matched']),'only_in_bill'=>count($result['only_in_bill']),'only_in_local'=>count($result['only_in_local']),'amount_diff'=>count($result['amount_diff']),]);return$result;}privatefunctionparseCsv(string$content): array{$lines=explode("\n", trim($content));if(count($lines)<2)return[];// 支付宝/微信账单首行是标题,末尾几行是汇总,需过滤$headers=str_getcsv(array_shift($lines));$rows=[];foreach($linesas$line){$line=trim($line);// 跳过汇总行(以"合计"开头)if(empty($line)||str_starts_with($line,'合计')||str_starts_with($line,'总计')){continue;}$values=str_getcsv($line);if(count($values)===count($headers)){$rows[]=array_combine($headers,$values);}}return$rows;}privatefunctionextractAmount(array$row, string$channel): float{// 支付宝账单列名:交易金额;微信账单列名:订单金额$key=$channel==='alipay'?'交易金额':'订单金额';return(float)($row[$key]??0);}privatefunctionstatusMatch(string$remoteStatus, string$localStatus, string$channel): bool{if($channel==='alipay'){return$remoteStatus==='TRADE_SUCCESS'&&$localStatus==='paid';}// wechatreturn$remoteStatus==='SUCCESS'&&$localStatus==='paid';}}---4. 异步对账任务 app/Job/ReconciliationJob.php<?php declare(strict_types=1);namespace App\Job;use App\Service\ReconciliationService;use Hyperf\AsyncQueue\Job;class ReconciliationJob extends Job{publicfunction__construct(private string$date, private string$channel// alipay|wechat){}publicfunctionhandle(): void{$service=make(ReconciliationService::class);$result=match($this->channel){'alipay'=>$service->reconcileAlipay($this->date),'wechat'=>$service->reconcileWechat($this->date), default=>throw new\InvalidArgumentException("Unknown channel: {$this->channel}"),};// 有差异时告警(可接入钉钉/企微/邮件)if(count($result['only_in_bill'])>0||count($result['only_in_local'])>0||count($result['amount_diff'])>0){// TODO: 发送告警通知}}}---5. 定时触发对账 config/autoload/crontab.php<?php use Hyperf\Crontab\Crontab;return['enable'=>true,'crontab'=>[// 每天凌晨2点跑前一天对账(new Crontab())->setName('daily-reconciliation')->setRule('0 2 * * *')->setCallback(function(){$date=date('Y-m-d', strtotime('-1 day'));foreach(['alipay','wechat']as$channel){\Hyperf\AsyncQueue\Driver\DriverFactory::make('default')->push(new\App\Job\ReconciliationJob($date,$channel));}})->setMemo('每日支付对账'),],];---6. HTTP 接口 app/Controller/ReconciliationController.php<?php declare(strict_types=1);namespace App\Controller;use App\Service\ReconciliationService;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\GetMapping;use Hyperf\HttpServer\Contract\RequestInterface;#[Controller(prefix: '/reconciliation')]class ReconciliationController{publicfunction__construct(private ReconciliationService$service){}// GET /reconciliation/daily?date=2026-04-21&channel=alipay#[GetMapping(path: 'daily')]publicfunctiondaily(RequestInterface$request): array{$date=$request->input('date', date('Y-m-d', strtotime('-1 day')));$channel=$request->input('channel','alipay');$result=match($channel){'alipay'=>$this->service->reconcileAlipay($date),'wechat'=>$this->service->reconcileWechat($date), default=>['error'=>'invalid channel'],};return$result;}// GET /reconciliation/query?out_trade_no=xxx&channel=alipay#[GetMapping(path: 'query')]publicfunctionquery(RequestInterface$request): array{return$this->service->queryOrder($request->input('out_trade_no'),$request->input('channel','alipay'));}}--- 核心流程说明 每日凌晨2点 ↓ Crontab 触发 → 推入异步队列 ↓ ReconciliationJob::handle()↓ 调用渠道 API 下载账单 CSV ↓ 解析 CSV → 与本地 orders 表比对 ↓ 输出四类结果: ✅ matched — 正常 ⚠️only_in_bill — 漏单(渠道收款但本地无记录) ⚠️only_in_local — 幽灵单(本地有但渠道无) ⚠️amount_diff — 金额不一致 ↓ 有差异 → 告警通知 --- 核心库是 https://github.com/yansongda/hyperf-pay,它封装了支付宝 alipay.data.dataservice.bill.downloadurl.query 和微信 v3/bill/tradebill 接口,download()方法直接返回账单流,query()方法做单笔实时核对。定时任务 + 异步队列保证对账不阻塞主流程。
http://www.jsqmd.com/news/688300/

相关文章:

  • 如何永久保存你的微信聊天记录?WeChatMsg开源工具终极指南
  • 不吹不黑,这款AI驱动的开源Wiki,解决了我们团队90%的文档痛点
  • 别再被PyTorch的F.cosine_similarity搞晕了!一个dim参数详解,附两两相似度计算实战
  • 终极指南:ViPER4Windows修复工具在Windows 10/11的完美解决方案
  • 【FDA认证级容器性能白皮书】:基于27.0.3+Linux 6.8内核的DICOM微服务吞吐量压测极限突破报告
  • 永磁同步电机滑模控制技术解析与应用实践
  • 如何免费在线制作专业PPT:PPTist开源工具完全指南
  • 别再用卖家例程了!手把手教你从零配置STM32F103驱动ST7789V2 TFT屏(附DMA加速技巧)
  • 2026年第一季度高端耳机精选:兼顾音质与体验,这5款值得留意 - 见闻解构
  • Java的java.util.HexFormat格式兼容性与旧版代码迁移在系统演进中
  • 北京九鼎众合餐饮管理:专业的北京盒饭配送选哪家 - LYL仔仔
  • 终极指南:如何用Jellyfin Kodi插件打造无缝家庭媒体中心
  • GetQzonehistory完整教程:3步永久备份你的QQ空间青春记忆
  • uniapp结合ucharts:实现Y轴刻度与标签的深度自定义实践
  • Hyperf对接风控
  • Vivado工程从‘红叉’到‘绿勾’:一次搞定XADC与DDR3核冲突的实战记录
  • 从‘恶作剧’到‘供应链攻击’:手把手教你用Node.js沙盒和ESLint插件检测Evil.js这类依赖包
  • 终极指南:3步让你的Windows电脑免费接收iPhone AirPlay 2投屏
  • 抖音无水印下载终极指南:3步搞定高清视频批量下载
  • ESXi 8.0 网络丢包排查实战全攻略
  • 给LoongArch CPU新手:手把手教你读懂20条指令的Verilog数据通路(附关键信号解析)
  • NEAT算法实战:训练AI玩《刺猬索尼克》
  • Windows驱动开发避坑:手把手教你用WFP实现网站访问限制(附完整代码)
  • Hyperf对接SCADA
  • 2022年MLOps赞助商技术突破与行业贡献解析
  • 如何高效解决跨平台音频格式兼容问题:专业qmc-decoder解密方案
  • 小目标检测效果差?试试Deformable DETR的多尺度注意力机制(原理+代码解读)
  • Zotero引用格式(Xie et al 2021)如何变成可点击的超链接?我的Word宏配置踩坑实录
  • 告别SD卡:全志V3s用16MB NOR Flash打造极简嵌入式Linux系统
  • 别再傻傻用软件AES了!手把手教你用STM32硬件AES加速物联网数据传输(附CubeMX配置)