微信小程序支付V3接口在ThinkPHP6中的优雅封装:从散装代码到可复用支付服务
ThinkPHP6中微信小程序支付V3接口的工程化实践微信支付作为小程序生态中最核心的商业化能力之一其技术实现的质量直接影响着用户体验和系统稳定性。许多开发团队在初期快速实现支付功能后往往会面临代码重复、维护困难、扩展性差等问题。本文将分享如何基于ThinkPHP6框架将支付功能从简单的功能实现升级为可复用、易维护的支付服务。1. 支付模块的架构设计思考支付功能看似简单实则涉及订单系统、支付网关、回调处理、对账等多个环节。一个典型的支付流程至少包含创建订单、调用支付接口、处理支付结果、异步通知处理等步骤。将这些逻辑全部堆砌在控制器中虽然能快速实现功能但会带来诸多问题代码重复多个支付场景需要复制粘贴相似代码维护困难支付逻辑变更需要在多处修改扩展性差新增支付方式或渠道需要重构大量代码测试困难支付逻辑与其他业务逻辑耦合难以单独测试理想的支付模块应具备以下特性配置化支付参数如商户号、证书等应集中管理便于不同环境切换标准化提供统一的接口定义隐藏底层实现细节可复用同一套支付逻辑可在不同业务场景中使用可扩展支持多种支付方式JSAPI、APP、H5等的灵活接入可测试支付核心逻辑可独立于业务进行测试2. 支付服务的分层设计基于上述思考我们可以将支付功能抽象为三层结构2.1 配置层支付配置应当集中管理避免硬编码在业务逻辑中。ThinkPHP6的配置系统非常适合这一需求// config/wechatpay.php return [ default mini_program, channels [ mini_program [ app_id env(WECHAT_MINI_APPID), mch_id env(WECHAT_MCH_ID), api_key env(WECHAT_API_KEY), cert_path root_path(cert/apiclient_cert.pem), key_path root_path(cert/apiclient_key.pem), notify_url env(APP_URL)./api/payment/notify, sign_type RSA, version v3 ], // 可扩展其他支付渠道配置 ] ];2.2 服务层服务层是支付功能的核心负责处理签名生成、API请求、结果解析等通用逻辑。我们可以创建一个PaymentService类namespace app\service; use think\facade\Config; use think\facade\Log; class PaymentService { protected $config; public function __construct($channel null) { $this-config Config::get(wechatpay); $this-channel $channel ?? $this-config[default]; } /** * JSAPI支付下单 */ public function jsapiPay(array $orderData) { $url https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi; $params [ appid $this-getConfig(app_id), mchid $this-getConfig(mch_id), description $orderData[description], out_trade_no $orderData[out_trade_no], notify_url $this-getConfig(notify_url), amount [ total $orderData[amount], currency CNY ], payer [ openid $orderData[openid] ] ]; $response $this-request(POST, $url, $params); if (!isset($response[prepay_id])) { throw new \Exception(支付下单失败: .json_encode($response)); } return $this-buildPaymentConfig($response[prepay_id]); } /** * 构建小程序支付配置 */ protected function buildPaymentConfig($prepayId) { $timestamp time(); $nonceStr uniqid(); return [ appId $this-getConfig(app_id), timeStamp (string)$timestamp, nonceStr $nonceStr, package prepay_id.$prepayId, signType RSA, paySign $this-generatePaySign([ appId $this-getConfig(app_id), timeStamp $timestamp, nonceStr $nonceStr, package prepay_id.$prepayId ]) ]; } /** * 发送HTTP请求 */ protected function request($method, $url, $data []) { $headers [ Content-Type: application/json, Accept: application/json, User-Agent: ThinkPHP6-Payment-Service, Authorization: .$this-buildAuthorization($method, $url, $data) ]; $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); if ($method POST) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response curl_exec($ch); $error curl_error($ch); curl_close($ch); if ($error) { throw new \Exception(支付请求失败: .$error); } return json_decode($response, true); } /** * 生成支付签名 */ protected function generatePaySign($params) { $message implode(\n, [ $params[appId], $params[timeStamp], $params[nonceStr], $params[package] ]); openssl_sign($message, $signature, $this-getPrivateKey(), OPENSSL_ALGO_SHA256); return base64_encode($signature); } /** * 构建Authorization头 */ protected function buildAuthorization($method, $url, $data) { $timestamp time(); $nonceStr uniqid(); $urlParts parse_url($url); $message implode(\n, [ $method, $urlParts[path], $timestamp, $nonceStr, json_encode($data), ]); openssl_sign($message, $signature, $this-getPrivateKey(), OPENSSL_ALGO_SHA256); $signature base64_encode($signature); return sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,timestamp%d,serial_no%s,signature%s, $this-getConfig(mch_id), $nonceStr, $timestamp, $this-getCertificateSerialNo(), $signature ); } /** * 获取私钥 */ protected function getPrivateKey() { $key file_get_contents($this-getConfig(key_path)); return openssl_pkey_get_private($key); } /** * 获取证书序列号 */ protected function getCertificateSerialNo() { $cert file_get_contents($this-getConfig(cert_path)); $info openssl_x509_parse($cert); return $info[serialNumberHex] ?? ; } /** * 获取配置项 */ protected function getConfig($key) { return $this-config[channels][$this-channel][$key] ?? null; } }2.3 业务层业务层负责处理具体的支付场景如商品购买、会员充值等。通过服务层的封装业务层代码变得非常简洁namespace app\controller; use app\service\PaymentService; use think\facade\Db; class OrderController { public function create() { Db::startTrans(); try { // 1. 创建业务订单 $order $this-createOrder(); // 2. 调用支付服务 $paymentService new PaymentService(); $paymentConfig $paymentService-jsapiPay([ out_trade_no $order-order_no, description $order-subject, amount $order-amount, openid $this-request-param(openid) ]); Db::commit(); return json([ code 1, data $paymentConfig ]); } catch (\Exception $e) { Db::rollback(); return json([ code 0, msg $e-getMessage() ]); } } protected function createOrder() { // 订单创建逻辑 } }3. 支付回调的优雅处理支付回调是支付流程中另一个关键环节需要特别注意安全性和幂等性处理。我们可以创建一个专门的控制器来处理回调namespace app\controller; use app\service\PaymentService; use think\facade\Log; class PaymentController { public function notify() { $rawData file_get_contents(php://input); $headers getallheaders(); try { $paymentService new PaymentService(); $result $paymentService-verifyNotify($rawData, $headers); // 处理支付成功逻辑 $this-handlePaymentSuccess($result); return response(xmlreturn_code![CDATA[SUCCESS]]/return_code/xml) -contentType(application/xml); } catch (\Exception $e) { Log::error(支付回调处理失败: .$e-getMessage()); return response(xmlreturn_code![CDATA[FAIL]]/return_code/xml) -contentType(application/xml); } } protected function handlePaymentSuccess($result) { // 根据$result中的out_trade_no更新订单状态 // 注意处理幂等性避免重复处理 } }在PaymentService中添加回调验证方法public function verifyNotify($rawData, $headers) { $signature $headers[Wechatpay-Signature] ?? ; $timestamp $headers[Wechatpay-Timestamp] ?? ; $nonce $headers[Wechatpay-Nonce] ?? ; $serialNo $headers[Wechatpay-Serial] ?? ; if (empty($signature) || empty($timestamp) || empty($nonce) || empty($serialNo)) { throw new \Exception(回调头部信息不完整); } $message $timestamp\n$nonce\n$rawData\n; $signature base64_decode($signature); $cert $this-getWechatpayCertificate($serialNo); $publicKey openssl_pkey_get_public($cert); $verify openssl_verify($message, $signature, $publicKey, sha256WithRSAEncryption); if ($verify ! 1) { throw new \Exception(签名验证失败); } $result json_decode($rawData, true); if (json_last_error() ! JSON_ERROR_NONE) { throw new \Exception(回调数据解析失败); } return $result; } protected function getWechatpayCertificate($serialNo) { // 实现从微信支付平台获取证书的逻辑 // 建议缓存证书避免频繁请求 }4. 支付服务的进阶优化基础支付服务实现后我们可以进一步优化提升系统的健壮性和可维护性4.1 引入依赖注入ThinkPHP6支持依赖注入我们可以将支付服务注册到容器中// 服务提供者 namespace app\provider; use app\service\PaymentService; use think\Service; class PaymentServiceProvider extends Service { public function register() { $this-app-bind(payment, function() { return new PaymentService(); }); } }然后在控制器中通过依赖注入使用public function create(\app\service\PaymentService $paymentService) { // 直接使用$paymentService }4.2 实现支付策略模式如果需要支持多种支付方式如微信支付、支付宝等可以使用策略模式interface PaymentStrategy { public function pay(array $orderData); public function verifyNotify($rawData, $headers); } class WechatPayment implements PaymentStrategy { // 实现微信支付逻辑 } class Alipayment implements PaymentStrategy { // 实现支付宝支付逻辑 } class PaymentContext { protected $strategy; public function __construct(PaymentStrategy $strategy) { $this-strategy $strategy; } public function pay(array $orderData) { return $this-strategy-pay($orderData); } public function verifyNotify($rawData, $headers) { return $this-strategy-verifyNotify($rawData, $headers); } }4.3 添加日志和监控支付流程中的关键操作应当记录日志便于问题排查public function jsapiPay(array $orderData) { try { Log::info(支付下单开始, [order $orderData]); $result $this-doPay($orderData); Log::info(支付下单成功, [result $result]); return $result; } catch (\Exception $e) { Log::error(支付下单失败, [ error $e-getMessage(), trace $e-getTraceAsString() ]); throw $e; } }4.4 实现支付结果查询支付结果查询是支付系统的重要补充功能public function queryPayment($outTradeNo) { $url sprintf( https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid%s, $outTradeNo, $this-getConfig(mch_id) ); $response $this-request(GET, $url); if (!isset($response[trade_state])) { throw new \Exception(支付查询失败: .json_encode($response)); } return [ out_trade_no $outTradeNo, trade_state $response[trade_state], transaction_id $response[transaction_id] ?? , success_time $response[success_time] ?? ]; }4.5 处理支付异常支付过程中可能遇到各种异常情况应当分类处理class PaymentException extends \Exception { const INVALID_CONFIG 1001; const SIGN_FAILED 1002; const REQUEST_FAILED 1003; const BUSINESS_ERROR 1004; public function __construct($message, $code 0, \Throwable $previous null) { parent::__construct($message, $code, $previous); } public function render() { switch ($this-code) { case self::INVALID_CONFIG: return 支付配置错误; case self::SIGN_FAILED: return 签名验证失败; case self::REQUEST_FAILED: return 支付请求失败; default: return $this-message; } } }在服务中使用自定义异常if (empty($this-getConfig(mch_id))) { throw new PaymentException(商户号未配置, PaymentException::INVALID_CONFIG); }