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:
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user