1. 完善易支付API调用全流程

2. 确定支付插件继承基础类和接口规范
3. 引入Yansongda\Pay支付快捷工具
4. 重新整理代码和功能结构
This commit is contained in:
技术老胡
2026-03-12 19:18:21 +08:00
parent 5dae6e7174
commit fa3abdcaff
19 changed files with 1042 additions and 578 deletions

View File

@@ -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) {

View File

@@ -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';
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}