mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-03-24 20:04:32 +08:00
1. 完善易支付API调用全流程
2. 确定支付插件继承基础类和接口规范 3. 引入Yansongda\Pay支付快捷工具 4. 重新整理代码和功能结构
This commit is contained in:
167
app/common/base/BasePayment.php
Normal file
167
app/common/base/BasePayment.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use app\common\contracts\PayPluginInterface;
|
||||
use app\exceptions\PaymentException;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use support\Log;
|
||||
|
||||
/**
|
||||
* 支付插件基类(建议所有插件继承)
|
||||
*
|
||||
* 目标:把“插件共性”集中在这里,具体渠道差异留给子类实现 `PaymentInterface`。
|
||||
*
|
||||
* 生命周期:
|
||||
* - 服务层会在每次动作前调用 `init($channelConfig)` 注入该通道配置。
|
||||
* - 子类可在 `init()` 中配置第三方 SDK(例如 yansongda/pay)或读取必填参数。
|
||||
*
|
||||
* 约定:
|
||||
* - 这里的 `$channelConfig` 来源通常是 `ma_pay_channel.config_json`,属于“通道级配置”。
|
||||
* - 业务级入参(如订单号、金额、回调地址等)不要混进 `$channelConfig`,应从 `pay()` 的 `$order` 参数获取。
|
||||
*/
|
||||
abstract class BasePayment implements PayPluginInterface
|
||||
{
|
||||
/**
|
||||
* 插件元信息(子类必须覆盖)
|
||||
*
|
||||
* 常用字段:
|
||||
* - code/name:后台展示与标识
|
||||
* - pay_types:声明支持的支付方式编码(如 alipay/wechat)
|
||||
* - config_schema:后台配置表单结构(fields...)
|
||||
* - 包含:code, name, author, link, pay_types, transfer_types, config_schema 等
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $paymentInfo = [];
|
||||
|
||||
/**
|
||||
* 通道配置(由 init 注入)
|
||||
*
|
||||
* 建议是“纯配置”:商户号/密钥/网关地址/产品开关等。
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $channelConfig = [];
|
||||
|
||||
/** HTTP 请求客户端(GuzzleHttp) */
|
||||
private ?Client $httpClient = null;
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
/**
|
||||
* 初始化插件,加载通道配置并创建 HTTP 客户端
|
||||
*
|
||||
* @param array<string, mixed> $channelConfig 通道配置(商户号、密钥等)
|
||||
* @return void
|
||||
*/
|
||||
public function init(array $channelConfig): void
|
||||
{
|
||||
$this->channelConfig = $channelConfig;
|
||||
$this->httpClient = new Client([
|
||||
'timeout' => 10,
|
||||
'connect_timeout' => 10,
|
||||
'verify' => true,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通道配置项
|
||||
*
|
||||
* @param string $key 配置键
|
||||
* @param mixed $default 默认值(键不存在时返回)
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getConfig(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->channelConfig[$key] ?? $default;
|
||||
}
|
||||
|
||||
// ==================== 插件元信息 ====================
|
||||
|
||||
/** 获取插件代码(唯一标识) */
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->paymentInfo['code'] ?? '';
|
||||
}
|
||||
|
||||
/** 获取插件名称 */
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->paymentInfo['name'] ?? '';
|
||||
}
|
||||
|
||||
/** 获取作者名称 */
|
||||
public function getAuthorName(): string
|
||||
{
|
||||
return $this->paymentInfo['author'] ?? '';
|
||||
}
|
||||
|
||||
/** 获取作者链接 */
|
||||
public function getAuthorLink(): string
|
||||
{
|
||||
return $this->paymentInfo['link'] ?? '';
|
||||
}
|
||||
|
||||
// ==================== 能力声明 ====================
|
||||
|
||||
/**
|
||||
* 获取插件支持的支付方式列表
|
||||
*
|
||||
* @return array<string> 支付方式代码数组,如 ['alipay', 'wechat']
|
||||
*/
|
||||
public function getEnabledPayTypes(): array
|
||||
{
|
||||
return $this->paymentInfo['pay_types'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件支持的转账方式列表
|
||||
*
|
||||
* @return array<string> 转账方式代码数组
|
||||
*/
|
||||
public function getEnabledTransferTypes(): array
|
||||
{
|
||||
return $this->paymentInfo['transfer_types'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件配置表单结构(用于后台配置界面)
|
||||
*
|
||||
* @return array<string, mixed> 表单字段定义数组
|
||||
*/
|
||||
public function getConfigSchema(): array
|
||||
{
|
||||
return $this->paymentInfo['config_schema'] ?? [];
|
||||
}
|
||||
|
||||
// ==================== HTTP 请求 ====================
|
||||
|
||||
/**
|
||||
* 统一 HTTP 请求(对外调用支付渠道 API)
|
||||
*
|
||||
* @param string $method 请求方法(GET/POST/PUT/DELETE 等)
|
||||
* @param string $url 请求地址
|
||||
* @param array<string, mixed> $options Guzzle 请求选项(headers、json、form_params 等)
|
||||
* @return ResponseInterface
|
||||
* @throws PaymentException 未调用 init() 或渠道请求失败时
|
||||
*/
|
||||
protected function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
if ($this->httpClient === null) {
|
||||
throw new PaymentException('支付插件未初始化,请先调用 init()');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->httpClient->request($method, $url, $options);
|
||||
} catch (GuzzleException $e) {
|
||||
Log::error(sprintf('[BasePayment] HTTP 请求失败: %s %s, error=%s', $method, $url, $e->getMessage()));
|
||||
throw new PaymentException('渠道请求失败:' . $e->getMessage(), 402, ['method' => $method, 'url' => $url]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
270
app/common/payment/AlipayPayment.php
Normal file
270
app/common/payment/AlipayPayment.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\payment;
|
||||
|
||||
use app\common\base\BasePayment;
|
||||
use app\common\contracts\PaymentInterface;
|
||||
use app\exceptions\PaymentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use support\Request;
|
||||
use Yansongda\Pay\Pay;
|
||||
use Yansongda\Supports\Collection;
|
||||
|
||||
/**
|
||||
* 支付宝支付插件(基于 yansongda/pay ~3.7)
|
||||
*
|
||||
* 支持:web(电脑网站)、h5(手机网站)、scan(扫码)、app(APP 支付)
|
||||
*
|
||||
* 通道配置:app_id, app_secret_cert, app_public_cert_path, alipay_public_cert_path,
|
||||
* alipay_root_cert_path, notify_url, return_url, mode(0正式/1沙箱)
|
||||
*/
|
||||
class AlipayPayment extends BasePayment implements PaymentInterface
|
||||
{
|
||||
protected array $paymentInfo = [
|
||||
'code' => 'alipay',
|
||||
'name' => '支付宝直连',
|
||||
'author' => '',
|
||||
'link' => '',
|
||||
'pay_types' => ['alipay'],
|
||||
'transfer_types' => [],
|
||||
'config_schema' => [
|
||||
'fields' => [
|
||||
['field' => 'app_id', 'label' => '应用ID', 'type' => 'text', 'required' => true],
|
||||
['field' => 'app_secret_cert', 'label' => '应用私钥', 'type' => 'textarea', 'required' => true],
|
||||
['field' => 'app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'text', 'required' => true],
|
||||
['field' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'text', 'required' => true],
|
||||
['field' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'text', 'required' => true],
|
||||
['field' => 'notify_url', 'label' => '异步通知地址', 'type' => 'text', 'required' => true],
|
||||
['field' => 'return_url', 'label' => '同步跳转地址', 'type' => 'text', 'required' => false],
|
||||
['field' => 'mode', 'label' => '环境', 'type' => 'select', 'options' => [['value' => '0', 'label' => '正式'], ['value' => '1', 'label' => '沙箱']]],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
private const PRODUCT_WEB = 'alipay_web';
|
||||
private const PRODUCT_H5 = 'alipay_h5';
|
||||
private const PRODUCT_SCAN = 'alipay_scan';
|
||||
private const PRODUCT_APP = 'alipay_app';
|
||||
|
||||
public function init(array $channelConfig): void
|
||||
{
|
||||
parent::init($channelConfig);
|
||||
Pay::config([
|
||||
'alipay' => [
|
||||
'default' => [
|
||||
'app_id' => $this->getConfig('app_id', ''),
|
||||
'app_secret_cert' => $this->getConfig('app_secret_cert', ''),
|
||||
'app_public_cert_path' => $this->getConfig('app_public_cert_path', ''),
|
||||
'alipay_public_cert_path' => $this->getConfig('alipay_public_cert_path', ''),
|
||||
'alipay_root_cert_path' => $this->getConfig('alipay_root_cert_path', ''),
|
||||
'notify_url' => $this->getConfig('notify_url', ''),
|
||||
'return_url' => $this->getConfig('return_url', ''),
|
||||
'mode' => (int)($this->getConfig('mode', Pay::MODE_NORMAL)),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function chooseProduct(array $order): string
|
||||
{
|
||||
$enabled = $this->channelConfig['enabled_products'] ?? ['alipay_web', 'alipay_h5', 'alipay_scan'];
|
||||
$env = $order['_env'] ?? 'pc';
|
||||
$map = ['pc' => self::PRODUCT_WEB, 'h5' => self::PRODUCT_H5, 'alipay' => self::PRODUCT_APP];
|
||||
$prefer = $map[$env] ?? self::PRODUCT_WEB;
|
||||
return in_array($prefer, $enabled, true) ? $prefer : ($enabled[0] ?? self::PRODUCT_WEB);
|
||||
}
|
||||
|
||||
public function pay(array $order): array
|
||||
{
|
||||
$orderId = $order['order_id'] ?? $order['mch_no'] ?? '';
|
||||
$amount = (float)($order['amount'] ?? 0);
|
||||
$subject = (string)($order['subject'] ?? '');
|
||||
$extra = $order['extra'] ?? [];
|
||||
$returnUrl = $extra['return_url'] ?? $this->getConfig('return_url', '');
|
||||
$notifyUrl = $this->getConfig('notify_url', '');
|
||||
|
||||
$params = [
|
||||
'out_trade_no' => $orderId,
|
||||
'total_amount' => sprintf('%.2f', $amount),
|
||||
'subject' => $subject,
|
||||
];
|
||||
if ($returnUrl !== '') {
|
||||
$params['_return_url'] = $returnUrl;
|
||||
}
|
||||
if ($notifyUrl !== '') {
|
||||
$params['_notify_url'] = $notifyUrl;
|
||||
}
|
||||
|
||||
$product = $this->chooseProduct($order);
|
||||
|
||||
try {
|
||||
return match ($product) {
|
||||
self::PRODUCT_WEB => $this->doWeb($params),
|
||||
self::PRODUCT_H5 => $this->doH5($params),
|
||||
self::PRODUCT_SCAN => $this->doScan($params),
|
||||
self::PRODUCT_APP => $this->doApp($params),
|
||||
default => throw new PaymentException('不支持的支付宝产品:' . $product, 402),
|
||||
};
|
||||
} catch (PaymentException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
throw new PaymentException('支付宝下单失败:' . $e->getMessage(), 402, ['order_id' => $orderId]);
|
||||
}
|
||||
}
|
||||
|
||||
private function doWeb(array $params): array
|
||||
{
|
||||
$response = Pay::alipay()->web($params);
|
||||
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
||||
return [
|
||||
'pay_params' => ['type' => 'form', 'method' => 'POST', 'action' => '', 'html' => $body],
|
||||
'chan_order_no' => $params['out_trade_no'],
|
||||
'chan_trade_no' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function doH5(array $params): array
|
||||
{
|
||||
$returnUrl = $params['_return_url'] ?? $this->getConfig('return_url', '');
|
||||
if ($returnUrl !== '') {
|
||||
$params['quit_url'] = $returnUrl;
|
||||
}
|
||||
$response = Pay::alipay()->h5($params);
|
||||
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
||||
return [
|
||||
'pay_params' => ['type' => 'form', 'method' => 'POST', 'action' => '', 'html' => $body],
|
||||
'chan_order_no' => $params['out_trade_no'],
|
||||
'chan_trade_no' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function doScan(array $params): array
|
||||
{
|
||||
/** @var Collection $result */
|
||||
$result = Pay::alipay()->scan($params);
|
||||
$qrCode = $result->get('qr_code', '');
|
||||
return [
|
||||
'pay_params' => ['type' => 'qrcode', 'qrcode_url' => $qrCode, 'qrcode_data' => $qrCode],
|
||||
'chan_order_no' => $params['out_trade_no'],
|
||||
'chan_trade_no' => $result->get('trade_no', ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function doApp(array $params): array
|
||||
{
|
||||
/** @var Collection $result */
|
||||
$result = Pay::alipay()->app($params);
|
||||
$orderStr = $result->get('order_string', '');
|
||||
return [
|
||||
'pay_params' => ['type' => 'jsapi', 'order_str' => $orderStr, 'urlscheme' => $orderStr],
|
||||
'chan_order_no' => $params['out_trade_no'],
|
||||
'chan_trade_no' => $result->get('trade_no', ''),
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $order): array
|
||||
{
|
||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
||||
|
||||
try {
|
||||
/** @var Collection $result */
|
||||
$result = Pay::alipay()->query(['out_trade_no' => $outTradeNo]);
|
||||
$tradeStatus = $result->get('trade_status', '');
|
||||
$tradeNo = $result->get('trade_no', '');
|
||||
$totalAmount = (float)$result->get('total_amount', 0);
|
||||
$status = in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true) ? 'success' : $tradeStatus;
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'chan_trade_no' => $tradeNo,
|
||||
'pay_amount' => $totalAmount,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
throw new PaymentException('支付宝查询失败:' . $e->getMessage(), 402);
|
||||
}
|
||||
}
|
||||
|
||||
public function close(array $order): array
|
||||
{
|
||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
||||
|
||||
try {
|
||||
Pay::alipay()->close(['out_trade_no' => $outTradeNo]);
|
||||
return ['success' => true, 'msg' => '关闭成功'];
|
||||
} catch (\Throwable $e) {
|
||||
throw new PaymentException('支付宝关单失败:' . $e->getMessage(), 402);
|
||||
}
|
||||
}
|
||||
|
||||
public function refund(array $order): array
|
||||
{
|
||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
||||
$refundAmount = (float)($order['refund_amount'] ?? 0);
|
||||
$refundNo = $order['refund_no'] ?? $order['order_id'] . '_' . time();
|
||||
$refundReason = (string)($order['refund_reason'] ?? '');
|
||||
|
||||
if ($outTradeNo === '' || $refundAmount <= 0) {
|
||||
throw new PaymentException('退款参数不完整', 402);
|
||||
}
|
||||
|
||||
$params = [
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'refund_amount' => sprintf('%.2f', $refundAmount),
|
||||
'out_request_no' => $refundNo,
|
||||
];
|
||||
if ($refundReason !== '') {
|
||||
$params['refund_reason'] = $refundReason;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var Collection $result */
|
||||
$result = Pay::alipay()->refund($params);
|
||||
$code = $result->get('code');
|
||||
$subMsg = $result->get('sub_msg', '');
|
||||
|
||||
if ($code === '10000' || $code === 10000) {
|
||||
return [
|
||||
'success' => true,
|
||||
'chan_refund_no'=> $result->get('trade_no', $refundNo),
|
||||
'msg' => '退款成功',
|
||||
];
|
||||
}
|
||||
throw new PaymentException($subMsg ?: '退款失败', 402);
|
||||
} catch (PaymentException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
throw new PaymentException('支付宝退款失败:' . $e->getMessage(), 402);
|
||||
}
|
||||
}
|
||||
|
||||
public function notify(Request $request): array
|
||||
{
|
||||
$params = array_merge($request->get(), $request->post());
|
||||
|
||||
try {
|
||||
/** @var Collection $result */
|
||||
$result = Pay::alipay()->callback($params);
|
||||
$tradeStatus = $result->get('trade_status', '');
|
||||
$outTradeNo = $result->get('out_trade_no', '');
|
||||
$tradeNo = $result->get('trade_no', '');
|
||||
$totalAmount = (float)$result->get('total_amount', 0);
|
||||
|
||||
if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
|
||||
throw new PaymentException('回调状态异常:' . $tradeStatus, 402);
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'pay_order_id' => $outTradeNo,
|
||||
'chan_trade_no'=> $tradeNo,
|
||||
'amount' => $totalAmount,
|
||||
];
|
||||
} catch (PaymentException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
throw new PaymentException('支付宝回调验签失败:' . $e->getMessage(), 402);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\payment;
|
||||
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
use app\common\base\BasePayment;
|
||||
use app\common\contracts\PaymentInterface;
|
||||
use app\exceptions\PaymentException;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 拉卡拉支付插件示例
|
||||
* 拉卡拉支付插件(最小可用示例)
|
||||
*
|
||||
* 支持多个支付方式:alipay、wechat、unionpay
|
||||
* 目的:先把 API 下单链路跑通,让现有 DB 配置(ma_pay_plugin=lakala)可用。
|
||||
* 后续你可以把这里替换为真实拉卡拉对接逻辑(HTTP 下单、验签回调等)。
|
||||
*/
|
||||
class LakalaPayment extends AbstractPayPlugin
|
||||
class LakalaPayment extends BasePayment implements PaymentInterface
|
||||
{
|
||||
public static function getCode(): string
|
||||
{
|
||||
return 'lakala';
|
||||
}
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return '拉卡拉支付';
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持多个支付方式
|
||||
*/
|
||||
public static function getSupportedMethods(): array
|
||||
{
|
||||
return ['alipay', 'wechat', 'unionpay'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付方式返回支持的产品
|
||||
*/
|
||||
public static function getSupportedProducts(string $methodCode): array
|
||||
{
|
||||
return match ($methodCode) {
|
||||
'alipay' => [
|
||||
['code' => 'alipay_h5', 'name' => '支付宝H5', 'device_type' => 'H5'],
|
||||
['code' => 'alipay_life', 'name' => '支付宝生活号', 'device_type' => 'ALIPAY_CLIENT'],
|
||||
['code' => 'alipay_app', 'name' => '支付宝APP', 'device_type' => 'ALIPAY_CLIENT'],
|
||||
['code' => 'alipay_qr', 'name' => '支付宝扫码', 'device_type' => 'PC'],
|
||||
protected array $paymentInfo = [
|
||||
'code' => 'lakala',
|
||||
'name' => '拉卡拉(示例)',
|
||||
'author' => '',
|
||||
'link' => '',
|
||||
'pay_types' => ['alipay', 'wechat'],
|
||||
'transfer_types' => [],
|
||||
'config_schema' => [
|
||||
'fields' => [
|
||||
['field' => 'notify_url', 'label' => '异步通知地址', 'type' => 'text', 'required' => false],
|
||||
],
|
||||
'wechat' => [
|
||||
['code' => 'wechat_jsapi', 'name' => '微信JSAPI', 'device_type' => 'WECHAT'],
|
||||
['code' => 'wechat_h5', 'name' => '微信H5', 'device_type' => 'H5'],
|
||||
['code' => 'wechat_native', 'name' => '微信扫码', 'device_type' => 'PC'],
|
||||
['code' => 'wechat_app', 'name' => '微信APP', 'device_type' => 'H5'],
|
||||
],
|
||||
'unionpay' => [
|
||||
['code' => 'unionpay_h5', 'name' => '云闪付H5', 'device_type' => 'H5'],
|
||||
['code' => 'unionpay_app', 'name' => '云闪付APP', 'device_type' => 'H5'],
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取配置Schema
|
||||
*/
|
||||
public static function getConfigSchema(string $methodCode): array
|
||||
public function pay(array $order): array
|
||||
{
|
||||
$baseFields = [
|
||||
['field' => 'merchant_id', 'label' => '商户号', 'type' => 'input', 'required' => true],
|
||||
['field' => 'secret_key', 'label' => '密钥', 'type' => 'input', 'required' => true],
|
||||
['field' => 'api_url', 'label' => '接口地址', 'type' => 'input', 'required' => true],
|
||||
];
|
||||
$orderId = (string)($order['order_id'] ?? '');
|
||||
$amount = (string)($order['amount'] ?? '0.00');
|
||||
$extra = is_array($order['extra'] ?? null) ? $order['extra'] : [];
|
||||
|
||||
// 根据支付方式添加特定字段
|
||||
if ($methodCode === 'alipay') {
|
||||
$baseFields[] = ['field' => 'alipay_app_id', 'label' => '支付宝AppId', 'type' => 'input'];
|
||||
} elseif ($methodCode === 'wechat') {
|
||||
$baseFields[] = ['field' => 'wechat_app_id', 'label' => '微信AppId', 'type' => 'input'];
|
||||
if ($orderId === '') {
|
||||
throw new PaymentException('缺少订单号', 402);
|
||||
}
|
||||
|
||||
return ['fields' => $baseFields];
|
||||
}
|
||||
// 这里先返回“可联调”的 pay_params:默认给一个 qrcode 字符串
|
||||
// 真实实现中应调用拉卡拉下单接口,返回二维码链接/支付链接/预支付信息等。
|
||||
$qrcode = $extra['mock_qrcode'] ?? ('LAKALA_MOCK_QRCODE:' . $orderId . ':' . $amount);
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*/
|
||||
public function unifiedOrder(array $orderData, array $channelConfig, string $requestEnv): array
|
||||
{
|
||||
// 1. 从通道已开通产品中选择(根据环境)
|
||||
$enabledProducts = $channelConfig['enabled_products'] ?? [];
|
||||
$allProducts = static::getSupportedProducts($this->currentMethod);
|
||||
$productCode = $this->selectProductByEnv($enabledProducts, $requestEnv, $allProducts);
|
||||
|
||||
if (!$productCode) {
|
||||
throw new \RuntimeException('当前环境无可用支付产品');
|
||||
}
|
||||
|
||||
// 2. 根据当前支付方式和产品调用不同的接口
|
||||
// 这里简化处理,实际应调用拉卡拉的API
|
||||
return match ($this->currentMethod) {
|
||||
'alipay' => $this->createAlipayOrder($orderData, $channelConfig, $productCode),
|
||||
'wechat' => $this->createWechatOrder($orderData, $channelConfig, $productCode),
|
||||
'unionpay' => $this->createUnionpayOrder($orderData, $channelConfig, $productCode),
|
||||
default => throw new \RuntimeException('未初始化的支付方式'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
*/
|
||||
public function query(array $orderData, array $channelConfig): array
|
||||
{
|
||||
// TODO: 实现查询逻辑
|
||||
return ['status' => 'PENDING'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*/
|
||||
public function refund(array $refundData, array $channelConfig): array
|
||||
{
|
||||
// TODO: 实现退款逻辑
|
||||
return ['status' => 'SUCCESS'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析回调
|
||||
*/
|
||||
public function parseNotify(array $requestData, array $channelConfig): array
|
||||
{
|
||||
// TODO: 实现回调解析和验签
|
||||
return [
|
||||
'status' => 'SUCCESS',
|
||||
'pay_order_id' => $requestData['out_trade_no'] ?? '',
|
||||
'channel_trade_no'=> $requestData['trade_no'] ?? '',
|
||||
'amount' => $requestData['total_amount'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function createAlipayOrder(array $orderData, array $config, string $productCode): array
|
||||
{
|
||||
// TODO: 调用拉卡拉的支付宝接口
|
||||
return [
|
||||
'product_code' => $productCode,
|
||||
'channel_order_no'=> '',
|
||||
'pay_params' => [
|
||||
'type' => 'redirect',
|
||||
'url' => 'https://example.com/pay?order=' . $orderData['pay_order_id'],
|
||||
'pay_params' => [
|
||||
'type' => 'qrcode',
|
||||
'qrcode_url' => $qrcode,
|
||||
'qrcode_data'=> $qrcode,
|
||||
],
|
||||
'chan_order_no' => $orderId,
|
||||
'chan_trade_no' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function createWechatOrder(array $orderData, array $config, string $productCode): array
|
||||
public function query(array $order): array
|
||||
{
|
||||
// TODO: 调用拉卡拉的微信接口
|
||||
return [
|
||||
'product_code' => $productCode,
|
||||
'channel_order_no'=> '',
|
||||
'pay_params' => [
|
||||
'type' => 'jsapi',
|
||||
'appId' => $config['wechat_app_id'] ?? '',
|
||||
'timeStamp' => time(),
|
||||
'nonceStr' => uniqid(),
|
||||
'package' => 'prepay_id=xxx',
|
||||
'signType' => 'MD5',
|
||||
'paySign' => 'xxx',
|
||||
],
|
||||
];
|
||||
throw new PaymentException('LakalaPayment::query 暂未实现', 402);
|
||||
}
|
||||
|
||||
private function createUnionpayOrder(array $orderData, array $config, string $productCode): array
|
||||
public function close(array $order): array
|
||||
{
|
||||
// TODO: 调用拉卡拉的云闪付接口
|
||||
return [
|
||||
'product_code' => $productCode,
|
||||
'channel_order_no'=> '',
|
||||
'pay_params' => [
|
||||
'type' => 'redirect',
|
||||
'url' => 'https://example.com/unionpay?order=' . $orderData['pay_order_id'],
|
||||
],
|
||||
];
|
||||
throw new PaymentException('LakalaPayment::close 暂未实现', 402);
|
||||
}
|
||||
|
||||
public function refund(array $order): array
|
||||
{
|
||||
throw new PaymentException('LakalaPayment::refund 暂未实现', 402);
|
||||
}
|
||||
|
||||
public function notify(Request $request): array
|
||||
{
|
||||
throw new PaymentException('LakalaPayment::notify 暂未实现', 402);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
65
app/common/utils/EpayUtil.php
Normal file
65
app/common/utils/EpayUtil.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
/**
|
||||
* 易支付签名工具(MD5)
|
||||
*
|
||||
* 规则:
|
||||
* - 排除 sign、sign_type
|
||||
* - 排除空值(null / '')
|
||||
* - 按字段名 ASCII 升序排序
|
||||
* - k=v&...&key=app_secret
|
||||
* - MD5 后转小写(兼容大小写比较)
|
||||
*/
|
||||
final class EpayUtil
|
||||
{
|
||||
/**
|
||||
* 生成签名字符串
|
||||
*
|
||||
* @param array<string, mixed> $params 请求参数
|
||||
*/
|
||||
public static function make(array $params, string $secret): string
|
||||
{
|
||||
unset($params['sign'], $params['sign_type']);
|
||||
|
||||
$filtered = [];
|
||||
foreach ($params as $k => $v) {
|
||||
if ($v === null) {
|
||||
continue;
|
||||
}
|
||||
if (is_string($v) && trim($v) === '') {
|
||||
continue;
|
||||
}
|
||||
$filtered[$k] = is_bool($v) ? ($v ? '1' : '0') : (string)$v;
|
||||
}
|
||||
|
||||
ksort($filtered);
|
||||
|
||||
$pairs = [];
|
||||
foreach ($filtered as $k => $v) {
|
||||
$pairs[] = $k . '=' . $v;
|
||||
}
|
||||
|
||||
$pairs[] = 'key=' . $secret;
|
||||
|
||||
return strtolower(md5(implode('&', $pairs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验签名
|
||||
*
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function verify(array $params, string $secret): bool
|
||||
{
|
||||
$sign = strtolower((string)($params['sign'] ?? ''));
|
||||
if ($sign === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals(self::make($params, $secret), $sign);
|
||||
}
|
||||
}
|
||||
|
||||
25
app/exceptions/PaymentException.php
Normal file
25
app/exceptions/PaymentException.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
use Webman\Exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 支付业务异常
|
||||
*
|
||||
* 用于支付相关业务错误,如:下单失败、退款失败、验签失败、渠道异常等。
|
||||
*
|
||||
* 示例:
|
||||
* throw new PaymentException('当前环境无可用支付产品');
|
||||
* throw new PaymentException('渠道返回错误', 402, ['channel_code' => 'lakala']);
|
||||
*/
|
||||
class PaymentException extends BusinessException
|
||||
{
|
||||
public function __construct(string $message = '支付业务异常', int $bizCode = 402, array $data = [])
|
||||
{
|
||||
parent::__construct($message, $bizCode);
|
||||
if (!empty($data)) {
|
||||
$this->data($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,11 @@ class EpayController extends BaseController
|
||||
*/
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$data = array_merge($request->get(), $request->post());
|
||||
$data = match ($request->method()) {
|
||||
'GET' => $request->get(),
|
||||
'POST' => $request->post(),
|
||||
default => array_merge($request->get(), $request->post()),
|
||||
};
|
||||
|
||||
try {
|
||||
// 参数校验(使用自定义 Validator + 场景)
|
||||
@@ -40,6 +44,9 @@ class EpayController extends BaseController
|
||||
}
|
||||
|
||||
if (($payParams['type'] ?? '') === 'form') {
|
||||
if (!empty($payParams['html'])) {
|
||||
return response($payParams['html'])->withHeaders(['Content-Type' => 'text/html; charset=UTF-8']);
|
||||
}
|
||||
return $this->renderForm($payParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,9 @@ class PaymentOrder extends BaseModel
|
||||
'expire_at' => 'datetime',
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 0;
|
||||
const STATUS_SUCCESS = 1;
|
||||
const STATUS_FAIL = 2;
|
||||
const STATUS_CLOSED = 3;
|
||||
/* 订单状态 */
|
||||
const STATUS_PENDING = 0; // 待支付
|
||||
const STATUS_SUCCESS = 1; // 支付成功
|
||||
const STATUS_FAIL = 2; // 支付失败
|
||||
const STATUS_CLOSED = 3; // 已关闭
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
use Webman\Route;
|
||||
use app\http\api\controller\EpayController;
|
||||
use app\http\api\middleware\EpayAuthMiddleware;
|
||||
|
||||
Route::group('', function () {
|
||||
// 页面跳转支付
|
||||
@@ -17,4 +16,4 @@ Route::group('', function () {
|
||||
|
||||
// API接口
|
||||
Route::get('/api.php', [EpayController::class, 'api']);
|
||||
})->middleware([EpayAuthMiddleware::class]);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ use app\common\base\BaseService;
|
||||
use app\exceptions\{BadRequestException, NotFoundException};
|
||||
use app\models\PaymentOrder;
|
||||
use app\repositories\{MerchantAppRepository, PaymentChannelRepository, PaymentMethodRepository, PaymentOrderRepository};
|
||||
use Illuminate\Database\QueryException;
|
||||
|
||||
/**
|
||||
* 支付订单服务
|
||||
@@ -28,11 +29,11 @@ class PayOrderService extends BaseService
|
||||
public function createOrder(array $data)
|
||||
{
|
||||
// 1. 基本参数校验
|
||||
$mchId = (int)($data['mch_id'] ?? $data['merchant_id'] ?? 0);
|
||||
$mchId = (int)($data['mch_id'] ?? 0);
|
||||
$appId = (int)($data['app_id'] ?? 0);
|
||||
$mchNo = trim((string)($data['mch_no'] ?? $data['mch_order_no'] ?? ''));
|
||||
$methodCode = trim((string)($data['method_code'] ?? ''));
|
||||
$amount = (float)($data['amount'] ?? 0);
|
||||
$mchNo = trim((string)($data['mch_order_no'] ?? ''));
|
||||
$payType = trim((string)($data['pay_type'] ?? ''));
|
||||
$amountFloat = (float)($data['amount'] ?? 0);
|
||||
$subject = trim((string)($data['subject'] ?? ''));
|
||||
|
||||
if ($mchId <= 0 || $appId <= 0) {
|
||||
@@ -41,10 +42,10 @@ class PayOrderService extends BaseService
|
||||
if ($mchNo === '') {
|
||||
throw new BadRequestException('商户订单号不能为空');
|
||||
}
|
||||
if ($methodCode === '') {
|
||||
if ($payType === '') {
|
||||
throw new BadRequestException('支付方式不能为空');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
if ($amountFloat <= 0) {
|
||||
throw new BadRequestException('订单金额必须大于0');
|
||||
}
|
||||
if ($subject === '') {
|
||||
@@ -52,12 +53,13 @@ class PayOrderService extends BaseService
|
||||
}
|
||||
|
||||
// 2. 查询支付方式ID
|
||||
$method = $this->methodRepository->findByCode($methodCode);
|
||||
$method = $this->methodRepository->findByCode($payType);
|
||||
if (!$method) {
|
||||
throw new BadRequestException('支付方式不存在');
|
||||
}
|
||||
|
||||
// 3. 幂等校验:同一商户应用下相同商户订单号只保留一条
|
||||
// 先查一次(减少异常成本),并发场景再用唯一键冲突兜底
|
||||
$existing = $this->orderRepository->findByMchNo($mchId, $appId, $mchNo);
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
@@ -65,25 +67,34 @@ class PayOrderService extends BaseService
|
||||
|
||||
// 4. 生成系统订单号
|
||||
$orderId = $this->generateOrderId();
|
||||
$amount = sprintf('%.2f', $amountFloat);
|
||||
|
||||
// 5. 创建订单
|
||||
return $this->orderRepository->create([
|
||||
'order_id' => $orderId,
|
||||
'merchant_id' => $mchId,
|
||||
'merchant_app_id' => $appId,
|
||||
'mch_order_no' => $mchNo,
|
||||
'method_id' => $method->id,
|
||||
'channel_id' => $data['channel_id'] ?? $data['chan_id'] ?? 0,
|
||||
'amount' => $amount,
|
||||
'real_amount' => $amount,
|
||||
'fee' => $data['fee'] ?? 0.00,
|
||||
'subject' => $subject,
|
||||
'body' => $data['body'] ?? $subject,
|
||||
'status' => PaymentOrder::STATUS_PENDING,
|
||||
'client_ip' => $data['client_ip'] ?? '',
|
||||
'expire_at' => $data['expire_at'] ?? $data['expire_time'] ?? date('Y-m-d H:i:s', time() + 1800),
|
||||
'extra' => $data['extra'] ?? [],
|
||||
]);
|
||||
$expireTime = (int)sys_config('order_expire_time', 0); // 0 表示不设置过期时间
|
||||
try {
|
||||
return $this->orderRepository->create([
|
||||
'order_id' => $orderId,
|
||||
'merchant_id' => $mchId,
|
||||
'merchant_app_id' => $appId,
|
||||
'mch_order_no' => $mchNo,
|
||||
'method_id' => $method->id,
|
||||
'amount' => $amount,
|
||||
'real_amount' => $amount,
|
||||
'subject' => $subject,
|
||||
'body' => $data['body'] ?? $subject,
|
||||
'status' => PaymentOrder::STATUS_PENDING,
|
||||
'client_ip' => $data['client_ip'] ?? '',
|
||||
'expire_at' => $expireTime > 0 ? date('Y-m-d H:i:s', time() + $expireTime) : null,
|
||||
'extra' => $data['extra'] ?? [],
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
// 并发场景:唯一键 uk_mch_order 冲突时回查返回已有订单
|
||||
$existing = $this->orderRepository->findByMchNo($mchId, $appId, $mchNo);
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +153,7 @@ class PayOrderService extends BaseService
|
||||
$channel->getConfigArray(),
|
||||
['enabled_products' => $channel->getEnabledProducts()]
|
||||
);
|
||||
$plugin->init($method->method_code, $channelConfig);
|
||||
$plugin->init($channelConfig);
|
||||
|
||||
// 7. 调用插件退款
|
||||
$refundData = [
|
||||
@@ -153,7 +164,7 @@ class PayOrderService extends BaseService
|
||||
'refund_reason' => $data['refund_reason'] ?? '',
|
||||
];
|
||||
|
||||
$refundResult = $plugin->refund($refundData, $channelConfig);
|
||||
$refundResult = $plugin->refund($refundData);
|
||||
|
||||
// 8. 如果是全额退款则关闭订单
|
||||
if ($refundAmount >= $order->amount) {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\contracts\PayPluginInterface;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\models\PaymentOrder;
|
||||
use app\repositories\{PaymentMethodRepository, PaymentOrderRepository};
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ class PayService extends BaseService
|
||||
* - mch_no
|
||||
* - pay_params
|
||||
*/
|
||||
public function unifiedPay(array $orderData, array $options = []): array
|
||||
public function pay(array $orderData, array $options = []): array
|
||||
{
|
||||
// 1. 创建订单(幂等)
|
||||
/** @var PaymentOrder $order */
|
||||
@@ -63,7 +63,7 @@ class PayService extends BaseService
|
||||
$channel->getConfigArray(),
|
||||
['enabled_products' => $channel->getEnabledProducts()]
|
||||
);
|
||||
$plugin->init($method->method_code, $channelConfig);
|
||||
$plugin->init($channelConfig);
|
||||
|
||||
// 5. 环境检测
|
||||
$device = $options['device'] ?? '';
|
||||
@@ -75,23 +75,27 @@ class PayService extends BaseService
|
||||
} elseif ($request instanceof Request) {
|
||||
$env = $this->detectEnvironment($request);
|
||||
} else {
|
||||
$env = AbstractPayPlugin::ENV_PC;
|
||||
$env = 'pc';
|
||||
}
|
||||
|
||||
// 6. 调用插件统一下单
|
||||
$pluginOrderData = [
|
||||
'order_id' => $order->order_id,
|
||||
'mch_no' => $order->mch_order_no,
|
||||
'amount' => $order->amount,
|
||||
'subject' => $order->subject,
|
||||
'body' => $order->body,
|
||||
'mch_no' => $order->mch_order_no,
|
||||
'amount' => $order->amount,
|
||||
'subject' => $order->subject,
|
||||
'body' => $order->body,
|
||||
'extra' => $order->extra ?? [],
|
||||
'_env' => $env,
|
||||
];
|
||||
|
||||
$payResult = $plugin->unifiedOrder($pluginOrderData, $channelConfig, $env);
|
||||
$payResult = $plugin->pay($pluginOrderData);
|
||||
|
||||
// 7. 计算实际支付金额(扣除手续费)
|
||||
$fee = $order->fee > 0 ? $order->fee : ($order->amount * ($channel->chan_cost / 100));
|
||||
$realAmount = $order->amount - $fee;
|
||||
$amount = (float)$order->amount;
|
||||
$chanCost = (float)$channel->chan_cost;
|
||||
$fee = ((float)$order->fee) > 0 ? (float)$order->fee : round($amount * ($chanCost / 100), 2);
|
||||
$realAmount = round($amount - $fee, 2);
|
||||
|
||||
// 8. 更新订单(通道、支付参数、实际金额)
|
||||
$extra = $order->extra ?? [];
|
||||
@@ -103,8 +107,8 @@ class PayService extends BaseService
|
||||
'channel_id' => $channel->id,
|
||||
'chan_order_no' => $chanOrderNo,
|
||||
'chan_trade_no' => $chanTradeNo,
|
||||
'real_amount' => $realAmount,
|
||||
'fee' => $fee,
|
||||
'real_amount' => sprintf('%.2f', $realAmount),
|
||||
'fee' => sprintf('%.2f', $fee),
|
||||
'extra' => $extra,
|
||||
]);
|
||||
|
||||
@@ -123,21 +127,21 @@ class PayService extends BaseService
|
||||
$ua = strtolower($request->header('User-Agent', ''));
|
||||
|
||||
if (strpos($ua, 'alipayclient') !== false) {
|
||||
return AbstractPayPlugin::ENV_ALIPAY_CLIENT;
|
||||
return 'alipay';
|
||||
}
|
||||
|
||||
if (strpos($ua, 'micromessenger') !== false) {
|
||||
return AbstractPayPlugin::ENV_WECHAT;
|
||||
return 'wechat';
|
||||
}
|
||||
|
||||
$mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
|
||||
foreach ($mobileKeywords as $keyword) {
|
||||
if (strpos($ua, $keyword) !== false) {
|
||||
return AbstractPayPlugin::ENV_H5;
|
||||
return 'h5';
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractPayPlugin::ENV_PC;
|
||||
return 'pc';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,15 +150,15 @@ class PayService extends BaseService
|
||||
private function mapDeviceToEnv(string $device): string
|
||||
{
|
||||
$mapping = [
|
||||
'pc' => AbstractPayPlugin::ENV_PC,
|
||||
'mobile' => AbstractPayPlugin::ENV_H5,
|
||||
'qq' => AbstractPayPlugin::ENV_H5,
|
||||
'wechat' => AbstractPayPlugin::ENV_WECHAT,
|
||||
'alipay' => AbstractPayPlugin::ENV_ALIPAY_CLIENT,
|
||||
'jump' => AbstractPayPlugin::ENV_PC,
|
||||
'pc' => 'pc',
|
||||
'mobile' => 'h5',
|
||||
'qq' => 'h5',
|
||||
'wechat' => 'wechat',
|
||||
'alipay' => 'alipay',
|
||||
'jump' => 'pc',
|
||||
];
|
||||
|
||||
return $mapping[strtolower($device)] ?? AbstractPayPlugin::ENV_PC;
|
||||
return $mapping[strtolower($device)] ?? 'pc';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
use app\common\contracts\PaymentInterface;
|
||||
use app\common\contracts\PayPluginInterface;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\repositories\PaymentPluginRepository;
|
||||
|
||||
@@ -36,8 +37,8 @@ class PluginService extends BaseService
|
||||
$plugin = $this->resolvePlugin($pluginCode, $row->class_name);
|
||||
$plugins[] = [
|
||||
'code' => $pluginCode,
|
||||
'name' => $plugin::getName(),
|
||||
'supported_methods'=> $plugin::getSupportedMethods(),
|
||||
'name' => $plugin->getName(),
|
||||
'supported_methods'=> $plugin->getEnabledPayTypes(),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略无法实例化的插件
|
||||
@@ -54,7 +55,7 @@ class PluginService extends BaseService
|
||||
public function getConfigSchema(string $pluginCode, string $methodCode): array
|
||||
{
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
return $plugin::getConfigSchema($methodCode);
|
||||
return $plugin->getConfigSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,8 +63,12 @@ class PluginService extends BaseService
|
||||
*/
|
||||
public function getSupportedProducts(string $pluginCode, string $methodCode): array
|
||||
{
|
||||
/** @var mixed $plugin */
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
return $plugin::getSupportedProducts($methodCode);
|
||||
if (method_exists($plugin, 'getSupportedProducts')) {
|
||||
return (array)$plugin->getSupportedProducts($methodCode);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +77,7 @@ class PluginService extends BaseService
|
||||
public function buildConfigFromForm(string $pluginCode, string $methodCode, array $formData): array
|
||||
{
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
$configSchema = $plugin::getConfigSchema($methodCode);
|
||||
$configSchema = $plugin->getConfigSchema();
|
||||
|
||||
$configJson = [];
|
||||
if (isset($configSchema['fields']) && is_array($configSchema['fields'])) {
|
||||
@@ -90,7 +95,7 @@ class PluginService extends BaseService
|
||||
/**
|
||||
* 对外统一提供:根据插件编码获取插件实例
|
||||
*/
|
||||
public function getPluginInstance(string $pluginCode): AbstractPayPlugin
|
||||
public function getPluginInstance(string $pluginCode): PaymentInterface&PayPluginInterface
|
||||
{
|
||||
$row = $this->pluginRepository->findActiveByCode($pluginCode);
|
||||
if (!$row) {
|
||||
@@ -103,7 +108,7 @@ class PluginService extends BaseService
|
||||
/**
|
||||
* 根据插件编码和 class_name 解析并实例化插件
|
||||
*/
|
||||
private function resolvePlugin(string $pluginCode, ?string $className = null): AbstractPayPlugin
|
||||
private function resolvePlugin(string $pluginCode, ?string $className = null): PaymentInterface&PayPluginInterface
|
||||
{
|
||||
$class = $className ?: 'app\\common\\payment\\' . ucfirst($pluginCode) . 'Payment';
|
||||
|
||||
@@ -112,7 +117,7 @@ class PluginService extends BaseService
|
||||
}
|
||||
|
||||
$plugin = new $class();
|
||||
if (!$plugin instanceof AbstractPayPlugin) {
|
||||
if (!$plugin instanceof PaymentInterface || !$plugin instanceof PayPluginInterface) {
|
||||
throw new NotFoundException('支付插件类型错误:' . $class);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace app\services\api;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\utils\EpayUtil;
|
||||
use app\services\PayOrderService;
|
||||
use app\services\PayService;
|
||||
use app\repositories\{MerchantAppRepository, PaymentMethodRepository, PaymentOrderRepository};
|
||||
use app\models\PaymentOrder;
|
||||
use app\exceptions\{BadRequestException, NotFoundException};
|
||||
use app\exceptions\{BadRequestException, NotFoundException, UnauthorizedException};
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
@@ -37,7 +38,7 @@ class EpayService extends BaseService
|
||||
throw new BadRequestException('暂不支持收银台模式,请指定支付方式 type');
|
||||
}
|
||||
|
||||
return $this->createUnifiedOrder($data, $request);
|
||||
return $this->createOrder($data, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,7 @@ class EpayService extends BaseService
|
||||
*/
|
||||
public function mapi(array $data, Request $request): array
|
||||
{
|
||||
$result = $this->createUnifiedOrder($data, $request);
|
||||
$result = $this->createOrder($data, $request);
|
||||
$payParams = $result['pay_params'] ?? [];
|
||||
|
||||
$response = [
|
||||
@@ -142,7 +143,7 @@ class EpayService extends BaseService
|
||||
'trade_no' => $order->order_id,
|
||||
'out_trade_no' => $order->mch_order_no,
|
||||
'api_trade_no' => $order->chan_trade_no ?? '',
|
||||
'type' => $this->mapMethodToEpayType($methodCode),
|
||||
'type' => $methodCode,
|
||||
'pid' => (int)$pid,
|
||||
'addtime' => $order->created_at,
|
||||
'endtime' => $order->pay_at,
|
||||
@@ -210,11 +211,11 @@ class EpayService extends BaseService
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
private function createUnifiedOrder(array $data, Request $request): array
|
||||
private function createOrder(array $data, Request $request): array
|
||||
{
|
||||
$pid = (int)($data['pid'] ?? 0);
|
||||
if ($pid <= 0) {
|
||||
throw new BadRequestException('商户ID不能为空');
|
||||
throw new BadRequestException('应用ID不能为空');
|
||||
}
|
||||
|
||||
// 根据 pid 映射应用(约定 pid = app_id)
|
||||
@@ -223,14 +224,21 @@ class EpayService extends BaseService
|
||||
throw new NotFoundException('商户应用不存在或已禁用');
|
||||
}
|
||||
|
||||
$methodCode = $this->mapEpayTypeToMethod($data['type'] ?? '');
|
||||
// 易支付签名校验:使用 app_secret 作为 key
|
||||
$signType = strtolower((string)($data['sign_type'] ?? 'md5'));
|
||||
if ($signType !== 'md5') {
|
||||
throw new BadRequestException('不支持的签名类型:' . ($data['sign_type'] ?? ''));
|
||||
}
|
||||
if (!EpayUtil::verify($data, (string)$app->app_secret)) {
|
||||
throw new UnauthorizedException('签名验证失败');
|
||||
}
|
||||
|
||||
$orderData = [
|
||||
'merchant_id' => $app->merchant_id,
|
||||
'mch_id' => $app->merchant_id,
|
||||
'app_id' => $app->id,
|
||||
'mch_order_no' => $data['out_trade_no'],
|
||||
'method_code' => $methodCode,
|
||||
'amount' => (float)$data['money'],
|
||||
'currency' => 'CNY',
|
||||
'pay_type' => $data['type'],
|
||||
'amount' => sprintf('%.2f', (float)$data['money']),
|
||||
'subject' => $data['name'],
|
||||
'body' => $data['name'],
|
||||
'client_ip' => $data['clientip'] ?? $request->getRemoteIp(),
|
||||
@@ -242,26 +250,12 @@ class EpayService extends BaseService
|
||||
];
|
||||
|
||||
// 调用通用支付服务完成通道选择与插件下单
|
||||
return $this->payService->unifiedPay($orderData, [
|
||||
return $this->payService->pay($orderData, [
|
||||
'device' => $data['device'] ?? '',
|
||||
'request' => $request,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射易支付 type 到内部 method_code
|
||||
*/
|
||||
private function mapEpayTypeToMethod(string $type): string
|
||||
{
|
||||
$mapping = [
|
||||
'alipay' => 'alipay',
|
||||
'wxpay' => 'wechat',
|
||||
'qqpay' => 'qq',
|
||||
];
|
||||
|
||||
return $mapping[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单获取支付方式编码
|
||||
*/
|
||||
@@ -270,19 +264,4 @@ class EpayService extends BaseService
|
||||
$method = $this->methodRepository->find($order->method_id);
|
||||
return $method ? $method->method_code : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射内部 method_code 到易支付 type
|
||||
*/
|
||||
private function mapMethodToEpayType(string $methodCode): string
|
||||
{
|
||||
$mapping = [
|
||||
'alipay' => 'alipay',
|
||||
'wechat' => 'wxpay',
|
||||
'qq' => 'qqpay',
|
||||
];
|
||||
|
||||
return $mapping[$methodCode] ?? $methodCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"webman/redis-queue": "^2.1",
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"webman/validation": "^2.2",
|
||||
"illuminate/pagination": "^12.53"
|
||||
"illuminate/pagination": "^12.53",
|
||||
"yansongda/pay": "~3.7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
|
||||
210
composer.lock
generated
210
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "49b81bfe43345cf72dbd33c88720b5a2",
|
||||
"content-hash": "ef4cd33c0940c77fc8b5fffd35cd9bb7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -6115,6 +6115,214 @@
|
||||
}
|
||||
],
|
||||
"time": "2026-01-09T03:26:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "yansongda/artful",
|
||||
"version": "v1.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yansongda/artful.git",
|
||||
"reference": "ddc203ef34ab369a5a31df057a0fda697d3ed855"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/yansongda/artful/zipball/ddc203ef34ab369a5a31df057a0fda697d3ed855",
|
||||
"reference": "ddc203ef34ab369a5a31df057a0fda697d3ed855",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/psr7": "^2.6",
|
||||
"php": ">=8.0",
|
||||
"psr/container": "^1.1 || ^2.0",
|
||||
"psr/event-dispatcher": "^1.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0",
|
||||
"yansongda/supports": "~4.0.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.44",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"hyperf/pimple": "^2.2",
|
||||
"mockery/mockery": "^1.4",
|
||||
"monolog/monolog": "^2.2",
|
||||
"phpstan/phpstan": "^1.0.0 || ^2.0.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"symfony/event-dispatcher": "^5.2.0",
|
||||
"symfony/http-foundation": "^5.2.0",
|
||||
"symfony/psr-http-message-bridge": "^2.1",
|
||||
"symfony/var-dumper": "^5.1"
|
||||
},
|
||||
"suggest": {
|
||||
"hyperf/pimple": "其它/无框架下使用 SDK,请安装,任选其一",
|
||||
"illuminate/container": "其它/无框架下使用 SDK,请安装,任选其一"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Yansongda\\Artful\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yansongda",
|
||||
"email": "me@yansongda.cn"
|
||||
}
|
||||
],
|
||||
"description": "Artful 是一个简单易用的 API 请求框架 PHP Api RequesT Framwork U Like。",
|
||||
"keywords": [
|
||||
"api",
|
||||
"artful",
|
||||
"framework",
|
||||
"request"
|
||||
],
|
||||
"support": {
|
||||
"homepage": "https://artful.yansongda.cn",
|
||||
"issues": "https://github.com/yansongda/artful/issues",
|
||||
"source": "https://github.com/yansongda/artful"
|
||||
},
|
||||
"time": "2025-07-24T09:39:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "yansongda/pay",
|
||||
"version": "v3.7.19",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yansongda/pay.git",
|
||||
"reference": "57eaeff84bd4a19c4d09656a3c45250c9a032aa2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/yansongda/pay/zipball/57eaeff84bd4a19c4d09656a3c45250c9a032aa2",
|
||||
"reference": "57eaeff84bd4a19c4d09656a3c45250c9a032aa2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-bcmath": "*",
|
||||
"ext-json": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-simplexml": "*",
|
||||
"php": ">=8.0",
|
||||
"yansongda/artful": "~1.1.3",
|
||||
"yansongda/supports": "~4.0.10"
|
||||
},
|
||||
"conflict": {
|
||||
"hyperf/framework": "<3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.44",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"hyperf/pimple": "^2.2",
|
||||
"jetbrains/phpstorm-attributes": "^1.1",
|
||||
"mockery/mockery": "^1.4",
|
||||
"monolog/monolog": "^2.2",
|
||||
"phpstan/phpstan": "^1.0.0 || ^2.0.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"symfony/event-dispatcher": "^5.2.0",
|
||||
"symfony/http-foundation": "^5.2.0",
|
||||
"symfony/psr-http-message-bridge": "^2.1",
|
||||
"symfony/var-dumper": "^5.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Yansongda\\Pay\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yansongda",
|
||||
"email": "me@yansongda.cn"
|
||||
}
|
||||
],
|
||||
"description": "可能是我用过的最优雅的 Alipay 和 WeChat 的支付 SDK 扩展包了",
|
||||
"keywords": [
|
||||
"alipay",
|
||||
"pay",
|
||||
"wechat"
|
||||
],
|
||||
"support": {
|
||||
"homepage": "https://pay.yansongda.cn",
|
||||
"issues": "https://github.com/yansongda/pay/issues",
|
||||
"source": "https://github.com/yansongda/pay"
|
||||
},
|
||||
"time": "2025-12-22T03:30:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "yansongda/supports",
|
||||
"version": "v4.0.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yansongda/supports.git",
|
||||
"reference": "308de376d20cb1cd4f959644793e0582ccd1ef6d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/yansongda/supports/zipball/308de376d20cb1cd4f959644793e0582ccd1ef6d",
|
||||
"reference": "308de376d20cb1cd4f959644793e0582ccd1ef6d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"mockery/mockery": "^1.4",
|
||||
"phpstan/phpstan": "^1.1.0",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"suggest": {
|
||||
"monolog/monolog": "Use logger",
|
||||
"symfony/console": "Use stdout logger"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Yansongda\\Supports\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yansongda",
|
||||
"email": "me@yansongda.cn"
|
||||
}
|
||||
],
|
||||
"description": "common components",
|
||||
"keywords": [
|
||||
"array",
|
||||
"collection",
|
||||
"config",
|
||||
"support"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/yansongda/supports/issues",
|
||||
"source": "https://github.com/yansongda/supports"
|
||||
},
|
||||
"time": "2025-01-08T08:55:20+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
@@ -42,7 +42,9 @@ ON DUPLICATE KEY UPDATE
|
||||
|
||||
-- 5) 插件注册表(按项目约定:app\\common\\payment\\{Code}Payment)
|
||||
INSERT INTO `ma_pay_plugin` (`plugin_code`, `plugin_name`, `class_name`, `status`, `created_at`, `updated_at`)
|
||||
VALUES ('lakala', '拉卡拉(示例)', 'app\\\\common\\\\payment\\\\LakalaPayment', 1, NOW(), NOW())
|
||||
VALUES
|
||||
('lakala', '拉卡拉(示例)', 'app\\\\common\\\\payment\\\\LakalaPayment', 1, NOW(), NOW()),
|
||||
('alipay', '支付宝直连', 'app\\\\common\\\\payment\\\\AlipayPayment', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`plugin_name` = VALUES(`plugin_name`),
|
||||
`class_name` = VALUES(`class_name`),
|
||||
|
||||
@@ -36,3 +36,24 @@ if (!function_exists('container_make')) {
|
||||
return Container::make($name, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sys_config')) {
|
||||
/**
|
||||
* 获取系统配置项(带默认值)
|
||||
*
|
||||
* 读取来源:ma_system_config 表,通过 SystemConfigService + 缓存。
|
||||
*
|
||||
* @param string $key 配置键名(config_key)
|
||||
* @param mixed $default 默认值(未配置或为 null 时返回)
|
||||
* @return mixed
|
||||
*
|
||||
* @example
|
||||
* $siteName = sys_config('site_name', 'MyPay');
|
||||
*/
|
||||
function sys_config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
/** @var \app\services\SystemConfigService $service */
|
||||
$service = Container::get(\app\services\SystemConfigService::class);
|
||||
return $service->getValue($key, $default);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user