更新数据库结构

This commit is contained in:
技术老胡
2026-03-10 13:47:28 +08:00
parent 54ad21ac8f
commit 9de902231f
54 changed files with 5070 additions and 501 deletions

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

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

View 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'],
],
];
}
}