mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-23 10:34:25 +08:00
更新数据库结构
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user