重构初始化

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,50 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\service\account\funds\MerchantAccountService;
use app\service\account\ledger\MerchantAccountLedgerService;
/**
* 商户门户余额服务。
*/
class MerchantPortalBalanceService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected MerchantAccountService $merchantAccountService,
protected MerchantAccountLedgerService $merchantAccountLedgerService
) {
}
public function withdrawableBalance(int $merchantId): array
{
$merchant = $this->supportService->merchantSummary($merchantId);
$snapshot = $this->merchantAccountService->getBalanceSnapshot($merchantId);
$snapshot['available_balance_text'] = $this->supportService->formatAmount((int) ($snapshot['available_balance'] ?? 0));
$snapshot['frozen_balance_text'] = $this->supportService->formatAmount((int) ($snapshot['frozen_balance'] ?? 0));
$snapshot['withdrawable_balance_text'] = $snapshot['available_balance_text'];
return [
'merchant' => $merchant,
'snapshot' => $snapshot,
];
}
public function balanceFlows(array $filters, int $merchantId, int $page, int $pageSize): array
{
$filters['merchant_id'] = $merchantId;
$paginator = $this->merchantAccountLedgerService->paginate($filters, $page, $pageSize);
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'snapshot' => $this->withdrawableBalance($merchantId)['snapshot'],
'list' => $paginator->items(),
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'size' => $paginator->perPage(),
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\common\constant\RouteConstant;
use app\repository\payment\config\PaymentChannelRepository;
/**
* 商户门户通道查询服务。
*
* 负责商户通道列表查询和通道行格式化。
*/
class MerchantPortalChannelQueryService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected PaymentChannelRepository $paymentChannelRepository
) {
}
/**
* 查询当前商户的通道列表。
*/
public function myChannels(array $filters, int $merchantId, int $page, int $pageSize): array
{
$query = $this->paymentChannelRepository->query()
->from('ma_payment_channel as c')
->leftJoin('ma_payment_type as t', 'c.pay_type_id', '=', 't.id')
->select([
'c.id',
'c.merchant_id',
'c.name',
'c.split_rate_bp',
'c.cost_rate_bp',
'c.channel_mode',
'c.pay_type_id',
'c.plugin_code',
'c.api_config_id',
'c.daily_limit_amount',
'c.daily_limit_count',
'c.min_amount',
'c.max_amount',
'c.remark',
'c.status',
'c.sort_no',
'c.created_at',
'c.updated_at',
])
->selectRaw("COALESCE(t.code, '') AS pay_type_code")
->selectRaw("COALESCE(t.name, '') AS pay_type_name")
->where('c.merchant_id', $merchantId);
$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('t.name', 'like', '%' . $keyword . '%');
});
}
$payTypeId = trim((string) ($filters['pay_type_id'] ?? ''));
if ($payTypeId !== '') {
$query->where('c.pay_type_id', (int) $payTypeId);
}
$status = trim((string) ($filters['status'] ?? ''));
if ($status !== '') {
$query->where('c.status', (int) $status);
}
$channelMode = trim((string) ($filters['channel_mode'] ?? ''));
if ($channelMode !== '') {
$query->where('c.channel_mode', (int) $channelMode);
}
$paginator = $query
->orderBy('c.sort_no')
->orderByDesc('c.id')
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
$paginator->getCollection()->transform(function ($row) {
return $this->decorateChannelRow($row);
});
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'pay_types' => $this->supportService->enabledPayTypeOptions(),
'list' => $paginator->items(),
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'size' => $paginator->perPage(),
];
}
private function decorateChannelRow(object $row): object
{
$row->channel_mode_text = (string) (RouteConstant::channelModeMap()[(int) $row->channel_mode] ?? '未知');
$row->status_text = (string) (CommonConstant::statusMap()[(int) $row->status] ?? '未知');
$row->split_rate_text = $this->supportService->formatRate((int) $row->split_rate_bp);
$row->cost_rate_text = $this->supportService->formatRate((int) $row->cost_rate_bp);
$row->daily_limit_amount_text = $this->supportService->formatAmountOrUnlimited((int) $row->daily_limit_amount);
$row->daily_limit_count_text = $this->supportService->formatCountOrUnlimited((int) $row->daily_limit_count);
$row->min_amount_text = $this->supportService->formatAmountOrUnlimited((int) $row->min_amount);
$row->max_amount_text = $this->supportService->formatAmountOrUnlimited((int) $row->max_amount);
$row->created_at_text = $this->supportService->formatDateTime($row->created_at ?? null);
$row->updated_at_text = $this->supportService->formatDateTime($row->updated_at ?? null);
return $row;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户门户通道门面服务。
*
* 对外保留原有调用契约,内部委托给通道查询和路由预览子服务。
*/
class MerchantPortalChannelService extends BaseService
{
public function __construct(
protected MerchantPortalChannelQueryService $queryService,
protected MerchantPortalRoutePreviewService $routePreviewService
) {
}
public function myChannels(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->queryService->myChannels($filters, $merchantId, $page, $pageSize);
}
public function routePreview(int $merchantId, int $payTypeId, int $payAmount, string $statDate = ''): array
{
return $this->routePreviewService->routePreview($merchantId, $payTypeId, $payAmount, $statDate);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\repository\merchant\credential\MerchantApiCredentialRepository;
use app\service\merchant\security\MerchantApiCredentialService;
/**
* 商户门户接口凭证命令服务。
*/
class MerchantPortalCredentialCommandService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected MerchantApiCredentialRepository $merchantApiCredentialRepository,
protected MerchantApiCredentialService $merchantApiCredentialService
) {
}
public function issueCredential(int $merchantId): array
{
$merchant = $this->supportService->merchantSummary($merchantId);
$credentialValue = $this->merchantApiCredentialService->issueCredential($merchantId);
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
return [
'merchant' => $merchant,
'credential_value' => $credentialValue,
'credential' => $credential ? $this->formatCredential($credential, $merchant) : null,
];
}
private function formatCredential(\app\model\merchant\MerchantApiCredential $credential, array $merchant): array
{
$signType = (int) $credential->sign_type;
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->supportService->maskCredentialValue((string) $credential->api_key),
'status' => (int) $credential->status,
'status_text' => (string) ($credential->status ? '启用' : '禁用'),
'last_used_at' => $this->supportService->formatDateTime($credential->last_used_at ?? null),
'created_at' => $this->supportService->formatDateTime($credential->created_at ?? null),
'updated_at' => $this->supportService->formatDateTime($credential->updated_at ?? null),
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\model\merchant\MerchantApiCredential;
use app\repository\merchant\credential\MerchantApiCredentialRepository;
/**
* 商户门户接口凭证查询服务。
*/
class MerchantPortalCredentialQueryService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected MerchantApiCredentialRepository $merchantApiCredentialRepository
) {
}
public function apiCredential(int $merchantId): array
{
$merchant = $this->supportService->merchantSummary($merchantId);
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
return [
'merchant' => $merchant,
'has_credential' => $credential !== null,
'credential' => $credential ? $this->formatCredential($credential, $merchant) : null,
];
}
private function formatCredential(MerchantApiCredential $credential, array $merchant): array
{
$signType = (int) $credential->sign_type;
$status = (int) $credential->status;
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->supportService->maskCredentialValue((string) $credential->api_key),
'status' => $status,
'status_text' => (string) (CommonConstant::statusMap()[$status] ?? '未知'),
'last_used_at' => $this->supportService->formatDateTime($credential->last_used_at ?? null),
'created_at' => $this->supportService->formatDateTime($credential->created_at ?? null),
'updated_at' => $this->supportService->formatDateTime($credential->updated_at ?? null),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户门户接口凭证门面服务。
*/
class MerchantPortalCredentialService extends BaseService
{
public function __construct(
protected MerchantPortalCredentialQueryService $queryService,
protected MerchantPortalCredentialCommandService $commandService
) {
}
public function apiCredential(int $merchantId): array
{
return $this->queryService->apiCredential($merchantId);
}
public function issueCredential(int $merchantId): array
{
return $this->commandService->issueCredential($merchantId);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户门户资金与清算门面服务。
*
* 对外保留原有调用契约,内部委托给清算与余额子服务。
*/
class MerchantPortalFinanceService extends BaseService
{
public function __construct(
protected MerchantPortalSettlementService $settlementService,
protected MerchantPortalBalanceService $balanceService
) {
}
public function settlementRecords(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->settlementService->settlementRecords($filters, $merchantId, $page, $pageSize);
}
public function settlementRecordDetail(string $settleNo, int $merchantId): ?array
{
return $this->settlementService->settlementRecordDetail($settleNo, $merchantId);
}
public function withdrawableBalance(int $merchantId): array
{
return $this->balanceService->withdrawableBalance($merchantId);
}
public function balanceFlows(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->balanceService->balanceFlows($filters, $merchantId, $page, $pageSize);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\exception\ResourceNotFoundException;
use app\exception\ValidationException;
use app\repository\merchant\base\MerchantRepository;
/**
* 商户门户资料命令服务。
*/
class MerchantPortalProfileCommandService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected MerchantRepository $merchantRepository
) {
}
public function updateProfile(int $merchantId, array $data): array
{
$merchant = $this->merchantRepository->find($merchantId);
if (!$merchant) {
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
}
$this->merchantRepository->updateById($merchantId, [
'merchant_short_name' => trim((string) ($data['merchant_short_name'] ?? $merchant->merchant_short_name)),
'contact_name' => trim((string) ($data['contact_name'] ?? $merchant->contact_name)),
'contact_phone' => trim((string) ($data['contact_phone'] ?? $merchant->contact_phone)),
'contact_email' => trim((string) ($data['contact_email'] ?? $merchant->contact_email)),
'settlement_account_name' => trim((string) ($data['settlement_account_name'] ?? $merchant->settlement_account_name)),
'settlement_account_no' => trim((string) ($data['settlement_account_no'] ?? $merchant->settlement_account_no)),
'settlement_bank_name' => trim((string) ($data['settlement_bank_name'] ?? $merchant->settlement_bank_name)),
'settlement_bank_branch' => trim((string) ($data['settlement_bank_branch'] ?? $merchant->settlement_bank_branch)),
]);
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'pay_types' => $this->supportService->enabledPayTypeOptions(),
];
}
public function changePassword(int $merchantId, array $data): array
{
$merchant = $this->merchantRepository->find($merchantId);
if (!$merchant) {
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
}
$currentPassword = trim((string) ($data['current_password'] ?? ''));
$newPassword = trim((string) ($data['password'] ?? ''));
if (!password_verify($currentPassword, (string) $merchant->password_hash)) {
throw new ValidationException('当前密码不正确');
}
$this->merchantRepository->updateById($merchantId, [
'password_hash' => password_hash($newPassword, PASSWORD_DEFAULT),
'password_updated_at' => $this->now(),
]);
return [
'updated' => true,
'password_updated_at' => $this->supportService->formatDateTime($this->now()),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户门户资料查询服务。
*/
class MerchantPortalProfileQueryService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService
) {
}
public function profile(int $merchantId): array
{
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'pay_types' => $this->supportService->enabledPayTypeOptions(),
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户门户资料门面服务。
*/
class MerchantPortalProfileService extends BaseService
{
public function __construct(
protected MerchantPortalProfileQueryService $queryService,
protected MerchantPortalProfileCommandService $commandService
) {
}
public function profile(int $merchantId): array
{
return $this->queryService->profile($merchantId);
}
public function updateProfile(int $merchantId, array $data): array
{
return $this->commandService->updateProfile($merchantId, $data);
}
public function changePassword(int $merchantId, array $data): array
{
return $this->commandService->changePassword($merchantId, $data);
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\common\constant\CommonConstant;
use app\common\constant\RouteConstant;
use app\common\util\FormatHelper;
use app\service\payment\runtime\PaymentRouteService;
use Throwable;
/**
* 商户门户路由预览服务。
*/
class MerchantPortalRoutePreviewService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected PaymentRouteService $paymentRouteService
) {
}
/**
* 预览当前商户的路由选择结果。
*/
public function routePreview(int $merchantId, int $payTypeId, int $payAmount, string $statDate = ''): array
{
$merchant = $this->supportService->merchantSummary($merchantId);
$statDate = trim($statDate) !== '' ? trim($statDate) : FormatHelper::timestamp(time(), 'Y-m-d');
$response = [
'merchant' => $merchant,
'pay_types' => $this->supportService->enabledPayTypeOptions(),
'pay_type_id' => $payTypeId,
'pay_amount' => $payAmount,
'pay_amount_text' => $this->supportService->formatAmount($payAmount),
'stat_date' => $statDate,
'available' => false,
'reason' => '请选择支付方式和金额后预览路由',
'merchant_group_id' => (int) ($merchant['merchant_group_id'] ?? 0),
'merchant_group_name' => (string) ($merchant['merchant_group_name'] ?? ''),
'bind' => null,
'poll_group' => null,
'selected_channel' => null,
'candidates' => [],
];
if ($payTypeId <= 0 || $payAmount <= 0) {
return $response;
}
if ((int) $merchant['merchant_group_id'] <= 0) {
$response['reason'] = '当前商户未配置商户分组,无法预览路由';
return $response;
}
try {
$resolved = $this->paymentRouteService->resolveByMerchantGroup(
(int) $merchant['merchant_group_id'],
$payTypeId,
$payAmount,
['stat_date' => $statDate]
);
$response['available'] = true;
$response['reason'] = '路由预览成功';
$response['bind'] = $this->normalizeBind($resolved['bind'] ?? null);
$response['poll_group'] = $this->normalizePollGroup($resolved['poll_group'] ?? null);
$response['selected_channel'] = $this->normalizePreviewCandidate($resolved['selected_channel'] ?? null);
$response['candidates'] = array_values(array_map(
fn (array $item) => $this->normalizePreviewCandidate($item),
(array) ($resolved['candidates'] ?? [])
));
} catch (Throwable $e) {
$response['reason'] = $e->getMessage() !== '' ? $e->getMessage() : '路由预览失败';
}
return $response;
}
private function normalizeBind(mixed $bind): ?array
{
$data = $this->supportService->normalizeModel($bind);
if ($data === null) {
return null;
}
$status = (int) ($data['status'] ?? 0);
return [
'merchant_group_id' => (int) ($data['merchant_group_id'] ?? 0),
'pay_type_id' => (int) ($data['pay_type_id'] ?? 0),
'poll_group_id' => (int) ($data['poll_group_id'] ?? 0),
'status' => $status,
'status_text' => (string) (CommonConstant::statusMap()[$status] ?? '未知'),
'remark' => (string) ($data['remark'] ?? ''),
'created_at' => $this->supportService->formatDateTime($data['created_at'] ?? null),
'updated_at' => $this->supportService->formatDateTime($data['updated_at'] ?? null),
];
}
private function normalizePollGroup(mixed $pollGroup): ?array
{
$data = $this->supportService->normalizeModel($pollGroup);
if ($data === null) {
return null;
}
$routeMode = (int) ($data['route_mode'] ?? 0);
$status = (int) ($data['status'] ?? 0);
return [
'id' => (int) ($data['id'] ?? 0),
'group_name' => (string) ($data['group_name'] ?? ''),
'pay_type_id' => (int) ($data['pay_type_id'] ?? 0),
'route_mode' => $routeMode,
'route_mode_text' => (string) (RouteConstant::routeModeMap()[$routeMode] ?? '未知'),
'status' => $status,
'status_text' => (string) (CommonConstant::statusMap()[$status] ?? '未知'),
'remark' => (string) ($data['remark'] ?? ''),
'created_at' => $this->supportService->formatDateTime($data['created_at'] ?? null),
'updated_at' => $this->supportService->formatDateTime($data['updated_at'] ?? null),
];
}
private function normalizePreviewCandidate(mixed $candidate): ?array
{
$data = is_array($candidate) ? $candidate : $this->supportService->normalizeModel($candidate);
if ($data === null) {
return null;
}
$channel = $this->supportService->normalizeModel($data['channel'] ?? null) ?? [];
$pollGroupChannel = $this->supportService->normalizeModel($data['poll_group_channel'] ?? null) ?? [];
$dailyStat = $this->supportService->normalizeModel($data['daily_stat'] ?? null) ?? [];
$channelMode = (int) ($channel['channel_mode'] ?? 0);
$status = (int) ($channel['status'] ?? 0);
$payTypeId = (int) ($channel['pay_type_id'] ?? 0);
return [
'channel_id' => (int) ($channel['id'] ?? 0),
'channel_name' => (string) ($channel['name'] ?? ''),
'pay_type_id' => $payTypeId,
'pay_type_name' => $this->supportService->paymentTypeName($payTypeId),
'channel_mode' => $channelMode,
'channel_mode_text' => (string) (RouteConstant::channelModeMap()[$channelMode] ?? '未知'),
'status' => $status,
'status_text' => (string) (CommonConstant::statusMap()[$status] ?? '未知'),
'plugin_code' => (string) ($channel['plugin_code'] ?? ''),
'sort_no' => (int) ($pollGroupChannel['sort_no'] ?? 0),
'weight' => (int) ($pollGroupChannel['weight'] ?? 0),
'is_default' => (int) ($pollGroupChannel['is_default'] ?? 0),
'health_score' => (int) ($dailyStat['health_score'] ?? 0),
'health_score_text' => (string) ($dailyStat['health_score'] ?? 0),
'success_rate_bp' => (int) ($dailyStat['success_rate_bp'] ?? 0),
'success_rate_text' => $this->supportService->formatRate((int) ($dailyStat['success_rate_bp'] ?? 0)),
'avg_latency_ms' => (int) ($dailyStat['avg_latency_ms'] ?? 0),
'avg_latency_text' => $this->supportService->formatLatency((int) ($dailyStat['avg_latency_ms'] ?? 0)),
'split_rate_bp' => (int) ($channel['split_rate_bp'] ?? 0),
'split_rate_text' => $this->supportService->formatRate((int) ($channel['split_rate_bp'] ?? 0)),
'cost_rate_bp' => (int) ($channel['cost_rate_bp'] ?? 0),
'cost_rate_text' => $this->supportService->formatRate((int) ($channel['cost_rate_bp'] ?? 0)),
'daily_limit_amount' => (int) ($channel['daily_limit_amount'] ?? 0),
'daily_limit_amount_text' => $this->supportService->formatAmountOrUnlimited((int) ($channel['daily_limit_amount'] ?? 0)),
'daily_limit_count' => (int) ($channel['daily_limit_count'] ?? 0),
'daily_limit_count_text' => $this->supportService->formatCountOrUnlimited((int) ($channel['daily_limit_count'] ?? 0)),
'min_amount' => (int) ($channel['min_amount'] ?? 0),
'min_amount_text' => $this->supportService->formatAmountOrUnlimited((int) ($channel['min_amount'] ?? 0)),
'max_amount' => (int) ($channel['max_amount'] ?? 0),
'max_amount_text' => $this->supportService->formatAmountOrUnlimited((int) ($channel['max_amount'] ?? 0)),
'remark' => (string) ($channel['remark'] ?? ''),
'created_at' => $this->supportService->formatDateTime($channel['created_at'] ?? null),
'updated_at' => $this->supportService->formatDateTime($channel['updated_at'] ?? null),
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
/**
* 商户后台基础页面服务门面。
*
* 仅保留控制器依赖的统一入口,具体能力拆到资料、通道、凭证和资金子服务。
*/
class MerchantPortalService extends BaseService
{
public function __construct(
protected MerchantPortalProfileService $profileService,
protected MerchantPortalChannelService $channelService,
protected MerchantPortalCredentialService $credentialService,
protected MerchantPortalFinanceService $financeService
) {
}
public function profile(int $merchantId): array
{
return $this->profileService->profile($merchantId);
}
public function updateProfile(int $merchantId, array $data): array
{
return $this->profileService->updateProfile($merchantId, $data);
}
public function changePassword(int $merchantId, array $data): array
{
return $this->profileService->changePassword($merchantId, $data);
}
public function myChannels(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->channelService->myChannels($filters, $merchantId, $page, $pageSize);
}
public function routePreview(int $merchantId, int $payTypeId, int $payAmount, string $statDate = ''): array
{
return $this->channelService->routePreview($merchantId, $payTypeId, $payAmount, $statDate);
}
public function apiCredential(int $merchantId): array
{
return $this->credentialService->apiCredential($merchantId);
}
public function issueCredential(int $merchantId): array
{
return $this->credentialService->issueCredential($merchantId);
}
public function settlementRecords(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->financeService->settlementRecords($filters, $merchantId, $page, $pageSize);
}
public function settlementRecordDetail(string $settleNo, int $merchantId): ?array
{
return $this->financeService->settlementRecordDetail($settleNo, $merchantId);
}
public function withdrawableBalance(int $merchantId): array
{
return $this->financeService->withdrawableBalance($merchantId);
}
public function balanceFlows(array $filters, int $merchantId, int $page, int $pageSize): array
{
return $this->financeService->balanceFlows($filters, $merchantId, $page, $pageSize);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\exception\ResourceNotFoundException;
use app\service\payment\settlement\SettlementOrderQueryService;
/**
* 商户门户清算服务。
*/
class MerchantPortalSettlementService extends BaseService
{
public function __construct(
protected MerchantPortalSupportService $supportService,
protected SettlementOrderQueryService $settlementOrderQueryService
) {
}
public function settlementRecords(array $filters, int $merchantId, int $page, int $pageSize): array
{
$paginator = $this->settlementOrderQueryService->paginate($filters, $page, $pageSize, $merchantId);
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'list' => $paginator->items(),
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'size' => $paginator->perPage(),
];
}
public function settlementRecordDetail(string $settleNo, int $merchantId): ?array
{
try {
$detail = $this->settlementOrderQueryService->detail($settleNo, $merchantId);
} catch (ResourceNotFoundException) {
return null;
}
return [
'merchant' => $this->supportService->merchantSummary($merchantId),
'settlement_order' => $detail['settlement_order'] ?? null,
'timeline' => $detail['timeline'] ?? [],
];
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace app\service\merchant\portal;
use app\common\base\BaseService;
use app\common\constant\AuthConstant;
use app\exception\ResourceNotFoundException;
use app\repository\merchant\base\MerchantRepository;
use app\service\merchant\MerchantService;
use app\service\payment\config\PaymentTypeService;
/**
* 商户门户公共支持服务。
*
* 统一承接商户门户里复用的商户摘要、支付方式和通用格式化能力。
*/
class MerchantPortalSupportService extends BaseService
{
public function __construct(
protected MerchantService $merchantService,
protected MerchantRepository $merchantRepository,
protected PaymentTypeService $paymentTypeService
) {
}
/**
* 当前商户基础资料摘要。
*/
public function merchantSummary(int $merchantId): array
{
$this->merchantService->ensureMerchantEnabled($merchantId);
$row = $this->merchantRepository->query()
->from('ma_merchant as m')
->leftJoin('ma_merchant_group as g', 'm.group_id', '=', 'g.id')
->select([
'm.id',
'm.merchant_no',
'm.merchant_name',
'm.merchant_short_name',
'm.merchant_type',
'm.group_id',
'm.risk_level',
'm.contact_name',
'm.contact_phone',
'm.contact_email',
'm.settlement_account_name',
'm.settlement_account_no',
'm.settlement_bank_name',
'm.settlement_bank_branch',
'm.status',
'm.last_login_at',
'm.last_login_ip',
'm.password_updated_at',
'm.remark',
'm.created_at',
'm.updated_at',
])
->selectRaw("COALESCE(g.group_name, '未分组') AS merchant_group_name")
->selectRaw("COALESCE(m.settlement_account_name, '') AS settlement_account_name_text")
->selectRaw("CASE WHEN m.settlement_account_no IS NULL OR m.settlement_account_no = '' THEN '' ELSE CONCAT(LEFT(m.settlement_account_no, 4), '****', RIGHT(m.settlement_account_no, 4)) END AS settlement_account_no_masked")
->selectRaw("COALESCE(m.settlement_bank_name, '') AS settlement_bank_name_text")
->selectRaw("COALESCE(m.settlement_bank_branch, '') AS settlement_bank_branch_text")
->selectRaw("CASE m.merchant_type WHEN 0 THEN '个人' WHEN 1 THEN '企业' ELSE '其他' END AS merchant_type_text")
->selectRaw("CASE m.risk_level WHEN 0 THEN '低' WHEN 1 THEN '中' WHEN 2 THEN '高' ELSE '未知' END AS risk_level_text")
->selectRaw("CASE m.status WHEN 0 THEN '停用' WHEN 1 THEN '启用' ELSE '未知' END AS status_text")
->where('m.id', $merchantId)
->first();
if (!$row) {
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
}
return [
'id' => (int) $row->id,
'merchant_id' => (int) $row->id,
'merchant_no' => (string) $row->merchant_no,
'merchant_name' => (string) $row->merchant_name,
'merchant_short_name' => (string) $row->merchant_short_name,
'merchant_type' => (int) $row->merchant_type,
'merchant_type_text' => (string) $row->merchant_type_text,
'merchant_group_id' => (int) $row->group_id,
'merchant_group_name' => (string) $row->merchant_group_name,
'risk_level' => (int) $row->risk_level,
'risk_level_text' => (string) $row->risk_level_text,
'contact_name' => (string) $row->contact_name,
'contact_phone' => (string) $row->contact_phone,
'contact_email' => (string) $row->contact_email,
'settlement_account_name' => (string) $row->settlement_account_name,
'settlement_account_no' => (string) $row->settlement_account_no,
'settlement_bank_name' => (string) $row->settlement_bank_name,
'settlement_bank_branch' => (string) $row->settlement_bank_branch,
'settlement_account_name_text' => (string) $row->settlement_account_name_text,
'settlement_account_no_masked' => (string) $row->settlement_account_no_masked,
'settlement_bank_name_text' => (string) $row->settlement_bank_name_text,
'settlement_bank_branch_text' => (string) $row->settlement_bank_branch_text,
'status' => (int) $row->status,
'status_text' => (string) $row->status_text,
'last_login_at' => $this->formatDateTime($row->last_login_at ?? null),
'last_login_ip' => (string) ($row->last_login_ip ?? ''),
'password_updated_at' => $this->formatDateTime($row->password_updated_at ?? null),
'remark' => (string) $row->remark,
'created_at' => $this->formatDateTime($row->created_at ?? null),
'updated_at' => $this->formatDateTime($row->updated_at ?? null),
];
}
/**
* 启用的支付方式选项。
*/
public function enabledPayTypeOptions(): array
{
return $this->paymentTypeService->enabledOptions();
}
/**
* 根据支付方式 ID 获取名称。
*/
public function paymentTypeName(int $payTypeId): string
{
foreach ($this->paymentTypeService->enabledOptions() as $option) {
if ((int) ($option['value'] ?? 0) === $payTypeId) {
return (string) ($option['label'] ?? '');
}
}
return $payTypeId > 0 ? '未知' : '';
}
/**
* 格式化金额,单位为元。
*/
public function formatAmount(int $amount): string
{
return parent::formatAmount($amount);
}
/**
* 格式化金额0 时显示不限。
*/
public function formatAmountOrUnlimited(int $amount): string
{
return parent::formatAmountOrUnlimited($amount);
}
/**
* 格式化次数0 时显示不限。
*/
public function formatCountOrUnlimited(int $count): string
{
return parent::formatCountOrUnlimited($count);
}
/**
* 格式化费率,单位为百分点。
*/
public function formatRate(int $basisPoints): string
{
return parent::formatRate($basisPoints);
}
/**
* 格式化延迟。
*/
public function formatLatency(int $latencyMs): string
{
return parent::formatLatency($latencyMs);
}
/**
* 格式化日期时间。
*/
public function formatDateTime(mixed $value, string $emptyText = ''): string
{
return parent::formatDateTime($value, $emptyText);
}
/**
* 归一化模型对象,兼容模型和数组。
*/
public function normalizeModel(mixed $value): ?array
{
return parent::normalizeModel($value);
}
/**
* 隐藏接口凭证明文。
*/
public function maskCredentialValue(string $credentialValue, bool $maskShortValue = true): string
{
return parent::maskCredentialValue($credentialValue, $maskShortValue);
}
/**
* 签名类型文案。
*/
public function signTypeText(int $signType): string
{
return $this->textFromMap($signType, AuthConstant::signTypeMap());
}
}