mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-05-09 02:14:53 +08:00
1. 维护代码健壮
2. 更新项目结构文档
This commit is contained in:
@@ -263,24 +263,28 @@ class MerchantCommandService extends BaseService
|
||||
/**
|
||||
* 生成或重置商户 API 凭证。
|
||||
*
|
||||
* 支持分别重置 V1 API Key 和 V2 RSA 密钥对。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array<string, mixed> $options 生成选项
|
||||
* @return array 凭证数据
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
public function issueCredential(int $merchantId): array
|
||||
public function issueCredential(int $merchantId, array $options = []): array
|
||||
{
|
||||
$merchant = $this->merchantQueryService->findById($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$credentialValue = $this->merchantApiCredentialService->issueCredential($merchantId);
|
||||
$credential = $this->merchantApiCredentialService->findByMerchantId($merchantId);
|
||||
$result = $this->merchantApiCredentialService->issueCredentialBundle($merchantId, $options);
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential_value' => $credentialValue,
|
||||
'credential' => $credential,
|
||||
'credential_value' => $result['credential_value'] ?? '',
|
||||
'merchant_private_key' => $result['merchant_private_key'] ?? '',
|
||||
'credential' => $result['credential'] ?? null,
|
||||
'generated' => $result['generated'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -330,6 +334,24 @@ class MerchantCommandService extends BaseService
|
||||
return $merchant;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户是否允许发起支付。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @return Merchant 商户模型
|
||||
* @throws ResourceNotFoundException
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function ensureMerchantPayEnabled(int $merchantId): Merchant
|
||||
{
|
||||
$merchant = $this->ensureMerchantEnabled($merchantId);
|
||||
if ((int) ($merchant->pay_status ?? 1) !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new BusinessStateException('商户支付已关闭', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
return $merchant;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户分组是否启用。
|
||||
*
|
||||
|
||||
@@ -199,11 +199,12 @@ class MerchantService extends BaseService
|
||||
* 生成或重置商户 API 凭证。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array<string, mixed> $options 生成选项
|
||||
* @return array 凭证数据
|
||||
*/
|
||||
public function issueCredential(int $merchantId): array
|
||||
public function issueCredential(int $merchantId, array $options = []): array
|
||||
{
|
||||
return $this->commandService->issueCredential($merchantId);
|
||||
return $this->commandService->issueCredential($merchantId, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,6 +240,17 @@ class MerchantService extends BaseService
|
||||
return $this->commandService->ensureMerchantEnabled($merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户是否允许发起支付。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @return Merchant 商户模型
|
||||
*/
|
||||
public function ensureMerchantPayEnabled(int $merchantId): Merchant
|
||||
{
|
||||
return $this->commandService->ensureMerchantPayEnabled($merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户分组是否启用。
|
||||
*
|
||||
@@ -261,5 +273,3 @@ class MerchantService extends BaseService
|
||||
return $this->queryService->findPolicy($merchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\service\merchant\auth;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\AuthConstant;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\util\JwtTokenManager;
|
||||
use app\exception\ValidationException;
|
||||
@@ -50,7 +51,7 @@ class MerchantAuthService extends BaseService
|
||||
$merchant = $this->merchantPortalSupportService->merchantSummary($merchantId);
|
||||
$credential = $merchantId > 0 ? $this->merchantApiCredentialRepository->findByMerchantId($merchantId) : null;
|
||||
|
||||
$isCredentialEnabled = (int) ($credential->status ?? 0) === 1;
|
||||
$isCredentialEnabled = (int) ($credential->status ?? 0) === AuthConstant::CREDENTIAL_STATUS_ENABLED;
|
||||
$user = [
|
||||
'id' => $merchantId,
|
||||
'deptId' => (string) ($merchant['merchant_group_id'] ?? 0),
|
||||
@@ -104,7 +105,7 @@ class MerchantAuthService extends BaseService
|
||||
*/
|
||||
public function authenticateToken(string $token, string $ip = '', string $userAgent = ''): ?array
|
||||
{
|
||||
$result = $this->jwtTokenManager->verify('merchant', $token, $ip, $userAgent);
|
||||
$result = $this->jwtTokenManager->verify(AuthConstant::GUARD_MERCHANT, $token, $ip, $userAgent);
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ class MerchantAuthService extends BaseService
|
||||
*/
|
||||
public function revokeToken(string $token): bool
|
||||
{
|
||||
return $this->jwtTokenManager->revoke('merchant', $token);
|
||||
return $this->jwtTokenManager->revoke(AuthConstant::GUARD_MERCHANT, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +196,7 @@ class MerchantAuthService extends BaseService
|
||||
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
|
||||
$issued = $this->jwtTokenManager->issue('merchant', [
|
||||
$issued = $this->jwtTokenManager->issue(AuthConstant::GUARD_MERCHANT, [
|
||||
'sub' => (string) $merchantId,
|
||||
'merchant_id' => (int) $merchant->id,
|
||||
'merchant_no' => (string) $merchant->merchant_no,
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\portal;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\exception\PaymentException;
|
||||
use app\model\payment\PaymentChannel;
|
||||
use app\model\payment\PaymentPlugin;
|
||||
use app\model\payment\PaymentPluginConf;
|
||||
use app\repository\payment\config\PaymentChannelRepository;
|
||||
use app\repository\payment\config\PaymentPluginConfRepository;
|
||||
use app\repository\payment\config\PaymentPluginRepository;
|
||||
use app\repository\payment\config\PaymentTypeRepository;
|
||||
|
||||
/**
|
||||
* 商户门户通道配置命令服务。
|
||||
*
|
||||
* 负责商户端插件配置、通道配置的新增修改删除,并集中校验商户归属与插件授权。
|
||||
*/
|
||||
class MerchantPortalChannelCommandService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantPortalSupportService $supportService,
|
||||
protected PaymentPluginRepository $paymentPluginRepository,
|
||||
protected PaymentPluginConfRepository $paymentPluginConfRepository,
|
||||
protected PaymentChannelRepository $paymentChannelRepository,
|
||||
protected PaymentTypeRepository $paymentTypeRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户端允许使用的插件选项。
|
||||
*
|
||||
* @return array 插件选项和支付方式
|
||||
*/
|
||||
public function createMeta(): array
|
||||
{
|
||||
$plugins = $this->paymentPluginRepository->merchantEnabledList([
|
||||
'code',
|
||||
'name',
|
||||
'config_schema',
|
||||
'pay_types',
|
||||
])->map(function (PaymentPlugin $plugin): array {
|
||||
return $this->pluginOption($plugin);
|
||||
})->values()->all();
|
||||
|
||||
return [
|
||||
'plugins' => $plugins,
|
||||
'pay_types' => $this->supportService->enabledPayTypeOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户插件配置列表。
|
||||
*
|
||||
* @param array $filters 筛选条件
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页条数
|
||||
* @return array 列表数据
|
||||
*/
|
||||
public function pluginConfigs(array $filters, int $merchantId, int $page, int $pageSize): 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.merchant_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")
|
||||
->where('c.merchant_id', $merchantId);
|
||||
|
||||
$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 . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$pluginCode = trim((string) ($filters['plugin_code'] ?? ''));
|
||||
if ($pluginCode !== '') {
|
||||
$query->where('c.plugin_code', $pluginCode);
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('c.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
$paginator->getCollection()->transform(function ($row) {
|
||||
$row->settlement_cycle_type_text = $this->textFromMap((int) $row->settlement_cycle_type, [
|
||||
0 => 'D0',
|
||||
1 => 'D1',
|
||||
2 => 'D7',
|
||||
3 => 'T1',
|
||||
4 => 'OTHER',
|
||||
]);
|
||||
$row->created_at_text = $this->formatDateTime($row->created_at ?? null);
|
||||
$row->updated_at_text = $this->formatDateTime($row->updated_at ?? null);
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return [
|
||||
'merchant' => $this->supportService->merchantSummary($merchantId),
|
||||
'plugins' => $this->createMeta()['plugins'],
|
||||
'list' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增商户插件配置。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array $data 写入数据
|
||||
* @return PaymentPluginConf 配置
|
||||
*/
|
||||
public function createPluginConfig(int $merchantId, array $data): PaymentPluginConf
|
||||
{
|
||||
$payload = $this->normalizePluginConfigPayload($merchantId, $data);
|
||||
$this->assertMerchantPluginAllowed((string) $payload['plugin_code']);
|
||||
|
||||
return $this->paymentPluginConfRepository->create($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改商户插件配置。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $id 配置ID
|
||||
* @param array $data 写入数据
|
||||
* @return PaymentPluginConf|null 配置
|
||||
*/
|
||||
public function updatePluginConfig(int $merchantId, int $id, array $data): ?PaymentPluginConf
|
||||
{
|
||||
$model = $this->paymentPluginConfRepository->findByMerchantAndId($merchantId, $id);
|
||||
if (!$model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = $this->normalizePluginConfigPayload($merchantId, $data);
|
||||
$this->assertMerchantPluginAllowed((string) $payload['plugin_code']);
|
||||
|
||||
$model->fill($payload);
|
||||
$model->save();
|
||||
|
||||
return $model->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户插件配置。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $id 配置ID
|
||||
* @return bool 是否删除
|
||||
*/
|
||||
public function deletePluginConfig(int $merchantId, int $id): bool
|
||||
{
|
||||
$model = $this->paymentPluginConfRepository->findByMerchantAndId($merchantId, $id);
|
||||
if (!$model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->paymentChannelRepository->existsBy([
|
||||
'merchant_id' => $merchantId,
|
||||
'api_config_id' => $id,
|
||||
])) {
|
||||
throw new PaymentException('该配置已被通道使用,不能删除', 40241);
|
||||
}
|
||||
|
||||
return (bool) $model->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户插件配置下拉选项。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param string $pluginCode 插件编码
|
||||
* @return array 配置选项
|
||||
*/
|
||||
public function pluginConfigOptions(int $merchantId, string $pluginCode = ''): 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")
|
||||
->where('c.merchant_id', $merchantId)
|
||||
->orderByDesc('c.id');
|
||||
|
||||
$pluginCode = trim($pluginCode);
|
||||
if ($pluginCode !== '') {
|
||||
$query->where('c.plugin_code', $pluginCode);
|
||||
}
|
||||
|
||||
return $query->get()->map(function ($row): array {
|
||||
return [
|
||||
'label' => sprintf('%s(%d)', (string) $row->plugin_name, (int) $row->id),
|
||||
'value' => (int) $row->id,
|
||||
'plugin_code' => (string) $row->plugin_code,
|
||||
'plugin_name' => (string) $row->plugin_name,
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增商户通道。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array $data 写入数据
|
||||
* @return PaymentChannel 通道
|
||||
*/
|
||||
public function createChannel(int $merchantId, array $data): PaymentChannel
|
||||
{
|
||||
$payload = $this->normalizeChannelPayload($merchantId, $data);
|
||||
$this->assertChannelWritable($merchantId, $payload);
|
||||
$this->assertChannelNameUnique($merchantId, (string) $payload['name']);
|
||||
|
||||
return $this->paymentChannelRepository->create($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改商户通道。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $id 通道ID
|
||||
* @param array $data 写入数据
|
||||
* @return PaymentChannel|null 通道
|
||||
*/
|
||||
public function updateChannel(int $merchantId, int $id, array $data): ?PaymentChannel
|
||||
{
|
||||
$model = $this->paymentChannelRepository->findByMerchantAndId($merchantId, $id);
|
||||
if (!$model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = $this->normalizeChannelPayload($merchantId, $data);
|
||||
$this->assertChannelWritable($merchantId, $payload);
|
||||
$this->assertChannelNameUnique($merchantId, (string) $payload['name'], $id);
|
||||
|
||||
$model->fill($payload);
|
||||
$model->save();
|
||||
|
||||
return $model->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户通道。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param int $id 通道ID
|
||||
* @return bool 是否删除
|
||||
*/
|
||||
public function deleteChannel(int $merchantId, int $id): bool
|
||||
{
|
||||
$model = $this->paymentChannelRepository->findByMerchantAndId($merchantId, $id);
|
||||
if (!$model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) $model->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据插件编码查询商户端可用插件结构。
|
||||
*
|
||||
* @param string $pluginCode 插件编码
|
||||
* @return array 配置结构
|
||||
*/
|
||||
public function pluginSchema(string $pluginCode): array
|
||||
{
|
||||
$plugin = $this->assertMerchantPluginAllowed($pluginCode);
|
||||
|
||||
return [
|
||||
'config_schema' => is_array($plugin->config_schema) ? array_values($plugin->config_schema) : [],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePluginConfigPayload(int $merchantId, array $data): array
|
||||
{
|
||||
return [
|
||||
'merchant_id' => $merchantId,
|
||||
'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 normalizeChannelPayload(int $merchantId, array $data): array
|
||||
{
|
||||
return [
|
||||
'merchant_id' => $merchantId,
|
||||
'name' => trim((string) ($data['name'] ?? '')),
|
||||
'split_rate_bp' => 10000,
|
||||
'cost_rate_bp' => 0,
|
||||
'channel_mode' => RouteConstant::CHANNEL_MODE_SELF,
|
||||
'pay_type_id' => (int) ($data['pay_type_id'] ?? 0),
|
||||
'plugin_code' => trim((string) ($data['plugin_code'] ?? '')),
|
||||
'api_config_id' => (int) ($data['api_config_id'] ?? 0),
|
||||
'daily_limit_amount' => max(0, (int) ($data['daily_limit_amount'] ?? 0)),
|
||||
'daily_limit_count' => max(0, (int) ($data['daily_limit_count'] ?? 0)),
|
||||
'min_amount' => max(0, (int) ($data['min_amount'] ?? 0)),
|
||||
'max_amount' => max(0, (int) ($data['max_amount'] ?? 0)),
|
||||
'remark' => trim((string) ($data['remark'] ?? '')),
|
||||
'status' => (int) ($data['status'] ?? CommonConstant::STATUS_ENABLED),
|
||||
'sort_no' => max(0, (int) ($data['sort_no'] ?? 0)),
|
||||
];
|
||||
}
|
||||
|
||||
private function assertChannelWritable(int $merchantId, array $payload): void
|
||||
{
|
||||
if ((string) $payload['name'] === '') {
|
||||
throw new PaymentException('通道名称不能为空', 40242);
|
||||
}
|
||||
|
||||
$plugin = $this->assertMerchantPluginAllowed((string) $payload['plugin_code']);
|
||||
$config = $this->paymentPluginConfRepository->findByMerchantAndId($merchantId, (int) $payload['api_config_id']);
|
||||
if (!$config || (string) $config->plugin_code !== (string) $payload['plugin_code']) {
|
||||
throw new PaymentException('插件配置不存在或不属于当前插件', 40243);
|
||||
}
|
||||
|
||||
$payType = $this->paymentTypeRepository->find((int) $payload['pay_type_id']);
|
||||
if (!$payType) {
|
||||
throw new PaymentException('支付方式不存在', 40244);
|
||||
}
|
||||
|
||||
$payTypes = is_array($plugin->pay_types) ? $plugin->pay_types : [];
|
||||
$payTypeCodes = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $payTypes)));
|
||||
if (!in_array((string) $payType->code, $payTypeCodes, true)) {
|
||||
throw new PaymentException('支付插件不支持当前支付方式', 40245);
|
||||
}
|
||||
|
||||
if ((int) $payload['max_amount'] > 0 && (int) $payload['min_amount'] > (int) $payload['max_amount']) {
|
||||
throw new PaymentException('单笔最小金额不能大于最大金额', 40246);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertChannelNameUnique(int $merchantId, string $name, int $ignoreId = 0): void
|
||||
{
|
||||
if ($this->paymentChannelRepository->existsByMerchantName($merchantId, $name, $ignoreId)) {
|
||||
throw new PaymentException('通道名称已存在', 40247);
|
||||
}
|
||||
|
||||
if ($this->paymentChannelRepository->existsByName($name, $ignoreId)) {
|
||||
throw new PaymentException('通道名称已被占用,请换一个名称', 40248);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertMerchantPluginAllowed(string $pluginCode): PaymentPlugin
|
||||
{
|
||||
$plugin = $this->paymentPluginRepository->findMerchantAllowed($pluginCode);
|
||||
if (!$plugin) {
|
||||
throw new PaymentException('该支付插件未开放给商户端使用', 40240, [
|
||||
'plugin_code' => $pluginCode,
|
||||
]);
|
||||
}
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
private function pluginOption(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) : [],
|
||||
'config_schema' => is_array($plugin->config_schema) ? array_values($plugin->config_schema) : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use app\common\base\BaseService;
|
||||
* 商户门户通道服务。
|
||||
*
|
||||
* @property MerchantPortalChannelQueryService $queryService 查询服务
|
||||
* @property MerchantPortalChannelCommandService $commandService 命令服务
|
||||
* @property MerchantPortalRoutePreviewService $routePreviewService 路由解析服务
|
||||
*/
|
||||
class MerchantPortalChannelService extends BaseService
|
||||
@@ -16,10 +17,12 @@ class MerchantPortalChannelService extends BaseService
|
||||
* 构造方法。
|
||||
*
|
||||
* @param MerchantPortalChannelQueryService $queryService 查询服务
|
||||
* @param MerchantPortalChannelCommandService $commandService 命令服务
|
||||
* @param MerchantPortalRoutePreviewService $routePreviewService 路由解析服务
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantPortalChannelQueryService $queryService,
|
||||
protected MerchantPortalChannelCommandService $commandService,
|
||||
protected MerchantPortalRoutePreviewService $routePreviewService
|
||||
) {
|
||||
}
|
||||
@@ -51,5 +54,54 @@ class MerchantPortalChannelService extends BaseService
|
||||
{
|
||||
return $this->routePreviewService->routePreview($merchantId, $payTypeId, $payAmount, $statDate);
|
||||
}
|
||||
}
|
||||
|
||||
public function createMeta(): array
|
||||
{
|
||||
return $this->commandService->createMeta();
|
||||
}
|
||||
|
||||
public function pluginConfigs(array $filters, int $merchantId, int $page, int $pageSize): array
|
||||
{
|
||||
return $this->commandService->pluginConfigs($filters, $merchantId, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function createPluginConfig(int $merchantId, array $data)
|
||||
{
|
||||
return $this->commandService->createPluginConfig($merchantId, $data);
|
||||
}
|
||||
|
||||
public function updatePluginConfig(int $merchantId, int $id, array $data)
|
||||
{
|
||||
return $this->commandService->updatePluginConfig($merchantId, $id, $data);
|
||||
}
|
||||
|
||||
public function deletePluginConfig(int $merchantId, int $id): bool
|
||||
{
|
||||
return $this->commandService->deletePluginConfig($merchantId, $id);
|
||||
}
|
||||
|
||||
public function pluginConfigOptions(int $merchantId, string $pluginCode = ''): array
|
||||
{
|
||||
return $this->commandService->pluginConfigOptions($merchantId, $pluginCode);
|
||||
}
|
||||
|
||||
public function createChannel(int $merchantId, array $data)
|
||||
{
|
||||
return $this->commandService->createChannel($merchantId, $data);
|
||||
}
|
||||
|
||||
public function updateChannel(int $merchantId, int $id, array $data)
|
||||
{
|
||||
return $this->commandService->updateChannel($merchantId, $id, $data);
|
||||
}
|
||||
|
||||
public function deleteChannel(int $merchantId, int $id): bool
|
||||
{
|
||||
return $this->commandService->deleteChannel($merchantId, $id);
|
||||
}
|
||||
|
||||
public function pluginSchema(string $pluginCode): array
|
||||
{
|
||||
return $this->commandService->pluginSchema($pluginCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,22 +33,44 @@ class MerchantPortalCredentialCommandService extends BaseService
|
||||
* 生成或重置商户 API 凭证。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array $options 生成选项
|
||||
* @return array 凭证数据
|
||||
*/
|
||||
public function issueCredential(int $merchantId): array
|
||||
public function issueCredential(int $merchantId, array $options = []): array
|
||||
{
|
||||
$merchant = $this->supportService->merchantSummary($merchantId);
|
||||
$credentialValue = $this->merchantApiCredentialService->issueCredential($merchantId);
|
||||
$result = $this->merchantApiCredentialService->issueCredentialBundle($merchantId, $options);
|
||||
$credentialValue = (string) ($result['credential_value'] ?? '');
|
||||
$merchantPrivateKey = (string) ($result['merchant_private_key'] ?? '');
|
||||
$generated = (array) ($result['generated'] ?? []);
|
||||
// 凭证明文只在发放当次返回一次,随后再查库只拿脱敏后的展示结构。
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'merchant' => $this->formatMerchant($merchant),
|
||||
'integration' => $this->supportService->apiIntegrationInfo($merchant),
|
||||
'credential_value' => $credentialValue,
|
||||
'merchant_private_key' => $merchantPrivateKey,
|
||||
'generated' => $generated,
|
||||
'credential' => $credential ? $this->formatCredential($credential, $merchant) : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化页面所需商户摘要。
|
||||
*
|
||||
* @param array $merchant 商户摘要
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatMerchant(array $merchant): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($merchant['id'] ?? $merchant['merchant_id'] ?? 0),
|
||||
'merchant_no' => (string) ($merchant['merchant_no'] ?? ''),
|
||||
'merchant_name' => (string) ($merchant['merchant_name'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化接口凭证展示数据。
|
||||
*
|
||||
@@ -58,22 +80,25 @@ class MerchantPortalCredentialCommandService extends BaseService
|
||||
*/
|
||||
private function formatCredential(\app\model\merchant\MerchantApiCredential $credential, array $merchant): array
|
||||
{
|
||||
$signType = (int) $credential->sign_type;
|
||||
$apiKey = trim((string) $credential->api_key);
|
||||
$merchantPublicKey = trim((string) ($credential->merchant_public_key ?? ''));
|
||||
$platformPublicKey = trim((string) config('epay.v2.platform_public_key', ''));
|
||||
|
||||
return [
|
||||
'id' => (int) $credential->id,
|
||||
'merchant_id' => (int) $credential->merchant_id,
|
||||
'merchant_no' => (string) ($merchant['merchant_no'] ?? ''),
|
||||
'merchant_name' => (string) ($merchant['merchant_name'] ?? ''),
|
||||
'sign_type' => $signType,
|
||||
'sign_type_text' => $this->supportService->signTypeText($signType),
|
||||
// 展示页只保留脱敏后的 key 片段,避免明文凭证再次暴露。
|
||||
'api_key_preview' => $this->maskCredentialValue((string) $credential->api_key),
|
||||
'api_key_preview' => $this->maskCredentialValue($apiKey),
|
||||
'api_key_full' => $apiKey,
|
||||
'merchant_public_key_full' => $merchantPublicKey,
|
||||
'merchant_public_key_preview' => $this->maskCredentialValue($merchantPublicKey),
|
||||
'platform_public_key_full' => $platformPublicKey,
|
||||
'platform_public_key_preview' => $this->maskCredentialValue($platformPublicKey),
|
||||
'supports_v1' => $apiKey !== '',
|
||||
'supports_v2' => $merchantPublicKey !== '' && $platformPublicKey !== '',
|
||||
'v1_status_text' => $apiKey !== '' ? '已配置' : '未配置',
|
||||
'v2_status_text' => $merchantPublicKey !== '' && $platformPublicKey !== '' ? '已配置' : '未配置',
|
||||
'status' => (int) $credential->status,
|
||||
'status_text' => (string) ($credential->status ? '启用' : '禁用'),
|
||||
'last_used_at' => $this->formatDateTime($credential->last_used_at ?? null),
|
||||
'created_at' => $this->formatDateTime($credential->created_at ?? null),
|
||||
'updated_at' => $this->formatDateTime($credential->updated_at ?? null),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace app\service\merchant\portal;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\constant\AuthConstant;
|
||||
use app\model\merchant\MerchantApiCredential;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
|
||||
@@ -39,12 +39,28 @@ class MerchantPortalCredentialQueryService extends BaseService
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'merchant' => $this->formatMerchant($merchant),
|
||||
'has_credential' => $credential !== null,
|
||||
'integration' => $this->supportService->apiIntegrationInfo($merchant),
|
||||
'credential' => $credential ? $this->formatCredential($credential, $merchant) : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化页面所需商户摘要。
|
||||
*
|
||||
* @param array $merchant 商户摘要
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatMerchant(array $merchant): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($merchant['id'] ?? $merchant['merchant_id'] ?? 0),
|
||||
'merchant_no' => (string) ($merchant['merchant_no'] ?? ''),
|
||||
'merchant_name' => (string) ($merchant['merchant_name'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化接口凭证展示数据。
|
||||
*
|
||||
@@ -54,24 +70,26 @@ class MerchantPortalCredentialQueryService extends BaseService
|
||||
*/
|
||||
private function formatCredential(MerchantApiCredential $credential, array $merchant): array
|
||||
{
|
||||
$signType = (int) $credential->sign_type;
|
||||
$status = (int) $credential->status;
|
||||
$apiKey = trim((string) $credential->api_key);
|
||||
$merchantPublicKey = trim((string) ($credential->merchant_public_key ?? ''));
|
||||
$platformPublicKey = trim((string) config('epay.v2.platform_public_key', ''));
|
||||
|
||||
return [
|
||||
'id' => (int) $credential->id,
|
||||
'merchant_id' => (int) $credential->merchant_id,
|
||||
'merchant_no' => (string) ($merchant['merchant_no'] ?? ''),
|
||||
'merchant_name' => (string) ($merchant['merchant_name'] ?? ''),
|
||||
'sign_type' => $signType,
|
||||
'sign_type_text' => $this->supportService->signTypeText($signType),
|
||||
'api_key_preview' => $this->maskCredentialValue((string) $credential->api_key),
|
||||
'api_key_preview' => $this->maskCredentialValue($apiKey),
|
||||
'api_key_full' => $apiKey,
|
||||
'merchant_public_key_full' => $merchantPublicKey,
|
||||
'merchant_public_key_preview' => $this->maskCredentialValue($merchantPublicKey),
|
||||
'platform_public_key_full' => $platformPublicKey,
|
||||
'platform_public_key_preview' => $this->maskCredentialValue($platformPublicKey),
|
||||
'supports_v1' => $apiKey !== '',
|
||||
'supports_v2' => $merchantPublicKey !== '' && $platformPublicKey !== '',
|
||||
'v1_status_text' => $apiKey !== '' ? '已配置' : '未配置',
|
||||
'v2_status_text' => $merchantPublicKey !== '' && $platformPublicKey !== '' ? '已配置' : '未配置',
|
||||
'status' => $status,
|
||||
'status_text' => (string) (CommonConstant::statusMap()[$status] ?? '未知'),
|
||||
'last_used_at' => $this->formatDateTime($credential->last_used_at ?? null),
|
||||
'created_at' => $this->formatDateTime($credential->created_at ?? null),
|
||||
'updated_at' => $this->formatDateTime($credential->updated_at ?? null),
|
||||
'status_text' => $this->textFromMap($status, AuthConstant::credentialStatusMap()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ class MerchantPortalCredentialService extends BaseService
|
||||
* 生成或重置商户 API 凭证。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array $options 生成选项
|
||||
* @return array 凭证数据
|
||||
*/
|
||||
public function issueCredential(int $merchantId): array
|
||||
public function issueCredential(int $merchantId, array $options = []): array
|
||||
{
|
||||
return $this->commandService->issueCredential($merchantId);
|
||||
return $this->commandService->issueCredential($merchantId, $options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class MerchantPortalRoutePreviewService extends BaseService
|
||||
));
|
||||
} catch (Throwable $e) {
|
||||
// 解析异常只影响路由结果,不影响基础信息展示,因此这里只回填失败原因。
|
||||
$response['reason'] = $e->getMessage() !== '' ? $e->getMessage() : '路由解析失败';
|
||||
$response['reason'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
@@ -79,6 +79,56 @@ class MerchantPortalService extends BaseService
|
||||
return $this->channelService->myChannels($filters, $merchantId, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function channelCreateMeta(): array
|
||||
{
|
||||
return $this->channelService->createMeta();
|
||||
}
|
||||
|
||||
public function pluginConfigs(array $filters, int $merchantId, int $page, int $pageSize): array
|
||||
{
|
||||
return $this->channelService->pluginConfigs($filters, $merchantId, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function createPluginConfig(int $merchantId, array $data)
|
||||
{
|
||||
return $this->channelService->createPluginConfig($merchantId, $data);
|
||||
}
|
||||
|
||||
public function updatePluginConfig(int $merchantId, int $id, array $data)
|
||||
{
|
||||
return $this->channelService->updatePluginConfig($merchantId, $id, $data);
|
||||
}
|
||||
|
||||
public function deletePluginConfig(int $merchantId, int $id): bool
|
||||
{
|
||||
return $this->channelService->deletePluginConfig($merchantId, $id);
|
||||
}
|
||||
|
||||
public function pluginConfigOptions(int $merchantId, string $pluginCode = ''): array
|
||||
{
|
||||
return $this->channelService->pluginConfigOptions($merchantId, $pluginCode);
|
||||
}
|
||||
|
||||
public function createChannel(int $merchantId, array $data)
|
||||
{
|
||||
return $this->channelService->createChannel($merchantId, $data);
|
||||
}
|
||||
|
||||
public function updateChannel(int $merchantId, int $id, array $data)
|
||||
{
|
||||
return $this->channelService->updateChannel($merchantId, $id, $data);
|
||||
}
|
||||
|
||||
public function deleteChannel(int $merchantId, int $id): bool
|
||||
{
|
||||
return $this->channelService->deleteChannel($merchantId, $id);
|
||||
}
|
||||
|
||||
public function pluginSchema(string $pluginCode): array
|
||||
{
|
||||
return $this->channelService->pluginSchema($pluginCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户路由解析结果。
|
||||
*
|
||||
@@ -108,11 +158,12 @@ class MerchantPortalService extends BaseService
|
||||
* 生成或重置商户门户接口凭证。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array $options 生成选项
|
||||
* @return array 凭证数据
|
||||
*/
|
||||
public function issueCredential(int $merchantId): array
|
||||
public function issueCredential(int $merchantId, array $options = []): array
|
||||
{
|
||||
return $this->credentialService->issueCredential($merchantId);
|
||||
return $this->credentialService->issueCredential($merchantId, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -127,7 +127,31 @@ class MerchantPortalSupportService extends BaseService
|
||||
*/
|
||||
public function enabledPayTypeOptions(): array
|
||||
{
|
||||
return $this->paymentTypeService->enabledOptions();
|
||||
return array_values(array_filter(
|
||||
$this->paymentTypeService->enabledOptions(),
|
||||
static function (array $option): bool {
|
||||
$label = trim((string) ($option['label'] ?? ''));
|
||||
$value = trim((string) ($option['value'] ?? ''));
|
||||
|
||||
return $label !== '' && $label !== $value;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户开放接口对接信息。
|
||||
*
|
||||
* @param array $merchant 商户摘要
|
||||
* @return array<string, mixed> 对接信息
|
||||
*/
|
||||
public function apiIntegrationInfo(array $merchant): array
|
||||
{
|
||||
$baseUrl = rtrim((string) sys_config('site_url', ''), '/');
|
||||
|
||||
return [
|
||||
'base_url' => $baseUrl,
|
||||
'merchant_id' => (int) ($merchant['merchant_id'] ?? $merchant['id'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,7 +63,12 @@ class MerchantApiCredentialQueryService extends BaseService
|
||||
|
||||
$paginator->getCollection()->transform(function ($row) {
|
||||
$row->sign_type_text = $this->textFromMap((int) $row->sign_type, AuthConstant::signTypeMap());
|
||||
$row->status_text = (int) $row->status === AuthConstant::LOGIN_STATUS_ENABLED ? '启用' : '禁用';
|
||||
$row->status_text = $this->textFromMap((int) $row->status, AuthConstant::credentialStatusMap());
|
||||
$row->platform_public_key_preview = $this->maskCredentialValue(
|
||||
trim((string) config('epay.v2.platform_public_key', '')),
|
||||
false
|
||||
);
|
||||
$row->platform_sign_type_text = (string) config('epay.v2.sign_type', AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA);
|
||||
|
||||
return $row;
|
||||
});
|
||||
@@ -110,6 +115,7 @@ class MerchantApiCredentialQueryService extends BaseService
|
||||
'c.id',
|
||||
'c.merchant_id',
|
||||
'c.sign_type',
|
||||
'c.merchant_public_key',
|
||||
'c.status',
|
||||
'c.last_used_at',
|
||||
'c.created_at',
|
||||
@@ -120,9 +126,12 @@ class MerchantApiCredentialQueryService extends BaseService
|
||||
|
||||
if ($maskCredentialValue) {
|
||||
$query->selectRaw("CASE WHEN c.api_key IS NULL OR c.api_key = '' THEN '' ELSE CONCAT(LEFT(c.api_key, 4), '****', RIGHT(c.api_key, 4)) END AS api_key_preview");
|
||||
$query->selectRaw("CASE WHEN c.merchant_public_key IS NULL OR c.merchant_public_key = '' THEN '' ELSE CONCAT(LEFT(c.merchant_public_key, 12), '****', RIGHT(c.merchant_public_key, 12)) END AS merchant_public_key_preview");
|
||||
} else {
|
||||
$query->addSelect('c.api_key');
|
||||
$query->addSelect('c.merchant_public_key');
|
||||
$query->selectRaw("COALESCE(c.api_key, '') AS api_key_full");
|
||||
$query->selectRaw("COALESCE(c.merchant_public_key, '') AS merchant_public_key_full");
|
||||
}
|
||||
|
||||
return $query;
|
||||
@@ -141,8 +150,12 @@ class MerchantApiCredentialQueryService extends BaseService
|
||||
}
|
||||
|
||||
$row->api_key_preview = $this->maskCredentialValue((string) ($row->api_key ?? ''), false);
|
||||
$row->merchant_public_key_preview = $this->maskCredentialValue((string) ($row->merchant_public_key ?? ''), false);
|
||||
$row->sign_type_text = $this->textFromMap((int) $row->sign_type, AuthConstant::signTypeMap());
|
||||
$row->status_text = (int) $row->status === AuthConstant::LOGIN_STATUS_ENABLED ? '启用' : '禁用';
|
||||
$row->status_text = $this->textFromMap((int) $row->status, AuthConstant::credentialStatusMap());
|
||||
$row->platform_public_key_full = trim((string) config('epay.v2.platform_public_key', ''));
|
||||
$row->platform_public_key_preview = $this->maskCredentialValue((string) $row->platform_public_key_full, false);
|
||||
$row->platform_sign_type_text = (string) config('epay.v2.sign_type', AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace app\service\merchant\security;
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\AuthConstant;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\util\RsaKeyPairGenerator;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\Merchant;
|
||||
@@ -13,9 +14,9 @@ use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
|
||||
/**
|
||||
* 商户对外接口凭证与签名校验服务。
|
||||
* 商户对外接口凭证服务。
|
||||
*
|
||||
* 负责商户外部接口签名校验、接口凭证发放和最近使用时间更新。
|
||||
* 负责接口凭证发放、查询和最近使用时间更新。
|
||||
*
|
||||
* @property MerchantRepository $merchantRepository 商户仓库
|
||||
* @property MerchantApiCredentialRepository $merchantApiCredentialRepository 商户 API 凭证仓库
|
||||
@@ -51,82 +52,7 @@ class MerchantApiCredentialService extends BaseService
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验外部支付接口的 MD5 签名。
|
||||
*
|
||||
* 会先校验商户和接口凭证是否存在,再按签名规则计算并比对请求签名。
|
||||
*
|
||||
* @param array $payload 请求载荷
|
||||
* @return array{merchant: Merchant, credential: MerchantApiCredential} 校验通过后的商户和凭证数据
|
||||
* @throws ValidationException
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
public function verifyMd5Sign(array $payload): array
|
||||
{
|
||||
$merchantId = (int) ($payload['pid'] ?? $payload['merchant_id'] ?? 0);
|
||||
$sign = trim((string) ($payload['sign'] ?? ''));
|
||||
$signType = strtoupper((string) ($payload['sign_type'] ?? 'MD5'));
|
||||
$providedKey = trim((string) ($payload['key'] ?? ''));
|
||||
|
||||
if ($merchantId <= 0 || $sign === '') {
|
||||
throw new ValidationException('pid/sign 参数缺失');
|
||||
}
|
||||
|
||||
if ($signType !== 'MD5') {
|
||||
throw new ValidationException('仅支持 MD5 签名');
|
||||
}
|
||||
|
||||
/** @var Merchant|null $merchant */
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant || (int) $merchant->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
/** @var MerchantApiCredential|null $credential */
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
if (!$credential || (int) $credential->status !== AuthConstant::LOGIN_STATUS_ENABLED) {
|
||||
throw new ValidationException('商户 API 凭证未开通');
|
||||
}
|
||||
|
||||
if ($providedKey !== '' && !hash_equals((string) $credential->api_key, $providedKey)) {
|
||||
throw new ValidationException('商户 API 凭证错误');
|
||||
}
|
||||
|
||||
// 签名字段本身不参与原文拼接,只保留业务参数。
|
||||
$params = $payload;
|
||||
unset($params['sign'], $params['sign_type'], $params['key']);
|
||||
// 过滤空值并按键名排序,保证不同参数顺序下得到同一签名串。
|
||||
foreach ($params as $paramKey => $paramValue) {
|
||||
if ($paramValue === '' || $paramValue === null) {
|
||||
unset($params[$paramKey]);
|
||||
}
|
||||
}
|
||||
ksort($params);
|
||||
|
||||
$key = (string) $credential->api_key;
|
||||
$query = [];
|
||||
// 旧版 ePay 采用 `a=1&b=2` 再拼接 key 的方式验签,这里保持兼容。
|
||||
foreach ($params as $paramKey => $paramValue) {
|
||||
$query[] = $paramKey . '=' . $paramValue;
|
||||
}
|
||||
$base = implode('&', $query) . $key;
|
||||
$expected = md5($base);
|
||||
|
||||
// 使用常量时间比较,避免签名对比被时序差异放大。
|
||||
if (!hash_equals(strtolower($expected), strtolower($sign))) {
|
||||
throw new ValidationException('签名验证失败');
|
||||
}
|
||||
|
||||
$credential->last_used_at = $this->now();
|
||||
$credential->save();
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential' => $credential,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 为商户生成并保存一份新的接口凭证。
|
||||
* 为商户生成并保存一份新的 V1 接口凭证。
|
||||
*
|
||||
* 返回值是明文接口凭证值,只会在调用时完整出现一次,后续仅保存脱敏展示。
|
||||
*
|
||||
@@ -135,24 +61,77 @@ class MerchantApiCredentialService extends BaseService
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
public function issueCredential(int $merchantId): string
|
||||
{
|
||||
$result = $this->issueCredentialBundle($merchantId, [
|
||||
'rotate_v1' => true,
|
||||
'rotate_v2' => false,
|
||||
]);
|
||||
|
||||
return (string) ($result['credential_value'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 为商户生成一组接口凭证。
|
||||
*
|
||||
* 该方法可同时重置 V1 API Key 和 V2 RSA 密钥对,适合管理后台的自动生成场景。
|
||||
* 生成后的私钥只在返回结果里出现一次,不会落库。
|
||||
*
|
||||
* @param int $merchantId 商户ID
|
||||
* @param array<string, mixed> $options 生成选项
|
||||
* @return array<string, mixed> 凭证数据和生成结果
|
||||
* @throws ResourceNotFoundException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function issueCredentialBundle(int $merchantId, array $options = []): array
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$credentialValue = $this->generateCredentialValue();
|
||||
$this->merchantApiCredentialRepository->updateOrCreate(
|
||||
$current = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
$rotateV1 = array_key_exists('rotate_v1', $options) ? (bool) $options['rotate_v1'] : true;
|
||||
$rotateV2 = array_key_exists('rotate_v2', $options) ? (bool) $options['rotate_v2'] : true;
|
||||
if (!$rotateV1 && !$rotateV2) {
|
||||
throw new ValidationException('请至少选择一种要生成的凭证类型');
|
||||
}
|
||||
|
||||
$signType = (int) ($options['sign_type'] ?? ($current?->sign_type ?? AuthConstant::API_SIGN_TYPE_MD5));
|
||||
$status = (int) ($options['status'] ?? ($current?->status ?? AuthConstant::CREDENTIAL_STATUS_ENABLED));
|
||||
$credentialValue = $rotateV1 ? $this->generateCredentialValue() : trim((string) ($current?->api_key ?? ''));
|
||||
$merchantPrivateKey = '';
|
||||
$merchantPublicKey = trim((string) ($current?->merchant_public_key ?? ''));
|
||||
|
||||
if ($rotateV2) {
|
||||
$pair = RsaKeyPairGenerator::generate();
|
||||
$merchantPrivateKey = $pair['private_key'];
|
||||
$merchantPublicKey = $pair['public_key'];
|
||||
}
|
||||
|
||||
$credential = $this->merchantApiCredentialRepository->updateOrCreate(
|
||||
['merchant_id' => $merchantId],
|
||||
[
|
||||
'merchant_id' => $merchantId,
|
||||
'sign_type' => AuthConstant::API_SIGN_TYPE_MD5,
|
||||
'sign_type' => $signType,
|
||||
'status' => $status,
|
||||
'api_key' => $credentialValue,
|
||||
'status' => AuthConstant::LOGIN_STATUS_ENABLED,
|
||||
'merchant_public_key' => $merchantPublicKey,
|
||||
]
|
||||
);
|
||||
|
||||
return $credentialValue;
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential' => $credential,
|
||||
'credential_value' => $credentialValue,
|
||||
'merchant_private_key' => $merchantPrivateKey,
|
||||
'generated' => [
|
||||
'rotate_v1' => $rotateV1,
|
||||
'rotate_v2' => $rotateV2,
|
||||
'api_key' => $rotateV1 ? $credentialValue : '',
|
||||
'merchant_private_key' => $merchantPrivateKey,
|
||||
'merchant_public_key' => $merchantPublicKey,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,6 +182,12 @@ class MerchantApiCredentialService extends BaseService
|
||||
}
|
||||
}
|
||||
|
||||
$apiKey = trim((string) ($data['api_key'] ?? ''));
|
||||
$merchantPublicKey = trim((string) ($data['merchant_public_key'] ?? ''));
|
||||
if ($apiKey === '' && $merchantPublicKey === '') {
|
||||
throw new ValidationException('请至少填写 V1 API Key 或 V2 商户 RSA 公钥');
|
||||
}
|
||||
|
||||
return $this->merchantApiCredentialRepository->create($this->normalizePayload($data, false));
|
||||
}
|
||||
|
||||
@@ -264,7 +249,7 @@ class MerchantApiCredentialService extends BaseService
|
||||
|
||||
/** @var MerchantApiCredential|null $credential */
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
if (!$credential || (int) $credential->status !== AuthConstant::LOGIN_STATUS_ENABLED) {
|
||||
if (!$credential || (int) $credential->status !== AuthConstant::CREDENTIAL_STATUS_ENABLED) {
|
||||
throw new ValidationException('商户 API 凭证未开通');
|
||||
}
|
||||
|
||||
@@ -288,24 +273,33 @@ class MerchantApiCredentialService extends BaseService
|
||||
* @param array $data 凭证数据
|
||||
* @param bool $isUpdate 是否更新
|
||||
* @param MerchantApiCredential|null $current 当前凭证
|
||||
* 更新场景下,空字符串视为“不修改”,避免手动配置时误清空已有密钥。
|
||||
* `sign_type` 在当前阶段只作为展示/默认接入说明,不再作为 V1/V2 互斥开关。
|
||||
*
|
||||
* @return array{merchant_id: int, sign_type: int, status: int, api_key?: string} 标准化后的写入数据
|
||||
*/
|
||||
private function normalizePayload(array $data, bool $isUpdate, ?MerchantApiCredential $current = null): array
|
||||
{
|
||||
// 更新场景下以现有记录的 merchant_id 为准,避免把凭证误挂到别的商户。
|
||||
$merchantId = (int) ($current?->merchant_id ?? ($data['merchant_id'] ?? 0));
|
||||
$currentSignType = (int) ($current?->sign_type ?? AuthConstant::API_SIGN_TYPE_MD5);
|
||||
$currentStatus = (int) ($current?->status ?? AuthConstant::CREDENTIAL_STATUS_ENABLED);
|
||||
$payload = [
|
||||
'merchant_id' => $merchantId,
|
||||
'sign_type' => (int) ($data['sign_type'] ?? AuthConstant::API_SIGN_TYPE_MD5),
|
||||
'status' => (int) ($data['status'] ?? AuthConstant::LOGIN_STATUS_ENABLED),
|
||||
'sign_type' => (int) ($data['sign_type'] ?? $currentSignType),
|
||||
'status' => (int) ($data['status'] ?? $currentStatus),
|
||||
];
|
||||
|
||||
$apiKey = trim((string) ($data['api_key'] ?? ''));
|
||||
if ($apiKey !== '') {
|
||||
$payload['api_key'] = $apiKey;
|
||||
} elseif (!$isUpdate) {
|
||||
// 新增凭证时如果前端没有传入明文 key,就自动补一份随机值。
|
||||
$payload['api_key'] = $this->generateCredentialValue();
|
||||
}
|
||||
|
||||
if (array_key_exists('merchant_public_key', $data)) {
|
||||
$merchantPublicKey = trim((string) ($data['merchant_public_key'] ?? ''));
|
||||
if ($merchantPublicKey !== '' || !$isUpdate) {
|
||||
$payload['merchant_public_key'] = $merchantPublicKey;
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
|
||||
Reference in New Issue
Block a user