更新统一使用 PHPDoc + PSR-19 标准注释

This commit is contained in:
技术老胡
2026-04-21 08:38:59 +08:00
parent dcd58e24ce
commit 9a16a88640
252 changed files with 9218 additions and 659 deletions

View File

@@ -13,9 +13,25 @@ use app\repository\payment\config\PaymentTypeRepository;
/**
* 支付通道命令服务。
*
* 负责支付通道的新增、修改、删除以及写入前的商户、插件和支付方式约束校验。
*
* @property MerchantRepository $merchantRepository 商户仓库
* @property PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
*/
class PaymentChannelCommandService extends BaseService
{
/**
* 构造方法。
*
* @param MerchantRepository $merchantRepository 商户仓库
* @param PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @return void
*/
public function __construct(
protected MerchantRepository $merchantRepository,
protected PaymentChannelRepository $paymentChannelRepository,
@@ -24,13 +40,27 @@ class PaymentChannelCommandService extends BaseService
) {
}
/**
* 按 ID 查询支付通道。
*
* @param int $id 支付通道ID
* @return PaymentChannel|null 支付通道模型
*/
public function findById(int $id): ?PaymentChannel
{
return $this->paymentChannelRepository->find($id);
}
/**
* 新增支付通道。
*
* @param array $data 写入数据
* @return PaymentChannel 新增后的支付通道模型
* @throws PaymentException
*/
public function create(array $data): PaymentChannel
{
// 新增通道前先校验名称、商户归属和插件支付方式兼容性。
$this->assertChannelNameUnique((string) ($data['name'] ?? ''));
$this->assertMerchantExists($data);
$this->assertPluginSupportsPayType($data);
@@ -38,8 +68,17 @@ class PaymentChannelCommandService extends BaseService
return $this->paymentChannelRepository->create($data);
}
/**
* 更新支付通道。
*
* @param int $id 支付通道ID
* @param array $data 写入数据
* @return PaymentChannel|null 更新后的支付通道模型
* @throws PaymentException
*/
public function update(int $id, array $data): ?PaymentChannel
{
// 更新通道时同样要先拦住冲突配置,避免保存后才发现路由不可用。
$this->assertChannelNameUnique((string) ($data['name'] ?? ''), $id);
$this->assertMerchantExists($data);
$this->assertPluginSupportsPayType($data);
@@ -51,11 +90,24 @@ class PaymentChannelCommandService extends BaseService
return $this->paymentChannelRepository->find($id);
}
/**
* 删除支付通道。
*
* @param int $id 支付通道ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->paymentChannelRepository->deleteById($id);
}
/**
* 校验通道所属商户是否存在。
*
* @param array $data 写入数据
* @return void
* @throws PaymentException
*/
private function assertMerchantExists(array $data): void
{
if (!array_key_exists('merchant_id', $data)) {
@@ -63,6 +115,7 @@ class PaymentChannelCommandService extends BaseService
}
$merchantId = (int) $data['merchant_id'];
// merchant_id 为空或为 0 时通常表示通道草稿,这里不强制拦截。
if ($merchantId === 0) {
return;
}
@@ -74,11 +127,19 @@ class PaymentChannelCommandService extends BaseService
}
}
/**
* 校验支付插件是否支持当前支付方式。
*
* @param array $data 写入数据
* @return void
* @throws PaymentException
*/
private function assertPluginSupportsPayType(array $data): void
{
$pluginCode = trim((string) ($data['plugin_code'] ?? ''));
$payTypeId = (int) ($data['pay_type_id'] ?? 0);
// 草稿态允许只填一半字段,只有插件和支付方式都明确时才做交叉校验。
if ($pluginCode === '' || $payTypeId <= 0) {
return;
}
@@ -90,6 +151,7 @@ class PaymentChannelCommandService extends BaseService
return;
}
// 插件支持的支付方式可能来自 JSON 配置,先统一压成编码列表再比对。
$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);
@@ -102,6 +164,14 @@ class PaymentChannelCommandService extends BaseService
}
}
/**
* 校验通道名称唯一。
*
* @param string $name 通道名称
* @param int $ignoreId 排除的通道ID
* @return void
* @throws PaymentException
*/
private function assertChannelNameUnique(string $name, int $ignoreId = 0): void
{
$name = trim($name);
@@ -117,3 +187,5 @@ class PaymentChannelCommandService extends BaseService
}
}
}

View File

@@ -8,15 +8,30 @@ use app\model\payment\PaymentChannel;
use app\repository\payment\config\PaymentChannelRepository;
/**
* 支付通道查询服务。
* 支付通道查询与选项拼装服务。
*
* 负责支付通道列表、详情、下拉选项和路由候选数据的查询拼装。
*
* @property PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
*/
class PaymentChannelQueryService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @return void
*/
public function __construct(
protected PaymentChannelRepository $paymentChannelRepository
) {
}
/**
* 获取启用支付通道选项。
*
* @return array<int, array{label: string, value: int}> 启用通道选项
*/
public function enabledOptions(): array
{
return $this->paymentChannelRepository->query()
@@ -38,6 +53,14 @@ class PaymentChannelQueryService extends BaseService
->all();
}
/**
* 搜索支付通道选项。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return array{list: array<int, array{label: string, value: int, merchant_id: int, merchant_no: string, merchant_name: string, channel_mode: int, pay_type_id: int, pay_type_name: string, plugin_code: string}>, total: int, page: int, size: int} 通道搜索结果
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
$query = $this->paymentChannelRepository->query()
@@ -59,12 +82,15 @@ class PaymentChannelQueryService extends BaseService
$ids = $this->normalizeIds($filters['ids'] ?? []);
if (!empty($ids)) {
// 显式传 ID 时,直接按 ID 集合返回,避免再叠加其他筛选条件影响回显。
$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 . '%')
@@ -87,6 +113,7 @@ class PaymentChannelQueryService extends BaseService
$excludeIds = $this->normalizeIds($filters['exclude_ids'] ?? []);
if (!empty($excludeIds)) {
// 编排时经常要排除当前已选项,这里提供反选列表避免重复挂载同一通道。
$query->whereNotIn('c.id', $excludeIds);
}
}
@@ -119,6 +146,12 @@ class PaymentChannelQueryService extends BaseService
];
}
/**
* 获取支付通道路由候选选项。
*
* @param array $filters 筛选条件
* @return array<int, array{label: string, value: int, merchant_id: int, channel_mode: int, pay_type_id: int, plugin_code: string, pay_type_name: string}> 路由候选选项
*/
public function routeOptions(array $filters = []): array
{
$query = $this->paymentChannelRepository->query()
@@ -140,6 +173,7 @@ class PaymentChannelQueryService extends BaseService
}
if (array_key_exists('merchant_id', $filters) && $filters['merchant_id'] !== '') {
// 路由预览/编排时会按商户分组筛选通道,这里直接用商户 ID 限定范围。
$query->where('c.merchant_id', (int) $filters['merchant_id']);
}
@@ -162,6 +196,14 @@ class PaymentChannelQueryService extends BaseService
->all();
}
/**
* 分页查询支付通道。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
$query = $this->paymentChannelRepository->query()
@@ -175,6 +217,7 @@ class PaymentChannelQueryService extends BaseService
$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 . '%')
@@ -210,11 +253,23 @@ class PaymentChannelQueryService extends BaseService
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 按 ID 查询支付通道。
*
* @param int $id 支付通道ID
* @return PaymentChannel|null 支付通道模型
*/
public function findById(int $id): ?PaymentChannel
{
return $this->paymentChannelRepository->find($id);
}
/**
* 归一化 ID 列表。
*
* @param array|string|int $ids 通道ID或ID列表
* @return array<int, int> ID 列表
*/
private function normalizeIds(array|string|int $ids): array
{
if (is_string($ids)) {
@@ -223,6 +278,9 @@ class PaymentChannelQueryService extends BaseService
$ids = [$ids];
}
// 下拉/搜索参数有时是字符串、有时是数组,统一压成正整数列表后再查询。
return array_values(array_filter(array_map(static fn ($id) => (int) $id, $ids), static fn ($id) => $id > 0));
}
}

View File

@@ -6,53 +6,116 @@ use app\common\base\BaseService;
use app\model\payment\PaymentChannel;
/**
* 支付通道门面服务。
* 支付通道服务。
*
* @property PaymentChannelQueryService $queryService 查询服务
* @property PaymentChannelCommandService $commandService 命令服务
*/
class PaymentChannelService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentChannelQueryService $queryService 查询服务
* @param PaymentChannelCommandService $commandService 命令服务
* @return void
*/
public function __construct(
protected PaymentChannelQueryService $queryService,
protected PaymentChannelCommandService $commandService
) {
}
/**
* 获取启用支付通道选项。
*
* @return array<int, array{label: string, value: int}> 启用通道选项
*/
public function enabledOptions(): array
{
return $this->queryService->enabledOptions();
}
/**
* 搜索支付通道选项。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return array{list: array<int, array<string, mixed>>, total: int, page: int, size: int} 通道搜索结果
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
return $this->queryService->searchOptions($filters, $page, $pageSize);
}
/**
* 获取支付渠道路由选项。
*
* @param array $filters 筛选条件
* @return array<int, array<string, mixed>> 路由候选选项
*/
public function routeOptions(array $filters = []): array
{
return $this->queryService->routeOptions($filters);
}
/**
* 分页查询支付通道。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
return $this->queryService->paginate($filters, $page, $pageSize);
}
/**
* 按 ID 查询支付通道。
*
* @param int $id 支付通道ID
* @return PaymentChannel|null 支付通道模型
*/
public function findById(int $id): ?PaymentChannel
{
return $this->queryService->findById($id);
}
/**
* 新增支付通道。
*
* @param array $data 写入数据
* @return PaymentChannel 新增后的支付通道模型
*/
public function create(array $data): PaymentChannel
{
return $this->commandService->create($data);
}
/**
* 更新支付通道。
*
* @param int $id 支付通道ID
* @param array $data 写入数据
* @return PaymentChannel|null 更新后的支付通道模型
*/
public function update(int $id, array $data): ?PaymentChannel
{
return $this->commandService->update($id, $data);
}
/**
* 删除支付通道。
*
* @param int $id 支付通道ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->commandService->delete($id);
}
}

View File

@@ -11,10 +11,20 @@ use app\repository\payment\config\PaymentPluginRepository;
/**
* 支付插件配置服务。
*
* 负责插件公共配置的增删改查下拉选项输出。
* 负责支付插件公共配置的增删改查下拉选项输出以及插件存在性校验
*
* @property PaymentPluginConfRepository $paymentPluginConfRepository 支付插件配置仓库
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
*/
class PaymentPluginConfService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPluginConfRepository $paymentPluginConfRepository 支付插件配置仓库
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @return void
*/
public function __construct(
protected PaymentPluginConfRepository $paymentPluginConfRepository,
protected PaymentPluginRepository $paymentPluginRepository
@@ -23,6 +33,11 @@ class PaymentPluginConfService extends BaseService
/**
* 分页查询插件配置。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
@@ -43,6 +58,7 @@ class PaymentPluginConfService extends BaseService
$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 . '%')
@@ -62,6 +78,9 @@ class PaymentPluginConfService extends BaseService
/**
* 按 ID 查询插件配置。
*
* @param int $id 支付插件配置ID
* @return PaymentPluginConf|null 插件配置模型
*/
public function findById(int $id): ?PaymentPluginConf
{
@@ -70,6 +89,10 @@ class PaymentPluginConfService extends BaseService
/**
* 新增插件配置。
*
* @param array $data 写入数据
* @return PaymentPluginConf 新增后的插件配置模型
* @throws PaymentException
*/
public function create(array $data): PaymentPluginConf
{
@@ -81,6 +104,11 @@ class PaymentPluginConfService extends BaseService
/**
* 修改插件配置。
*
* @param int $id 支付插件配置ID
* @param array $data 写入数据
* @return PaymentPluginConf|null 更新后的插件配置模型
* @throws PaymentException
*/
public function update(int $id, array $data): ?PaymentPluginConf
{
@@ -96,6 +124,9 @@ class PaymentPluginConfService extends BaseService
/**
* 删除插件配置。
*
* @param int $id 支付插件配置ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
@@ -104,6 +135,9 @@ class PaymentPluginConfService extends BaseService
/**
* 查询插件配置下拉选项。
*
* @param string|null $pluginCode 插件编码
* @return array<int, array{label: string, value: int, plugin_code: string, plugin_name: string}> 配置选项
*/
public function options(?string $pluginCode = null): array
{
@@ -120,6 +154,7 @@ class PaymentPluginConfService extends BaseService
->orderByDesc('c.id');
if ($pluginCode !== '') {
// 如果前端已经明确指定插件编码,就只回这个插件下的配置选项。
$query->where('c.plugin_code', $pluginCode);
}
@@ -138,7 +173,12 @@ class PaymentPluginConfService extends BaseService
}
/**
* 远程查询插件配置选择项。
* 搜索插件配置选择项。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return array{list: array<int, array{label: string, value: int, plugin_code: string, plugin_name: string}>, total: int, page: int, size: int} 配置搜索结果
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
@@ -154,15 +194,18 @@ class PaymentPluginConfService extends BaseService
$ids = $filters['ids'] ?? [];
if (is_array($ids) && $ids !== []) {
// 显式传 ID 时优先按配置主键回显,避免关键词过滤把已选项漏掉。
$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 !== '') {
// 数字关键词既可以按配置 ID 查,也可以按编码或备注查。
$query->where(function ($builder) use ($keyword) {
$builder->where('c.plugin_code', 'like', '%' . $keyword . '%')
->orWhere('p.name', 'like', '%' . $keyword . '%')
@@ -197,13 +240,18 @@ class PaymentPluginConfService extends BaseService
}
/**
* 标准化写入数据。
* 标准化插件配置写入数据。
*
* @param array $data 写入数据
* @return array<string, mixed> 标准化后的数据
*/
private function normalizePayload(array $data): array
{
return [
'plugin_code' => trim((string) ($data['plugin_code'] ?? '')),
// 配置内容统一按数组保存,外部传入非数组时直接回退为空数组。
'config' => is_array($data['config'] ?? null) ? $data['config'] : [],
// 默认结算周期按日配置,截止时间默认按当天 23:59:59 收口。
'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'] ?? '')),
@@ -212,6 +260,10 @@ class PaymentPluginConfService extends BaseService
/**
* 校验插件是否存在。
*
* @param string $pluginCode 插件编码
* @return void
* @throws PaymentException
*/
private function assertPluginExists(string $pluginCode): void
{
@@ -219,6 +271,7 @@ class PaymentPluginConfService extends BaseService
throw new PaymentException('插件编码不能为空', 40230);
}
// 插件配置必须挂到已存在的插件定义上,避免配置和实际实现脱节。
if (!$this->paymentPluginRepository->findByCode($pluginCode)) {
throw new PaymentException('支付插件不存在', 40231, [
'plugin_code' => $pluginCode,

View File

@@ -11,11 +11,18 @@ use app\repository\payment\config\PaymentPluginRepository;
* 支付插件管理服务。
*
* 负责插件目录同步、插件列表查询,以及 JSON 字段写入前的归一化。
*
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @property PaymentPluginSyncService $paymentPluginSyncService 支付插件同步服务
*/
class PaymentPluginService extends BaseService
{
/**
* 构造函数,注入支付插件仓库
* 构造方法
*
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @param PaymentPluginSyncService $paymentPluginSyncService 支付插件同步服务
* @return void
*/
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository,
@@ -25,6 +32,11 @@ class PaymentPluginService extends BaseService
/**
* 分页查询支付插件。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
@@ -60,6 +72,8 @@ class PaymentPluginService extends BaseService
/**
* 查询启用中的支付插件选项。
*
* @return array<int, array{label: string, value: string, code: string, name: string}> 启用插件选项
*/
public function enabledOptions(): array
{
@@ -77,7 +91,12 @@ class PaymentPluginService extends BaseService
}
/**
* 远程查询支付插件选择项。
* 搜索支付插件选择项。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return array{list: array<int, array{label: string, value: string, code: string, name: string, pay_types: array<int, string>}>, total: int, page: int, size: int} 插件搜索结果
*/
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
{
@@ -88,6 +107,7 @@ class PaymentPluginService extends BaseService
$ids = $filters['ids'] ?? [];
if (is_array($ids) && $ids !== []) {
// 显式传 ID 时优先按编码集合回显,避免关键词过滤把手工选择项漏掉。
$query->whereIn('code', array_values(array_filter(array_map('strval', $ids))));
} else {
$keyword = trim((string) ($filters['keyword'] ?? ''));
@@ -100,6 +120,7 @@ class PaymentPluginService extends BaseService
$payTypeCode = trim((string) ($filters['pay_type_code'] ?? ''));
if ($payTypeCode !== '') {
// 如果前端按支付方式筛选,就只保留 pay_types 中包含该编码的插件。
$query->whereJsonContains('pay_types', $payTypeCode);
}
}
@@ -124,9 +145,12 @@ class PaymentPluginService extends BaseService
/**
* 查询通道配置场景使用的支付插件选项。
*
* @return array<int, array{label: string, value: string, code: string, name: string, pay_types: array<int, string>}> 通道配置选项
*/
public function channelOptions(): array
{
// 通道配置场景只需要启用中的插件,并且要带上支付方式集合供前端联动展示。
return $this->paymentPluginRepository->enabledList([
'code',
'name',
@@ -147,6 +171,9 @@ class PaymentPluginService extends BaseService
/**
* 按插件编码查询插件。
*
* @param string $code 插件编码
* @return PaymentPlugin|null 插件模型
*/
public function findByCode(string $code): ?PaymentPlugin
{
@@ -156,7 +183,9 @@ class PaymentPluginService extends BaseService
/**
* 查询插件配置结构。
*
* @return array<string, mixed>
* @param string $code 插件编码
* @return array{config_schema: array<int, mixed>} 配置结构
* @throws PaymentException
*/
public function getSchema(string $code): array
{
@@ -174,10 +203,15 @@ class PaymentPluginService extends BaseService
/**
* 更新支付插件。
*
* @param string $code 插件编码
* @param array $data 写入数据
* @return PaymentPlugin|null 更新后的插件模型
*/
public function update(string $code, array $data): ?PaymentPlugin
{
$payload = [];
// 插件元信息由文件同步维护,后台这里只允许调整状态和备注,避免人工改动覆盖同步结果。
if (array_key_exists('status', $data)) {
$payload['status'] = (int) $data['status'];
}
@@ -199,6 +233,8 @@ class PaymentPluginService extends BaseService
/**
* 从插件目录刷新并同步支付插件定义。
*
* @return array{count: int, plugins: array<int, PaymentPlugin>} 同步结果
*/
public function refreshFromClasses(): array
{

View File

@@ -12,26 +12,40 @@ use app\repository\payment\config\PaymentPluginRepository;
/**
* 支付插件同步服务。
*
* 负责扫描插件目录、实例化插件类并同步数据库定义。
* 负责扫描插件目录、实例化插件类并同步数据库中的插件定义。
*
* @property PaymentPluginRepository $paymentPluginRepository 支付插件仓库
*/
class PaymentPluginSyncService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPluginRepository $paymentPluginRepository 支付插件仓库
* @return void
*/
public function __construct(
protected PaymentPluginRepository $paymentPluginRepository
) {}
/**
* 从插件目录刷新并同步支付插件定义。
*
* @return array{count: int, plugins: array<int, PaymentPlugin>} 同步结果
* @throws PaymentException
*/
public function refreshFromClasses(): array
{
$directory = base_path() . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'payment';
// 扫描固定目录下的插件类文件,每个文件都可能对应一个可同步的插件定义。
$files = glob($directory . DIRECTORY_SEPARATOR . '*.php') ?: [];
// 以插件 code 为键去重,避免同一个插件被多个类重复注册。
$rows = [];
foreach ($files as $file) {
$shortClassName = pathinfo($file, PATHINFO_FILENAME);
$className = 'app\\common\\payment\\' . $shortClassName;
// 先实例化插件,再从实例上读取元信息作为同步源。
$plugin = $this->instantiatePlugin($className);
if (!$plugin) {
continue;
@@ -62,6 +76,7 @@ class PaymentPluginSyncService extends BaseService
];
}
// 先固定排序,再和数据库现有记录逐条对比,保证同步过程稳定可复现。
ksort($rows);
$existing = $this->paymentPluginRepository->query()
@@ -79,6 +94,7 @@ class PaymentPluginSyncService extends BaseService
]);
if ($current) {
// 已存在的插件只覆盖元信息,不改动人工维护的状态和备注。
$current->fill($payload);
$current->save();
unset($existing[$code]);
@@ -88,6 +104,7 @@ class PaymentPluginSyncService extends BaseService
$this->paymentPluginRepository->create($payload);
}
// 数据库里还残留、但文件中已不存在的插件,直接删除避免配置漂移。
foreach ($existing as $plugin) {
$plugin->delete();
}
@@ -105,6 +122,9 @@ class PaymentPluginSyncService extends BaseService
/**
* 实例化插件类并过滤非支付插件类。
*
* @param string $className 插件类名
* @return null|(PaymentInterface&PayPluginInterface) 支付插件实例
*/
private function instantiatePlugin(string $className): null|(PaymentInterface & PayPluginInterface)
{
@@ -120,3 +140,5 @@ class PaymentPluginSyncService extends BaseService
return $instance;
}
}

View File

@@ -11,9 +11,23 @@ use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 商户分组路由绑定服务。
*
* 负责把商户分组和支付方式绑定到指定轮询组,并校验轮询组与支付方式的匹配关系。
*
* @property PaymentPollGroupBindRepository $paymentPollGroupBindRepository 支付轮询分组绑定仓库
* @property MerchantGroupRepository $merchantGroupRepository 商户分组仓库
* @property PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
*/
class PaymentPollGroupBindService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupBindRepository $paymentPollGroupBindRepository 支付轮询分组绑定仓库
* @param MerchantGroupRepository $merchantGroupRepository 商户分组仓库
* @param PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
* @return void
*/
public function __construct(
protected PaymentPollGroupBindRepository $paymentPollGroupBindRepository,
protected MerchantGroupRepository $merchantGroupRepository,
@@ -23,6 +37,11 @@ class PaymentPollGroupBindService extends BaseService
/**
* 分页查询商户分组路由绑定。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
@@ -76,11 +95,24 @@ class PaymentPollGroupBindService extends BaseService
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 按 ID 查询路由绑定。
*
* @param int $id 绑定ID
* @return PaymentPollGroupBind|null 绑定模型
*/
public function findById(int $id): ?PaymentPollGroupBind
{
return $this->paymentPollGroupBindRepository->find($id);
}
/**
* 创建路由绑定。
*
* @param array $data 写入数据
* @return PaymentPollGroupBind 新增后的绑定模型
* @throws PaymentException
*/
public function create(array $data): PaymentPollGroupBind
{
$this->assertBindingUnique((int) $data['merchant_group_id'], (int) $data['pay_type_id']);
@@ -89,6 +121,14 @@ class PaymentPollGroupBindService extends BaseService
return $this->paymentPollGroupBindRepository->create($this->normalizePayload($data));
}
/**
* 更新路由绑定。
*
* @param int $id 绑定ID
* @param array $data 写入数据
* @return PaymentPollGroupBind|null 更新后的绑定模型
* @throws PaymentException
*/
public function update(int $id, array $data): ?PaymentPollGroupBind
{
$current = $this->paymentPollGroupBindRepository->find($id);
@@ -96,6 +136,7 @@ class PaymentPollGroupBindService extends BaseService
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);
@@ -108,11 +149,23 @@ class PaymentPollGroupBindService extends BaseService
return $this->paymentPollGroupBindRepository->find($id);
}
/**
* 删除路由绑定。
*
* @param int $id 绑定ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->paymentPollGroupBindRepository->deleteById($id);
}
/**
* 标准化路由绑定写入数据。
*
* @param array $data 写入数据
* @return array<string, mixed> 标准化后的数据
*/
private function normalizePayload(array $data): array
{
return [
@@ -124,6 +177,15 @@ class PaymentPollGroupBindService extends BaseService
];
}
/**
* 校验商户分组与支付方式的绑定唯一性。
*
* @param int $merchantGroupId 商户分组ID
* @param int $payTypeId 支付方式ID
* @param int $ignoreId 排除的绑定ID
* @return void
* @throws PaymentException
*/
private function assertBindingUnique(int $merchantGroupId, int $payTypeId, int $ignoreId = 0): void
{
$query = $this->paymentPollGroupBindRepository->query()
@@ -142,11 +204,19 @@ class PaymentPollGroupBindService extends BaseService
}
}
/**
* 校验轮询组与支付方式是否一致。
*
* @param array $data 写入数据
* @return void
* @throws PaymentException
*/
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;
@@ -160,3 +230,5 @@ class PaymentPollGroupBindService extends BaseService
}
}
}

View File

@@ -11,9 +11,23 @@ use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 轮询组通道编排服务。
*
* 负责维护轮询组内通道的顺序、权重、默认通道以及支付方式一致性。
*
* @property PaymentPollGroupChannelRepository $paymentPollGroupChannelRepository 支付轮询分组渠道仓库
* @property PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
* @property PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
*/
class PaymentPollGroupChannelService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupChannelRepository $paymentPollGroupChannelRepository 支付轮询分组渠道仓库
* @param PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
* @param PaymentChannelRepository $paymentChannelRepository 支付渠道仓库
* @return void
*/
public function __construct(
protected PaymentPollGroupChannelRepository $paymentPollGroupChannelRepository,
protected PaymentPollGroupRepository $paymentPollGroupRepository,
@@ -23,6 +37,11 @@ class PaymentPollGroupChannelService extends BaseService
/**
* 分页查询轮询组通道编排。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
@@ -79,11 +98,24 @@ class PaymentPollGroupChannelService extends BaseService
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 按 ID 查询轮询组通道编排。
*
* @param int $id 编排ID
* @return PaymentPollGroupChannel|null 编排模型
*/
public function findById(int $id): ?PaymentPollGroupChannel
{
return $this->paymentPollGroupChannelRepository->find($id);
}
/**
* 创建轮询组通道编排。
*
* @param array $data 写入数据
* @return PaymentPollGroupChannel 新增后的编排模型
* @throws PaymentException
*/
public function create(array $data): PaymentPollGroupChannel
{
$this->assertPairUnique((int) $data['poll_group_id'], (int) $data['channel_id']);
@@ -91,6 +123,7 @@ class PaymentPollGroupChannelService extends BaseService
$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']);
}
@@ -99,6 +132,14 @@ class PaymentPollGroupChannelService extends BaseService
});
}
/**
* 更新轮询组通道编排。
*
* @param int $id 编排ID
* @param array $data 写入数据
* @return PaymentPollGroupChannel|null 更新后的编排模型
* @throws PaymentException
*/
public function update(int $id, array $data): ?PaymentPollGroupChannel
{
$current = $this->paymentPollGroupChannelRepository->find($id);
@@ -114,6 +155,7 @@ class PaymentPollGroupChannelService extends BaseService
$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);
}
@@ -126,17 +168,30 @@ class PaymentPollGroupChannelService extends BaseService
});
}
/**
* 删除轮询组通道编排。
*
* @param int $id 编排ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->paymentPollGroupChannelRepository->deleteById($id);
}
/**
* 标准化编排写入数据。
*
* @param array $data 写入数据
* @return array<string, mixed> 标准化后的数据
*/
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),
// 权重至少为 1避免轮询时出现 0 权重通道导致随机分配失真。
'weight' => max(1, (int) ($data['weight'] ?? 100)),
'is_default' => (int) ($data['is_default'] ?? 0),
'status' => (int) ($data['status'] ?? 1),
@@ -144,6 +199,15 @@ class PaymentPollGroupChannelService extends BaseService
];
}
/**
* 校验轮询组与通道的组合唯一性。
*
* @param int $pollGroupId 轮询组ID
* @param int $channelId 通道ID
* @param int $ignoreId 排除的编排ID
* @return void
* @throws PaymentException
*/
private function assertPairUnique(int $pollGroupId, int $channelId, int $ignoreId = 0): void
{
$query = $this->paymentPollGroupChannelRepository->query()
@@ -162,6 +226,13 @@ class PaymentPollGroupChannelService extends BaseService
}
}
/**
* 校验通道支付方式与轮询组支付方式一致。
*
* @param array $data 写入数据
* @return void
* @throws PaymentException
*/
private function assertChannelMatchesPollGroup(array $data): void
{
$pollGroupId = (int) ($data['poll_group_id'] ?? 0);
@@ -174,6 +245,7 @@ class PaymentPollGroupChannelService extends BaseService
return;
}
// 轮询组和通道必须属于同一支付方式,否则排序再正确也会在运行时被路由规则拦下。
if ((int) $pollGroup->pay_type_id !== (int) $channel->pay_type_id) {
throw new PaymentException('轮询组与支付通道的支付方式不一致', 40231, [
'poll_group_id' => $pollGroupId,
@@ -182,3 +254,6 @@ class PaymentPollGroupChannelService extends BaseService
}
}
}

View File

@@ -9,22 +9,47 @@ use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 支付轮询组命令服务。
*
* @property PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
*/
class PaymentPollGroupCommandService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
* @return void
*/
public function __construct(
protected PaymentPollGroupRepository $paymentPollGroupRepository
) {
}
/**
* 创建支付轮询组。
*
* @param array $data 写入数据
* @return PaymentPollGroup 新增后的轮询组模型
* @throws PaymentException
*/
public function create(array $data): PaymentPollGroup
{
// 新增前先确保轮询组名称不冲突,避免后台同时出现两个同名配置。
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''));
return $this->paymentPollGroupRepository->create($data);
}
/**
* 更新支付轮询组。
*
* @param int $id 轮询组ID
* @param array $data 写入数据
* @return PaymentPollGroup|null 更新后的轮询组模型
* @throws PaymentException
*/
public function update(int $id, array $data): ?PaymentPollGroup
{
// 更新时同样要排除自身后再做唯一性判断,防止修改回原名时误报冲突。
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''), $id);
if (!$this->paymentPollGroupRepository->updateById($id, $data)) {
return null;
@@ -33,11 +58,25 @@ class PaymentPollGroupCommandService extends BaseService
return $this->paymentPollGroupRepository->find($id);
}
/**
* 删除支付轮询组。
*
* @param int $id 轮询组ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->paymentPollGroupRepository->deleteById($id);
}
/**
* 校验轮询组名称唯一。
*
* @param string $groupName 轮询组名称
* @param int $ignoreId 排除的轮询组ID
* @return void
* @throws PaymentException
*/
private function assertGroupNameUnique(string $groupName, int $ignoreId = 0): void
{
$groupName = trim($groupName);
@@ -53,3 +92,6 @@ class PaymentPollGroupCommandService extends BaseService
}
}
}

View File

@@ -7,21 +7,40 @@ use app\model\payment\PaymentPollGroup;
use app\repository\payment\config\PaymentPollGroupRepository;
/**
* 支付轮询组查询服务。
* 支付轮询组查询与选项拼装服务。
*
* 负责轮询组列表、详情和启用选项输出。
*
* @property PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
*/
class PaymentPollGroupQueryService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupRepository $paymentPollGroupRepository 支付轮询分组仓库
* @return void
*/
public function __construct(
protected PaymentPollGroupRepository $paymentPollGroupRepository
) {
}
/**
* 分页查询支付轮询组。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
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 . '%');
}
@@ -47,12 +66,19 @@ class PaymentPollGroupQueryService extends BaseService
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
}
/**
* 获取启用支付轮询组选项。
*
* @param array $filters 筛选条件
* @return array<int, array{label: string, value: int, pay_type_id: int, route_mode: int}> 启用轮询组选项
*/
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);
}
@@ -72,8 +98,17 @@ class PaymentPollGroupQueryService extends BaseService
->all();
}
/**
* 按 ID 查询轮询组。
*
* @param int $id 轮询组ID
* @return PaymentPollGroup|null 轮询组模型
*/
public function findById(int $id): ?PaymentPollGroup
{
return $this->paymentPollGroupRepository->find($id);
}
}

View File

@@ -6,43 +6,94 @@ use app\common\base\BaseService;
use app\model\payment\PaymentPollGroup;
/**
* 支付轮询组门面服务。
* 支付轮询组服务。
*
* @property PaymentPollGroupQueryService $queryService 查询服务
* @property PaymentPollGroupCommandService $commandService 命令服务
*/
class PaymentPollGroupService extends BaseService
{
/**
* 构造方法。
*
* @param PaymentPollGroupQueryService $queryService 查询服务
* @param PaymentPollGroupCommandService $commandService 命令服务
* @return void
*/
public function __construct(
protected PaymentPollGroupQueryService $queryService,
protected PaymentPollGroupCommandService $commandService
) {
}
/**
* 分页查询支付轮询组。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
return $this->queryService->paginate($filters, $page, $pageSize);
}
/**
* 获取启用支付轮询组选项。
*
* @param array $filters 筛选条件
* @return array<int, array<string, mixed>> 启用轮询组选项
*/
public function enabledOptions(array $filters = []): array
{
return $this->queryService->enabledOptions($filters);
}
/**
* 按 ID 查询支付轮询组。
*
* @param int $id 轮询组ID
* @return PaymentPollGroup|null 轮询组模型
*/
public function findById(int $id): ?PaymentPollGroup
{
return $this->queryService->findById($id);
}
/**
* 新增支付轮询组。
*
* @param array $data 写入数据
* @return PaymentPollGroup 新增后的轮询组模型
*/
public function create(array $data): PaymentPollGroup
{
return $this->commandService->create($data);
}
/**
* 更新支付轮询组。
*
* @param int $id 轮询组ID
* @param array $data 写入数据
* @return PaymentPollGroup|null 更新后的轮询组模型
*/
public function update(int $id, array $data): ?PaymentPollGroup
{
return $this->commandService->update($id, $data);
}
/**
* 删除支付轮询组。
*
* @param int $id 轮询组ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->commandService->delete($id);
}
}

View File

@@ -11,11 +11,16 @@ use app\repository\payment\config\PaymentTypeRepository;
* 支付方式字典服务。
*
* 负责支付方式的基础列表查询、新增、修改、删除和下拉选项输出。
*
* @property PaymentTypeRepository $paymentTypeRepository 支付类型仓库
*/
class PaymentTypeService extends BaseService
{
/**
* 构造函数,注入支付方式仓库
* 构造方法
*
* @param PaymentTypeRepository $paymentTypeRepository 支付类型仓库
* @return void
*/
public function __construct(
protected PaymentTypeRepository $paymentTypeRepository
@@ -24,6 +29,11 @@ class PaymentTypeService extends BaseService
/**
* 分页查询支付方式。
*
* @param array $filters 筛选条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 分页结果
*/
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
{
@@ -59,6 +69,8 @@ class PaymentTypeService extends BaseService
/**
* 查询启用中的支付方式选项。
*
* @return array<int, array{label: string, value: int, code: string}> 启用支付方式选项
*/
public function enabledOptions(): array
{
@@ -76,6 +88,10 @@ class PaymentTypeService extends BaseService
/**
* 解析启用中的支付方式,优先按编码匹配,未命中则取首个启用项。
*
* @param string $code 支付方式编码
* @return PaymentType 支付方式模型
* @throws ValidationException
*/
public function resolveEnabledType(string $code = ''): PaymentType
{
@@ -87,6 +103,7 @@ class PaymentTypeService extends BaseService
}
}
// 没有传编码或编码不可用时,直接回退到系统当前首个启用支付方式。
$paymentType = $this->paymentTypeRepository->enabledList()->first();
if (!$paymentType) {
throw new ValidationException('未配置可用支付方式');
@@ -97,6 +114,9 @@ class PaymentTypeService extends BaseService
/**
* 根据支付方式编码查询字典。
*
* @param string $code 支付方式编码
* @return PaymentType|null 支付方式模型
*/
public function findByCode(string $code): ?PaymentType
{
@@ -105,6 +125,9 @@ class PaymentTypeService extends BaseService
/**
* 根据支付方式 ID 解析支付方式编码。
*
* @param int $id 支付方式ID
* @return string 支付方式编码
*/
public function resolveCodeById(int $id): string
{
@@ -114,6 +137,9 @@ class PaymentTypeService extends BaseService
/**
* 按 ID 查询支付方式。
*
* @param int $id 支付方式ID
* @return PaymentType|null 支付方式模型
*/
public function findById(int $id): ?PaymentType
{
@@ -122,6 +148,9 @@ class PaymentTypeService extends BaseService
/**
* 新增支付方式。
*
* @param array $data 写入数据
* @return PaymentType 新增后的支付方式模型
*/
public function create(array $data): PaymentType
{
@@ -130,6 +159,10 @@ class PaymentTypeService extends BaseService
/**
* 更新支付方式。
*
* @param int $id 支付方式ID
* @param array $data 写入数据
* @return PaymentType|null 更新后的支付方式模型
*/
public function update(int $id, array $data): ?PaymentType
{
@@ -142,9 +175,14 @@ class PaymentTypeService extends BaseService
/**
* 删除支付方式。
*
* @param int $id 支付方式ID
* @return bool 是否删除成功
*/
public function delete(int $id): bool
{
return $this->paymentTypeRepository->deleteById($id);
}
}