mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-22 10:04:27 +08:00
1. 完善易支付API调用全流程
2. 确定支付插件继承基础类和接口规范 3. 引入Yansongda\Pay支付快捷工具 4. 重新整理代码和功能结构
This commit is contained in:
@@ -1,229 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\contracts;
|
||||
|
||||
use app\common\contracts\PayPluginInterface;
|
||||
use support\Log;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 支付插件抽象基类
|
||||
*
|
||||
* 提供通用的环境检测、HTTP请求、日志记录等功能
|
||||
*/
|
||||
abstract class AbstractPayPlugin implements PayPluginInterface
|
||||
{
|
||||
/**
|
||||
* 环境常量
|
||||
*/
|
||||
const ENV_PC = 'PC';
|
||||
const ENV_H5 = 'H5';
|
||||
const ENV_WECHAT = 'WECHAT';
|
||||
const ENV_ALIPAY_CLIENT = 'ALIPAY_CLIENT';
|
||||
|
||||
/**
|
||||
* 当前支付方式
|
||||
*/
|
||||
protected string $currentMethod = '';
|
||||
|
||||
/**
|
||||
* 当前通道配置
|
||||
*/
|
||||
protected array $currentConfig = [];
|
||||
|
||||
/**
|
||||
* 初始化插件(切换到指定支付方式)
|
||||
*/
|
||||
public function init(string $methodCode, array $channelConfig): void
|
||||
{
|
||||
if (!in_array($methodCode, static::getSupportedMethods())) {
|
||||
throw new \RuntimeException("插件不支持支付方式:{$methodCode}");
|
||||
}
|
||||
$this->currentMethod = $methodCode;
|
||||
$this->currentConfig = $channelConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测请求环境
|
||||
*
|
||||
* @param Request $request
|
||||
* @return string 环境代码(PC/H5/WECHAT/ALIPAY_CLIENT)
|
||||
*/
|
||||
protected function detectEnvironment(Request $request): string
|
||||
{
|
||||
$ua = strtolower($request->header('User-Agent', ''));
|
||||
|
||||
// 支付宝客户端
|
||||
if (strpos($ua, 'alipayclient') !== false) {
|
||||
return self::ENV_ALIPAY_CLIENT;
|
||||
}
|
||||
|
||||
// 微信内浏览器
|
||||
if (strpos($ua, 'micromessenger') !== false) {
|
||||
return self::ENV_WECHAT;
|
||||
}
|
||||
|
||||
// 移动设备
|
||||
$mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
|
||||
foreach ($mobileKeywords as $keyword) {
|
||||
if (strpos($ua, $keyword) !== false) {
|
||||
return self::ENV_H5;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认PC
|
||||
return self::ENV_PC;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据环境选择产品
|
||||
*
|
||||
* @param array $enabledProducts 已启用的产品列表
|
||||
* @param string $env 环境代码
|
||||
* @param array $allProducts 所有可用产品(产品代码 => 产品名称)
|
||||
* @return string|null 选择的产品代码,如果没有匹配则返回null
|
||||
*/
|
||||
protected function selectProductByEnv(array $enabledProducts, string $env, array $allProducts): ?string
|
||||
{
|
||||
// 环境到产品的映射规则(子类可以重写此方法实现自定义逻辑)
|
||||
$envProductMap = [
|
||||
self::ENV_PC => ['pc', 'web', 'wap'],
|
||||
self::ENV_H5 => ['h5', 'wap', 'mobile'],
|
||||
self::ENV_WECHAT => ['jsapi', 'wechat', 'h5'],
|
||||
self::ENV_ALIPAY_CLIENT => ['app', 'alipay', 'h5'],
|
||||
];
|
||||
|
||||
$candidates = $envProductMap[$env] ?? [];
|
||||
|
||||
// 优先匹配已启用的产品
|
||||
foreach ($candidates as $candidate) {
|
||||
if (in_array($candidate, $enabledProducts)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配,返回第一个已启用的产品
|
||||
if (!empty($enabledProducts)) {
|
||||
return $enabledProducts[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST JSON请求
|
||||
*
|
||||
* @param string $url 请求URL
|
||||
* @param array $data 请求数据
|
||||
* @param array $headers 额外请求头
|
||||
* @return array 响应数据(已解析JSON)
|
||||
*/
|
||||
protected function httpPostJson(string $url, array $data, array $headers = []): array
|
||||
{
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->buildHeaders($headers));
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new \RuntimeException("HTTP请求失败:{$error}");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new \RuntimeException("HTTP请求失败,状态码:{$httpCode}");
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException("JSON解析失败:" . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST Form请求
|
||||
*
|
||||
* @param string $url 请求URL
|
||||
* @param array $data 请求数据
|
||||
* @param array $headers 额外请求头
|
||||
* @return string 响应内容
|
||||
*/
|
||||
protected function httpPostForm(string $url, array $data, array $headers = []): string
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->buildHeaders($headers));
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new \RuntimeException("HTTP请求失败:{$error}");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new \RuntimeException("HTTP请求失败,状态码:{$httpCode}");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建HTTP请求头数组
|
||||
*
|
||||
* @param array $headers 请求头数组
|
||||
* @return array
|
||||
*/
|
||||
private function buildHeaders(array $headers): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
$result[] = "{$key}: {$value}";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*
|
||||
* @param string $action 操作名称
|
||||
* @param array $data 请求数据
|
||||
* @param mixed $response 响应数据
|
||||
* @return void
|
||||
*/
|
||||
protected function logRequest(string $action, array $data, $response = null): void
|
||||
{
|
||||
$logData = [
|
||||
'plugin' => static::getCode(),
|
||||
'method' => $this->currentMethod,
|
||||
'action' => $action,
|
||||
'request' => $data,
|
||||
'response' => $response,
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
Log::debug('支付插件请求', $logData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\contracts;
|
||||
|
||||
/**
|
||||
* 支付插件接口
|
||||
*
|
||||
* 所有支付插件必须实现此接口
|
||||
* 支付插件“基础契约”接口
|
||||
*
|
||||
* 职责边界:
|
||||
* - `PayPluginInterface`:插件生命周期 + 元信息(后台可展示/可配置/可路由)。
|
||||
* - `PaymentInterface`:支付动作能力(下单/查询/关单/退款/回调)。
|
||||
*
|
||||
* 约定:
|
||||
* - `init()` 会在每次发起支付/退款等动作前由服务层调用,用于注入该通道的 `config_json`。
|
||||
* - 元信息方法应为“纯读取”,不要依赖外部状态或数据库。
|
||||
*/
|
||||
interface PayPluginInterface
|
||||
{
|
||||
/**
|
||||
* 获取插件代码(唯一标识)
|
||||
*
|
||||
* @return string
|
||||
* 初始化插件(注入通道配置)
|
||||
*
|
||||
* 典型来源:`ma_pay_channel.config_json`(以及服务层合并的 enabled_products 等)。
|
||||
* 插件应在这里完成:缓存配置、初始化 SDK/HTTP 客户端等。
|
||||
*
|
||||
* @param array<string, mixed> $channelConfig
|
||||
*/
|
||||
public static function getCode(): string;
|
||||
|
||||
public function init(array $channelConfig): void;
|
||||
|
||||
/** 插件代码(与 ma_pay_plugin.plugin_code 对应) */
|
||||
public function getCode(): string;
|
||||
|
||||
/** 插件名称(用于后台展示) */
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* 获取插件名称
|
||||
*
|
||||
* @return string
|
||||
* 插件声明支持的支付方式编码
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getName(): string;
|
||||
|
||||
public function getEnabledPayTypes(): array;
|
||||
|
||||
/**
|
||||
* 获取插件支持的支付方式列表
|
||||
*
|
||||
* @return array<string> 支付方式代码数组,如 ['alipay', 'wechat']
|
||||
* 插件配置结构(用于后台渲染表单/校验)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getSupportedMethods(): array;
|
||||
|
||||
/**
|
||||
* 获取指定支付方式支持的产品列表
|
||||
*
|
||||
* @param string $methodCode 支付方式代码
|
||||
* @return array<string, string> 产品代码 => 产品名称
|
||||
*/
|
||||
public static function getSupportedProducts(string $methodCode): array;
|
||||
|
||||
/**
|
||||
* 获取指定支付方式的配置表单结构
|
||||
*
|
||||
* @param string $methodCode 支付方式代码
|
||||
* @return array 表单字段定义数组
|
||||
*/
|
||||
public static function getConfigSchema(string $methodCode): array;
|
||||
|
||||
/**
|
||||
* 初始化插件(切换到指定支付方式)
|
||||
*
|
||||
* @param string $methodCode 支付方式代码
|
||||
* @param array $channelConfig 通道配置
|
||||
* @return void
|
||||
*/
|
||||
public function init(string $methodCode, array $channelConfig): void;
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*
|
||||
* @param array $orderData 订单数据
|
||||
* @param array $channelConfig 通道配置
|
||||
* @param string $requestEnv 请求环境(PC/H5/WECHAT/ALIPAY_CLIENT)
|
||||
* @return array 支付结果,包含:
|
||||
* - product_code: 选择的产品代码
|
||||
* - channel_order_no: 渠道订单号(如果有)
|
||||
* - pay_params: 支付参数(根据产品类型不同,结构不同)
|
||||
*/
|
||||
public function unifiedOrder(array $orderData, array $channelConfig, string $requestEnv): array;
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
*
|
||||
* @param array $orderData 订单数据(至少包含 pay_order_id 或 channel_order_no)
|
||||
* @param array $channelConfig 通道配置
|
||||
* @return array 订单状态信息
|
||||
*/
|
||||
public function query(array $orderData, array $channelConfig): array;
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*
|
||||
* @param array $refundData 退款数据
|
||||
* @param array $channelConfig 通道配置
|
||||
* @return array 退款结果
|
||||
*/
|
||||
public function refund(array $refundData, array $channelConfig): array;
|
||||
|
||||
/**
|
||||
* 解析回调通知
|
||||
*
|
||||
* @param array $requestData 回调请求数据
|
||||
* @param array $channelConfig 通道配置
|
||||
* @return array 解析结果,包含:
|
||||
* - status: 订单状态(SUCCESS/FAIL/PENDING)
|
||||
* - pay_order_id: 系统订单号
|
||||
* - channel_trade_no: 渠道交易号
|
||||
* - amount: 支付金额
|
||||
* - pay_time: 支付时间
|
||||
*/
|
||||
public function parseNotify(array $requestData, array $channelConfig): array;
|
||||
public function getConfigSchema(): array;
|
||||
}
|
||||
|
||||
|
||||
84
app/common/contracts/PaymentInterface.php
Normal file
84
app/common/contracts/PaymentInterface.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\contracts;
|
||||
|
||||
use app\exceptions\PaymentException;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 支付插件接口
|
||||
*
|
||||
* 所有支付插件必须实现此接口,用于统一下单、订单查询、关闭、退款及回调通知等核心能力。
|
||||
* 建议继承 BasePayment 获得 HTTP 请求等通用能力,再实现本接口。
|
||||
*
|
||||
* 异常约定:实现类在业务失败时应抛出 PaymentException,便于统一处理和返回。
|
||||
*/
|
||||
interface PaymentInterface
|
||||
{
|
||||
// ==================== 订单操作 ====================
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*
|
||||
* @param array<string, mixed> $order 订单数据,通常包含:
|
||||
* - order_id: 系统订单号
|
||||
* - mch_no: 商户号
|
||||
* - amount: 金额(元)
|
||||
* - subject: 商品标题
|
||||
* - body: 商品描述
|
||||
* @return array<string, mixed> 支付参数,需包含 pay_params、chan_order_no、chan_trade_no
|
||||
* @throws PaymentException 下单失败、渠道异常、参数错误等
|
||||
*/
|
||||
public function pay(array $order): array;
|
||||
|
||||
/**
|
||||
* 查询订单状态
|
||||
*
|
||||
* @param array<string, mixed> $order 订单数据(至少含 order_id、chan_order_no)
|
||||
* @return array<string, mixed> 订单状态信息,通常包含:
|
||||
* - status: 订单状态
|
||||
* - chan_trade_no: 渠道交易号
|
||||
* - pay_amount: 实付金额
|
||||
* @throws PaymentException 查询失败、渠道异常等
|
||||
*/
|
||||
public function query(array $order): array;
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*
|
||||
* @param array<string, mixed> $order 订单数据(至少含 order_id、chan_order_no)
|
||||
* @return array<string, mixed> 关闭结果,通常包含 success、msg
|
||||
* @throws PaymentException 关闭失败、渠道异常等
|
||||
*/
|
||||
public function close(array $order): array;
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
*
|
||||
* @param array<string, mixed> $order 退款数据,通常包含:
|
||||
* - order_id: 原订单号
|
||||
* - chan_order_no: 渠道订单号
|
||||
* - refund_amount: 退款金额
|
||||
* - refund_no: 退款单号
|
||||
* @return array<string, mixed> 退款结果,通常包含 success、chan_refund_no、msg
|
||||
* @throws PaymentException 退款失败、渠道异常等
|
||||
*/
|
||||
public function refund(array $order): array;
|
||||
|
||||
// ==================== 异步通知 ====================
|
||||
|
||||
/**
|
||||
* 解析并验证支付回调通知
|
||||
*
|
||||
* @param Request $request 支付渠道的异步通知请求(GET/POST 参数)
|
||||
* @return array<string, mixed> 解析结果,通常包含:
|
||||
* - status: 支付状态
|
||||
* - pay_order_id: 系统订单号
|
||||
* - chan_trade_no: 渠道交易号
|
||||
* - amount: 支付金额
|
||||
* @throws PaymentException 验签失败、数据异常等
|
||||
*/
|
||||
public function notify(Request $request): array;
|
||||
}
|
||||
Reference in New Issue
Block a user