mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-26 03:54:25 +08:00
重构初始化
This commit is contained in:
28
.env.example
Normal file
28
.env.example
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 数据库配置
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=mpay
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=your-password
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_DATABASE=0
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
AUTH_JWT_ISSUER=mpay
|
||||||
|
AUTH_JWT_LEEWAY=30
|
||||||
|
AUTH_JWT_SECRET=change-me-jwt-secret-use-at-least-32-chars
|
||||||
|
|
||||||
|
AUTH_ADMIN_JWT_SECRET=change-me-admin-jwt-secret-use-at-least-32-chars
|
||||||
|
AUTH_ADMIN_JWT_TTL=86400
|
||||||
|
AUTH_ADMIN_JWT_REDIS_PREFIX=mpay:auth:admin:
|
||||||
|
|
||||||
|
AUTH_MERCHANT_JWT_SECRET=change-me-merchant-jwt-secret-use-at-least-32-chars
|
||||||
|
AUTH_MERCHANT_JWT_TTL=86400
|
||||||
|
AUTH_MERCHANT_JWT_REDIS_PREFIX=mpay:auth:merchant:
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM php:8.3.22-cli-alpine
|
||||||
|
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
|
&& apk update --no-cache \
|
||||||
|
&& docker-php-source extract
|
||||||
|
|
||||||
|
# install extensions
|
||||||
|
RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
|
||||||
|
|
||||||
|
# enable opcache and pcntl
|
||||||
|
RUN docker-php-ext-enable opcache pcntl
|
||||||
|
RUN docker-php-source delete \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
RUN mkdir -p /app
|
||||||
|
WORKDIR /app
|
||||||
667
app/command/EpayMapiTest.php
Normal file
667
app/command/EpayMapiTest.php
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\constant\RouteConstant;
|
||||||
|
use app\common\constant\TradeConstant;
|
||||||
|
use app\common\util\FormatHelper;
|
||||||
|
use app\http\api\controller\adapter\EpayController;
|
||||||
|
use app\model\merchant\Merchant;
|
||||||
|
use app\model\merchant\MerchantApiCredential;
|
||||||
|
use app\model\payment\PaymentChannel;
|
||||||
|
use app\model\payment\PaymentPollGroup;
|
||||||
|
use app\model\payment\PaymentType;
|
||||||
|
use app\repository\merchant\base\MerchantRepository;
|
||||||
|
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||||
|
use app\repository\payment\config\PaymentChannelRepository;
|
||||||
|
use app\repository\payment\config\PaymentPollGroupBindRepository;
|
||||||
|
use app\repository\payment\config\PaymentPollGroupChannelRepository;
|
||||||
|
use app\repository\payment\config\PaymentPollGroupRepository;
|
||||||
|
use app\repository\payment\config\PaymentTypeRepository;
|
||||||
|
use app\repository\payment\trade\BizOrderRepository;
|
||||||
|
use app\repository\payment\trade\PayOrderRepository;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use support\Request;
|
||||||
|
|
||||||
|
#[AsCommand('epay:mapi', '运行 ePay mapi 兼容接口烟雾测试')]
|
||||||
|
class EpayMapiTest extends Command
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('自动读取真实商户、路由和插件配置,测试 ePay mapi 是否正常调用并返回结果。')
|
||||||
|
->addOption('live', null, InputOption::VALUE_NONE, '使用真实数据库并发起实际 mapi 调用')
|
||||||
|
->addOption('merchant-id', null, InputOption::VALUE_OPTIONAL, '指定商户 ID')
|
||||||
|
->addOption('merchant-no', null, InputOption::VALUE_OPTIONAL, '指定商户号')
|
||||||
|
->addOption('type', null, InputOption::VALUE_OPTIONAL, '支付方式编码,默认 alipay', 'alipay')
|
||||||
|
->addOption('money', null, InputOption::VALUE_OPTIONAL, '支付金额,单位元,默认 1.00', '1.00')
|
||||||
|
->addOption('device', null, InputOption::VALUE_OPTIONAL, '设备类型,默认 pc', 'pc')
|
||||||
|
->addOption('out-trade-no', null, InputOption::VALUE_OPTIONAL, '商户订单号,默认自动生成');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$output->writeln('<info>epay mapi 烟雾测试</info>');
|
||||||
|
|
||||||
|
if (!$this->optionBool($input, 'live', false)) {
|
||||||
|
$this->ensureDependencies();
|
||||||
|
$output->writeln('<info>[通过]</info> 依赖检查正常,使用 --live 才会真正发起 mapi 请求。');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$typeCode = trim($this->optionString($input, 'type', 'alipay'));
|
||||||
|
$money = $this->normalizeMoney($this->optionString($input, 'money', '1.00'));
|
||||||
|
$device = $this->normalizeDevice($this->optionString($input, 'device', 'pc'));
|
||||||
|
$merchantIdOption = $this->optionInt($input, 'merchant-id', 0);
|
||||||
|
$merchantNoOption = trim($this->optionString($input, 'merchant-no', ''));
|
||||||
|
$outTradeNo = $this->buildMerchantOrderNo(trim($this->optionString($input, 'out-trade-no', '')));
|
||||||
|
|
||||||
|
$context = $this->discoverContext($merchantIdOption, $merchantNoOption, $typeCode);
|
||||||
|
$merchant = $context['merchant'];
|
||||||
|
$credential = $context['credential'];
|
||||||
|
$paymentType = $context['payment_type'];
|
||||||
|
$route = $context['route'];
|
||||||
|
$siteUrl = $this->resolveSiteUrl();
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'商户: id=%d no=%s name=%s group_id=%d',
|
||||||
|
(int) $merchant->id,
|
||||||
|
(string) $merchant->merchant_no,
|
||||||
|
(string) $merchant->merchant_name,
|
||||||
|
(int) $merchant->group_id
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'接口凭证: %s',
|
||||||
|
FormatHelper::maskCredentialValue((string) $credential->api_key)
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'支付方式: %s(%d) 金额: %s 设备: %s',
|
||||||
|
(string) $paymentType->code,
|
||||||
|
(int) $paymentType->id,
|
||||||
|
$money,
|
||||||
|
$device
|
||||||
|
));
|
||||||
|
$this->writeRouteSnapshot($output, $route);
|
||||||
|
|
||||||
|
$payload = $this->buildPayload(
|
||||||
|
merchant: $merchant,
|
||||||
|
credential: $credential,
|
||||||
|
paymentType: $paymentType,
|
||||||
|
merchantOrderNo: $outTradeNo,
|
||||||
|
money: $money,
|
||||||
|
device: $device,
|
||||||
|
siteUrl: $siteUrl
|
||||||
|
);
|
||||||
|
$controller = $this->resolve(EpayController::class);
|
||||||
|
$response = $controller->mapi($this->buildRequest($payload));
|
||||||
|
$responseData = $this->decodeResponse($response->rawBody());
|
||||||
|
$orderSnapshot = $this->loadOrderSnapshot((int) $merchant->id, $outTradeNo);
|
||||||
|
|
||||||
|
$this->writeAttempt($output, $payload, $responseData, $orderSnapshot);
|
||||||
|
|
||||||
|
$status = $this->classifyAttempt($responseData, $orderSnapshot);
|
||||||
|
return $status === 'pass' ? self::SUCCESS : self::FAILURE;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$output->writeln('<error>[失败]</error> ' . $this->formatThrowable($e));
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureDependencies(): void
|
||||||
|
{
|
||||||
|
$this->resolve(EpayController::class);
|
||||||
|
$this->resolve(MerchantRepository::class);
|
||||||
|
$this->resolve(MerchantApiCredentialRepository::class);
|
||||||
|
$this->resolve(PaymentTypeRepository::class);
|
||||||
|
$this->resolve(PaymentPollGroupBindRepository::class);
|
||||||
|
$this->resolve(PaymentPollGroupRepository::class);
|
||||||
|
$this->resolve(PaymentPollGroupChannelRepository::class);
|
||||||
|
$this->resolve(PaymentChannelRepository::class);
|
||||||
|
$this->resolve(BizOrderRepository::class);
|
||||||
|
$this->resolve(PayOrderRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{merchant:Merchant,credential:MerchantApiCredential,payment_type:PaymentType,route:array}
|
||||||
|
*/
|
||||||
|
private function discoverContext(int $merchantIdOption, string $merchantNoOption, string $typeCode): array
|
||||||
|
{
|
||||||
|
/** @var PaymentTypeRepository $paymentTypeRepository */
|
||||||
|
$paymentTypeRepository = $this->resolve(PaymentTypeRepository::class);
|
||||||
|
$paymentType = $paymentTypeRepository->findByCode($typeCode);
|
||||||
|
if (!$paymentType || (int) $paymentType->status !== 1) {
|
||||||
|
throw new RuntimeException('未找到可用的支付方式: ' . $typeCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchant = $this->pickMerchant($merchantIdOption, $merchantNoOption);
|
||||||
|
$credential = $this->findMerchantCredential((int) $merchant->id);
|
||||||
|
if (!$credential) {
|
||||||
|
throw new RuntimeException('商户未开通有效接口凭证: ' . $merchant->merchant_no);
|
||||||
|
}
|
||||||
|
|
||||||
|
$route = $this->buildRouteSnapshot((int) $merchant->group_id, (int) $paymentType->id);
|
||||||
|
if ($route === null) {
|
||||||
|
throw new RuntimeException('商户未配置可用路由: ' . $merchant->merchant_no);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'merchant' => $merchant,
|
||||||
|
'credential' => $credential,
|
||||||
|
'payment_type' => $paymentType,
|
||||||
|
'route' => $route,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pickMerchant(int $merchantIdOption, string $merchantNoOption): Merchant
|
||||||
|
{
|
||||||
|
/** @var MerchantRepository $merchantRepository */
|
||||||
|
$merchantRepository = $this->resolve(MerchantRepository::class);
|
||||||
|
|
||||||
|
if ($merchantIdOption > 0) {
|
||||||
|
$merchant = $merchantRepository->find($merchantIdOption);
|
||||||
|
if (!$merchant || (int) $merchant->status !== 1) {
|
||||||
|
throw new RuntimeException('指定商户不存在或未启用: ' . $merchantIdOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merchantNoOption !== '' && (string) $merchant->merchant_no !== $merchantNoOption) {
|
||||||
|
throw new RuntimeException('merchant-id 和 merchant-no 不匹配。');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merchantNoOption !== '') {
|
||||||
|
$merchant = $merchantRepository->findByMerchantNo($merchantNoOption);
|
||||||
|
if (!$merchant || (int) $merchant->status !== 1) {
|
||||||
|
throw new RuntimeException('指定商户不存在或未启用: ' . $merchantNoOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchant = $merchantRepository->enabledList(['id', 'merchant_no', 'merchant_name', 'group_id', 'status'])->first();
|
||||||
|
if (!$merchant) {
|
||||||
|
throw new RuntimeException('未找到启用中的真实商户。');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findMerchantCredential(int $merchantId): ?MerchantApiCredential
|
||||||
|
{
|
||||||
|
/** @var MerchantApiCredentialRepository $repository */
|
||||||
|
$repository = $this->resolve(MerchantApiCredentialRepository::class);
|
||||||
|
$credential = $repository->findByMerchantId($merchantId);
|
||||||
|
if (!$credential || (int) $credential->status !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{bind:mixed,poll_group:PaymentPollGroup,candidates:array<int,array<string,mixed>>}|null
|
||||||
|
*/
|
||||||
|
private function buildRouteSnapshot(int $merchantGroupId, int $payTypeId): ?array
|
||||||
|
{
|
||||||
|
/** @var PaymentPollGroupBindRepository $bindRepository */
|
||||||
|
$bindRepository = $this->resolve(PaymentPollGroupBindRepository::class);
|
||||||
|
/** @var PaymentPollGroupRepository $pollGroupRepository */
|
||||||
|
$pollGroupRepository = $this->resolve(PaymentPollGroupRepository::class);
|
||||||
|
/** @var PaymentPollGroupChannelRepository $pollGroupChannelRepository */
|
||||||
|
$pollGroupChannelRepository = $this->resolve(PaymentPollGroupChannelRepository::class);
|
||||||
|
/** @var PaymentChannelRepository $channelRepository */
|
||||||
|
$channelRepository = $this->resolve(PaymentChannelRepository::class);
|
||||||
|
|
||||||
|
$bind = $bindRepository->findActiveByMerchantGroupAndPayType($merchantGroupId, $payTypeId);
|
||||||
|
if (!$bind) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pollGroup = $pollGroupRepository->find((int) $bind->poll_group_id);
|
||||||
|
if (!$pollGroup || (int) $pollGroup->status !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateRows = $pollGroupChannelRepository->listByPollGroupId((int) $pollGroup->id);
|
||||||
|
if ($candidateRows->isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$channelIds = $candidateRows->pluck('channel_id')->all();
|
||||||
|
$channels = $channelRepository->query()
|
||||||
|
->whereIn('id', $channelIds)
|
||||||
|
->where('status', 1)
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
foreach ($candidateRows as $row) {
|
||||||
|
$channel = $channels->get((int) $row->channel_id);
|
||||||
|
if (!$channel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $channel->pay_type_id !== $payTypeId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates[] = [
|
||||||
|
'channel' => $channel,
|
||||||
|
'poll_group_channel' => $row,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidates === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'bind' => $bind,
|
||||||
|
'poll_group' => $pollGroup,
|
||||||
|
'candidates' => $candidates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPayload(
|
||||||
|
Merchant $merchant,
|
||||||
|
MerchantApiCredential $credential,
|
||||||
|
PaymentType $paymentType,
|
||||||
|
string $merchantOrderNo,
|
||||||
|
string $money,
|
||||||
|
string $device,
|
||||||
|
string $siteUrl
|
||||||
|
): array {
|
||||||
|
$siteUrl = rtrim($siteUrl, '/');
|
||||||
|
$payload = [
|
||||||
|
'pid' => (int) $merchant->id,
|
||||||
|
'key' => (string) $credential->api_key,
|
||||||
|
'type' => (string) $paymentType->code,
|
||||||
|
'out_trade_no' => $merchantOrderNo,
|
||||||
|
'notify_url' => $siteUrl . '/epay/mapi/notify',
|
||||||
|
'return_url' => $siteUrl . '/epay/mapi/return',
|
||||||
|
'name' => trim(sprintf('mpay epay mapi smoke %s', (string) $merchant->merchant_name)),
|
||||||
|
'money' => $money,
|
||||||
|
'clientip' => '127.0.0.1',
|
||||||
|
'device' => $device,
|
||||||
|
'sign_type' => 'MD5',
|
||||||
|
];
|
||||||
|
$payload['sign'] = $this->signPayload($payload, (string) $credential->api_key);
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function classifyAttempt(array $responseData, array $orderSnapshot): string
|
||||||
|
{
|
||||||
|
$responseCode = (int) ($responseData['code'] ?? 0);
|
||||||
|
$payOrder = $orderSnapshot['pay_order'] ?? null;
|
||||||
|
$bizOrder = $orderSnapshot['biz_order'] ?? null;
|
||||||
|
|
||||||
|
if ($responseCode !== 1) {
|
||||||
|
return $payOrder ? 'fail' : 'skip';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($payOrder && $bizOrder) ? 'pass' : 'fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeRouteSnapshot(OutputInterface $output, array $route): void
|
||||||
|
{
|
||||||
|
/** @var PaymentPollGroup $pollGroup */
|
||||||
|
$pollGroup = $route['poll_group'];
|
||||||
|
$candidates = $route['candidates'];
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'路由: group_id=%d group_name=%s mode=%s',
|
||||||
|
(int) $pollGroup->id,
|
||||||
|
(string) $pollGroup->group_name,
|
||||||
|
$this->routeModeLabel((int) $pollGroup->route_mode)
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(' 候选通道: %d 个', count($candidates)));
|
||||||
|
foreach ($candidates as $item) {
|
||||||
|
/** @var PaymentChannel $channel */
|
||||||
|
$channel = $item['channel'];
|
||||||
|
$pollGroupChannel = $item['poll_group_channel'];
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' - channel_id=%d name=%s default=%s sort_no=%d weight=%d mode=%s pay_type_id=%d plugin=%s',
|
||||||
|
(int) $channel->id,
|
||||||
|
(string) $channel->name,
|
||||||
|
(int) $pollGroupChannel->is_default === 1 ? 'yes' : 'no',
|
||||||
|
(int) $pollGroupChannel->sort_no,
|
||||||
|
(int) $pollGroupChannel->weight,
|
||||||
|
$this->channelModeLabel((int) $channel->channel_mode),
|
||||||
|
(int) $channel->pay_type_id,
|
||||||
|
(string) $channel->plugin_code
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeAttempt(OutputInterface $output, array $payload, array $responseData, array $orderSnapshot): void
|
||||||
|
{
|
||||||
|
$status = $this->classifyAttempt($responseData, $orderSnapshot);
|
||||||
|
$label = match ($status) {
|
||||||
|
'pass' => '<info>[通过]</info>',
|
||||||
|
'skip' => '<comment>[跳过]</comment>',
|
||||||
|
default => '<error>[失败]</error>',
|
||||||
|
};
|
||||||
|
$payOrder = $orderSnapshot['pay_order'] ?? [];
|
||||||
|
$bizOrder = $orderSnapshot['biz_order'] ?? [];
|
||||||
|
$channel = $orderSnapshot['channel'] ?? [];
|
||||||
|
$paymentType = $orderSnapshot['payment_type'] ?? [];
|
||||||
|
|
||||||
|
$output->writeln(sprintf('%s mapi - out_trade_no=%s', $label, $payload['out_trade_no']));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 请求: pid=%d type=%s money=%s device=%s clientip=%s',
|
||||||
|
(int) $payload['pid'],
|
||||||
|
(string) $payload['type'],
|
||||||
|
(string) $payload['money'],
|
||||||
|
(string) $payload['device'],
|
||||||
|
(string) $payload['clientip']
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 响应: code=%s msg=%s',
|
||||||
|
(string) ($responseData['code'] ?? ''),
|
||||||
|
(string) ($responseData['msg'] ?? '')
|
||||||
|
));
|
||||||
|
foreach (['trade_no', 'payurl', 'origin_payurl', 'qrcode', 'urlscheme'] as $key) {
|
||||||
|
if (!isset($responseData[$key]) || $responseData[$key] === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$output->writeln(sprintf(' 返回: %s=%s', $key, $this->stringifyValue($responseData[$key])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$bizOrder || !$payOrder) {
|
||||||
|
$output->writeln(' 订单: 未创建或未查到业务单/支付单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 业务单: biz_no=%s status=%s active_pay_no=%s attempt_count=%d',
|
||||||
|
(string) ($bizOrder['biz_no'] ?? ''),
|
||||||
|
$this->orderStatusLabel((int) ($bizOrder['status'] ?? 0)),
|
||||||
|
(string) ($bizOrder['active_pay_no'] ?? ''),
|
||||||
|
(int) ($bizOrder['attempt_count'] ?? 0)
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 支付单: pay_no=%s status=%s channel_id=%d channel=%s plugin=%s pay_type=%s',
|
||||||
|
(string) ($payOrder['pay_no'] ?? ''),
|
||||||
|
$this->orderStatusLabel((int) ($payOrder['status'] ?? 0)),
|
||||||
|
(int) ($payOrder['channel_id'] ?? 0),
|
||||||
|
(string) ($channel['name'] ?? ''),
|
||||||
|
(string) ($payOrder['plugin_code'] ?? ''),
|
||||||
|
(string) ($paymentType['code'] ?? '')
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 支付单状态: channel_request_no=%s channel_order_no=%s channel_trade_no=%s',
|
||||||
|
(string) ($payOrder['channel_request_no'] ?? ''),
|
||||||
|
(string) ($payOrder['channel_order_no'] ?? ''),
|
||||||
|
(string) ($payOrder['channel_trade_no'] ?? '')
|
||||||
|
));
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' 失败信息: code=%s msg=%s',
|
||||||
|
(string) ($payOrder['channel_error_code'] ?? ''),
|
||||||
|
(string) ($payOrder['channel_error_msg'] ?? '')
|
||||||
|
));
|
||||||
|
|
||||||
|
$extJson = (array) ($payOrder['ext_json'] ?? []);
|
||||||
|
$summary = $this->summarizePayParamsSnapshot((array) ($extJson['pay_params_snapshot'] ?? []));
|
||||||
|
if ($summary !== []) {
|
||||||
|
$output->writeln(' 插件返回:');
|
||||||
|
$output->writeln(' ' . $this->formatJson($summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function summarizePayParamsSnapshot(array $snapshot): array
|
||||||
|
{
|
||||||
|
if ($snapshot === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = ['type' => (string) ($snapshot['type'] ?? '')];
|
||||||
|
if (isset($snapshot['pay_product'])) {
|
||||||
|
$summary['pay_product'] = (string) $snapshot['pay_product'];
|
||||||
|
}
|
||||||
|
if (isset($snapshot['pay_action'])) {
|
||||||
|
$summary['pay_action'] = (string) $snapshot['pay_action'];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ((string) ($snapshot['type'] ?? '')) {
|
||||||
|
case 'form':
|
||||||
|
$html = $this->stringifyValue($snapshot['html'] ?? '');
|
||||||
|
$summary['html_length'] = strlen($html);
|
||||||
|
$summary['html_head'] = $this->limitString($this->normalizeWhitespace($html), 160);
|
||||||
|
break;
|
||||||
|
case 'qrcode':
|
||||||
|
$summary['qrcode_url'] = $this->stringifyValue($snapshot['qrcode_url'] ?? $snapshot['qrcode_data'] ?? '');
|
||||||
|
break;
|
||||||
|
case 'urlscheme':
|
||||||
|
$summary['urlscheme'] = $this->stringifyValue($snapshot['urlscheme'] ?? $snapshot['order_str'] ?? '');
|
||||||
|
break;
|
||||||
|
case 'url':
|
||||||
|
$summary['payurl'] = $this->stringifyValue($snapshot['payurl'] ?? '');
|
||||||
|
$summary['origin_payurl'] = $this->stringifyValue($snapshot['origin_payurl'] ?? '');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isset($snapshot['raw']) && is_array($snapshot['raw'])) {
|
||||||
|
$summary['raw_keys'] = array_values(array_map('strval', array_keys($snapshot['raw'])));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function routeModeLabel(int $routeMode): string
|
||||||
|
{
|
||||||
|
return RouteConstant::routeModeMap()[$routeMode] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function channelModeLabel(int $channelMode): string
|
||||||
|
{
|
||||||
|
return RouteConstant::channelModeMap()[$channelMode] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function orderStatusLabel(int $status): string
|
||||||
|
{
|
||||||
|
return TradeConstant::orderStatusMap()[$status] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMerchantOrderNo(string $base): string
|
||||||
|
{
|
||||||
|
$base = trim($base);
|
||||||
|
if ($base !== '') {
|
||||||
|
return substr($base, 0, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'EPAY-MAPI-' . FormatHelper::timestamp(time(), 'YmdHis') . random_int(1000, 9999);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function signPayload(array $payload, string $key): string
|
||||||
|
{
|
||||||
|
$params = $payload;
|
||||||
|
unset($params['sign'], $params['sign_type'], $params['key']);
|
||||||
|
foreach ($params as $paramKey => $paramValue) {
|
||||||
|
if ($paramValue === '' || $paramValue === null) {
|
||||||
|
unset($params[$paramKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($params);
|
||||||
|
$query = [];
|
||||||
|
foreach ($params as $paramKey => $paramValue) {
|
||||||
|
$query[] = $paramKey . '=' . $paramValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5(implode('&', $query) . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequest(array $payload): Request
|
||||||
|
{
|
||||||
|
$body = http_build_query($payload, '', '&', PHP_QUERY_RFC1738);
|
||||||
|
$siteUrl = $this->resolveSiteUrl();
|
||||||
|
$host = parse_url($siteUrl, PHP_URL_HOST) ?: 'localhost';
|
||||||
|
$port = parse_url($siteUrl, PHP_URL_PORT);
|
||||||
|
$hostHeader = $port ? sprintf('%s:%s', $host, $port) : $host;
|
||||||
|
|
||||||
|
$rawRequest = implode("\r\n", [
|
||||||
|
'POST /mapi.php HTTP/1.1',
|
||||||
|
'Host: ' . $hostHeader,
|
||||||
|
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'Content-Length: ' . strlen($body),
|
||||||
|
'Connection: close',
|
||||||
|
'',
|
||||||
|
$body,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new Request($rawRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadOrderSnapshot(int $merchantId, string $merchantOrderNo): array
|
||||||
|
{
|
||||||
|
/** @var BizOrderRepository $bizOrderRepository */
|
||||||
|
$bizOrderRepository = $this->resolve(BizOrderRepository::class);
|
||||||
|
/** @var PayOrderRepository $payOrderRepository */
|
||||||
|
$payOrderRepository = $this->resolve(PayOrderRepository::class);
|
||||||
|
/** @var PaymentChannelRepository $channelRepository */
|
||||||
|
$channelRepository = $this->resolve(PaymentChannelRepository::class);
|
||||||
|
/** @var PaymentTypeRepository $typeRepository */
|
||||||
|
$typeRepository = $this->resolve(PaymentTypeRepository::class);
|
||||||
|
|
||||||
|
$bizOrder = $bizOrderRepository->findByMerchantAndOrderNo($merchantId, $merchantOrderNo);
|
||||||
|
$payOrder = $bizOrder ? $payOrderRepository->findLatestByBizNo((string) $bizOrder->biz_no) : null;
|
||||||
|
$channel = $payOrder ? $channelRepository->find((int) $payOrder->channel_id) : null;
|
||||||
|
$paymentType = $payOrder ? $typeRepository->find((int) $payOrder->pay_type_id) : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'biz_order' => $bizOrder ? $bizOrder->toArray() : null,
|
||||||
|
'pay_order' => $payOrder ? $payOrder->toArray() : null,
|
||||||
|
'channel' => $channel ? $channel->toArray() : null,
|
||||||
|
'payment_type' => $paymentType ? $paymentType->toArray() : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveSiteUrl(): string
|
||||||
|
{
|
||||||
|
$siteUrl = trim((string) sys_config('site_url'));
|
||||||
|
return $siteUrl !== '' ? rtrim($siteUrl, '/') : 'http://localhost:8787';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMoney(string $money): string
|
||||||
|
{
|
||||||
|
$money = trim($money);
|
||||||
|
if ($money === '') {
|
||||||
|
return '1.00';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^\d+(?:\.\d{1,2})?$/', $money)) {
|
||||||
|
throw new RuntimeException('money 参数不合法: ' . $money);
|
||||||
|
}
|
||||||
|
|
||||||
|
return number_format((float) $money, 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDevice(string $device): string
|
||||||
|
{
|
||||||
|
$device = strtolower(trim($device));
|
||||||
|
return $device !== '' ? $device : 'pc';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeResponse(string $body): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
return is_array($decoded) ? $decoded : ['raw' => $body];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stringifyValue(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (is_string($value)) {
|
||||||
|
return trim($value);
|
||||||
|
}
|
||||||
|
if (is_int($value) || is_float($value) || is_bool($value)) {
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
if (is_array($value) || is_object($value)) {
|
||||||
|
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
return $json !== false ? $json : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function limitString(string $value, int $length): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen($value) <= $length ? $value : substr($value, 0, $length) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeWhitespace(string $value): string
|
||||||
|
{
|
||||||
|
return preg_replace('/\s+/', ' ', trim($value)) ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatJson(mixed $value): string
|
||||||
|
{
|
||||||
|
return FormatHelper::json($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatThrowable(\Throwable $e): string
|
||||||
|
{
|
||||||
|
$data = method_exists($e, 'getData') ? $e->getData() : [];
|
||||||
|
$suffix = is_array($data) && $data !== [] ? ' ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : '';
|
||||||
|
|
||||||
|
return $e::class . ': ' . $e->getMessage() . $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionString(InputInterface $input, string $name, string $default = ''): string
|
||||||
|
{
|
||||||
|
$value = $input->getOption($name);
|
||||||
|
return $value === null || $value === false ? $default : (is_string($value) ? $value : (string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionInt(InputInterface $input, string $name, int $default = 0): int
|
||||||
|
{
|
||||||
|
$value = $input->getOption($name);
|
||||||
|
return is_numeric($value) ? (int) $value : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionBool(InputInterface $input, string $name, bool $default = false): bool
|
||||||
|
{
|
||||||
|
$value = $input->getOption($name);
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
|
return $filtered === null ? $default : $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolve(string $class): object
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$instance = container_make($class, []);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new RuntimeException("无法解析 {$class}: " . $e->getMessage(), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_object($instance)) {
|
||||||
|
throw new RuntimeException("解析后的 {$class} 不是对象。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
510
app/command/MpayTest.php
Normal file
510
app/command/MpayTest.php
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\constant\TradeConstant;
|
||||||
|
use app\common\util\FormatHelper;
|
||||||
|
use app\service\account\funds\MerchantAccountService;
|
||||||
|
use app\service\merchant\MerchantService;
|
||||||
|
use app\service\payment\order\PayOrderService;
|
||||||
|
use app\service\payment\order\RefundService;
|
||||||
|
use app\service\payment\settlement\SettlementService;
|
||||||
|
use app\service\payment\trace\TradeTraceService;
|
||||||
|
use app\repository\payment\trade\PayOrderRepository;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand('mpay:test', '运行支付、退款、清结算、余额和追踪烟雾测试')]
|
||||||
|
class MpayTest extends Command
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('运行支付、退款、清结算、余额和追踪烟雾测试。')
|
||||||
|
->addOption('payment', null, InputOption::VALUE_NONE, '仅运行支付检查')
|
||||||
|
->addOption('refund', null, InputOption::VALUE_NONE, '仅运行退款检查')
|
||||||
|
->addOption('settlement', null, InputOption::VALUE_NONE, '仅运行清结算检查')
|
||||||
|
->addOption('balance', null, InputOption::VALUE_NONE, '仅运行余额检查')
|
||||||
|
->addOption('trace', null, InputOption::VALUE_NONE, '仅运行追踪检查')
|
||||||
|
->addOption('all', null, InputOption::VALUE_NONE, '运行全部检查')
|
||||||
|
->addOption('live', null, InputOption::VALUE_NONE, '在提供测试数据时运行真实数据库检查');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$cases = $this->resolveCases($input);
|
||||||
|
$live = (bool) $input->getOption('live');
|
||||||
|
|
||||||
|
$output->writeln('<info>mpay 烟雾测试</info>');
|
||||||
|
$output->writeln('模式: ' . ($live ? '真实数据' : '依赖连通性'));
|
||||||
|
$output->writeln('测试项: ' . implode(', ', $cases));
|
||||||
|
|
||||||
|
$summary = [];
|
||||||
|
foreach ($cases as $case) {
|
||||||
|
$result = match ($case) {
|
||||||
|
'payment' => $this->checkPayment($live),
|
||||||
|
'refund' => $this->checkRefund($live),
|
||||||
|
'settlement' => $this->checkSettlement($live),
|
||||||
|
'balance' => $this->checkBalance($live),
|
||||||
|
'trace' => $this->checkTrace($live),
|
||||||
|
default => ['status' => 'skip', 'message' => '未知测试项'],
|
||||||
|
};
|
||||||
|
|
||||||
|
$summary[] = $result['status'];
|
||||||
|
$this->writeResult($output, strtoupper($case), $result['status'], $result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$failed = count(array_filter($summary, static fn (string $status) => $status === 'fail'));
|
||||||
|
$skipped = count(array_filter($summary, static fn (string $status) => $status === 'skip'));
|
||||||
|
$passed = count(array_filter($summary, static fn (string $status) => $status === 'pass'));
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'<info>汇总</info>: %d 通过, %d 跳过, %d 失败',
|
||||||
|
$passed,
|
||||||
|
$skipped,
|
||||||
|
$failed
|
||||||
|
));
|
||||||
|
|
||||||
|
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据命令行选项解析需要执行的测试项。
|
||||||
|
*/
|
||||||
|
private function resolveCases(InputInterface $input): array
|
||||||
|
{
|
||||||
|
$selected = [];
|
||||||
|
foreach (['payment', 'refund', 'settlement', 'balance', 'trace'] as $case) {
|
||||||
|
if ((bool) $input->getOption($case)) {
|
||||||
|
$selected[] = $case;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) $input->getOption('all') || empty($selected)) {
|
||||||
|
return ['payment', 'refund', 'settlement', 'balance', 'trace'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查支付链路。
|
||||||
|
*/
|
||||||
|
private function checkPayment(bool $live): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$service = $this->resolve(PayOrderService::class);
|
||||||
|
$this->ensureMethod($service, 'preparePayAttempt');
|
||||||
|
$this->ensureMethod($service, 'timeoutPayOrder');
|
||||||
|
|
||||||
|
if (!$live) {
|
||||||
|
return ['status' => 'pass', 'message' => '服务依赖连通性正常'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchantId = $this->envInt('MPAY_TEST_PAYMENT_MERCHANT_ID');
|
||||||
|
$payTypeId = $this->envInt('MPAY_TEST_PAYMENT_TYPE_ID');
|
||||||
|
$payAmount = $this->envInt('MPAY_TEST_PAYMENT_AMOUNT');
|
||||||
|
$merchantOrderNo = $this->envString('MPAY_TEST_PAYMENT_ORDER_NO', $this->generateTestNo('PAY-TEST-'));
|
||||||
|
|
||||||
|
if ($merchantId <= 0 || $payTypeId <= 0 || $payAmount <= 0) {
|
||||||
|
return ['status' => 'skip', 'message' => '缺少 MPAY_TEST_PAYMENT_* 测试配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $service->preparePayAttempt([
|
||||||
|
'merchant_id' => $merchantId,
|
||||||
|
'merchant_order_no' => $merchantOrderNo,
|
||||||
|
'pay_type_id' => $payTypeId,
|
||||||
|
'pay_amount' => $payAmount,
|
||||||
|
'subject' => $this->envString('MPAY_TEST_PAYMENT_SUBJECT', 'mpay smoke payment'),
|
||||||
|
'body' => $this->envString('MPAY_TEST_PAYMENT_BODY', 'mpay smoke payment'),
|
||||||
|
'ext_json' => $this->envJson('MPAY_TEST_PAYMENT_EXT_JSON', []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$payOrder = $result['pay_order'];
|
||||||
|
$selectedChannel = $result['route']['selected_channel']['channel'] ?? null;
|
||||||
|
$message = sprintf(
|
||||||
|
'已创建支付单 pay_no=%s biz_no=%s channel_id=%s',
|
||||||
|
(string) $payOrder->pay_no,
|
||||||
|
(string) $result['biz_order']->biz_no,
|
||||||
|
$selectedChannel ? (string) $selectedChannel->id : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->envBool('MPAY_TEST_PAYMENT_MARK_TIMEOUT', false)) {
|
||||||
|
$service->timeoutPayOrder((string) $payOrder->pay_no, [
|
||||||
|
'timeout_at' => $this->envString('MPAY_TEST_PAYMENT_TIMEOUT_AT', FormatHelper::timestamp(time())),
|
||||||
|
'reason' => $this->envString('MPAY_TEST_PAYMENT_TIMEOUT_REASON', 'mpay smoke timeout'),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记超时';
|
||||||
|
} elseif ($this->envBool('MPAY_TEST_PAYMENT_MARK_SUCCESS', false)) {
|
||||||
|
$service->markPaySuccess((string) $payOrder->pay_no, [
|
||||||
|
'fee_actual_amount' => $this->envInt('MPAY_TEST_PAYMENT_FEE_AMOUNT', (int) $payOrder->fee_estimated_amount),
|
||||||
|
'channel_trade_no' => $this->envString('MPAY_TEST_PAYMENT_CHANNEL_TRADE_NO', $this->generateTestNo('CH-')),
|
||||||
|
'channel_order_no' => $this->envString('MPAY_TEST_PAYMENT_CHANNEL_ORDER_NO', $this->generateTestNo('CO-')),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记成功';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 'pass', 'message' => $message];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['status' => 'fail', 'message' => $this->formatThrowable($e)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查退款链路。
|
||||||
|
*/
|
||||||
|
private function checkRefund(bool $live): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$service = $this->resolve(RefundService::class);
|
||||||
|
$this->ensureMethod($service, 'createRefund');
|
||||||
|
$this->ensureMethod($service, 'markRefundProcessing');
|
||||||
|
$this->ensureMethod($service, 'retryRefund');
|
||||||
|
$this->ensureMethod($service, 'markRefundFailed');
|
||||||
|
|
||||||
|
if (!$live) {
|
||||||
|
return ['status' => 'pass', 'message' => '服务依赖连通性正常'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payNo = $this->envString('MPAY_TEST_REFUND_PAY_NO');
|
||||||
|
if ($payNo === '') {
|
||||||
|
return ['status' => 'skip', 'message' => '缺少 MPAY_TEST_REFUND_PAY_NO 测试配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund = $service->createRefund([
|
||||||
|
'pay_no' => $payNo,
|
||||||
|
'merchant_refund_no' => $this->envString('MPAY_TEST_REFUND_NO', $this->generateTestNo('RFD-TEST-')),
|
||||||
|
'reason' => $this->envString('MPAY_TEST_REFUND_REASON', 'mpay smoke refund'),
|
||||||
|
'ext_json' => $this->envJson('MPAY_TEST_REFUND_EXT_JSON', []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = '已创建退款单 refund_no=' . (string) $refund->refund_no;
|
||||||
|
|
||||||
|
if ($this->envBool('MPAY_TEST_REFUND_MARK_PROCESSING', false)) {
|
||||||
|
$service->markRefundProcessing((string) $refund->refund_no, [
|
||||||
|
'processing_at' => $this->envString('MPAY_TEST_REFUND_PROCESSING_AT', FormatHelper::timestamp(time())),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记处理中';
|
||||||
|
} elseif ($this->envBool('MPAY_TEST_REFUND_MARK_RETRY', false)) {
|
||||||
|
$service->retryRefund((string) $refund->refund_no, [
|
||||||
|
'processing_at' => $this->envString('MPAY_TEST_REFUND_RETRY_AT', FormatHelper::timestamp(time())),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记重试';
|
||||||
|
} elseif ($this->envBool('MPAY_TEST_REFUND_MARK_FAIL', false)) {
|
||||||
|
$service->markRefundFailed((string) $refund->refund_no, [
|
||||||
|
'failed_at' => $this->envString('MPAY_TEST_REFUND_FAILED_AT', FormatHelper::timestamp(time())),
|
||||||
|
'last_error' => $this->envString('MPAY_TEST_REFUND_LAST_ERROR', 'mpay smoke refund failed'),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记失败';
|
||||||
|
} elseif ($this->envBool('MPAY_TEST_REFUND_MARK_SUCCESS', false)) {
|
||||||
|
$service->markRefundSuccess((string) $refund->refund_no, [
|
||||||
|
'channel_refund_no' => $this->envString('MPAY_TEST_REFUND_CHANNEL_NO', $this->generateTestNo('CR-')),
|
||||||
|
]);
|
||||||
|
$message .= ', 已标记成功';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 'pass', 'message' => $message];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['status' => 'fail', 'message' => $this->formatThrowable($e)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查清结算链路。
|
||||||
|
*/
|
||||||
|
private function checkSettlement(bool $live): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$service = $this->resolve(SettlementService::class);
|
||||||
|
$this->ensureMethod($service, 'createSettlementOrder');
|
||||||
|
$this->ensureMethod($service, 'completeSettlement');
|
||||||
|
$this->ensureMethod($service, 'failSettlement');
|
||||||
|
|
||||||
|
if (!$live) {
|
||||||
|
return ['status' => 'pass', 'message' => '服务依赖连通性正常'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchantId = 0;
|
||||||
|
$merchantGroupId = 0;
|
||||||
|
$channelId = 0;
|
||||||
|
$settleNo = $this->envString('MPAY_TEST_SETTLEMENT_NO', $this->generateTestNo('STL-TEST-'));
|
||||||
|
$items = $this->envJson('MPAY_TEST_SETTLEMENT_ITEMS_JSON', []);
|
||||||
|
|
||||||
|
if (empty($items)) {
|
||||||
|
$payNo = $this->envString('MPAY_TEST_SETTLEMENT_PAY_NO');
|
||||||
|
if ($payNo !== '') {
|
||||||
|
$payOrderRepository = $this->resolve(PayOrderRepository::class);
|
||||||
|
$payOrder = $payOrderRepository->findByPayNo($payNo);
|
||||||
|
if ($payOrder) {
|
||||||
|
$items = [[
|
||||||
|
'pay_no' => (string) $payOrder->pay_no,
|
||||||
|
'refund_no' => '',
|
||||||
|
'pay_amount' => (int) $payOrder->pay_amount,
|
||||||
|
'fee_amount' => (int) $payOrder->fee_actual_amount,
|
||||||
|
'refund_amount' => 0,
|
||||||
|
'fee_reverse_amount' => 0,
|
||||||
|
'net_amount' => max(0, (int) $payOrder->pay_amount - (int) $payOrder->fee_actual_amount),
|
||||||
|
'item_status' => TradeConstant::SETTLEMENT_STATUS_PENDING,
|
||||||
|
]];
|
||||||
|
$merchantId = (int) $payOrder->merchant_id;
|
||||||
|
$merchantGroupId = (int) $payOrder->merchant_group_id;
|
||||||
|
$channelId = (int) $payOrder->channel_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merchantId <= 0) {
|
||||||
|
$merchantId = $this->envInt('MPAY_TEST_SETTLEMENT_MERCHANT_ID');
|
||||||
|
}
|
||||||
|
if ($merchantGroupId <= 0) {
|
||||||
|
$merchantGroupId = $this->envInt('MPAY_TEST_SETTLEMENT_MERCHANT_GROUP_ID');
|
||||||
|
}
|
||||||
|
if ($channelId <= 0) {
|
||||||
|
$channelId = $this->envInt('MPAY_TEST_SETTLEMENT_CHANNEL_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merchantId <= 0 || $merchantGroupId <= 0 || $channelId <= 0) {
|
||||||
|
return ['status' => 'skip', 'message' => '缺少 MPAY_TEST_SETTLEMENT_* 测试配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($items)) {
|
||||||
|
$items = [[
|
||||||
|
'pay_no' => '',
|
||||||
|
'refund_no' => '',
|
||||||
|
'pay_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_GROSS_AMOUNT', 100),
|
||||||
|
'fee_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_FEE_AMOUNT', 0),
|
||||||
|
'refund_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_REFUND_AMOUNT', 0),
|
||||||
|
'fee_reverse_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_FEE_REVERSE_AMOUNT', 0),
|
||||||
|
'net_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_NET_AMOUNT', 100),
|
||||||
|
'item_status' => TradeConstant::SETTLEMENT_STATUS_PENDING,
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$settlement = $service->createSettlementOrder([
|
||||||
|
'settle_no' => $settleNo,
|
||||||
|
'merchant_id' => $merchantId,
|
||||||
|
'merchant_group_id' => $merchantGroupId,
|
||||||
|
'channel_id' => $channelId,
|
||||||
|
'cycle_type' => $this->envInt('MPAY_TEST_SETTLEMENT_CYCLE_TYPE', TradeConstant::SETTLEMENT_CYCLE_OTHER),
|
||||||
|
'cycle_key' => $this->envString('MPAY_TEST_SETTLEMENT_CYCLE_KEY', FormatHelper::timestamp(time(), 'Y-m-d')),
|
||||||
|
'accounted_amount' => $this->envInt('MPAY_TEST_SETTLEMENT_ACCOUNTED_AMOUNT', 0),
|
||||||
|
'status' => TradeConstant::SETTLEMENT_STATUS_PENDING,
|
||||||
|
'ext_json' => $this->envJson('MPAY_TEST_SETTLEMENT_EXT_JSON', []),
|
||||||
|
], $items);
|
||||||
|
|
||||||
|
$message = '已创建清结算单 settle_no=' . (string) $settlement->settle_no;
|
||||||
|
|
||||||
|
if ($this->envBool('MPAY_TEST_SETTLEMENT_FAIL', false)) {
|
||||||
|
$service->failSettlement(
|
||||||
|
(string) $settlement->settle_no,
|
||||||
|
$this->envString('MPAY_TEST_SETTLEMENT_FAIL_REASON', 'mpay smoke settlement fail')
|
||||||
|
);
|
||||||
|
$message .= ', 已标记失败';
|
||||||
|
} elseif ($this->envBool('MPAY_TEST_SETTLEMENT_COMPLETE', false)) {
|
||||||
|
$service->completeSettlement((string) $settlement->settle_no);
|
||||||
|
$message .= ', 已完成入账';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 'pass', 'message' => $message];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['status' => 'fail', 'message' => $this->formatThrowable($e)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查余额链路。
|
||||||
|
*/
|
||||||
|
private function checkBalance(bool $live): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$accountService = $this->resolve(MerchantAccountService::class);
|
||||||
|
$this->ensureMethod($accountService, 'getBalanceSnapshot');
|
||||||
|
$this->resolve(MerchantService::class);
|
||||||
|
|
||||||
|
if (!$live) {
|
||||||
|
return ['status' => 'pass', 'message' => '服务依赖连通性正常'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchantId = $this->envInt('MPAY_TEST_BALANCE_MERCHANT_ID');
|
||||||
|
if ($merchantId <= 0) {
|
||||||
|
$merchantNo = $this->envString('MPAY_TEST_BALANCE_MERCHANT_NO');
|
||||||
|
if ($merchantNo !== '') {
|
||||||
|
$merchantService = $this->resolve(MerchantService::class);
|
||||||
|
$merchant = $merchantService->findEnabledMerchantByNo($merchantNo);
|
||||||
|
$merchantId = (int) $merchant->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merchantId <= 0) {
|
||||||
|
return ['status' => 'skip', 'message' => '缺少 MPAY_TEST_BALANCE_* 测试配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $accountService->getBalanceSnapshot($merchantId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'pass',
|
||||||
|
'message' => sprintf(
|
||||||
|
'余额 merchant_id=%d 可用=%d 冻结=%d',
|
||||||
|
(int) $snapshot['merchant_id'],
|
||||||
|
(int) $snapshot['available_balance'],
|
||||||
|
(int) $snapshot['frozen_balance']
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['status' => 'fail', 'message' => $this->formatThrowable($e)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查统一追踪链路。
|
||||||
|
*/
|
||||||
|
private function checkTrace(bool $live): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$service = $this->resolve(TradeTraceService::class);
|
||||||
|
$this->ensureMethod($service, 'queryByTraceNo');
|
||||||
|
|
||||||
|
if (!$live) {
|
||||||
|
return ['status' => 'pass', 'message' => '服务依赖连通性正常'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$traceNo = $this->envString('MPAY_TEST_TRACE_NO');
|
||||||
|
if ($traceNo === '') {
|
||||||
|
return ['status' => 'skip', 'message' => '缺少 MPAY_TEST_TRACE_NO 测试配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $service->queryByTraceNo($traceNo);
|
||||||
|
if (empty($result)) {
|
||||||
|
return ['status' => 'fail', 'message' => '追踪结果为空'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
'trace_no=%s 支付=%d 退款=%d 清结算=%d 流水=%d',
|
||||||
|
(string) ($result['resolved_trace_no'] ?? $traceNo),
|
||||||
|
count($result['pay_orders'] ?? []),
|
||||||
|
count($result['refund_orders'] ?? []),
|
||||||
|
count($result['settlement_orders'] ?? []),
|
||||||
|
count($result['account_ledgers'] ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['status' => 'pass', 'message' => $message];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['status' => 'fail', 'message' => $this->formatThrowable($e)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从容器中解析指定类实例。
|
||||||
|
*/
|
||||||
|
private function resolve(string $class): object
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$instance = container_make($class, []);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new RuntimeException("无法解析 {$class}: " . $e->getMessage(), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_object($instance)) {
|
||||||
|
throw new RuntimeException("解析后的 {$class} 不是对象。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查实例是否包含指定方法。
|
||||||
|
*/
|
||||||
|
private function ensureMethod(object $instance, string $method): void
|
||||||
|
{
|
||||||
|
if (!method_exists($instance, $method)) {
|
||||||
|
throw new RuntimeException(sprintf('未找到方法 %s::%s。', $instance::class, $method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取字符串环境变量。
|
||||||
|
*/
|
||||||
|
private function envString(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
$value = env($key, $default);
|
||||||
|
|
||||||
|
return is_string($value) ? $value : (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取整数环境变量。
|
||||||
|
*/
|
||||||
|
private function envInt(string $key, int $default = 0): int
|
||||||
|
{
|
||||||
|
$value = env($key, null);
|
||||||
|
|
||||||
|
return is_numeric($value) ? (int) $value : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取布尔环境变量。
|
||||||
|
*/
|
||||||
|
private function envBool(string $key, bool $default = false): bool
|
||||||
|
{
|
||||||
|
$value = env($key, null);
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
|
return $filtered === null ? $default : $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取结构化环境变量。
|
||||||
|
*/
|
||||||
|
private function envJson(string $key, array $default = []): array
|
||||||
|
{
|
||||||
|
$value = trim($this->envString($key));
|
||||||
|
if ($value === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
return is_array($decoded) ? $decoded : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成测试编号。
|
||||||
|
*/
|
||||||
|
private function generateTestNo(string $prefix): string
|
||||||
|
{
|
||||||
|
return $prefix . FormatHelper::timestamp(time(), 'YmdHis') . random_int(1000, 9999);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将异常格式化为可读文本。
|
||||||
|
*/
|
||||||
|
private function formatThrowable(\Throwable $e): string
|
||||||
|
{
|
||||||
|
$data = method_exists($e, 'getData') ? $e->getData() : [];
|
||||||
|
$suffix = $data ? ' ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : '';
|
||||||
|
|
||||||
|
return $e::class . ': ' . $e->getMessage() . $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出单个测试项的执行结果。
|
||||||
|
*/
|
||||||
|
private function writeResult(OutputInterface $output, string $case, string $status, string $message): void
|
||||||
|
{
|
||||||
|
$label = match ($status) {
|
||||||
|
'pass' => '<info>[通过]</info>',
|
||||||
|
'skip' => '<comment>[跳过]</comment>',
|
||||||
|
default => '<error>[失败]</error>',
|
||||||
|
};
|
||||||
|
|
||||||
|
$output->writeln(sprintf('%s %s - %s', $label, $case, $message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
70
app/command/SystemConfigSync.php
Normal file
70
app/command/SystemConfigSync.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\repository\system\config\SystemConfigRepository;
|
||||||
|
use app\service\system\config\SystemConfigDefinitionService;
|
||||||
|
use app\service\system\config\SystemConfigRuntimeService;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand('system:config-sync', '同步系统配置默认值到数据库')]
|
||||||
|
class SystemConfigSync extends Command
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('同步 config/system_config.php 中定义的系统配置默认值到数据库。');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var SystemConfigDefinitionService $definitionService */
|
||||||
|
$definitionService = container_make(SystemConfigDefinitionService::class, []);
|
||||||
|
/** @var SystemConfigRepository $repository */
|
||||||
|
$repository = container_make(SystemConfigRepository::class, []);
|
||||||
|
/** @var SystemConfigRuntimeService $runtimeService */
|
||||||
|
$runtimeService = container_make(SystemConfigRuntimeService::class, []);
|
||||||
|
|
||||||
|
$tabs = $definitionService->tabs();
|
||||||
|
$written = 0;
|
||||||
|
|
||||||
|
foreach ($tabs as $tab) {
|
||||||
|
$groupCode = (string) ($tab['key'] ?? '');
|
||||||
|
foreach ((array) ($tab['rules'] ?? []) as $rule) {
|
||||||
|
if (!is_array($rule)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$configKey = strtolower(trim((string) ($rule['field'] ?? '')));
|
||||||
|
if ($configKey === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repository->updateOrCreate(
|
||||||
|
['config_key' => $configKey],
|
||||||
|
[
|
||||||
|
'group_code' => $groupCode,
|
||||||
|
'config_value' => (string) ($rule['value'] ?? ''),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$written++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$runtimeService->refresh();
|
||||||
|
|
||||||
|
$output->writeln(sprintf('<info>系统配置同步完成</info>,写入 %d 项。', $written));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$output->writeln('<error>系统配置同步失败:' . $e->getMessage() . '</error>');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,49 +2,44 @@
|
|||||||
|
|
||||||
namespace app\common\base;
|
namespace app\common\base;
|
||||||
|
|
||||||
|
use app\exception\ValidationException;
|
||||||
|
use support\Context;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制器基础父类
|
* HTTP 层基础控制器。
|
||||||
*
|
*
|
||||||
* 约定统一的 JSON 返回结构:
|
* 统一提供响应封装、参数校验、请求上下文读取等通用能力。
|
||||||
* {
|
|
||||||
* "code": 200,
|
|
||||||
* "message": "success",
|
|
||||||
* "data": ...
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
class BaseController
|
class BaseController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 成功返回
|
* 返回成功响应。
|
||||||
*/
|
*/
|
||||||
protected function success(mixed $data = null, string $message = 'success', int $code = 200): Response
|
protected function success(mixed $data = null, string $message = '操作成功', int $code = 200): Response
|
||||||
{
|
{
|
||||||
return json([
|
return json([
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
'msg' => $message,
|
'msg' => $message,
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 失败返回
|
* 返回失败响应。
|
||||||
*/
|
*/
|
||||||
protected function fail(string $message = 'error', int $code = 500, mixed $data = null): Response
|
protected function fail(string $message = '操作失败', int $code = 500, mixed $data = null): Response
|
||||||
{
|
{
|
||||||
return json([
|
return json([
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
'msg' => $message,
|
'msg' => $message,
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一分页返回结构
|
* 返回统一分页响应。
|
||||||
*
|
|
||||||
* @param mixed $paginator Laravel/Eloquent paginator
|
|
||||||
*/
|
*/
|
||||||
protected function page(mixed $paginator): Response
|
protected function page(mixed $paginator): Response
|
||||||
{
|
{
|
||||||
@@ -58,28 +53,76 @@ class BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'list' => $paginator->items(),
|
'list' => $paginator->items(),
|
||||||
'total' => $paginator->total(),
|
'total' => $paginator->total(),
|
||||||
'page' => $paginator->currentPage(),
|
'page' => $paginator->currentPage(),
|
||||||
'size' => $paginator->perPage(),
|
'size' => $paginator->perPage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前登录用户的 token 载荷
|
* 通过校验器类验证请求数据。
|
||||||
*
|
*
|
||||||
* 从 AuthMiddleware 注入的用户信息中获取
|
* @param class-string $validatorClass
|
||||||
*/
|
*/
|
||||||
protected function currentUser(Request $request): ?array
|
protected function validated(array $data, string $validatorClass, ?string $scene = null): array
|
||||||
{
|
{
|
||||||
return $request->user ?? null;
|
$validator = $validatorClass::make($data);
|
||||||
|
|
||||||
|
if ($scene !== null) {
|
||||||
|
$validator = $validator->withScene($scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $validator
|
||||||
|
->withException(ValidationException::class)
|
||||||
|
->validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前登录用户ID
|
* 获取中间件预处理后的标准化参数。
|
||||||
*/
|
*/
|
||||||
protected function currentUserId(Request $request): int
|
protected function payload(Request $request): array
|
||||||
{
|
{
|
||||||
return (int) ($request->userId ?? 0);
|
$payload = (array) $request->all();
|
||||||
|
$normalized = Context::get('mpay.normalized_input', []);
|
||||||
|
|
||||||
|
if (is_array($normalized) && !empty($normalized)) {
|
||||||
|
$payload = array_replace($payload, $normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取请求属性。
|
||||||
|
*/
|
||||||
|
protected function requestAttribute(Request $request, string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return Context::get($key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中间件注入的当前管理员 ID。
|
||||||
|
*/
|
||||||
|
protected function currentAdminId(Request $request): int
|
||||||
|
{
|
||||||
|
return (int) $this->requestAttribute($request, 'auth.admin_id', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中间件注入的当前商户 ID。
|
||||||
|
*/
|
||||||
|
protected function currentMerchantId(Request $request): int
|
||||||
|
{
|
||||||
|
return (int) $this->requestAttribute($request, 'auth.merchant_id', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中间件注入的当前商户编号。
|
||||||
|
*/
|
||||||
|
protected function currentMerchantNo(Request $request): string
|
||||||
|
{
|
||||||
|
return (string) $this->requestAttribute($request, 'auth.merchant_no', '');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,50 @@
|
|||||||
|
|
||||||
namespace app\common\base;
|
namespace app\common\base;
|
||||||
|
|
||||||
|
use app\common\util\FormatHelper;
|
||||||
|
use DateTimeInterface;
|
||||||
use support\Model;
|
use support\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 所有业务模型的基础父类
|
* 所有业务模型的基础父类。
|
||||||
|
*
|
||||||
|
* 统一主键、时间戳和默认批量赋值策略。
|
||||||
*/
|
*/
|
||||||
class BaseModel extends Model
|
class BaseModel extends Model
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 约定所有主键字段名
|
* 默认主键字段名。
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $primaryKey = 'id';
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否自动维护 created_at / updated_at
|
* 是否自动维护 created_at / updated_at。
|
||||||
*
|
*
|
||||||
* 大部分业务表都有这两个字段,如不需要可在子类里覆盖为 false。
|
* 大部分业务表都包含这两个字段,如有例外可在子类中覆盖为 false。
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
public $timestamps = false;
|
public $timestamps = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认不禁止任何字段的批量赋值
|
* 默认仅保护主键,其他字段按子类 fillable 约束。
|
||||||
*
|
*
|
||||||
* 建议在具体模型中按需设置 $fillable 或 $guarded。
|
* 建议在具体模型中显式声明 $fillable。
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $guarded = [];
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一模型时间字段的 JSON 输出格式。
|
||||||
|
*
|
||||||
|
* 避免前端收到 ISO8601(如 2026-04-02T01:50:40.000000Z)这类不直观的时间串,
|
||||||
|
* 统一改为后台常用的本地展示格式。
|
||||||
|
*/
|
||||||
|
protected function serializeDate(DateTimeInterface $date): string
|
||||||
|
{
|
||||||
|
return FormatHelper::dateTime($date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\common\base;
|
namespace app\common\base;
|
||||||
|
|
||||||
use app\common\contracts\PayPluginInterface;
|
use app\common\interface\PayPluginInterface;
|
||||||
use app\exceptions\PaymentException;
|
use app\exception\PaymentException;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
@@ -21,7 +21,7 @@ use support\Log;
|
|||||||
* - 子类可在 `init()` 中配置第三方 SDK(例如 yansongda/pay)或读取必填参数。
|
* - 子类可在 `init()` 中配置第三方 SDK(例如 yansongda/pay)或读取必填参数。
|
||||||
*
|
*
|
||||||
* 约定:
|
* 约定:
|
||||||
* - 这里的 `$channelConfig` 来源通常是 `ma_pay_channel.config_json`,属于“通道级配置”。
|
* - 这里的 `$channelConfig` 来源通常是 `ma_payment_plugin_conf.config`,并附带通道维度上下文。
|
||||||
* - 业务级入参(如订单号、金额、回调地址等)不要混进 `$channelConfig`,应从 `pay()` 的 `$order` 参数获取。
|
* - 业务级入参(如订单号、金额、回调地址等)不要混进 `$channelConfig`,应从 `pay()` 的 `$order` 参数获取。
|
||||||
*/
|
*/
|
||||||
abstract class BasePayment implements PayPluginInterface
|
abstract class BasePayment implements PayPluginInterface
|
||||||
@@ -108,6 +108,12 @@ abstract class BasePayment implements PayPluginInterface
|
|||||||
return $this->paymentInfo['link'] ?? '';
|
return $this->paymentInfo['link'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取版本号 */
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return $this->paymentInfo['version'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 能力声明 ====================
|
// ==================== 能力声明 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,65 +2,194 @@
|
|||||||
|
|
||||||
namespace app\common\base;
|
namespace app\common\base;
|
||||||
|
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
use support\Model;
|
use support\Model;
|
||||||
|
use support\Db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仓储层基础父类
|
* 仓储层基础类。
|
||||||
*
|
*
|
||||||
* 封装单表常用的 CRUD / 分页操作,具体仓储继承后可扩展业务查询。
|
* 封装通用 CRUD、条件查询、加锁查询和分页查询能力。
|
||||||
*/
|
*/
|
||||||
abstract class BaseRepository
|
abstract class BaseRepository
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Model
|
* 当前仓储绑定的模型实例。
|
||||||
*/
|
*/
|
||||||
protected Model $model;
|
protected Model $model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数,绑定模型实例。
|
||||||
|
*/
|
||||||
public function __construct(Model $model)
|
public function __construct(Model $model)
|
||||||
{
|
{
|
||||||
$this->model = $model;
|
$this->model = $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据主键查询
|
* 获取查询构造器。
|
||||||
*/
|
*/
|
||||||
public function find(int $id, array $columns = ['*']): ?Model
|
public function query()
|
||||||
{
|
{
|
||||||
return $this->model->newQuery()->find($id, $columns);
|
return $this->model->newQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新建记录
|
* 按主键查询记录。
|
||||||
|
*/
|
||||||
|
public function find(int|string $id, array $columns = ['*']): ?Model
|
||||||
|
{
|
||||||
|
return $this->query()->find($id, $columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增记录。
|
||||||
*/
|
*/
|
||||||
public function create(array $data): Model
|
public function create(array $data): Model
|
||||||
{
|
{
|
||||||
return $this->model->newQuery()->create($data);
|
return $this->query()->create($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按主键更新
|
* 按主键更新记录。
|
||||||
*/
|
*/
|
||||||
public function updateById(int $id, array $data): bool
|
public function updateById(int|string $id, array $data): bool
|
||||||
{
|
{
|
||||||
return (bool) $this->model->newQuery()->whereKey($id)->update($data);
|
return (bool) $this->query()->whereKey($id)->update($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按主键删除
|
* 按唯一键更新记录。
|
||||||
*/
|
*/
|
||||||
public function deleteById(int $id): bool
|
public function updateByKey(int|string $key, array $data): bool
|
||||||
{
|
{
|
||||||
return (bool) $this->model->newQuery()->whereKey($id)->delete();
|
return (bool) $this->query()->whereKey($key)->update($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 简单分页查询示例
|
* 按条件批量更新记录。
|
||||||
|
*/
|
||||||
|
public function updateWhere(array $where, array $data): int
|
||||||
|
{
|
||||||
|
$query = $this->query();
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$query->where($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主键删除记录。
|
||||||
|
*/
|
||||||
|
public function deleteById(int|string $id): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->query()->whereKey($id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按条件批量删除记录。
|
||||||
|
*/
|
||||||
|
public function deleteWhere(array $where): int
|
||||||
|
{
|
||||||
|
$query = $this->query();
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$query->where($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按条件获取首条记录。
|
||||||
|
*/
|
||||||
|
public function firstBy(array $where = [], array $columns = ['*']): ?Model
|
||||||
|
{
|
||||||
|
$query = $this->query();
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$query->where($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->first($columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 先查后更,不存在则创建。
|
||||||
|
*/
|
||||||
|
public function updateOrCreate(array $where, array $data = []): Model
|
||||||
|
{
|
||||||
|
if ($where === []) {
|
||||||
|
return $this->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Db::transaction(function () use ($where, $data): Model {
|
||||||
|
$query = $this->query()->lockForUpdate();
|
||||||
|
$query->where($where);
|
||||||
|
|
||||||
|
/** @var Model|null $model */
|
||||||
|
$model = $query->first();
|
||||||
|
if ($model) {
|
||||||
|
$model->fill($data);
|
||||||
|
$model->save();
|
||||||
|
|
||||||
|
return $model->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->create(array_merge($where, $data));
|
||||||
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
|
$model = $this->firstBy($where);
|
||||||
|
if (!$model) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->fill($data);
|
||||||
|
$model->save();
|
||||||
|
|
||||||
|
return $model->refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按条件统计数量。
|
||||||
|
*/
|
||||||
|
public function countBy(array $where = []): int
|
||||||
|
{
|
||||||
|
$query = $this->query();
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$query->where($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断条件下是否存在记录。
|
||||||
|
*/
|
||||||
|
public function existsBy(array $where = []): bool
|
||||||
|
{
|
||||||
|
$query = $this->query();
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$query->where($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询。
|
||||||
*
|
*
|
||||||
* @param array $where ['字段' => 值],值为 null / '' 时会被忽略
|
* @param array $where 条件数组,空值会被忽略
|
||||||
*/
|
*/
|
||||||
public function paginate(array $where = [], int $page = 1, int $pageSize = 10, array $columns = ['*'])
|
public function paginate(array $where = [], int $page = 1, int $pageSize = 10, array $columns = ['*'])
|
||||||
{
|
{
|
||||||
$query = $this->model->newQuery();
|
$query = $this->query();
|
||||||
|
|
||||||
if (!empty($where)) {
|
if (!empty($where)) {
|
||||||
$query->where($where);
|
$query->where($where);
|
||||||
|
|||||||
@@ -2,23 +2,172 @@
|
|||||||
|
|
||||||
namespace app\common\base;
|
namespace app\common\base;
|
||||||
|
|
||||||
|
use app\common\util\FormatHelper;
|
||||||
use support\Db;
|
use support\Db;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务服务层基础父类
|
* 业务服务层基础类。
|
||||||
|
*
|
||||||
|
* 统一承载业务单号生成、时间获取和事务封装等通用能力。
|
||||||
*/
|
*/
|
||||||
class BaseService
|
class BaseService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 事务封装
|
* 生成业务单号。
|
||||||
*
|
*
|
||||||
* 使用方式:
|
* 适用于 biz_no / pay_no / refund_no / settle_no / notify_no / ledger_no 等场景。
|
||||||
* $this->transaction(function () { ... });
|
* 默认使用时间前缀 + 随机数,保证可读性和基本唯一性。
|
||||||
|
*/
|
||||||
|
protected function generateNo(string $prefix = ''): string
|
||||||
|
{
|
||||||
|
$time = FormatHelper::timestamp(time(), 'YmdHis');
|
||||||
|
$rand = (string) random_int(100000, 999999);
|
||||||
|
|
||||||
|
return $prefix === '' ? $time . $rand : $prefix . $time . $rand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前时间字符串。
|
||||||
|
*
|
||||||
|
* 统一返回 `Y-m-d H:i:s` 格式,便于数据库写入和日志输出。
|
||||||
|
*/
|
||||||
|
protected function now(): string
|
||||||
|
{
|
||||||
|
return FormatHelper::timestamp(time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额格式化,单位为元。
|
||||||
|
*/
|
||||||
|
protected function formatAmount(int $amount): string
|
||||||
|
{
|
||||||
|
return FormatHelper::amount($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额格式化,0 时显示不限。
|
||||||
|
*/
|
||||||
|
protected function formatAmountOrUnlimited(int $amount): string
|
||||||
|
{
|
||||||
|
return FormatHelper::amountOrUnlimited($amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 次数格式化,0 时显示不限。
|
||||||
|
*/
|
||||||
|
protected function formatCountOrUnlimited(int $count): string
|
||||||
|
{
|
||||||
|
return FormatHelper::countOrUnlimited($count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 费率格式化,单位为百分点。
|
||||||
|
*/
|
||||||
|
protected function formatRate(int $basisPoints): string
|
||||||
|
{
|
||||||
|
return FormatHelper::rate($basisPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟格式化。
|
||||||
|
*/
|
||||||
|
protected function formatLatency(int $latencyMs): string
|
||||||
|
{
|
||||||
|
return FormatHelper::latency($latencyMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期格式化。
|
||||||
|
*/
|
||||||
|
protected function formatDate(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
return FormatHelper::date($value, $emptyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期时间格式化。
|
||||||
|
*/
|
||||||
|
protected function formatDateTime(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
return FormatHelper::dateTime($value, $emptyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 文本格式化。
|
||||||
|
*/
|
||||||
|
protected function formatJson(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
return FormatHelper::json($value, $emptyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射表文本转换。
|
||||||
|
*/
|
||||||
|
protected function textFromMap(int $value, array $map, string $default = '未知'): string
|
||||||
|
{
|
||||||
|
return FormatHelper::textFromMap($value, $map, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口凭证明文脱敏。
|
||||||
|
*/
|
||||||
|
protected function maskCredentialValue(string $credentialValue, bool $maskShortValue = true): string
|
||||||
|
{
|
||||||
|
return FormatHelper::maskCredentialValue($credentialValue, $maskShortValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将模型或对象归一化成数组。
|
||||||
|
*/
|
||||||
|
protected function normalizeModel(mixed $value): ?array
|
||||||
|
{
|
||||||
|
return FormatHelper::normalizeModel($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事务封装。
|
||||||
|
*
|
||||||
|
* 适合单次数据库事务,不包含自动重试逻辑。
|
||||||
|
*
|
||||||
|
* @param callable $callback 事务体
|
||||||
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
protected function transaction(callable $callback)
|
protected function transaction(callable $callback)
|
||||||
{
|
{
|
||||||
return Db::connection()->transaction(function () use ($callback) {
|
return Db::transaction(function () use ($callback) {
|
||||||
return $callback();
|
return $callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持重试的事务封装。
|
||||||
|
*
|
||||||
|
* 适合余额冻结、扣减、状态推进和幂等写入等容易发生锁冲突的场景。
|
||||||
|
*/
|
||||||
|
protected function transactionRetry(callable $callback, int $attempts = 3, int $sleepMs = 50)
|
||||||
|
{
|
||||||
|
$attempts = max(1, $attempts);
|
||||||
|
|
||||||
|
beginning:
|
||||||
|
try {
|
||||||
|
return $this->transaction($callback);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$message = strtolower($e->getMessage());
|
||||||
|
$retryable = str_contains($message, 'deadlock')
|
||||||
|
|| str_contains($message, 'lock wait timeout')
|
||||||
|
|| str_contains($message, 'try restarting transaction');
|
||||||
|
|
||||||
|
if (--$attempts > 0 && $retryable) {
|
||||||
|
if ($sleepMs > 0) {
|
||||||
|
usleep($sleepMs * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
goto beginning;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
39
app/common/constant/AuthConstant.php
Normal file
39
app/common/constant/AuthConstant.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证相关常量。
|
||||||
|
*
|
||||||
|
* 统一管理登录域、令牌状态、签名类型等枚举值。
|
||||||
|
*/
|
||||||
|
final class AuthConstant
|
||||||
|
{
|
||||||
|
public const GUARD_ADMIN = 1;
|
||||||
|
public const GUARD_MERCHANT = 2;
|
||||||
|
|
||||||
|
public const JWT_ALG_HS256 = 'HS256';
|
||||||
|
|
||||||
|
public const TOKEN_STATUS_DISABLED = 0;
|
||||||
|
public const TOKEN_STATUS_ENABLED = 1;
|
||||||
|
|
||||||
|
public const LOGIN_STATUS_DISABLED = 0;
|
||||||
|
public const LOGIN_STATUS_ENABLED = 1;
|
||||||
|
|
||||||
|
public const API_SIGN_TYPE_MD5 = 0;
|
||||||
|
|
||||||
|
public static function signTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::API_SIGN_TYPE_MD5 => 'MD5',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function guardMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::GUARD_ADMIN => 'admin',
|
||||||
|
self::GUARD_MERCHANT => 'merchant',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/common/constant/CommonConstant.php
Normal file
31
app/common/constant/CommonConstant.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用状态常量。
|
||||||
|
*/
|
||||||
|
final class CommonConstant
|
||||||
|
{
|
||||||
|
public const STATUS_DISABLED = 0;
|
||||||
|
public const STATUS_ENABLED = 1;
|
||||||
|
|
||||||
|
public const NO = 0;
|
||||||
|
public const YES = 1;
|
||||||
|
|
||||||
|
public static function statusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_DISABLED => '禁用',
|
||||||
|
self::STATUS_ENABLED => '启用',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function yesNoMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::NO => '否',
|
||||||
|
self::YES => '是',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
135
app/common/constant/FileConstant.php
Normal file
135
app/common/constant/FileConstant.php
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件相关常量。
|
||||||
|
*/
|
||||||
|
final class FileConstant
|
||||||
|
{
|
||||||
|
public const SOURCE_UPLOAD = 1;
|
||||||
|
public const SOURCE_REMOTE_URL = 2;
|
||||||
|
|
||||||
|
public const VISIBILITY_PUBLIC = 1;
|
||||||
|
public const VISIBILITY_PRIVATE = 2;
|
||||||
|
|
||||||
|
public const SCENE_IMAGE = 1;
|
||||||
|
public const SCENE_CERTIFICATE = 2;
|
||||||
|
public const SCENE_TEXT = 3;
|
||||||
|
public const SCENE_OTHER = 4;
|
||||||
|
|
||||||
|
public const STORAGE_LOCAL = 1;
|
||||||
|
public const STORAGE_ALIYUN_OSS = 2;
|
||||||
|
public const STORAGE_TENCENT_COS = 3;
|
||||||
|
public const STORAGE_REMOTE_URL = 4;
|
||||||
|
|
||||||
|
public const CONFIG_DEFAULT_ENGINE = 'file_storage_default_engine';
|
||||||
|
public const CONFIG_LOCAL_PUBLIC_BASE_URL = 'file_storage_local_public_base_url';
|
||||||
|
public const CONFIG_LOCAL_PUBLIC_DIR = 'file_storage_local_public_dir';
|
||||||
|
public const CONFIG_LOCAL_PRIVATE_DIR = 'file_storage_local_private_dir';
|
||||||
|
public const CONFIG_UPLOAD_MAX_SIZE_MB = 'file_storage_upload_max_size_mb';
|
||||||
|
public const CONFIG_REMOTE_DOWNLOAD_LIMIT_MB = 'file_storage_remote_download_limit_mb';
|
||||||
|
public const CONFIG_ALLOWED_EXTENSIONS = 'file_storage_allowed_extensions';
|
||||||
|
public const CONFIG_OSS_ENDPOINT = 'file_storage_aliyun_oss_endpoint';
|
||||||
|
public const CONFIG_OSS_BUCKET = 'file_storage_aliyun_oss_bucket';
|
||||||
|
public const CONFIG_OSS_ACCESS_KEY_ID = 'file_storage_aliyun_oss_access_key_id';
|
||||||
|
public const CONFIG_OSS_ACCESS_KEY_SECRET = 'file_storage_aliyun_oss_access_key_secret';
|
||||||
|
public const CONFIG_OSS_PUBLIC_DOMAIN = 'file_storage_aliyun_oss_public_domain';
|
||||||
|
public const CONFIG_OSS_REGION = 'file_storage_aliyun_oss_region';
|
||||||
|
public const CONFIG_COS_REGION = 'file_storage_tencent_cos_region';
|
||||||
|
public const CONFIG_COS_BUCKET = 'file_storage_tencent_cos_bucket';
|
||||||
|
public const CONFIG_COS_SECRET_ID = 'file_storage_tencent_cos_secret_id';
|
||||||
|
public const CONFIG_COS_SECRET_KEY = 'file_storage_tencent_cos_secret_key';
|
||||||
|
public const CONFIG_COS_PUBLIC_DOMAIN = 'file_storage_tencent_cos_public_domain';
|
||||||
|
|
||||||
|
public static function sourceTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SOURCE_UPLOAD => '上传',
|
||||||
|
self::SOURCE_REMOTE_URL => '远程导入',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function visibilityMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::VISIBILITY_PUBLIC => '公开',
|
||||||
|
self::VISIBILITY_PRIVATE => '私有',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sceneMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SCENE_IMAGE => '图片',
|
||||||
|
self::SCENE_CERTIFICATE => '证书',
|
||||||
|
self::SCENE_TEXT => '文本',
|
||||||
|
self::SCENE_OTHER => '其他',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function storageEngineMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STORAGE_LOCAL => '本地存储',
|
||||||
|
self::STORAGE_ALIYUN_OSS => '阿里云 OSS',
|
||||||
|
self::STORAGE_TENCENT_COS => '腾讯云 COS',
|
||||||
|
self::STORAGE_REMOTE_URL => '远程引用',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function selectableStorageEngineMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STORAGE_LOCAL => '本地存储',
|
||||||
|
self::STORAGE_ALIYUN_OSS => '阿里云 OSS',
|
||||||
|
self::STORAGE_TENCENT_COS => '腾讯云 COS',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function imageExtensionMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'jpg' => true,
|
||||||
|
'jpeg' => true,
|
||||||
|
'png' => true,
|
||||||
|
'gif' => true,
|
||||||
|
'webp' => true,
|
||||||
|
'bmp' => true,
|
||||||
|
'svg' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function certificateExtensionMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'pem' => true,
|
||||||
|
'crt' => true,
|
||||||
|
'cer' => true,
|
||||||
|
'key' => true,
|
||||||
|
'p12' => true,
|
||||||
|
'pfx' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function textExtensionMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'txt' => true,
|
||||||
|
'log' => true,
|
||||||
|
'csv' => true,
|
||||||
|
'json' => true,
|
||||||
|
'xml' => true,
|
||||||
|
'md' => true,
|
||||||
|
'ini' => true,
|
||||||
|
'conf' => true,
|
||||||
|
'yaml' => true,
|
||||||
|
'yml' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultAllowedExtensions(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::imageExtensionMap() + self::certificateExtensionMap() + self::textExtensionMap());
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/common/constant/LedgerConstant.php
Normal file
54
app/common/constant/LedgerConstant.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户流水相关枚举。
|
||||||
|
*/
|
||||||
|
final class LedgerConstant
|
||||||
|
{
|
||||||
|
public const BIZ_TYPE_PAY_FREEZE = 0;
|
||||||
|
public const BIZ_TYPE_PAY_DEDUCT = 1;
|
||||||
|
public const BIZ_TYPE_PAY_RELEASE = 2;
|
||||||
|
public const BIZ_TYPE_SETTLEMENT_CREDIT = 3;
|
||||||
|
public const BIZ_TYPE_REFUND_REVERSE = 4;
|
||||||
|
public const BIZ_TYPE_MANUAL_ADJUST = 5;
|
||||||
|
|
||||||
|
public const EVENT_TYPE_CREATE = 0;
|
||||||
|
public const EVENT_TYPE_SUCCESS = 1;
|
||||||
|
public const EVENT_TYPE_FAILED = 2;
|
||||||
|
public const EVENT_TYPE_REVERSE = 3;
|
||||||
|
|
||||||
|
public const DIRECTION_IN = 0;
|
||||||
|
public const DIRECTION_OUT = 1;
|
||||||
|
|
||||||
|
public static function bizTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::BIZ_TYPE_PAY_FREEZE => '支付冻结',
|
||||||
|
self::BIZ_TYPE_PAY_DEDUCT => '支付扣费',
|
||||||
|
self::BIZ_TYPE_PAY_RELEASE => '支付释放',
|
||||||
|
self::BIZ_TYPE_SETTLEMENT_CREDIT => '清算入账',
|
||||||
|
self::BIZ_TYPE_REFUND_REVERSE => '退款冲正',
|
||||||
|
self::BIZ_TYPE_MANUAL_ADJUST => '人工调整',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function eventTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::EVENT_TYPE_CREATE => '创建',
|
||||||
|
self::EVENT_TYPE_SUCCESS => '成功',
|
||||||
|
self::EVENT_TYPE_FAILED => '失败',
|
||||||
|
self::EVENT_TYPE_REVERSE => '冲正',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function directionMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::DIRECTION_IN => '入账',
|
||||||
|
self::DIRECTION_OUT => '出账',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/common/constant/MerchantConstant.php
Normal file
35
app/common/constant/MerchantConstant.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户相关枚举。
|
||||||
|
*/
|
||||||
|
final class MerchantConstant
|
||||||
|
{
|
||||||
|
public const TYPE_PERSON = 0;
|
||||||
|
public const TYPE_COMPANY = 1;
|
||||||
|
public const TYPE_OTHER = 2;
|
||||||
|
|
||||||
|
public const RISK_LOW = 0;
|
||||||
|
public const RISK_MEDIUM = 1;
|
||||||
|
public const RISK_HIGH = 2;
|
||||||
|
|
||||||
|
public static function typeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::TYPE_PERSON => '个人',
|
||||||
|
self::TYPE_COMPANY => '企业',
|
||||||
|
self::TYPE_OTHER => '其他',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function riskLevelMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::RISK_LOW => '低',
|
||||||
|
self::RISK_MEDIUM => '中',
|
||||||
|
self::RISK_HIGH => '高',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
70
app/common/constant/NotifyConstant.php
Normal file
70
app/common/constant/NotifyConstant.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知与回调相关枚举。
|
||||||
|
*/
|
||||||
|
final class NotifyConstant
|
||||||
|
{
|
||||||
|
public const NOTIFY_TYPE_ASYNC = 0;
|
||||||
|
public const NOTIFY_TYPE_QUERY = 1;
|
||||||
|
|
||||||
|
public const CALLBACK_TYPE_ASYNC = 0;
|
||||||
|
public const CALLBACK_TYPE_SYNC = 1;
|
||||||
|
|
||||||
|
public const VERIFY_STATUS_UNKNOWN = 0;
|
||||||
|
public const VERIFY_STATUS_SUCCESS = 1;
|
||||||
|
public const VERIFY_STATUS_FAILED = 2;
|
||||||
|
|
||||||
|
public const PROCESS_STATUS_PENDING = 0;
|
||||||
|
public const PROCESS_STATUS_SUCCESS = 1;
|
||||||
|
public const PROCESS_STATUS_FAILED = 2;
|
||||||
|
|
||||||
|
public const TASK_STATUS_PENDING = 0;
|
||||||
|
public const TASK_STATUS_SUCCESS = 1;
|
||||||
|
public const TASK_STATUS_FAILED = 2;
|
||||||
|
|
||||||
|
public static function notifyTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::NOTIFY_TYPE_ASYNC => '异步通知',
|
||||||
|
self::NOTIFY_TYPE_QUERY => '查单',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function callbackTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::CALLBACK_TYPE_ASYNC => '异步通知',
|
||||||
|
self::CALLBACK_TYPE_SYNC => '同步返回',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verifyStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::VERIFY_STATUS_UNKNOWN => '未知',
|
||||||
|
self::VERIFY_STATUS_SUCCESS => '成功',
|
||||||
|
self::VERIFY_STATUS_FAILED => '失败',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function processStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::PROCESS_STATUS_PENDING => '待处理',
|
||||||
|
self::PROCESS_STATUS_SUCCESS => '成功',
|
||||||
|
self::PROCESS_STATUS_FAILED => '失败',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function taskStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::TASK_STATUS_PENDING => '待通知',
|
||||||
|
self::TASK_STATUS_SUCCESS => '成功',
|
||||||
|
self::TASK_STATUS_FAILED => '失败',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/common/constant/RouteConstant.php
Normal file
55
app/common/constant/RouteConstant.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由与通道编排相关枚举。
|
||||||
|
*/
|
||||||
|
final class RouteConstant
|
||||||
|
{
|
||||||
|
public const CHANNEL_TYPE_PLATFORM_COLLECT = 0;
|
||||||
|
public const CHANNEL_TYPE_MERCHANT_SELF = 1;
|
||||||
|
|
||||||
|
public const CHANNEL_MODE_COLLECT = 0;
|
||||||
|
public const CHANNEL_MODE_SELF = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询组模式:按编排顺序依次轮询可用通道。
|
||||||
|
*/
|
||||||
|
public const ROUTE_MODE_ORDER = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询组模式:按通道权重随机选择可用通道。
|
||||||
|
*/
|
||||||
|
public const ROUTE_MODE_WEIGHTED = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询组模式:优先选择默认启用通道,默认不可用时回退到首个可用通道。
|
||||||
|
*/
|
||||||
|
public const ROUTE_MODE_FIRST_AVAILABLE = 2;
|
||||||
|
|
||||||
|
public static function channelTypeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::CHANNEL_TYPE_PLATFORM_COLLECT => '平台代收',
|
||||||
|
self::CHANNEL_TYPE_MERCHANT_SELF => '商户自有',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function channelModeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::CHANNEL_MODE_COLLECT => '代收',
|
||||||
|
self::CHANNEL_MODE_SELF => '自收',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function routeModeMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::ROUTE_MODE_ORDER => '顺序依次轮询',
|
||||||
|
self::ROUTE_MODE_WEIGHTED => '权重随机轮询',
|
||||||
|
self::ROUTE_MODE_FIRST_AVAILABLE => '默认启用通道',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
157
app/common/constant/TradeConstant.php
Normal file
157
app/common/constant/TradeConstant.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易、订单与结算相关枚举。
|
||||||
|
*/
|
||||||
|
final class TradeConstant
|
||||||
|
{
|
||||||
|
public const SETTLEMENT_CYCLE_D0 = 0;
|
||||||
|
public const SETTLEMENT_CYCLE_D1 = 1;
|
||||||
|
public const SETTLEMENT_CYCLE_D7 = 2;
|
||||||
|
public const SETTLEMENT_CYCLE_T1 = 3;
|
||||||
|
public const SETTLEMENT_CYCLE_OTHER = 4;
|
||||||
|
|
||||||
|
public const ORDER_STATUS_CREATED = 0;
|
||||||
|
public const ORDER_STATUS_PAYING = 1;
|
||||||
|
public const ORDER_STATUS_SUCCESS = 2;
|
||||||
|
public const ORDER_STATUS_FAILED = 3;
|
||||||
|
public const ORDER_STATUS_CLOSED = 4;
|
||||||
|
public const ORDER_STATUS_TIMEOUT = 5;
|
||||||
|
|
||||||
|
public const FEE_STATUS_NONE = 0;
|
||||||
|
public const FEE_STATUS_FROZEN = 1;
|
||||||
|
public const FEE_STATUS_DEDUCTED = 2;
|
||||||
|
public const FEE_STATUS_RELEASED = 3;
|
||||||
|
|
||||||
|
public const SETTLEMENT_STATUS_NONE = 0;
|
||||||
|
public const SETTLEMENT_STATUS_PENDING = 1;
|
||||||
|
public const SETTLEMENT_STATUS_SETTLED = 2;
|
||||||
|
public const SETTLEMENT_STATUS_REVERSED = 3;
|
||||||
|
|
||||||
|
public const REFUND_STATUS_CREATED = 0;
|
||||||
|
public const REFUND_STATUS_PROCESSING = 1;
|
||||||
|
public const REFUND_STATUS_SUCCESS = 2;
|
||||||
|
public const REFUND_STATUS_FAILED = 3;
|
||||||
|
public const REFUND_STATUS_CLOSED = 4;
|
||||||
|
|
||||||
|
public static function settlementCycleMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SETTLEMENT_CYCLE_D0 => 'D0',
|
||||||
|
self::SETTLEMENT_CYCLE_D1 => 'D1',
|
||||||
|
self::SETTLEMENT_CYCLE_D7 => 'D7',
|
||||||
|
self::SETTLEMENT_CYCLE_T1 => 'T1',
|
||||||
|
self::SETTLEMENT_CYCLE_OTHER => 'OTHER',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function orderStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::ORDER_STATUS_CREATED => '待创建',
|
||||||
|
self::ORDER_STATUS_PAYING => '支付中',
|
||||||
|
self::ORDER_STATUS_SUCCESS => '成功',
|
||||||
|
self::ORDER_STATUS_FAILED => '失败',
|
||||||
|
self::ORDER_STATUS_CLOSED => '关闭',
|
||||||
|
self::ORDER_STATUS_TIMEOUT => '超时',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function feeStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::FEE_STATUS_NONE => '无',
|
||||||
|
self::FEE_STATUS_FROZEN => '冻结',
|
||||||
|
self::FEE_STATUS_DEDUCTED => '已扣',
|
||||||
|
self::FEE_STATUS_RELEASED => '已释放',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function settlementStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SETTLEMENT_STATUS_NONE => '无',
|
||||||
|
self::SETTLEMENT_STATUS_PENDING => '待清算',
|
||||||
|
self::SETTLEMENT_STATUS_SETTLED => '已清算',
|
||||||
|
self::SETTLEMENT_STATUS_REVERSED => '已冲正',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function refundStatusMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::REFUND_STATUS_CREATED => '待创建',
|
||||||
|
self::REFUND_STATUS_PROCESSING => '处理中',
|
||||||
|
self::REFUND_STATUS_SUCCESS => '成功',
|
||||||
|
self::REFUND_STATUS_FAILED => '失败',
|
||||||
|
self::REFUND_STATUS_CLOSED => '关闭',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function orderMutableStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::ORDER_STATUS_CREATED,
|
||||||
|
self::ORDER_STATUS_PAYING,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function orderTerminalStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::ORDER_STATUS_SUCCESS,
|
||||||
|
self::ORDER_STATUS_FAILED,
|
||||||
|
self::ORDER_STATUS_CLOSED,
|
||||||
|
self::ORDER_STATUS_TIMEOUT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isOrderTerminalStatus(int $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, self::orderTerminalStatuses(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function refundMutableStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::REFUND_STATUS_CREATED,
|
||||||
|
self::REFUND_STATUS_PROCESSING,
|
||||||
|
self::REFUND_STATUS_FAILED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function refundTerminalStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::REFUND_STATUS_SUCCESS,
|
||||||
|
self::REFUND_STATUS_CLOSED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isRefundTerminalStatus(int $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, self::refundTerminalStatuses(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function settlementMutableStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SETTLEMENT_STATUS_PENDING,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function settlementTerminalStatuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::SETTLEMENT_STATUS_SETTLED,
|
||||||
|
self::SETTLEMENT_STATUS_REVERSED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isSettlementTerminalStatus(int $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, self::settlementTerminalStatuses(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\constants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用是/否布尔枚举
|
|
||||||
* 可复用在 is_admin 等字段
|
|
||||||
*/
|
|
||||||
class YesNo
|
|
||||||
{
|
|
||||||
public const NO = 0;
|
|
||||||
public const YES = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\enums;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单类型枚举
|
|
||||||
* 对应表:menus.type
|
|
||||||
* 1 目录 2 菜单 3 按钮
|
|
||||||
*/
|
|
||||||
class MenuType
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 目录
|
|
||||||
*/
|
|
||||||
public const DIRECTORY = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单
|
|
||||||
*/
|
|
||||||
public const MENU = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按钮(权限点)
|
|
||||||
*/
|
|
||||||
public const BUTTON = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace app\common\contracts;
|
namespace app\common\interface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付插件“基础契约”接口
|
* 支付插件“基础契约”接口
|
||||||
@@ -11,7 +11,7 @@ namespace app\common\contracts;
|
|||||||
* - `PaymentInterface`:支付动作能力(下单/查询/关单/退款/回调)。
|
* - `PaymentInterface`:支付动作能力(下单/查询/关单/退款/回调)。
|
||||||
*
|
*
|
||||||
* 约定:
|
* 约定:
|
||||||
* - `init()` 会在每次发起支付/退款等动作前由服务层调用,用于注入该通道的 `config_json`。
|
* - `init()` 会在每次发起支付/退款等动作前由服务层调用,用于注入该通道对应的插件配置。
|
||||||
* - 元信息方法应为“纯读取”,不要依赖外部状态或数据库。
|
* - 元信息方法应为“纯读取”,不要依赖外部状态或数据库。
|
||||||
*/
|
*/
|
||||||
interface PayPluginInterface
|
interface PayPluginInterface
|
||||||
@@ -19,19 +19,28 @@ interface PayPluginInterface
|
|||||||
/**
|
/**
|
||||||
* 初始化插件(注入通道配置)
|
* 初始化插件(注入通道配置)
|
||||||
*
|
*
|
||||||
* 典型来源:`ma_pay_channel.config_json`(以及服务层合并的 enabled_products 等)。
|
* 典型来源:`ma_payment_plugin_conf.config`,并由服务层额外合并通道信息、支付方式声明等上下文。
|
||||||
* 插件应在这里完成:缓存配置、初始化 SDK/HTTP 客户端等。
|
* 插件应在这里完成:缓存配置、初始化 SDK/HTTP 客户端等。
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $channelConfig
|
* @param array<string, mixed> $channelConfig
|
||||||
*/
|
*/
|
||||||
public function init(array $channelConfig): void;
|
public function init(array $channelConfig): void;
|
||||||
|
|
||||||
/** 插件代码(与 ma_pay_plugin.plugin_code 对应) */
|
/** 插件代码(与 ma_payment_plugin.code 对应) */
|
||||||
public function getCode(): string;
|
public function getCode(): string;
|
||||||
|
|
||||||
/** 插件名称(用于后台展示) */
|
/** 插件名称(用于后台展示) */
|
||||||
public function getName(): string;
|
public function getName(): string;
|
||||||
|
|
||||||
|
/** 插件作者名称(用于后台展示) */
|
||||||
|
public function getAuthorName(): string;
|
||||||
|
|
||||||
|
/** 插件作者链接(用于后台展示) */
|
||||||
|
public function getAuthorLink(): string;
|
||||||
|
|
||||||
|
/** 插件版本号(用于后台展示) */
|
||||||
|
public function getVersion(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件声明支持的支付方式编码
|
* 插件声明支持的支付方式编码
|
||||||
*
|
*
|
||||||
@@ -39,6 +48,9 @@ interface PayPluginInterface
|
|||||||
*/
|
*/
|
||||||
public function getEnabledPayTypes(): array;
|
public function getEnabledPayTypes(): array;
|
||||||
|
|
||||||
|
/** 插件声明支持的转账方式编码 */
|
||||||
|
public function getEnabledTransferTypes(): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件配置结构(用于后台渲染表单/校验)
|
* 插件配置结构(用于后台渲染表单/校验)
|
||||||
*
|
*
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace app\common\contracts;
|
namespace app\common\interface;
|
||||||
|
|
||||||
use app\exceptions\PaymentException;
|
use app\exception\PaymentException;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
|
||||||
@@ -24,11 +24,12 @@ interface PaymentInterface
|
|||||||
* 统一下单
|
* 统一下单
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $order 订单数据,通常包含:
|
* @param array<string, mixed> $order 订单数据,通常包含:
|
||||||
* - order_id: 系统订单号
|
* - order_id: 系统支付单号,建议直接使用 pay_no
|
||||||
* - mch_no: 商户号
|
* - amount: 金额(分)
|
||||||
* - amount: 金额(元)
|
|
||||||
* - subject: 商品标题
|
* - subject: 商品标题
|
||||||
* - body: 商品描述
|
* - body: 商品描述
|
||||||
|
* - callback_url: 第三方异步回调地址(回调到本系统)
|
||||||
|
* - return_url: 支付完成跳转地址
|
||||||
* @return array<string, mixed> 支付参数,需包含 pay_params、chan_order_no、chan_trade_no
|
* @return array<string, mixed> 支付参数,需包含 pay_params、chan_order_no、chan_trade_no
|
||||||
* @throws PaymentException 下单失败、渠道异常、参数错误等
|
* @throws PaymentException 下单失败、渠道异常、参数错误等
|
||||||
*/
|
*/
|
||||||
@@ -59,9 +60,9 @@ interface PaymentInterface
|
|||||||
* 申请退款
|
* 申请退款
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $order 退款数据,通常包含:
|
* @param array<string, mixed> $order 退款数据,通常包含:
|
||||||
* - order_id: 原订单号
|
* - order_id: 原支付单号
|
||||||
* - chan_order_no: 渠道订单号
|
* - chan_order_no: 渠道订单号
|
||||||
* - refund_amount: 退款金额
|
* - refund_amount: 退款金额(分)
|
||||||
* - refund_no: 退款单号
|
* - refund_no: 退款单号
|
||||||
* @return array<string, mixed> 退款结果,通常包含 success、chan_refund_no、msg
|
* @return array<string, mixed> 退款结果,通常包含 success、chan_refund_no、msg
|
||||||
* @throws PaymentException 退款失败、渠道异常等
|
* @throws PaymentException 退款失败、渠道异常等
|
||||||
@@ -75,15 +76,24 @@ interface PaymentInterface
|
|||||||
*
|
*
|
||||||
* @param Request $request 支付渠道的异步通知请求(GET/POST 参数)
|
* @param Request $request 支付渠道的异步通知请求(GET/POST 参数)
|
||||||
* @return array<string, mixed> 解析结果,通常包含:
|
* @return array<string, mixed> 解析结果,通常包含:
|
||||||
* - status: 支付状态
|
* - success: 是否支付成功
|
||||||
* - pay_order_id: 系统订单号
|
* - status: 插件解析出的渠道状态文本
|
||||||
|
* - pay_order_id: 系统支付单号
|
||||||
* - chan_trade_no: 渠道交易号
|
* - chan_trade_no: 渠道交易号
|
||||||
* - amount: 支付金额
|
* - chan_order_no: 渠道订单号
|
||||||
|
* - amount: 支付金额(分)
|
||||||
|
* - paid_at: 支付成功时间
|
||||||
* @throws PaymentException 验签失败、数据异常等
|
* @throws PaymentException 验签失败、数据异常等
|
||||||
*/
|
*/
|
||||||
public function notify(Request $request): array;
|
public function notify(Request $request): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调处理成功时返回给第三方的平台响应。
|
||||||
|
*/
|
||||||
public function notifySuccess(): string|Response;
|
public function notifySuccess(): string|Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调处理失败时返回给第三方的平台响应。
|
||||||
|
*/
|
||||||
public function notifyFail(): string|Response;
|
public function notifyFail(): string|Response;
|
||||||
}
|
}
|
||||||
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
namespace app\common\middleware;
|
namespace app\common\middleware;
|
||||||
|
|
||||||
use Webman\MiddlewareInterface;
|
|
||||||
use Webman\Http\Response;
|
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
|
use Webman\Http\Response;
|
||||||
|
use Webman\MiddlewareInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局跨域中间件
|
* 全局跨域中间件。
|
||||||
* 处理前后端分离项目中的跨域请求问题
|
*
|
||||||
|
* 统一处理预检请求和跨域响应头。
|
||||||
*/
|
*/
|
||||||
class Cors implements MiddlewareInterface
|
class Cors implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 处理请求
|
* 处理请求。
|
||||||
|
*
|
||||||
* @param Request $request 请求对象
|
* @param Request $request 请求对象
|
||||||
* @param callable $handler 下一个中间件处理函数
|
* @param callable $handler 下一个中间件处理函数
|
||||||
* @return Response 响应对象
|
* @return Response 响应对象
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* This file is part of webman.
|
|
||||||
*
|
|
||||||
* Licensed under The MIT License
|
|
||||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
|
||||||
* Redistributions of files must retain the above copyright notice.
|
|
||||||
*
|
|
||||||
* @author walkor<walkor@workerman.net>
|
|
||||||
* @copyright walkor<walkor@workerman.net>
|
|
||||||
* @link http://www.workerman.net/
|
|
||||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace app\common\middleware;
|
|
||||||
|
|
||||||
use Webman\MiddlewareInterface;
|
|
||||||
use Webman\Http\Response;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class StaticFile
|
|
||||||
* @package app\middleware
|
|
||||||
*/
|
|
||||||
class StaticFile implements MiddlewareInterface
|
|
||||||
{
|
|
||||||
public function process(Request $request, callable $handler): Response
|
|
||||||
{
|
|
||||||
// Access to files beginning with. Is prohibited
|
|
||||||
if (strpos($request->path(), '/.') !== false) {
|
|
||||||
return response('<h1>403 forbidden</h1>', 403);
|
|
||||||
}
|
|
||||||
/** @var Response $response */
|
|
||||||
$response = $handler($request);
|
|
||||||
// Add cross domain HTTP header
|
|
||||||
/*$response->withHeaders([
|
|
||||||
'Access-Control-Allow-Origin' => '*',
|
|
||||||
'Access-Control-Allow-Credentials' => 'true',
|
|
||||||
]);*/
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace app\common\payment;
|
namespace app\common\payment;
|
||||||
|
|
||||||
use app\common\base\BasePayment;
|
use app\common\base\BasePayment;
|
||||||
use app\common\contracts\PaymentInterface;
|
use app\common\interface\PaymentInterface;
|
||||||
use app\exceptions\PaymentException;
|
use app\common\util\FormatHelper;
|
||||||
|
use app\exception\PaymentException;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
@@ -16,43 +17,83 @@ use Yansongda\Supports\Collection;
|
|||||||
/**
|
/**
|
||||||
* 支付宝支付插件(基于 yansongda/pay ~3.7)
|
* 支付宝支付插件(基于 yansongda/pay ~3.7)
|
||||||
*
|
*
|
||||||
* 支持:web(电脑网站)、h5(手机网站)、scan(扫码)、app(APP 支付)
|
* 支持:web(电脑网站)、h5(手机网站)、app(APP 支付)、mini(小程序)、pos(刷卡)、scan(扫码)、transfer(转账)
|
||||||
*
|
*
|
||||||
* 通道配置:app_id, app_secret_cert, app_public_cert_path, alipay_public_cert_path,
|
* 通道配置:app_id, app_secret_cert, app_public_cert_path, alipay_public_cert_path,
|
||||||
* alipay_root_cert_path, notify_url, return_url, mode(0正式/1沙箱)
|
* alipay_root_cert_path, mode(0正式/1沙箱)
|
||||||
*/
|
*/
|
||||||
class AlipayPayment extends BasePayment implements PaymentInterface
|
class AlipayPayment extends BasePayment implements PaymentInterface
|
||||||
{
|
{
|
||||||
|
private const PRODUCT_WEB = 'alipay_web';
|
||||||
|
private const PRODUCT_H5 = 'alipay_h5';
|
||||||
|
private const PRODUCT_APP = 'alipay_app';
|
||||||
|
private const PRODUCT_MINI = 'alipay_mini';
|
||||||
|
private const PRODUCT_POS = 'alipay_pos';
|
||||||
|
private const PRODUCT_SCAN = 'alipay_scan';
|
||||||
|
private const PRODUCT_TRANSFER = 'alipay_transfer';
|
||||||
|
|
||||||
|
private const DEFAULT_ENABLED_PRODUCTS = [
|
||||||
|
self::PRODUCT_H5,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const PRODUCT_ACTION_MAP = [
|
||||||
|
self::PRODUCT_WEB => 'web',
|
||||||
|
self::PRODUCT_H5 => 'h5',
|
||||||
|
self::PRODUCT_APP => 'app',
|
||||||
|
self::PRODUCT_MINI => 'mini',
|
||||||
|
self::PRODUCT_POS => 'pos',
|
||||||
|
self::PRODUCT_SCAN => 'scan',
|
||||||
|
self::PRODUCT_TRANSFER => 'transfer',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const ACTION_PRODUCT_MAP = [
|
||||||
|
'web' => self::PRODUCT_WEB,
|
||||||
|
'h5' => self::PRODUCT_H5,
|
||||||
|
'app' => self::PRODUCT_APP,
|
||||||
|
'mini' => self::PRODUCT_MINI,
|
||||||
|
'pos' => self::PRODUCT_POS,
|
||||||
|
'scan' => self::PRODUCT_SCAN,
|
||||||
|
'transfer' => self::PRODUCT_TRANSFER,
|
||||||
|
];
|
||||||
|
|
||||||
protected array $paymentInfo = [
|
protected array $paymentInfo = [
|
||||||
'code' => 'alipay',
|
'code' => 'alipay',
|
||||||
'name' => '支付宝直连',
|
'name' => '支付宝直连',
|
||||||
'author' => '',
|
'author' => '技术老胡',
|
||||||
'link' => '',
|
'link' => 'https://www.baidu.com',
|
||||||
'pay_types' => ['alipay'],
|
'version' => '1.0.0',
|
||||||
'transfer_types' => [],
|
'pay_types' => ['alipay', 'alipay_app'],
|
||||||
|
'transfer_types' => ['alipay', 'alipay_app'],
|
||||||
'config_schema' => [
|
'config_schema' => [
|
||||||
'fields' => [
|
["type" => "input", "field" => "app_id", "title" => "应用ID", "value" => "", "props" => ["placeholder" => "请输入应用ID"], "validate" => [["required" => true, "message" => "应用ID不能为空"]]],
|
||||||
['field' => 'app_id', 'label' => '应用ID', 'type' => 'text', 'required' => true],
|
["type" => "textarea", "field" => "app_secret_cert", "title" => "应用私钥", "value" => "", "props" => ["placeholder" => "请输入应用私钥", "rows" => 4], "validate" => [["required" => true, "message" => "应用私钥不能为空"]]],
|
||||||
['field' => 'app_secret_cert', 'label' => '应用私钥', 'type' => 'textarea', 'required' => true],
|
["type" => "input", "field" => "app_public_cert_path", "title" => "应用公钥证书路径", "value" => "", "props" => ["placeholder" => "请输入应用公钥证书路径"], "validate" => [["required" => true, "message" => "应用公钥证书路径不能为空"]]],
|
||||||
['field' => 'app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'text', 'required' => true],
|
["type" => "input", "field" => "alipay_public_cert_path", "title" => "支付宝公钥证书路径", "value" => "", "props" => ["placeholder" => "请输入支付宝公钥证书路径"], "validate" => [["required" => true, "message" => "支付宝公钥证书路径不能为空"]]],
|
||||||
['field' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'text', 'required' => true],
|
["type" => "input", "field" => "alipay_root_cert_path", "title" => "支付宝根证书路径", "value" => "", "props" => ["placeholder" => "请输入支付宝根证书路径"], "validate" => [["required" => true, "message" => "支付宝根证书路径不能为空"]]],
|
||||||
['field' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'text', 'required' => true],
|
[
|
||||||
['field' => 'notify_url', 'label' => '异步通知地址', 'type' => 'text', 'required' => true],
|
"type" => "checkbox",
|
||||||
['field' => 'return_url', 'label' => '同步跳转地址', 'type' => 'text', 'required' => false],
|
"field" => "enabled_products",
|
||||||
['field' => 'mode', 'label' => '环境', 'type' => 'select', 'options' => [['value' => '0', 'label' => '正式'], ['value' => '1', 'label' => '沙箱']]],
|
"title" => "已开通产品",
|
||||||
|
"value" => self::DEFAULT_ENABLED_PRODUCTS,
|
||||||
|
"options" => [
|
||||||
|
["value" => self::PRODUCT_WEB, "label" => "web - 网页支付"],
|
||||||
|
["value" => self::PRODUCT_H5, "label" => "h5 - H5 支付"],
|
||||||
|
["value" => self::PRODUCT_APP, "label" => "app - APP 支付"],
|
||||||
|
["value" => self::PRODUCT_MINI, "label" => "mini - 小程序支付"],
|
||||||
|
["value" => self::PRODUCT_POS, "label" => "pos - 刷卡支付"],
|
||||||
|
["value" => self::PRODUCT_SCAN, "label" => "scan - 扫码支付"],
|
||||||
|
["value" => self::PRODUCT_TRANSFER, "label" => "transfer - 账户转账"],
|
||||||
|
],
|
||||||
|
"validate" => [["required" => true, "message" => "请至少选择一个已开通产品"]],
|
||||||
],
|
],
|
||||||
|
["type" => "select", "field" => "mode", "title" => "环境", "value" => "0", "props" => ["placeholder" => "请选择环境"], "options" => [["value" => "0", "label" => "正式"], ["value" => "1", "label" => "沙箱"]]],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
private const PRODUCT_WEB = 'alipay_web';
|
|
||||||
private const PRODUCT_H5 = 'alipay_h5';
|
|
||||||
private const PRODUCT_SCAN = 'alipay_scan';
|
|
||||||
private const PRODUCT_APP = 'alipay_app';
|
|
||||||
|
|
||||||
public function init(array $channelConfig): void
|
public function init(array $channelConfig): void
|
||||||
{
|
{
|
||||||
parent::init($channelConfig);
|
parent::init($channelConfig);
|
||||||
Pay::config([
|
$config = [
|
||||||
'alipay' => [
|
'alipay' => [
|
||||||
'default' => [
|
'default' => [
|
||||||
'app_id' => $this->getConfig('app_id', ''),
|
'app_id' => $this->getConfig('app_id', ''),
|
||||||
@@ -65,47 +106,229 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
'mode' => (int)($this->getConfig('mode', Pay::MODE_NORMAL)),
|
'mode' => (int)($this->getConfig('mode', Pay::MODE_NORMAL)),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
];
|
||||||
|
Pay::config(array_merge($config, ['_force' => true]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function chooseProduct(array $order): string
|
private function chooseProduct(array $order, bool $validateEnabled = true): string
|
||||||
{
|
{
|
||||||
$enabled = $this->channelConfig['enabled_products'] ?? ['alipay_web', 'alipay_h5', 'alipay_scan'];
|
$enabled = $this->normalizeEnabledProducts($this->channelConfig['enabled_products'] ?? self::DEFAULT_ENABLED_PRODUCTS);
|
||||||
$env = $order['_env'] ?? 'pc';
|
$explicit = $this->resolveExplicitProduct($order);
|
||||||
$map = ['pc' => self::PRODUCT_WEB, 'h5' => self::PRODUCT_H5, 'alipay' => self::PRODUCT_APP];
|
if ($explicit !== null) {
|
||||||
$prefer = $map[$env] ?? self::PRODUCT_WEB;
|
if ($validateEnabled && !in_array($explicit, $enabled, true)) {
|
||||||
|
throw new PaymentException('支付宝产品未开通:' . $this->productAction($explicit), 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $explicit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = strtolower((string) ($order['_env'] ?? $order['device'] ?? 'pc'));
|
||||||
|
$map = [
|
||||||
|
'pc' => self::PRODUCT_WEB,
|
||||||
|
'web' => self::PRODUCT_WEB,
|
||||||
|
'desktop' => self::PRODUCT_WEB,
|
||||||
|
'mobile' => self::PRODUCT_H5,
|
||||||
|
'h5' => self::PRODUCT_H5,
|
||||||
|
'wechat' => self::PRODUCT_H5,
|
||||||
|
'qq' => self::PRODUCT_H5,
|
||||||
|
'alipay' => self::PRODUCT_APP,
|
||||||
|
'app' => self::PRODUCT_APP,
|
||||||
|
'mini' => self::PRODUCT_MINI,
|
||||||
|
'pos' => self::PRODUCT_POS,
|
||||||
|
'scan' => self::PRODUCT_SCAN,
|
||||||
|
'transfer' => self::PRODUCT_TRANSFER,
|
||||||
|
];
|
||||||
|
$prefer = $map[$env] ?? self::PRODUCT_WEB;
|
||||||
|
|
||||||
|
$payTypeCode = strtolower((string) ($order['pay_type_code'] ?? $order['type_code'] ?? ''));
|
||||||
|
if ($payTypeCode === 'alipay_app') {
|
||||||
|
$prefer = self::PRODUCT_APP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$validateEnabled) {
|
||||||
|
return $prefer;
|
||||||
|
}
|
||||||
|
|
||||||
return in_array($prefer, $enabled, true) ? $prefer : ($enabled[0] ?? self::PRODUCT_WEB);
|
return in_array($prefer, $enabled, true) ? $prefer : ($enabled[0] ?? self::PRODUCT_WEB);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeEnabledProducts(mixed $products): array
|
||||||
|
{
|
||||||
|
if (is_string($products)) {
|
||||||
|
$decoded = json_decode($products, true);
|
||||||
|
$products = is_array($decoded) ? $decoded : [$products];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($products)) {
|
||||||
|
return self::DEFAULT_ENABLED_PRODUCTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$value = strtolower(trim((string) $product));
|
||||||
|
if ($value !== '') {
|
||||||
|
$normalized[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array_values(array_unique($normalized));
|
||||||
|
|
||||||
|
return $normalized !== [] ? $normalized : self::DEFAULT_ENABLED_PRODUCTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveExplicitProduct(array $order): ?string
|
||||||
|
{
|
||||||
|
$context = $this->collectOrderContext($order);
|
||||||
|
$candidates = [
|
||||||
|
$context['pay_product'] ?? null,
|
||||||
|
$context['product'] ?? null,
|
||||||
|
$context['alipay_product'] ?? null,
|
||||||
|
$context['pay_action'] ?? null,
|
||||||
|
$context['action'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$product = $this->normalizeProductCode($candidate);
|
||||||
|
if ($product !== null) {
|
||||||
|
return $product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeProductCode(mixed $value): ?string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim((string) $value));
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset(self::ACTION_PRODUCT_MAP[$value])) {
|
||||||
|
return self::ACTION_PRODUCT_MAP[$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset(self::PRODUCT_ACTION_MAP[$value])) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function productAction(string $product): string
|
||||||
|
{
|
||||||
|
return self::PRODUCT_ACTION_MAP[$product] ?? $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function collectOrderContext(array $order): array
|
||||||
|
{
|
||||||
|
$context = $order;
|
||||||
|
$extra = isset($order['extra']) && is_array($order['extra']) ? $order['extra'] : [];
|
||||||
|
if ($extra !== []) {
|
||||||
|
$context = array_merge($context, $extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
$param = $this->normalizeParamBag($context['param'] ?? null);
|
||||||
|
if ($param !== []) {
|
||||||
|
$context = array_merge($context, $param);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeParamBag(mixed $param): array
|
||||||
|
{
|
||||||
|
if (is_array($param)) {
|
||||||
|
return $param;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($param) && $param !== '') {
|
||||||
|
$decoded = json_decode($param, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str($param, $parsed);
|
||||||
|
if (is_array($parsed) && $parsed !== []) {
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildBasePayParams(array $params): array
|
||||||
|
{
|
||||||
|
$base = [
|
||||||
|
'out_trade_no' => (string) ($params['out_trade_no'] ?? ''),
|
||||||
|
'total_amount' => FormatHelper::amount((int) ($params['amount'] ?? 0)),
|
||||||
|
'subject' => (string) ($params['subject'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$body = (string) ($params['body'] ?? '');
|
||||||
|
if ($body !== '') {
|
||||||
|
$base['body'] = $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnUrl = (string) ($params['_return_url'] ?? '');
|
||||||
|
if ($returnUrl !== '') {
|
||||||
|
$base['_return_url'] = $returnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notifyUrl = (string) ($params['_notify_url'] ?? '');
|
||||||
|
if ($notifyUrl !== '') {
|
||||||
|
$base['_notify_url'] = $notifyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractCollectionValue(Collection $result, array $keys, mixed $default = ''): mixed
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$value = $result->get($key);
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
public function pay(array $order): array
|
public function pay(array $order): array
|
||||||
{
|
{
|
||||||
$orderId = $order['order_id'] ?? $order['mch_no'] ?? '';
|
$orderId = (string) ($order['order_id'] ?? $order['pay_no'] ?? '');
|
||||||
$amount = (float)($order['amount'] ?? 0);
|
$amount = (int) ($order['amount'] ?? 0);
|
||||||
$subject = (string)($order['subject'] ?? '');
|
$subject = (string)($order['subject'] ?? '');
|
||||||
|
$body = (string)($order['body'] ?? '');
|
||||||
$extra = $order['extra'] ?? [];
|
$extra = $order['extra'] ?? [];
|
||||||
$returnUrl = $extra['return_url'] ?? $this->getConfig('return_url', '');
|
$returnUrl = (string) ($order['return_url'] ?? $extra['return_url'] ?? $this->getConfig('return_url', ''));
|
||||||
$notifyUrl = $this->getConfig('notify_url', '');
|
$notifyUrl = (string) ($order['callback_url'] ?? $this->getConfig('notify_url', ''));
|
||||||
|
|
||||||
$params = [
|
if ($orderId === '' || $amount <= 0 || $subject === '') {
|
||||||
|
throw new PaymentException('支付宝下单参数不完整', 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->buildBasePayParams([
|
||||||
'out_trade_no' => $orderId,
|
'out_trade_no' => $orderId,
|
||||||
'total_amount' => sprintf('%.2f', $amount),
|
'amount' => $amount,
|
||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
];
|
'body' => $body,
|
||||||
if ($returnUrl !== '') {
|
'_return_url' => $returnUrl,
|
||||||
$params['_return_url'] = $returnUrl;
|
'_notify_url' => $notifyUrl,
|
||||||
}
|
]);
|
||||||
if ($notifyUrl !== '') {
|
|
||||||
$params['_notify_url'] = $notifyUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
$product = $this->chooseProduct($order);
|
$product = $this->chooseProduct($order);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return match ($product) {
|
return match ($product) {
|
||||||
self::PRODUCT_WEB => $this->doWeb($params),
|
self::PRODUCT_WEB => $this->doWeb($params),
|
||||||
self::PRODUCT_H5 => $this->doH5($params),
|
self::PRODUCT_H5 => $this->doH5($params),
|
||||||
self::PRODUCT_SCAN => $this->doScan($params),
|
self::PRODUCT_SCAN => $this->doScan($params),
|
||||||
self::PRODUCT_APP => $this->doApp($params),
|
self::PRODUCT_APP => $this->doApp($params),
|
||||||
|
self::PRODUCT_MINI => $this->doMini($params, $order),
|
||||||
|
self::PRODUCT_POS => $this->doPos($params, $order),
|
||||||
|
self::PRODUCT_TRANSFER => $this->doTransfer($params, $order),
|
||||||
default => throw new PaymentException('不支持的支付宝产品:' . $product, 402),
|
default => throw new PaymentException('不支持的支付宝产品:' . $product, 402),
|
||||||
};
|
};
|
||||||
} catch (PaymentException $e) {
|
} catch (PaymentException $e) {
|
||||||
@@ -120,7 +343,16 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
$response = Pay::alipay()->web($params);
|
$response = Pay::alipay()->web($params);
|
||||||
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
||||||
return [
|
return [
|
||||||
'pay_params' => ['type' => 'form', 'method' => 'POST', 'action' => '', 'html' => $body],
|
'pay_product' => self::PRODUCT_WEB,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_WEB),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'form',
|
||||||
|
'method' => 'POST',
|
||||||
|
'action' => '',
|
||||||
|
'html' => $body,
|
||||||
|
'pay_product' => self::PRODUCT_WEB,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_WEB),
|
||||||
|
],
|
||||||
'chan_order_no' => $params['out_trade_no'],
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
'chan_trade_no' => '',
|
'chan_trade_no' => '',
|
||||||
];
|
];
|
||||||
@@ -135,7 +367,16 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
$response = Pay::alipay()->h5($params);
|
$response = Pay::alipay()->h5($params);
|
||||||
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
$body = $response instanceof ResponseInterface ? (string)$response->getBody() : '';
|
||||||
return [
|
return [
|
||||||
'pay_params' => ['type' => 'form', 'method' => 'POST', 'action' => '', 'html' => $body],
|
'pay_product' => self::PRODUCT_H5,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_H5),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'form',
|
||||||
|
'method' => 'POST',
|
||||||
|
'action' => '',
|
||||||
|
'html' => $body,
|
||||||
|
'pay_product' => self::PRODUCT_H5,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_H5),
|
||||||
|
],
|
||||||
'chan_order_no' => $params['out_trade_no'],
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
'chan_trade_no' => '',
|
'chan_trade_no' => '',
|
||||||
];
|
];
|
||||||
@@ -147,7 +388,15 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
$result = Pay::alipay()->scan($params);
|
$result = Pay::alipay()->scan($params);
|
||||||
$qrCode = $result->get('qr_code', '');
|
$qrCode = $result->get('qr_code', '');
|
||||||
return [
|
return [
|
||||||
'pay_params' => ['type' => 'qrcode', 'qrcode_url' => $qrCode, 'qrcode_data' => $qrCode],
|
'pay_product' => self::PRODUCT_SCAN,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_SCAN),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'qrcode',
|
||||||
|
'qrcode_url' => $qrCode,
|
||||||
|
'qrcode_data' => $qrCode,
|
||||||
|
'pay_product' => self::PRODUCT_SCAN,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_SCAN),
|
||||||
|
],
|
||||||
'chan_order_no' => $params['out_trade_no'],
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
'chan_trade_no' => $result->get('trade_no', ''),
|
'chan_trade_no' => $result->get('trade_no', ''),
|
||||||
];
|
];
|
||||||
@@ -159,28 +408,161 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
$result = Pay::alipay()->app($params);
|
$result = Pay::alipay()->app($params);
|
||||||
$orderStr = $result->get('order_string', '');
|
$orderStr = $result->get('order_string', '');
|
||||||
return [
|
return [
|
||||||
'pay_params' => ['type' => 'jsapi', 'order_str' => $orderStr, 'urlscheme' => $orderStr],
|
'pay_product' => self::PRODUCT_APP,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_APP),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'jsapi',
|
||||||
|
'order_str' => $orderStr,
|
||||||
|
'urlscheme' => $orderStr,
|
||||||
|
'pay_product' => self::PRODUCT_APP,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_APP),
|
||||||
|
],
|
||||||
'chan_order_no' => $params['out_trade_no'],
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
'chan_trade_no' => $result->get('trade_no', ''),
|
'chan_trade_no' => $result->get('trade_no', ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function doMini(array $params, array $order): array
|
||||||
|
{
|
||||||
|
$context = $this->collectOrderContext($order);
|
||||||
|
$buyerId = trim((string) ($context['buyer_id'] ?? ''));
|
||||||
|
if ($buyerId === '') {
|
||||||
|
throw new PaymentException('支付宝小程序支付缺少 buyer_id', 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
$miniParams = array_merge($params, [
|
||||||
|
'buyer_id' => $buyerId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var Collection $result */
|
||||||
|
$result = Pay::alipay()->mini($miniParams);
|
||||||
|
$tradeNo = (string) $this->extractCollectionValue($result, ['trade_no', 'order_id', 'out_trade_no'], '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pay_product' => self::PRODUCT_MINI,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_MINI),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'mini',
|
||||||
|
'trade_no' => $tradeNo,
|
||||||
|
'buyer_id' => $buyerId,
|
||||||
|
'pay_product' => self::PRODUCT_MINI,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_MINI),
|
||||||
|
'raw' => $result->all(),
|
||||||
|
],
|
||||||
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
|
'chan_trade_no' => $tradeNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doPos(array $params, array $order): array
|
||||||
|
{
|
||||||
|
$context = $this->collectOrderContext($order);
|
||||||
|
$authCode = trim((string) ($context['auth_code'] ?? ''));
|
||||||
|
if ($authCode === '') {
|
||||||
|
throw new PaymentException('支付宝刷卡支付缺少 auth_code', 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
$posParams = array_merge($params, [
|
||||||
|
'auth_code' => $authCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var Collection $result */
|
||||||
|
$result = Pay::alipay()->pos($posParams);
|
||||||
|
$tradeNo = (string) $this->extractCollectionValue($result, ['trade_no', 'order_id', 'out_trade_no'], '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pay_product' => self::PRODUCT_POS,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_POS),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'pos',
|
||||||
|
'trade_no' => $tradeNo,
|
||||||
|
'auth_code' => $authCode,
|
||||||
|
'pay_product' => self::PRODUCT_POS,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_POS),
|
||||||
|
'raw' => $result->all(),
|
||||||
|
],
|
||||||
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
|
'chan_trade_no' => $tradeNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doTransfer(array $params, array $order): array
|
||||||
|
{
|
||||||
|
$context = $this->collectOrderContext($order);
|
||||||
|
$payeeInfo = $this->normalizeParamBag($context['payee_info'] ?? null);
|
||||||
|
if ($payeeInfo === []) {
|
||||||
|
throw new PaymentException('支付宝转账缺少 payee_info', 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transferParams = [
|
||||||
|
'out_biz_no' => $params['out_trade_no'],
|
||||||
|
'trans_amount' => $params['total_amount'],
|
||||||
|
'payee_info' => $payeeInfo,
|
||||||
|
];
|
||||||
|
|
||||||
|
$notifyUrl = (string) ($params['_notify_url'] ?? '');
|
||||||
|
if ($notifyUrl !== '') {
|
||||||
|
$transferParams['_notify_url'] = $notifyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderTitle = trim((string) ($context['order_title'] ?? $context['subject'] ?? ''));
|
||||||
|
if ($orderTitle !== '') {
|
||||||
|
$transferParams['order_title'] = $orderTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remark = trim((string) ($context['remark'] ?? $context['body'] ?? ''));
|
||||||
|
if ($remark !== '') {
|
||||||
|
$transferParams['remark'] = $remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Collection $result */
|
||||||
|
$result = Pay::alipay()->transfer($transferParams);
|
||||||
|
$tradeNo = (string) $this->extractCollectionValue($result, ['trade_no', 'order_id', 'out_biz_no'], '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pay_product' => self::PRODUCT_TRANSFER,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_TRANSFER),
|
||||||
|
'pay_params' => [
|
||||||
|
'type' => 'transfer',
|
||||||
|
'trade_no' => $tradeNo,
|
||||||
|
'out_biz_no' => $transferParams['out_biz_no'],
|
||||||
|
'trans_amount' => $transferParams['trans_amount'],
|
||||||
|
'payee_info' => $payeeInfo,
|
||||||
|
'pay_product' => self::PRODUCT_TRANSFER,
|
||||||
|
'pay_action' => $this->productAction(self::PRODUCT_TRANSFER),
|
||||||
|
'raw' => $result->all(),
|
||||||
|
],
|
||||||
|
'chan_order_no' => $params['out_trade_no'],
|
||||||
|
'chan_trade_no' => $tradeNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function query(array $order): array
|
public function query(array $order): array
|
||||||
{
|
{
|
||||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
$product = $this->chooseProduct($order, false);
|
||||||
|
$action = $this->productAction($product);
|
||||||
|
$outTradeNo = (string) ($order['chan_order_no'] ?? $order['order_id'] ?? $order['out_trade_no'] ?? '');
|
||||||
|
$queryParams = $action === 'transfer'
|
||||||
|
? ['out_biz_no' => $outTradeNo, '_action' => $action]
|
||||||
|
: ['out_trade_no' => $outTradeNo, '_action' => $action];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @var Collection $result */
|
/** @var Collection $result */
|
||||||
$result = Pay::alipay()->query(['out_trade_no' => $outTradeNo]);
|
$result = Pay::alipay()->query($queryParams);
|
||||||
$tradeStatus = $result->get('trade_status', '');
|
$tradeStatus = (string) $result->get('trade_status', $result->get('status', ''));
|
||||||
$tradeNo = $result->get('trade_no', '');
|
$tradeNo = (string) $this->extractCollectionValue($result, ['trade_no', 'order_id', 'out_biz_no'], '');
|
||||||
$totalAmount = (float)$result->get('total_amount', 0);
|
$totalAmount = (string) $this->extractCollectionValue($result, ['total_amount', 'trans_amount', 'amount'], '0');
|
||||||
$status = in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true) ? 'success' : $tradeStatus;
|
$status = match ($action) {
|
||||||
|
'transfer' => in_array($tradeStatus, ['SUCCESS', 'PAY_SUCCESS', 'SUCCESSFUL'], true) ? 'success' : $tradeStatus,
|
||||||
|
default => in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true) ? 'success' : $tradeStatus,
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'pay_product' => $product,
|
||||||
|
'pay_action' => $action,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'chan_trade_no' => $tradeNo,
|
'chan_trade_no' => $tradeNo,
|
||||||
'pay_amount' => $totalAmount,
|
'pay_amount' => (int) round(((float) $totalAmount) * 100),
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
throw new PaymentException('支付宝查询失败:' . $e->getMessage(), 402);
|
throw new PaymentException('支付宝查询失败:' . $e->getMessage(), 402);
|
||||||
@@ -189,11 +571,21 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
|
|
||||||
public function close(array $order): array
|
public function close(array $order): array
|
||||||
{
|
{
|
||||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
$product = $this->chooseProduct($order, false);
|
||||||
|
$action = $this->productAction($product);
|
||||||
|
if ($action === 'transfer') {
|
||||||
|
throw new PaymentException('支付宝转账不支持关单', 402);
|
||||||
|
}
|
||||||
|
|
||||||
|
$outTradeNo = (string) ($order['chan_order_no'] ?? $order['order_id'] ?? $order['out_trade_no'] ?? '');
|
||||||
|
$closeParams = [
|
||||||
|
'out_trade_no' => $outTradeNo,
|
||||||
|
'_action' => $action,
|
||||||
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Pay::alipay()->close(['out_trade_no' => $outTradeNo]);
|
Pay::alipay()->close($closeParams);
|
||||||
return ['success' => true, 'msg' => '关闭成功'];
|
return ['success' => true, 'msg' => '关闭成功', 'pay_product' => $product, 'pay_action' => $action];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
throw new PaymentException('支付宝关单失败:' . $e->getMessage(), 402);
|
throw new PaymentException('支付宝关单失败:' . $e->getMessage(), 402);
|
||||||
}
|
}
|
||||||
@@ -201,9 +593,11 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
|
|
||||||
public function refund(array $order): array
|
public function refund(array $order): array
|
||||||
{
|
{
|
||||||
$outTradeNo = $order['chan_order_no'] ?? $order['order_id'] ?? '';
|
$product = $this->chooseProduct($order, false);
|
||||||
$refundAmount = (float)($order['refund_amount'] ?? 0);
|
$action = $this->productAction($product);
|
||||||
$refundNo = $order['refund_no'] ?? $order['order_id'] . '_' . time();
|
$outTradeNo = (string) ($order['chan_order_no'] ?? $order['order_id'] ?? $order['out_trade_no'] ?? '');
|
||||||
|
$refundAmount = (int) ($order['refund_amount'] ?? 0);
|
||||||
|
$refundNo = (string) ($order['refund_no'] ?? (($order['order_id'] ?? 'refund') . '_' . time()));
|
||||||
$refundReason = (string)($order['refund_reason'] ?? '');
|
$refundReason = (string)($order['refund_reason'] ?? '');
|
||||||
|
|
||||||
if ($outTradeNo === '' || $refundAmount <= 0) {
|
if ($outTradeNo === '' || $refundAmount <= 0) {
|
||||||
@@ -211,9 +605,10 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'out_trade_no' => $outTradeNo,
|
$action === 'transfer' ? 'out_biz_no' : 'out_trade_no' => $outTradeNo,
|
||||||
'refund_amount' => sprintf('%.2f', $refundAmount),
|
'refund_amount' => FormatHelper::amount($refundAmount),
|
||||||
'out_request_no' => $refundNo,
|
'out_request_no' => $refundNo,
|
||||||
|
'_action' => $action,
|
||||||
];
|
];
|
||||||
if ($refundReason !== '') {
|
if ($refundReason !== '') {
|
||||||
$params['refund_reason'] = $refundReason;
|
$params['refund_reason'] = $refundReason;
|
||||||
@@ -227,9 +622,11 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
|
|
||||||
if ($code === '10000' || $code === 10000) {
|
if ($code === '10000' || $code === 10000) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'chan_refund_no'=> $result->get('trade_no', $refundNo),
|
'pay_product' => $product,
|
||||||
'msg' => '退款成功',
|
'pay_action' => $action,
|
||||||
|
'chan_refund_no' => (string) $this->extractCollectionValue($result, ['trade_no', 'refund_no', 'out_request_no'], $refundNo),
|
||||||
|
'msg' => '退款成功',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
throw new PaymentException($subMsg ?: '退款失败', 402);
|
throw new PaymentException($subMsg ?: '退款失败', 402);
|
||||||
@@ -250,17 +647,21 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
$tradeStatus = $result->get('trade_status', '');
|
$tradeStatus = $result->get('trade_status', '');
|
||||||
$outTradeNo = $result->get('out_trade_no', '');
|
$outTradeNo = $result->get('out_trade_no', '');
|
||||||
$tradeNo = $result->get('trade_no', '');
|
$tradeNo = $result->get('trade_no', '');
|
||||||
$totalAmount = (float)$result->get('total_amount', 0);
|
$totalAmount = (string) $result->get('total_amount', '0');
|
||||||
|
$paidAt = (string) $result->get('gmt_payment', '');
|
||||||
|
|
||||||
if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
|
if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
|
||||||
throw new PaymentException('回调状态异常:' . $tradeStatus, 402);
|
throw new PaymentException('回调状态异常:' . $tradeStatus, 402);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'success' => true,
|
||||||
'pay_order_id' => $outTradeNo,
|
'status' => 'success',
|
||||||
'chan_trade_no'=> $tradeNo,
|
'pay_order_id' => $outTradeNo,
|
||||||
'amount' => $totalAmount,
|
'chan_order_no' => $outTradeNo,
|
||||||
|
'chan_trade_no' => $tradeNo,
|
||||||
|
'amount' => (int) round(((float) $totalAmount) * 100),
|
||||||
|
'paid_at' => $paidAt !== '' ? (FormatHelper::timestamp((int) strtotime($paidAt)) ?: null) : null,
|
||||||
];
|
];
|
||||||
} catch (PaymentException $e) {
|
} catch (PaymentException $e) {
|
||||||
throw $e;
|
throw $e;
|
||||||
@@ -268,6 +669,7 @@ class AlipayPayment extends BasePayment implements PaymentInterface
|
|||||||
throw new PaymentException('支付宝回调验签失败:' . $e->getMessage(), 402);
|
throw new PaymentException('支付宝回调验签失败:' . $e->getMessage(), 402);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notifySuccess(): string|Response
|
public function notifySuccess(): string|Response
|
||||||
{
|
{
|
||||||
return 'success';
|
return 'success';
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\common\payment;
|
|
||||||
|
|
||||||
use app\common\base\BasePayment;
|
|
||||||
use app\common\contracts\PaymentInterface;
|
|
||||||
use app\exceptions\PaymentException;
|
|
||||||
use support\Request;
|
|
||||||
use support\Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拉卡拉支付插件(最小可用示例)
|
|
||||||
*
|
|
||||||
* 目的:先把 API 下单链路跑通,让现有 DB 配置(ma_pay_plugin=lakala)可用。
|
|
||||||
* 后续你可以把这里替换为真实拉卡拉对接逻辑(HTTP 下单、验签回调等)。
|
|
||||||
*/
|
|
||||||
class LakalaPayment extends BasePayment implements PaymentInterface
|
|
||||||
{
|
|
||||||
protected array $paymentInfo = [
|
|
||||||
'code' => 'lakala',
|
|
||||||
'name' => '拉卡拉(示例)',
|
|
||||||
'author' => '',
|
|
||||||
'link' => '',
|
|
||||||
'pay_types' => ['alipay', 'wechat'],
|
|
||||||
'transfer_types' => [],
|
|
||||||
'config_schema' => [
|
|
||||||
'fields' => [
|
|
||||||
['field' => 'notify_url', 'label' => '异步通知地址', 'type' => 'text', 'required' => false],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
public function pay(array $order): array
|
|
||||||
{
|
|
||||||
$orderId = (string)($order['order_id'] ?? '');
|
|
||||||
$amount = (string)($order['amount'] ?? '0.00');
|
|
||||||
$extra = is_array($order['extra'] ?? null) ? $order['extra'] : [];
|
|
||||||
|
|
||||||
if ($orderId === '') {
|
|
||||||
throw new PaymentException('缺少订单号', 402);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里先返回“可联调”的 pay_params:默认给一个 qrcode 字符串
|
|
||||||
// 真实实现中应调用拉卡拉下单接口,返回二维码链接/支付链接/预支付信息等。
|
|
||||||
$qrcode = $extra['mock_qrcode'] ?? ('LAKALA_MOCK_QRCODE:' . $orderId . ':' . $amount);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'pay_params' => [
|
|
||||||
'type' => 'qrcode',
|
|
||||||
'qrcode_url' => $qrcode,
|
|
||||||
'qrcode_data'=> $qrcode,
|
|
||||||
],
|
|
||||||
'chan_order_no' => $orderId,
|
|
||||||
'chan_trade_no' => '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function query(array $order): array
|
|
||||||
{
|
|
||||||
throw new PaymentException('LakalaPayment::query 暂未实现', 402);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function close(array $order): array
|
|
||||||
{
|
|
||||||
throw new PaymentException('LakalaPayment::close 暂未实现', 402);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function refund(array $order): array
|
|
||||||
{
|
|
||||||
throw new PaymentException('LakalaPayment::refund 暂未实现', 402);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notify(Request $request): array
|
|
||||||
{
|
|
||||||
throw new PaymentException('LakalaPayment::notify 暂未实现', 402);
|
|
||||||
}
|
|
||||||
public function notifySuccess(): string|Response
|
|
||||||
{
|
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notifyFail(): string|Response
|
|
||||||
{
|
|
||||||
return 'fail';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
188
app/common/util/FormatHelper.php
Normal file
188
app/common/util/FormatHelper.php
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\util;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用格式化帮助类。
|
||||||
|
*
|
||||||
|
* 集中处理金额、时间、JSON、映射文案和脱敏逻辑,避免各服务层重复实现。
|
||||||
|
*/
|
||||||
|
class FormatHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 金额格式化,单位为元。
|
||||||
|
*/
|
||||||
|
public static function amount(int $amount): string
|
||||||
|
{
|
||||||
|
return number_format($amount / 100, 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额格式化,0 时显示不限。
|
||||||
|
*/
|
||||||
|
public static function amountOrUnlimited(int $amount): string
|
||||||
|
{
|
||||||
|
return $amount > 0 ? self::amount($amount) : '不限';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 次数格式化,0 时显示不限。
|
||||||
|
*/
|
||||||
|
public static function countOrUnlimited(int $count): string
|
||||||
|
{
|
||||||
|
return $count > 0 ? (string) $count : '不限';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 费率格式化,单位为百分点。
|
||||||
|
*/
|
||||||
|
public static function rate(int $basisPoints): string
|
||||||
|
{
|
||||||
|
return number_format($basisPoints / 100, 2, '.', '') . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟格式化。
|
||||||
|
*/
|
||||||
|
public static function latency(int $latencyMs): string
|
||||||
|
{
|
||||||
|
return $latencyMs > 0 ? $latencyMs . ' ms' : '0 ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期格式化。
|
||||||
|
*/
|
||||||
|
public static function date(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
return self::formatTemporalValue($value, 'Y-m-d', $emptyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期时间格式化。
|
||||||
|
*/
|
||||||
|
public static function dateTime(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
return self::formatTemporalValue($value, 'Y-m-d H:i:s', $emptyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按时间戳格式化。
|
||||||
|
*/
|
||||||
|
public static function timestamp(int $timestamp, string $pattern = 'Y-m-d H:i:s', string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
if ($timestamp <= 0) {
|
||||||
|
return $emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date($pattern, $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 文本格式化。
|
||||||
|
*/
|
||||||
|
public static function json(mixed $value, string $emptyText = ''): string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '' || $value === []) {
|
||||||
|
return $emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$encoded = json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
return $encoded !== false ? $encoded : $emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
return $encoded !== false ? $encoded : $emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射表文本转换。
|
||||||
|
*/
|
||||||
|
public static function textFromMap(int $value, array $map, string $default = '未知'): string
|
||||||
|
{
|
||||||
|
return (string) ($map[$value] ?? $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口凭证明文脱敏。
|
||||||
|
*/
|
||||||
|
public static function maskCredentialValue(string $credentialValue, bool $maskShortValue = true): string
|
||||||
|
{
|
||||||
|
$credentialValue = trim($credentialValue);
|
||||||
|
if ($credentialValue === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($credentialValue);
|
||||||
|
if ($length <= 8) {
|
||||||
|
return $maskShortValue ? str_repeat('*', $length) : $credentialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($credentialValue, 0, 4) . '****' . substr($credentialValue, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将模型或对象归一化成数组。
|
||||||
|
*/
|
||||||
|
public static function normalizeModel(mixed $value): ?array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value)) {
|
||||||
|
if (method_exists($value, 'toArray')) {
|
||||||
|
$data = $value->toArray();
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一格式化时间值。
|
||||||
|
*/
|
||||||
|
private static function formatTemporalValue(mixed $value, string $pattern, string $emptyText): string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return $emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
$text = trim($value);
|
||||||
|
return $text === '' ? $emptyText : $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format($pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value) && method_exists($value, 'format')) {
|
||||||
|
return $value->format($pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
254
app/common/util/JwtTokenManager.php
Normal file
254
app/common/util/JwtTokenManager.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\util;
|
||||||
|
|
||||||
|
use Firebase\JWT\ExpiredException;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
use Firebase\JWT\SignatureInvalidException;
|
||||||
|
use support\Redis;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 工具类,负责签发、验证和撤销登录态。
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - JWT 只负责承载身份声明,不保存业务权限细节。
|
||||||
|
* - Redis 保存会话态,支持主动注销、过期控制和最近访问时间更新。
|
||||||
|
* - guard 用于区分不同登录域,例如管理员和商户。
|
||||||
|
* - Redis Key 推荐由配置中的前缀 + jti 组成,例如:
|
||||||
|
* `mpay:auth:admin:{jti}`、`mpay:auth:merchant:{jti}`。
|
||||||
|
*/
|
||||||
|
class JwtTokenManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 签发 JWT,并把会话态写入 Redis。
|
||||||
|
*
|
||||||
|
* @param string $guard 认证域名称,例如 admin 或 merchant
|
||||||
|
* @param array $claims JWT 自定义声明,通常包含主体 ID 等核心身份信息
|
||||||
|
* @param array $sessionData Redis 会话数据,通常包含用户展示信息和登录上下文
|
||||||
|
* @param int|null $ttlSeconds 有效期,单位秒;为空时使用 guard 默认 TTL
|
||||||
|
* @return array{token:string,expires_in:int,jti:string,claims:array,session:array}
|
||||||
|
*/
|
||||||
|
public function issue(string $guard, array $claims, array $sessionData, ?int $ttlSeconds = null): array
|
||||||
|
{
|
||||||
|
$guardConfig = $this->guardConfig($guard);
|
||||||
|
$this->assertHmacSecretLength($guard, (string) $guardConfig['secret']);
|
||||||
|
$ttlSeconds = max(60, $ttlSeconds ?? (int) $guardConfig['ttl']);
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$jti = bin2hex(random_bytes(16));
|
||||||
|
$payload = array_merge([
|
||||||
|
'iss' => (string) config('auth.issuer', 'mpay'),
|
||||||
|
'iat' => $now,
|
||||||
|
'nbf' => $now,
|
||||||
|
'exp' => $now + $ttlSeconds,
|
||||||
|
'jti' => $jti,
|
||||||
|
'guard' => $guard,
|
||||||
|
], $claims);
|
||||||
|
|
||||||
|
$token = JWT::encode($payload, (string) $guardConfig['secret'], 'HS256');
|
||||||
|
|
||||||
|
$session = array_merge($sessionData, [
|
||||||
|
'guard' => $guard,
|
||||||
|
'jti' => $jti,
|
||||||
|
'issued_at' => FormatHelper::timestamp($now),
|
||||||
|
'expires_at' => FormatHelper::timestamp($now + $ttlSeconds),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->storeSession($guard, $jti, $session, $ttlSeconds);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => $token,
|
||||||
|
'expires_in' => $ttlSeconds,
|
||||||
|
'jti' => $jti,
|
||||||
|
'claims' => $payload,
|
||||||
|
'session' => $session,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 JWT,并恢复对应的 Redis 会话数据。
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 先校验签名和过期时间。
|
||||||
|
* - 再通过 jti 反查 Redis 会话,确保 token 仍然有效。
|
||||||
|
* - 每次命中会刷新最近访问时间。
|
||||||
|
*
|
||||||
|
* @return array{claims:array,session:array}|null
|
||||||
|
*/
|
||||||
|
public function verify(string $guard, string $token, string $ip = '', string $userAgent = ''): ?array
|
||||||
|
{
|
||||||
|
$payload = $this->decode($guard, $token);
|
||||||
|
if ($payload === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jti = (string) ($payload['jti'] ?? '');
|
||||||
|
if ($jti === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $this->session($guard, $jti);
|
||||||
|
if ($session === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$expiresAt = (int) ($payload['exp'] ?? 0);
|
||||||
|
$ttl = max(1, $expiresAt - $now);
|
||||||
|
|
||||||
|
$session['last_used_at'] = FormatHelper::timestamp($now);
|
||||||
|
if ($ip !== '') {
|
||||||
|
$session['last_used_ip'] = $ip;
|
||||||
|
}
|
||||||
|
if ($userAgent !== '') {
|
||||||
|
$session['user_agent'] = $userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storeSession($guard, $jti, $session, $ttl);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'claims' => $payload,
|
||||||
|
'session' => $session,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 token 撤销登录态。
|
||||||
|
*
|
||||||
|
* 适用于主动退出登录场景。
|
||||||
|
*/
|
||||||
|
public function revoke(string $guard, string $token): bool
|
||||||
|
{
|
||||||
|
$payload = $this->decode($guard, $token);
|
||||||
|
if ($payload === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jti = (string) ($payload['jti'] ?? '');
|
||||||
|
if ($jti === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) Redis::connection()->del($this->sessionKey($guard, $jti));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 jti 直接撤销登录态。
|
||||||
|
*
|
||||||
|
* 适用于已经掌握会话标识但没有原始 token 的补偿清理场景。
|
||||||
|
*/
|
||||||
|
public function revokeByJti(string $guard, string $jti): bool
|
||||||
|
{
|
||||||
|
if ($jti === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) Redis::connection()->del($this->sessionKey($guard, $jti));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 jti 获取会话数据。
|
||||||
|
*
|
||||||
|
* 返回值来自 Redis,若已过期或不存在则返回 null。
|
||||||
|
*/
|
||||||
|
public function session(string $guard, string $jti): ?array
|
||||||
|
{
|
||||||
|
$raw = Redis::connection()->get($this->sessionKey($guard, $jti));
|
||||||
|
if (!is_string($raw) || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = json_decode($raw, true);
|
||||||
|
return is_array($session) ? $session : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解码并校验 JWT。
|
||||||
|
*
|
||||||
|
* 只做签名、过期和 guard 校验,不处理 Redis 会话。
|
||||||
|
*/
|
||||||
|
protected function decode(string $guard, string $token): ?array
|
||||||
|
{
|
||||||
|
$guardConfig = $this->guardConfig($guard);
|
||||||
|
$this->assertHmacSecretLength($guard, (string) $guardConfig['secret']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JWT::$leeway = (int) config('auth.leeway', 30);
|
||||||
|
$payload = JWT::decode($token, new Key((string) $guardConfig['secret'], 'HS256'));
|
||||||
|
} catch (ExpiredException|SignatureInvalidException|Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(json_encode($payload, JSON_UNESCAPED_UNICODE), true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($data['guard'] ?? '') !== $guard) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将会话数据写入 Redis,并设置 TTL。
|
||||||
|
*/
|
||||||
|
protected function storeSession(string $guard, string $jti, array $session, int $ttlSeconds): void
|
||||||
|
{
|
||||||
|
Redis::connection()->setEx(
|
||||||
|
$this->sessionKey($guard, $jti),
|
||||||
|
max(60, $ttlSeconds),
|
||||||
|
json_encode($session, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 Redis 会话键。
|
||||||
|
*
|
||||||
|
* 最终格式由 guard 对应的 redis_prefix 加上 jti 组成。
|
||||||
|
*/
|
||||||
|
protected function sessionKey(string $guard, string $jti): string
|
||||||
|
{
|
||||||
|
return $this->guardConfig($guard)['redis_prefix'] . $jti;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定认证域的配置。
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException 当 guard 未配置时抛出
|
||||||
|
*/
|
||||||
|
protected function guardConfig(string $guard): array
|
||||||
|
{
|
||||||
|
$guards = (array) config('auth.guards', []);
|
||||||
|
if (!isset($guards[$guard])) {
|
||||||
|
throw new \InvalidArgumentException("Unknown auth guard: {$guard}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $guards[$guard];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 HS256 密钥长度,避免 firebase/php-jwt 抛出底层异常。
|
||||||
|
*/
|
||||||
|
protected function assertHmacSecretLength(string $guard, string $secret): void
|
||||||
|
{
|
||||||
|
if (strlen($secret) >= 32) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$envNames = match ($guard) {
|
||||||
|
'admin' => 'AUTH_ADMIN_JWT_SECRET or AUTH_JWT_SECRET',
|
||||||
|
'merchant' => 'AUTH_MERCHANT_JWT_SECRET or AUTH_JWT_SECRET',
|
||||||
|
default => 'the configured JWT secret',
|
||||||
|
};
|
||||||
|
|
||||||
|
throw new \RuntimeException(sprintf(
|
||||||
|
'JWT secret for guard "%s" is too short for HS256. Please set %s to at least 32 ASCII characters.',
|
||||||
|
$guard,
|
||||||
|
$envNames
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\common\utils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 易支付签名工具(MD5)
|
|
||||||
*
|
|
||||||
* 规则:
|
|
||||||
* - 排除 sign、sign_type
|
|
||||||
* - 排除空值(null / '')
|
|
||||||
* - 按字段名 ASCII 升序排序
|
|
||||||
* - k=v&...&key=app_secret
|
|
||||||
* - MD5 后转小写(兼容大小写比较)
|
|
||||||
*/
|
|
||||||
final class EpayUtil
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 生成签名字符串
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $params 请求参数
|
|
||||||
*/
|
|
||||||
public static function make(array $params, string $secret): string
|
|
||||||
{
|
|
||||||
unset($params['sign'], $params['sign_type']);
|
|
||||||
|
|
||||||
$filtered = [];
|
|
||||||
foreach ($params as $k => $v) {
|
|
||||||
if ($v === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (is_string($v) && trim($v) === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$filtered[$k] = is_bool($v) ? ($v ? '1' : '0') : (string)$v;
|
|
||||||
}
|
|
||||||
|
|
||||||
ksort($filtered);
|
|
||||||
|
|
||||||
$pairs = [];
|
|
||||||
foreach ($filtered as $k => $v) {
|
|
||||||
$pairs[] = $k . '=' . $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pairs[] = 'key=' . $secret;
|
|
||||||
|
|
||||||
return strtolower(md5(implode('&', $pairs)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验签名
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $params
|
|
||||||
*/
|
|
||||||
public static function verify(array $params, string $secret): bool
|
|
||||||
{
|
|
||||||
$sign = strtolower((string)($params['sign'] ?? ''));
|
|
||||||
if ($sign === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash_equals(self::make($params, $secret), $sign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\utils;
|
|
||||||
|
|
||||||
use Firebase\JWT\JWT;
|
|
||||||
use Firebase\JWT\Key;
|
|
||||||
|
|
||||||
class JwtUtil
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 生成 JWT
|
|
||||||
*/
|
|
||||||
public static function generateToken(array $payloadBase): string
|
|
||||||
{
|
|
||||||
$config = config('jwt', []);
|
|
||||||
$secret = $config['secret'] ?? 'mpay-secret';
|
|
||||||
$ttl = (int)($config['ttl'] ?? 7200);
|
|
||||||
$alg = $config['alg'] ?? 'HS256';
|
|
||||||
|
|
||||||
$now = time();
|
|
||||||
$payload = array_merge($payloadBase, [
|
|
||||||
'iat' => $now,
|
|
||||||
'exp' => $now + $ttl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return JWT::encode($payload, $secret, $alg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 JWT
|
|
||||||
*/
|
|
||||||
public static function parseToken(string $token): array
|
|
||||||
{
|
|
||||||
$config = config('jwt', []);
|
|
||||||
$secret = $config['secret'] ?? 'mpay-secret';
|
|
||||||
$alg = $config['alg'] ?? 'HS256';
|
|
||||||
|
|
||||||
$decoded = JWT::decode($token, new Key($secret, $alg));
|
|
||||||
return json_decode(json_encode($decoded, JSON_UNESCAPED_UNICODE), true) ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 ttl(秒)
|
|
||||||
*/
|
|
||||||
public static function getTtl(): int
|
|
||||||
{
|
|
||||||
$config = config('jwt', []);
|
|
||||||
return (int)($config['ttl'] ?? 7200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存前缀
|
|
||||||
*/
|
|
||||||
public static function getCachePrefix(): string
|
|
||||||
{
|
|
||||||
$config = config('jwt', []);
|
|
||||||
return $config['cache_prefix'] ?? 'token_';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\events;
|
|
||||||
|
|
||||||
use app\repositories\SystemConfigRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统配置相关事件处理
|
|
||||||
*
|
|
||||||
* 负责在配置更新后重新从数据库加载缓存
|
|
||||||
*/
|
|
||||||
class SystemConfig
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 重新加载系统配置缓存
|
|
||||||
*
|
|
||||||
* @param mixed $data 事件数据(此处用不到)
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function reload($data = null): void
|
|
||||||
{
|
|
||||||
// 通过仓储重新加载缓存
|
|
||||||
$repository = new SystemConfigRepository();
|
|
||||||
$repository->reloadCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
33
app/exception/BalanceInsufficientException.php
Normal file
33
app/exception/BalanceInsufficientException.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户余额不足异常。
|
||||||
|
*/
|
||||||
|
class BalanceInsufficientException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(int $merchantId = 0, int $needAmount = 0, int $availableAmount = 0, array $data = [])
|
||||||
|
{
|
||||||
|
parent::__construct('余额不足', 40011);
|
||||||
|
|
||||||
|
$payload = array_filter([
|
||||||
|
'merchant_id' => $merchantId ?: null,
|
||||||
|
'need_amount' => $needAmount ?: null,
|
||||||
|
'available_amount' => $availableAmount ?: null,
|
||||||
|
], static fn ($value) => $value !== null && $value !== '');
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$payload = array_merge($payload, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($payload)) {
|
||||||
|
$this->data($payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/exception/BusinessStateException.php
Normal file
25
app/exception/BusinessStateException.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务状态异常。
|
||||||
|
*
|
||||||
|
* 用于状态不允许、资源已禁用、资源不可用和不支持等场景,统一业务码为 40910。
|
||||||
|
*/
|
||||||
|
class BusinessStateException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(string $message = '业务状态不允许当前操作', array $data = [], int $bizCode = 40910)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $bizCode);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$this->data($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/exception/ConflictException.php
Normal file
25
app/exception/ConflictException.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冲突异常。
|
||||||
|
*
|
||||||
|
* 用于幂等冲突、重复请求和重复提交等场景,统一业务码为 40900。
|
||||||
|
*/
|
||||||
|
class ConflictException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(string $message = '业务冲突', array $data = [], int $bizCode = 40900)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $bizCode);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$this->data($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/exception/NotifyRetryExceededException.php
Normal file
31
app/exception/NotifyRetryExceededException.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知重试次数超限异常。
|
||||||
|
*/
|
||||||
|
class NotifyRetryExceededException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(string $notifyNo = '', array $data = [])
|
||||||
|
{
|
||||||
|
parent::__construct('通知重试次数超限', 40016);
|
||||||
|
|
||||||
|
$payload = array_filter([
|
||||||
|
'notify_no' => $notifyNo,
|
||||||
|
], static fn ($value) => $value !== '' && $value !== null);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$payload = array_merge($payload, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($payload)) {
|
||||||
|
$this->data($payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/exception/PaymentException.php
Normal file
25
app/exception/PaymentException.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付插件异常。
|
||||||
|
*
|
||||||
|
* 用于第三方渠道下单、查单、退款、验签和插件装配失败等场景。
|
||||||
|
*/
|
||||||
|
class PaymentException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,统一组装业务码与附加数据。
|
||||||
|
*/
|
||||||
|
public function __construct(string $message = '支付渠道处理失败', int $bizCode = 40200, array $data = [])
|
||||||
|
{
|
||||||
|
parent::__construct($message, $bizCode);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$this->data($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/exception/ResourceNotFoundException.php
Normal file
25
app/exception/ResourceNotFoundException.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源不存在异常。
|
||||||
|
*
|
||||||
|
* 作为所有“找不到”类业务异常的统一基类,统一业务码为 40400。
|
||||||
|
*/
|
||||||
|
class ResourceNotFoundException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(string $message = '资源不存在', array $data = [], int $bizCode = 40400)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $bizCode);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$this->data($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/exception/ValidationException.php
Normal file
30
app/exception/ValidationException.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\exception;
|
||||||
|
|
||||||
|
use Webman\Exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数校验异常。
|
||||||
|
*/
|
||||||
|
class ValidationException extends BusinessException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,组装异常信息。
|
||||||
|
*/
|
||||||
|
public function __construct(string $message = '参数校验失败', int|array $bizCodeOrData = 40001, array $data = [])
|
||||||
|
{
|
||||||
|
if (is_array($bizCodeOrData)) {
|
||||||
|
$data = $bizCodeOrData;
|
||||||
|
$bizCode = 40001;
|
||||||
|
} else {
|
||||||
|
$bizCode = $bizCodeOrData;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($message, $bizCode);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$this->data($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 请求参数错误异常
|
|
||||||
* 用于请求参数格式错误、验证码错误等情况
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new BadRequestException('验证码错误或已失效');
|
|
||||||
* throw new BadRequestException('请求参数格式错误');
|
|
||||||
*/
|
|
||||||
class BadRequestException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '请求参数错误', int $bizCode = 400, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 禁止访问异常
|
|
||||||
* 用于无权限、账号被禁用等情况
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new ForbiddenException('账号已被禁用');
|
|
||||||
* throw new ForbiddenException('无权限访问该资源');
|
|
||||||
*/
|
|
||||||
class ForbiddenException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '禁止访问', int $bizCode = 403, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统内部错误异常
|
|
||||||
* 用于配置文件错误、系统错误等不可预期的错误
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new InternalServerException('字典配置文件不存在');
|
|
||||||
* throw new InternalServerException('配置文件格式错误:' . json_last_error_msg());
|
|
||||||
* throw new InternalServerException('保存失败');
|
|
||||||
*/
|
|
||||||
class InternalServerException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '系统内部错误', int $bizCode = 500, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源不存在异常
|
|
||||||
* 用于资源未找到的情况
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new NotFoundException('用户不存在');
|
|
||||||
* throw new NotFoundException('未找到指定的字典:' . $code);
|
|
||||||
*/
|
|
||||||
class NotFoundException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '资源不存在', int $bizCode = 404, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付业务异常
|
|
||||||
*
|
|
||||||
* 用于支付相关业务错误,如:下单失败、退款失败、验签失败、渠道异常等。
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new PaymentException('当前环境无可用支付产品');
|
|
||||||
* throw new PaymentException('渠道返回错误', 402, ['channel_code' => 'lakala']);
|
|
||||||
*/
|
|
||||||
class PaymentException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '支付业务异常', int $bizCode = 402, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 未授权异常
|
|
||||||
* 用于认证失败、token无效等情况
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new UnauthorizedException('账号或密码错误');
|
|
||||||
* throw new UnauthorizedException('认证令牌已过期');
|
|
||||||
* throw new UnauthorizedException('认证令牌无效');
|
|
||||||
*/
|
|
||||||
class UnauthorizedException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '未授权', int $bizCode = 401, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\exceptions;
|
|
||||||
|
|
||||||
use Webman\Exception\BusinessException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参数校验异常
|
|
||||||
* 最常用的异常类型,用于参数验证、业务规则验证等
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* throw new ValidationException('优惠券和会员不可叠加使用');
|
|
||||||
* throw new ValidationException('手机号格式不正确');
|
|
||||||
* throw new ValidationException('金额必须大于0');
|
|
||||||
*/
|
|
||||||
class ValidationException extends BusinessException
|
|
||||||
{
|
|
||||||
public function __construct(string $message = '参数校验失败', int $bizCode = 422, array $data = [])
|
|
||||||
{
|
|
||||||
parent::__construct($message, $bizCode);
|
|
||||||
if (!empty($data)) {
|
|
||||||
$this->data($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\services\AuthService;
|
|
||||||
use app\services\CaptchaService;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 认证控制器
|
|
||||||
*
|
|
||||||
* 处理登录、验证码等认证相关接口
|
|
||||||
*/
|
|
||||||
class AuthController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected CaptchaService $captchaService,
|
|
||||||
protected AuthService $authService
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /captcha
|
|
||||||
*
|
|
||||||
* 生成验证码
|
|
||||||
*/
|
|
||||||
public function captcha(Request $request)
|
|
||||||
{
|
|
||||||
$data = $this->captchaService->generate();
|
|
||||||
return $this->success($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /login
|
|
||||||
*
|
|
||||||
* 用户登录
|
|
||||||
*/
|
|
||||||
public function login(Request $request)
|
|
||||||
{
|
|
||||||
$username = $request->post('username', '');
|
|
||||||
$password = $request->post('password', '');
|
|
||||||
$verifyCode = $request->post('verifyCode', '');
|
|
||||||
$captchaId = $request->post('captchaId', '');
|
|
||||||
|
|
||||||
// 参数校验
|
|
||||||
if (empty($username) || empty($password) || empty($verifyCode) || empty($captchaId)) {
|
|
||||||
return $this->fail('请填写完整登录信息', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->authService->login($username, $password, $verifyCode, $captchaId);
|
|
||||||
return $this->success($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,651 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\PaymentChannelRepository;
|
|
||||||
use app\repositories\PaymentMethodRepository;
|
|
||||||
use app\repositories\PaymentOrderRepository;
|
|
||||||
use app\services\ChannelRoutePolicyService;
|
|
||||||
use app\services\ChannelRouterService;
|
|
||||||
use app\services\PluginService;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
class ChannelController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected PaymentChannelRepository $channelRepository,
|
|
||||||
protected PaymentMethodRepository $methodRepository,
|
|
||||||
protected PaymentOrderRepository $orderRepository,
|
|
||||||
protected PluginService $pluginService,
|
|
||||||
protected ChannelRoutePolicyService $routePolicyService,
|
|
||||||
protected ChannelRouterService $channelRouterService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = max(1, (int)$request->get('page', 1));
|
|
||||||
$pageSize = max(1, (int)$request->get('page_size', 10));
|
|
||||||
$filters = $this->resolveChannelFilters($request, true);
|
|
||||||
return $this->page($this->channelRepository->searchPaginate($filters, $page, $pageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('通道ID不能为空', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$channel = $this->channelRepository->find($id);
|
|
||||||
if (!$channel) {
|
|
||||||
return $this->fail('通道不存在', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$methodCode = '';
|
|
||||||
if ((int)$channel->method_id > 0) {
|
|
||||||
$method = $this->methodRepository->find((int)$channel->method_id);
|
|
||||||
$methodCode = $method ? (string)$method->method_code : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$configSchema = $this->pluginService->getConfigSchema((string)$channel->plugin_code, $methodCode);
|
|
||||||
$currentConfig = $channel->getConfigArray();
|
|
||||||
if (isset($configSchema['fields']) && is_array($configSchema['fields'])) {
|
|
||||||
foreach ($configSchema['fields'] as &$field) {
|
|
||||||
$fieldName = $field['field'] ?? '';
|
|
||||||
if ($fieldName !== '' && array_key_exists($fieldName, $currentConfig)) {
|
|
||||||
$field['value'] = $currentConfig[$fieldName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($field);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$configSchema = ['fields' => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'channel' => $channel,
|
|
||||||
'method_code' => $methodCode,
|
|
||||||
'config_schema' => $configSchema,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = (int)($data['id'] ?? 0);
|
|
||||||
$merchantId = (int)($data['merchant_id'] ?? 0);
|
|
||||||
$merchantAppId = (int)($data['merchant_app_id'] ?? ($data['app_id'] ?? 0));
|
|
||||||
$channelCode = trim((string)($data['channel_code'] ?? ($data['chan_code'] ?? '')));
|
|
||||||
$channelName = trim((string)($data['channel_name'] ?? ($data['chan_name'] ?? '')));
|
|
||||||
$pluginCode = trim((string)($data['plugin_code'] ?? ''));
|
|
||||||
$methodCode = trim((string)($data['method_code'] ?? ''));
|
|
||||||
$enabledProducts = $data['enabled_products'] ?? [];
|
|
||||||
|
|
||||||
if ($merchantId <= 0) {
|
|
||||||
return $this->fail('请选择所属商户', 400);
|
|
||||||
}
|
|
||||||
if ($merchantAppId <= 0) {
|
|
||||||
return $this->fail('请选择所属应用', 400);
|
|
||||||
}
|
|
||||||
if ($channelName === '') {
|
|
||||||
return $this->fail('请输入通道名称', 400);
|
|
||||||
}
|
|
||||||
if ($pluginCode === '' || $methodCode === '') {
|
|
||||||
return $this->fail('支付插件和支付方式不能为空', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$method = $this->methodRepository->findAnyByCode($methodCode);
|
|
||||||
if (!$method) {
|
|
||||||
return $this->fail('支付方式不存在', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($channelCode !== '') {
|
|
||||||
$exists = $this->channelRepository->findByChanCode($channelCode);
|
|
||||||
if ($exists && (int)$exists->id !== $id) {
|
|
||||||
return $this->fail('通道编码已存在', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$configJson = $this->pluginService->buildConfigFromForm($pluginCode, $methodCode, $data);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return $this->fail('插件不存在或配置错误:' . $e->getMessage(), 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$channelData = [
|
|
||||||
'mer_id' => $merchantId,
|
|
||||||
'app_id' => $merchantAppId,
|
|
||||||
'chan_code' => $channelCode !== '' ? $channelCode : 'CH' . date('YmdHis') . mt_rand(1000, 9999),
|
|
||||||
'chan_name' => $channelName,
|
|
||||||
'plugin_code' => $pluginCode,
|
|
||||||
'pay_type_id' => (int)$method->id,
|
|
||||||
'config' => array_merge($configJson, [
|
|
||||||
'enabled_products' => is_array($enabledProducts) ? array_values($enabledProducts) : [],
|
|
||||||
]),
|
|
||||||
'split_ratio' => isset($data['split_ratio']) ? (float)$data['split_ratio'] : 100,
|
|
||||||
'chan_cost' => isset($data['channel_cost']) ? (float)$data['channel_cost'] : 0,
|
|
||||||
'chan_mode' => in_array(strtolower(trim((string)($data['channel_mode'] ?? 'wallet'))), ['1', 'direct', 'merchant'], true) ? 1 : 0,
|
|
||||||
'daily_limit' => isset($data['daily_limit']) ? (float)$data['daily_limit'] : 0,
|
|
||||||
'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) {
|
|
||||||
$channel = $this->channelRepository->find($id);
|
|
||||||
if (!$channel) {
|
|
||||||
return $this->fail('通道不存在', 404);
|
|
||||||
}
|
|
||||||
$this->channelRepository->updateById($id, $channelData);
|
|
||||||
} else {
|
|
||||||
$channel = $this->channelRepository->create($channelData);
|
|
||||||
$id = (int)$channel->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(['id' => $id], '保存成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
$status = $request->post('status', null);
|
|
||||||
if ($id <= 0 || $status === null) {
|
|
||||||
return $this->fail('参数错误', 400);
|
|
||||||
}
|
|
||||||
$ok = $this->channelRepository->updateById($id, ['status' => (int)$status]);
|
|
||||||
return $ok ? $this->success(null, '操作成功') : $this->fail('操作失败', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function monitor(Request $request)
|
|
||||||
{
|
|
||||||
$filters = $this->resolveChannelFilters($request);
|
|
||||||
$days = $this->resolveDays($request->get('days', 7));
|
|
||||||
$channels = $this->channelRepository->searchList($filters);
|
|
||||||
if ($channels->isEmpty()) {
|
|
||||||
return $this->success(['list' => [], 'summary' => $this->buildMonitorSummary([])]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderFilters = [
|
|
||||||
'merchant_id' => $filters['merchant_id'] ?? null,
|
|
||||||
'merchant_app_id' => $filters['merchant_app_id'] ?? null,
|
|
||||||
'method_id' => $filters['method_id'] ?? null,
|
|
||||||
'created_from' => $days['created_from'],
|
|
||||||
'created_to' => $days['created_to'],
|
|
||||||
];
|
|
||||||
$channelIds = [];
|
|
||||||
foreach ($channels as $channel) {
|
|
||||||
$channelIds[] = (int)$channel->id;
|
|
||||||
}
|
|
||||||
$statsMap = $this->orderRepository->aggregateByChannel($channelIds, $orderFilters);
|
|
||||||
$rows = [];
|
|
||||||
foreach ($channels as $channel) {
|
|
||||||
$rows[] = $this->buildMonitorRow($channel->toArray(), $statsMap[(int)$channel->id] ?? []);
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, function (array $left, array $right) {
|
|
||||||
if (($right['health_score'] ?? 0) === ($left['health_score'] ?? 0)) {
|
|
||||||
return ($left['sort'] ?? 0) <=> ($right['sort'] ?? 0);
|
|
||||||
}
|
|
||||||
return ($right['health_score'] ?? 0) <=> ($left['health_score'] ?? 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this->success(['list' => $rows, 'summary' => $this->buildMonitorSummary($rows)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function polling(Request $request)
|
|
||||||
{
|
|
||||||
$filters = $this->resolveChannelFilters($request);
|
|
||||||
$days = $this->resolveDays($request->get('days', 7));
|
|
||||||
$channels = $this->channelRepository->searchList($filters);
|
|
||||||
$testAmount = $request->get('test_amount', null);
|
|
||||||
$testAmount = ($testAmount === null || $testAmount === '') ? null : (float)$testAmount;
|
|
||||||
if ($channels->isEmpty()) {
|
|
||||||
return $this->success(['list' => [], 'summary' => $this->buildPollingSummary([])]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderFilters = [
|
|
||||||
'merchant_id' => $filters['merchant_id'] ?? null,
|
|
||||||
'merchant_app_id' => $filters['merchant_app_id'] ?? null,
|
|
||||||
'method_id' => $filters['method_id'] ?? null,
|
|
||||||
'created_from' => $days['created_from'],
|
|
||||||
'created_to' => $days['created_to'],
|
|
||||||
];
|
|
||||||
$channelIds = [];
|
|
||||||
foreach ($channels as $channel) {
|
|
||||||
$channelIds[] = (int)$channel->id;
|
|
||||||
}
|
|
||||||
$statsMap = $this->orderRepository->aggregateByChannel($channelIds, $orderFilters);
|
|
||||||
$rows = [];
|
|
||||||
foreach ($channels as $channel) {
|
|
||||||
$monitorRow = $this->buildMonitorRow($channel->toArray(), $statsMap[(int)$channel->id] ?? []);
|
|
||||||
$rows[] = $this->buildPollingRow($monitorRow, $testAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stateWeight = ['ready' => 0, 'degraded' => 1, 'blocked' => 2];
|
|
||||||
usort($rows, function (array $left, array $right) use ($stateWeight) {
|
|
||||||
$leftWeight = $stateWeight[$left['route_state'] ?? 'blocked'] ?? 9;
|
|
||||||
$rightWeight = $stateWeight[$right['route_state'] ?? 'blocked'] ?? 9;
|
|
||||||
if ($leftWeight === $rightWeight) {
|
|
||||||
if (($right['route_score'] ?? 0) === ($left['route_score'] ?? 0)) {
|
|
||||||
return ($left['sort'] ?? 0) <=> ($right['sort'] ?? 0);
|
|
||||||
}
|
|
||||||
return ($right['route_score'] ?? 0) <=> ($left['route_score'] ?? 0);
|
|
||||||
}
|
|
||||||
return $leftWeight <=> $rightWeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($rows as $index => &$row) {
|
|
||||||
$row['route_rank'] = $index + 1;
|
|
||||||
}
|
|
||||||
unset($row);
|
|
||||||
|
|
||||||
return $this->success(['list' => $rows, 'summary' => $this->buildPollingSummary($rows)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function policyList(Request $request)
|
|
||||||
{
|
|
||||||
$merchantId = (int)$request->get('merchant_id', 0);
|
|
||||||
$merchantAppId = (int)$request->get('merchant_app_id', $request->get('app_id', 0));
|
|
||||||
$methodCode = trim((string)$request->get('method_code', ''));
|
|
||||||
$pluginCode = trim((string)$request->get('plugin_code', ''));
|
|
||||||
$status = $request->get('status', null);
|
|
||||||
|
|
||||||
$policies = $this->routePolicyService->list();
|
|
||||||
$channelMap = [];
|
|
||||||
foreach ($this->channelRepository->searchList([]) as $channel) {
|
|
||||||
$channelMap[(int)$channel->id] = $channel->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
$filtered = array_values(array_filter($policies, function (array $policy) use ($merchantId, $merchantAppId, $methodCode, $pluginCode, $status) {
|
|
||||||
if ($merchantId > 0 && (int)($policy['merchant_id'] ?? 0) !== $merchantId) return false;
|
|
||||||
if ($merchantAppId > 0 && (int)($policy['merchant_app_id'] ?? 0) !== $merchantAppId) return false;
|
|
||||||
if ($methodCode !== '' && (string)($policy['method_code'] ?? '') !== $methodCode) return false;
|
|
||||||
if ($pluginCode !== '' && (string)($policy['plugin_code'] ?? '') !== $pluginCode) return false;
|
|
||||||
if ($status !== null && $status !== '' && (int)($policy['status'] ?? 0) !== (int)$status) return false;
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
|
|
||||||
$list = [];
|
|
||||||
foreach ($filtered as $policy) {
|
|
||||||
$items = [];
|
|
||||||
foreach (($policy['items'] ?? []) as $index => $item) {
|
|
||||||
$channelId = (int)($item['channel_id'] ?? 0);
|
|
||||||
$channel = $channelMap[$channelId] ?? [];
|
|
||||||
$items[] = [
|
|
||||||
'channel_id' => $channelId,
|
|
||||||
'role' => trim((string)($item['role'] ?? ($index === 0 ? 'primary' : 'backup'))),
|
|
||||||
'weight' => max(0, (int)($item['weight'] ?? 100)),
|
|
||||||
'priority' => max(1, (int)($item['priority'] ?? ($index + 1))),
|
|
||||||
'chan_code' => (string)($channel['chan_code'] ?? ''),
|
|
||||||
'chan_name' => (string)($channel['chan_name'] ?? ''),
|
|
||||||
'channel_status' => isset($channel['status']) ? (int)$channel['status'] : null,
|
|
||||||
'sort' => (int)($channel['sort'] ?? 0),
|
|
||||||
'plugin_code' => (string)($channel['plugin_code'] ?? ''),
|
|
||||||
'method_id' => (int)($channel['method_id'] ?? 0),
|
|
||||||
'merchant_id' => (int)($channel['merchant_id'] ?? 0),
|
|
||||||
'merchant_app_id' => (int)($channel['merchant_app_id'] ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
usort($items, fn(array $left, array $right) => ($left['priority'] ?? 0) <=> ($right['priority'] ?? 0));
|
|
||||||
$policy['items'] = $items;
|
|
||||||
$policy['channel_count'] = count($items);
|
|
||||||
$list[] = $policy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'list' => $list,
|
|
||||||
'summary' => [
|
|
||||||
'total' => count($list),
|
|
||||||
'enabled' => count(array_filter($list, fn(array $policy) => (int)($policy['status'] ?? 0) === 1)),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function policySave(Request $request)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$payload = $this->preparePolicyPayload($request->post(), true);
|
|
||||||
return $this->success($this->routePolicyService->save($payload), '保存成功');
|
|
||||||
} catch (\InvalidArgumentException $e) {
|
|
||||||
return $this->fail($e->getMessage(), 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function policyPreview(Request $request)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$payload = $this->preparePolicyPayload($request->post(), false);
|
|
||||||
$testAmount = $request->post('test_amount', $request->post('preview_amount', 0));
|
|
||||||
$amount = ($testAmount === null || $testAmount === '') ? 0 : (float)$testAmount;
|
|
||||||
$preview = $this->channelRouterService->previewPolicyDraft(
|
|
||||||
(int)$payload['merchant_id'],
|
|
||||||
(int)$payload['merchant_app_id'],
|
|
||||||
(int)$payload['method_id'],
|
|
||||||
$payload,
|
|
||||||
$amount
|
|
||||||
);
|
|
||||||
return $this->success($preview);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return $this->fail($e->getMessage(), 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function policyDelete(Request $request)
|
|
||||||
{
|
|
||||||
$id = trim((string)$request->post('id', ''));
|
|
||||||
if ($id === '') {
|
|
||||||
return $this->fail('策略ID不能为空', 400);
|
|
||||||
}
|
|
||||||
$ok = $this->routePolicyService->delete($id);
|
|
||||||
return $ok ? $this->success(null, '删除成功') : $this->fail('策略不存在或已删除', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function preparePolicyPayload(array $data, bool $requirePolicyName = true): array
|
|
||||||
{
|
|
||||||
$policyName = trim((string)($data['policy_name'] ?? ''));
|
|
||||||
$merchantId = (int)($data['merchant_id'] ?? 0);
|
|
||||||
$merchantAppId = (int)($data['merchant_app_id'] ?? ($data['app_id'] ?? 0));
|
|
||||||
$methodCode = trim((string)($data['method_code'] ?? ''));
|
|
||||||
$pluginCode = trim((string)($data['plugin_code'] ?? ''));
|
|
||||||
$routeMode = trim((string)($data['route_mode'] ?? 'priority'));
|
|
||||||
$status = (int)($data['status'] ?? 1);
|
|
||||||
$itemsInput = $data['items'] ?? [];
|
|
||||||
|
|
||||||
if ($requirePolicyName && $policyName === '') throw new \InvalidArgumentException('请输入策略名称');
|
|
||||||
if ($methodCode === '') throw new \InvalidArgumentException('请选择支付方式');
|
|
||||||
if (!in_array($routeMode, ['priority', 'weight', 'failover'], true)) throw new \InvalidArgumentException('路由模式不合法');
|
|
||||||
if (!is_array($itemsInput) || $itemsInput === []) throw new \InvalidArgumentException('请至少选择一个通道');
|
|
||||||
if ($merchantId <= 0 || $merchantAppId <= 0) throw new \InvalidArgumentException('请先选择商户和应用');
|
|
||||||
|
|
||||||
$method = $this->methodRepository->findAnyByCode($methodCode);
|
|
||||||
if (!$method) throw new \InvalidArgumentException('支付方式不存在');
|
|
||||||
|
|
||||||
$channelMap = [];
|
|
||||||
foreach ($this->channelRepository->searchList([]) as $channel) {
|
|
||||||
$channelMap[(int)$channel->id] = $channel->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
$normalizedItems = [];
|
|
||||||
$usedChannelIds = [];
|
|
||||||
foreach ($itemsInput as $index => $item) {
|
|
||||||
$channelId = (int)($item['channel_id'] ?? 0);
|
|
||||||
if ($channelId <= 0) throw new \InvalidArgumentException('策略项中的通道ID不合法');
|
|
||||||
if (in_array($channelId, $usedChannelIds, true)) throw new \InvalidArgumentException('策略中存在重复通道,请去重后再提交');
|
|
||||||
|
|
||||||
$channel = $channelMap[$channelId] ?? null;
|
|
||||||
if (!$channel) throw new \InvalidArgumentException('存在未找到的通道,请刷新后重试');
|
|
||||||
if ($merchantId > 0 && (int)$channel['merchant_id'] !== $merchantId) throw new \InvalidArgumentException('策略中的通道与商户不匹配');
|
|
||||||
if ($merchantAppId > 0 && (int)$channel['merchant_app_id'] !== $merchantAppId) throw new \InvalidArgumentException('策略中的通道与应用不匹配');
|
|
||||||
if ((int)$channel['method_id'] !== (int)$method->id) throw new \InvalidArgumentException('策略中的通道与支付方式不匹配');
|
|
||||||
if ($pluginCode !== '' && (string)$channel['plugin_code'] !== $pluginCode) throw new \InvalidArgumentException('策略中的通道与插件不匹配');
|
|
||||||
|
|
||||||
$defaultRole = $routeMode === 'weight' ? 'normal' : ($index === 0 ? 'primary' : 'backup');
|
|
||||||
$role = trim((string)($item['role'] ?? $defaultRole));
|
|
||||||
if (!in_array($role, ['primary', 'backup', 'normal'], true)) {
|
|
||||||
$role = $defaultRole;
|
|
||||||
}
|
|
||||||
$normalizedItems[] = [
|
|
||||||
'channel_id' => $channelId,
|
|
||||||
'role' => $role,
|
|
||||||
'weight' => max(0, (int)($item['weight'] ?? 100)),
|
|
||||||
'priority' => max(1, (int)($item['priority'] ?? ($index + 1))),
|
|
||||||
];
|
|
||||||
$usedChannelIds[] = $channelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($normalizedItems, function (array $left, array $right) {
|
|
||||||
if (($left['priority'] ?? 0) === ($right['priority'] ?? 0)) {
|
|
||||||
return ($right['weight'] ?? 0) <=> ($left['weight'] ?? 0);
|
|
||||||
}
|
|
||||||
return ($left['priority'] ?? 0) <=> ($right['priority'] ?? 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($normalizedItems as $index => &$item) {
|
|
||||||
$item['priority'] = $index + 1;
|
|
||||||
if ($routeMode === 'weight' && $item['role'] === 'backup') {
|
|
||||||
$item['role'] = 'normal';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($item);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => trim((string)($data['id'] ?? '')),
|
|
||||||
'policy_name' => $policyName !== '' ? $policyName : '策略草稿预览',
|
|
||||||
'merchant_id' => $merchantId,
|
|
||||||
'merchant_app_id' => $merchantAppId,
|
|
||||||
'method_code' => $methodCode,
|
|
||||||
'method_id' => (int)$method->id,
|
|
||||||
'plugin_code' => $pluginCode,
|
|
||||||
'route_mode' => $routeMode,
|
|
||||||
'status' => $status,
|
|
||||||
'circuit_breaker_threshold' => max(0, min(100, (int)($data['circuit_breaker_threshold'] ?? 50))),
|
|
||||||
'failover_cooldown' => max(0, (int)($data['failover_cooldown'] ?? 10)),
|
|
||||||
'remark' => trim((string)($data['remark'] ?? '')),
|
|
||||||
'items' => $normalizedItems,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveChannelFilters(Request $request, bool $withKeywords = false): array
|
|
||||||
{
|
|
||||||
$filters = [];
|
|
||||||
$merchantId = (int)$request->get('merchant_id', 0);
|
|
||||||
if ($merchantId > 0) $filters['merchant_id'] = $merchantId;
|
|
||||||
$merchantAppId = (int)$request->get('merchant_app_id', $request->get('app_id', 0));
|
|
||||||
if ($merchantAppId > 0) $filters['merchant_app_id'] = $merchantAppId;
|
|
||||||
$methodCode = trim((string)$request->get('method_code', ''));
|
|
||||||
if ($methodCode !== '') {
|
|
||||||
$method = $this->methodRepository->findAnyByCode($methodCode);
|
|
||||||
$filters['method_id'] = $method ? (int)$method->id : -1;
|
|
||||||
}
|
|
||||||
$pluginCode = trim((string)$request->get('plugin_code', ''));
|
|
||||||
if ($pluginCode !== '') $filters['plugin_code'] = $pluginCode;
|
|
||||||
$status = $request->get('status', null);
|
|
||||||
if ($status !== null && $status !== '') $filters['status'] = (int)$status;
|
|
||||||
if ($withKeywords) {
|
|
||||||
$chanCode = trim((string)$request->get('chan_code', ''));
|
|
||||||
if ($chanCode !== '') $filters['chan_code'] = $chanCode;
|
|
||||||
$chanName = trim((string)$request->get('chan_name', ''));
|
|
||||||
if ($chanName !== '') $filters['chan_name'] = $chanName;
|
|
||||||
}
|
|
||||||
return $filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveDays(mixed $daysInput): array
|
|
||||||
{
|
|
||||||
$days = max(1, min(30, (int)$daysInput));
|
|
||||||
return [
|
|
||||||
'days' => $days,
|
|
||||||
'created_from' => date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')),
|
|
||||||
'created_to' => date('Y-m-d H:i:s'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildMonitorRow(array $channel, array $stats): array
|
|
||||||
{
|
|
||||||
$totalOrders = (int)($stats['total_orders'] ?? 0);
|
|
||||||
$successOrders = (int)($stats['success_orders'] ?? 0);
|
|
||||||
$pendingOrders = (int)($stats['pending_orders'] ?? 0);
|
|
||||||
$failOrders = (int)($stats['fail_orders'] ?? 0);
|
|
||||||
$closedOrders = (int)($stats['closed_orders'] ?? 0);
|
|
||||||
$todayOrders = (int)($stats['today_orders'] ?? 0);
|
|
||||||
$todaySuccessOrders = (int)($stats['today_success_orders'] ?? 0);
|
|
||||||
$todaySuccessAmount = round((float)($stats['today_success_amount'] ?? 0), 2);
|
|
||||||
$successRate = $totalOrders > 0 ? round($successOrders / $totalOrders * 100, 2) : 0;
|
|
||||||
$dailyLimit = isset($channel['daily_limit']) ? (float)$channel['daily_limit'] : 0;
|
|
||||||
$dailyCnt = isset($channel['daily_cnt']) ? (int)$channel['daily_cnt'] : 0;
|
|
||||||
$todayLimitUsageRate = $dailyLimit > 0 ? round(min(100, ($todaySuccessAmount / $dailyLimit) * 100), 2) : null;
|
|
||||||
$healthScore = 0;
|
|
||||||
$healthLevel = 'disabled';
|
|
||||||
$status = (int)($channel['status'] ?? 0);
|
|
||||||
|
|
||||||
if ($status === 1) {
|
|
||||||
if ($totalOrders === 0) {
|
|
||||||
$healthScore = 60;
|
|
||||||
$healthLevel = 'idle';
|
|
||||||
} else {
|
|
||||||
$healthScore = 90;
|
|
||||||
if ($successRate < 95) $healthScore -= 10;
|
|
||||||
if ($successRate < 80) $healthScore -= 15;
|
|
||||||
if ($successRate < 60) $healthScore -= 20;
|
|
||||||
if ($failOrders > 0) $healthScore -= min(15, $failOrders * 3);
|
|
||||||
if ($pendingOrders > max(3, (int)floor($successOrders / 2))) $healthScore -= 10;
|
|
||||||
if ($todayLimitUsageRate !== null && $todayLimitUsageRate >= 90) $healthScore -= 20;
|
|
||||||
elseif ($todayLimitUsageRate !== null && $todayLimitUsageRate >= 75) $healthScore -= 10;
|
|
||||||
$healthScore = max(0, min(100, $healthScore));
|
|
||||||
if ($healthScore >= 80) $healthLevel = 'healthy';
|
|
||||||
elseif ($healthScore >= 60) $healthLevel = 'warning';
|
|
||||||
else $healthLevel = 'danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => (int)($channel['id'] ?? 0),
|
|
||||||
'merchant_id' => (int)($channel['merchant_id'] ?? 0),
|
|
||||||
'merchant_app_id' => (int)($channel['merchant_app_id'] ?? 0),
|
|
||||||
'chan_code' => (string)($channel['chan_code'] ?? ''),
|
|
||||||
'chan_name' => (string)($channel['chan_name'] ?? ''),
|
|
||||||
'plugin_code' => (string)($channel['plugin_code'] ?? ''),
|
|
||||||
'method_id' => (int)($channel['method_id'] ?? 0),
|
|
||||||
'status' => $status,
|
|
||||||
'sort' => (int)($channel['sort'] ?? 0),
|
|
||||||
'daily_limit' => $dailyLimit > 0 ? round($dailyLimit, 2) : 0,
|
|
||||||
'daily_cnt' => $dailyCnt > 0 ? $dailyCnt : 0,
|
|
||||||
'min_amount' => $channel['min_amount'] === null ? null : round((float)$channel['min_amount'], 2),
|
|
||||||
'max_amount' => $channel['max_amount'] === null ? null : round((float)$channel['max_amount'], 2),
|
|
||||||
'total_orders' => $totalOrders,
|
|
||||||
'success_orders' => $successOrders,
|
|
||||||
'pending_orders' => $pendingOrders,
|
|
||||||
'fail_orders' => $failOrders,
|
|
||||||
'closed_orders' => $closedOrders,
|
|
||||||
'today_orders' => $todayOrders,
|
|
||||||
'today_success_orders' => $todaySuccessOrders,
|
|
||||||
'total_amount' => round((float)($stats['total_amount'] ?? 0), 2),
|
|
||||||
'success_amount' => round((float)($stats['success_amount'] ?? 0), 2),
|
|
||||||
'today_amount' => round((float)($stats['today_amount'] ?? 0), 2),
|
|
||||||
'today_success_amount' => $todaySuccessAmount,
|
|
||||||
'last_order_at' => $stats['last_order_at'] ?? null,
|
|
||||||
'last_success_at' => $stats['last_success_at'] ?? null,
|
|
||||||
'success_rate' => $successRate,
|
|
||||||
'today_limit_usage_rate' => $todayLimitUsageRate,
|
|
||||||
'health_score' => $healthScore,
|
|
||||||
'health_level' => $healthLevel,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildMonitorSummary(array $rows): array
|
|
||||||
{
|
|
||||||
$summary = [
|
|
||||||
'total_channels' => count($rows),
|
|
||||||
'enabled_channels' => 0,
|
|
||||||
'healthy_channels' => 0,
|
|
||||||
'warning_channels' => 0,
|
|
||||||
'danger_channels' => 0,
|
|
||||||
'total_orders' => 0,
|
|
||||||
'success_rate' => 0,
|
|
||||||
'today_success_amount' => 0,
|
|
||||||
];
|
|
||||||
$successOrders = 0;
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
if ((int)($row['status'] ?? 0) === 1) $summary['enabled_channels']++;
|
|
||||||
$level = $row['health_level'] ?? '';
|
|
||||||
if ($level === 'healthy') $summary['healthy_channels']++;
|
|
||||||
elseif ($level === 'warning') $summary['warning_channels']++;
|
|
||||||
elseif ($level === 'danger') $summary['danger_channels']++;
|
|
||||||
$summary['total_orders'] += (int)($row['total_orders'] ?? 0);
|
|
||||||
$summary['today_success_amount'] = round($summary['today_success_amount'] + (float)($row['today_success_amount'] ?? 0), 2);
|
|
||||||
$successOrders += (int)($row['success_orders'] ?? 0);
|
|
||||||
}
|
|
||||||
if ($summary['total_orders'] > 0) {
|
|
||||||
$summary['success_rate'] = round($successOrders / $summary['total_orders'] * 100, 2);
|
|
||||||
}
|
|
||||||
return $summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildPollingRow(array $monitorRow, ?float $testAmount): array
|
|
||||||
{
|
|
||||||
$reasons = [];
|
|
||||||
$status = (int)($monitorRow['status'] ?? 0);
|
|
||||||
$dailyLimit = (float)($monitorRow['daily_limit'] ?? 0);
|
|
||||||
$dailyCnt = (int)($monitorRow['daily_cnt'] ?? 0);
|
|
||||||
$todaySuccessAmount = (float)($monitorRow['today_success_amount'] ?? 0);
|
|
||||||
$todayOrders = (int)($monitorRow['today_orders'] ?? 0);
|
|
||||||
$minAmount = $monitorRow['min_amount'];
|
|
||||||
$maxAmount = $monitorRow['max_amount'];
|
|
||||||
$remainingDailyLimit = $dailyLimit > 0 ? round($dailyLimit - $todaySuccessAmount, 2) : null;
|
|
||||||
$remainingDailyCount = $dailyCnt > 0 ? $dailyCnt - $todayOrders : null;
|
|
||||||
$routeState = 'ready';
|
|
||||||
|
|
||||||
if ($status !== 1) { $routeState = 'blocked'; $reasons[] = '通道已禁用'; }
|
|
||||||
if ($testAmount !== null) {
|
|
||||||
if ($minAmount !== null && $testAmount < (float)$minAmount) { $routeState = 'blocked'; $reasons[] = '低于最小支付金额'; }
|
|
||||||
if ($maxAmount !== null && (float)$maxAmount > 0 && $testAmount > (float)$maxAmount) { $routeState = 'blocked'; $reasons[] = '超过最大支付金额'; }
|
|
||||||
}
|
|
||||||
if ($remainingDailyLimit !== null && $remainingDailyLimit <= 0) { $routeState = 'blocked'; $reasons[] = '单日限额已用尽'; }
|
|
||||||
if ($remainingDailyCount !== null && $remainingDailyCount <= 0) { $routeState = 'blocked'; $reasons[] = '单日笔数已用尽'; }
|
|
||||||
if ($routeState !== 'blocked') {
|
|
||||||
if (($monitorRow['health_level'] ?? '') === 'warning' || ($monitorRow['health_level'] ?? '') === 'danger') { $routeState = 'degraded'; $reasons[] = '监控健康度偏低'; }
|
|
||||||
if ((int)($monitorRow['total_orders'] ?? 0) === 0) { $routeState = 'degraded'; $reasons[] = '暂无订单样本,建议灰度'; }
|
|
||||||
if ((float)($monitorRow['success_rate'] ?? 0) < 80 && (int)($monitorRow['total_orders'] ?? 0) > 0) { $routeState = 'degraded'; $reasons[] = '成功率偏低'; }
|
|
||||||
if ((int)($monitorRow['pending_orders'] ?? 0) > max(3, (int)($monitorRow['success_orders'] ?? 0))) { $routeState = 'degraded'; $reasons[] = '待支付订单偏多'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
$priorityBonus = max(0, 20 - min(20, (int)($monitorRow['sort'] ?? 0) * 2));
|
|
||||||
$sampleBonus = (int)($monitorRow['total_orders'] ?? 0) > 0 ? min(10, (int)floor(((float)($monitorRow['success_rate'] ?? 0)) / 10)) : 5;
|
|
||||||
$routeScore = round(max(0, min(100, ((float)($monitorRow['health_score'] ?? 0) * 0.7) + $priorityBonus + $sampleBonus)), 2);
|
|
||||||
if ($routeState === 'degraded') $routeScore = max(0, round($routeScore - 15, 2));
|
|
||||||
if ($routeState === 'blocked') $routeScore = 0;
|
|
||||||
|
|
||||||
return array_merge($monitorRow, [
|
|
||||||
'route_state' => $routeState,
|
|
||||||
'route_rank' => 0,
|
|
||||||
'route_score' => $routeScore,
|
|
||||||
'remaining_daily_limit' => $remainingDailyLimit === null ? null : round(max(0, $remainingDailyLimit), 2),
|
|
||||||
'remaining_daily_count' => $remainingDailyCount === null ? null : max(0, $remainingDailyCount),
|
|
||||||
'reasons' => array_values(array_unique($reasons)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildPollingSummary(array $rows): array
|
|
||||||
{
|
|
||||||
$summary = [
|
|
||||||
'total_channels' => count($rows),
|
|
||||||
'ready_channels' => 0,
|
|
||||||
'degraded_channels' => 0,
|
|
||||||
'blocked_channels' => 0,
|
|
||||||
'recommended_channel' => null,
|
|
||||||
'fallback_chain' => [],
|
|
||||||
];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$state = $row['route_state'] ?? 'blocked';
|
|
||||||
if ($state === 'ready') $summary['ready_channels']++;
|
|
||||||
elseif ($state === 'degraded') $summary['degraded_channels']++;
|
|
||||||
else $summary['blocked_channels']++;
|
|
||||||
}
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
if ($summary['recommended_channel'] === null && ($row['route_state'] ?? '') !== 'blocked') {
|
|
||||||
$summary['recommended_channel'] = $row;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (($row['route_state'] ?? '') !== 'blocked' && count($summary['fallback_chain']) < 5) {
|
|
||||||
$summary['fallback_chain'][] = sprintf('%s(%s)', (string)($row['chan_name'] ?? ''), (string)($row['chan_code'] ?? ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($summary['recommended_channel'] !== null) {
|
|
||||||
$recommendedId = (int)($summary['recommended_channel']['id'] ?? 0);
|
|
||||||
if ($recommendedId > 0) {
|
|
||||||
$summary['fallback_chain'] = [];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
if ((int)($row['id'] ?? 0) === $recommendedId || ($row['route_state'] ?? '') === 'blocked') continue;
|
|
||||||
$summary['fallback_chain'][] = sprintf('%s(%s)', (string)($row['chan_name'] ?? ''), (string)($row['chan_code'] ?? ''));
|
|
||||||
if (count($summary['fallback_chain']) >= 5) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $summary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\PaymentMethodRepository;
|
|
||||||
use support\Db;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
class FinanceController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected PaymentMethodRepository $methodRepository,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reconciliation(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(*) AS total_orders,
|
|
||||||
SUM(CASE WHEN o.status = 1 THEN 1 ELSE 0 END) AS success_orders,
|
|
||||||
SUM(CASE WHEN o.status = 0 THEN 1 ELSE 0 END) AS pending_orders,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders,
|
|
||||||
COALESCE(SUM(o.amount), 0) AS total_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS total_fee,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS total_net_amount'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
"o.*, m.merchant_no, m.merchant_name, ma.app_id AS merchant_app_code, ma.app_name, pm.method_code, pm.method_name,
|
|
||||||
pc.chan_code, pc.chan_name,
|
|
||||||
COALESCE(o.real_amount - o.fee, 0) AS net_amount,
|
|
||||||
JSON_UNQUOTE(JSON_EXTRACT(o.extra, '$.routing.policy.policy_name')) AS route_policy_name"
|
|
||||||
)
|
|
||||||
->orderByDesc('o.id')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['reconcile_status'] = $this->reconcileStatus((int)($item['status'] ?? 0), (int)($item['notify_stat'] ?? 0));
|
|
||||||
$item['reconcile_status_text'] = $this->reconcileStatusText($item['reconcile_status']);
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'total_orders' => (int)($summaryRow->total_orders ?? 0),
|
|
||||||
'success_orders' => (int)($summaryRow->success_orders ?? 0),
|
|
||||||
'pending_orders' => (int)($summaryRow->pending_orders ?? 0),
|
|
||||||
'notify_pending_orders' => (int)($summaryRow->notify_pending_orders ?? 0),
|
|
||||||
'total_amount' => (string)($summaryRow->total_amount ?? '0.00'),
|
|
||||||
'total_fee' => (string)($summaryRow->total_fee ?? '0.00'),
|
|
||||||
'total_net_amount' => (string)($summaryRow->total_net_amount ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function settlement(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters)->where('o.status', 1);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT o.merchant_app_id) AS app_count,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS notify_pending_amount'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'o.merchant_id, o.merchant_app_id,
|
|
||||||
m.merchant_no, m.merchant_name, ma.app_id AS merchant_app_code, ma.app_name,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COUNT(DISTINCT o.channel_id) AS channel_count,
|
|
||||||
COUNT(DISTINCT o.method_id) AS method_count,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS notify_pending_amount,
|
|
||||||
MAX(o.pay_at) AS last_pay_at'
|
|
||||||
)
|
|
||||||
->groupBy('o.merchant_id', 'o.merchant_app_id', 'm.merchant_no', 'm.merchant_name', 'ma.app_id', 'ma.app_name')
|
|
||||||
->orderByRaw('SUM(o.real_amount - o.fee) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'app_count' => (int)($summaryRow->app_count ?? 0),
|
|
||||||
'success_orders' => (int)($summaryRow->success_orders ?? 0),
|
|
||||||
'gross_amount' => (string)($summaryRow->gross_amount ?? '0.00'),
|
|
||||||
'fee_amount' => (string)($summaryRow->fee_amount ?? '0.00'),
|
|
||||||
'net_amount' => (string)($summaryRow->net_amount ?? '0.00'),
|
|
||||||
'notify_pending_orders' => (int)($summaryRow->notify_pending_orders ?? 0),
|
|
||||||
'notify_pending_amount' => (string)($summaryRow->notify_pending_amount ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => array_map(fn ($row) => (array)$row, $paginator->items()),
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fee(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters);
|
|
||||||
|
|
||||||
if (($filters['status'] ?? '') === '') {
|
|
||||||
$baseQuery->where('o.status', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT o.channel_id) AS channel_count,
|
|
||||||
COUNT(DISTINCT o.method_id) AS method_count,
|
|
||||||
COUNT(*) AS order_count,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS total_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS total_fee'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'o.merchant_id, o.channel_id, o.method_id,
|
|
||||||
m.merchant_no, m.merchant_name,
|
|
||||||
pm.method_code, pm.method_name,
|
|
||||||
pc.chan_code, pc.chan_name,
|
|
||||||
COUNT(*) AS order_count,
|
|
||||||
SUM(CASE WHEN o.status = 1 THEN 1 ELSE 0 END) AS success_orders,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS total_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS total_fee,
|
|
||||||
COALESCE(AVG(CASE WHEN o.real_amount > 0 THEN o.fee / o.real_amount ELSE NULL END), 0) AS avg_fee_rate,
|
|
||||||
MAX(o.created_at) AS last_order_at'
|
|
||||||
)
|
|
||||||
->groupBy('o.merchant_id', 'o.channel_id', 'o.method_id', 'm.merchant_no', 'm.merchant_name', 'pm.method_code', 'pm.method_name', 'pc.chan_code', 'pc.chan_name')
|
|
||||||
->orderByRaw('SUM(o.fee) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['avg_fee_rate_percent'] = round(((float)($item['avg_fee_rate'] ?? 0)) * 100, 4);
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'channel_count' => (int)($summaryRow->channel_count ?? 0),
|
|
||||||
'method_count' => (int)($summaryRow->method_count ?? 0),
|
|
||||||
'order_count' => (int)($summaryRow->order_count ?? 0),
|
|
||||||
'total_amount' => (string)($summaryRow->total_amount ?? '0.00'),
|
|
||||||
'total_fee' => (string)($summaryRow->total_fee ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function settlementRecord(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters)->where('o.status', 1);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
"COUNT(DISTINCT CONCAT(o.merchant_app_id, '#', DATE(COALESCE(o.pay_at, o.created_at)))) AS record_count,
|
|
||||||
COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT o.merchant_app_id) AS app_count,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders"
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
"DATE(COALESCE(o.pay_at, o.created_at)) AS settlement_date,
|
|
||||||
o.merchant_id, o.merchant_app_id,
|
|
||||||
m.merchant_no, m.merchant_name, ma.app_id AS merchant_app_code, ma.app_name,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders,
|
|
||||||
MAX(o.pay_at) AS last_pay_at"
|
|
||||||
)
|
|
||||||
->groupByRaw("DATE(COALESCE(o.pay_at, o.created_at)), o.merchant_id, o.merchant_app_id, m.merchant_no, m.merchant_name, ma.app_id, ma.app_name")
|
|
||||||
->orderByDesc('settlement_date')
|
|
||||||
->orderByRaw('SUM(o.real_amount - o.fee) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['settlement_status'] = (int)($item['notify_pending_orders'] ?? 0) > 0 ? 'pending' : 'ready';
|
|
||||||
$item['settlement_status_text'] = $item['settlement_status'] === 'ready' ? 'ready' : 'pending_notify';
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'record_count' => (int)($summaryRow->record_count ?? 0),
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'app_count' => (int)($summaryRow->app_count ?? 0),
|
|
||||||
'success_orders' => (int)($summaryRow->success_orders ?? 0),
|
|
||||||
'gross_amount' => (string)($summaryRow->gross_amount ?? '0.00'),
|
|
||||||
'fee_amount' => (string)($summaryRow->fee_amount ?? '0.00'),
|
|
||||||
'net_amount' => (string)($summaryRow->net_amount ?? '0.00'),
|
|
||||||
'notify_pending_orders' => (int)($summaryRow->notify_pending_orders ?? 0),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function batchSettlement(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters)->where('o.status', 1);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
"COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT o.merchant_app_id) AS app_count,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COUNT(DISTINCT CONCAT(o.merchant_app_id, '#', DATE(COALESCE(o.pay_at, o.created_at)))) AS batch_days,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS ready_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS pending_amount"
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
"o.merchant_id, o.merchant_app_id,
|
|
||||||
m.merchant_no, m.merchant_name, ma.app_id AS merchant_app_code, ma.app_name,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COUNT(DISTINCT DATE(COALESCE(o.pay_at, o.created_at))) AS batch_days,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS ready_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS pending_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS pending_notify_orders,
|
|
||||||
MAX(o.pay_at) AS last_pay_at"
|
|
||||||
)
|
|
||||||
->groupBy('o.merchant_id', 'o.merchant_app_id', 'm.merchant_no', 'm.merchant_name', 'ma.app_id', 'ma.app_name')
|
|
||||||
->orderByRaw('SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['batch_status'] = (float)($item['pending_amount'] ?? 0) > 0 ? 'pending' : 'ready';
|
|
||||||
$item['batch_status_text'] = $item['batch_status'] === 'ready' ? 'ready_to_batch' : 'pending_notify';
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'app_count' => (int)($summaryRow->app_count ?? 0),
|
|
||||||
'success_orders' => (int)($summaryRow->success_orders ?? 0),
|
|
||||||
'batch_days' => (int)($summaryRow->batch_days ?? 0),
|
|
||||||
'ready_amount' => (string)($summaryRow->ready_amount ?? '0.00'),
|
|
||||||
'pending_amount' => (string)($summaryRow->pending_amount ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function split(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters)->where('o.status', 1);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT o.channel_id) AS channel_count,
|
|
||||||
COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(*) AS order_count,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
COALESCE(SUM((o.real_amount - o.fee) * COALESCE(pc.split_ratio, 100) / 100), 0) AS merchant_share_amount,
|
|
||||||
COALESCE(SUM((o.real_amount - o.fee) * (100 - COALESCE(pc.split_ratio, 100)) / 100), 0) AS platform_share_amount,
|
|
||||||
COALESCE(SUM(o.real_amount * COALESCE(pc.chan_cost, 0) / 100), 0) AS channel_cost_amount'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'o.merchant_id, o.channel_id, o.method_id,
|
|
||||||
m.merchant_no, m.merchant_name,
|
|
||||||
pm.method_code, pm.method_name,
|
|
||||||
pc.chan_code, pc.chan_name,
|
|
||||||
COALESCE(pc.split_ratio, 100) AS split_ratio,
|
|
||||||
COALESCE(pc.chan_cost, 0) AS chan_cost,
|
|
||||||
COUNT(*) AS order_count,
|
|
||||||
COALESCE(SUM(o.real_amount), 0) AS gross_amount,
|
|
||||||
COALESCE(SUM(o.fee), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
COALESCE(SUM((o.real_amount - o.fee) * COALESCE(pc.split_ratio, 100) / 100), 0) AS merchant_share_amount,
|
|
||||||
COALESCE(SUM((o.real_amount - o.fee) * (100 - COALESCE(pc.split_ratio, 100)) / 100), 0) AS platform_share_amount,
|
|
||||||
COALESCE(SUM(o.real_amount * COALESCE(pc.chan_cost, 0) / 100), 0) AS channel_cost_amount,
|
|
||||||
MAX(o.pay_at) AS last_pay_at'
|
|
||||||
)
|
|
||||||
->groupBy('o.merchant_id', 'o.channel_id', 'o.method_id', 'm.merchant_no', 'm.merchant_name', 'pm.method_code', 'pm.method_name', 'pc.chan_code', 'pc.chan_name', 'pc.split_ratio', 'pc.chan_cost')
|
|
||||||
->orderByRaw('SUM((o.real_amount - o.fee) * COALESCE(pc.split_ratio, 100) / 100) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'channel_count' => (int)($summaryRow->channel_count ?? 0),
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'order_count' => (int)($summaryRow->order_count ?? 0),
|
|
||||||
'gross_amount' => (string)($summaryRow->gross_amount ?? '0.00'),
|
|
||||||
'net_amount' => (string)($summaryRow->net_amount ?? '0.00'),
|
|
||||||
'merchant_share_amount' => (string)($summaryRow->merchant_share_amount ?? '0.00'),
|
|
||||||
'platform_share_amount' => (string)($summaryRow->platform_share_amount ?? '0.00'),
|
|
||||||
'channel_cost_amount' => (string)($summaryRow->channel_cost_amount ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => array_map(fn ($row) => (array)$row, $paginator->items()),
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invoice(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildFilters($request);
|
|
||||||
$baseQuery = $this->buildOrderQuery($filters)->where('o.status', 1);
|
|
||||||
|
|
||||||
$summaryRow = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT o.merchant_id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT o.merchant_app_id) AS app_count,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS invoiceable_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS pending_invoice_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS pending_notify_orders'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$paginator = (clone $baseQuery)
|
|
||||||
->selectRaw(
|
|
||||||
'o.merchant_id, o.merchant_app_id,
|
|
||||||
m.merchant_no, m.merchant_name, ma.app_id AS merchant_app_code, ma.app_name,
|
|
||||||
COUNT(*) AS success_orders,
|
|
||||||
COALESCE(SUM(o.real_amount - o.fee), 0) AS net_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS invoiceable_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.notify_stat = 0 THEN o.real_amount - o.fee ELSE 0 END), 0) AS pending_invoice_amount,
|
|
||||||
SUM(CASE WHEN o.notify_stat = 0 THEN 1 ELSE 0 END) AS pending_notify_orders,
|
|
||||||
MAX(o.pay_at) AS last_pay_at'
|
|
||||||
)
|
|
||||||
->groupBy('o.merchant_id', 'o.merchant_app_id', 'm.merchant_no', 'm.merchant_name', 'ma.app_id', 'ma.app_name')
|
|
||||||
->orderByRaw('SUM(CASE WHEN o.notify_stat = 1 THEN o.real_amount - o.fee ELSE 0 END) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['invoice_status'] = (float)($item['pending_invoice_amount'] ?? 0) > 0 ? 'pending' : 'ready';
|
|
||||||
$item['invoice_status_text'] = $item['invoice_status'] === 'ready' ? 'ready' : 'pending_review';
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'app_count' => (int)($summaryRow->app_count ?? 0),
|
|
||||||
'success_orders' => (int)($summaryRow->success_orders ?? 0),
|
|
||||||
'invoiceable_amount' => (string)($summaryRow->invoiceable_amount ?? '0.00'),
|
|
||||||
'pending_invoice_amount' => (string)($summaryRow->pending_invoice_amount ?? '0.00'),
|
|
||||||
'pending_notify_orders' => (int)($summaryRow->pending_notify_orders ?? 0),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildFilters(Request $request): array
|
|
||||||
{
|
|
||||||
$methodCode = trim((string)$request->get('method_code', ''));
|
|
||||||
$methodId = 0;
|
|
||||||
if ($methodCode !== '') {
|
|
||||||
$method = $this->methodRepository->findAnyByCode($methodCode);
|
|
||||||
$methodId = $method ? (int)$method->id : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'merchant_id' => (int)$request->get('merchant_id', 0),
|
|
||||||
'merchant_app_id' => (int)$request->get('merchant_app_id', 0),
|
|
||||||
'method_id' => $methodId,
|
|
||||||
'channel_id' => (int)$request->get('channel_id', 0),
|
|
||||||
'status' => (string)$request->get('status', ''),
|
|
||||||
'notify_stat' => (string)$request->get('notify_stat', ''),
|
|
||||||
'order_id' => trim((string)$request->get('order_id', '')),
|
|
||||||
'mch_order_no' => trim((string)$request->get('mch_order_no', '')),
|
|
||||||
'created_from' => trim((string)$request->get('created_from', '')),
|
|
||||||
'created_to' => trim((string)$request->get('created_to', '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildOrderQuery(array $filters)
|
|
||||||
{
|
|
||||||
$query = Db::table('ma_pay_order as o')
|
|
||||||
->leftJoin('ma_merchant as m', 'm.id', '=', 'o.merchant_id')
|
|
||||||
->leftJoin('ma_merchant_app as ma', 'ma.id', '=', 'o.merchant_app_id')
|
|
||||||
->leftJoin('ma_pay_method as pm', 'pm.id', '=', 'o.method_id')
|
|
||||||
->leftJoin('ma_pay_channel as pc', 'pc.id', '=', 'o.channel_id');
|
|
||||||
|
|
||||||
if (!empty($filters['merchant_id'])) {
|
|
||||||
$query->where('o.merchant_id', (int)$filters['merchant_id']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['merchant_app_id'])) {
|
|
||||||
$query->where('o.merchant_app_id', (int)$filters['merchant_app_id']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['method_id'])) {
|
|
||||||
$query->where('o.method_id', (int)$filters['method_id']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['channel_id'])) {
|
|
||||||
$query->where('o.channel_id', (int)$filters['channel_id']);
|
|
||||||
}
|
|
||||||
if (($filters['status'] ?? '') !== '') {
|
|
||||||
$query->where('o.status', (int)$filters['status']);
|
|
||||||
}
|
|
||||||
if (($filters['notify_stat'] ?? '') !== '') {
|
|
||||||
$query->where('o.notify_stat', (int)$filters['notify_stat']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['order_id'])) {
|
|
||||||
$query->where('o.order_id', 'like', '%' . $filters['order_id'] . '%');
|
|
||||||
}
|
|
||||||
if (!empty($filters['mch_order_no'])) {
|
|
||||||
$query->where('o.mch_order_no', 'like', '%' . $filters['mch_order_no'] . '%');
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_from'])) {
|
|
||||||
$query->where('o.created_at', '>=', $filters['created_from']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_to'])) {
|
|
||||||
$query->where('o.created_at', '<=', $filters['created_to']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function reconcileStatus(int $status, int $notifyStat): string
|
|
||||||
{
|
|
||||||
if ($status === 1 && $notifyStat === 1) {
|
|
||||||
return 'matched';
|
|
||||||
}
|
|
||||||
if ($status === 1 && $notifyStat === 0) {
|
|
||||||
return 'notify_pending';
|
|
||||||
}
|
|
||||||
if ($status === 0) {
|
|
||||||
return 'pending';
|
|
||||||
}
|
|
||||||
if ($status === 2) {
|
|
||||||
return 'failed';
|
|
||||||
}
|
|
||||||
if ($status === 3) {
|
|
||||||
return 'closed';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function reconcileStatusText(string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
'matched' => 'matched',
|
|
||||||
'notify_pending' => 'notify_pending',
|
|
||||||
'pending' => 'pending',
|
|
||||||
'failed' => 'failed',
|
|
||||||
'closed' => 'closed',
|
|
||||||
default => 'unknown',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\services\MenuService;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单控制器
|
|
||||||
*/
|
|
||||||
class MenuController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected MenuService $menuService
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRouters()
|
|
||||||
{
|
|
||||||
$routers = $this->menuService->getRouters();
|
|
||||||
return $this->success($routers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\MerchantAppRepository;
|
|
||||||
use app\repositories\MerchantRepository;
|
|
||||||
use app\services\SystemConfigService;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
class MerchantAppController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected MerchantAppRepository $merchantAppRepository,
|
|
||||||
protected MerchantRepository $merchantRepository,
|
|
||||||
protected SystemConfigService $systemConfigService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
|
|
||||||
$filters = [
|
|
||||||
'merchant_id' => (int)$request->get('merchant_id', 0),
|
|
||||||
'status' => $request->get('status', ''),
|
|
||||||
'app_id' => trim((string)$request->get('app_id', '')),
|
|
||||||
'app_name' => trim((string)$request->get('app_name', '')),
|
|
||||||
'api_type' => trim((string)$request->get('api_type', '')),
|
|
||||||
];
|
|
||||||
|
|
||||||
$paginator = $this->merchantAppRepository->searchPaginate($filters, $page, $pageSize);
|
|
||||||
$packageMap = $this->buildPackageMap();
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
|
|
||||||
$packageCode = trim((string)($item['package_code'] ?? ''));
|
|
||||||
$item['package_code'] = $packageCode;
|
|
||||||
$item['package_name'] = $packageCode !== '' ? ($packageMap[$packageCode] ?? $packageCode) : '';
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('app id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->merchantAppRepository->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
return $this->fail('app not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(method_exists($row, 'toArray') ? $row->toArray() : (array)$row);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function configDetail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('app id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = $this->merchantAppRepository->find($id);
|
|
||||||
if (!$app) {
|
|
||||||
return $this->fail('app not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$appRow = method_exists($app, 'toArray') ? $app->toArray() : (array)$app;
|
|
||||||
$config = $this->buildAppConfig($appRow);
|
|
||||||
return $this->success([
|
|
||||||
'app' => $app,
|
|
||||||
'config' => $config,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = (int)($data['id'] ?? 0);
|
|
||||||
|
|
||||||
$merchantId = (int)($data['merchant_id'] ?? 0);
|
|
||||||
$apiType = trim((string)($data['api_type'] ?? 'epay'));
|
|
||||||
$appId = trim((string)($data['app_id'] ?? ''));
|
|
||||||
$appName = trim((string)($data['app_name'] ?? ''));
|
|
||||||
$status = (int)($data['status'] ?? 1);
|
|
||||||
|
|
||||||
if ($merchantId <= 0 || $appId === '' || $appName === '') {
|
|
||||||
return $this->fail('merchant_id, app_id and app_name are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchant = $this->merchantRepository->find($merchantId);
|
|
||||||
if (!$merchant) {
|
|
||||||
return $this->fail('merchant not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($apiType, ['openapi', 'epay', 'custom', 'default'], true)) {
|
|
||||||
return $this->fail('invalid api_type', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($id > 0) {
|
|
||||||
$row = $this->merchantAppRepository->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
return $this->fail('app not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($row->app_id !== $appId) {
|
|
||||||
$exists = $this->merchantAppRepository->findAnyByAppId($appId);
|
|
||||||
if ($exists) {
|
|
||||||
return $this->fail('app_id already exists', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$update = [
|
|
||||||
'mer_id' => $merchantId,
|
|
||||||
'api_type' => $apiType,
|
|
||||||
'app_code' => $appId,
|
|
||||||
'app_name' => $appName,
|
|
||||||
'status' => $status,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!empty($data['app_secret'])) {
|
|
||||||
$update['app_secret'] = (string)$data['app_secret'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->merchantAppRepository->updateById($id, $update);
|
|
||||||
} else {
|
|
||||||
$exists = $this->merchantAppRepository->findAnyByAppId($appId);
|
|
||||||
if ($exists) {
|
|
||||||
return $this->fail('app_id already exists', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$secret = !empty($data['app_secret']) ? (string)$data['app_secret'] : $this->generateSecret();
|
|
||||||
$this->merchantAppRepository->create([
|
|
||||||
'mer_id' => $merchantId,
|
|
||||||
'api_type' => $apiType,
|
|
||||||
'app_code' => $appId,
|
|
||||||
'app_secret' => $secret,
|
|
||||||
'app_name' => $appName,
|
|
||||||
'status' => $status,
|
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resetSecret(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('app id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->merchantAppRepository->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
return $this->fail('app not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$secret = $this->generateSecret();
|
|
||||||
$this->merchantAppRepository->updateById($id, ['app_secret' => $secret]);
|
|
||||||
|
|
||||||
return $this->success(['app_secret' => $secret], 'reset success');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
$status = $request->post('status', null);
|
|
||||||
|
|
||||||
if ($id <= 0 || $status === null) {
|
|
||||||
return $this->fail('invalid params', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = $this->merchantAppRepository->updateById($id, ['status' => (int)$status]);
|
|
||||||
return $ok ? $this->success(null, 'updated') : $this->fail('update failed', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function configSave(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('app id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = $this->merchantAppRepository->find($id);
|
|
||||||
if (!$app) {
|
|
||||||
return $this->fail('app not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$signType = trim((string)$request->post('sign_type', 'md5'));
|
|
||||||
$callbackMode = trim((string)$request->post('callback_mode', 'server'));
|
|
||||||
if (!in_array($signType, ['md5', 'sha256', 'hmac-sha256'], true)) {
|
|
||||||
return $this->fail('invalid sign_type', 400);
|
|
||||||
}
|
|
||||||
if (!in_array($callbackMode, ['server', 'server+page', 'manual'], true)) {
|
|
||||||
return $this->fail('invalid callback_mode', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = [
|
|
||||||
'package_code' => trim((string)$request->post('package_code', '')),
|
|
||||||
'notify_url' => trim((string)$request->post('notify_url', '')),
|
|
||||||
'return_url' => trim((string)$request->post('return_url', '')),
|
|
||||||
'callback_mode' => $callbackMode,
|
|
||||||
'sign_type' => $signType,
|
|
||||||
'order_expire_minutes' => max(0, (int)$request->post('order_expire_minutes', 30)),
|
|
||||||
'callback_retry_limit' => max(0, (int)$request->post('callback_retry_limit', 6)),
|
|
||||||
'ip_whitelist' => trim((string)$request->post('ip_whitelist', '')),
|
|
||||||
'amount_min' => max(0, (float)$request->post('amount_min', 0)),
|
|
||||||
'amount_max' => max(0, (float)$request->post('amount_max', 0)),
|
|
||||||
'daily_limit' => max(0, (float)$request->post('daily_limit', 0)),
|
|
||||||
'notify_enabled' => (int)$request->post('notify_enabled', 1) === 1 ? 1 : 0,
|
|
||||||
'remark' => trim((string)$request->post('remark', '')),
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($config['package_code'] !== '') {
|
|
||||||
$packageExists = false;
|
|
||||||
foreach ($this->getConfigEntries('merchant_packages') as $package) {
|
|
||||||
if (($package['package_code'] ?? '') === $config['package_code']) {
|
|
||||||
$packageExists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$packageExists) {
|
|
||||||
return $this->fail('package_code not found', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$updateData = $config;
|
|
||||||
$updateData['updated_at'] = date('Y-m-d H:i:s');
|
|
||||||
$this->merchantAppRepository->updateById($id, $updateData);
|
|
||||||
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateSecret(): string
|
|
||||||
{
|
|
||||||
$raw = random_bytes(24);
|
|
||||||
return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getConfigEntries(string $configKey): array
|
|
||||||
{
|
|
||||||
$raw = $this->systemConfigService->getValue($configKey, '[]');
|
|
||||||
if (!is_string($raw) || $raw === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
if (!is_array($decoded)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_filter($decoded, 'is_array'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildAppConfig(array $app): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'package_code' => trim((string)($app['package_code'] ?? '')),
|
|
||||||
'notify_url' => trim((string)($app['notify_url'] ?? '')),
|
|
||||||
'return_url' => trim((string)($app['return_url'] ?? '')),
|
|
||||||
'callback_mode' => trim((string)($app['callback_mode'] ?? 'server')) ?: 'server',
|
|
||||||
'sign_type' => trim((string)($app['sign_type'] ?? 'md5')) ?: 'md5',
|
|
||||||
'order_expire_minutes' => (int)($app['order_expire_minutes'] ?? 30),
|
|
||||||
'callback_retry_limit' => (int)($app['callback_retry_limit'] ?? 6),
|
|
||||||
'ip_whitelist' => trim((string)($app['ip_whitelist'] ?? '')),
|
|
||||||
'amount_min' => (string)($app['amount_min'] ?? '0.00'),
|
|
||||||
'amount_max' => (string)($app['amount_max'] ?? '0.00'),
|
|
||||||
'daily_limit' => (string)($app['daily_limit'] ?? '0.00'),
|
|
||||||
'notify_enabled' => (int)($app['notify_enabled'] ?? 1),
|
|
||||||
'remark' => trim((string)($app['remark'] ?? '')),
|
|
||||||
'updated_at' => (string)($app['updated_at'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildPackageMap(): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
foreach ($this->getConfigEntries('merchant_packages') as $package) {
|
|
||||||
$packageCode = trim((string)($package['package_code'] ?? ''));
|
|
||||||
if ($packageCode === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$map[$packageCode] = trim((string)($package['package_name'] ?? $packageCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,884 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\MerchantRepository;
|
|
||||||
use app\services\SystemConfigService;
|
|
||||||
use support\Db;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
class MerchantController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected MerchantRepository $merchantRepository,
|
|
||||||
protected SystemConfigService $systemConfigService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
|
|
||||||
$filters = [
|
|
||||||
'status' => $request->get('status', ''),
|
|
||||||
'merchant_no' => trim((string)$request->get('merchant_no', '')),
|
|
||||||
'merchant_name' => trim((string)$request->get('merchant_name', '')),
|
|
||||||
'email' => trim((string)$request->get('email', '')),
|
|
||||||
'balance' => trim((string)$request->get('balance', '')),
|
|
||||||
];
|
|
||||||
|
|
||||||
$paginator = $this->merchantRepository->searchPaginate($filters, $page, $pageSize);
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
|
|
||||||
$items[] = $this->normalizeMerchantRow($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('merchant id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->merchantRepository->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
return $this->fail('merchant not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchant = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
|
|
||||||
return $this->success($this->normalizeMerchantRow($merchant));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function profileDetail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->fail('merchant id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchant = $this->merchantRepository->find($id);
|
|
||||||
if (!$merchant) {
|
|
||||||
return $this->fail('merchant not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchantRow = method_exists($merchant, 'toArray') ? $merchant->toArray() : (array)$merchant;
|
|
||||||
return $this->success([
|
|
||||||
'merchant' => $this->normalizeMerchantRow($merchantRow),
|
|
||||||
'profile' => $this->buildMerchantProfile($merchantRow),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = (int)($data['id'] ?? 0);
|
|
||||||
|
|
||||||
$merchantNo = trim((string)($data['merchant_no'] ?? ''));
|
|
||||||
$merchantName = trim((string)($data['merchant_name'] ?? ''));
|
|
||||||
$balance = max(0, (float)($data['balance'] ?? 0));
|
|
||||||
$email = trim((string)($data['email'] ?? $data['notify_email'] ?? ''));
|
|
||||||
$status = (int)($data['status'] ?? 1);
|
|
||||||
$remark = trim((string)($data['remark'] ?? ''));
|
|
||||||
|
|
||||||
if ($merchantNo === '' || $merchantName === '') {
|
|
||||||
return $this->fail('merchant_no and merchant_name are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($id > 0) {
|
|
||||||
$this->merchantRepository->updateById($id, [
|
|
||||||
'merchant_no' => $merchantNo,
|
|
||||||
'merchant_name' => $merchantName,
|
|
||||||
'balance' => $balance,
|
|
||||||
'email' => $email,
|
|
||||||
'status' => $status,
|
|
||||||
'remark' => $remark,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$exists = $this->merchantRepository->findByMerchantNo($merchantNo);
|
|
||||||
if ($exists) {
|
|
||||||
return $this->fail('merchant_no already exists', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->merchantRepository->create([
|
|
||||||
'merchant_no' => $merchantNo,
|
|
||||||
'merchant_name' => $merchantName,
|
|
||||||
'balance' => $balance,
|
|
||||||
'email' => $email,
|
|
||||||
'status' => $status,
|
|
||||||
'remark' => $remark,
|
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
$status = $request->post('status', null);
|
|
||||||
|
|
||||||
if ($id <= 0 || $status === null) {
|
|
||||||
return $this->fail('invalid params', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = $this->merchantRepository->updateById($id, ['status' => (int)$status]);
|
|
||||||
return $ok ? $this->success(null, 'updated') : $this->fail('update failed', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function profileSave(Request $request)
|
|
||||||
{
|
|
||||||
$merchantId = (int)$request->post('merchant_id', 0);
|
|
||||||
if ($merchantId <= 0) {
|
|
||||||
return $this->fail('merchant_id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchant = $this->merchantRepository->find($merchantId);
|
|
||||||
if (!$merchant) {
|
|
||||||
return $this->fail('merchant not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchantRow = method_exists($merchant, 'toArray') ? $merchant->toArray() : (array)$merchant;
|
|
||||||
|
|
||||||
$profile = [
|
|
||||||
'email' => trim((string)$request->post('email', $request->post('notify_email', ''))),
|
|
||||||
'remark' => trim((string)$request->post('remark', '')),
|
|
||||||
'balance' => max(0, (float)$request->post('balance', $merchantRow['balance'] ?? 0)),
|
|
||||||
];
|
|
||||||
|
|
||||||
$updateData = $profile;
|
|
||||||
$updateData['updated_at'] = date('Y-m-d H:i:s');
|
|
||||||
$this->merchantRepository->updateById($merchantId, $updateData);
|
|
||||||
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function statistics(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildOpFilters($request);
|
|
||||||
|
|
||||||
$summaryQuery = Db::table('ma_mer as m')
|
|
||||||
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id')
|
|
||||||
->leftJoin('ma_pay_channel as pc', 'pc.mer_id', '=', 'm.id')
|
|
||||||
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
|
|
||||||
$join->on('o.merchant_id', '=', 'm.id');
|
|
||||||
if (!empty($filters['created_from'])) {
|
|
||||||
$join->where('o.created_at', '>=', $filters['created_from']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_to'])) {
|
|
||||||
$join->where('o.created_at', '<=', $filters['created_to']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$this->applyMerchantFilters($summaryQuery, $filters);
|
|
||||||
|
|
||||||
$summaryRow = $summaryQuery
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT m.id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN m.status = 1 THEN m.id END) AS active_merchant_count,
|
|
||||||
COUNT(DISTINCT ma.id) AS app_count,
|
|
||||||
COUNT(DISTINCT pc.id) AS channel_count,
|
|
||||||
COUNT(DISTINCT o.id) AS order_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.status = 1 THEN o.id END) AS success_order_count,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount ELSE 0 END), 0) AS success_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.fee ELSE 0 END), 0) AS fee_amount'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$listQuery = Db::table('ma_mer as m')
|
|
||||||
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id')
|
|
||||||
->leftJoin('ma_pay_channel as pc', 'pc.mer_id', '=', 'm.id')
|
|
||||||
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
|
|
||||||
$join->on('o.merchant_id', '=', 'm.id');
|
|
||||||
if (!empty($filters['created_from'])) {
|
|
||||||
$join->where('o.created_at', '>=', $filters['created_from']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_to'])) {
|
|
||||||
$join->where('o.created_at', '<=', $filters['created_to']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$this->applyMerchantFilters($listQuery, $filters);
|
|
||||||
|
|
||||||
$paginator = $listQuery
|
|
||||||
->selectRaw(
|
|
||||||
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at,
|
|
||||||
COUNT(DISTINCT ma.id) AS app_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN ma.status = 1 THEN ma.id END) AS active_app_count,
|
|
||||||
COUNT(DISTINCT pc.id) AS channel_count,
|
|
||||||
COUNT(DISTINCT o.id) AS order_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.status = 1 THEN o.id END) AS success_order_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.status = 0 THEN o.id END) AS pending_order_count,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount ELSE 0 END), 0) AS success_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.fee ELSE 0 END), 0) AS fee_amount,
|
|
||||||
MAX(o.created_at) AS last_order_at'
|
|
||||||
)
|
|
||||||
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at')
|
|
||||||
->orderByDesc('m.id')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'active_merchant_count' => (int)($summaryRow->active_merchant_count ?? 0),
|
|
||||||
'app_count' => (int)($summaryRow->app_count ?? 0),
|
|
||||||
'channel_count' => (int)($summaryRow->channel_count ?? 0),
|
|
||||||
'order_count' => (int)($summaryRow->order_count ?? 0),
|
|
||||||
'success_order_count' => (int)($summaryRow->success_order_count ?? 0),
|
|
||||||
'success_amount' => (string)($summaryRow->success_amount ?? '0.00'),
|
|
||||||
'fee_amount' => (string)($summaryRow->fee_amount ?? '0.00'),
|
|
||||||
],
|
|
||||||
'list' => array_map(fn ($row) => (array)$row, $paginator->items()),
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function funds(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildOpFilters($request);
|
|
||||||
|
|
||||||
$summaryQuery = Db::table('ma_mer as m')
|
|
||||||
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
|
|
||||||
$join->on('o.merchant_id', '=', 'm.id');
|
|
||||||
if (!empty($filters['created_from'])) {
|
|
||||||
$join->where('o.created_at', '>=', $filters['created_from']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_to'])) {
|
|
||||||
$join->where('o.created_at', '<=', $filters['created_to']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$this->applyMerchantFilters($summaryQuery, $filters);
|
|
||||||
|
|
||||||
$summaryRow = $summaryQuery
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT m.id) AS merchant_count,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount ELSE 0 END), 0) AS settled_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 0 THEN o.amount ELSE 0 END), 0) AS pending_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.fee ELSE 0 END), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS net_amount,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.notify_stat = 0 THEN o.id END) AS notify_pending_orders'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$listQuery = Db::table('ma_mer as m')
|
|
||||||
->leftJoin('ma_pay_order as o', function ($join) use ($filters) {
|
|
||||||
$join->on('o.merchant_id', '=', 'm.id');
|
|
||||||
if (!empty($filters['created_from'])) {
|
|
||||||
$join->where('o.created_at', '>=', $filters['created_from']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['created_to'])) {
|
|
||||||
$join->where('o.created_at', '<=', $filters['created_to']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$this->applyMerchantFilters($listQuery, $filters);
|
|
||||||
|
|
||||||
$paginator = $listQuery
|
|
||||||
->selectRaw(
|
|
||||||
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.status = 1 THEN o.id END) AS success_order_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.status = 0 THEN o.id END) AS pending_order_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN o.notify_stat = 0 THEN o.id END) AS notify_pending_orders,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount ELSE 0 END), 0) AS settled_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 0 THEN o.amount ELSE 0 END), 0) AS pending_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.fee ELSE 0 END), 0) AS fee_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) AS net_amount,
|
|
||||||
MAX(o.pay_at) AS last_pay_at'
|
|
||||||
)
|
|
||||||
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at')
|
|
||||||
->orderByRaw('COALESCE(SUM(CASE WHEN o.status = 1 THEN o.real_amount - o.fee ELSE 0 END), 0) DESC')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'settled_amount' => (string)($summaryRow->settled_amount ?? '0.00'),
|
|
||||||
'pending_amount' => (string)($summaryRow->pending_amount ?? '0.00'),
|
|
||||||
'fee_amount' => (string)($summaryRow->fee_amount ?? '0.00'),
|
|
||||||
'net_amount' => (string)($summaryRow->net_amount ?? '0.00'),
|
|
||||||
'notify_pending_orders' => (int)($summaryRow->notify_pending_orders ?? 0),
|
|
||||||
],
|
|
||||||
'list' => array_map(fn ($row) => (array)$row, $paginator->items()),
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function audit(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$auditStatus = trim((string)$request->get('audit_status', ''));
|
|
||||||
$keyword = trim((string)$request->get('keyword', ''));
|
|
||||||
|
|
||||||
$summaryQuery = Db::table('ma_mer as m');
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$summaryQuery->where(function ($query) use ($keyword) {
|
|
||||||
$query->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
|
||||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if ($auditStatus === 'pending') {
|
|
||||||
$summaryQuery->where('m.status', 0);
|
|
||||||
} elseif ($auditStatus === 'approved') {
|
|
||||||
$summaryQuery->where('m.status', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$summaryRow = $summaryQuery
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(DISTINCT m.id) AS merchant_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN m.status = 0 THEN m.id END) AS pending_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN m.status = 1 THEN m.id END) AS approved_count'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$listQuery = Db::table('ma_mer as m')
|
|
||||||
->leftJoin('ma_pay_app as ma', 'ma.mer_id', '=', 'm.id');
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$listQuery->where(function ($query) use ($keyword) {
|
|
||||||
$query->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
|
||||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if ($auditStatus === 'pending') {
|
|
||||||
$listQuery->where('m.status', 0);
|
|
||||||
} elseif ($auditStatus === 'approved') {
|
|
||||||
$listQuery->where('m.status', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$paginator = $listQuery
|
|
||||||
->selectRaw(
|
|
||||||
'm.id, m.merchant_no, m.merchant_name, m.balance, m.email, m.status, m.remark, m.created_at, m.updated_at,
|
|
||||||
COUNT(DISTINCT ma.id) AS app_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN ma.status = 1 THEN ma.id END) AS active_app_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN ma.status = 0 THEN ma.id END) AS disabled_app_count'
|
|
||||||
)
|
|
||||||
->groupBy('m.id', 'm.merchant_no', 'm.merchant_name', 'm.balance', 'm.email', 'm.status', 'm.remark', 'm.created_at', 'm.updated_at')
|
|
||||||
->orderBy('m.status', 'asc')
|
|
||||||
->orderByDesc('m.id')
|
|
||||||
->paginate($pageSize, ['*'], 'page', $page);
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$item = (array)$row;
|
|
||||||
$item['audit_status'] = (int)($item['status'] ?? 0) === 1 ? 'approved' : 'pending';
|
|
||||||
$item['audit_status_text'] = $item['audit_status'] === 'approved' ? 'approved' : 'pending';
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'summary' => [
|
|
||||||
'merchant_count' => (int)($summaryRow->merchant_count ?? 0),
|
|
||||||
'pending_count' => (int)($summaryRow->pending_count ?? 0),
|
|
||||||
'approved_count' => (int)($summaryRow->approved_count ?? 0),
|
|
||||||
],
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function auditAction(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
$action = trim((string)$request->post('action', ''));
|
|
||||||
|
|
||||||
if ($id <= 0 || !in_array($action, ['approve', 'suspend'], true)) {
|
|
||||||
return $this->fail('invalid params', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = $action === 'approve' ? 1 : 0;
|
|
||||||
Db::connection()->transaction(function () use ($id, $status) {
|
|
||||||
Db::table('ma_mer')->where('id', $id)->update([
|
|
||||||
'status' => $status,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
Db::table('ma_pay_app')->where('mer_id', $id)->update([
|
|
||||||
'status' => $status,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this->success(null, $action === 'approve' ? 'approved' : 'suspended');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupList(Request $request)
|
|
||||||
{
|
|
||||||
$page = max(1, (int)$request->get('page', 1));
|
|
||||||
$pageSize = max(1, (int)$request->get('page_size', 10));
|
|
||||||
$keyword = trim((string)$request->get('keyword', ''));
|
|
||||||
$status = $request->get('status', '');
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizeGroupItem'], $this->getConfigEntries('merchant_groups'));
|
|
||||||
$items = array_values(array_filter($items, function (array $item) use ($keyword, $status) {
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$haystacks = [
|
|
||||||
strtolower((string)($item['group_code'] ?? '')),
|
|
||||||
strtolower((string)($item['group_name'] ?? '')),
|
|
||||||
strtolower((string)($item['remark'] ?? '')),
|
|
||||||
];
|
|
||||||
$needle = strtolower($keyword);
|
|
||||||
$matched = false;
|
|
||||||
foreach ($haystacks as $haystack) {
|
|
||||||
if ($haystack !== '' && str_contains($haystack, $needle)) {
|
|
||||||
$matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$matched) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status !== '' && (int)$item['status'] !== (int)$status) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
|
|
||||||
usort($items, function (array $a, array $b) {
|
|
||||||
$sortCompare = (int)($a['sort'] ?? 0) <=> (int)($b['sort'] ?? 0);
|
|
||||||
if ($sortCompare !== 0) {
|
|
||||||
return $sortCompare;
|
|
||||||
}
|
|
||||||
return strcmp((string)($b['updated_at'] ?? ''), (string)($a['updated_at'] ?? ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this->success($this->buildConfigPagePayload(
|
|
||||||
$items,
|
|
||||||
$page,
|
|
||||||
$pageSize,
|
|
||||||
[
|
|
||||||
'group_count' => count($items),
|
|
||||||
'active_count' => count(array_filter($items, fn (array $item) => (int)$item['status'] === 1)),
|
|
||||||
'disabled_count' => count(array_filter($items, fn (array $item) => (int)$item['status'] !== 1)),
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupSave(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = trim((string)($data['id'] ?? ''));
|
|
||||||
$groupCode = trim((string)($data['group_code'] ?? ''));
|
|
||||||
$groupName = trim((string)($data['group_name'] ?? ''));
|
|
||||||
|
|
||||||
if ($groupCode === '' || $groupName === '') {
|
|
||||||
return $this->fail('group_code and group_name are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizeGroupItem'], $this->getConfigEntries('merchant_groups'));
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if (($item['group_code'] ?? '') === $groupCode && ($item['id'] ?? '') !== $id) {
|
|
||||||
return $this->fail('group_code already exists', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
$saved = false;
|
|
||||||
foreach ($items as &$item) {
|
|
||||||
if (($item['id'] ?? '') !== $id || $id === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$item = array_merge($item, [
|
|
||||||
'group_code' => $groupCode,
|
|
||||||
'group_name' => $groupName,
|
|
||||||
'sort' => (int)($data['sort'] ?? 0),
|
|
||||||
'status' => (int)($data['status'] ?? 1),
|
|
||||||
'remark' => trim((string)($data['remark'] ?? '')),
|
|
||||||
'updated_at' => $now,
|
|
||||||
]);
|
|
||||||
$saved = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unset($item);
|
|
||||||
|
|
||||||
if (!$saved) {
|
|
||||||
$items[] = [
|
|
||||||
'id' => $id !== '' ? $id : uniqid('grp_', true),
|
|
||||||
'group_code' => $groupCode,
|
|
||||||
'group_name' => $groupName,
|
|
||||||
'sort' => (int)($data['sort'] ?? 0),
|
|
||||||
'status' => (int)($data['status'] ?? 1),
|
|
||||||
'remark' => trim((string)($data['remark'] ?? '')),
|
|
||||||
'merchant_count' => 0,
|
|
||||||
'created_at' => $now,
|
|
||||||
'updated_at' => $now,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setConfigEntries('merchant_groups', $items);
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupDelete(Request $request)
|
|
||||||
{
|
|
||||||
$id = trim((string)$request->post('id', ''));
|
|
||||||
if ($id === '') {
|
|
||||||
return $this->fail('id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizeGroupItem'], $this->getConfigEntries('merchant_groups'));
|
|
||||||
$filtered = array_values(array_filter($items, fn (array $item) => ($item['id'] ?? '') !== $id));
|
|
||||||
if (count($filtered) === count($items)) {
|
|
||||||
return $this->fail('group not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setConfigEntries('merchant_groups', $filtered);
|
|
||||||
return $this->success(null, 'deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function packageList(Request $request)
|
|
||||||
{
|
|
||||||
$page = max(1, (int)$request->get('page', 1));
|
|
||||||
$pageSize = max(1, (int)$request->get('page_size', 10));
|
|
||||||
$keyword = trim((string)$request->get('keyword', ''));
|
|
||||||
$status = $request->get('status', '');
|
|
||||||
$apiType = trim((string)$request->get('api_type', ''));
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizePackageItem'], $this->getConfigEntries('merchant_packages'));
|
|
||||||
$items = array_values(array_filter($items, function (array $item) use ($keyword, $status, $apiType) {
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$haystacks = [
|
|
||||||
strtolower((string)($item['package_code'] ?? '')),
|
|
||||||
strtolower((string)($item['package_name'] ?? '')),
|
|
||||||
strtolower((string)($item['fee_desc'] ?? '')),
|
|
||||||
strtolower((string)($item['remark'] ?? '')),
|
|
||||||
];
|
|
||||||
$needle = strtolower($keyword);
|
|
||||||
$matched = false;
|
|
||||||
foreach ($haystacks as $haystack) {
|
|
||||||
if ($haystack !== '' && str_contains($haystack, $needle)) {
|
|
||||||
$matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$matched) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status !== '' && (int)$item['status'] !== (int)$status) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($apiType !== '' && (string)$item['api_type'] !== $apiType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
|
|
||||||
usort($items, function (array $a, array $b) {
|
|
||||||
$sortCompare = (int)($a['sort'] ?? 0) <=> (int)($b['sort'] ?? 0);
|
|
||||||
if ($sortCompare !== 0) {
|
|
||||||
return $sortCompare;
|
|
||||||
}
|
|
||||||
return strcmp((string)($b['updated_at'] ?? ''), (string)($a['updated_at'] ?? ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
$apiTypeCount = [];
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$type = (string)($item['api_type'] ?? 'custom');
|
|
||||||
$apiTypeCount[$type] = ($apiTypeCount[$type] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success($this->buildConfigPagePayload(
|
|
||||||
$items,
|
|
||||||
$page,
|
|
||||||
$pageSize,
|
|
||||||
[
|
|
||||||
'package_count' => count($items),
|
|
||||||
'active_count' => count(array_filter($items, fn (array $item) => (int)$item['status'] === 1)),
|
|
||||||
'disabled_count' => count(array_filter($items, fn (array $item) => (int)$item['status'] !== 1)),
|
|
||||||
'api_type_count' => $apiTypeCount,
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function packageSave(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = trim((string)($data['id'] ?? ''));
|
|
||||||
$packageCode = trim((string)($data['package_code'] ?? ''));
|
|
||||||
$packageName = trim((string)($data['package_name'] ?? ''));
|
|
||||||
$apiType = trim((string)($data['api_type'] ?? 'epay'));
|
|
||||||
|
|
||||||
if ($packageCode === '' || $packageName === '') {
|
|
||||||
return $this->fail('package_code and package_name are required', 400);
|
|
||||||
}
|
|
||||||
if (!in_array($apiType, ['epay', 'openapi', 'custom'], true)) {
|
|
||||||
return $this->fail('invalid api_type', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizePackageItem'], $this->getConfigEntries('merchant_packages'));
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if (($item['package_code'] ?? '') === $packageCode && ($item['id'] ?? '') !== $id) {
|
|
||||||
return $this->fail('package_code already exists', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
$saved = false;
|
|
||||||
foreach ($items as &$item) {
|
|
||||||
if (($item['id'] ?? '') !== $id || $id === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$item = array_merge($item, [
|
|
||||||
'package_code' => $packageCode,
|
|
||||||
'package_name' => $packageName,
|
|
||||||
'api_type' => $apiType,
|
|
||||||
'sort' => (int)($data['sort'] ?? 0),
|
|
||||||
'status' => (int)($data['status'] ?? 1),
|
|
||||||
'channel_limit' => max(0, (int)($data['channel_limit'] ?? 0)),
|
|
||||||
'daily_limit' => trim((string)($data['daily_limit'] ?? '')),
|
|
||||||
'fee_desc' => trim((string)($data['fee_desc'] ?? '')),
|
|
||||||
'callback_policy' => trim((string)($data['callback_policy'] ?? '')),
|
|
||||||
'remark' => trim((string)($data['remark'] ?? '')),
|
|
||||||
'updated_at' => $now,
|
|
||||||
]);
|
|
||||||
$saved = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unset($item);
|
|
||||||
|
|
||||||
if (!$saved) {
|
|
||||||
$items[] = [
|
|
||||||
'id' => $id !== '' ? $id : uniqid('pkg_', true),
|
|
||||||
'package_code' => $packageCode,
|
|
||||||
'package_name' => $packageName,
|
|
||||||
'api_type' => $apiType,
|
|
||||||
'sort' => (int)($data['sort'] ?? 0),
|
|
||||||
'status' => (int)($data['status'] ?? 1),
|
|
||||||
'channel_limit' => max(0, (int)($data['channel_limit'] ?? 0)),
|
|
||||||
'daily_limit' => trim((string)($data['daily_limit'] ?? '')),
|
|
||||||
'fee_desc' => trim((string)($data['fee_desc'] ?? '')),
|
|
||||||
'callback_policy' => trim((string)($data['callback_policy'] ?? '')),
|
|
||||||
'remark' => trim((string)($data['remark'] ?? '')),
|
|
||||||
'created_at' => $now,
|
|
||||||
'updated_at' => $now,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setConfigEntries('merchant_packages', $items);
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function packageDelete(Request $request)
|
|
||||||
{
|
|
||||||
$id = trim((string)$request->post('id', ''));
|
|
||||||
if ($id === '') {
|
|
||||||
return $this->fail('id is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = array_map([$this, 'normalizePackageItem'], $this->getConfigEntries('merchant_packages'));
|
|
||||||
$filtered = array_values(array_filter($items, fn (array $item) => ($item['id'] ?? '') !== $id));
|
|
||||||
if (count($filtered) === count($items)) {
|
|
||||||
return $this->fail('package not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setConfigEntries('merchant_packages', $filtered);
|
|
||||||
return $this->success(null, 'deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildOpFilters(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'merchant_id' => (int)$request->get('merchant_id', 0),
|
|
||||||
'status' => (string)$request->get('status', ''),
|
|
||||||
'keyword' => trim((string)$request->get('keyword', '')),
|
|
||||||
'email' => trim((string)$request->get('email', '')),
|
|
||||||
'balance' => trim((string)$request->get('balance', '')),
|
|
||||||
'created_from' => trim((string)$request->get('created_from', '')),
|
|
||||||
'created_to' => trim((string)$request->get('created_to', '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyMerchantFilters($query, array $filters): void
|
|
||||||
{
|
|
||||||
if (($filters['status'] ?? '') !== '') {
|
|
||||||
$query->where('m.status', (int)$filters['status']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['merchant_id'])) {
|
|
||||||
$query->where('m.id', (int)$filters['merchant_id']);
|
|
||||||
}
|
|
||||||
if (!empty($filters['keyword'])) {
|
|
||||||
$query->where(function ($builder) use ($filters) {
|
|
||||||
$builder->where('m.merchant_no', 'like', '%' . $filters['keyword'] . '%')
|
|
||||||
->orWhere('m.merchant_name', 'like', '%' . $filters['keyword'] . '%')
|
|
||||||
->orWhere('m.email', 'like', '%' . $filters['keyword'] . '%');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!empty($filters['email'])) {
|
|
||||||
$query->where('m.email', 'like', '%' . $filters['email'] . '%');
|
|
||||||
}
|
|
||||||
if (isset($filters['balance']) && $filters['balance'] !== '') {
|
|
||||||
$query->where('m.balance', (string)$filters['balance']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getConfigEntries(string $configKey): array
|
|
||||||
{
|
|
||||||
$raw = $this->systemConfigService->getValue($configKey, '[]');
|
|
||||||
if (!is_string($raw) || $raw === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
if (!is_array($decoded)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_filter($decoded, 'is_array'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function setConfigEntries(string $configKey, array $items): void
|
|
||||||
{
|
|
||||||
$this->systemConfigService->setValue($configKey, array_values($items));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildConfigPagePayload(array $items, int $page, int $pageSize, array $summary): array
|
|
||||||
{
|
|
||||||
$offset = ($page - 1) * $pageSize;
|
|
||||||
return [
|
|
||||||
'summary' => $summary,
|
|
||||||
'list' => array_values(array_slice($items, $offset, $pageSize)),
|
|
||||||
'total' => count($items),
|
|
||||||
'page' => $page,
|
|
||||||
'size' => $pageSize,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeGroupItem(array $item): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => (string)($item['id'] ?? ''),
|
|
||||||
'group_code' => trim((string)($item['group_code'] ?? '')),
|
|
||||||
'group_name' => trim((string)($item['group_name'] ?? '')),
|
|
||||||
'sort' => (int)($item['sort'] ?? 0),
|
|
||||||
'status' => (int)($item['status'] ?? 1),
|
|
||||||
'remark' => trim((string)($item['remark'] ?? '')),
|
|
||||||
'merchant_count' => max(0, (int)($item['merchant_count'] ?? 0)),
|
|
||||||
'created_at' => (string)($item['created_at'] ?? ''),
|
|
||||||
'updated_at' => (string)($item['updated_at'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizePackageItem(array $item): array
|
|
||||||
{
|
|
||||||
$apiType = trim((string)($item['api_type'] ?? 'epay'));
|
|
||||||
if (!in_array($apiType, ['epay', 'openapi', 'custom'], true)) {
|
|
||||||
$apiType = 'custom';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => (string)($item['id'] ?? ''),
|
|
||||||
'package_code' => trim((string)($item['package_code'] ?? '')),
|
|
||||||
'package_name' => trim((string)($item['package_name'] ?? '')),
|
|
||||||
'api_type' => $apiType,
|
|
||||||
'sort' => (int)($item['sort'] ?? 0),
|
|
||||||
'status' => (int)($item['status'] ?? 1),
|
|
||||||
'channel_limit' => max(0, (int)($item['channel_limit'] ?? 0)),
|
|
||||||
'daily_limit' => trim((string)($item['daily_limit'] ?? '')),
|
|
||||||
'fee_desc' => trim((string)($item['fee_desc'] ?? '')),
|
|
||||||
'callback_policy' => trim((string)($item['callback_policy'] ?? '')),
|
|
||||||
'remark' => trim((string)($item['remark'] ?? '')),
|
|
||||||
'created_at' => (string)($item['created_at'] ?? ''),
|
|
||||||
'updated_at' => (string)($item['updated_at'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function merchantProfileKey(int $merchantId): string
|
|
||||||
{
|
|
||||||
return 'merchant_profile_' . $merchantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function defaultMerchantProfile(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'group_code' => '',
|
|
||||||
'contact_name' => '',
|
|
||||||
'contact_phone' => '',
|
|
||||||
'notify_email' => '',
|
|
||||||
'callback_domain' => '',
|
|
||||||
'callback_ip_whitelist' => '',
|
|
||||||
'risk_level' => 'standard',
|
|
||||||
'single_limit' => 0,
|
|
||||||
'daily_limit' => 0,
|
|
||||||
'settlement_cycle' => 't1',
|
|
||||||
'tech_support' => '',
|
|
||||||
'remark' => '',
|
|
||||||
'updated_at' => '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildGroupMap(): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
foreach ($this->getConfigEntries('merchant_groups') as $group) {
|
|
||||||
$groupCode = trim((string)($group['group_code'] ?? ''));
|
|
||||||
if ($groupCode === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$map[$groupCode] = trim((string)($group['group_name'] ?? $groupCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeMerchantRow(array $merchant): array
|
|
||||||
{
|
|
||||||
$merchant['merchant_no'] = trim((string)($merchant['merchant_no'] ?? ''));
|
|
||||||
$merchant['merchant_name'] = trim((string)($merchant['merchant_name'] ?? ''));
|
|
||||||
$merchant['balance'] = (string)($merchant['balance'] ?? '0.00');
|
|
||||||
$merchant['email'] = trim((string)($merchant['email'] ?? ''));
|
|
||||||
$merchant['remark'] = trim((string)($merchant['remark'] ?? ''));
|
|
||||||
$merchant['status'] = (int)($merchant['status'] ?? 1);
|
|
||||||
$merchant['created_at'] = (string)($merchant['created_at'] ?? '');
|
|
||||||
$merchant['updated_at'] = (string)($merchant['updated_at'] ?? '');
|
|
||||||
return $merchant;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildMerchantProfile(array $merchant): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'merchant_no' => trim((string)($merchant['merchant_no'] ?? '')),
|
|
||||||
'merchant_name' => trim((string)($merchant['merchant_name'] ?? '')),
|
|
||||||
'balance' => (string)($merchant['balance'] ?? '0.00'),
|
|
||||||
'email' => trim((string)($merchant['email'] ?? '')),
|
|
||||||
'status' => (int)($merchant['status'] ?? 1),
|
|
||||||
'remark' => trim((string)($merchant['remark'] ?? '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getConfigObject(string $configKey): array
|
|
||||||
{
|
|
||||||
$raw = $this->systemConfigService->getValue($configKey, '{}');
|
|
||||||
if (!is_string($raw) || $raw === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
return is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\models\Merchant;
|
|
||||||
use app\models\MerchantApp;
|
|
||||||
use app\models\PaymentChannel;
|
|
||||||
use app\models\PaymentMethod;
|
|
||||||
use app\repositories\PaymentMethodRepository;
|
|
||||||
use app\repositories\PaymentOrderRepository;
|
|
||||||
use app\services\PayOrderService;
|
|
||||||
use support\Request;
|
|
||||||
use support\Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单管理
|
|
||||||
*/
|
|
||||||
class OrderController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected PaymentOrderRepository $orderRepository,
|
|
||||||
protected PaymentMethodRepository $methodRepository,
|
|
||||||
protected PayOrderService $payOrderService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /adminapi/order/list
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
$filters = $this->buildListFilters($request);
|
|
||||||
|
|
||||||
$paginator = $this->orderRepository->searchPaginate($filters, $page, $pageSize);
|
|
||||||
$items = [];
|
|
||||||
foreach ($paginator->items() as $row) {
|
|
||||||
$items[] = $this->formatOrderRow($row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'list' => $items,
|
|
||||||
'total' => $paginator->total(),
|
|
||||||
'page' => $paginator->currentPage(),
|
|
||||||
'size' => $paginator->perPage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /adminapi/order/detail?id=1 或 order_id=P...
|
|
||||||
*/
|
|
||||||
public function detail(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->get('id', 0);
|
|
||||||
$orderId = trim((string)$request->get('order_id', ''));
|
|
||||||
|
|
||||||
if ($id > 0) {
|
|
||||||
$row = $this->orderRepository->find($id);
|
|
||||||
} elseif ($orderId !== '') {
|
|
||||||
$row = $this->orderRepository->findByOrderId($orderId);
|
|
||||||
} else {
|
|
||||||
return $this->fail('参数错误', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$row) {
|
|
||||||
return $this->fail('订单不存在', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success($this->formatOrderRow($row));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /adminapi/order/export
|
|
||||||
*/
|
|
||||||
public function export(Request $request): Response
|
|
||||||
{
|
|
||||||
$limit = 5000;
|
|
||||||
$filters = $this->buildListFilters($request);
|
|
||||||
$rows = $this->orderRepository->searchList($filters, $limit);
|
|
||||||
|
|
||||||
$merchantIds = [];
|
|
||||||
$merchantAppIds = [];
|
|
||||||
$methodIds = [];
|
|
||||||
$channelIds = [];
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$item = $this->formatOrderRow($row);
|
|
||||||
$items[] = $item;
|
|
||||||
if (!empty($item['merchant_id'])) {
|
|
||||||
$merchantIds[] = (int)$item['merchant_id'];
|
|
||||||
}
|
|
||||||
if (!empty($item['merchant_app_id'])) {
|
|
||||||
$merchantAppIds[] = (int)$item['merchant_app_id'];
|
|
||||||
}
|
|
||||||
if (!empty($item['method_id'])) {
|
|
||||||
$methodIds[] = (int)$item['method_id'];
|
|
||||||
}
|
|
||||||
if (!empty($item['channel_id'])) {
|
|
||||||
$channelIds[] = (int)$item['channel_id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchantMap = Merchant::query()
|
|
||||||
->whereIn('id', array_values(array_unique($merchantIds)))
|
|
||||||
->get(['id', 'merchant_no', 'merchant_name'])
|
|
||||||
->keyBy('id');
|
|
||||||
$merchantAppMap = MerchantApp::query()
|
|
||||||
->whereIn('id', array_values(array_unique($merchantAppIds)))
|
|
||||||
->get(['id', 'app_id', 'app_name'])
|
|
||||||
->keyBy('id');
|
|
||||||
$methodMap = PaymentMethod::query()
|
|
||||||
->whereIn('id', array_values(array_unique($methodIds)))
|
|
||||||
->get(['id', 'method_code', 'method_name'])
|
|
||||||
->keyBy('id');
|
|
||||||
$channelMap = PaymentChannel::query()
|
|
||||||
->whereIn('id', array_values(array_unique($channelIds)))
|
|
||||||
->get(['id', 'chan_code', 'chan_name'])
|
|
||||||
->keyBy('id');
|
|
||||||
|
|
||||||
$stream = fopen('php://temp', 'r+');
|
|
||||||
fwrite($stream, "\xEF\xBB\xBF");
|
|
||||||
fputcsv($stream, [
|
|
||||||
'系统单号',
|
|
||||||
'商户单号',
|
|
||||||
'商户编号',
|
|
||||||
'商户名称',
|
|
||||||
'应用APPID',
|
|
||||||
'应用名称',
|
|
||||||
'支付方式编码',
|
|
||||||
'支付方式名称',
|
|
||||||
'通道编码',
|
|
||||||
'通道名称',
|
|
||||||
'订单金额',
|
|
||||||
'实收金额',
|
|
||||||
'手续费',
|
|
||||||
'币种',
|
|
||||||
'订单状态',
|
|
||||||
'路由结果',
|
|
||||||
'路由模式',
|
|
||||||
'策略名称',
|
|
||||||
'通道单号',
|
|
||||||
'通道交易号',
|
|
||||||
'通知状态',
|
|
||||||
'通知次数',
|
|
||||||
'客户端IP',
|
|
||||||
'商品标题',
|
|
||||||
'创建时间',
|
|
||||||
'支付时间',
|
|
||||||
'路由错误',
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$merchant = $merchantMap->get((int)($item['merchant_id'] ?? 0));
|
|
||||||
$merchantApp = $merchantAppMap->get((int)($item['merchant_app_id'] ?? 0));
|
|
||||||
$method = $methodMap->get((int)($item['method_id'] ?? 0));
|
|
||||||
$channel = $channelMap->get((int)($item['channel_id'] ?? 0));
|
|
||||||
|
|
||||||
fputcsv($stream, [
|
|
||||||
(string)($item['order_id'] ?? ''),
|
|
||||||
(string)($item['mch_order_no'] ?? ''),
|
|
||||||
(string)($merchant->merchant_no ?? ''),
|
|
||||||
(string)($merchant->merchant_name ?? ''),
|
|
||||||
(string)($merchantApp->app_id ?? ''),
|
|
||||||
(string)($merchantApp->app_name ?? ''),
|
|
||||||
(string)($method->method_code ?? ''),
|
|
||||||
(string)($method->method_name ?? ''),
|
|
||||||
(string)($channel->chan_code ?? $item['route_channel_code'] ?? ''),
|
|
||||||
(string)($channel->chan_name ?? $item['route_channel_name'] ?? ''),
|
|
||||||
(string)($item['amount'] ?? '0.00'),
|
|
||||||
(string)($item['real_amount'] ?? '0.00'),
|
|
||||||
(string)($item['fee'] ?? '0.00'),
|
|
||||||
(string)($item['currency'] ?? ''),
|
|
||||||
$this->statusText((int)($item['status'] ?? 0)),
|
|
||||||
(string)($item['route_source_text'] ?? ''),
|
|
||||||
(string)($item['route_mode_text'] ?? ''),
|
|
||||||
(string)($item['route_policy_name'] ?? ''),
|
|
||||||
(string)($item['chan_order_no'] ?? ''),
|
|
||||||
(string)($item['chan_trade_no'] ?? ''),
|
|
||||||
$this->notifyStatusText((int)($item['notify_stat'] ?? 0)),
|
|
||||||
(string)($item['notify_cnt'] ?? '0'),
|
|
||||||
(string)($item['client_ip'] ?? ''),
|
|
||||||
(string)($item['subject'] ?? ''),
|
|
||||||
(string)($item['created_at'] ?? ''),
|
|
||||||
(string)($item['pay_at'] ?? ''),
|
|
||||||
(string)($item['route_error']['message'] ?? ''),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
rewind($stream);
|
|
||||||
$content = stream_get_contents($stream) ?: '';
|
|
||||||
fclose($stream);
|
|
||||||
|
|
||||||
$filename = 'orders-' . date('Ymd-His') . '.csv';
|
|
||||||
return response($content, 200, [
|
|
||||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
||||||
'Content-Disposition' => "attachment; filename*=UTF-8''" . rawurlencode($filename),
|
|
||||||
'X-Export-Count' => (string)count($items),
|
|
||||||
'X-Export-Limit' => (string)$limit,
|
|
||||||
'X-Export-Limited' => count($items) >= $limit ? '1' : '0',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /adminapi/order/refund
|
|
||||||
* - order_id: 系统订单号
|
|
||||||
* - refund_amount: 退款金额
|
|
||||||
*/
|
|
||||||
public function refund(Request $request)
|
|
||||||
{
|
|
||||||
$orderId = trim((string)$request->post('order_id', ''));
|
|
||||||
$refundAmount = (float)$request->post('refund_amount', 0);
|
|
||||||
$refundReason = trim((string)$request->post('refund_reason', ''));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $this->payOrderService->refundOrder([
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'refund_amount' => $refundAmount,
|
|
||||||
'refund_reason' => $refundReason,
|
|
||||||
]);
|
|
||||||
return $this->success($result, '退款发起成功');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return $this->fail($e->getMessage(), 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function formatOrderRow(object $row): array
|
|
||||||
{
|
|
||||||
$data = method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
|
|
||||||
$extra = is_array($data['extra'] ?? null) ? $data['extra'] : [];
|
|
||||||
$routing = is_array($extra['routing'] ?? null) ? $extra['routing'] : null;
|
|
||||||
$routeError = is_array($extra['route_error'] ?? null) ? $extra['route_error'] : null;
|
|
||||||
|
|
||||||
$data['routing'] = $routing;
|
|
||||||
$data['route_error'] = $routeError;
|
|
||||||
$data['route_candidates'] = is_array($routing['candidates'] ?? null) ? $routing['candidates'] : [];
|
|
||||||
$data['route_policy_name'] = (string)($routing['policy']['policy_name'] ?? '');
|
|
||||||
$data['route_source'] = (string)($routing['source'] ?? '');
|
|
||||||
$data['route_source_text'] = $this->routeSourceText($routing, $routeError);
|
|
||||||
$data['route_mode_text'] = $this->routeModeText((string)($routing['route_mode'] ?? ''));
|
|
||||||
$data['route_channel_name'] = (string)($routing['selected_channel_name'] ?? '');
|
|
||||||
$data['route_channel_code'] = (string)($routing['selected_channel_code'] ?? '');
|
|
||||||
$data['route_state'] = $routeError
|
|
||||||
? 'error'
|
|
||||||
: ($routing ? (string)($routing['source'] ?? 'unknown') : 'none');
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildListFilters(Request $request): array
|
|
||||||
{
|
|
||||||
$methodCode = trim((string)$request->get('method_code', ''));
|
|
||||||
$methodId = 0;
|
|
||||||
if ($methodCode !== '') {
|
|
||||||
$method = $this->methodRepository->findAnyByCode($methodCode);
|
|
||||||
$methodId = $method ? (int)$method->id : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'merchant_id' => (int)$request->get('merchant_id', 0),
|
|
||||||
'merchant_app_id' => (int)$request->get('merchant_app_id', 0),
|
|
||||||
'method_id' => $methodId,
|
|
||||||
'channel_id' => (int)$request->get('channel_id', 0),
|
|
||||||
'route_state' => trim((string)$request->get('route_state', '')),
|
|
||||||
'route_policy_name' => trim((string)$request->get('route_policy_name', '')),
|
|
||||||
'status' => $request->get('status', ''),
|
|
||||||
'order_id' => trim((string)$request->get('order_id', '')),
|
|
||||||
'mch_order_no' => trim((string)$request->get('mch_order_no', '')),
|
|
||||||
'created_from' => trim((string)$request->get('created_from', '')),
|
|
||||||
'created_to' => trim((string)$request->get('created_to', '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function statusText(int $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
0 => '待支付',
|
|
||||||
1 => '成功',
|
|
||||||
2 => '失败',
|
|
||||||
3 => '关闭',
|
|
||||||
default => (string)$status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function notifyStatusText(int $notifyStatus): string
|
|
||||||
{
|
|
||||||
return $notifyStatus === 1 ? '已通知' : '待通知';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function routeSourceText(?array $routing, ?array $routeError): string
|
|
||||||
{
|
|
||||||
if ($routeError) {
|
|
||||||
return '路由失败';
|
|
||||||
}
|
|
||||||
|
|
||||||
return match ((string)($routing['source'] ?? '')) {
|
|
||||||
'policy' => '策略命中',
|
|
||||||
'fallback' => '回退选择',
|
|
||||||
default => '未记录',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function routeModeText(string $routeMode): string
|
|
||||||
{
|
|
||||||
return match ($routeMode) {
|
|
||||||
'priority' => '优先级',
|
|
||||||
'weight' => '权重分流',
|
|
||||||
'failover' => '主备切换',
|
|
||||||
'sort' => '排序兜底',
|
|
||||||
default => $routeMode ?: '-',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\PaymentMethodRepository;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付方式管理
|
|
||||||
*/
|
|
||||||
class PayMethodController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected PaymentMethodRepository $methodRepository,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /adminapi/pay-method/list
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
|
|
||||||
$filters = [
|
|
||||||
'status' => $request->get('status', ''),
|
|
||||||
'method_code' => trim((string)$request->get('method_code', '')),
|
|
||||||
'method_name' => trim((string)$request->get('method_name', '')),
|
|
||||||
];
|
|
||||||
|
|
||||||
$paginator = $this->methodRepository->searchPaginate($filters, $page, $pageSize);
|
|
||||||
return $this->page($paginator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /adminapi/pay-method/save
|
|
||||||
*/
|
|
||||||
public function save(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$id = (int)($data['id'] ?? 0);
|
|
||||||
|
|
||||||
$code = trim((string)($data['method_code'] ?? ''));
|
|
||||||
$name = trim((string)($data['method_name'] ?? ''));
|
|
||||||
$icon = trim((string)($data['icon'] ?? ''));
|
|
||||||
$sort = (int)($data['sort'] ?? 0);
|
|
||||||
$status = (int)($data['status'] ?? 1);
|
|
||||||
|
|
||||||
if ($code === '' || $name === '') {
|
|
||||||
return $this->fail('支付方式编码与名称不能为空', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($id > 0) {
|
|
||||||
$this->methodRepository->updateById($id, [
|
|
||||||
'type' => $code,
|
|
||||||
'name' => $name,
|
|
||||||
'icon' => $icon,
|
|
||||||
'sort' => $sort,
|
|
||||||
'status' => $status,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$exists = $this->methodRepository->findAnyByCode($code);
|
|
||||||
if ($exists) {
|
|
||||||
return $this->fail('支付方式编码已存在', 400);
|
|
||||||
}
|
|
||||||
$this->methodRepository->create([
|
|
||||||
'type' => $code,
|
|
||||||
'name' => $name,
|
|
||||||
'icon' => $icon,
|
|
||||||
'sort' => $sort,
|
|
||||||
'status' => $status,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(null, '保存成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /adminapi/pay-method/toggle
|
|
||||||
*/
|
|
||||||
public function toggle(Request $request)
|
|
||||||
{
|
|
||||||
$id = (int)$request->post('id', 0);
|
|
||||||
$status = $request->post('status', null);
|
|
||||||
|
|
||||||
if ($id <= 0 || $status === null) {
|
|
||||||
return $this->fail('参数错误', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = $this->methodRepository->updateById($id, ['status' => (int)$status]);
|
|
||||||
return $ok ? $this->success(null, '操作成功') : $this->fail('操作失败', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\repositories\PaymentPluginRepository;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件注册表管理(ma_pay_plugin)
|
|
||||||
*
|
|
||||||
* 注意:与 /channel/plugin/* 的“插件能力读取(schema/products)”不同,这里负责维护插件注册表本身。
|
|
||||||
*/
|
|
||||||
class PayPluginController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected PaymentPluginRepository $pluginRepository,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /adminapi/pay-plugin/list
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = (int)$request->get('page', 1);
|
|
||||||
$pageSize = (int)$request->get('page_size', 10);
|
|
||||||
|
|
||||||
$filters = [
|
|
||||||
'status' => $request->get('status', ''),
|
|
||||||
'plugin_code' => trim((string)$request->get('plugin_code', '')),
|
|
||||||
'plugin_name' => trim((string)$request->get('plugin_name', '')),
|
|
||||||
];
|
|
||||||
|
|
||||||
$paginator = $this->pluginRepository->searchPaginate($filters, $page, $pageSize);
|
|
||||||
return $this->page($paginator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /adminapi/pay-plugin/save
|
|
||||||
*/
|
|
||||||
public function save(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
|
|
||||||
$pluginCode = trim((string)($data['plugin_code'] ?? ''));
|
|
||||||
$pluginName = trim((string)($data['plugin_name'] ?? ''));
|
|
||||||
$className = trim((string)($data['class_name'] ?? ''));
|
|
||||||
$status = (int)($data['status'] ?? 1);
|
|
||||||
|
|
||||||
if ($pluginCode === '' || $pluginName === '') {
|
|
||||||
return $this->fail('插件编码与名称不能为空', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($className === '') {
|
|
||||||
// 默认约定类名
|
|
||||||
$className = ucfirst($pluginCode) . 'Payment';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pluginRepository->upsertByCode($pluginCode, [
|
|
||||||
'name' => $pluginName,
|
|
||||||
'class_name' => $className,
|
|
||||||
'status' => $status,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->success(null, '保存成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /adminapi/pay-plugin/toggle
|
|
||||||
*/
|
|
||||||
public function toggle(Request $request)
|
|
||||||
{
|
|
||||||
$pluginCode = trim((string)$request->post('plugin_code', ''));
|
|
||||||
$status = $request->post('status', null);
|
|
||||||
|
|
||||||
if ($pluginCode === '' || $status === null) {
|
|
||||||
return $this->fail('参数错误', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = $this->pluginRepository->updateStatus($pluginCode, (int)$status);
|
|
||||||
return $ok ? $this->success(null, '操作成功') : $this->fail('操作失败', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?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,312 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\controller;
|
|
||||||
|
|
||||||
use app\common\base\BaseController;
|
|
||||||
use app\services\SystemConfigService;
|
|
||||||
use app\services\SystemSettingService;
|
|
||||||
use support\Db;
|
|
||||||
use support\Request;
|
|
||||||
|
|
||||||
class SystemController extends BaseController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected SystemSettingService $settingService,
|
|
||||||
protected SystemConfigService $configService
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDict(Request $request, string $code = '')
|
|
||||||
{
|
|
||||||
$data = $this->settingService->getDict($code);
|
|
||||||
return $this->success($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTabsConfig()
|
|
||||||
{
|
|
||||||
return $this->success($this->settingService->getTabs());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFormConfig(Request $request, string $tabKey)
|
|
||||||
{
|
|
||||||
return $this->success($this->settingService->getFormConfig($tabKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function submitConfig(Request $request, string $tabKey)
|
|
||||||
{
|
|
||||||
$formData = $request->post();
|
|
||||||
if (empty($formData)) {
|
|
||||||
return $this->fail('submitted data is empty', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->settingService->saveFormConfig($tabKey, $formData);
|
|
||||||
return $this->success(null, 'saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logFiles()
|
|
||||||
{
|
|
||||||
$logDir = runtime_path('logs');
|
|
||||||
if (!is_dir($logDir)) {
|
|
||||||
return $this->success([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach (glob($logDir . DIRECTORY_SEPARATOR . '*.log') ?: [] as $file) {
|
|
||||||
if (!is_file($file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items[] = [
|
|
||||||
'name' => basename($file),
|
|
||||||
'size' => filesize($file) ?: 0,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s', filemtime($file) ?: time()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($items, fn ($a, $b) => strcmp((string)$b['updated_at'], (string)$a['updated_at']));
|
|
||||||
|
|
||||||
return $this->success($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logSummary()
|
|
||||||
{
|
|
||||||
$logDir = runtime_path('logs');
|
|
||||||
if (!is_dir($logDir)) {
|
|
||||||
return $this->success([
|
|
||||||
'total_files' => 0,
|
|
||||||
'total_size' => 0,
|
|
||||||
'latest_file' => '',
|
|
||||||
'categories' => [],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = glob($logDir . DIRECTORY_SEPARATOR . '*.log') ?: [];
|
|
||||||
$categoryStats = [];
|
|
||||||
$totalSize = 0;
|
|
||||||
$latestFile = '';
|
|
||||||
$latestTime = 0;
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (!is_file($file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$size = filesize($file) ?: 0;
|
|
||||||
$updatedAt = filemtime($file) ?: 0;
|
|
||||||
$name = basename($file);
|
|
||||||
$category = $this->resolveLogCategory($name);
|
|
||||||
|
|
||||||
$totalSize += $size;
|
|
||||||
if (!isset($categoryStats[$category])) {
|
|
||||||
$categoryStats[$category] = [
|
|
||||||
'category' => $category,
|
|
||||||
'file_count' => 0,
|
|
||||||
'total_size' => 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$categoryStats[$category]['file_count']++;
|
|
||||||
$categoryStats[$category]['total_size'] += $size;
|
|
||||||
|
|
||||||
if ($updatedAt >= $latestTime) {
|
|
||||||
$latestTime = $updatedAt;
|
|
||||||
$latestFile = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'total_files' => count($files),
|
|
||||||
'total_size' => $totalSize,
|
|
||||||
'latest_file' => $latestFile,
|
|
||||||
'categories' => array_values($categoryStats),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logContent(Request $request)
|
|
||||||
{
|
|
||||||
$file = basename(trim((string)$request->get('file', '')));
|
|
||||||
$lines = max(20, min(1000, (int)$request->get('lines', 200)));
|
|
||||||
$keyword = trim((string)$request->get('keyword', ''));
|
|
||||||
$level = strtoupper(trim((string)$request->get('level', '')));
|
|
||||||
if ($file === '') {
|
|
||||||
return $this->fail('file is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$logDir = runtime_path('logs');
|
|
||||||
$fullPath = realpath($logDir . DIRECTORY_SEPARATOR . $file);
|
|
||||||
$realLogDir = realpath($logDir);
|
|
||||||
|
|
||||||
if (!$fullPath || !$realLogDir || !str_starts_with($fullPath, $realLogDir) || !is_file($fullPath)) {
|
|
||||||
return $this->fail('log file not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$contentLines = file($fullPath, FILE_IGNORE_NEW_LINES);
|
|
||||||
if (!is_array($contentLines)) {
|
|
||||||
return $this->fail('failed to read log file', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$contentLines = array_values(array_filter($contentLines, static function ($line) use ($keyword) {
|
|
||||||
return stripos($line, $keyword) !== false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($level !== '') {
|
|
||||||
$contentLines = array_values(array_filter($contentLines, static function ($line) use ($level) {
|
|
||||||
return stripos(strtoupper($line), $level) !== false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
$matchedLineCount = count($contentLines);
|
|
||||||
$tail = array_slice($contentLines, -$lines);
|
|
||||||
return $this->success([
|
|
||||||
'file' => $file,
|
|
||||||
'size' => filesize($fullPath) ?: 0,
|
|
||||||
'updated_at' => date('Y-m-d H:i:s', filemtime($fullPath) ?: time()),
|
|
||||||
'line_count' => $matchedLineCount,
|
|
||||||
'keyword' => $keyword,
|
|
||||||
'level' => $level,
|
|
||||||
'lines' => $tail,
|
|
||||||
'content' => implode(PHP_EOL, $tail),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function noticeOverview()
|
|
||||||
{
|
|
||||||
$config = $this->configService->getValues([
|
|
||||||
'smtp_host',
|
|
||||||
'smtp_port',
|
|
||||||
'smtp_ssl',
|
|
||||||
'smtp_username',
|
|
||||||
'smtp_password',
|
|
||||||
'from_email',
|
|
||||||
'from_name',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$taskSummary = Db::table('ma_notify_task')
|
|
||||||
->selectRaw(
|
|
||||||
'COUNT(*) AS total_tasks,
|
|
||||||
SUM(CASE WHEN status = \'PENDING\' THEN 1 ELSE 0 END) AS pending_tasks,
|
|
||||||
SUM(CASE WHEN status = \'SUCCESS\' THEN 1 ELSE 0 END) AS success_tasks,
|
|
||||||
SUM(CASE WHEN status = \'FAIL\' THEN 1 ELSE 0 END) AS fail_tasks,
|
|
||||||
MAX(last_notify_at) AS last_notify_at'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$orderSummary = Db::table('ma_pay_order')
|
|
||||||
->selectRaw(
|
|
||||||
'SUM(CASE WHEN status = 1 AND notify_stat = 0 THEN 1 ELSE 0 END) AS notify_pending_orders,
|
|
||||||
SUM(CASE WHEN status = 1 AND notify_stat = 1 THEN 1 ELSE 0 END) AS notified_orders'
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$requiredKeys = ['smtp_host', 'smtp_port', 'smtp_username', 'smtp_password', 'from_email'];
|
|
||||||
$configuredCount = 0;
|
|
||||||
foreach ($requiredKeys as $key) {
|
|
||||||
if (!empty($config[$key])) {
|
|
||||||
$configuredCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'config' => [
|
|
||||||
'smtp_host' => (string)($config['smtp_host'] ?? ''),
|
|
||||||
'smtp_port' => (string)($config['smtp_port'] ?? ''),
|
|
||||||
'smtp_ssl' => in_array(strtolower((string)($config['smtp_ssl'] ?? '')), ['1', 'true', 'yes', 'on'], true),
|
|
||||||
'smtp_username' => $this->maskString((string)($config['smtp_username'] ?? '')),
|
|
||||||
'from_email' => (string)($config['from_email'] ?? ''),
|
|
||||||
'from_name' => (string)($config['from_name'] ?? ''),
|
|
||||||
'configured_fields' => $configuredCount,
|
|
||||||
'required_fields' => count($requiredKeys),
|
|
||||||
'is_ready' => $configuredCount === count($requiredKeys),
|
|
||||||
],
|
|
||||||
'tasks' => [
|
|
||||||
'total_tasks' => (int)($taskSummary->total_tasks ?? 0),
|
|
||||||
'pending_tasks' => (int)($taskSummary->pending_tasks ?? 0),
|
|
||||||
'success_tasks' => (int)($taskSummary->success_tasks ?? 0),
|
|
||||||
'fail_tasks' => (int)($taskSummary->fail_tasks ?? 0),
|
|
||||||
'last_notify_at' => (string)($taskSummary->last_notify_at ?? ''),
|
|
||||||
],
|
|
||||||
'orders' => [
|
|
||||||
'notify_pending_orders' => (int)($orderSummary->notify_pending_orders ?? 0),
|
|
||||||
'notified_orders' => (int)($orderSummary->notified_orders ?? 0),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function noticeTest(Request $request)
|
|
||||||
{
|
|
||||||
$config = $this->configService->getValues([
|
|
||||||
'smtp_host',
|
|
||||||
'smtp_port',
|
|
||||||
'smtp_ssl',
|
|
||||||
'smtp_username',
|
|
||||||
'smtp_password',
|
|
||||||
'from_email',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$missingFields = [];
|
|
||||||
foreach (['smtp_host', 'smtp_port', 'smtp_username', 'smtp_password', 'from_email'] as $field) {
|
|
||||||
if (empty($config[$field])) {
|
|
||||||
$missingFields[] = $field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($missingFields !== []) {
|
|
||||||
return $this->fail('missing config fields: ' . implode(', ', $missingFields), 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$host = (string)$config['smtp_host'];
|
|
||||||
$port = (int)$config['smtp_port'];
|
|
||||||
$useSsl = in_array(strtolower((string)($config['smtp_ssl'] ?? '')), ['1', 'true', 'yes', 'on'], true);
|
|
||||||
$transport = ($useSsl ? 'ssl://' : 'tcp://') . $host . ':' . $port;
|
|
||||||
|
|
||||||
$errno = 0;
|
|
||||||
$errstr = '';
|
|
||||||
$connection = @stream_socket_client($transport, $errno, $errstr, 5, STREAM_CLIENT_CONNECT);
|
|
||||||
|
|
||||||
if (!is_resource($connection)) {
|
|
||||||
return $this->fail('smtp connection failed: ' . ($errstr !== '' ? $errstr : 'unknown error'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_set_timeout($connection, 3);
|
|
||||||
$banner = fgets($connection, 512) ?: '';
|
|
||||||
fclose($connection);
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'transport' => $transport,
|
|
||||||
'banner' => trim($banner),
|
|
||||||
'checked_at' => date('Y-m-d H:i:s'),
|
|
||||||
'note' => 'only smtp connectivity and basic config were verified; no test email was sent',
|
|
||||||
], 'smtp connection ok');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveLogCategory(string $fileName): string
|
|
||||||
{
|
|
||||||
$name = strtolower($fileName);
|
|
||||||
if (str_contains($name, 'pay') || str_contains($name, 'notify')) {
|
|
||||||
return 'payment';
|
|
||||||
}
|
|
||||||
if (str_contains($name, 'queue') || str_contains($name, 'job')) {
|
|
||||||
return 'queue';
|
|
||||||
}
|
|
||||||
if (str_contains($name, 'error') || str_contains($name, 'exception')) {
|
|
||||||
return 'error';
|
|
||||||
}
|
|
||||||
if (str_contains($name, 'admin') || str_contains($name, 'system')) {
|
|
||||||
return 'system';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'other';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function maskString(string $value): string
|
|
||||||
{
|
|
||||||
$length = strlen($value);
|
|
||||||
if ($length <= 4) {
|
|
||||||
return $value === '' ? '' : str_repeat('*', $length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($value, 0, 2) . str_repeat('*', max(2, $length - 4)) . substr($value, -2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\account;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantAccountValidator;
|
||||||
|
use app\service\account\funds\MerchantAccountService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户账户控制器。
|
||||||
|
*/
|
||||||
|
class MerchantAccountController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入商户账户服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantAccountService $merchantAccountService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户账户列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantAccountValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->merchantAccountService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资金中心概览。
|
||||||
|
*/
|
||||||
|
public function summary(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->merchantAccountService->summary());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户账户详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantAccountValidator::class, 'show');
|
||||||
|
$account = $this->merchantAccountService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$account) {
|
||||||
|
return $this->fail('商户账户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\account;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantAccountLedgerValidator;
|
||||||
|
use app\service\account\ledger\MerchantAccountLedgerService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户账户流水控制器。
|
||||||
|
*/
|
||||||
|
class MerchantAccountLedgerController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入账户流水服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantAccountLedgerService $merchantAccountLedgerService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账户流水列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantAccountLedgerValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->merchantAccountLedgerService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账户流水详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantAccountLedgerValidator::class, 'show');
|
||||||
|
$ledger = $this->merchantAccountLedgerService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$ledger) {
|
||||||
|
return $this->fail('账户流水不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($ledger);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
app/http/admin/controller/file/FileRecordController.php
Normal file
118
app/http/admin/controller/file/FileRecordController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\file;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\FileRecordValidator;
|
||||||
|
use app\service\file\FileRecordService;
|
||||||
|
use Webman\Http\UploadFile;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件控制器。
|
||||||
|
*/
|
||||||
|
class FileRecordController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected FileRecordService $fileRecordService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), FileRecordValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->fileRecordService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->fileRecordService->options());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], FileRecordValidator::class, 'show');
|
||||||
|
|
||||||
|
return $this->success($this->fileRecordService->detail((int) $data['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(array_merge($this->payload($request), ['scene' => $request->input('scene')]), FileRecordValidator::class, 'store');
|
||||||
|
$uploadedFile = $request->file('file');
|
||||||
|
if ($uploadedFile === null) {
|
||||||
|
return $this->fail('请先选择上传文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdBy = $this->currentAdminId($request);
|
||||||
|
$createdByName = (string) $this->requestAttribute($request, 'auth.admin_username', '');
|
||||||
|
|
||||||
|
if (is_array($uploadedFile)) {
|
||||||
|
$items = [];
|
||||||
|
foreach ($uploadedFile as $file) {
|
||||||
|
if ($file instanceof UploadFile) {
|
||||||
|
$items[] = $this->fileRecordService->upload($file, $data, $createdBy, $createdByName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'list' => $items,
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$uploadedFile instanceof UploadFile) {
|
||||||
|
return $this->fail('上传文件无效', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($this->fileRecordService->upload($uploadedFile, $data, $createdBy, $createdByName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importRemote(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($this->payload($request), FileRecordValidator::class, 'importRemote');
|
||||||
|
$createdBy = $this->currentAdminId($request);
|
||||||
|
$createdByName = (string) $this->requestAttribute($request, 'auth.admin_username', '');
|
||||||
|
|
||||||
|
return $this->success(
|
||||||
|
$this->fileRecordService->importRemote(
|
||||||
|
(string) $data['remote_url'],
|
||||||
|
$data,
|
||||||
|
$createdBy,
|
||||||
|
$createdByName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preview(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], FileRecordValidator::class, 'preview');
|
||||||
|
|
||||||
|
return $this->fileRecordService->previewResponse((int) $data['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], FileRecordValidator::class, 'download');
|
||||||
|
|
||||||
|
return $this->fileRecordService->downloadResponse((int) $data['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], FileRecordValidator::class, 'destroy');
|
||||||
|
if (!$this->fileRecordService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('文件不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\merchant;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantApiCredentialValidator;
|
||||||
|
use app\service\merchant\security\MerchantApiCredentialService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户接口凭证管理控制器。
|
||||||
|
*/
|
||||||
|
class MerchantApiCredentialController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入商户 API 凭证服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantApiCredentialService $merchantApiCredentialService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户 API 凭证列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantApiCredentialValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->merchantApiCredentialService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户 API 凭证详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantApiCredentialValidator::class, 'show');
|
||||||
|
$credential = $this->merchantApiCredentialService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$credential) {
|
||||||
|
return $this->fail('商户接口凭证不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增商户 API 凭证。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantApiCredentialValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->merchantApiCredentialService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改商户 API 凭证。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['id' => (int) $id]),
|
||||||
|
MerchantApiCredentialValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
$credential = $this->merchantApiCredentialService->update((int) $data['id'], $data);
|
||||||
|
if (!$credential) {
|
||||||
|
return $this->fail('商户接口凭证不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除商户 API 凭证。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantApiCredentialValidator::class, 'destroy');
|
||||||
|
$credential = $this->merchantApiCredentialService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$credential) {
|
||||||
|
return $this->fail('商户接口凭证不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->merchantApiCredentialService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('商户接口凭证删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
141
app/http/admin/controller/merchant/MerchantController.php
Normal file
141
app/http/admin/controller/merchant/MerchantController.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\merchant;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantValidator;
|
||||||
|
use app\service\merchant\MerchantService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户管理控制器。
|
||||||
|
*
|
||||||
|
* 当前先提供商户列表查询,后续可继续扩展商户详情、新增、编辑等能力。
|
||||||
|
*/
|
||||||
|
class MerchantController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入商户服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantService $merchantService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户列表。
|
||||||
|
*
|
||||||
|
* 返回值里额外携带启用中的商户分组选项,方便前端一次性渲染筛选条件。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->paginateWithGroupOptions($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantValidator::class, 'show');
|
||||||
|
$merchant = $this->merchantService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$merchant) {
|
||||||
|
return $this->fail('商户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($merchant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增商户。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantValidator::class, 'store');
|
||||||
|
return $this->success($this->merchantService->createWithDetail($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新商户。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = array_merge($request->all(), ['id' => (int) $id]);
|
||||||
|
$scene = count(array_diff(array_keys($request->all()), ['status'])) === 0 ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated($payload, MerchantValidator::class, $scene);
|
||||||
|
$merchant = $this->merchantService->updateWithDetail((int) $data['id'], $data);
|
||||||
|
|
||||||
|
if (!$merchant) {
|
||||||
|
return $this->fail('商户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($merchant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除商户。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantValidator::class, 'destroy');
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->delete((int) $data['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置商户登录密码。
|
||||||
|
*/
|
||||||
|
public function resetPassword(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = array_merge($request->all(), ['id' => (int) $id]);
|
||||||
|
$data = $this->validated($payload, MerchantValidator::class, 'resetPassword');
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->resetPassword((int) $data['id'], (string) $data['password']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成或重置商户接口凭证。
|
||||||
|
*/
|
||||||
|
public function issueCredential(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$merchantId = (int) $id;
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->issueCredential($merchantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户总览。
|
||||||
|
*/
|
||||||
|
public function overview(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantValidator::class, 'overview');
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->overview((int) $data['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户下拉选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->merchantService->enabledOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程查询商户选择项。
|
||||||
|
*/
|
||||||
|
public function selectOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
$page = max(1, (int) $request->input('page', 1));
|
||||||
|
$pageSize = min(50, max(1, (int) $request->input('page_size', 20)));
|
||||||
|
|
||||||
|
return $this->success($this->merchantService->searchOptions($request->all(), $page, $pageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
118
app/http/admin/controller/merchant/MerchantGroupController.php
Normal file
118
app/http/admin/controller/merchant/MerchantGroupController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\merchant;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantGroupValidator;
|
||||||
|
use app\service\merchant\group\MerchantGroupService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户分组管理控制器。
|
||||||
|
*
|
||||||
|
* 负责商户分组的列表、详情、新增、修改和删除。
|
||||||
|
*/
|
||||||
|
class MerchantGroupController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入商户分组服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantGroupService $merchantGroupService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/merchant-groups
|
||||||
|
*
|
||||||
|
* 查询商户分组列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantGroupValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->merchantGroupService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/merchant-groups/{id}
|
||||||
|
*
|
||||||
|
* 查询商户分组详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantGroupValidator::class, 'show');
|
||||||
|
$merchantGroup = $this->merchantGroupService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$merchantGroup) {
|
||||||
|
return $this->fail('商户分组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($merchantGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/merchant-groups
|
||||||
|
*
|
||||||
|
* 新增商户分组。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantGroupValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->merchantGroupService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /admin/merchant-groups/{id}
|
||||||
|
*
|
||||||
|
* 修改商户分组。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['id' => (int) $id]),
|
||||||
|
MerchantGroupValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
$merchantGroup = $this->merchantGroupService->update((int) $data['id'], $data);
|
||||||
|
if (!$merchantGroup) {
|
||||||
|
return $this->fail('商户分组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($merchantGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /admin/merchant-groups/{id}
|
||||||
|
*
|
||||||
|
* 删除商户分组。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], MerchantGroupValidator::class, 'destroy');
|
||||||
|
|
||||||
|
if (!$this->merchantGroupService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('商户分组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商户分组下拉选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->merchantGroupService->enabledOptions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\merchant;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\MerchantPolicyValidator;
|
||||||
|
use app\service\merchant\policy\MerchantPolicyService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户策略控制器。
|
||||||
|
*/
|
||||||
|
class MerchantPolicyController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected MerchantPolicyService $merchantPolicyService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantPolicyValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->merchantPolicyService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $merchantId): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['merchant_id' => (int) $merchantId], MerchantPolicyValidator::class, 'show');
|
||||||
|
|
||||||
|
return $this->success($this->merchantPolicyService->findByMerchantId((int) $data['merchant_id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), MerchantPolicyValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->merchantPolicyService->saveByMerchantId((int) $data['merchant_id'], $data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $merchantId): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['merchant_id' => (int) $merchantId]),
|
||||||
|
MerchantPolicyValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->success($this->merchantPolicyService->saveByMerchantId((int) $data['merchant_id'], $data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $merchantId): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['merchant_id' => (int) $merchantId], MerchantPolicyValidator::class, 'show');
|
||||||
|
|
||||||
|
return $this->success($this->merchantPolicyService->deleteByMerchantId((int) $data['merchant_id']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
app/http/admin/controller/ops/ChannelDailyStatController.php
Normal file
54
app/http/admin/controller/ops/ChannelDailyStatController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\ops;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\ChannelDailyStatValidator;
|
||||||
|
use app\service\ops\stat\ChannelDailyStatService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通道日统计控制器。
|
||||||
|
*/
|
||||||
|
class ChannelDailyStatController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入通道日统计服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected ChannelDailyStatService $channelDailyStatService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询通道日统计列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), ChannelDailyStatValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->channelDailyStatService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询通道日统计详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], ChannelDailyStatValidator::class, 'show');
|
||||||
|
$stat = $this->channelDailyStatService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$stat) {
|
||||||
|
return $this->fail('通道日统计不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/http/admin/controller/ops/ChannelNotifyLogController.php
Normal file
54
app/http/admin/controller/ops/ChannelNotifyLogController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\ops;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\ChannelNotifyLogValidator;
|
||||||
|
use app\service\ops\log\ChannelNotifyLogService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道通知日志控制器。
|
||||||
|
*/
|
||||||
|
class ChannelNotifyLogController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入渠道通知日志服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected ChannelNotifyLogService $channelNotifyLogService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询渠道通知日志列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), ChannelNotifyLogValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->channelNotifyLogService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询渠道通知日志详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], ChannelNotifyLogValidator::class, 'show');
|
||||||
|
$log = $this->channelNotifyLogService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$log) {
|
||||||
|
return $this->fail('渠道通知日志不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($log);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/http/admin/controller/ops/PayCallbackLogController.php
Normal file
54
app/http/admin/controller/ops/PayCallbackLogController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\ops;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PayCallbackLogValidator;
|
||||||
|
use app\service\ops\log\PayCallbackLogService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付回调日志控制器。
|
||||||
|
*/
|
||||||
|
class PayCallbackLogController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入支付回调日志服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PayCallbackLogService $payCallbackLogService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付回调日志列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PayCallbackLogValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->payCallbackLogService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付回调日志详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PayCallbackLogValidator::class, 'show');
|
||||||
|
$log = $this->payCallbackLogService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$log) {
|
||||||
|
return $this->fail('支付回调日志不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($log);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
app/http/admin/controller/payment/PaymentChannelController.php
Normal file
138
app/http/admin/controller/payment/PaymentChannelController.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentChannelValidator;
|
||||||
|
use app\service\payment\config\PaymentChannelService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付通道管理控制器。
|
||||||
|
*
|
||||||
|
* 负责支付通道的列表、详情、新增、修改和删除。
|
||||||
|
*/
|
||||||
|
class PaymentChannelController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入支付通道服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentChannelService $paymentChannelService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/payment-channels
|
||||||
|
*
|
||||||
|
* 查询支付通道列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentChannelValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->paymentChannelService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/payment-channels/{id}
|
||||||
|
*
|
||||||
|
* 查询支付通道详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentChannelValidator::class, 'show');
|
||||||
|
$paymentChannel = $this->paymentChannelService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$paymentChannel) {
|
||||||
|
return $this->fail('支付通道不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/payment-channels
|
||||||
|
*
|
||||||
|
* 新增支付通道。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentChannelValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentChannelService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /admin/payment-channels/{id}
|
||||||
|
*
|
||||||
|
* 修改支付通道。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = $request->all();
|
||||||
|
$scene = array_diff(array_keys($payload), ['status']) === [] ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($payload, ['id' => (int) $id]),
|
||||||
|
PaymentChannelValidator::class,
|
||||||
|
$scene
|
||||||
|
);
|
||||||
|
|
||||||
|
$paymentChannel = $this->paymentChannelService->update((int) $data['id'], $data);
|
||||||
|
if (!$paymentChannel) {
|
||||||
|
return $this->fail('支付通道不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /admin/payment-channels/{id}
|
||||||
|
*
|
||||||
|
* 删除支付通道。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentChannelValidator::class, 'destroy');
|
||||||
|
|
||||||
|
if (!$this->paymentChannelService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('支付通道不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询启用中的通道选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->paymentChannelService->enabledOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询路由编排场景下的通道选项。
|
||||||
|
*/
|
||||||
|
public function routeOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->paymentChannelService->routeOptions($request->all()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程查询支付通道选择项。
|
||||||
|
*/
|
||||||
|
public function selectOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
$page = max(1, (int) $request->input('page', 1));
|
||||||
|
$pageSize = min(50, max(1, (int) $request->input('page_size', 20)));
|
||||||
|
|
||||||
|
return $this->success($this->paymentChannelService->searchOptions($request->all(), $page, $pageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentPluginConfValidator;
|
||||||
|
use app\service\payment\config\PaymentPluginConfService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付插件配置控制器。
|
||||||
|
*
|
||||||
|
* 负责插件公共配置的列表、详情、增删改和选项输出。
|
||||||
|
*/
|
||||||
|
class PaymentPluginConfController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentPluginConfService $paymentPluginConfService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginConfValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->page($this->paymentPluginConfService->paginate($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPluginConfValidator::class, 'show');
|
||||||
|
$pluginConf = $this->paymentPluginConfService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$pluginConf) {
|
||||||
|
return $this->fail('插件配置不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($pluginConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginConfValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentPluginConfService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['id' => (int) $id]),
|
||||||
|
PaymentPluginConfValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
$pluginConf = $this->paymentPluginConfService->update((int) $data['id'], $data);
|
||||||
|
if (!$pluginConf) {
|
||||||
|
return $this->fail('插件配置不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($pluginConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPluginConfValidator::class, 'destroy');
|
||||||
|
|
||||||
|
if (!$this->paymentPluginConfService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('插件配置不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginConfValidator::class, 'options');
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'configs' => $this->paymentPluginConfService->options((string) ($data['plugin_code'] ?? '')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程查询插件配置选项。
|
||||||
|
*/
|
||||||
|
public function selectOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginConfValidator::class, 'selectOptions');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = min(50, max(1, (int) ($data['page_size'] ?? 20)));
|
||||||
|
|
||||||
|
return $this->success($this->paymentPluginConfService->searchOptions($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
123
app/http/admin/controller/payment/PaymentPluginController.php
Normal file
123
app/http/admin/controller/payment/PaymentPluginController.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentPluginValidator;
|
||||||
|
use app\service\payment\config\PaymentPluginService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付插件管理控制器。
|
||||||
|
*
|
||||||
|
* 负责插件字典的列表、详情、刷新同步和状态备注维护。
|
||||||
|
*/
|
||||||
|
class PaymentPluginController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入支付插件服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentPluginService $paymentPluginService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付插件列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->page($this->paymentPluginService->paginate($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付插件详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $code): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['code' => $code], PaymentPluginValidator::class, 'show');
|
||||||
|
$paymentPlugin = $this->paymentPluginService->findByCode((string) $data['code']);
|
||||||
|
|
||||||
|
if (!$paymentPlugin) {
|
||||||
|
return $this->fail('支付插件不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改支付插件。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $code): Response
|
||||||
|
{
|
||||||
|
$payload = $request->all();
|
||||||
|
$scene = array_diff(array_keys($payload), ['status']) === [] ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($payload, ['code' => $code]),
|
||||||
|
PaymentPluginValidator::class,
|
||||||
|
$scene
|
||||||
|
);
|
||||||
|
|
||||||
|
$paymentPlugin = $this->paymentPluginService->update((string) $data['code'], $data);
|
||||||
|
if (!$paymentPlugin) {
|
||||||
|
return $this->fail('支付插件不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从插件目录刷新同步支付插件。
|
||||||
|
*/
|
||||||
|
public function refresh(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->paymentPluginService->refreshFromClasses());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付插件下拉选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success([
|
||||||
|
'plugins' => $this->paymentPluginService->enabledOptions(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程查询支付插件选项。
|
||||||
|
*/
|
||||||
|
public function selectOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPluginValidator::class, 'selectOptions');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = min(50, max(1, (int) ($data['page_size'] ?? 20)));
|
||||||
|
|
||||||
|
return $this->success($this->paymentPluginService->searchOptions($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询通道配置场景下的插件选项。
|
||||||
|
*/
|
||||||
|
public function channelOptions(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success([
|
||||||
|
'plugins' => $this->paymentPluginService->channelOptions(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询插件配置结构。
|
||||||
|
*/
|
||||||
|
public function schema(Request $request, string $code): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['code' => $code], PaymentPluginValidator::class, 'show');
|
||||||
|
|
||||||
|
return $this->success($this->paymentPluginService->getSchema((string) $data['code']));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentPollGroupBindValidator;
|
||||||
|
use app\service\payment\config\PaymentPollGroupBindService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户分组路由绑定控制器。
|
||||||
|
*/
|
||||||
|
class PaymentPollGroupBindController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentPollGroupBindService $paymentPollGroupBindService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupBindValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->paymentPollGroupBindService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupBindValidator::class, 'show');
|
||||||
|
$row = $this->paymentPollGroupBindService->findById((int) $data['id']);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->fail('商户分组路由绑定不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupBindValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentPollGroupBindService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['id' => (int) $id]),
|
||||||
|
PaymentPollGroupBindValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = $this->paymentPollGroupBindService->update((int) $data['id'], $data);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->fail('商户分组路由绑定不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupBindValidator::class, 'destroy');
|
||||||
|
if (!$this->paymentPollGroupBindService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('商户分组路由绑定不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentPollGroupChannelValidator;
|
||||||
|
use app\service\payment\config\PaymentPollGroupChannelService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询组通道编排控制器。
|
||||||
|
*/
|
||||||
|
class PaymentPollGroupChannelController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentPollGroupChannelService $paymentPollGroupChannelService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupChannelValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->paymentPollGroupChannelService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupChannelValidator::class, 'show');
|
||||||
|
$row = $this->paymentPollGroupChannelService->findById((int) $data['id']);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->fail('轮询组通道编排不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupChannelValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentPollGroupChannelService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = $request->all();
|
||||||
|
$scene = array_diff(array_keys($payload), ['status']) === [] ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($payload, ['id' => (int) $id]),
|
||||||
|
PaymentPollGroupChannelValidator::class,
|
||||||
|
$scene
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = $this->paymentPollGroupChannelService->update((int) $data['id'], $data);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->fail('轮询组通道编排不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupChannelValidator::class, 'destroy');
|
||||||
|
if (!$this->paymentPollGroupChannelService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('轮询组通道编排不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
app/http/admin/controller/payment/PaymentPollGroupController.php
Normal file
119
app/http/admin/controller/payment/PaymentPollGroupController.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentPollGroupValidator;
|
||||||
|
use app\service\payment\config\PaymentPollGroupService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付轮询组管理控制器。
|
||||||
|
*
|
||||||
|
* 负责轮询组的列表、详情、新增、修改和删除。
|
||||||
|
*/
|
||||||
|
class PaymentPollGroupController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入轮询组服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentPollGroupService $paymentPollGroupService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/payment-poll-groups
|
||||||
|
*
|
||||||
|
* 查询轮询组列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->paymentPollGroupService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/payment-poll-groups/{id}
|
||||||
|
*
|
||||||
|
* 查询轮询组详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupValidator::class, 'show');
|
||||||
|
$paymentPollGroup = $this->paymentPollGroupService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$paymentPollGroup) {
|
||||||
|
return $this->fail('轮询组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentPollGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/payment-poll-groups
|
||||||
|
*
|
||||||
|
* 新增轮询组。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentPollGroupValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentPollGroupService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /admin/payment-poll-groups/{id}
|
||||||
|
*
|
||||||
|
* 修改轮询组。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = $request->all();
|
||||||
|
$scene = array_diff(array_keys($payload), ['status']) === [] ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($payload, ['id' => (int) $id]),
|
||||||
|
PaymentPollGroupValidator::class,
|
||||||
|
$scene
|
||||||
|
);
|
||||||
|
|
||||||
|
$paymentPollGroup = $this->paymentPollGroupService->update((int) $data['id'], $data);
|
||||||
|
if (!$paymentPollGroup) {
|
||||||
|
return $this->fail('轮询组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentPollGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /admin/payment-poll-groups/{id}
|
||||||
|
*
|
||||||
|
* 删除轮询组。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentPollGroupValidator::class, 'destroy');
|
||||||
|
|
||||||
|
if (!$this->paymentPollGroupService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('轮询组不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询轮询组下拉选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->paymentPollGroupService->enabledOptions($request->all()));
|
||||||
|
}
|
||||||
|
}
|
||||||
105
app/http/admin/controller/payment/PaymentTypeController.php
Normal file
105
app/http/admin/controller/payment/PaymentTypeController.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PaymentTypeValidator;
|
||||||
|
use app\service\payment\config\PaymentTypeService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付方式管理控制器。
|
||||||
|
*
|
||||||
|
* 负责支付方式字典的列表、详情、新增、修改、删除和选项输出。
|
||||||
|
*/
|
||||||
|
class PaymentTypeController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入支付方式服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentTypeService $paymentTypeService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付方式列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentTypeValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->page($this->paymentTypeService->paginate($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付方式详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentTypeValidator::class, 'show');
|
||||||
|
$paymentType = $this->paymentTypeService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$paymentType) {
|
||||||
|
return $this->fail('支付方式不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增支付方式。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PaymentTypeValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->paymentTypeService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改支付方式。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$payload = $request->all();
|
||||||
|
$scene = array_diff(array_keys($payload), ['status']) === [] ? 'updateStatus' : 'update';
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($payload, ['id' => (int) $id]),
|
||||||
|
PaymentTypeValidator::class,
|
||||||
|
$scene
|
||||||
|
);
|
||||||
|
|
||||||
|
$paymentType = $this->paymentTypeService->update((int) $data['id'], $data);
|
||||||
|
if (!$paymentType) {
|
||||||
|
return $this->fail('支付方式不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($paymentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付方式。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], PaymentTypeValidator::class, 'destroy');
|
||||||
|
|
||||||
|
if (!$this->paymentTypeService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('支付方式不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付方式下拉选项。
|
||||||
|
*/
|
||||||
|
public function options(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->paymentTypeService->enabledOptions());
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/http/admin/controller/payment/RouteController.php
Normal file
43
app/http/admin/controller/payment/RouteController.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\payment;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\RouteResolveValidator;
|
||||||
|
use app\service\payment\runtime\PaymentRouteService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台路由预览控制器。
|
||||||
|
*
|
||||||
|
* 负责按商户分组、支付方式和金额条件解析可用通道。
|
||||||
|
*/
|
||||||
|
class RouteController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入路由服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PaymentRouteService $paymentRouteService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/routes/resolve
|
||||||
|
*
|
||||||
|
* 解析路由结果。
|
||||||
|
*/
|
||||||
|
public function resolve(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), RouteResolveValidator::class, 'resolve');
|
||||||
|
|
||||||
|
return $this->success($this->paymentRouteService->resolveByMerchantGroup(
|
||||||
|
(int) $data['merchant_group_id'],
|
||||||
|
(int) $data['pay_type_id'],
|
||||||
|
(int) $data['pay_amount'],
|
||||||
|
$data
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
122
app/http/admin/controller/system/AdminUserController.php
Normal file
122
app/http/admin/controller/system/AdminUserController.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\system;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\AdminUserValidator;
|
||||||
|
use app\service\system\user\AdminUserService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员用户管理控制器。
|
||||||
|
*
|
||||||
|
* 负责管理员账号的列表、详情、新增、修改和删除。
|
||||||
|
*/
|
||||||
|
class AdminUserController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入管理员用户服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected AdminUserService $adminUserService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /adminapi/admin-users
|
||||||
|
*
|
||||||
|
* 查询管理员用户列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), AdminUserValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->adminUserService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /adminapi/admin-users/{id}
|
||||||
|
*
|
||||||
|
* 查询管理员用户详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], AdminUserValidator::class, 'show');
|
||||||
|
$adminUser = $this->adminUserService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$adminUser) {
|
||||||
|
return $this->fail('管理员用户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($adminUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /adminapi/admin-users
|
||||||
|
*
|
||||||
|
* 新增管理员用户。
|
||||||
|
*/
|
||||||
|
public function store(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), AdminUserValidator::class, 'store');
|
||||||
|
|
||||||
|
return $this->success($this->adminUserService->create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /adminapi/admin-users/{id}
|
||||||
|
*
|
||||||
|
* 修改管理员用户。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['id' => (int) $id]),
|
||||||
|
AdminUserValidator::class,
|
||||||
|
'update'
|
||||||
|
);
|
||||||
|
|
||||||
|
$adminUser = $this->adminUserService->update((int) $data['id'], $data);
|
||||||
|
if (!$adminUser) {
|
||||||
|
return $this->fail('管理员用户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($adminUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /adminapi/admin-users/{id}
|
||||||
|
*
|
||||||
|
* 删除管理员用户。
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, string $id): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['id' => (int) $id], AdminUserValidator::class, 'destroy');
|
||||||
|
$adminUser = $this->adminUserService->findById((int) $data['id']);
|
||||||
|
|
||||||
|
if (!$adminUser) {
|
||||||
|
return $this->fail('管理员用户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $adminUser->is_super === 1) {
|
||||||
|
return $this->fail('超级管理员不允许删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $data['id'] === $this->currentAdminId($request)) {
|
||||||
|
return $this->fail('不允许删除当前登录用户');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->adminUserService->delete((int) $data['id'])) {
|
||||||
|
return $this->fail('管理员用户删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/http/admin/controller/system/AuthController.php
Normal file
65
app/http/admin/controller/system/AuthController.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\system;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\AuthValidator;
|
||||||
|
use app\service\system\access\AdminAuthService;
|
||||||
|
use app\service\system\user\AdminUserService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员认证控制器。
|
||||||
|
*/
|
||||||
|
class AuthController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AdminAuthService $adminAuthService,
|
||||||
|
protected AdminUserService $adminUserService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), AuthValidator::class, 'login');
|
||||||
|
|
||||||
|
return $this->success($this->adminAuthService->authenticateCredentials(
|
||||||
|
(string) $data['username'],
|
||||||
|
(string) $data['password'],
|
||||||
|
$request->getRealIp(),
|
||||||
|
$request->header('user-agent', '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request): Response
|
||||||
|
{
|
||||||
|
$token = trim((string) ($request->header('authorization', '') ?: $request->header('x-admin-token', '')));
|
||||||
|
$token = preg_replace('/^Bearer\s+/i', '', $token) ?: $token;
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
return $this->fail('未获取到登录令牌', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->adminAuthService->revokeToken($token);
|
||||||
|
|
||||||
|
return $this->success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录管理员的信息
|
||||||
|
*/
|
||||||
|
public function profile(Request $request): Response
|
||||||
|
{
|
||||||
|
$adminId = $this->currentAdminId($request);
|
||||||
|
if ($adminId <= 0) {
|
||||||
|
return $this->fail('未获取到当前管理员信息', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($this->adminUserService->profile(
|
||||||
|
$adminId,
|
||||||
|
(string) $this->requestAttribute($request, 'auth.admin_username', '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\system;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\SystemConfigPageValidator;
|
||||||
|
use app\service\system\config\SystemConfigPageService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
class SystemConfigPageController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected SystemConfigPageService $systemConfigPageService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->systemConfigPageService->tabs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $groupCode): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['group_code' => $groupCode], SystemConfigPageValidator::class, 'show');
|
||||||
|
|
||||||
|
return $this->success($this->systemConfigPageService->detail((string) $data['group_code']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, string $groupCode): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($this->payload($request), ['group_code' => $groupCode]),
|
||||||
|
SystemConfigPageValidator::class,
|
||||||
|
'store'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->success(
|
||||||
|
$this->systemConfigPageService->save((string) $data['group_code'], (array) ($data['values'] ?? []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/http/admin/controller/system/SystemController.php
Normal file
30
app/http/admin/controller/system/SystemController.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\system;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\service\bootstrap\SystemBootstrapService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台系统数据控制器。
|
||||||
|
*/
|
||||||
|
class SystemController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected SystemBootstrapService $systemBootstrapService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function menuTree(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->systemBootstrapService->getMenuTree('admin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dictItems(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->systemBootstrapService->getDictItems((string) $request->get('code', '')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
38
app/http/admin/controller/trade/PayOrderController.php
Normal file
38
app/http/admin/controller/trade/PayOrderController.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\trade;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\PayOrderValidator;
|
||||||
|
use app\service\payment\order\PayOrderService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单管理控制器。
|
||||||
|
*
|
||||||
|
* 当前先提供列表查询,后续可以继续扩展支付单详情、关闭、重试等管理操作。
|
||||||
|
*/
|
||||||
|
class PayOrderController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入支付订单服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected PayOrderService $payOrderService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付订单列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), PayOrderValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->success($this->payOrderService->paginate($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
56
app/http/admin/controller/trade/RefundOrderController.php
Normal file
56
app/http/admin/controller/trade/RefundOrderController.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\trade;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\http\admin\validation\RefundActionValidator;
|
||||||
|
use app\http\admin\validation\RefundOrderValidator;
|
||||||
|
use app\service\payment\order\RefundService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款订单管理控制器。
|
||||||
|
*/
|
||||||
|
class RefundOrderController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected RefundService $refundService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询退款订单列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), RefundOrderValidator::class, 'index');
|
||||||
|
$page = max(1, (int) ($data['page'] ?? 1));
|
||||||
|
$pageSize = max(1, (int) ($data['page_size'] ?? 10));
|
||||||
|
|
||||||
|
return $this->success($this->refundService->paginate($data, $page, $pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询退款订单详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $refundNo): Response
|
||||||
|
{
|
||||||
|
return $this->success($this->refundService->detail($refundNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试退款。
|
||||||
|
*/
|
||||||
|
public function retry(Request $request, string $refundNo): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(
|
||||||
|
array_merge($request->all(), ['refund_no' => $refundNo]),
|
||||||
|
RefundActionValidator::class,
|
||||||
|
'retry'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->success($this->refundService->retryRefund($refundNo, $data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\controller\trade;
|
||||||
|
|
||||||
|
use app\common\base\BaseController;
|
||||||
|
use app\exception\ResourceNotFoundException;
|
||||||
|
use app\http\admin\validation\SettlementOrderValidator;
|
||||||
|
use app\service\payment\settlement\SettlementOrderQueryService;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清算订单控制器。
|
||||||
|
*/
|
||||||
|
class SettlementOrderController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入清算订单服务。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected SettlementOrderQueryService $settlementOrderQueryService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询清算订单列表。
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated($request->all(), SettlementOrderValidator::class, 'index');
|
||||||
|
|
||||||
|
return $this->page(
|
||||||
|
$this->settlementOrderQueryService->paginate(
|
||||||
|
$data,
|
||||||
|
(int) ($data['page'] ?? 1),
|
||||||
|
(int) ($data['page_size'] ?? 10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询清算订单详情。
|
||||||
|
*/
|
||||||
|
public function show(Request $request, string $settleNo): Response
|
||||||
|
{
|
||||||
|
$data = $this->validated(['settle_no' => $settleNo], SettlementOrderValidator::class, 'show');
|
||||||
|
try {
|
||||||
|
return $this->success($this->settlementOrderQueryService->detail((string) $data['settle_no']));
|
||||||
|
} catch (ResourceNotFoundException) {
|
||||||
|
return $this->fail('清算订单不存在', 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/http/admin/middleware/AdminAuthMiddleware.php
Normal file
62
app/http/admin/middleware/AdminAuthMiddleware.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\middleware;
|
||||||
|
|
||||||
|
use app\service\system\access\AdminAuthService;
|
||||||
|
use support\Context;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
use Webman\Http\Response;
|
||||||
|
use Webman\MiddlewareInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员认证中间件。
|
||||||
|
*
|
||||||
|
* 负责读取管理员 token,并把管理员身份写入请求上下文。
|
||||||
|
*/
|
||||||
|
class AdminAuthMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数,注入对应依赖。
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected AdminAuthService $adminAuthService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理请求。
|
||||||
|
*/
|
||||||
|
public function process(Request $request, callable $handler): Response
|
||||||
|
{
|
||||||
|
$token = trim((string) ($request->header('authorization', '') ?: $request->header('x-admin-token', '')));
|
||||||
|
$token = preg_replace('/^Bearer\s+/i', '', $token) ?: $token;
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
if ((int) env('AUTH_MIDDLEWARE_STRICT', 1) === 1) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => 'admin unauthorized',
|
||||||
|
'data' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$admin = $this->adminAuthService->authenticateToken(
|
||||||
|
$token,
|
||||||
|
$request->getRealIp(),
|
||||||
|
$request->header('user-agent', '')
|
||||||
|
);
|
||||||
|
if (!$admin) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => 'admin unauthorized',
|
||||||
|
'data' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::set('auth.admin_id', (int) $admin->id);
|
||||||
|
Context::set('auth.admin_username', (string) $admin->username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\http\admin\middleware;
|
|
||||||
|
|
||||||
use Webman\MiddlewareInterface;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
use Webman\Http\Response;
|
|
||||||
use app\common\utils\JwtUtil;
|
|
||||||
use app\exceptions\UnauthorizedException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT 认证中间件
|
|
||||||
*
|
|
||||||
* 验证请求中的 JWT token,并将用户信息注入到请求对象中
|
|
||||||
*/
|
|
||||||
class AuthMiddleware implements MiddlewareInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 处理请求
|
|
||||||
* @param Request $request 请求对象
|
|
||||||
* @param callable $handler 下一个中间件处理函数
|
|
||||||
* @return Response 响应对象
|
|
||||||
*/
|
|
||||||
public function process(Request $request, callable $handler): Response
|
|
||||||
{
|
|
||||||
// 从请求头中获取 token
|
|
||||||
$auth = $request->header('Authorization', '');
|
|
||||||
if (!$auth) {
|
|
||||||
throw new UnauthorizedException('缺少认证令牌');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容 "Bearer xxx" 或直接 "xxx"
|
|
||||||
if (str_starts_with($auth, 'Bearer ')) {
|
|
||||||
$token = substr($auth, 7);
|
|
||||||
} else {
|
|
||||||
$token = $auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$token) {
|
|
||||||
throw new UnauthorizedException('认证令牌格式错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 解析 JWT token
|
|
||||||
$payload = JwtUtil::parseToken($token);
|
|
||||||
|
|
||||||
if (empty($payload) || !isset($payload['user_id'])) {
|
|
||||||
throw new UnauthorizedException('认证令牌无效');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将用户信息存储到请求对象中,供控制器使用
|
|
||||||
$request->user = $payload;
|
|
||||||
$request->userId = (int) ($payload['user_id'] ?? 0);
|
|
||||||
|
|
||||||
// 继续处理请求
|
|
||||||
return $handler($request);
|
|
||||||
} catch (UnauthorizedException $e) {
|
|
||||||
// 重新抛出业务异常,让框架处理
|
|
||||||
throw $e;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// 根据异常类型返回不同的错误信息
|
|
||||||
$message = $e->getMessage();
|
|
||||||
if (str_contains($message, 'expired') || str_contains($message, 'Expired')) {
|
|
||||||
throw new UnauthorizedException('认证令牌已过期');
|
|
||||||
} elseif (str_contains($message, 'signature') || str_contains($message, 'Signature')) {
|
|
||||||
throw new UnauthorizedException('认证令牌签名无效');
|
|
||||||
} else {
|
|
||||||
throw new UnauthorizedException('认证令牌验证失败:' . $message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
83
app/http/admin/validation/AdminUserValidator.php
Normal file
83
app/http/admin/validation/AdminUserValidator.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员用户参数校验器。
|
||||||
|
*/
|
||||||
|
class AdminUserValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'username' => 'sometimes|string|alpha_dash|min:2|max:32',
|
||||||
|
'password' => 'nullable|string|min:6|max:64',
|
||||||
|
'real_name' => 'sometimes|string|min:2|max:50',
|
||||||
|
'mobile' => 'nullable|string|max:20',
|
||||||
|
'email' => 'nullable|email|max:100',
|
||||||
|
'is_super' => 'sometimes|integer|in:0,1',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'remark' => 'nullable|string|max:500',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '管理员ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'username' => '登录账号',
|
||||||
|
'password' => '登录密码',
|
||||||
|
'real_name' => '真实姓名',
|
||||||
|
'mobile' => '手机号',
|
||||||
|
'email' => '邮箱',
|
||||||
|
'is_super' => '超级管理员',
|
||||||
|
'status' => '状态',
|
||||||
|
'remark' => '备注',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'status', 'is_super', 'page', 'page_size'],
|
||||||
|
'store' => ['username', 'password', 'real_name', 'mobile', 'email', 'is_super', 'status', 'remark'],
|
||||||
|
'update' => ['id', 'username', 'password', 'real_name', 'mobile', 'email', 'is_super', 'status', 'remark'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneStore(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'username' => 'required|string|alpha_dash|min:2|max:32',
|
||||||
|
'password' => 'required|string|min:6|max:64',
|
||||||
|
'real_name' => 'required|string|min:2|max:50',
|
||||||
|
'is_super' => 'required|integer|in:0,1',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdate(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'username' => 'required|string|alpha_dash|min:2|max:32',
|
||||||
|
'real_name' => 'required|string|min:2|max:50',
|
||||||
|
'is_super' => 'required|integer|in:0,1',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneShow(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneDestroy(): static
|
||||||
|
{
|
||||||
|
return $this->sceneShow();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/http/admin/validation/AuthValidator.php
Normal file
27
app/http/admin/validation/AuthValidator.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登录参数校验器。
|
||||||
|
*
|
||||||
|
* 用于校验后台管理员登录入参。
|
||||||
|
*/
|
||||||
|
class AuthValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'username' => 'required|string|min:1|max:32',
|
||||||
|
'password' => 'required|string|min:6|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'username' => '用户名',
|
||||||
|
'password' => '密码',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'login' => ['username', 'password'],
|
||||||
|
];
|
||||||
|
}
|
||||||
36
app/http/admin/validation/ChannelDailyStatValidator.php
Normal file
36
app/http/admin/validation/ChannelDailyStatValidator.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通道日统计参数校验器。
|
||||||
|
*/
|
||||||
|
class ChannelDailyStatValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'channel_id' => 'sometimes|integer|min:1',
|
||||||
|
'stat_date' => 'sometimes|date_format:Y-m-d',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '统计ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'channel_id' => '所属通道',
|
||||||
|
'stat_date' => '统计日期',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'channel_id', 'stat_date', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
40
app/http/admin/validation/ChannelNotifyLogValidator.php
Normal file
40
app/http/admin/validation/ChannelNotifyLogValidator.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道通知日志参数校验器。
|
||||||
|
*/
|
||||||
|
class ChannelNotifyLogValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'channel_id' => 'sometimes|integer|min:1',
|
||||||
|
'notify_type' => 'sometimes|integer|in:0,1',
|
||||||
|
'verify_status' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'process_status' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '日志ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'channel_id' => '所属通道',
|
||||||
|
'notify_type' => '通知类型',
|
||||||
|
'verify_status' => '验签状态',
|
||||||
|
'process_status' => '处理状态',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'channel_id', 'notify_type', 'verify_status', 'process_status', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
45
app/http/admin/validation/FileRecordValidator.php
Normal file
45
app/http/admin/validation/FileRecordValidator.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件参数校验器。
|
||||||
|
*/
|
||||||
|
class FileRecordValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'scene' => 'nullable|integer|in:1,2,3,4',
|
||||||
|
'source_type' => 'nullable|integer|in:1,2',
|
||||||
|
'visibility' => 'nullable|integer|in:1,2',
|
||||||
|
'storage_engine' => 'nullable|integer|in:1,2,3,4',
|
||||||
|
'remote_url' => 'nullable|string|max:2048|url',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '文件ID',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'scene' => '文件场景',
|
||||||
|
'source_type' => '来源类型',
|
||||||
|
'visibility' => '可见性',
|
||||||
|
'storage_engine' => '存储引擎',
|
||||||
|
'remote_url' => '远程地址',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'scene', 'source_type', 'visibility', 'storage_engine', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
'preview' => ['id'],
|
||||||
|
'download' => ['id'],
|
||||||
|
'store' => ['scene', 'visibility'],
|
||||||
|
'importRemote' => ['remote_url', 'scene', 'visibility'],
|
||||||
|
];
|
||||||
|
}
|
||||||
38
app/http/admin/validation/MerchantAccountLedgerValidator.php
Normal file
38
app/http/admin/validation/MerchantAccountLedgerValidator.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户账户流水参数校验器。
|
||||||
|
*/
|
||||||
|
class MerchantAccountLedgerValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'biz_type' => 'sometimes|integer|min:0',
|
||||||
|
'event_type' => 'sometimes|integer|min:0',
|
||||||
|
'direction' => 'sometimes|integer|in:0,1',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '流水ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'biz_type' => '业务类型',
|
||||||
|
'event_type' => '事件类型',
|
||||||
|
'direction' => '方向',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'biz_type', 'event_type', 'direction', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
32
app/http/admin/validation/MerchantAccountValidator.php
Normal file
32
app/http/admin/validation/MerchantAccountValidator.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户账户参数校验器。
|
||||||
|
*/
|
||||||
|
class MerchantAccountValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '账户ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
71
app/http/admin/validation/MerchantApiCredentialValidator.php
Normal file
71
app/http/admin/validation/MerchantApiCredentialValidator.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户接口凭证参数校验器。
|
||||||
|
*/
|
||||||
|
class MerchantApiCredentialValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1|exists:ma_merchant,id',
|
||||||
|
'sign_type' => 'sometimes|integer|in:0',
|
||||||
|
'api_key' => 'nullable|string|max:128',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '凭证ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'sign_type' => '签名类型',
|
||||||
|
'api_key' => '接口凭证值',
|
||||||
|
'status' => '状态',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'status', 'page', 'page_size'],
|
||||||
|
'store' => ['merchant_id', 'sign_type', 'api_key', 'status'],
|
||||||
|
'update' => ['id', 'sign_type', 'api_key', 'status'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneStore(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'merchant_id' => 'required|integer|min:1|exists:ma_merchant,id',
|
||||||
|
'sign_type' => 'required|integer|in:0',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdate(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'sign_type' => 'required|integer|in:0',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneShow(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneDestroy(): static
|
||||||
|
{
|
||||||
|
return $this->sceneShow();
|
||||||
|
}
|
||||||
|
}
|
||||||
70
app/http/admin/validation/MerchantGroupValidator.php
Normal file
70
app/http/admin/validation/MerchantGroupValidator.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户分组参数校验器。
|
||||||
|
*
|
||||||
|
* 用于校验商户分组的查询和增删改参数。
|
||||||
|
*/
|
||||||
|
class MerchantGroupValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'group_name' => 'sometimes|string|min:2|max:128',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'remark' => 'nullable|string|max:255',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '分组ID',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'group_name' => '分组名称',
|
||||||
|
'status' => '状态',
|
||||||
|
'remark' => '备注',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'group_name', 'status', 'page', 'page_size'],
|
||||||
|
'store' => ['group_name', 'status', 'remark'],
|
||||||
|
'update' => ['id', 'group_name', 'status', 'remark'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneStore(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'group_name' => 'required|string|min:2|max:128',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdate(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'group_name' => 'required|string|min:2|max:128',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneShow(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneDestroy(): static
|
||||||
|
{
|
||||||
|
return $this->sceneShow();
|
||||||
|
}
|
||||||
|
}
|
||||||
89
app/http/admin/validation/MerchantPolicyValidator.php
Normal file
89
app/http/admin/validation/MerchantPolicyValidator.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户策略校验器。
|
||||||
|
*/
|
||||||
|
class MerchantPolicyValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1|exists:ma_merchant,id',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'group_id' => 'sometimes|integer|min:1|exists:ma_merchant_group,id',
|
||||||
|
'has_policy' => 'sometimes|integer|in:0,1',
|
||||||
|
'settlement_cycle_override' => 'sometimes|integer|in:0,1,2,3,4',
|
||||||
|
'auto_payout' => 'sometimes|integer|in:0,1',
|
||||||
|
'min_settlement_amount' => 'sometimes|integer|min:0',
|
||||||
|
'retry_policy_json' => 'sometimes|array',
|
||||||
|
'route_policy_json' => 'sometimes|array',
|
||||||
|
'fee_rule_override_json' => 'sometimes|array',
|
||||||
|
'risk_policy_json' => 'sometimes|array',
|
||||||
|
'remark' => 'sometimes|string|max:500',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '策略ID',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'group_id' => '商户分组',
|
||||||
|
'has_policy' => '策略状态',
|
||||||
|
'settlement_cycle_override' => '结算周期覆盖',
|
||||||
|
'auto_payout' => '自动处理',
|
||||||
|
'min_settlement_amount' => '最小结算金额',
|
||||||
|
'retry_policy_json' => '重试策略',
|
||||||
|
'route_policy_json' => '路由策略',
|
||||||
|
'fee_rule_override_json' => '费率覆盖策略',
|
||||||
|
'risk_policy_json' => '风控策略',
|
||||||
|
'remark' => '备注',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'group_id', 'has_policy', 'settlement_cycle_override', 'auto_payout', 'page', 'page_size'],
|
||||||
|
'show' => ['merchant_id'],
|
||||||
|
'store' => [
|
||||||
|
'merchant_id',
|
||||||
|
'settlement_cycle_override',
|
||||||
|
'auto_payout',
|
||||||
|
'min_settlement_amount',
|
||||||
|
'retry_policy_json',
|
||||||
|
'route_policy_json',
|
||||||
|
'fee_rule_override_json',
|
||||||
|
'risk_policy_json',
|
||||||
|
'remark',
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'merchant_id',
|
||||||
|
'settlement_cycle_override',
|
||||||
|
'auto_payout',
|
||||||
|
'min_settlement_amount',
|
||||||
|
'retry_policy_json',
|
||||||
|
'route_policy_json',
|
||||||
|
'fee_rule_override_json',
|
||||||
|
'risk_policy_json',
|
||||||
|
'remark',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneStore(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'merchant_id' => 'required|integer|min:1|exists:ma_merchant,id',
|
||||||
|
'settlement_cycle_override' => 'required|integer|in:0,1,2,3,4',
|
||||||
|
'auto_payout' => 'required|integer|in:0,1',
|
||||||
|
'min_settlement_amount' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdate(): static
|
||||||
|
{
|
||||||
|
return $this->sceneStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
154
app/http/admin/validation/MerchantValidator.php
Normal file
154
app/http/admin/validation/MerchantValidator.php
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户列表参数校验器。
|
||||||
|
*
|
||||||
|
* 目前只用于列表查询,后续如需新增、编辑、详情校验,可以继续补充场景。
|
||||||
|
*/
|
||||||
|
class MerchantValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'group_id' => 'sometimes|integer|min:1',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'merchant_type' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'merchant_no' => 'sometimes|string|max:32',
|
||||||
|
'merchant_name' => 'sometimes|string|max:100',
|
||||||
|
'merchant_short_name' => 'sometimes|string|max:60',
|
||||||
|
'password' => 'sometimes|string|min:6|max:32',
|
||||||
|
'password_confirm' => 'sometimes|string|min:6|max:32|same:password',
|
||||||
|
'risk_level' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'contact_name' => 'sometimes|string|max:50',
|
||||||
|
'contact_phone' => 'sometimes|string|max:20',
|
||||||
|
'contact_email' => 'sometimes|string|max:100',
|
||||||
|
'settlement_account_name' => 'sometimes|string|max:100',
|
||||||
|
'settlement_account_no' => 'sometimes|string|max:100',
|
||||||
|
'settlement_bank_name' => 'sometimes|string|max:100',
|
||||||
|
'settlement_bank_branch' => 'sometimes|string|max:100',
|
||||||
|
'remark' => 'sometimes|string|max:500',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '商户ID',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'group_id' => '商户分组',
|
||||||
|
'status' => '状态',
|
||||||
|
'merchant_type' => '商户类型',
|
||||||
|
'merchant_no' => '商户号',
|
||||||
|
'merchant_name' => '商户名称',
|
||||||
|
'merchant_short_name' => '商户简称',
|
||||||
|
'password' => '登录密码',
|
||||||
|
'password_confirm' => '确认密码',
|
||||||
|
'risk_level' => '风控等级',
|
||||||
|
'contact_name' => '联系人',
|
||||||
|
'contact_phone' => '联系电话',
|
||||||
|
'contact_email' => '联系邮箱',
|
||||||
|
'settlement_account_name' => '结算账户名',
|
||||||
|
'settlement_account_no' => '结算账号',
|
||||||
|
'settlement_bank_name' => '开户行',
|
||||||
|
'settlement_bank_branch' => '开户支行',
|
||||||
|
'remark' => '备注',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'group_id', 'status', 'merchant_type', 'risk_level', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'overview' => ['id'],
|
||||||
|
'store' => [
|
||||||
|
'merchant_name',
|
||||||
|
'merchant_short_name',
|
||||||
|
'merchant_type',
|
||||||
|
'group_id',
|
||||||
|
'risk_level',
|
||||||
|
'contact_name',
|
||||||
|
'contact_phone',
|
||||||
|
'contact_email',
|
||||||
|
'settlement_account_name',
|
||||||
|
'settlement_account_no',
|
||||||
|
'settlement_bank_name',
|
||||||
|
'settlement_bank_branch',
|
||||||
|
'status',
|
||||||
|
'remark',
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'id',
|
||||||
|
'merchant_name',
|
||||||
|
'merchant_short_name',
|
||||||
|
'merchant_type',
|
||||||
|
'group_id',
|
||||||
|
'risk_level',
|
||||||
|
'contact_name',
|
||||||
|
'contact_phone',
|
||||||
|
'contact_email',
|
||||||
|
'settlement_account_name',
|
||||||
|
'settlement_account_no',
|
||||||
|
'settlement_bank_name',
|
||||||
|
'settlement_bank_branch',
|
||||||
|
'status',
|
||||||
|
'remark',
|
||||||
|
],
|
||||||
|
'updateStatus' => ['id', 'status'],
|
||||||
|
'resetPassword' => ['id', 'password', 'password_confirm'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneStore(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'merchant_name' => 'required|string|max:100',
|
||||||
|
'merchant_type' => 'required|integer|in:0,1,2',
|
||||||
|
'group_id' => 'required|integer|min:1|exists:ma_merchant_group,id',
|
||||||
|
'risk_level' => 'required|integer|in:0,1,2',
|
||||||
|
'contact_name' => 'required|string|max:50',
|
||||||
|
'contact_phone' => 'required|string|max:20',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdate(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'merchant_name' => 'required|string|max:100',
|
||||||
|
'merchant_type' => 'required|integer|in:0,1,2',
|
||||||
|
'group_id' => 'required|integer|min:1|exists:ma_merchant_group,id',
|
||||||
|
'risk_level' => 'required|integer|in:0,1,2',
|
||||||
|
'contact_name' => 'required|string|max:50',
|
||||||
|
'contact_phone' => 'required|string|max:20',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneUpdateStatus(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneResetPassword(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'password' => 'required|string|min:6|max:32',
|
||||||
|
'password_confirm' => 'required|string|min:6|max:32|same:password',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneDestroy(): static
|
||||||
|
{
|
||||||
|
return $this->appendRules([
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/http/admin/validation/PayCallbackLogValidator.php
Normal file
40
app/http/admin/validation/PayCallbackLogValidator.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付回调日志参数校验器。
|
||||||
|
*/
|
||||||
|
class PayCallbackLogValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'channel_id' => 'sometimes|integer|min:1',
|
||||||
|
'callback_type' => 'sometimes|integer|in:0,1',
|
||||||
|
'verify_status' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'process_status' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '日志ID',
|
||||||
|
'keyword' => '关键词',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'channel_id' => '所属通道',
|
||||||
|
'callback_type' => '回调类型',
|
||||||
|
'verify_status' => '验签状态',
|
||||||
|
'process_status' => '处理状态',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'channel_id', 'callback_type', 'verify_status', 'process_status', 'page', 'page_size'],
|
||||||
|
'show' => ['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
39
app/http/admin/validation/PayOrderValidator.php
Normal file
39
app/http/admin/validation/PayOrderValidator.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单列表参数校验器。
|
||||||
|
*
|
||||||
|
* 仅供管理后台使用。
|
||||||
|
*/
|
||||||
|
class PayOrderValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:1',
|
||||||
|
'pay_type_id' => 'sometimes|integer|min:1',
|
||||||
|
'status' => 'sometimes|integer|in:0,1,2,3,4,5',
|
||||||
|
'channel_mode' => 'sometimes|integer|in:0,1',
|
||||||
|
'callback_status' => 'sometimes|integer|in:0,1,2',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'merchant_id' => '商户ID',
|
||||||
|
'pay_type_id' => '支付方式',
|
||||||
|
'status' => '状态',
|
||||||
|
'channel_mode' => '通道模式',
|
||||||
|
'callback_status' => '回调状态',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'pay_type_id', 'status', 'channel_mode', 'callback_status', 'page', 'page_size'],
|
||||||
|
];
|
||||||
|
}
|
||||||
103
app/http/admin/validation/PaymentChannelValidator.php
Normal file
103
app/http/admin/validation/PaymentChannelValidator.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付通道参数校验器。
|
||||||
|
*
|
||||||
|
* 用于校验支付通道的查询和增删改参数。
|
||||||
|
*/
|
||||||
|
class PaymentChannelValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'merchant_id' => 'sometimes|integer|min:0',
|
||||||
|
'name' => 'sometimes|string|min:2|max:128',
|
||||||
|
'split_rate_bp' => 'sometimes|integer|min:0|max:10000',
|
||||||
|
'cost_rate_bp' => 'sometimes|integer|min:0|max:10000',
|
||||||
|
'channel_mode' => 'sometimes|integer|in:0,1',
|
||||||
|
'pay_type_id' => 'sometimes|integer|min:1|exists:ma_payment_type,id',
|
||||||
|
'plugin_code' => 'sometimes|string|min:1|max:64|exists:ma_payment_plugin,code',
|
||||||
|
'api_config_id' => 'nullable|integer|min:1',
|
||||||
|
'daily_limit_amount' => 'nullable|integer|min:0',
|
||||||
|
'daily_limit_count' => 'nullable|integer|min:0',
|
||||||
|
'min_amount' => 'nullable|integer|min:0',
|
||||||
|
'max_amount' => 'nullable|integer|min:0',
|
||||||
|
'remark' => 'nullable|string|max:255',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'sort_no' => 'nullable|integer|min:0',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '通道ID',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'merchant_id' => '所属商户',
|
||||||
|
'name' => '通道名称',
|
||||||
|
'split_rate_bp' => '分成比例',
|
||||||
|
'cost_rate_bp' => '通道成本',
|
||||||
|
'channel_mode' => '通道模式',
|
||||||
|
'pay_type_id' => '支付方式',
|
||||||
|
'plugin_code' => '支付插件',
|
||||||
|
'api_config_id' => '配置ID',
|
||||||
|
'daily_limit_amount' => '单日限额',
|
||||||
|
'daily_limit_count' => '单日限笔',
|
||||||
|
'min_amount' => '最小金额',
|
||||||
|
'max_amount' => '最大金额',
|
||||||
|
'remark' => '备注',
|
||||||
|
'status' => '状态',
|
||||||
|
'sort_no' => '排序',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'merchant_id', 'pay_type_id', 'plugin_code', 'channel_mode', 'status', 'page', 'page_size'],
|
||||||
|
'store' => ['merchant_id', 'name', 'split_rate_bp', 'cost_rate_bp', 'channel_mode', 'pay_type_id', 'plugin_code', 'api_config_id', 'daily_limit_amount', 'daily_limit_count', 'min_amount', 'max_amount', 'remark', 'status', 'sort_no'],
|
||||||
|
'update' => ['id', 'merchant_id', 'name', 'split_rate_bp', 'cost_rate_bp', 'channel_mode', 'pay_type_id', 'plugin_code', 'api_config_id', 'daily_limit_amount', 'daily_limit_count', 'min_amount', 'max_amount', 'remark', 'status', 'sort_no'],
|
||||||
|
'updateStatus' => ['id', 'status'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$rules = parent::rules();
|
||||||
|
|
||||||
|
return match ($this->scene()) {
|
||||||
|
'store' => array_merge($rules, [
|
||||||
|
'merchant_id' => 'required|integer|min:0',
|
||||||
|
'name' => 'required|string|min:2|max:128',
|
||||||
|
'split_rate_bp' => 'required|integer|min:0|max:10000',
|
||||||
|
'cost_rate_bp' => 'required|integer|min:0|max:10000',
|
||||||
|
'channel_mode' => 'required|integer|in:0,1',
|
||||||
|
'pay_type_id' => 'required|integer|min:1|exists:ma_payment_type,id',
|
||||||
|
'plugin_code' => 'required|string|min:1|max:64|exists:ma_payment_plugin,code',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]),
|
||||||
|
'update' => array_merge($rules, [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'merchant_id' => 'required|integer|min:0',
|
||||||
|
'name' => 'required|string|min:2|max:128',
|
||||||
|
'split_rate_bp' => 'required|integer|min:0|max:10000',
|
||||||
|
'cost_rate_bp' => 'required|integer|min:0|max:10000',
|
||||||
|
'channel_mode' => 'required|integer|in:0,1',
|
||||||
|
'pay_type_id' => 'required|integer|min:1|exists:ma_payment_type,id',
|
||||||
|
'plugin_code' => 'required|string|min:1|max:64|exists:ma_payment_plugin,code',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]),
|
||||||
|
'updateStatus' => array_merge($rules, [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'status' => 'required|integer|in:0,1',
|
||||||
|
]),
|
||||||
|
'show', 'destroy' => array_merge($rules, [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]),
|
||||||
|
default => $rules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/http/admin/validation/PaymentPluginConfValidator.php
Normal file
68
app/http/admin/validation/PaymentPluginConfValidator.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付插件配置参数校验器。
|
||||||
|
*
|
||||||
|
* 用于校验插件配置列表、详情和增删改参数。
|
||||||
|
*/
|
||||||
|
class PaymentPluginConfValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'id' => 'sometimes|integer|min:1',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'plugin_code' => 'sometimes|string|alpha_dash|min:2|max:32',
|
||||||
|
'config' => 'nullable|array',
|
||||||
|
'settlement_cycle_type' => 'sometimes|integer|in:0,1,2,3,4',
|
||||||
|
'settlement_cutoff_time' => 'nullable|date_format:H:i:s',
|
||||||
|
'remark' => 'nullable|string|max:500',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
'ids' => 'sometimes|array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'id' => '配置ID',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'plugin_code' => '插件编码',
|
||||||
|
'config' => '插件配置',
|
||||||
|
'settlement_cycle_type' => '结算周期',
|
||||||
|
'settlement_cutoff_time' => '结算截止时间',
|
||||||
|
'remark' => '备注',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
'ids' => '配置ID集合',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'plugin_code', 'page', 'page_size'],
|
||||||
|
'store' => ['plugin_code', 'config', 'settlement_cycle_type', 'settlement_cutoff_time', 'remark'],
|
||||||
|
'update' => ['id', 'plugin_code', 'config', 'settlement_cycle_type', 'settlement_cutoff_time', 'remark'],
|
||||||
|
'show' => ['id'],
|
||||||
|
'destroy' => ['id'],
|
||||||
|
'options' => ['plugin_code'],
|
||||||
|
'selectOptions' => ['keyword', 'plugin_code', 'page', 'page_size', 'ids'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$rules = parent::rules();
|
||||||
|
|
||||||
|
return match ($this->scene()) {
|
||||||
|
'store' => array_merge($rules, [
|
||||||
|
'plugin_code' => 'required|string|alpha_dash|min:2|max:32',
|
||||||
|
]),
|
||||||
|
'update' => array_merge($rules, [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'plugin_code' => 'required|string|alpha_dash|min:2|max:32',
|
||||||
|
]),
|
||||||
|
'show', 'destroy' => array_merge($rules, [
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
]),
|
||||||
|
default => $rules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/http/admin/validation/PaymentPluginValidator.php
Normal file
45
app/http/admin/validation/PaymentPluginValidator.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\http\admin\validation;
|
||||||
|
|
||||||
|
use support\validation\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付插件参数校验器。
|
||||||
|
*
|
||||||
|
* 用于校验支付插件的查询、详情和状态备注更新参数。
|
||||||
|
*/
|
||||||
|
class PaymentPluginValidator extends Validator
|
||||||
|
{
|
||||||
|
protected array $rules = [
|
||||||
|
'code' => 'sometimes|string|alpha_dash|min:2|max:32',
|
||||||
|
'status' => 'sometimes|integer|in:0,1',
|
||||||
|
'remark' => 'nullable|string|max:500',
|
||||||
|
'keyword' => 'sometimes|string|max:128',
|
||||||
|
'name' => 'sometimes|string|max:50',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'page_size' => 'sometimes|integer|min:1|max:100',
|
||||||
|
'pay_type_code' => 'sometimes|string|max:32',
|
||||||
|
'ids' => 'sometimes|array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $attributes = [
|
||||||
|
'code' => '插件编码',
|
||||||
|
'name' => '插件名称',
|
||||||
|
'status' => '状态',
|
||||||
|
'remark' => '备注',
|
||||||
|
'keyword' => '关键字',
|
||||||
|
'page' => '页码',
|
||||||
|
'page_size' => '每页条数',
|
||||||
|
'pay_type_code' => '支付方式编码',
|
||||||
|
'ids' => '插件编码集合',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $scenes = [
|
||||||
|
'index' => ['keyword', 'code', 'name', 'status', 'page', 'page_size'],
|
||||||
|
'update' => ['code', 'status', 'remark'],
|
||||||
|
'updateStatus' => ['code', 'status'],
|
||||||
|
'show' => ['code'],
|
||||||
|
'selectOptions' => ['keyword', 'page', 'page_size', 'pay_type_code', 'ids'],
|
||||||
|
];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user