重构初始化

This commit is contained in:
技术老胡
2026-04-15 11:45:46 +08:00
parent 72d72d735b
commit 7612026773
381 changed files with 28287 additions and 14717 deletions

View File

@@ -0,0 +1,119 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\exception\PaymentException;
use app\model\payment\PaymentChannel;
use app\repository\merchant\base\MerchantRepository;
use app\repository\payment\config\PaymentChannelRepository;
use app\repository\payment\config\PaymentPluginRepository;
use app\repository\payment\config\PaymentTypeRepository;
/**
* 支付通道命令服务。
*/
class PaymentChannelCommandService extends BaseService
{
public function __construct(
protected MerchantRepository $merchantRepository,
protected PaymentChannelRepository $paymentChannelRepository,
protected PaymentPluginRepository $paymentPluginRepository,
protected PaymentTypeRepository $paymentTypeRepository
) {
}
public function findById(int $id): ?PaymentChannel
{
return $this->paymentChannelRepository->find($id);
}
public function create(array $data): PaymentChannel
{
$this->assertChannelNameUnique((string) ($data['name'] ?? ''));
$this->assertMerchantExists($data);
$this->assertPluginSupportsPayType($data);
return $this->paymentChannelRepository->create($data);
}
public function update(int $id, array $data): ?PaymentChannel
{
$this->assertChannelNameUnique((string) ($data['name'] ?? ''), $id);
$this->assertMerchantExists($data);
$this->assertPluginSupportsPayType($data);
if (!$this->paymentChannelRepository->updateById($id, $data)) {
return null;
}
return $this->paymentChannelRepository->find($id);
}
public function delete(int $id): bool
{
return $this->paymentChannelRepository->deleteById($id);
}
private function assertMerchantExists(array $data): void
{
if (!array_key_exists('merchant_id', $data)) {
return;
}
$merchantId = (int) $data['merchant_id'];
if ($merchantId === 0) {
return;
}
if (!$this->merchantRepository->find($merchantId)) {
throw new PaymentException('所属商户不存在', 40209, [
'merchant_id' => $merchantId,
]);
}
}
private function assertPluginSupportsPayType(array $data): void
{
$pluginCode = trim((string) ($data['plugin_code'] ?? ''));
$payTypeId = (int) ($data['pay_type_id'] ?? 0);
if ($pluginCode === '' || $payTypeId <= 0) {
return;
}
$plugin = $this->paymentPluginRepository->findByCode($pluginCode);
$paymentType = $this->paymentTypeRepository->find($payTypeId);
if (!$plugin || !$paymentType) {
return;
}
$payTypes = is_array($plugin->pay_types) ? $plugin->pay_types : [];
$payTypeCodes = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $payTypes)));
$payTypeCode = trim((string) $paymentType->code);
if ($payTypeCode === '' || !in_array($payTypeCode, $payTypeCodes, true)) {
throw new PaymentException('支付插件不支持当前支付方式', 40210, [
'plugin_code' => $pluginCode,
'pay_type_code' => $payTypeCode,
]);
}
}
private function assertChannelNameUnique(string $name, int $ignoreId = 0): void
{
$name = trim($name);
if ($name === '') {
return;
}
if ($this->paymentChannelRepository->existsByName($name, $ignoreId)) {
throw new PaymentException('通道名称已存在', 40215, [
'name' => $name,
'ignore_id' => $ignoreId,
]);
}
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\model\payment\PaymentChannel;
use app\repository\payment\config\PaymentChannelRepository;
/**
* 支付通道查询服务。
*/
class PaymentChannelQueryService extends BaseService
{
public function __construct(
protected PaymentChannelRepository $paymentChannelRepository
) {
}
public function enabledOptions(): array
{
return $this->paymentChannelRepository->query()
->from('ma_payment_channel as c')
->where('c.status', CommonConstant::STATUS_ENABLED)
->orderBy('c.sort_no')
->orderByDesc('c.id')
->get([
'c.id',
'c.name',
])
->map(function (PaymentChannel $channel): array {
return [
'label' => sprintf('%s%d', (string) $channel->name, (int) $channel->id),
'value' => (int) $channel->id,
];
})
->values()
->all();
}
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
$query = $this->paymentChannelRepository->query()
->from('ma_payment_channel as c')
->leftJoin('ma_merchant as m', 'c.merchant_id', '=', 'm.id')
->leftJoin('ma_payment_type as t', 'c.pay_type_id', '=', 't.id')
->select([
'c.id',
'c.name',
'c.merchant_id',
'c.channel_mode',
'c.pay_type_id',
'c.plugin_code',
'c.status',
])
->selectRaw("COALESCE(m.merchant_no, '') AS merchant_no")
->selectRaw("COALESCE(m.merchant_name, '') AS merchant_name")
->selectRaw("COALESCE(t.name, '') AS pay_type_name");
$ids = $this->normalizeIds($filters['ids'] ?? []);
if (!empty($ids)) {
$query->whereIn('c.id', $ids);
} else {
$query->where('c.status', CommonConstant::STATUS_ENABLED);
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('c.name', 'like', '%' . $keyword . '%')
->orWhere('c.plugin_code', 'like', '%' . $keyword . '%')
->orWhere('m.merchant_no', 'like', '%' . $keyword . '%')
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%');
});
}
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('c.pay_type_id', $payTypeId);
}
if (array_key_exists('merchant_id', $filters) && $filters['merchant_id'] !== '' && $filters['merchant_id'] !== null) {
$query->where('c.merchant_id', (int) $filters['merchant_id']);
}
if (array_key_exists('channel_mode', $filters) && $filters['channel_mode'] !== '' && $filters['channel_mode'] !== null) {
$query->where('c.channel_mode', (int) $filters['channel_mode']);
}
$excludeIds = $this->normalizeIds($filters['exclude_ids'] ?? []);
if (!empty($excludeIds)) {
$query->whereNotIn('c.id', $excludeIds);
}
}
$paginator = $query
->orderBy('c.sort_no')
->orderByDesc('c.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
return [
'list' => collect($paginator->items())
->map(function ($channel): array {
return [
'label' => sprintf('%s%d', (string) $channel->name, (int) $channel->id),
'value' => (int) $channel->id,
'merchant_id' => (int) $channel->merchant_id,
'merchant_no' => (string) ($channel->merchant_no ?? ''),
'merchant_name' => (string) ($channel->merchant_name ?? ''),
'channel_mode' => (int) $channel->channel_mode,
'pay_type_id' => (int) $channel->pay_type_id,
'pay_type_name' => (string) ($channel->pay_type_name ?? ''),
'plugin_code' => (string) $channel->plugin_code,
];
})
->values()
->all(),
'total' => (int) $paginator->total(),
'page' => (int) $paginator->currentPage(),
'size' => (int) $paginator->perPage(),
];
}
public function routeOptions(array $filters = []): array
{
$query = $this->paymentChannelRepository->query()
->from('ma_payment_channel as c')
->leftJoin('ma_payment_type as t', 'c.pay_type_id', '=', 't.id')
->where('c.status', CommonConstant::STATUS_ENABLED)
->select([
'c.id',
'c.name',
'c.merchant_id',
'c.channel_mode',
'c.pay_type_id',
'c.plugin_code',
])
->selectRaw("COALESCE(t.name, '') AS pay_type_name");
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('c.pay_type_id', $payTypeId);
}
if (array_key_exists('merchant_id', $filters) && $filters['merchant_id'] !== '') {
$query->where('c.merchant_id', (int) $filters['merchant_id']);
}
return $query
->orderBy('c.sort_no')
->orderByDesc('c.id')
->get()
->map(function (PaymentChannel $channel): array {
return [
'label' => sprintf('%s%d', (string) $channel->name, (int) $channel->id),
'value' => (int) $channel->id,
'merchant_id' => (int) $channel->merchant_id,
'channel_mode' => (int) $channel->channel_mode,
'pay_type_id' => (int) $channel->pay_type_id,
'plugin_code' => (string) $channel->plugin_code,
'pay_type_name' => (string) ($channel->pay_type_name ?? ''),
];
})
->values()
->all();
}
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentChannelRepository->query()
->from('ma_payment_channel as c')
->leftJoin('ma_merchant as m', 'c.merchant_id', '=', 'm.id')
->select([
'c.*',
])
->selectRaw("COALESCE(m.merchant_no, '') AS merchant_no")
->selectRaw("COALESCE(m.merchant_name, '') AS merchant_name");
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('c.name', 'like', '%' . $keyword . '%')
->orWhere('c.plugin_code', 'like', '%' . $keyword . '%')
->orWhere('m.merchant_no', 'like', '%' . $keyword . '%')
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%');
});
}
if (array_key_exists('merchant_id', $filters) && $filters['merchant_id'] !== '') {
$query->where('c.merchant_id', (int) $filters['merchant_id']);
}
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('c.pay_type_id', $payTypeId);
}
$pluginCode = trim((string) ($filters['plugin_code'] ?? ''));
if ($pluginCode !== '') {
$query->where('c.plugin_code', 'like', '%' . $pluginCode . '%');
}
if (array_key_exists('channel_mode', $filters) && $filters['channel_mode'] !== '') {
$query->where('c.channel_mode', (int) $filters['channel_mode']);
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('c.status', (int) $filters['status']);
}
return $query
->orderBy('c.sort_no')
->orderByDesc('c.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
public function findById(int $id): ?PaymentChannel
{
return $this->paymentChannelRepository->find($id);
}
private function normalizeIds(array|string|int $ids): array
{
if (is_string($ids)) {
$ids = array_filter(array_map('trim', explode(',', $ids)));
} elseif (!is_array($ids)) {
$ids = [$ids];
}
return array_values(array_filter(array_map(static fn ($id) => (int) $id, $ids), static fn ($id) => $id > 0));
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\model\payment\PaymentChannel;
/**
* 支付通道门面服务。
*/
class PaymentChannelService extends BaseService
{
public function __construct(
protected PaymentChannelQueryService $queryService,
protected PaymentChannelCommandService $commandService
) {
}
public function enabledOptions(): array
{
return $this->queryService->enabledOptions();
}
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
return $this->queryService->searchOptions($filters, $page, $pageSize);
}
public function routeOptions(array $filters = []): array
{
return $this->queryService->routeOptions($filters);
}
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
return $this->queryService->paginate($filters, $page, $pageSize);
}
public function findById(int $id): ?PaymentChannel
{
return $this->queryService->findById($id);
}
public function create(array $data): PaymentChannel
{
return $this->commandService->create($data);
}
public function update(int $id, array $data): ?PaymentChannel
{
return $this->commandService->update($id, $data);
}
public function delete(int $id): bool
{
return $this->commandService->delete($id);
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\PaymentException;
use app\model\payment\PaymentPluginConf;
use app\repository\payment\config\PaymentPluginConfRepository;
use app\repository\payment\config\PaymentPluginRepository;
/**
* 支付插件配置服务。
*
* 负责插件公共配置的增删改查和下拉选项输出。
*/
class PaymentPluginConfService extends BaseService
{
public function __construct(
protected PaymentPluginConfRepository $paymentPluginConfRepository,
protected PaymentPluginRepository $paymentPluginRepository
) {
}
/**
* 分页查询插件配置。
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentPluginConfRepository->query()
->from('ma_payment_plugin_conf as c')
->leftJoin('ma_payment_plugin as p', 'c.plugin_code', '=', 'p.code')
->select([
'c.id',
'c.plugin_code',
'c.config',
'c.settlement_cycle_type',
'c.settlement_cutoff_time',
'c.remark',
'c.created_at',
'c.updated_at',
])
->selectRaw("COALESCE(NULLIF(p.name, ''), c.plugin_code) AS plugin_name");
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('c.plugin_code', 'like', '%' . $keyword . '%')
->orWhere('c.remark', 'like', '%' . $keyword . '%')
->orWhere('p.name', 'like', '%' . $keyword . '%');
});
}
$pluginCode = trim((string) ($filters['plugin_code'] ?? ''));
if ($pluginCode !== '') {
$query->where('c.plugin_code', $pluginCode);
}
return $query
->orderByDesc('c.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 按 ID 查询插件配置。
*/
public function findById(int $id): ?PaymentPluginConf
{
return $this->paymentPluginConfRepository->find($id);
}
/**
* 新增插件配置。
*/
public function create(array $data): PaymentPluginConf
{
$payload = $this->normalizePayload($data);
$this->assertPluginExists((string) $payload['plugin_code']);
return $this->paymentPluginConfRepository->create($payload);
}
/**
* 修改插件配置。
*/
public function update(int $id, array $data): ?PaymentPluginConf
{
$payload = $this->normalizePayload($data);
$this->assertPluginExists((string) $payload['plugin_code']);
if (!$this->paymentPluginConfRepository->updateById($id, $payload)) {
return null;
}
return $this->paymentPluginConfRepository->find($id);
}
/**
* 删除插件配置。
*/
public function delete(int $id): bool
{
return $this->paymentPluginConfRepository->deleteById($id);
}
/**
* 查询插件配置下拉选项。
*/
public function options(?string $pluginCode = null): array
{
$pluginCode = trim((string) $pluginCode);
$query = $this->paymentPluginConfRepository->query()
->from('ma_payment_plugin_conf as c')
->leftJoin('ma_payment_plugin as p', 'c.plugin_code', '=', 'p.code')
->select([
'c.id',
'c.plugin_code',
])
->selectRaw("COALESCE(NULLIF(p.name, ''), c.plugin_code) AS plugin_name")
->orderByDesc('c.id');
if ($pluginCode !== '') {
$query->where('c.plugin_code', $pluginCode);
}
return $query->get()->map(function ($item): array {
$pluginName = trim((string) ($item->plugin_name ?? ''));
$pluginCode = trim((string) ($item->plugin_code ?? ''));
$label = $pluginName !== '' ? $pluginName : $pluginCode;
return [
'label' => sprintf('%s%d', $label, (int) $item->id),
'value' => (int) $item->id,
'plugin_code' => $pluginCode,
'plugin_name' => $pluginName !== '' ? $pluginName : $pluginCode,
];
})->values()->all();
}
/**
* 远程查询插件配置选择项。
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
$query = $this->paymentPluginConfRepository->query()
->from('ma_payment_plugin_conf as c')
->leftJoin('ma_payment_plugin as p', 'c.plugin_code', '=', 'p.code')
->select([
'c.id',
'c.plugin_code',
])
->selectRaw("COALESCE(NULLIF(p.name, ''), c.plugin_code) AS plugin_name")
->orderByDesc('c.id');
$ids = $filters['ids'] ?? [];
if (is_array($ids) && $ids !== []) {
$query->whereIn('c.id', array_map('intval', $ids));
} else {
$pluginCode = trim((string) ($filters['plugin_code'] ?? ''));
if ($pluginCode !== '') {
$query->where('c.plugin_code', $pluginCode);
}
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('c.plugin_code', 'like', '%' . $keyword . '%')
->orWhere('p.name', 'like', '%' . $keyword . '%')
->orWhere('c.remark', 'like', '%' . $keyword . '%');
if (ctype_digit($keyword)) {
$builder->orWhere('c.id', (int) $keyword);
}
});
}
}
$paginator = $query->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
return [
'list' => collect($paginator->items())->map(function ($item): array {
$pluginName = trim((string) ($item->plugin_name ?? ''));
$pluginCode = trim((string) ($item->plugin_code ?? ''));
$label = $pluginName !== '' ? $pluginName : $pluginCode;
return [
'label' => sprintf('%s%d', $label, (int) $item->id),
'value' => (int) $item->id,
'plugin_code' => $pluginCode,
'plugin_name' => $pluginName !== '' ? $pluginName : $pluginCode,
];
})->values()->all(),
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'size' => $paginator->perPage(),
];
}
/**
* 标准化写入数据。
*/
private function normalizePayload(array $data): array
{
return [
'plugin_code' => trim((string) ($data['plugin_code'] ?? '')),
'config' => is_array($data['config'] ?? null) ? $data['config'] : [],
'settlement_cycle_type' => (int) ($data['settlement_cycle_type'] ?? 1),
'settlement_cutoff_time' => trim((string) ($data['settlement_cutoff_time'] ?? '23:59:59')) ?: '23:59:59',
'remark' => trim((string) ($data['remark'] ?? '')),
];
}
/**
* 校验插件是否存在。
*/
private function assertPluginExists(string $pluginCode): void
{
if ($pluginCode === '') {
throw new PaymentException('插件编码不能为空', 40230);
}
if (!$this->paymentPluginRepository->findByCode($pluginCode)) {
throw new PaymentException('支付插件不存在', 40231, [
'plugin_code' => $pluginCode,
]);
}
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\PaymentException;
use app\model\payment\PaymentPlugin;
use app\repository\payment\config\PaymentPluginRepository;
/**
* 支付插件管理服务。
*
* 负责插件目录同步、插件列表查询,以及 JSON 字段写入前的归一化。
*/
class PaymentPluginService extends BaseService
{
/**
* 构造函数,注入支付插件仓库。
*/
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository,
protected PaymentPluginSyncService $paymentPluginSyncService
) {
}
/**
* 分页查询支付插件。
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentPluginRepository->query();
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('code', 'like', '%' . $keyword . '%')
->orWhere('name', 'like', '%' . $keyword . '%')
->orWhere('class_name', 'like', '%' . $keyword . '%');
});
}
$code = trim((string) ($filters['code'] ?? ''));
if ($code !== '') {
$query->where('code', 'like', '%' . $code . '%');
}
$name = trim((string) ($filters['name'] ?? ''));
if ($name !== '') {
$query->where('name', 'like', '%' . $name . '%');
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('status', (int) $filters['status']);
}
return $query
->orderBy('code')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 查询启用中的支付插件选项。
*/
public function enabledOptions(): array
{
return $this->paymentPluginRepository->enabledList(['code', 'name'])
->map(function (PaymentPlugin $plugin): array {
return [
'label' => sprintf('%s%s', (string) $plugin->name, (string) $plugin->code),
'value' => (string) $plugin->code,
'code' => (string) $plugin->code,
'name' => (string) $plugin->name,
];
})
->values()
->all();
}
/**
* 远程查询支付插件选择项。
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
$query = $this->paymentPluginRepository->query()
->where('status', 1)
->select(['code', 'name', 'pay_types'])
->orderBy('code');
$ids = $filters['ids'] ?? [];
if (is_array($ids) && $ids !== []) {
$query->whereIn('code', array_values(array_filter(array_map('strval', $ids))));
} else {
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('code', 'like', '%' . $keyword . '%')
->orWhere('name', 'like', '%' . $keyword . '%');
});
}
$payTypeCode = trim((string) ($filters['pay_type_code'] ?? ''));
if ($payTypeCode !== '') {
$query->whereJsonContains('pay_types', $payTypeCode);
}
}
$paginator = $query->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
return [
'list' => collect($paginator->items())->map(function (PaymentPlugin $plugin): array {
return [
'label' => sprintf('%s%s', (string) $plugin->name, (string) $plugin->code),
'value' => (string) $plugin->code,
'code' => (string) $plugin->code,
'name' => (string) $plugin->name,
'pay_types' => is_array($plugin->pay_types) ? array_values($plugin->pay_types) : [],
];
})->values()->all(),
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'size' => $paginator->perPage(),
];
}
/**
* 查询通道配置场景使用的支付插件选项。
*/
public function channelOptions(): array
{
return $this->paymentPluginRepository->enabledList([
'code',
'name',
'pay_types',
])
->map(function (PaymentPlugin $plugin): array {
return [
'label' => sprintf('%s%s', (string) $plugin->name, (string) $plugin->code),
'value' => (string) $plugin->code,
'code' => (string) $plugin->code,
'name' => (string) $plugin->name,
'pay_types' => is_array($plugin->pay_types) ? array_values($plugin->pay_types) : [],
];
})
->values()
->all();
}
/**
* 按插件编码查询插件。
*/
public function findByCode(string $code): ?PaymentPlugin
{
return $this->paymentPluginRepository->findByCode($code);
}
/**
* 查询插件配置结构。
*
* @return array<string, mixed>
*/
public function getSchema(string $code): array
{
$plugin = $this->paymentPluginRepository->findByCode($code);
if (!$plugin) {
throw new PaymentException('支付插件不存在', 404, [
'plugin_code' => $code,
]);
}
return [
'config_schema' => is_array($plugin->config_schema) ? array_values($plugin->config_schema) : [],
];
}
/**
* 更新支付插件。
*/
public function update(string $code, array $data): ?PaymentPlugin
{
$payload = [];
if (array_key_exists('status', $data)) {
$payload['status'] = (int) $data['status'];
}
if (array_key_exists('remark', $data)) {
$payload['remark'] = trim((string) $data['remark']);
}
if ($payload === []) {
return $this->paymentPluginRepository->findByCode($code);
}
if (!$this->paymentPluginRepository->updateByKey($code, $payload)) {
return null;
}
return $this->paymentPluginRepository->findByCode($code);
}
/**
* 从插件目录刷新并同步支付插件定义。
*/
public function refreshFromClasses(): array
{
return $this->paymentPluginSyncService->refreshFromClasses();
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\common\interface\PayPluginInterface;
use app\common\interface\PaymentInterface;
use app\exception\PaymentException;
use app\model\payment\PaymentPlugin;
use app\repository\payment\config\PaymentPluginRepository;
/**
* 支付插件同步服务。
*
* 负责扫描插件目录、实例化插件类并同步数据库定义。
*/
class PaymentPluginSyncService extends BaseService
{
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository
) {}
/**
* 从插件目录刷新并同步支付插件定义。
*/
public function refreshFromClasses(): array
{
$directory = base_path() . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'payment';
$files = glob($directory . DIRECTORY_SEPARATOR . '*.php') ?: [];
$rows = [];
foreach ($files as $file) {
$shortClassName = pathinfo($file, PATHINFO_FILENAME);
$className = 'app\\common\\payment\\' . $shortClassName;
$plugin = $this->instantiatePlugin($className);
if (!$plugin) {
continue;
}
$code = trim((string) $plugin->getCode());
if ($code === '') {
throw new PaymentException('支付插件编码不能为空', 40220, ['class_name' => $className]);
}
if (isset($rows[$code])) {
throw new PaymentException('支付插件编码重复', 40221, [
'plugin_code' => $code,
'class_name' => $className,
]);
}
$rows[$code] = [
'code' => $plugin->getCode(),
'name' => $plugin->getName(),
'class_name' => $shortClassName,
'config_schema' => $plugin->getConfigSchema(),
'pay_types' => $plugin->getEnabledPayTypes(),
'transfer_types' => $plugin->getEnabledTransferTypes(),
'version' => $plugin->getVersion(),
'author' => $plugin->getAuthorName(),
'link' => $plugin->getAuthorLink(),
];
}
ksort($rows);
$existing = $this->paymentPluginRepository->query()
->get()
->keyBy('code')
->all();
$this->transaction(function () use ($rows, $existing) {
foreach ($rows as $code => $row) {
/** @var PaymentPlugin|null $current */
$current = $existing[$code] ?? null;
$payload = array_merge($row, [
'status' => (int) ($current->status ?? 1),
'remark' => (string) ($current->remark ?? ''),
]);
if ($current) {
$current->fill($payload);
$current->save();
unset($existing[$code]);
continue;
}
$this->paymentPluginRepository->create($payload);
}
foreach ($existing as $plugin) {
$plugin->delete();
}
});
return [
'count' => count($rows),
'plugins' => $this->paymentPluginRepository->query()
->orderBy('code')
->get()
->values()
->all(),
];
}
/**
* 实例化插件类并过滤非支付插件类。
*/
private function instantiatePlugin(string $className): null|(PaymentInterface & PayPluginInterface)
{
if (!class_exists($className)) {
return null;
}
$instance = container_make($className, []);
if (!$instance instanceof PayPluginInterface || !$instance instanceof PaymentInterface) {
return null;
}
return $instance;
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\PaymentException;
use app\model\payment\PaymentPollGroupBind;
use app\repository\merchant\base\MerchantGroupRepository;
use app\repository\payment\config\PaymentPollGroupBindRepository;
use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 商户分组路由绑定服务。
*/
class PaymentPollGroupBindService extends BaseService
{
public function __construct(
protected PaymentPollGroupBindRepository $paymentPollGroupBindRepository,
protected MerchantGroupRepository $merchantGroupRepository,
protected PaymentPollGroupRepository $paymentPollGroupRepository
) {
}
/**
* 分页查询商户分组路由绑定。
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentPollGroupBindRepository->query()
->from('ma_payment_poll_group_bind as b')
->leftJoin('ma_merchant_group as mg', 'mg.id', '=', 'b.merchant_group_id')
->leftJoin('ma_payment_type as t', 't.id', '=', 'b.pay_type_id')
->leftJoin('ma_payment_poll_group as pg', 'pg.id', '=', 'b.poll_group_id')
->select([
'b.id',
'b.merchant_group_id',
'b.pay_type_id',
'b.poll_group_id',
'b.status',
'b.remark',
'b.created_at',
'b.updated_at',
'mg.group_name as merchant_group_name',
't.name as pay_type_name',
'pg.group_name as poll_group_name',
'pg.route_mode',
]);
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('mg.group_name', 'like', '%' . $keyword . '%')
->orWhere('t.name', 'like', '%' . $keyword . '%')
->orWhere('pg.group_name', 'like', '%' . $keyword . '%');
});
}
if (($merchantGroupId = (int) ($filters['merchant_group_id'] ?? 0)) > 0) {
$query->where('b.merchant_group_id', $merchantGroupId);
}
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('b.pay_type_id', $payTypeId);
}
if (($pollGroupId = (int) ($filters['poll_group_id'] ?? 0)) > 0) {
$query->where('b.poll_group_id', $pollGroupId);
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('b.status', (int) $filters['status']);
}
return $query
->orderByDesc('b.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
public function findById(int $id): ?PaymentPollGroupBind
{
return $this->paymentPollGroupBindRepository->find($id);
}
public function create(array $data): PaymentPollGroupBind
{
$this->assertBindingUnique((int) $data['merchant_group_id'], (int) $data['pay_type_id']);
$this->assertPollGroupMatchesPayType($data);
return $this->paymentPollGroupBindRepository->create($this->normalizePayload($data));
}
public function update(int $id, array $data): ?PaymentPollGroupBind
{
$current = $this->paymentPollGroupBindRepository->find($id);
if (!$current) {
return null;
}
$merchantGroupId = (int) ($data['merchant_group_id'] ?? $current->merchant_group_id);
$payTypeId = (int) ($data['pay_type_id'] ?? $current->pay_type_id);
$this->assertBindingUnique($merchantGroupId, $payTypeId, $id);
$this->assertPollGroupMatchesPayType(array_merge($current->toArray(), $data));
if (!$this->paymentPollGroupBindRepository->updateById($id, $this->normalizePayload($data))) {
return null;
}
return $this->paymentPollGroupBindRepository->find($id);
}
public function delete(int $id): bool
{
return $this->paymentPollGroupBindRepository->deleteById($id);
}
private function normalizePayload(array $data): array
{
return [
'merchant_group_id' => (int) $data['merchant_group_id'],
'pay_type_id' => (int) $data['pay_type_id'],
'poll_group_id' => (int) $data['poll_group_id'],
'status' => (int) ($data['status'] ?? 1),
'remark' => trim((string) ($data['remark'] ?? '')),
];
}
private function assertBindingUnique(int $merchantGroupId, int $payTypeId, int $ignoreId = 0): void
{
$query = $this->paymentPollGroupBindRepository->query()
->where('merchant_group_id', $merchantGroupId)
->where('pay_type_id', $payTypeId);
if ($ignoreId > 0) {
$query->where('id', '<>', $ignoreId);
}
if ($query->exists()) {
throw new PaymentException('当前商户分组与支付方式已绑定轮询组', 40232, [
'merchant_group_id' => $merchantGroupId,
'pay_type_id' => $payTypeId,
]);
}
}
private function assertPollGroupMatchesPayType(array $data): void
{
$pollGroupId = (int) ($data['poll_group_id'] ?? 0);
$payTypeId = (int) ($data['pay_type_id'] ?? 0);
$pollGroup = $this->paymentPollGroupRepository->find($pollGroupId);
if (!$pollGroup) {
return;
}
if ((int) $pollGroup->pay_type_id !== $payTypeId) {
throw new PaymentException('轮询组与支付方式不一致', 40233, [
'poll_group_id' => $pollGroupId,
'pay_type_id' => $payTypeId,
]);
}
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\PaymentException;
use app\model\payment\PaymentPollGroupChannel;
use app\repository\payment\config\PaymentChannelRepository;
use app\repository\payment\config\PaymentPollGroupChannelRepository;
use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 轮询组通道编排服务。
*/
class PaymentPollGroupChannelService extends BaseService
{
public function __construct(
protected PaymentPollGroupChannelRepository $paymentPollGroupChannelRepository,
protected PaymentPollGroupRepository $paymentPollGroupRepository,
protected PaymentChannelRepository $paymentChannelRepository
) {
}
/**
* 分页查询轮询组通道编排。
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentPollGroupChannelRepository->query()
->from('ma_payment_poll_group_channel as pgc')
->leftJoin('ma_payment_poll_group as pg', 'pg.id', '=', 'pgc.poll_group_id')
->leftJoin('ma_payment_channel as c', 'c.id', '=', 'pgc.channel_id')
->leftJoin('ma_payment_type as t', 't.id', '=', 'pg.pay_type_id')
->select([
'pgc.id',
'pgc.poll_group_id',
'pgc.channel_id',
'pgc.sort_no',
'pgc.weight',
'pgc.is_default',
'pgc.status',
'pgc.remark',
'pgc.created_at',
'pgc.updated_at',
'pg.group_name as poll_group_name',
'pg.pay_type_id',
'c.name as channel_name',
'c.merchant_id',
'c.channel_mode',
'c.plugin_code',
't.name as pay_type_name',
]);
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('pg.group_name', 'like', '%' . $keyword . '%')
->orWhere('c.name', 'like', '%' . $keyword . '%')
->orWhere('c.plugin_code', 'like', '%' . $keyword . '%');
});
}
if (($pollGroupId = (int) ($filters['poll_group_id'] ?? 0)) > 0) {
$query->where('pgc.poll_group_id', $pollGroupId);
}
if (($channelId = (int) ($filters['channel_id'] ?? 0)) > 0) {
$query->where('pgc.channel_id', $channelId);
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('pgc.status', (int) $filters['status']);
}
return $query
->orderBy('pgc.poll_group_id')
->orderBy('pgc.sort_no')
->orderByDesc('pgc.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
public function findById(int $id): ?PaymentPollGroupChannel
{
return $this->paymentPollGroupChannelRepository->find($id);
}
public function create(array $data): PaymentPollGroupChannel
{
$this->assertPairUnique((int) $data['poll_group_id'], (int) $data['channel_id']);
$this->assertChannelMatchesPollGroup($data);
$payload = $this->normalizePayload($data);
return $this->transaction(function () use ($payload) {
if ((int) ($payload['is_default'] ?? 0) === 1) {
$this->paymentPollGroupChannelRepository->clearDefaultExcept((int) $payload['poll_group_id']);
}
return $this->paymentPollGroupChannelRepository->create($payload);
});
}
public function update(int $id, array $data): ?PaymentPollGroupChannel
{
$current = $this->paymentPollGroupChannelRepository->find($id);
if (!$current) {
return null;
}
$pollGroupId = (int) ($data['poll_group_id'] ?? $current->poll_group_id);
$channelId = (int) ($data['channel_id'] ?? $current->channel_id);
$this->assertPairUnique($pollGroupId, $channelId, $id);
$this->assertChannelMatchesPollGroup(array_merge($current->toArray(), $data));
$payload = $this->normalizePayload($data);
return $this->transaction(function () use ($id, $payload) {
if ((int) ($payload['is_default'] ?? 0) === 1) {
$this->paymentPollGroupChannelRepository->clearDefaultExcept((int) $payload['poll_group_id'], $id);
}
if (!$this->paymentPollGroupChannelRepository->updateById($id, $payload)) {
return null;
}
return $this->paymentPollGroupChannelRepository->find($id);
});
}
public function delete(int $id): bool
{
return $this->paymentPollGroupChannelRepository->deleteById($id);
}
private function normalizePayload(array $data): array
{
return [
'poll_group_id' => (int) $data['poll_group_id'],
'channel_id' => (int) $data['channel_id'],
'sort_no' => (int) ($data['sort_no'] ?? 0),
'weight' => max(1, (int) ($data['weight'] ?? 100)),
'is_default' => (int) ($data['is_default'] ?? 0),
'status' => (int) ($data['status'] ?? 1),
'remark' => trim((string) ($data['remark'] ?? '')),
];
}
private function assertPairUnique(int $pollGroupId, int $channelId, int $ignoreId = 0): void
{
$query = $this->paymentPollGroupChannelRepository->query()
->where('poll_group_id', $pollGroupId)
->where('channel_id', $channelId);
if ($ignoreId > 0) {
$query->where('id', '<>', $ignoreId);
}
if ($query->exists()) {
throw new PaymentException('该轮询组已添加当前支付通道', 40230, [
'poll_group_id' => $pollGroupId,
'channel_id' => $channelId,
]);
}
}
private function assertChannelMatchesPollGroup(array $data): void
{
$pollGroupId = (int) ($data['poll_group_id'] ?? 0);
$channelId = (int) ($data['channel_id'] ?? 0);
$pollGroup = $this->paymentPollGroupRepository->find($pollGroupId);
$channel = $this->paymentChannelRepository->find($channelId);
if (!$pollGroup || !$channel) {
return;
}
if ((int) $pollGroup->pay_type_id !== (int) $channel->pay_type_id) {
throw new PaymentException('轮询组与支付通道的支付方式不一致', 40231, [
'poll_group_id' => $pollGroupId,
'channel_id' => $channelId,
]);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\PaymentException;
use app\model\payment\PaymentPollGroup;
use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 支付轮询组命令服务。
*/
class PaymentPollGroupCommandService extends BaseService
{
public function __construct(
protected PaymentPollGroupRepository $paymentPollGroupRepository
) {
}
public function create(array $data): PaymentPollGroup
{
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''));
return $this->paymentPollGroupRepository->create($data);
}
public function update(int $id, array $data): ?PaymentPollGroup
{
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''), $id);
if (!$this->paymentPollGroupRepository->updateById($id, $data)) {
return null;
}
return $this->paymentPollGroupRepository->find($id);
}
public function delete(int $id): bool
{
return $this->paymentPollGroupRepository->deleteById($id);
}
private function assertGroupNameUnique(string $groupName, int $ignoreId = 0): void
{
$groupName = trim($groupName);
if ($groupName === '') {
return;
}
if ($this->paymentPollGroupRepository->existsByGroupName($groupName, $ignoreId)) {
throw new PaymentException('轮询组名称已存在', 40234, [
'group_name' => $groupName,
'ignore_id' => $ignoreId,
]);
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\model\payment\PaymentPollGroup;
use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 支付轮询组查询服务。
*/
class PaymentPollGroupQueryService extends BaseService
{
public function __construct(
protected PaymentPollGroupRepository $paymentPollGroupRepository
) {
}
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentPollGroupRepository->query();
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where('group_name', 'like', '%' . $keyword . '%');
}
$groupName = trim((string) ($filters['group_name'] ?? ''));
if ($groupName !== '') {
$query->where('group_name', 'like', '%' . $groupName . '%');
}
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('pay_type_id', $payTypeId);
}
if (array_key_exists('route_mode', $filters) && $filters['route_mode'] !== '') {
$query->where('route_mode', (int) $filters['route_mode']);
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('status', (int) $filters['status']);
}
return $query
->orderByDesc('id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
public function enabledOptions(array $filters = []): array
{
$query = $this->paymentPollGroupRepository->query()
->where('status', 1);
if (($payTypeId = (int) ($filters['pay_type_id'] ?? 0)) > 0) {
$query->where('pay_type_id', $payTypeId);
}
return $query
->orderBy('group_name')
->orderByDesc('id')
->get(['id', 'group_name', 'pay_type_id', 'route_mode'])
->map(function (PaymentPollGroup $pollGroup): array {
return [
'label' => sprintf('%s%d', (string) $pollGroup->group_name, (int) $pollGroup->id),
'value' => (int) $pollGroup->id,
'pay_type_id' => (int) $pollGroup->pay_type_id,
'route_mode' => (int) $pollGroup->route_mode,
];
})
->values()
->all();
}
public function findById(int $id): ?PaymentPollGroup
{
return $this->paymentPollGroupRepository->find($id);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\model\payment\PaymentPollGroup;
/**
* 支付轮询组门面服务。
*/
class PaymentPollGroupService extends BaseService
{
public function __construct(
protected PaymentPollGroupQueryService $queryService,
protected PaymentPollGroupCommandService $commandService
) {
}
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
return $this->queryService->paginate($filters, $page, $pageSize);
}
public function enabledOptions(array $filters = []): array
{
return $this->queryService->enabledOptions($filters);
}
public function findById(int $id): ?PaymentPollGroup
{
return $this->queryService->findById($id);
}
public function create(array $data): PaymentPollGroup
{
return $this->commandService->create($data);
}
public function update(int $id, array $data): ?PaymentPollGroup
{
return $this->commandService->update($id, $data);
}
public function delete(int $id): bool
{
return $this->commandService->delete($id);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace app\service\payment\config;
use app\common\base\BaseService;
use app\exception\ValidationException;
use app\model\payment\PaymentType;
use app\repository\payment\config\PaymentTypeRepository;
/**
* 支付方式字典服务。
*
* 负责支付方式的基础列表查询、新增、修改、删除和下拉选项输出。
*/
class PaymentTypeService extends BaseService
{
/**
* 构造函数,注入支付方式仓库。
*/
public function __construct(
protected PaymentTypeRepository $paymentTypeRepository
) {
}
/**
* 分页查询支付方式。
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentTypeRepository->query();
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($builder) use ($keyword) {
$builder->where('code', 'like', '%' . $keyword . '%')
->orWhere('name', 'like', '%' . $keyword . '%');
});
}
$code = trim((string) ($filters['code'] ?? ''));
if ($code !== '') {
$query->where('code', 'like', '%' . $code . '%');
}
$name = trim((string) ($filters['name'] ?? ''));
if ($name !== '') {
$query->where('name', 'like', '%' . $name . '%');
}
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
$query->where('status', (int) $filters['status']);
}
return $query
->orderBy('sort_no')
->orderByDesc('id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 查询启用中的支付方式选项。
*/
public function enabledOptions(): array
{
return $this->paymentTypeRepository->enabledList(['id', 'code', 'name'])
->map(function (PaymentType $paymentType): array {
return [
'label' => (string) $paymentType->name,
'value' => (int) $paymentType->id,
'code' => (string) $paymentType->code,
];
})
->values()
->all();
}
/**
* 解析启用中的支付方式,优先按编码匹配,未命中则取首个启用项。
*/
public function resolveEnabledType(string $code = ''): PaymentType
{
$code = trim($code);
if ($code !== '') {
$paymentType = $this->paymentTypeRepository->findByCode($code);
if ($paymentType && (int) $paymentType->status === 1) {
return $paymentType;
}
}
$paymentType = $this->paymentTypeRepository->enabledList()->first();
if (!$paymentType) {
throw new ValidationException('未配置可用支付方式');
}
return $paymentType;
}
/**
* 根据支付方式编码查询字典。
*/
public function findByCode(string $code): ?PaymentType
{
return $this->paymentTypeRepository->findByCode(trim($code));
}
/**
* 根据支付方式 ID 解析支付方式编码。
*/
public function resolveCodeById(int $id): string
{
$paymentType = $this->paymentTypeRepository->find($id);
return $paymentType ? (string) $paymentType->code : '';
}
/**
* 按 ID 查询支付方式。
*/
public function findById(int $id): ?PaymentType
{
return $this->paymentTypeRepository->find($id);
}
/**
* 新增支付方式。
*/
public function create(array $data): PaymentType
{
return $this->paymentTypeRepository->create($data);
}
/**
* 更新支付方式。
*/
public function update(int $id, array $data): ?PaymentType
{
if (!$this->paymentTypeRepository->updateById($id, $data)) {
return null;
}
return $this->paymentTypeRepository->find($id);
}
/**
* 删除支付方式。
*/
public function delete(int $id): bool
{
return $this->paymentTypeRepository->deleteById($id);
}
}