mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-24 11:04:26 +08:00
更新数据库结构
This commit is contained in:
229
app/common/contracts/AbstractPayPlugin.php
Normal file
229
app/common/contracts/AbstractPayPlugin.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
103
app/common/contracts/PayPluginInterface.php
Normal file
103
app/common/contracts/PayPluginInterface.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\contracts;
|
||||
|
||||
/**
|
||||
* 支付插件接口
|
||||
*
|
||||
* 所有支付插件必须实现此接口
|
||||
*/
|
||||
interface PayPluginInterface
|
||||
{
|
||||
/**
|
||||
* 获取插件代码(唯一标识)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getCode(): string;
|
||||
|
||||
/**
|
||||
* 获取插件名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getName(): string;
|
||||
|
||||
/**
|
||||
* 获取插件支持的支付方式列表
|
||||
*
|
||||
* @return array<string> 支付方式代码数组,如 ['alipay', 'wechat']
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
180
app/common/payment/LakalaPayment.php
Normal file
180
app/common/payment/LakalaPayment.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\payment;
|
||||
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
|
||||
/**
|
||||
* 拉卡拉支付插件示例
|
||||
*
|
||||
* 支持多个支付方式:alipay、wechat、unionpay
|
||||
*/
|
||||
class LakalaPayment extends AbstractPayPlugin
|
||||
{
|
||||
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'],
|
||||
],
|
||||
'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
|
||||
{
|
||||
$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],
|
||||
];
|
||||
|
||||
// 根据支付方式添加特定字段
|
||||
if ($methodCode === 'alipay') {
|
||||
$baseFields[] = ['field' => 'alipay_app_id', 'label' => '支付宝AppId', 'type' => 'input'];
|
||||
} elseif ($methodCode === 'wechat') {
|
||||
$baseFields[] = ['field' => 'wechat_app_id', 'label' => '微信AppId', 'type' => 'input'];
|
||||
}
|
||||
|
||||
return ['fields' => $baseFields];
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*/
|
||||
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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function createWechatOrder(array $orderData, array $config, string $productCode): 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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function createUnionpayOrder(array $orderData, array $config, string $productCode): array
|
||||
{
|
||||
// TODO: 调用拉卡拉的云闪付接口
|
||||
return [
|
||||
'product_code' => $productCode,
|
||||
'channel_order_no'=> '',
|
||||
'pay_params' => [
|
||||
'type' => 'redirect',
|
||||
'url' => 'https://example.com/unionpay?order=' . $orderData['pay_order_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
app/http/admin/controller/AdminController.php
Normal file
34
app/http/admin/controller/AdminController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\AdminService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 管理员控制器
|
||||
*/
|
||||
class AdminController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminService $adminService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /admin/getUserInfo
|
||||
*
|
||||
* 获取当前登录管理员信息
|
||||
*/
|
||||
public function getUserInfo(Request $request)
|
||||
{
|
||||
$adminId = $this->currentUserId($request);
|
||||
if ($adminId <= 0) {
|
||||
return $this->fail('未获取到用户信息,请先登录', 401);
|
||||
}
|
||||
|
||||
$data = $this->adminService->getInfoById($adminId);
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
164
app/http/admin/controller/ChannelController.php
Normal file
164
app/http/admin/controller/ChannelController.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\repositories\{PaymentChannelRepository, PaymentMethodRepository};
|
||||
use app\services\PluginService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 通道管理控制器
|
||||
*/
|
||||
class ChannelController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected PaymentChannelRepository $channelRepository,
|
||||
protected PaymentMethodRepository $methodRepository,
|
||||
protected PluginService $pluginService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通道列表
|
||||
* GET /adminapi/channel/list
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
$merchantId = (int)$request->get('merchant_id', 0);
|
||||
$appId = (int)$request->get('app_id', 0);
|
||||
$methodCode = trim((string)$request->get('method_code', ''));
|
||||
|
||||
$where = [];
|
||||
if ($merchantId > 0) {
|
||||
$where['merchant_id'] = $merchantId;
|
||||
}
|
||||
if ($appId > 0) {
|
||||
$where['merchant_app_id'] = $appId;
|
||||
}
|
||||
if ($methodCode !== '') {
|
||||
$method = $this->methodRepository->findByCode($methodCode);
|
||||
if ($method) {
|
||||
$where['method_id'] = $method->id;
|
||||
}
|
||||
}
|
||||
|
||||
$page = (int)($request->get('page', 1));
|
||||
$pageSize = (int)($request->get('page_size', 10));
|
||||
|
||||
$result = $this->channelRepository->paginate($where, $page, $pageSize);
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通道详情
|
||||
* GET /adminapi/channel/detail
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = (int)$request->get('id', 0);
|
||||
if (!$id) {
|
||||
return $this->fail('通道ID不能为空', 400);
|
||||
}
|
||||
|
||||
$channel = $this->channelRepository->find($id);
|
||||
if (!$channel) {
|
||||
return $this->fail('通道不存在', 404);
|
||||
}
|
||||
|
||||
$methodCode = '';
|
||||
if ($channel->method_id) {
|
||||
$method = $this->methodRepository->find($channel->method_id);
|
||||
$methodCode = $method ? $method->method_code : '';
|
||||
}
|
||||
|
||||
try {
|
||||
$configSchema = $this->pluginService->getConfigSchema($channel->plugin_code, $methodCode);
|
||||
|
||||
// 合并当前配置值
|
||||
$currentConfig = $channel->getConfigArray();
|
||||
if (isset($configSchema['fields']) && is_array($configSchema['fields'])) {
|
||||
foreach ($configSchema['fields'] as &$field) {
|
||||
if (isset($field['field']) && isset($currentConfig[$field['field']])) {
|
||||
$field['value'] = $currentConfig[$field['field']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'channel' => $channel,
|
||||
'config_schema' => $configSchema,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->success([
|
||||
'channel' => $channel,
|
||||
'config_schema' => ['fields' => []],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存通道
|
||||
* POST /adminapi/channel/save
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$pluginCode = $data['plugin_code'] ?? '';
|
||||
$methodCode = $data['method_code'] ?? '';
|
||||
$enabledProducts = $data['enabled_products'] ?? [];
|
||||
|
||||
if (empty($pluginCode) || empty($methodCode)) {
|
||||
return $this->fail('插件编码和支付方式不能为空', 400);
|
||||
}
|
||||
|
||||
// 提取配置参数(从表单字段中提取)
|
||||
try {
|
||||
$configJson = $this->pluginService->buildConfigFromForm($pluginCode, $methodCode, $data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('插件不存在或配置错误:' . $e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$method = $this->methodRepository->findByCode($methodCode);
|
||||
if (!$method) {
|
||||
return $this->fail('支付方式不存在', 400);
|
||||
}
|
||||
|
||||
$configWithProducts = array_merge($configJson, ['enabled_products' => is_array($enabledProducts) ? $enabledProducts : []]);
|
||||
|
||||
$channelData = [
|
||||
'merchant_id' => (int)($data['merchant_id'] ?? 0),
|
||||
'merchant_app_id' => (int)($data['app_id'] ?? 0),
|
||||
'chan_code' => $data['channel_code'] ?? $data['chan_code'] ?? '',
|
||||
'chan_name' => $data['channel_name'] ?? $data['chan_name'] ?? '',
|
||||
'plugin_code' => $pluginCode,
|
||||
'method_id' => $method->id,
|
||||
'config_json' => $configWithProducts,
|
||||
'split_ratio' => isset($data['split_ratio']) ? (float)$data['split_ratio'] : 100.00,
|
||||
'chan_cost' => isset($data['channel_cost']) ? (float)$data['channel_cost'] : 0.00,
|
||||
'chan_mode' => $data['channel_mode'] ?? 'wallet',
|
||||
'daily_limit' => isset($data['daily_limit']) ? (float)$data['daily_limit'] : 0.00,
|
||||
'daily_cnt' => isset($data['daily_count']) ? (int)$data['daily_count'] : 0,
|
||||
'min_amount' => isset($data['min_amount']) && $data['min_amount'] !== '' ? (float)$data['min_amount'] : null,
|
||||
'max_amount' => isset($data['max_amount']) && $data['max_amount'] !== '' ? (float)$data['max_amount'] : null,
|
||||
'status' => (int)($data['status'] ?? 1),
|
||||
'sort' => (int)($data['sort'] ?? 0),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
// 更新
|
||||
$this->channelRepository->updateById($id, $channelData);
|
||||
} else {
|
||||
if (empty($channelData['chan_code'])) {
|
||||
$channelData['chan_code'] = 'CH' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
}
|
||||
$this->channelRepository->create($channelData);
|
||||
}
|
||||
|
||||
return $this->success(null, '保存成功');
|
||||
}
|
||||
}
|
||||
|
||||
71
app/http/admin/controller/PluginController.php
Normal file
71
app/http/admin/controller/PluginController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\PluginService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 插件管理控制器
|
||||
*/
|
||||
class PluginController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected PluginService $pluginService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用插件列表
|
||||
* GET /adminapi/channel/plugins
|
||||
*/
|
||||
public function plugins()
|
||||
{
|
||||
$plugins = $this->pluginService->listPlugins();
|
||||
return $this->success($plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件配置Schema
|
||||
* GET /adminapi/channel/plugin/config-schema
|
||||
*/
|
||||
public function configSchema(Request $request)
|
||||
{
|
||||
$pluginCode = $request->get('plugin_code', '');
|
||||
$methodCode = $request->get('method_code', '');
|
||||
|
||||
if (empty($pluginCode) || empty($methodCode)) {
|
||||
return $this->fail('插件编码和支付方式不能为空', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$schema = $this->pluginService->getConfigSchema($pluginCode, $methodCode);
|
||||
return $this->success($schema);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取配置Schema失败:' . $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件支持的支付产品列表
|
||||
* GET /adminapi/channel/plugin/products
|
||||
*/
|
||||
public function products(Request $request)
|
||||
{
|
||||
$pluginCode = $request->get('plugin_code', '');
|
||||
$methodCode = $request->get('method_code', '');
|
||||
|
||||
if (empty($pluginCode) || empty($methodCode)) {
|
||||
return $this->fail('插件编码和支付方式不能为空', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$products = $this->pluginService->getSupportedProducts($pluginCode, $methodCode);
|
||||
return $this->success($products);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取产品列表失败:' . $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\UserService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 用户接口示例控制器
|
||||
*
|
||||
* 主要用于演示 BaseController / Service / Repository / Model 的调用链路。
|
||||
*/
|
||||
class UserController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected UserService $userService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /user/getUserInfo
|
||||
*
|
||||
* 从 JWT token 中获取当前登录用户信息
|
||||
* 前端通过 Authorization: Bearer {token} 请求头传递 token
|
||||
*/
|
||||
public function getUserInfo(Request $request)
|
||||
{
|
||||
// 从JWT中间件注入的用户信息中获取用户ID
|
||||
$userId = $this->currentUserId($request);
|
||||
|
||||
if ($userId <= 0) {
|
||||
return $this->fail('未获取到用户信息,请先登录', 401);
|
||||
}
|
||||
|
||||
$data = $this->userService->getUserInfoById($userId);
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
|
||||
132
app/http/api/controller/EpayController.php
Normal file
132
app/http/api/controller/EpayController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\api\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\api\EpayService;
|
||||
use app\validation\EpayValidator;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 易支付控制器
|
||||
*/
|
||||
class EpayController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected EpayService $epayService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 页面跳转支付
|
||||
*/
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$data = array_merge($request->get(), $request->post());
|
||||
|
||||
try {
|
||||
// 参数校验(使用自定义 Validator + 场景)
|
||||
$params = EpayValidator::make($data)
|
||||
->withScene('submit')
|
||||
->validate();
|
||||
|
||||
// 业务处理:创建订单并获取支付参数
|
||||
$result = $this->epayService->submit($params, $request);
|
||||
$payParams = $result['pay_params'] ?? [];
|
||||
|
||||
// 根据支付参数类型返回响应
|
||||
if (($payParams['type'] ?? '') === 'redirect' && !empty($payParams['url'])) {
|
||||
return redirect($payParams['url']);
|
||||
}
|
||||
|
||||
if (($payParams['type'] ?? '') === 'form') {
|
||||
return $this->renderForm($payParams);
|
||||
}
|
||||
|
||||
// 如果没有匹配的类型,返回错误
|
||||
return $this->fail('支付参数生成失败');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API接口支付
|
||||
*/
|
||||
public function mapi(Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
|
||||
try {
|
||||
$params = EpayValidator::make($data)
|
||||
->withScene('mapi')
|
||||
->validate();
|
||||
|
||||
$result = $this->epayService->mapi($params, $request);
|
||||
|
||||
return json($result);
|
||||
} catch (\Throwable $e) {
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API接口
|
||||
*/
|
||||
public function api(Request $request)
|
||||
{
|
||||
$data = array_merge($request->get(), $request->post());
|
||||
|
||||
try {
|
||||
$act = strtolower($data['act'] ?? '');
|
||||
|
||||
if ($act === 'order') {
|
||||
$params = EpayValidator::make($data)
|
||||
->withScene('api_order')
|
||||
->validate();
|
||||
$result = $this->epayService->api($params);
|
||||
} elseif ($act === 'refund') {
|
||||
$params = EpayValidator::make($data)
|
||||
->withScene('api_refund')
|
||||
->validate();
|
||||
$result = $this->epayService->api($params);
|
||||
} else {
|
||||
$result = [
|
||||
'code' => 0,
|
||||
'msg' => '不支持的操作类型',
|
||||
];
|
||||
}
|
||||
|
||||
return json($result);
|
||||
} catch (\Throwable $e) {
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表单提交 HTML(用于页面跳转支付)
|
||||
*/
|
||||
private function renderForm(array $formParams): Response
|
||||
{
|
||||
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>跳转支付</title></head><body>';
|
||||
$html .= '<form id="payForm" method="' . htmlspecialchars($formParams['method'] ?? 'POST') . '" action="' . htmlspecialchars($formParams['action'] ?? '') . '">';
|
||||
|
||||
if (isset($formParams['fields']) && is_array($formParams['fields'])) {
|
||||
foreach ($formParams['fields'] as $name => $value) {
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars((string)$value) . '">';
|
||||
}
|
||||
}
|
||||
|
||||
$html .= '</form>';
|
||||
$html .= '<script>document.getElementById("payForm").submit();</script>';
|
||||
$html .= '</body></html>';
|
||||
|
||||
return response($html)->withHeaders(['Content-Type' => 'text/html; charset=UTF-8']);
|
||||
}
|
||||
}
|
||||
41
app/http/api/controller/PayController.php
Normal file
41
app/http/api/controller/PayController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\api\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\PayOrderService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 支付控制器(OpenAPI)
|
||||
*/
|
||||
class PayController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected PayOrderService $payOrderService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
public function create(Request $request) {}
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
*/
|
||||
public function query(Request $request) {}
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*/
|
||||
public function close(Request $request) {}
|
||||
|
||||
/**
|
||||
* 订单退款
|
||||
*/
|
||||
public function refund(Request $request) {}
|
||||
/**
|
||||
* 异步通知
|
||||
*/
|
||||
public function notify(Request $request) {}
|
||||
}
|
||||
69
app/http/api/middleware/EpayAuthMiddleware.php
Normal file
69
app/http/api/middleware/EpayAuthMiddleware.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\api\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\exceptions\UnauthorizedException;
|
||||
use app\repositories\MerchantAppRepository;
|
||||
|
||||
/**
|
||||
* OpenAPI 签名认证中间件
|
||||
*
|
||||
* 验证 AppId + 签名
|
||||
*/
|
||||
class EpayAuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected MerchantAppRepository $merchantAppRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 延迟加载,避免循环依赖
|
||||
$this->merchantAppRepository = new MerchantAppRepository();
|
||||
}
|
||||
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$appId = $request->header('X-App-Id', '') ?: ($request->post('app_id', '') ?: $request->get('app_id', ''));
|
||||
$timestamp = $request->header('X-Timestamp', '') ?: ($request->post('timestamp', '') ?: $request->get('timestamp', ''));
|
||||
$nonce = $request->header('X-Nonce', '') ?: ($request->post('nonce', '') ?: $request->get('nonce', ''));
|
||||
$signature = $request->header('X-Signature', '') ?: ($request->post('signature', '') ?: $request->get('signature', ''));
|
||||
|
||||
if (empty($appId) || empty($timestamp) || empty($nonce) || empty($signature)) {
|
||||
throw new UnauthorizedException('缺少认证参数');
|
||||
}
|
||||
|
||||
// 验证时间戳(5分钟内有效)
|
||||
if (abs(time() - (int)$timestamp) > 300) {
|
||||
throw new UnauthorizedException('请求已过期');
|
||||
}
|
||||
|
||||
// 查询应用
|
||||
$app = $this->merchantAppRepository->findByAppId($appId);
|
||||
if (!$app) {
|
||||
throw new UnauthorizedException('应用不存在或已禁用');
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
$method = $request->method();
|
||||
$path = $request->path();
|
||||
$body = $request->rawBody();
|
||||
$bodySha256 = hash('sha256', $body);
|
||||
|
||||
$signString = "app_id={$appId}×tamp={$timestamp}&nonce={$nonce}&method={$method}&path={$path}&body_sha256={$bodySha256}";
|
||||
$expectedSignature = hash_hmac('sha256', $signString, $app->app_secret);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
throw new UnauthorizedException('签名验证失败');
|
||||
}
|
||||
|
||||
// 将应用信息注入到请求对象
|
||||
$request->app = $app;
|
||||
$request->merchantId = $app->merchant_id;
|
||||
$request->appId = $app->id;
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
|
||||
69
app/http/api/middleware/OpenApiAuthMiddleware.php
Normal file
69
app/http/api/middleware/OpenApiAuthMiddleware.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\api\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\exceptions\UnauthorizedException;
|
||||
use app\repositories\MerchantAppRepository;
|
||||
|
||||
/**
|
||||
* OpenAPI 签名认证中间件
|
||||
*
|
||||
* 验证 AppId + 签名
|
||||
*/
|
||||
class OpenApiAuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected MerchantAppRepository $merchantAppRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 延迟加载,避免循环依赖
|
||||
$this->merchantAppRepository = new MerchantAppRepository();
|
||||
}
|
||||
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$appId = $request->header('X-App-Id', '') ?: ($request->post('app_id', '') ?: $request->get('app_id', ''));
|
||||
$timestamp = $request->header('X-Timestamp', '') ?: ($request->post('timestamp', '') ?: $request->get('timestamp', ''));
|
||||
$nonce = $request->header('X-Nonce', '') ?: ($request->post('nonce', '') ?: $request->get('nonce', ''));
|
||||
$signature = $request->header('X-Signature', '') ?: ($request->post('signature', '') ?: $request->get('signature', ''));
|
||||
|
||||
if (empty($appId) || empty($timestamp) || empty($nonce) || empty($signature)) {
|
||||
throw new UnauthorizedException('缺少认证参数');
|
||||
}
|
||||
|
||||
// 验证时间戳(5分钟内有效)
|
||||
if (abs(time() - (int)$timestamp) > 300) {
|
||||
throw new UnauthorizedException('请求已过期');
|
||||
}
|
||||
|
||||
// 查询应用
|
||||
$app = $this->merchantAppRepository->findByAppId($appId);
|
||||
if (!$app) {
|
||||
throw new UnauthorizedException('应用不存在或已禁用');
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
$method = $request->method();
|
||||
$path = $request->path();
|
||||
$body = $request->rawBody();
|
||||
$bodySha256 = hash('sha256', $body);
|
||||
|
||||
$signString = "app_id={$appId}×tamp={$timestamp}&nonce={$nonce}&method={$method}&path={$path}&body_sha256={$bodySha256}";
|
||||
$expectedSignature = hash_hmac('sha256', $signString, $app->app_secret);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
throw new UnauthorizedException('签名验证失败');
|
||||
}
|
||||
|
||||
// 将应用信息注入到请求对象
|
||||
$request->app = $app;
|
||||
$request->merchantId = $app->merchant_id;
|
||||
$request->appId = $app->id;
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
|
||||
51
app/jobs/NotifyMerchantJob.php
Normal file
51
app/jobs/NotifyMerchantJob.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\jobs;
|
||||
|
||||
use app\models\PaymentNotifyTask;
|
||||
use app\repositories\PaymentNotifyTaskRepository;
|
||||
use app\services\NotifyService;
|
||||
use support\Log;
|
||||
|
||||
/**
|
||||
* 商户通知任务
|
||||
*
|
||||
* 异步发送支付结果通知给商户
|
||||
*/
|
||||
class NotifyMerchantJob
|
||||
{
|
||||
public function __construct(
|
||||
protected PaymentNotifyTaskRepository $notifyTaskRepository,
|
||||
protected NotifyService $notifyService
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$tasks = $this->notifyTaskRepository->getPendingRetryTasks(100);
|
||||
|
||||
foreach ($tasks as $taskData) {
|
||||
try {
|
||||
$task = $this->notifyTaskRepository->find($taskData['id']);
|
||||
if (!$task) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($task->retry_cnt >= 10) {
|
||||
$this->notifyTaskRepository->updateById($task->id, [
|
||||
'status' => PaymentNotifyTask::STATUS_FAIL,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->notifyService->sendNotify($task);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('通知任务处理失败', [
|
||||
'task_id' => $taskData['id'] ?? 0,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
app/models/Admin.php
Normal file
36
app/models/Admin.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 管理员模型
|
||||
*
|
||||
* 对应表:ma_admin
|
||||
*/
|
||||
class Admin extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_admin';
|
||||
|
||||
protected $fillable = [
|
||||
'user_name',
|
||||
'password',
|
||||
'nick_name',
|
||||
'avatar',
|
||||
'mobile',
|
||||
'email',
|
||||
'status',
|
||||
'login_ip',
|
||||
'login_at',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
'login_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $hidden = ['password'];
|
||||
}
|
||||
27
app/models/Merchant.php
Normal file
27
app/models/Merchant.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 商户模型
|
||||
*/
|
||||
class Merchant extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_merchant';
|
||||
|
||||
protected $fillable = [
|
||||
'merchant_no',
|
||||
'merchant_name',
|
||||
'funds_mode',
|
||||
'status',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
30
app/models/MerchantApp.php
Normal file
30
app/models/MerchantApp.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 商户应用模型
|
||||
*/
|
||||
class MerchantApp extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_merchant_app';
|
||||
|
||||
protected $fillable = [
|
||||
'merchant_id',
|
||||
'api_type',
|
||||
'app_id',
|
||||
'app_secret',
|
||||
'app_name',
|
||||
'status',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'merchant_id' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
33
app/models/PaymentCallbackLog.php
Normal file
33
app/models/PaymentCallbackLog.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 支付回调日志模型
|
||||
*
|
||||
* 对应表:ma_pay_callback_log
|
||||
*/
|
||||
class PaymentCallbackLog extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_pay_callback_log';
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'channel_id',
|
||||
'callback_type',
|
||||
'request_data',
|
||||
'verify_status',
|
||||
'process_status',
|
||||
'process_result',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'channel_id' => 'integer',
|
||||
'verify_status' => 'integer',
|
||||
'process_status' => 'integer',
|
||||
];
|
||||
}
|
||||
62
app/models/PaymentChannel.php
Normal file
62
app/models/PaymentChannel.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 支付通道模型
|
||||
*
|
||||
* 对应表:ma_pay_channel
|
||||
*/
|
||||
class PaymentChannel extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_pay_channel';
|
||||
|
||||
protected $fillable = [
|
||||
'merchant_id',
|
||||
'merchant_app_id',
|
||||
'chan_code',
|
||||
'chan_name',
|
||||
'plugin_code',
|
||||
'method_id',
|
||||
'config_json',
|
||||
'split_ratio',
|
||||
'chan_cost',
|
||||
'chan_mode',
|
||||
'daily_limit',
|
||||
'daily_cnt',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'merchant_id' => 'integer',
|
||||
'merchant_app_id' => 'integer',
|
||||
'method_id' => 'integer',
|
||||
'config_json' => 'array',
|
||||
'split_ratio' => 'decimal:2',
|
||||
'chan_cost' => 'decimal:2',
|
||||
'daily_limit' => 'decimal:2',
|
||||
'daily_cnt' => 'integer',
|
||||
'min_amount' => 'decimal:2',
|
||||
'max_amount' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
|
||||
public function getConfigArray(): array
|
||||
{
|
||||
return $this->config_json ?? [];
|
||||
}
|
||||
|
||||
public function getEnabledProducts(): array
|
||||
{
|
||||
$config = $this->getConfigArray();
|
||||
return $config['enabled_products'] ?? [];
|
||||
}
|
||||
}
|
||||
30
app/models/PaymentMethod.php
Normal file
30
app/models/PaymentMethod.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 支付方式模型
|
||||
*
|
||||
* 对应表:ma_pay_method
|
||||
*/
|
||||
class PaymentMethod extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_pay_method';
|
||||
|
||||
protected $fillable = [
|
||||
'method_code',
|
||||
'method_name',
|
||||
'icon',
|
||||
'sort',
|
||||
'status',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
42
app/models/PaymentNotifyTask.php
Normal file
42
app/models/PaymentNotifyTask.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 商户通知任务模型
|
||||
*
|
||||
* 对应表:ma_notify_task
|
||||
*/
|
||||
class PaymentNotifyTask extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_notify_task';
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'merchant_id',
|
||||
'merchant_app_id',
|
||||
'notify_url',
|
||||
'notify_data',
|
||||
'status',
|
||||
'retry_cnt',
|
||||
'next_retry_at',
|
||||
'last_notify_at',
|
||||
'last_response',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'merchant_id' => 'integer',
|
||||
'merchant_app_id' => 'integer',
|
||||
'retry_cnt' => 'integer',
|
||||
'next_retry_at' => 'datetime',
|
||||
'last_notify_at' => 'datetime',
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 'PENDING';
|
||||
const STATUS_SUCCESS = 'SUCCESS';
|
||||
const STATUS_FAIL = 'FAIL';
|
||||
}
|
||||
62
app/models/PaymentOrder.php
Normal file
62
app/models/PaymentOrder.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 支付订单模型
|
||||
*
|
||||
* 对应表:ma_pay_order
|
||||
*/
|
||||
class PaymentOrder extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_pay_order';
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'merchant_id',
|
||||
'merchant_app_id',
|
||||
'mch_order_no',
|
||||
'method_id',
|
||||
'channel_id',
|
||||
'amount',
|
||||
'real_amount',
|
||||
'fee',
|
||||
'currency',
|
||||
'subject',
|
||||
'body',
|
||||
'status',
|
||||
'chan_order_no',
|
||||
'chan_trade_no',
|
||||
'pay_at',
|
||||
'expire_at',
|
||||
'client_ip',
|
||||
'notify_stat',
|
||||
'notify_cnt',
|
||||
'extra',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'merchant_id' => 'integer',
|
||||
'merchant_app_id' => 'integer',
|
||||
'method_id' => 'integer',
|
||||
'channel_id' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'real_amount' => 'decimal:2',
|
||||
'fee' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'notify_stat' => 'integer',
|
||||
'notify_cnt' => 'integer',
|
||||
'extra' => 'array',
|
||||
'pay_at' => 'datetime',
|
||||
'expire_at' => 'datetime',
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 0;
|
||||
const STATUS_SUCCESS = 1;
|
||||
const STATUS_FAIL = 2;
|
||||
const STATUS_CLOSED = 3;
|
||||
}
|
||||
34
app/models/PaymentPlugin.php
Normal file
34
app/models/PaymentPlugin.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 支付插件模型
|
||||
*
|
||||
* 对应表:ma_pay_plugin(主键 plugin_code)
|
||||
*/
|
||||
class PaymentPlugin extends BaseModel
|
||||
{
|
||||
protected $table = 'ma_pay_plugin';
|
||||
|
||||
protected $primaryKey = 'plugin_code';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'plugin_code',
|
||||
'plugin_name',
|
||||
'class_name',
|
||||
'status',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 用户模型
|
||||
*
|
||||
* 对应表:users
|
||||
*/
|
||||
class User extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 表名
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* 关联角色(多对多)
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
app/repositories/AdminRepository.php
Normal file
31
app/repositories/AdminRepository.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\Admin;
|
||||
|
||||
/**
|
||||
* 管理员仓储
|
||||
*/
|
||||
class AdminRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Admin());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询
|
||||
*/
|
||||
public function findByUserName(string $userName): ?Admin
|
||||
{
|
||||
/** @var Admin|null $admin */
|
||||
$admin = $this->model
|
||||
->newQuery()
|
||||
->where('user_name', $userName)
|
||||
->first();
|
||||
|
||||
return $admin;
|
||||
}
|
||||
}
|
||||
41
app/repositories/MerchantAppRepository.php
Normal file
41
app/repositories/MerchantAppRepository.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\MerchantApp;
|
||||
|
||||
/**
|
||||
* 商户应用仓储
|
||||
*/
|
||||
class MerchantAppRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new MerchantApp());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据AppId查询
|
||||
*/
|
||||
public function findByAppId(string $appId): ?MerchantApp
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('app_id', $appId)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户ID和应用ID查询
|
||||
*/
|
||||
public function findByMerchantAndApp(int $merchantId, int $appId): ?MerchantApp
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('merchant_id', $merchantId)
|
||||
->where('id', $appId)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
28
app/repositories/MerchantRepository.php
Normal file
28
app/repositories/MerchantRepository.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\Merchant;
|
||||
|
||||
/**
|
||||
* 商户仓储
|
||||
*/
|
||||
class MerchantRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Merchant());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户号查询
|
||||
*/
|
||||
public function findByMerchantNo(string $merchantNo): ?Merchant
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('merchant_no', $merchantNo)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
22
app/repositories/PaymentCallbackLogRepository.php
Normal file
22
app/repositories/PaymentCallbackLogRepository.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentCallbackLog;
|
||||
|
||||
/**
|
||||
* 支付回调日志仓储
|
||||
*/
|
||||
class PaymentCallbackLogRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentCallbackLog());
|
||||
}
|
||||
|
||||
public function createLog(array $data): PaymentCallbackLog
|
||||
{
|
||||
return $this->model->newQuery()->create($data);
|
||||
}
|
||||
}
|
||||
38
app/repositories/PaymentChannelRepository.php
Normal file
38
app/repositories/PaymentChannelRepository.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentChannel;
|
||||
|
||||
/**
|
||||
* 支付通道仓储
|
||||
*/
|
||||
class PaymentChannelRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentChannel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户、应用、支付方式查找可用通道
|
||||
*/
|
||||
public function findAvailableChannel(int $merchantId, int $merchantAppId, int $methodId): ?PaymentChannel
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('merchant_id', $merchantId)
|
||||
->where('merchant_app_id', $merchantAppId)
|
||||
->where('method_id', $methodId)
|
||||
->where('status', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function findByChanCode(string $chanCode): ?PaymentChannel
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('chan_code', $chanCode)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
34
app/repositories/PaymentMethodRepository.php
Normal file
34
app/repositories/PaymentMethodRepository.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentMethod;
|
||||
|
||||
/**
|
||||
* 支付方式仓储
|
||||
*/
|
||||
class PaymentMethodRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentMethod());
|
||||
}
|
||||
|
||||
public function getAllEnabled(): array
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('status', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function findByCode(string $methodCode): ?PaymentMethod
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('method_code', $methodCode)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
34
app/repositories/PaymentNotifyTaskRepository.php
Normal file
34
app/repositories/PaymentNotifyTaskRepository.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentNotifyTask;
|
||||
|
||||
/**
|
||||
* 商户通知任务仓储
|
||||
*/
|
||||
class PaymentNotifyTaskRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentNotifyTask());
|
||||
}
|
||||
|
||||
public function findByOrderId(string $orderId): ?PaymentNotifyTask
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('order_id', $orderId)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function getPendingRetryTasks(int $limit = 100): array
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('status', PaymentNotifyTask::STATUS_PENDING)
|
||||
->where('next_retry_at', '<=', date('Y-m-d H:i:s'))
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
56
app/repositories/PaymentOrderRepository.php
Normal file
56
app/repositories/PaymentOrderRepository.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentOrder;
|
||||
|
||||
/**
|
||||
* 支付订单仓储
|
||||
*/
|
||||
class PaymentOrderRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentOrder());
|
||||
}
|
||||
|
||||
public function findByOrderId(string $orderId): ?PaymentOrder
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('order_id', $orderId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户订单号查询(幂等校验)
|
||||
*/
|
||||
public function findByMchNo(int $merchantId, int $merchantAppId, string $mchOrderNo): ?PaymentOrder
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('merchant_id', $merchantId)
|
||||
->where('merchant_app_id', $merchantAppId)
|
||||
->where('mch_order_no', $mchOrderNo)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function updateStatus(string $orderId, int $status, array $extra = []): bool
|
||||
{
|
||||
$data = array_merge(['status' => $status], $extra);
|
||||
$order = $this->findByOrderId($orderId);
|
||||
return $order ? $this->updateById($order->id, $data) : false;
|
||||
}
|
||||
|
||||
public function updateChannelInfo(string $orderId, string $chanOrderNo, string $chanTradeNo = ''): bool
|
||||
{
|
||||
$order = $this->findByOrderId($orderId);
|
||||
if (!$order) {
|
||||
return false;
|
||||
}
|
||||
$data = ['chan_order_no' => $chanOrderNo];
|
||||
if ($chanTradeNo !== '') {
|
||||
$data['chan_trade_no'] = $chanTradeNo;
|
||||
}
|
||||
return $this->updateById($order->id, $data);
|
||||
}
|
||||
}
|
||||
32
app/repositories/PaymentPluginRepository.php
Normal file
32
app/repositories/PaymentPluginRepository.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\PaymentPlugin;
|
||||
|
||||
/**
|
||||
* 支付插件仓储
|
||||
*/
|
||||
class PaymentPluginRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new PaymentPlugin());
|
||||
}
|
||||
|
||||
public function getActivePlugins()
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('status', 1)
|
||||
->get(['plugin_code', 'class_name']);
|
||||
}
|
||||
|
||||
public function findActiveByCode(string $pluginCode): ?PaymentPlugin
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->where('plugin_code', $pluginCode)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\User;
|
||||
|
||||
/**
|
||||
* 用户仓储
|
||||
*/
|
||||
class UserRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new User());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*/
|
||||
public function findByUserName(string $userName): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $this->model
|
||||
->newQuery()
|
||||
->where('user_name', $userName)
|
||||
->first();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查询并预加载角色
|
||||
*/
|
||||
public function findWithRoles(int $id): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $this->model
|
||||
->newQuery()
|
||||
->with('roles')
|
||||
->find($id);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,31 +9,43 @@
|
||||
|
||||
use Webman\Route;
|
||||
use app\http\admin\controller\AuthController;
|
||||
use app\http\admin\controller\UserController;
|
||||
use app\http\admin\controller\AdminController;
|
||||
use app\http\admin\controller\MenuController;
|
||||
use app\http\admin\controller\SystemController;
|
||||
use app\http\admin\controller\ChannelController;
|
||||
use app\http\admin\controller\PluginController;
|
||||
use app\common\middleware\Cors;
|
||||
use app\http\admin\middleware\AuthMiddleware;
|
||||
|
||||
Route::group('/adminapi', function () {
|
||||
// 认证相关(无需JWT验证)
|
||||
Route::get('/captcha', [AuthController::class, 'captcha']);
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::get('/captcha', [AuthController::class, 'captcha'])->name('captcha')->setParams(['real_name' => '验证码']);
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('login')->setParams(['real_name' => '登录']);
|
||||
|
||||
// 需要认证的路由组
|
||||
Route::group('', function () {
|
||||
// 用户相关(需要JWT验证)
|
||||
Route::get('/user/getUserInfo', [UserController::class, 'getUserInfo']);
|
||||
Route::get('/user/getUserInfo', [AdminController::class, 'getUserInfo'])->name('getUserInfo')->setParams(['real_name' => '获取管理员信息']);
|
||||
|
||||
// 菜单相关(需要JWT验证)
|
||||
Route::get('/menu/getRouters', [MenuController::class, 'getRouters']);
|
||||
Route::get('/menu/getRouters', [MenuController::class, 'getRouters'])->name('getRouters')->setParams(['real_name' => '获取菜单']);
|
||||
|
||||
// 系统相关(需要JWT验证)
|
||||
Route::get('/system/getDict[/{code}]', [SystemController::class, 'getDict']);
|
||||
Route::get('/system/getDict[/{code}]', [SystemController::class, 'getDict'])->name('getDict')->setParams(['real_name' => '获取字典']);
|
||||
|
||||
// 系统配置相关(需要JWT验证)
|
||||
Route::get('/system/base-config/tabs', [SystemController::class, 'getTabsConfig']);
|
||||
Route::get('/system/base-config/form/{tabKey}', [SystemController::class, 'getFormConfig']);
|
||||
Route::post('/system/base-config/submit/{tabKey}', [SystemController::class, 'submitConfig']);
|
||||
Route::get('/system/base-config/tabs', [SystemController::class, 'getTabsConfig'])->name('getTabsConfig')->setParams(['real_name' => '获取系统配置tabs']);
|
||||
Route::get('/system/base-config/form/{tabKey}', [SystemController::class, 'getFormConfig'])->name('getFormConfig')->setParams(['real_name' => '获取系统配置form']);
|
||||
Route::post('/system/base-config/submit/{tabKey}', [SystemController::class, 'submitConfig'])->name('submitConfig')->setParams(['real_name' => '提交系统配置']);
|
||||
|
||||
// 通道管理相关(需要JWT验证)
|
||||
Route::get('/channel/list', [ChannelController::class, 'list'])->name('list')->setParams(['real_name' => '获取通道列表']);
|
||||
Route::get('/channel/detail', [ChannelController::class, 'detail'])->name('detail')->setParams(['real_name' => '获取通道详情']);
|
||||
Route::post('/channel/save', [ChannelController::class, 'save'])->name('save')->setParams(['real_name' => '保存通道']);
|
||||
|
||||
// 插件管理相关(需要JWT验证)
|
||||
Route::get('/channel/plugins', [PluginController::class, 'plugins'])->name('plugins')->setParams(['real_name' => '获取插件列表']);
|
||||
Route::get('/channel/plugin/config-schema', [PluginController::class, 'configSchema'])->name('configSchema')->setParams(['real_name' => '获取插件配置schema']);
|
||||
Route::get('/channel/plugin/products', [PluginController::class, 'products'])->name('products')->setParams(['real_name' => '获取插件产品列表']);
|
||||
})->middleware([AuthMiddleware::class]);
|
||||
})->middleware([Cors::class]);
|
||||
@@ -1,8 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* API 路由定义
|
||||
* API 路由定义(易支付接口标准)
|
||||
*/
|
||||
|
||||
use Webman\Route;
|
||||
use app\http\api\controller\EpayController;
|
||||
use app\http\api\middleware\EpayAuthMiddleware;
|
||||
|
||||
Route::group('', function () {
|
||||
// 页面跳转支付
|
||||
Route::any('/submit.php', [EpayController::class, 'submit']);
|
||||
|
||||
// API接口支付
|
||||
Route::post('/mapi.php', [EpayController::class, 'mapi']);
|
||||
|
||||
// API接口
|
||||
Route::get('/api.php', [EpayController::class, 'api']);
|
||||
})->middleware([EpayAuthMiddleware::class]);
|
||||
|
||||
37
app/services/AdminService.php
Normal file
37
app/services/AdminService.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\repositories\AdminRepository;
|
||||
|
||||
/**
|
||||
* 管理员业务服务
|
||||
*/
|
||||
class AdminService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminRepository $adminRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取管理员信息
|
||||
*
|
||||
* @return array ['user' => array, 'roles' => array, 'permissions' => array]
|
||||
*/
|
||||
public function getInfoById(int $id): array
|
||||
{
|
||||
$admin = $this->adminRepository->find($id);
|
||||
if (!$admin) {
|
||||
throw new NotFoundException('管理员不存在');
|
||||
}
|
||||
|
||||
return [
|
||||
'user' => $admin->toArray(),
|
||||
'roles' => ['admin'],
|
||||
'permissions' => ['*:*:*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,25 @@ namespace app\services;
|
||||
use app\common\base\BaseService;
|
||||
use app\common\utils\JwtUtil;
|
||||
use app\exceptions\{BadRequestException, ForbiddenException, UnauthorizedException};
|
||||
use app\repositories\UserRepository;
|
||||
use app\models\Admin;
|
||||
use app\repositories\AdminRepository;
|
||||
use support\Cache;
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
*
|
||||
* 处理登录、token 生成等认证相关业务
|
||||
* 处理管理员登录、token 生成等认证相关业务
|
||||
*/
|
||||
class AuthService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $userRepository,
|
||||
protected AdminRepository $adminRepository,
|
||||
protected CaptchaService $captchaService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* 登录成功后返回 token,前端使用该 token 通过 Authorization 请求头访问需要认证的接口
|
||||
* 管理员登录
|
||||
*
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
@@ -34,110 +33,64 @@ class AuthService extends BaseService
|
||||
*/
|
||||
public function login(string $username, string $password, string $verifyCode, string $captchaId): array
|
||||
{
|
||||
// 1. 校验验证码
|
||||
if (!$this->captchaService->validate($captchaId, $verifyCode)) {
|
||||
throw new BadRequestException('验证码错误或已失效');
|
||||
}
|
||||
|
||||
// 2. 查询用户
|
||||
$user = $this->userRepository->findByUserName($username);
|
||||
if (!$user) {
|
||||
$admin = $this->adminRepository->findByUserName($username);
|
||||
if (!$admin) {
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
|
||||
// 3. 校验密码
|
||||
if (!$this->validatePassword($password, $user->password)) {
|
||||
if (!$this->validatePassword($password, $admin->password)) {
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if ($user->status !== 1) {
|
||||
if ($admin->status !== 1) {
|
||||
throw new ForbiddenException('账号已被禁用');
|
||||
}
|
||||
|
||||
// 5. 生成 JWT token(包含用户ID、用户名、昵称等信息)
|
||||
$token = $this->generateToken($user);
|
||||
$token = $this->generateToken($admin);
|
||||
$this->cacheToken($token, $admin->id);
|
||||
$this->updateLoginInfo($admin);
|
||||
|
||||
// 6. 将 token 信息存入 Redis(用于后续刷新、黑名单等)
|
||||
$this->cacheToken($token, $user->id);
|
||||
|
||||
// 7. 更新用户最后登录信息
|
||||
$this->updateLoginInfo($user);
|
||||
|
||||
// 返回 token,前端使用该 token 访问需要认证的接口
|
||||
return [
|
||||
'token' => $token,
|
||||
];
|
||||
return ['token' => $token];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码
|
||||
*
|
||||
* @param string $password 明文密码
|
||||
* @param string|null $hash 数据库中的密码hash
|
||||
* @return bool
|
||||
*/
|
||||
private function validatePassword(string $password, ?string $hash): bool
|
||||
{
|
||||
// 如果数据库密码为空,允许使用默认密码(仅用于开发/演示)
|
||||
if ($hash === null || $hash === '') {
|
||||
// 开发环境:允许 admin/123456 和 common/123456 无密码登录
|
||||
// 生产环境应移除此逻辑
|
||||
return in_array($password, ['123456'], true);
|
||||
}
|
||||
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT token
|
||||
*
|
||||
* @param \app\models\User $user
|
||||
* @return string
|
||||
*/
|
||||
private function generateToken($user): string
|
||||
private function generateToken(Admin $admin): string
|
||||
{
|
||||
$payload = [
|
||||
'user_id' => $user->id,
|
||||
'user_name' => $user->user_name,
|
||||
'nick_name' => $user->nick_name,
|
||||
'user_id' => $admin->id,
|
||||
'user_name' => $admin->user_name,
|
||||
'nick_name' => $admin->nick_name,
|
||||
];
|
||||
|
||||
return JwtUtil::generateToken($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 token 信息缓存到 Redis
|
||||
*
|
||||
* @param string $token
|
||||
* @param int $userId
|
||||
*/
|
||||
private function cacheToken(string $token, int $userId): void
|
||||
private function cacheToken(string $token, int $adminId): void
|
||||
{
|
||||
$key = JwtUtil::getCachePrefix() . $token;
|
||||
$data = [
|
||||
'user_id' => $userId,
|
||||
'created_at' => time(),
|
||||
];
|
||||
$data = ['user_id' => $adminId, 'created_at' => time()];
|
||||
Cache::set($key, $data, JwtUtil::getTtl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户登录信息
|
||||
*
|
||||
* @param \app\models\User $user
|
||||
*/
|
||||
private function updateLoginInfo($user): void
|
||||
private function updateLoginInfo(Admin $admin): void
|
||||
{
|
||||
// 获取客户端真实IP(优先使用 x-real-ip,其次 x-forwarded-for,最后 remoteIp)
|
||||
$request = request();
|
||||
$ip = $request->header('x-real-ip', '')
|
||||
?: ($request->header('x-forwarded-for', '') ? explode(',', $request->header('x-forwarded-for', ''))[0] : '')
|
||||
$ip = $request->header('x-real-ip', '')
|
||||
?: ($request->header('x-forwarded-for', '') ? trim(explode(',', $request->header('x-forwarded-for', ''))[0]) : '')
|
||||
?: $request->getRemoteIp();
|
||||
|
||||
$user->login_ip = trim($ip);
|
||||
$user->login_at = date('Y-m-d H:i:s');
|
||||
$user->save();
|
||||
$admin->login_ip = trim($ip);
|
||||
$admin->login_at = date('Y-m-d H:i:s');
|
||||
$admin->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
app/services/ChannelRouterService.php
Normal file
42
app/services/ChannelRouterService.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\models\PaymentChannel;
|
||||
use app\repositories\PaymentChannelRepository;
|
||||
|
||||
/**
|
||||
* 通道路由服务
|
||||
*
|
||||
* 负责根据商户、应用、支付方式选择合适的通道
|
||||
*/
|
||||
class ChannelRouterService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected PaymentChannelRepository $channelRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择通道
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $merchantAppId 商户应用ID
|
||||
* @param int $methodId 支付方式ID
|
||||
* @return PaymentChannel
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function chooseChannel(int $merchantId, int $merchantAppId, int $methodId): PaymentChannel
|
||||
{
|
||||
$channel = $this->channelRepository->findAvailableChannel($merchantId, $merchantAppId, $methodId);
|
||||
|
||||
if (!$channel) {
|
||||
throw new NotFoundException("未找到可用的支付通道:商户ID={$merchantId}, 应用ID={$merchantAppId}, 支付方式ID={$methodId}");
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
}
|
||||
|
||||
121
app/services/NotifyService.php
Normal file
121
app/services/NotifyService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\models\PaymentNotifyTask;
|
||||
use app\repositories\{PaymentNotifyTaskRepository, PaymentOrderRepository};
|
||||
use support\Log;
|
||||
|
||||
/**
|
||||
* 商户通知服务
|
||||
*
|
||||
* 负责向商户发送支付结果通知
|
||||
*/
|
||||
class NotifyService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected PaymentNotifyTaskRepository $notifyTaskRepository,
|
||||
protected PaymentOrderRepository $orderRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知任务
|
||||
* notify_url 从订单 extra 中获取(下单时由请求传入)
|
||||
*/
|
||||
public function createNotifyTask(string $orderId): void
|
||||
{
|
||||
$order = $this->orderRepository->findByOrderId($orderId);
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->notifyTaskRepository->findByOrderId($orderId);
|
||||
if ($existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notifyUrl = $order->extra['notify_url'] ?? '';
|
||||
if (empty($notifyUrl)) {
|
||||
Log::warning('订单缺少 notify_url,跳过创建通知任务', ['order_id' => $orderId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->notifyTaskRepository->create([
|
||||
'order_id' => $orderId,
|
||||
'merchant_id' => $order->merchant_id,
|
||||
'merchant_app_id' => $order->merchant_app_id,
|
||||
'notify_url' => $notifyUrl,
|
||||
'notify_data' => json_encode([
|
||||
'order_id' => $order->order_id,
|
||||
'mch_order_no' => $order->mch_order_no,
|
||||
'status' => $order->status,
|
||||
'amount' => $order->amount,
|
||||
'pay_time' => $order->pay_at,
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'status' => PaymentNotifyTask::STATUS_PENDING,
|
||||
'retry_cnt' => 0,
|
||||
'next_retry_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*/
|
||||
public function sendNotify(PaymentNotifyTask $task): bool
|
||||
{
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $task->notify_url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $task->notify_data);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$success = ($httpCode === 200 && strtolower(trim($response)) === 'success');
|
||||
|
||||
$this->notifyTaskRepository->updateById($task->id, [
|
||||
'status' => $success ? PaymentNotifyTask::STATUS_SUCCESS : PaymentNotifyTask::STATUS_PENDING,
|
||||
'retry_cnt' => $task->retry_cnt + 1,
|
||||
'last_notify_at' => date('Y-m-d H:i:s'),
|
||||
'last_response' => $response,
|
||||
'next_retry_at' => $success ? null : $this->calculateNextRetryTime($task->retry_cnt + 1),
|
||||
]);
|
||||
|
||||
return $success;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('发送通知失败', [
|
||||
'task_id' => $task->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->notifyTaskRepository->updateById($task->id, [
|
||||
'retry_cnt' => $task->retry_cnt + 1,
|
||||
'last_notify_at' => date('Y-m-d H:i:s'),
|
||||
'last_response' => $e->getMessage(),
|
||||
'next_retry_at' => $this->calculateNextRetryTime($task->retry_cnt + 1),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下次重试时间(指数退避)
|
||||
*/
|
||||
private function calculateNextRetryTime(int $retryCount): string
|
||||
{
|
||||
$intervals = [60, 300, 900, 3600]; // 1分钟、5分钟、15分钟、1小时
|
||||
$interval = $intervals[min($retryCount - 1, count($intervals) - 1)] ?? 3600;
|
||||
return date('Y-m-d H:i:s', time() + $interval);
|
||||
}
|
||||
}
|
||||
|
||||
182
app/services/PayOrderService.php
Normal file
182
app/services/PayOrderService.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exceptions\{BadRequestException, NotFoundException};
|
||||
use app\models\PaymentOrder;
|
||||
use app\repositories\{MerchantAppRepository, PaymentChannelRepository, PaymentMethodRepository, PaymentOrderRepository};
|
||||
|
||||
/**
|
||||
* 支付订单服务
|
||||
*
|
||||
* 负责订单创建、统一下单、状态管理等
|
||||
*/
|
||||
class PayOrderService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantAppRepository $merchantAppRepository,
|
||||
protected PaymentChannelRepository $channelRepository,
|
||||
protected PaymentOrderRepository $orderRepository,
|
||||
protected PaymentMethodRepository $methodRepository,
|
||||
protected PluginService $pluginService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
public function createOrder(array $data)
|
||||
{
|
||||
// 1. 基本参数校验
|
||||
$mchId = (int)($data['mch_id'] ?? $data['merchant_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);
|
||||
$subject = trim((string)($data['subject'] ?? ''));
|
||||
|
||||
if ($mchId <= 0 || $appId <= 0) {
|
||||
throw new BadRequestException('商户或应用信息不完整');
|
||||
}
|
||||
if ($mchNo === '') {
|
||||
throw new BadRequestException('商户订单号不能为空');
|
||||
}
|
||||
if ($methodCode === '') {
|
||||
throw new BadRequestException('支付方式不能为空');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
throw new BadRequestException('订单金额必须大于0');
|
||||
}
|
||||
if ($subject === '') {
|
||||
throw new BadRequestException('订单标题不能为空');
|
||||
}
|
||||
|
||||
// 2. 查询支付方式ID
|
||||
$method = $this->methodRepository->findByCode($methodCode);
|
||||
if (!$method) {
|
||||
throw new BadRequestException('支付方式不存在');
|
||||
}
|
||||
|
||||
// 3. 幂等校验:同一商户应用下相同商户订单号只保留一条
|
||||
$existing = $this->orderRepository->findByMchNo($mchId, $appId, $mchNo);
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
// 4. 生成系统订单号
|
||||
$orderId = $this->generateOrderId();
|
||||
|
||||
// 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'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款(供易支付等接口调用)
|
||||
*
|
||||
* @param array $data
|
||||
* - order_id: 系统订单号(必填)
|
||||
* - refund_amount: 退款金额(必填)
|
||||
* - refund_reason: 退款原因(可选)
|
||||
* @return array
|
||||
*/
|
||||
public function refundOrder(array $data): array
|
||||
{
|
||||
$orderId = (string)($data['order_id'] ?? $data['pay_order_id'] ?? '');
|
||||
$refundAmount = (float)($data['refund_amount'] ?? 0);
|
||||
|
||||
if ($orderId === '') {
|
||||
throw new BadRequestException('订单号不能为空');
|
||||
}
|
||||
if ($refundAmount <= 0) {
|
||||
throw new BadRequestException('退款金额必须大于0');
|
||||
}
|
||||
|
||||
// 1. 查询订单
|
||||
$order = $this->orderRepository->findByOrderId($orderId);
|
||||
if (!$order) {
|
||||
throw new NotFoundException('订单不存在');
|
||||
}
|
||||
|
||||
// 2. 验证订单状态
|
||||
if ($order->status !== PaymentOrder::STATUS_SUCCESS) {
|
||||
throw new BadRequestException('订单状态不允许退款');
|
||||
}
|
||||
|
||||
// 3. 验证退款金额
|
||||
if ($refundAmount > $order->amount) {
|
||||
throw new BadRequestException('退款金额不能大于订单金额');
|
||||
}
|
||||
|
||||
// 4. 查询通道
|
||||
$channel = $this->channelRepository->find($order->channel_id);
|
||||
if (!$channel) {
|
||||
throw new NotFoundException('支付通道不存在');
|
||||
}
|
||||
|
||||
// 5. 查询支付方式
|
||||
$method = $this->methodRepository->find($order->method_id);
|
||||
if (!$method) {
|
||||
throw new NotFoundException('支付方式不存在');
|
||||
}
|
||||
|
||||
// 6. 实例化插件并初始化(通过插件服务)
|
||||
$plugin = $this->pluginService->getPluginInstance($channel->plugin_code);
|
||||
|
||||
$channelConfig = array_merge(
|
||||
$channel->getConfigArray(),
|
||||
['enabled_products' => $channel->getEnabledProducts()]
|
||||
);
|
||||
$plugin->init($method->method_code, $channelConfig);
|
||||
|
||||
// 7. 调用插件退款
|
||||
$refundData = [
|
||||
'order_id' => $order->order_id,
|
||||
'chan_order_no' => $order->chan_order_no,
|
||||
'chan_trade_no' => $order->chan_trade_no,
|
||||
'refund_amount' => $refundAmount,
|
||||
'refund_reason' => $data['refund_reason'] ?? '',
|
||||
];
|
||||
|
||||
$refundResult = $plugin->refund($refundData, $channelConfig);
|
||||
|
||||
// 8. 如果是全额退款则关闭订单
|
||||
if ($refundAmount >= $order->amount) {
|
||||
$this->orderRepository->updateById($order->id, [
|
||||
'status' => PaymentOrder::STATUS_CLOSED,
|
||||
'extra' => array_merge($order->extra ?? [], [
|
||||
'refund_info' => $refundResult,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'order_id' => $order->order_id,
|
||||
'refund_amount' => $refundAmount,
|
||||
'refund_result' => $refundResult,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付订单号
|
||||
*/
|
||||
private function generateOrderId(): string
|
||||
{
|
||||
return 'P' . date('YmdHis') . mt_rand(100000, 999999);
|
||||
}
|
||||
}
|
||||
161
app/services/PayService.php
Normal file
161
app/services/PayService.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\models\PaymentOrder;
|
||||
use app\repositories\{PaymentMethodRepository, PaymentOrderRepository};
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 支付服务
|
||||
*
|
||||
* 负责聚合支付流程:通道路由、插件调用、订单更新等
|
||||
*/
|
||||
class PayService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected PayOrderService $payOrderService,
|
||||
protected ChannelRouterService $channelRouterService,
|
||||
protected PaymentOrderRepository $orderRepository,
|
||||
protected PaymentMethodRepository $methodRepository,
|
||||
protected PluginService $pluginService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 统一支付:创建订单(含幂等)、选择通道、调用插件统一下单
|
||||
*
|
||||
* @param array $orderData 内部订单数据
|
||||
* - mch_id, app_id, mch_no, method_code, amount, subject, body, client_ip, extra...
|
||||
* @param array $options 额外选项
|
||||
* - device: 设备类型(pc/mobile/wechat/alipay/qq/jump)
|
||||
* - request: Request 对象(用于从 UA 检测环境)
|
||||
* @return array
|
||||
* - order_id
|
||||
* - mch_no
|
||||
* - pay_params
|
||||
*/
|
||||
public function unifiedPay(array $orderData, array $options = []): array
|
||||
{
|
||||
// 1. 创建订单(幂等)
|
||||
/** @var PaymentOrder $order */
|
||||
$order = $this->payOrderService->createOrder($orderData);
|
||||
|
||||
// 2. 查询支付方式
|
||||
$method = $this->methodRepository->find($order->method_id);
|
||||
if (!$method) {
|
||||
throw new NotFoundException('支付方式不存在');
|
||||
}
|
||||
|
||||
// 3. 通道路由
|
||||
$channel = $this->channelRouterService->chooseChannel(
|
||||
$order->merchant_id,
|
||||
$order->merchant_app_id,
|
||||
$order->method_id
|
||||
);
|
||||
|
||||
// 4. 实例化插件并初始化(通过插件服务)
|
||||
$plugin = $this->pluginService->getPluginInstance($channel->plugin_code);
|
||||
|
||||
$channelConfig = array_merge(
|
||||
$channel->getConfigArray(),
|
||||
['enabled_products' => $channel->getEnabledProducts()]
|
||||
);
|
||||
$plugin->init($method->method_code, $channelConfig);
|
||||
|
||||
// 5. 环境检测
|
||||
$device = $options['device'] ?? '';
|
||||
/** @var Request|null $request */
|
||||
$request = $options['request'] ?? null;
|
||||
|
||||
if ($device) {
|
||||
$env = $this->mapDeviceToEnv($device);
|
||||
} elseif ($request instanceof Request) {
|
||||
$env = $this->detectEnvironment($request);
|
||||
} else {
|
||||
$env = AbstractPayPlugin::ENV_PC;
|
||||
}
|
||||
|
||||
// 6. 调用插件统一下单
|
||||
$pluginOrderData = [
|
||||
'order_id' => $order->order_id,
|
||||
'mch_no' => $order->mch_order_no,
|
||||
'amount' => $order->amount,
|
||||
'subject' => $order->subject,
|
||||
'body' => $order->body,
|
||||
];
|
||||
|
||||
$payResult = $plugin->unifiedOrder($pluginOrderData, $channelConfig, $env);
|
||||
|
||||
// 7. 计算实际支付金额(扣除手续费)
|
||||
$fee = $order->fee > 0 ? $order->fee : ($order->amount * ($channel->chan_cost / 100));
|
||||
$realAmount = $order->amount - $fee;
|
||||
|
||||
// 8. 更新订单(通道、支付参数、实际金额)
|
||||
$extra = $order->extra ?? [];
|
||||
$extra['pay_params'] = $payResult['pay_params'] ?? null;
|
||||
$chanOrderNo = $payResult['chan_order_no'] ?? $payResult['channel_order_no'] ?? '';
|
||||
$chanTradeNo = $payResult['chan_trade_no'] ?? $payResult['channel_trade_no'] ?? '';
|
||||
|
||||
$this->orderRepository->updateById($order->id, [
|
||||
'channel_id' => $channel->id,
|
||||
'chan_order_no' => $chanOrderNo,
|
||||
'chan_trade_no' => $chanTradeNo,
|
||||
'real_amount' => $realAmount,
|
||||
'fee' => $fee,
|
||||
'extra' => $extra,
|
||||
]);
|
||||
|
||||
return [
|
||||
'order_id' => $order->order_id,
|
||||
'mch_no' => $order->mch_order_no,
|
||||
'pay_params' => $payResult['pay_params'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据请求 UA 检测环境
|
||||
*/
|
||||
private function detectEnvironment(Request $request): string
|
||||
{
|
||||
$ua = strtolower($request->header('User-Agent', ''));
|
||||
|
||||
if (strpos($ua, 'alipayclient') !== false) {
|
||||
return AbstractPayPlugin::ENV_ALIPAY_CLIENT;
|
||||
}
|
||||
|
||||
if (strpos($ua, 'micromessenger') !== false) {
|
||||
return AbstractPayPlugin::ENV_WECHAT;
|
||||
}
|
||||
|
||||
$mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
|
||||
foreach ($mobileKeywords as $keyword) {
|
||||
if (strpos($ua, $keyword) !== false) {
|
||||
return AbstractPayPlugin::ENV_H5;
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractPayPlugin::ENV_PC;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射设备类型到环境代码
|
||||
*/
|
||||
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,
|
||||
];
|
||||
|
||||
return $mapping[strtolower($device)] ?? AbstractPayPlugin::ENV_PC;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
122
app/services/PluginService.php
Normal file
122
app/services/PluginService.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\contracts\AbstractPayPlugin;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\repositories\PaymentPluginRepository;
|
||||
|
||||
/**
|
||||
* 插件服务
|
||||
*
|
||||
* 负责与支付插件注册表和具体插件交互,供后台控制器等调用
|
||||
*/
|
||||
class PluginService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected PaymentPluginRepository $pluginRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用插件列表
|
||||
*
|
||||
* @return array<array{code:string,name:string,supported_methods:array}>
|
||||
*/
|
||||
public function listPlugins(): array
|
||||
{
|
||||
$rows = $this->pluginRepository->getActivePlugins();
|
||||
|
||||
$plugins = [];
|
||||
foreach ($rows as $row) {
|
||||
$pluginCode = $row->plugin_code;
|
||||
|
||||
try {
|
||||
$plugin = $this->resolvePlugin($pluginCode, $row->class_name);
|
||||
$plugins[] = [
|
||||
'code' => $pluginCode,
|
||||
'name' => $plugin::getName(),
|
||||
'supported_methods'=> $plugin::getSupportedMethods(),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略无法实例化的插件
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件配置 Schema
|
||||
*/
|
||||
public function getConfigSchema(string $pluginCode, string $methodCode): array
|
||||
{
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
return $plugin::getConfigSchema($methodCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件支持的支付产品列表
|
||||
*/
|
||||
public function getSupportedProducts(string $pluginCode, string $methodCode): array
|
||||
{
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
return $plugin::getSupportedProducts($methodCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表单数据中提取插件配置参数(根据插件 Schema)
|
||||
*/
|
||||
public function buildConfigFromForm(string $pluginCode, string $methodCode, array $formData): array
|
||||
{
|
||||
$plugin = $this->getPluginInstance($pluginCode);
|
||||
$configSchema = $plugin::getConfigSchema($methodCode);
|
||||
|
||||
$configJson = [];
|
||||
if (isset($configSchema['fields']) && is_array($configSchema['fields'])) {
|
||||
foreach ($configSchema['fields'] as $field) {
|
||||
$fieldName = $field['field'] ?? '';
|
||||
if ($fieldName && array_key_exists($fieldName, $formData)) {
|
||||
$configJson[$fieldName] = $formData[$fieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $configJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外统一提供:根据插件编码获取插件实例
|
||||
*/
|
||||
public function getPluginInstance(string $pluginCode): AbstractPayPlugin
|
||||
{
|
||||
$row = $this->pluginRepository->findActiveByCode($pluginCode);
|
||||
if (!$row) {
|
||||
throw new NotFoundException('支付插件未注册或已禁用:' . $pluginCode);
|
||||
}
|
||||
|
||||
return $this->resolvePlugin($pluginCode, $row->class_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据插件编码和 class_name 解析并实例化插件
|
||||
*/
|
||||
private function resolvePlugin(string $pluginCode, ?string $className = null): AbstractPayPlugin
|
||||
{
|
||||
$class = $className ?: 'app\\common\\payment\\' . ucfirst($pluginCode) . 'Payment';
|
||||
|
||||
if (!class_exists($class)) {
|
||||
throw new NotFoundException('支付插件类不存在:' . $class);
|
||||
}
|
||||
|
||||
$plugin = new $class();
|
||||
if (!$plugin instanceof AbstractPayPlugin) {
|
||||
throw new NotFoundException('支付插件类型错误:' . $class);
|
||||
}
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constants\RoleCode;
|
||||
use app\exceptions\NotFoundException;
|
||||
use app\repositories\UserRepository;
|
||||
|
||||
/**
|
||||
* 用户相关业务服务示例
|
||||
*/
|
||||
class UserService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $users
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取用户信息(附带角色与权限)
|
||||
*
|
||||
* 返回结构尽量与前端 mock 的 /user/getUserInfo 保持一致:
|
||||
* {
|
||||
* "user": {...}, // 用户信息,roles 字段为角色对象数组
|
||||
* "roles": ["admin"], // 角色 code 数组
|
||||
* "permissions": ["*:*:*"] // 权限标识数组
|
||||
* }
|
||||
*/
|
||||
public function getUserInfoById(int $id): array
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
if (!$user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
$userArray = $user->toArray();
|
||||
|
||||
return [
|
||||
'user' => $userArray,
|
||||
'roles' => ['admin'],
|
||||
'permissions' => ['*:*:*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
288
app/services/api/EpayService.php
Normal file
288
app/services/api/EpayService.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\api;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\services\PayOrderService;
|
||||
use app\services\PayService;
|
||||
use app\repositories\{MerchantAppRepository, PaymentMethodRepository, PaymentOrderRepository};
|
||||
use app\models\PaymentOrder;
|
||||
use app\exceptions\{BadRequestException, NotFoundException};
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 易支付服务
|
||||
*/
|
||||
class EpayService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected PayOrderService $payOrderService,
|
||||
protected MerchantAppRepository $merchantAppRepository,
|
||||
protected PaymentOrderRepository $orderRepository,
|
||||
protected PaymentMethodRepository $methodRepository,
|
||||
protected PayService $payService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 页面跳转支付(submit.php)
|
||||
*
|
||||
* @param array $data 已通过验证的请求参数
|
||||
* @param Request $request 请求对象(用于环境检测)
|
||||
* @return array 包含 pay_order_id 与 pay_params
|
||||
*/
|
||||
public function submit(array $data, Request $request): array
|
||||
{
|
||||
// type 在文档中可选,这里如果为空暂不支持收银台模式
|
||||
if (empty($data['type'])) {
|
||||
throw new BadRequestException('暂不支持收银台模式,请指定支付方式 type');
|
||||
}
|
||||
|
||||
return $this->createUnifiedOrder($data, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* API 接口支付(mapi.php)
|
||||
*
|
||||
* @param array $data
|
||||
* @param Request $request
|
||||
* @return array 符合易支付文档的返回结构
|
||||
*/
|
||||
public function mapi(array $data, Request $request): array
|
||||
{
|
||||
$result = $this->createUnifiedOrder($data, $request);
|
||||
$payParams = $result['pay_params'] ?? [];
|
||||
|
||||
$response = [
|
||||
'code' => 1,
|
||||
'msg' => 'success',
|
||||
'trade_no' => $result['order_id'],
|
||||
];
|
||||
|
||||
if (!empty($payParams['type'])) {
|
||||
switch ($payParams['type']) {
|
||||
case 'redirect':
|
||||
$response['payurl'] = $payParams['url'] ?? '';
|
||||
break;
|
||||
case 'qrcode':
|
||||
$response['qrcode'] = $payParams['qrcode_url'] ?? $payParams['qrcode_data'] ?? '';
|
||||
break;
|
||||
case 'jsapi':
|
||||
if (!empty($payParams['urlscheme'])) {
|
||||
$response['urlscheme'] = $payParams['urlscheme'];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 不识别的类型不返回额外字段
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 接口(api.php)- 处理 act=order / refund 等
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function api(array $data): array
|
||||
{
|
||||
$act = strtolower($data['act'] ?? '');
|
||||
|
||||
return match ($act) {
|
||||
'order' => $this->apiOrder($data),
|
||||
'refund' => $this->apiRefund($data),
|
||||
default => [
|
||||
'code' => 0,
|
||||
'msg' => '不支持的操作类型',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* api.php?act=order 查询单个订单
|
||||
*/
|
||||
private function apiOrder(array $data): array
|
||||
{
|
||||
$pid = (int)($data['pid'] ?? 0);
|
||||
$key = (string)($data['key'] ?? '');
|
||||
|
||||
if ($pid <= 0 || $key === '') {
|
||||
throw new BadRequestException('商户参数错误');
|
||||
}
|
||||
|
||||
$app = $this->merchantAppRepository->findByAppId((string)$pid);
|
||||
if (!$app || $app->app_secret !== $key) {
|
||||
throw new NotFoundException('商户不存在或密钥错误');
|
||||
}
|
||||
|
||||
$tradeNo = $data['trade_no'] ?? '';
|
||||
$outTradeNo = $data['out_trade_no'] ?? '';
|
||||
|
||||
if ($tradeNo === '' && $outTradeNo === '') {
|
||||
throw new BadRequestException('系统订单号与商户订单号不能同时为空');
|
||||
}
|
||||
|
||||
if ($tradeNo !== '') {
|
||||
$order = $this->orderRepository->findByOrderId($tradeNo);
|
||||
} else {
|
||||
$order = $this->orderRepository->findByMchNo($app->merchant_id, $app->id, $outTradeNo);
|
||||
}
|
||||
|
||||
if (!$order) {
|
||||
throw new NotFoundException('订单不存在');
|
||||
}
|
||||
|
||||
$methodCode = $this->getMethodCodeByOrder($order);
|
||||
|
||||
return [
|
||||
'code' => 1,
|
||||
'msg' => '查询订单号成功!',
|
||||
'trade_no' => $order->order_id,
|
||||
'out_trade_no' => $order->mch_order_no,
|
||||
'api_trade_no' => $order->chan_trade_no ?? '',
|
||||
'type' => $this->mapMethodToEpayType($methodCode),
|
||||
'pid' => (int)$pid,
|
||||
'addtime' => $order->created_at,
|
||||
'endtime' => $order->pay_at,
|
||||
'name' => $order->subject,
|
||||
'money' => (string)$order->amount,
|
||||
'status' => $order->status === PaymentOrder::STATUS_SUCCESS ? 1 : 0,
|
||||
'param' => $order->extra['param'] ?? '',
|
||||
'buyer' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* api.php?act=refund 提交订单退款
|
||||
*/
|
||||
private function apiRefund(array $data): array
|
||||
{
|
||||
$pid = (int)($data['pid'] ?? 0);
|
||||
$key = (string)($data['key'] ?? '');
|
||||
|
||||
if ($pid <= 0 || $key === '') {
|
||||
throw new BadRequestException('商户参数错误');
|
||||
}
|
||||
|
||||
$app = $this->merchantAppRepository->findByAppId((string)$pid);
|
||||
if (!$app || $app->app_secret !== $key) {
|
||||
throw new NotFoundException('商户不存在或密钥错误');
|
||||
}
|
||||
|
||||
$tradeNo = $data['trade_no'] ?? '';
|
||||
$outTradeNo = $data['out_trade_no'] ?? '';
|
||||
$money = (float)($data['money'] ?? 0);
|
||||
|
||||
if ($tradeNo === '' && $outTradeNo === '') {
|
||||
throw new BadRequestException('系统订单号与商户订单号不能同时为空');
|
||||
}
|
||||
if ($money <= 0) {
|
||||
throw new BadRequestException('退款金额必须大于0');
|
||||
}
|
||||
|
||||
if ($tradeNo !== '') {
|
||||
$order = $this->orderRepository->findByOrderId($tradeNo);
|
||||
} else {
|
||||
$order = $this->orderRepository->findByMchNo($app->merchant_id, $app->id, $outTradeNo);
|
||||
}
|
||||
|
||||
if (!$order) {
|
||||
throw new NotFoundException('订单不存在');
|
||||
}
|
||||
|
||||
$refundResult = $this->payOrderService->refundOrder([
|
||||
'order_id' => $order->order_id,
|
||||
'refund_amount' => $money,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '退款成功',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单并调用插件统一下单
|
||||
*
|
||||
* @param array $data
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
private function createUnifiedOrder(array $data, Request $request): array
|
||||
{
|
||||
$pid = (int)($data['pid'] ?? 0);
|
||||
if ($pid <= 0) {
|
||||
throw new BadRequestException('商户ID不能为空');
|
||||
}
|
||||
|
||||
// 根据 pid 映射应用(约定 pid = app_id)
|
||||
$app = $this->merchantAppRepository->findByAppId((string)$pid);
|
||||
if (!$app || $app->status !== 1) {
|
||||
throw new NotFoundException('商户应用不存在或已禁用');
|
||||
}
|
||||
|
||||
$methodCode = $this->mapEpayTypeToMethod($data['type'] ?? '');
|
||||
$orderData = [
|
||||
'merchant_id' => $app->merchant_id,
|
||||
'app_id' => $app->id,
|
||||
'mch_order_no' => $data['out_trade_no'],
|
||||
'method_code' => $methodCode,
|
||||
'amount' => (float)$data['money'],
|
||||
'currency' => 'CNY',
|
||||
'subject' => $data['name'],
|
||||
'body' => $data['name'],
|
||||
'client_ip' => $data['clientip'] ?? $request->getRemoteIp(),
|
||||
'extra' => [
|
||||
'param' => $data['param'] ?? '',
|
||||
'notify_url' => $data['notify_url'] ?? '',
|
||||
'return_url' => $data['return_url'] ?? '',
|
||||
],
|
||||
];
|
||||
|
||||
// 调用通用支付服务完成通道选择与插件下单
|
||||
return $this->payService->unifiedPay($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单获取支付方式编码
|
||||
*/
|
||||
private function getMethodCodeByOrder(PaymentOrder $order): string
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
122
app/validation/EpayValidator.php
Normal file
122
app/validation/EpayValidator.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\validation;
|
||||
|
||||
use support\validation\Validator;
|
||||
|
||||
/**
|
||||
* 易支付参数验证器
|
||||
*
|
||||
* 根据 doc/epay.md 定义各接口所需参数规则
|
||||
*/
|
||||
class EpayValidator extends Validator
|
||||
{
|
||||
/**
|
||||
* 通用规则定义
|
||||
*
|
||||
* 通过场景选择实际需要的字段
|
||||
*/
|
||||
protected array $rules = [
|
||||
// 基础认证相关
|
||||
'pid' => 'required|integer',
|
||||
'key' => 'sometimes|string',
|
||||
|
||||
// 支付相关
|
||||
'type' => 'sometimes|string',
|
||||
'out_trade_no' => 'required|string|max:64',
|
||||
'trade_no' => 'sometimes|string|max:64',
|
||||
'notify_url' => 'required|url|max:255',
|
||||
'return_url' => 'sometimes|url|max:255',
|
||||
'name' => 'required|string|max:127',
|
||||
'money' => 'required|numeric|min:0.01',
|
||||
'clientip' => 'sometimes|ip',
|
||||
'device' => 'sometimes|string|in:pc,mobile,qq,wechat,alipay,jump',
|
||||
'param' => 'sometimes|string|max:255',
|
||||
|
||||
// 签名相关
|
||||
'sign' => 'required|string|size:32',
|
||||
'sign_type' => 'required|string|in:MD5,md5',
|
||||
|
||||
// API 动作
|
||||
'act' => 'required|string',
|
||||
'limit' => 'sometimes|integer|min:1|max:50',
|
||||
'page' => 'sometimes|integer|min:1',
|
||||
];
|
||||
|
||||
protected array $messages = [];
|
||||
|
||||
protected array $attributes = [
|
||||
'pid' => '商户ID',
|
||||
'key' => '商户密钥',
|
||||
'type' => '支付方式',
|
||||
'out_trade_no' => '商户订单号',
|
||||
'trade_no' => '系统订单号',
|
||||
'notify_url' => '异步通知地址',
|
||||
'return_url' => '跳转通知地址',
|
||||
'name' => '商品名称',
|
||||
'money' => '商品金额',
|
||||
'clientip' => '用户IP地址',
|
||||
'device' => '设备类型',
|
||||
'param' => '业务扩展参数',
|
||||
'sign' => '签名字符串',
|
||||
'sign_type' => '签名类型',
|
||||
'act' => '操作类型',
|
||||
'limit' => '查询数量',
|
||||
'page' => '页码',
|
||||
];
|
||||
|
||||
/**
|
||||
* 不同接口场景
|
||||
*/
|
||||
protected array $scenes = [
|
||||
// 页面跳转支付 submit.php
|
||||
'submit' => [
|
||||
'pid',
|
||||
'type',
|
||||
'out_trade_no',
|
||||
'notify_url',
|
||||
'return_url',
|
||||
'name',
|
||||
'money',
|
||||
'param',
|
||||
'sign',
|
||||
'sign_type',
|
||||
],
|
||||
|
||||
// API 接口支付 mapi.php
|
||||
'mapi' => [
|
||||
'pid',
|
||||
'type',
|
||||
'out_trade_no',
|
||||
'notify_url',
|
||||
'return_url',
|
||||
'name',
|
||||
'money',
|
||||
'clientip',
|
||||
'device',
|
||||
'param',
|
||||
'sign',
|
||||
'sign_type',
|
||||
],
|
||||
|
||||
// api.php?act=order 查询单个订单
|
||||
'api_order' => [
|
||||
'act',
|
||||
'pid',
|
||||
'key',
|
||||
// trade_no 与 out_trade_no 至少一个,由业务层进一步校验
|
||||
],
|
||||
|
||||
// api.php?act=refund 提交退款
|
||||
'api_refund' => [
|
||||
'act',
|
||||
'pid',
|
||||
'key',
|
||||
'money',
|
||||
// trade_no/out_trade_no 至少一个
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user