mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-28 21:14:32 +08:00
重构初始化
This commit is contained in:
269
app/service/merchant/MerchantCommandService.php
Normal file
269
app/service/merchant/MerchantCommandService.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\model\merchant\MerchantGroup;
|
||||
use app\repository\account\balance\MerchantAccountRepository;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
use app\repository\merchant\base\MerchantGroupRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
use app\repository\payment\config\PaymentChannelRepository;
|
||||
use app\repository\payment\settlement\SettlementOrderRepository;
|
||||
use app\repository\payment\trade\BizOrderRepository;
|
||||
use app\repository\payment\trade\RefundOrderRepository;
|
||||
use app\service\account\funds\MerchantAccountService;
|
||||
use app\service\merchant\security\MerchantApiCredentialService;
|
||||
|
||||
/**
|
||||
* 商户命令服务。
|
||||
*
|
||||
* 负责商户创建、更新、删除、密码和登录元数据这类写操作。
|
||||
*/
|
||||
class MerchantCommandService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantRepository $merchantRepository,
|
||||
protected MerchantGroupRepository $merchantGroupRepository,
|
||||
protected MerchantQueryService $merchantQueryService,
|
||||
protected MerchantApiCredentialService $merchantApiCredentialService,
|
||||
protected MerchantAccountRepository $merchantAccountRepository,
|
||||
protected MerchantApiCredentialRepository $merchantApiCredentialRepository,
|
||||
protected PaymentChannelRepository $paymentChannelRepository,
|
||||
protected BizOrderRepository $bizOrderRepository,
|
||||
protected RefundOrderRepository $refundOrderRepository,
|
||||
protected SettlementOrderRepository $settlementOrderRepository,
|
||||
protected MerchantAccountService $merchantAccountService
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(array $data): Merchant
|
||||
{
|
||||
return $this->transaction(function () use ($data) {
|
||||
$merchantName = trim((string) ($data['merchant_name'] ?? ''));
|
||||
$contactName = trim((string) ($data['contact_name'] ?? ''));
|
||||
$contactPhone = trim((string) ($data['contact_phone'] ?? ''));
|
||||
$groupId = (int) ($data['group_id'] ?? 0);
|
||||
if ($merchantName === '') {
|
||||
throw new ValidationException('商户名称不能为空');
|
||||
}
|
||||
if ($groupId <= 0) {
|
||||
throw new ValidationException('请选择商户分组');
|
||||
}
|
||||
if ($contactName === '') {
|
||||
throw new ValidationException('联系人不能为空');
|
||||
}
|
||||
if ($contactPhone === '') {
|
||||
throw new ValidationException('联系电话不能为空');
|
||||
}
|
||||
if ($groupId > 0) {
|
||||
$this->ensureMerchantGroupEnabled($groupId);
|
||||
}
|
||||
|
||||
$merchantNo = $this->generateMerchantNo();
|
||||
$plainPassword = $this->generateTemporaryPassword();
|
||||
|
||||
$merchant = $this->merchantRepository->create([
|
||||
'merchant_no' => $merchantNo,
|
||||
'password_hash' => password_hash($plainPassword, PASSWORD_DEFAULT),
|
||||
'merchant_name' => $merchantName,
|
||||
'merchant_short_name' => trim((string) ($data['merchant_short_name'] ?? '')),
|
||||
'merchant_type' => (int) ($data['merchant_type'] ?? 0),
|
||||
'group_id' => $groupId,
|
||||
'risk_level' => (int) ($data['risk_level'] ?? 0),
|
||||
'contact_name' => $contactName,
|
||||
'contact_phone' => $contactPhone,
|
||||
'contact_email' => trim((string) ($data['contact_email'] ?? '')),
|
||||
'settlement_account_name' => trim((string) ($data['settlement_account_name'] ?? '')),
|
||||
'settlement_account_no' => trim((string) ($data['settlement_account_no'] ?? '')),
|
||||
'settlement_bank_name' => trim((string) ($data['settlement_bank_name'] ?? '')),
|
||||
'settlement_bank_branch' => trim((string) ($data['settlement_bank_branch'] ?? '')),
|
||||
'status' => (int) ($data['status'] ?? CommonConstant::STATUS_ENABLED),
|
||||
'password_updated_at' => $this->now(),
|
||||
'remark' => trim((string) ($data['remark'] ?? '')),
|
||||
]);
|
||||
|
||||
$merchant->plain_password = $plainPassword;
|
||||
|
||||
$this->merchantAccountService->ensureAccountInCurrentTransaction((int) $merchant->id);
|
||||
|
||||
return $merchant;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(int $merchantId, array $data): ?Merchant
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$groupId = array_key_exists('group_id', $data) ? (int) $data['group_id'] : (int) $merchant->group_id;
|
||||
if ($groupId > 0) {
|
||||
$this->ensureMerchantGroupEnabled($groupId);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'merchant_name' => (string) ($data['merchant_name'] ?? $merchant->merchant_name),
|
||||
'merchant_short_name' => (string) ($data['merchant_short_name'] ?? $merchant->merchant_short_name),
|
||||
'merchant_type' => (int) ($data['merchant_type'] ?? $merchant->merchant_type),
|
||||
'group_id' => $groupId,
|
||||
'risk_level' => (int) ($data['risk_level'] ?? $merchant->risk_level),
|
||||
'contact_name' => (string) ($data['contact_name'] ?? $merchant->contact_name),
|
||||
'contact_phone' => (string) ($data['contact_phone'] ?? $merchant->contact_phone),
|
||||
'contact_email' => (string) ($data['contact_email'] ?? $merchant->contact_email),
|
||||
'settlement_account_name' => (string) ($data['settlement_account_name'] ?? $merchant->settlement_account_name),
|
||||
'settlement_account_no' => (string) ($data['settlement_account_no'] ?? $merchant->settlement_account_no),
|
||||
'settlement_bank_name' => (string) ($data['settlement_bank_name'] ?? $merchant->settlement_bank_name),
|
||||
'settlement_bank_branch' => (string) ($data['settlement_bank_branch'] ?? $merchant->settlement_bank_branch),
|
||||
'status' => (int) ($data['status'] ?? $merchant->status),
|
||||
'remark' => (string) ($data['remark'] ?? $merchant->remark),
|
||||
];
|
||||
|
||||
if (!$this->merchantRepository->updateById($merchantId, $payload)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->merchantRepository->find($merchantId);
|
||||
}
|
||||
|
||||
public function delete(int $merchantId): bool
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$dependencies = [
|
||||
['count' => $this->paymentChannelRepository->countByMerchantId($merchantId), 'message' => '已配置支付通道'],
|
||||
['count' => $this->bizOrderRepository->countByMerchantId($merchantId), 'message' => '已存在支付订单'],
|
||||
['count' => $this->refundOrderRepository->countByMerchantId($merchantId), 'message' => '已存在退款订单'],
|
||||
['count' => $this->settlementOrderRepository->countByMerchantId($merchantId), 'message' => '已存在清算记录'],
|
||||
['count' => $this->merchantAccountRepository->countByMerchantId($merchantId), 'message' => '已存在资金账户'],
|
||||
['count' => $this->merchantApiCredentialRepository->countByMerchantId($merchantId), 'message' => '已开通接口凭证'],
|
||||
];
|
||||
|
||||
foreach ($dependencies as $dependency) {
|
||||
if ((int) $dependency['count'] > 0) {
|
||||
throw new BusinessStateException("当前商户{$dependency['message']},请先清理关联数据后再删除", [
|
||||
'merchant_id' => $merchantId,
|
||||
'message' => $dependency['message'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->merchantRepository->deleteById($merchantId);
|
||||
}
|
||||
|
||||
public function resetPassword(int $merchantId, string $password): Merchant
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$this->merchantRepository->updateById($merchantId, [
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'password_updated_at' => $this->now(),
|
||||
]);
|
||||
|
||||
return $this->merchantRepository->find($merchantId);
|
||||
}
|
||||
|
||||
public function verifyPassword(Merchant $merchant, string $password): bool
|
||||
{
|
||||
return $password !== '' && password_verify($password, (string) $merchant->password_hash);
|
||||
}
|
||||
|
||||
public function touchLoginMeta(int $merchantId, string $ip = ''): void
|
||||
{
|
||||
$this->merchantRepository->updateById($merchantId, [
|
||||
'last_login_at' => $this->now(),
|
||||
'last_login_ip' => trim($ip),
|
||||
]);
|
||||
}
|
||||
|
||||
public function issueCredential(int $merchantId): array
|
||||
{
|
||||
$merchant = $this->merchantQueryService->findById($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$credentialValue = $this->merchantApiCredentialService->issueCredential($merchantId);
|
||||
$credential = $this->merchantApiCredentialService->findByMerchantId($merchantId);
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential_value' => $credentialValue,
|
||||
'credential' => $credential,
|
||||
];
|
||||
}
|
||||
|
||||
public function findEnabledMerchantByNo(string $merchantNo): Merchant
|
||||
{
|
||||
$merchant = $this->merchantRepository->findByMerchantNo($merchantNo);
|
||||
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_no' => $merchantNo]);
|
||||
}
|
||||
|
||||
if ((int) $merchant->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new BusinessStateException('商户已禁用', ['merchant_no' => $merchantNo]);
|
||||
}
|
||||
|
||||
return $merchant;
|
||||
}
|
||||
|
||||
public function ensureMerchantEnabled(int $merchantId): Merchant
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
if ((int) $merchant->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new BusinessStateException('商户已禁用', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
return $merchant;
|
||||
}
|
||||
|
||||
public function ensureMerchantGroupEnabled(int $groupId): MerchantGroup
|
||||
{
|
||||
$group = $this->merchantGroupRepository->find($groupId);
|
||||
|
||||
if (!$group) {
|
||||
throw new ResourceNotFoundException('商户分组不存在', ['merchant_group_id' => $groupId]);
|
||||
}
|
||||
|
||||
if ((int) $group->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new BusinessStateException('商户分组已禁用', ['merchant_group_id' => $groupId]);
|
||||
}
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
private function generateMerchantNo(): string
|
||||
{
|
||||
do {
|
||||
$merchantNo = $this->generateNo('M');
|
||||
} while ($this->merchantRepository->findByMerchantNo($merchantNo) !== null);
|
||||
|
||||
return $merchantNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成商户初始临时密码。
|
||||
*/
|
||||
private function generateTemporaryPassword(): string
|
||||
{
|
||||
return bin2hex(random_bytes(8));
|
||||
}
|
||||
}
|
||||
130
app/service/merchant/MerchantOverviewQueryService.php
Normal file
130
app/service/merchant/MerchantOverviewQueryService.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\constant\RouteConstant;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\repository\account\balance\MerchantAccountRepository;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
use app\repository\payment\config\PaymentChannelRepository;
|
||||
use app\repository\payment\config\PaymentPollGroupBindRepository;
|
||||
use app\repository\payment\settlement\SettlementOrderRepository;
|
||||
use app\repository\payment\trade\PayOrderRepository;
|
||||
|
||||
/**
|
||||
* 商户总览查询服务。
|
||||
*
|
||||
* 负责商户资料、接口凭证、资金、路由、通道和最近交易的总览拼装。
|
||||
*/
|
||||
class MerchantOverviewQueryService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantQueryService $merchantQueryService,
|
||||
protected MerchantAccountRepository $merchantAccountRepository,
|
||||
protected MerchantApiCredentialRepository $merchantApiCredentialRepository,
|
||||
protected PaymentChannelRepository $paymentChannelRepository,
|
||||
protected PaymentPollGroupBindRepository $paymentPollGroupBindRepository,
|
||||
protected PayOrderRepository $payOrderRepository,
|
||||
protected SettlementOrderRepository $settlementOrderRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户总览。
|
||||
*/
|
||||
public function overview(int $merchantId): array
|
||||
{
|
||||
$merchant = $this->merchantQueryService->findById($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$account = $this->merchantAccountRepository->findByMerchantId($merchantId);
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
$channelSummary = $this->paymentChannelRepository->summaryByMerchantId($merchantId);
|
||||
|
||||
$bindSummary = [];
|
||||
if ((int) $merchant->group_id > 0) {
|
||||
$bindSummary = $this->paymentPollGroupBindRepository
|
||||
->listSummaryByMerchantGroupId((int) $merchant->group_id)
|
||||
->map(function ($row) {
|
||||
$row->status_text = (int) $row->status === CommonConstant::STATUS_ENABLED ? '启用' : '禁用';
|
||||
$routeModeMap = RouteConstant::routeModeMap();
|
||||
$row->route_mode_text = (string) ($routeModeMap[(int) ($row->route_mode ?? -1)] ?? '未知');
|
||||
|
||||
return $row;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
$recentPayOrders = $this->payOrderRepository
|
||||
->recentByMerchantId($merchantId, 5)
|
||||
->map(function ($row) {
|
||||
$row->pay_amount_text = $this->formatAmount((int) $row->pay_amount);
|
||||
$row->status_text = match ((int) $row->status) {
|
||||
0 => '待创建',
|
||||
1 => '支付中',
|
||||
2 => '成功',
|
||||
3 => '失败',
|
||||
4 => '关闭',
|
||||
5 => '超时',
|
||||
default => '未知',
|
||||
};
|
||||
|
||||
return $row;
|
||||
})
|
||||
->all();
|
||||
|
||||
$recentSettlements = $this->settlementOrderRepository
|
||||
->recentByMerchantId($merchantId, 5)
|
||||
->map(function ($row) {
|
||||
$row->net_amount_text = $this->formatAmount((int) $row->net_amount);
|
||||
$row->status_text = match ((int) $row->status) {
|
||||
0 => '待处理',
|
||||
1 => '处理中',
|
||||
2 => '成功',
|
||||
3 => '失败',
|
||||
4 => '已冲正',
|
||||
default => '未知',
|
||||
};
|
||||
|
||||
return $row;
|
||||
})
|
||||
->all();
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'access' => [
|
||||
'login_identity' => (string) $merchant->merchant_no,
|
||||
'login_mode_text' => '商户号 + 密码',
|
||||
'has_credential' => $credential !== null,
|
||||
'credential_enabled' => (int) ($credential->status ?? 0) === CommonConstant::STATUS_ENABLED,
|
||||
'credential_status_text' => (int) ($credential->status ?? 0) === CommonConstant::STATUS_ENABLED ? '已开通' : '未开通',
|
||||
'sign_type_text' => $this->textFromMap((int) ($credential->sign_type ?? 0), \app\common\constant\AuthConstant::signTypeMap()),
|
||||
'credential_last_used_at' => $this->formatDateTime($credential->last_used_at ?? null),
|
||||
],
|
||||
'route' => [
|
||||
'merchant_group_id' => (int) $merchant->group_id,
|
||||
'merchant_group_name' => (string) ($merchant->group_name ?? '未分组'),
|
||||
'bind_count' => count($bindSummary),
|
||||
'binds' => $bindSummary,
|
||||
],
|
||||
'funds' => [
|
||||
'has_account' => $account !== null,
|
||||
'available_balance' => (int) ($account->available_balance ?? 0),
|
||||
'available_balance_text' => $this->formatAmount((int) ($account->available_balance ?? 0)),
|
||||
'frozen_balance' => (int) ($account->frozen_balance ?? 0),
|
||||
'frozen_balance_text' => $this->formatAmount((int) ($account->frozen_balance ?? 0)),
|
||||
],
|
||||
'channels' => [
|
||||
'total_count' => (int) ($channelSummary->total_count ?? 0),
|
||||
'enabled_count' => (int) ($channelSummary->enabled_count ?? 0),
|
||||
'self_count' => (int) ($channelSummary->self_count ?? 0),
|
||||
],
|
||||
'recent_pay_orders' => $recentPayOrders,
|
||||
'recent_settlements' => $recentSettlements,
|
||||
];
|
||||
}
|
||||
}
|
||||
235
app/service/merchant/MerchantQueryService.php
Normal file
235
app/service/merchant/MerchantQueryService.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\model\merchant\MerchantPolicy;
|
||||
use app\repository\merchant\base\MerchantGroupRepository;
|
||||
use app\repository\merchant\base\MerchantPolicyRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
|
||||
/**
|
||||
* 商户查询服务。
|
||||
*
|
||||
* 负责商户列表、详情和总览这类只读查询。
|
||||
*/
|
||||
class MerchantQueryService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantRepository $merchantRepository,
|
||||
protected MerchantGroupRepository $merchantGroupRepository,
|
||||
protected MerchantPolicyRepository $merchantPolicyRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
$query = $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 group_name")
|
||||
->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");
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_short_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.contact_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.contact_phone', 'like', '%' . $keyword . '%')
|
||||
->orWhere('g.group_name', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$groupId = trim((string) ($filters['group_id'] ?? ''));
|
||||
if ($groupId !== '') {
|
||||
$query->where('m.group_id', (int) $groupId);
|
||||
}
|
||||
|
||||
$status = trim((string) ($filters['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$query->where('m.status', (int) $status);
|
||||
}
|
||||
|
||||
$merchantType = trim((string) ($filters['merchant_type'] ?? ''));
|
||||
if ($merchantType !== '') {
|
||||
$query->where('m.merchant_type', (int) $merchantType);
|
||||
}
|
||||
|
||||
$riskLevel = trim((string) ($filters['risk_level'] ?? ''));
|
||||
if ($riskLevel !== '') {
|
||||
$query->where('m.risk_level', (int) $riskLevel);
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderByDesc('m.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
}
|
||||
|
||||
public function paginateWithGroupOptions(array $filters = [], int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
$paginator = $this->paginate($filters, $page, $pageSize);
|
||||
|
||||
return [
|
||||
'list' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage(),
|
||||
'groups' => $this->enabledGroupOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
public function enabledOptions(): array
|
||||
{
|
||||
return $this->merchantRepository->enabledList(['id', 'merchant_no', 'merchant_name'])
|
||||
->map(function (Merchant $merchant): array {
|
||||
return [
|
||||
'label' => sprintf('%s(%s)', (string) $merchant->merchant_name, (string) $merchant->merchant_no),
|
||||
'value' => (int) $merchant->id,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function enabledGroupOptions(): array
|
||||
{
|
||||
return $this->merchantGroupRepository->enabledList(['id', 'group_name'])
|
||||
->map(static function ($group): array {
|
||||
return [
|
||||
'label' => (string) $group->group_name,
|
||||
'value' => (int) $group->id,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
$query = $this->merchantRepository->query()
|
||||
->from('ma_merchant as m')
|
||||
->where('m.status', CommonConstant::STATUS_ENABLED)
|
||||
->select([
|
||||
'm.id',
|
||||
'm.merchant_no',
|
||||
'm.merchant_name',
|
||||
'm.merchant_short_name',
|
||||
]);
|
||||
|
||||
$ids = $this->normalizeIds($filters['ids'] ?? []);
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
|
||||
if (!empty($ids)) {
|
||||
$query->whereIn('m.id', $ids);
|
||||
} elseif ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_short_name', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('m.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
return [
|
||||
'list' => collect($paginator->items())
|
||||
->map(function ($merchant): array {
|
||||
return [
|
||||
'label' => sprintf('%s(%s)', (string) $merchant->merchant_name, (string) $merchant->merchant_no),
|
||||
'value' => (int) $merchant->id,
|
||||
'merchant_no' => (string) $merchant->merchant_no,
|
||||
'merchant_name' => (string) $merchant->merchant_name,
|
||||
'merchant_short_name' => (string) ($merchant->merchant_short_name ?? ''),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all(),
|
||||
'total' => (int) $paginator->total(),
|
||||
'page' => (int) $paginator->currentPage(),
|
||||
'size' => (int) $paginator->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
public function findById(int $merchantId): ?object
|
||||
{
|
||||
return $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 group_name")
|
||||
->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();
|
||||
}
|
||||
|
||||
public function findPolicy(int $merchantId): ?MerchantPolicy
|
||||
{
|
||||
return $this->merchantPolicyRepository->findByMerchantId($merchantId);
|
||||
}
|
||||
|
||||
private function normalizeIds(array|string|int $ids): array
|
||||
{
|
||||
if (is_string($ids)) {
|
||||
$ids = array_filter(array_map('trim', explode(',', $ids)));
|
||||
} elseif (!is_array($ids)) {
|
||||
$ids = [$ids];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(static fn ($id) => (int) $id, $ids), static fn ($id) => $id > 0));
|
||||
}
|
||||
}
|
||||
129
app/service/merchant/MerchantService.php
Normal file
129
app/service/merchant/MerchantService.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\model\merchant\MerchantGroup;
|
||||
use app\model\merchant\MerchantPolicy;
|
||||
|
||||
/**
|
||||
* 商户基础服务门面。
|
||||
*
|
||||
* 仅保留现有控制器和其他服务依赖的统一入口。
|
||||
*/
|
||||
class MerchantService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantQueryService $queryService,
|
||||
protected MerchantCommandService $commandService,
|
||||
protected MerchantOverviewQueryService $overviewQueryService
|
||||
) {
|
||||
}
|
||||
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
return $this->queryService->paginate($filters, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function paginateWithGroupOptions(array $filters = [], int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
return $this->queryService->paginateWithGroupOptions($filters, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function enabledOptions(): array
|
||||
{
|
||||
return $this->queryService->enabledOptions();
|
||||
}
|
||||
|
||||
public function searchOptions(array $filters = [], int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
return $this->queryService->searchOptions($filters, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function findById(int $merchantId): ?object
|
||||
{
|
||||
return $this->queryService->findById($merchantId);
|
||||
}
|
||||
|
||||
public function create(array $data): Merchant
|
||||
{
|
||||
return $this->commandService->create($data);
|
||||
}
|
||||
|
||||
public function createWithDetail(array $data): ?object
|
||||
{
|
||||
$merchant = $this->create($data);
|
||||
$detail = $this->findById((int) $merchant->id);
|
||||
if ($detail && isset($merchant->plain_password)) {
|
||||
$detail->plain_password = (string) $merchant->plain_password;
|
||||
}
|
||||
|
||||
return $detail ?? $merchant;
|
||||
}
|
||||
|
||||
public function update(int $merchantId, array $data): ?Merchant
|
||||
{
|
||||
return $this->commandService->update($merchantId, $data);
|
||||
}
|
||||
|
||||
public function updateWithDetail(int $merchantId, array $data): ?object
|
||||
{
|
||||
$merchant = $this->update($merchantId, $data);
|
||||
if (!$merchant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findById($merchantId);
|
||||
}
|
||||
|
||||
public function delete(int $merchantId): bool
|
||||
{
|
||||
return $this->commandService->delete($merchantId);
|
||||
}
|
||||
|
||||
public function resetPassword(int $merchantId, string $password): Merchant
|
||||
{
|
||||
return $this->commandService->resetPassword($merchantId, $password);
|
||||
}
|
||||
|
||||
public function verifyPassword(Merchant $merchant, string $password): bool
|
||||
{
|
||||
return $this->commandService->verifyPassword($merchant, $password);
|
||||
}
|
||||
|
||||
public function touchLoginMeta(int $merchantId, string $ip = ''): void
|
||||
{
|
||||
$this->commandService->touchLoginMeta($merchantId, $ip);
|
||||
}
|
||||
|
||||
public function issueCredential(int $merchantId): array
|
||||
{
|
||||
return $this->commandService->issueCredential($merchantId);
|
||||
}
|
||||
|
||||
public function overview(int $merchantId): array
|
||||
{
|
||||
return $this->overviewQueryService->overview($merchantId);
|
||||
}
|
||||
|
||||
public function findEnabledMerchantByNo(string $merchantNo): Merchant
|
||||
{
|
||||
return $this->commandService->findEnabledMerchantByNo($merchantNo);
|
||||
}
|
||||
|
||||
public function ensureMerchantEnabled(int $merchantId): Merchant
|
||||
{
|
||||
return $this->commandService->ensureMerchantEnabled($merchantId);
|
||||
}
|
||||
|
||||
public function ensureMerchantGroupEnabled(int $groupId): MerchantGroup
|
||||
{
|
||||
return $this->commandService->ensureMerchantGroupEnabled($groupId);
|
||||
}
|
||||
|
||||
public function findPolicy(int $merchantId): ?MerchantPolicy
|
||||
{
|
||||
return $this->queryService->findPolicy($merchantId);
|
||||
}
|
||||
}
|
||||
181
app/service/merchant/auth/MerchantAuthService.php
Normal file
181
app/service/merchant/auth/MerchantAuthService.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\auth;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\common\util\JwtTokenManager;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
use app\service\merchant\portal\MerchantPortalSupportService;
|
||||
|
||||
/**
|
||||
* 商户认证服务。
|
||||
*/
|
||||
class MerchantAuthService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantRepository $merchantRepository,
|
||||
protected MerchantApiCredentialRepository $merchantApiCredentialRepository,
|
||||
protected MerchantPortalSupportService $merchantPortalSupportService,
|
||||
protected JwtTokenManager $jwtTokenManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录商户的资料。
|
||||
*/
|
||||
public function profile(int $merchantId, string $merchantNo = ''): array
|
||||
{
|
||||
$merchant = $this->merchantPortalSupportService->merchantSummary($merchantId);
|
||||
$credential = $merchantId > 0 ? $this->merchantApiCredentialRepository->findByMerchantId($merchantId) : null;
|
||||
|
||||
$isCredentialEnabled = (int) ($credential->status ?? 0) === 1;
|
||||
$user = [
|
||||
'id' => $merchantId,
|
||||
'deptId' => (string) ($merchant['merchant_group_id'] ?? 0),
|
||||
'deptName' => (string) ($merchant['merchant_group_name'] ?? '未分组'),
|
||||
'userName' => (string) ($merchant['merchant_no'] !== '' ? $merchant['merchant_no'] : trim($merchantNo)),
|
||||
'nickName' => (string) ($merchant['merchant_name'] ?? '商户账号'),
|
||||
'email' => (string) ($merchant['contact_email'] ?? ''),
|
||||
'phone' => (string) ($merchant['contact_phone'] ?? ''),
|
||||
'sex' => 2,
|
||||
'avatar' => '',
|
||||
'status' => (int) ($merchant['status'] ?? 1),
|
||||
'description' => '商户主体账号(商户号 + 密码)',
|
||||
'roles' => [
|
||||
[
|
||||
'code' => 'common',
|
||||
'name' => '普通用户',
|
||||
'admin' => false,
|
||||
'disabled' => false,
|
||||
],
|
||||
],
|
||||
'loginIp' => (string) ($merchant['last_login_ip'] ?? ''),
|
||||
'loginDate' => (string) ($merchant['last_login_at'] ?? ''),
|
||||
'createBy' => '系统',
|
||||
'createTime' => (string) ($merchant['created_at'] ?? ''),
|
||||
'updateBy' => null,
|
||||
'updateTime' => (string) ($merchant['updated_at'] ?? ''),
|
||||
'admin' => false,
|
||||
'credential_status' => (int) ($credential->status ?? 0),
|
||||
'credential_status_text' => $isCredentialEnabled ? '已开通' : '未开通',
|
||||
'credential_last_used_at' => (string) ($credential->last_used_at ?? ''),
|
||||
'password_updated_at' => (string) ($merchant['password_updated_at'] ?? ''),
|
||||
];
|
||||
|
||||
return [
|
||||
'merchant_id' => $merchantId,
|
||||
'merchant_no' => (string) ($merchant['merchant_no'] !== '' ? $merchant['merchant_no'] : trim($merchantNo)),
|
||||
'merchant' => $merchant,
|
||||
'user' => $user,
|
||||
'roles' => ['common'],
|
||||
'permissions' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户登录 token,并返回商户与登录态信息。
|
||||
*/
|
||||
public function authenticateToken(string $token, string $ip = '', string $userAgent = ''): ?array
|
||||
{
|
||||
$result = $this->jwtTokenManager->verify('merchant', $token, $ip, $userAgent);
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$merchantId = (int) ($result['session']['merchant_id'] ?? $result['claims']['merchant_id'] ?? 0);
|
||||
if ($merchantId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Merchant|null $merchant */
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant || (int) $merchant->status !== CommonConstant::STATUS_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential' => $credential,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户登录凭证并签发 JWT。
|
||||
*/
|
||||
public function authenticateCredentials(string $merchantNo, string $password, string $ip = '', string $userAgent = ''): array
|
||||
{
|
||||
$merchantNo = trim($merchantNo);
|
||||
$password = trim($password);
|
||||
if ($merchantNo === '' || $password === '') {
|
||||
throw new ValidationException('商户号或密码错误');
|
||||
}
|
||||
|
||||
/** @var Merchant|null $merchant */
|
||||
$merchant = $this->merchantRepository->findByMerchantNo($merchantNo);
|
||||
if (!$merchant || (int) $merchant->status !== CommonConstant::STATUS_ENABLED) {
|
||||
throw new ValidationException('商户号或密码错误');
|
||||
}
|
||||
|
||||
if (!password_verify($password, (string) $merchant->password_hash)) {
|
||||
throw new ValidationException('商户号或密码错误');
|
||||
}
|
||||
|
||||
$this->merchantRepository->updateById((int) $merchant->id, [
|
||||
'last_login_at' => $this->now(),
|
||||
'last_login_ip' => trim($ip),
|
||||
]);
|
||||
|
||||
return $this->issueToken((int) $merchant->id, 86400, $ip, $userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销当前商户登录 token。
|
||||
*/
|
||||
public function revokeToken(string $token): bool
|
||||
{
|
||||
return $this->jwtTokenManager->revoke('merchant', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 签发新的商户登录 token。
|
||||
*/
|
||||
public function issueToken(int $merchantId, int $ttlSeconds = 86400, string $ip = '', string $userAgent = ''): array
|
||||
{
|
||||
/** @var Merchant|null $merchant */
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ValidationException('商户不存在');
|
||||
}
|
||||
|
||||
$credential = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
|
||||
$issued = $this->jwtTokenManager->issue('merchant', [
|
||||
'sub' => (string) $merchantId,
|
||||
'merchant_id' => (int) $merchant->id,
|
||||
'merchant_no' => (string) $merchant->merchant_no,
|
||||
], [
|
||||
'merchant_id' => (int) $merchant->id,
|
||||
'merchant_no' => (string) $merchant->merchant_no,
|
||||
'last_login_ip' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
], $ttlSeconds);
|
||||
|
||||
return [
|
||||
'token' => $issued['token'],
|
||||
'expires_in' => $issued['expires_in'],
|
||||
'merchant' => $merchant,
|
||||
'credential' => $credential ? [
|
||||
'status' => (int) ($credential->status ?? 0),
|
||||
'sign_type' => (int) ($credential->sign_type ?? 0),
|
||||
'last_used_at' => $credential->last_used_at,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
120
app/service/merchant/group/MerchantGroupService.php
Normal file
120
app/service/merchant/group/MerchantGroupService.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\group;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\MerchantGroup;
|
||||
use app\repository\merchant\base\MerchantGroupRepository;
|
||||
|
||||
/**
|
||||
* 商户分组管理服务。
|
||||
*/
|
||||
class MerchantGroupService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantGroupRepository $merchantGroupRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用中的商户分组选项。
|
||||
*
|
||||
* 前端筛选框直接使用 `label / value` 结构即可。
|
||||
*/
|
||||
public function enabledOptions(): array
|
||||
{
|
||||
return $this->merchantGroupRepository->enabledList(['id', 'group_name'])
|
||||
->map(function (MerchantGroup $group): array {
|
||||
return [
|
||||
'label' => (string) $group->group_name,
|
||||
'value' => (int) $group->id,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询商户分组。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
$query = $this->merchantGroupRepository->query();
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where('group_name', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$groupName = trim((string) ($filters['group_name'] ?? ''));
|
||||
if ($groupName !== '') {
|
||||
$query->where('group_name', 'like', '%' . $groupName . '%');
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $filters) && $filters['status'] !== '') {
|
||||
$query->where('status', (int) $filters['status']);
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderByDesc('id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询商户分组。
|
||||
*/
|
||||
public function findById(int $id): ?MerchantGroup
|
||||
{
|
||||
return $this->merchantGroupRepository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增商户分组。
|
||||
*/
|
||||
public function create(array $data): MerchantGroup
|
||||
{
|
||||
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''));
|
||||
return $this->merchantGroupRepository->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商户分组。
|
||||
*/
|
||||
public function update(int $id, array $data): ?MerchantGroup
|
||||
{
|
||||
$this->assertGroupNameUnique((string) ($data['group_name'] ?? ''), $id);
|
||||
if (!$this->merchantGroupRepository->updateById($id, $data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->merchantGroupRepository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户分组。
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
return $this->merchantGroupRepository->deleteById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验商户分组名称唯一。
|
||||
*/
|
||||
private function assertGroupNameUnique(string $groupName, int $ignoreId = 0): void
|
||||
{
|
||||
$groupName = trim($groupName);
|
||||
if ($groupName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->merchantGroupRepository->existsByGroupName($groupName, $ignoreId)) {
|
||||
throw new ValidationException('分组名称已存在');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
app/service/merchant/policy/MerchantPolicyService.php
Normal file
161
app/service/merchant/policy/MerchantPolicyService.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\policy;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\model\merchant\MerchantPolicy;
|
||||
use app\repository\merchant\base\MerchantPolicyRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
|
||||
/**
|
||||
* 商户策略服务。
|
||||
*/
|
||||
class MerchantPolicyService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected MerchantPolicyRepository $merchantPolicyRepository,
|
||||
protected MerchantRepository $merchantRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询商户策略列表。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
$query = $this->merchantRepository->query()
|
||||
->from('ma_merchant as m')
|
||||
->leftJoin('ma_merchant_group as g', 'm.group_id', '=', 'g.id')
|
||||
->leftJoin('ma_merchant_policy as p', 'm.id', '=', 'p.merchant_id')
|
||||
->select([
|
||||
'm.id as merchant_id',
|
||||
'm.merchant_no',
|
||||
'm.merchant_name',
|
||||
'm.merchant_short_name',
|
||||
'm.group_id',
|
||||
'm.status as merchant_status',
|
||||
'g.group_name',
|
||||
'p.id',
|
||||
'p.settlement_cycle_override',
|
||||
'p.auto_payout',
|
||||
'p.min_settlement_amount',
|
||||
'p.retry_policy_json',
|
||||
'p.route_policy_json',
|
||||
'p.fee_rule_override_json',
|
||||
'p.risk_policy_json',
|
||||
'p.remark',
|
||||
'p.created_at',
|
||||
'p.updated_at',
|
||||
]);
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_short_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('g.group_name', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (($merchantId = (int) ($filters['merchant_id'] ?? 0)) > 0) {
|
||||
$query->where('m.id', $merchantId);
|
||||
}
|
||||
|
||||
if (($groupId = (int) ($filters['group_id'] ?? 0)) > 0) {
|
||||
$query->where('m.group_id', $groupId);
|
||||
}
|
||||
|
||||
if (($hasPolicy = (string) ($filters['has_policy'] ?? '')) !== '') {
|
||||
if ((int) $hasPolicy === 1) {
|
||||
$query->whereNotNull('p.id');
|
||||
} else {
|
||||
$query->whereNull('p.id');
|
||||
}
|
||||
}
|
||||
|
||||
if (($settlementCycle = (string) ($filters['settlement_cycle_override'] ?? '')) !== '') {
|
||||
$query->where('p.settlement_cycle_override', (int) $settlementCycle);
|
||||
}
|
||||
|
||||
if (($autoPayout = (string) ($filters['auto_payout'] ?? '')) !== '') {
|
||||
$query->where('p.auto_payout', (int) $autoPayout);
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('m.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
$paginator->getCollection()->transform(function ($row) {
|
||||
$row->has_policy = $row->id ? 1 : 0;
|
||||
$row->has_policy_text = $row->id ? '已配置' : '未配置';
|
||||
$row->settlement_cycle_text = $this->settlementCycleText((int) ($row->settlement_cycle_override ?? 0));
|
||||
$row->auto_payout_text = (int) ($row->auto_payout ?? 0) === 1 ? '是' : '否';
|
||||
$row->min_settlement_amount_text = $this->formatAmount((int) ($row->min_settlement_amount ?? 0));
|
||||
$row->has_retry_policy = !empty((array) ($row->retry_policy_json ?? []));
|
||||
$row->has_route_policy = !empty((array) ($row->route_policy_json ?? []));
|
||||
$row->has_fee_rule_override = !empty((array) ($row->fee_rule_override_json ?? []));
|
||||
$row->has_risk_policy = !empty((array) ($row->risk_policy_json ?? []));
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单个商户的策略。
|
||||
*/
|
||||
public function findByMerchantId(int $merchantId): ?MerchantPolicy
|
||||
{
|
||||
return $this->merchantPolicyRepository->findByMerchantId($merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存商户策略。
|
||||
*/
|
||||
public function saveByMerchantId(int $merchantId, array $data): MerchantPolicy
|
||||
{
|
||||
if (!$this->merchantRepository->find($merchantId)) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
return $this->merchantPolicyRepository->updateOrCreate(
|
||||
['merchant_id' => $merchantId],
|
||||
[
|
||||
'merchant_id' => $merchantId,
|
||||
'settlement_cycle_override' => (int) ($data['settlement_cycle_override'] ?? 1),
|
||||
'auto_payout' => (int) ($data['auto_payout'] ?? 0),
|
||||
'min_settlement_amount' => (int) ($data['min_settlement_amount'] ?? 0),
|
||||
'retry_policy_json' => $data['retry_policy_json'] ?? [],
|
||||
'route_policy_json' => $data['route_policy_json'] ?? [],
|
||||
'fee_rule_override_json' => $data['fee_rule_override_json'] ?? [],
|
||||
'risk_policy_json' => $data['risk_policy_json'] ?? [],
|
||||
'remark' => (string) ($data['remark'] ?? ''),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户策略。
|
||||
*/
|
||||
public function deleteByMerchantId(int $merchantId): bool
|
||||
{
|
||||
return $this->merchantPolicyRepository->deleteWhere(['merchant_id' => $merchantId]) > 0;
|
||||
}
|
||||
|
||||
private function settlementCycleText(int $value): string
|
||||
{
|
||||
return match ($value) {
|
||||
0 => 'D0',
|
||||
1 => 'D1',
|
||||
2 => 'D7',
|
||||
3 => 'T1',
|
||||
4 => 'OTHER',
|
||||
default => '未设置',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
50
app/service/merchant/portal/MerchantPortalBalanceService.php
Normal file
50
app/service/merchant/portal/MerchantPortalBalanceService.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
29
app/service/merchant/portal/MerchantPortalChannelService.php
Normal file
29
app/service/merchant/portal/MerchantPortalChannelService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
39
app/service/merchant/portal/MerchantPortalFinanceService.php
Normal file
39
app/service/merchant/portal/MerchantPortalFinanceService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/service/merchant/portal/MerchantPortalProfileService.php
Normal file
32
app/service/merchant/portal/MerchantPortalProfileService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
76
app/service/merchant/portal/MerchantPortalService.php
Normal file
76
app/service/merchant/portal/MerchantPortalService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
201
app/service/merchant/portal/MerchantPortalSupportService.php
Normal file
201
app/service/merchant/portal/MerchantPortalSupportService.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\security;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\AuthConstant;
|
||||
use app\model\merchant\MerchantApiCredential;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
|
||||
/**
|
||||
* 商户接口凭证查询服务。
|
||||
*
|
||||
* 负责凭证列表和详情展示,不承载验签和写入逻辑。
|
||||
*/
|
||||
class MerchantApiCredentialQueryService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantApiCredentialRepository $merchantApiCredentialRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询商户接口凭证。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
$query = $this->baseQuery(true);
|
||||
|
||||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($builder) use ($keyword) {
|
||||
$builder->where('m.merchant_no', 'like', '%' . $keyword . '%')
|
||||
->orWhere('m.merchant_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('c.api_key', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$merchantId = (string) ($filters['merchant_id'] ?? '');
|
||||
if ($merchantId !== '') {
|
||||
$query->where('c.merchant_id', (int) $merchantId);
|
||||
}
|
||||
|
||||
$status = (string) ($filters['status'] ?? '');
|
||||
if ($status !== '') {
|
||||
$query->where('c.status', (int) $status);
|
||||
}
|
||||
|
||||
$paginator = $query
|
||||
->orderByDesc('c.id')
|
||||
->paginate(max(1, $pageSize), ['*'], 'page', max(1, $page));
|
||||
|
||||
$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 ? '启用' : '禁用';
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户接口凭证详情。
|
||||
*/
|
||||
public function findById(int $id): ?MerchantApiCredential
|
||||
{
|
||||
$row = $this->baseQuery(false)->where('c.id', $id)->first();
|
||||
return $this->decorateRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户对应的接口凭证详情。
|
||||
*/
|
||||
public function findByMerchantId(int $merchantId): ?MerchantApiCredential
|
||||
{
|
||||
$row = $this->baseQuery(false)->where('c.merchant_id', $merchantId)->first();
|
||||
return $this->decorateRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一构造查询对象。
|
||||
*/
|
||||
private function baseQuery(bool $maskCredentialValue = false)
|
||||
{
|
||||
$query = $this->merchantApiCredentialRepository->query()
|
||||
->from('ma_merchant_api_credential as c')
|
||||
->leftJoin('ma_merchant as m', 'c.merchant_id', '=', 'm.id')
|
||||
->select([
|
||||
'c.id',
|
||||
'c.merchant_id',
|
||||
'c.sign_type',
|
||||
'c.status',
|
||||
'c.last_used_at',
|
||||
'c.created_at',
|
||||
'c.updated_at',
|
||||
])
|
||||
->selectRaw("COALESCE(m.merchant_no, '') AS merchant_no")
|
||||
->selectRaw("COALESCE(m.merchant_name, '') AS merchant_name");
|
||||
|
||||
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");
|
||||
} else {
|
||||
$query->addSelect('c.api_key');
|
||||
$query->selectRaw("COALESCE(c.api_key, '') AS api_key_full");
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给详情行补充展示字段。
|
||||
*/
|
||||
private function decorateRow(mixed $row): ?MerchantApiCredential
|
||||
{
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row->api_key_preview = $this->maskCredentialValue((string) ($row->api_key ?? ''), false);
|
||||
$row->sign_type_text = $this->textFromMap((int) $row->sign_type, AuthConstant::signTypeMap());
|
||||
$row->status_text = (int) $row->status === AuthConstant::LOGIN_STATUS_ENABLED ? '启用' : '禁用';
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
265
app/service/merchant/security/MerchantApiCredentialService.php
Normal file
265
app/service/merchant/security/MerchantApiCredentialService.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\merchant\security;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constant\AuthConstant;
|
||||
use app\common\constant\CommonConstant;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\exception\ValidationException;
|
||||
use app\model\merchant\Merchant;
|
||||
use app\model\merchant\MerchantApiCredential;
|
||||
use app\repository\merchant\credential\MerchantApiCredentialRepository;
|
||||
use app\repository\merchant\base\MerchantRepository;
|
||||
|
||||
/**
|
||||
* 商户对外接口凭证与签名校验服务。
|
||||
*
|
||||
* 负责外部支付接口的签名验证、接口凭证发放和最近使用时间更新。
|
||||
*/
|
||||
class MerchantApiCredentialService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造函数,注入对应依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantRepository $merchantRepository,
|
||||
protected MerchantApiCredentialRepository $merchantApiCredentialRepository,
|
||||
protected MerchantApiCredentialQueryService $merchantApiCredentialQueryService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询商户接口凭证。
|
||||
*/
|
||||
public function paginate(array $filters = [], int $page = 1, int $pageSize = 10)
|
||||
{
|
||||
return $this->merchantApiCredentialQueryService->paginate($filters, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验外部支付接口的 MD5 签名。
|
||||
*
|
||||
* @return array{merchant:\app\model\merchant\Merchant,credential:\app\model\merchant\MerchantApiCredential}
|
||||
*/
|
||||
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('商户接口凭证未开通');
|
||||
}
|
||||
|
||||
if ($providedKey !== '' && !hash_equals((string) $credential->api_key, $providedKey)) {
|
||||
throw new ValidationException('商户接口凭证错误');
|
||||
}
|
||||
|
||||
$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 = [];
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 为商户生成并保存一份新的接口凭证。
|
||||
*
|
||||
* 返回值是明文接口凭证值,只会在调用时完整出现一次,后续仅保存脱敏展示。
|
||||
*/
|
||||
public function issueCredential(int $merchantId): string
|
||||
{
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$credentialValue = $this->generateCredentialValue();
|
||||
$this->merchantApiCredentialRepository->updateOrCreate(
|
||||
['merchant_id' => $merchantId],
|
||||
[
|
||||
'merchant_id' => $merchantId,
|
||||
'sign_type' => AuthConstant::API_SIGN_TYPE_MD5,
|
||||
'api_key' => $credentialValue,
|
||||
'status' => AuthConstant::LOGIN_STATUS_ENABLED,
|
||||
]
|
||||
);
|
||||
|
||||
return $credentialValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户接口凭证详情。
|
||||
*/
|
||||
public function findById(int $id): ?MerchantApiCredential
|
||||
{
|
||||
return $this->merchantApiCredentialQueryService->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商户对应的接口凭证详情。
|
||||
*/
|
||||
public function findByMerchantId(int $merchantId): ?MerchantApiCredential
|
||||
{
|
||||
return $this->merchantApiCredentialQueryService->findByMerchantId($merchantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或更新商户接口凭证。
|
||||
*/
|
||||
public function create(array $data): MerchantApiCredential
|
||||
{
|
||||
$merchantId = (int) ($data['merchant_id'] ?? 0);
|
||||
$merchant = $this->merchantRepository->find($merchantId);
|
||||
if (!$merchant) {
|
||||
throw new ResourceNotFoundException('商户不存在', ['merchant_id' => $merchantId]);
|
||||
}
|
||||
|
||||
$current = $this->merchantApiCredentialRepository->findByMerchantId($merchantId);
|
||||
if ($current) {
|
||||
$updated = $this->update((int) $current->id, $data);
|
||||
if ($updated) {
|
||||
return $updated;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->merchantApiCredentialRepository->create($this->normalizePayload($data, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改商户接口凭证。
|
||||
*/
|
||||
public function update(int $id, array $data): ?MerchantApiCredential
|
||||
{
|
||||
$current = $this->merchantApiCredentialRepository->find($id);
|
||||
if (!$current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = $this->normalizePayload($data, true, $current);
|
||||
if (!$this->merchantApiCredentialRepository->updateById($id, $payload)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户接口凭证。
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
return $this->merchantApiCredentialRepository->deleteById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用商户 ID 和接口凭证直接进行身份校验。
|
||||
*
|
||||
* 该方法用于兼容 epay 风格的查询接口,不涉及签名串验签。
|
||||
*
|
||||
* @return array{merchant:\app\model\merchant\Merchant,credential:\app\model\merchant\MerchantApiCredential}
|
||||
*/
|
||||
public function authenticateByKey(int $merchantId, string $key): array
|
||||
{
|
||||
if ($merchantId <= 0 || $key === '') {
|
||||
throw new ValidationException('pid/key 参数缺失');
|
||||
}
|
||||
|
||||
/** @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('商户接口凭证未开通');
|
||||
}
|
||||
|
||||
if (!hash_equals((string) $credential->api_key, $key)) {
|
||||
throw new ValidationException('商户接口凭证错误');
|
||||
}
|
||||
|
||||
$credential->last_used_at = $this->now();
|
||||
$credential->save();
|
||||
|
||||
return [
|
||||
'merchant' => $merchant,
|
||||
'credential' => $credential,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理写入字段。
|
||||
*/
|
||||
private function normalizePayload(array $data, bool $isUpdate, ?MerchantApiCredential $current = null): array
|
||||
{
|
||||
$merchantId = (int) ($current?->merchant_id ?? ($data['merchant_id'] ?? 0));
|
||||
$payload = [
|
||||
'merchant_id' => $merchantId,
|
||||
'sign_type' => (int) ($data['sign_type'] ?? AuthConstant::API_SIGN_TYPE_MD5),
|
||||
'status' => (int) ($data['status'] ?? AuthConstant::LOGIN_STATUS_ENABLED),
|
||||
];
|
||||
|
||||
$apiKey = trim((string) ($data['api_key'] ?? ''));
|
||||
if ($apiKey !== '') {
|
||||
$payload['api_key'] = $apiKey;
|
||||
} elseif (!$isUpdate) {
|
||||
$payload['api_key'] = $this->generateCredentialValue();
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的接口凭证值。
|
||||
*/
|
||||
private function generateCredentialValue(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user