feat: 完善支付通道和收款监听链路

新增 ChannelNotifyPayloadInterface 等支付插件通知契约,规范 pay_no 定位和插件返回校验。

新增微信、支付宝、收钱吧、Postar 个人收款插件适配,支持余额识别与备注识别。

新增 receipt-watcher 后端进程、Redis 队列 job 和平台事件监听,覆盖收款流水通知、商户通知、退款派发、转账派发与清算完成。

补齐个人收款监听相关系统配置、仓储、服务费冻结明细、订单后台操作和通道测试能力。

重构支付单创建、回调、费用、风控、结算和通道统计链路,统一状态流转与幂等处理。
This commit is contained in:
技术老胡
2026-05-11 16:28:48 +08:00
parent 0e5de50337
commit fd1f53f2ee
136 changed files with 14416 additions and 3992 deletions

View File

@@ -125,7 +125,6 @@ class MerchantOverviewQueryService extends BaseService
'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' => [
@@ -153,4 +152,3 @@ class MerchantOverviewQueryService extends BaseService
}

View File

@@ -136,7 +136,7 @@ class MerchantAuthService extends BaseService
* @param string $password 密码
* @param string $ip 请求 IP
* @param string $userAgent 用户代理
* @return array{token: string, expires_in: int, merchant: Merchant, credential: array{status: int, sign_type: int, last_used_at: mixed}|null} 登录结果
* @return array{token: string, expires_in: int, merchant: Merchant, credential: array{status: int, last_used_at: mixed}|null} 登录结果
* @throws ValidationException
*/
public function authenticateCredentials(string $merchantNo, string $password, string $ip = '', string $userAgent = ''): array
@@ -183,7 +183,7 @@ class MerchantAuthService extends BaseService
* @param int $ttlSeconds 过期秒数
* @param string $ip 请求 IP
* @param string $userAgent 用户代理
* @return array{token: string, expires_in: int, merchant: Merchant, credential: array{status: int, sign_type: int, last_used_at: mixed}|null} 登录结果
* @return array{token: string, expires_in: int, merchant: Merchant, credential: array{status: int, last_used_at: mixed}|null} 登录结果
* @throws ValidationException
*/
public function issueToken(int $merchantId, int $ttlSeconds = 86400, string $ip = '', string $userAgent = ''): array
@@ -213,7 +213,6 @@ class MerchantAuthService extends BaseService
'merchant' => $merchant,
'credential' => $credential ? [
'status' => (int) ($credential->status ?? 0),
'sign_type' => (int) ($credential->sign_type ?? 0),
'last_used_at' => $credential->last_used_at,
] : null,
];

View File

@@ -3,7 +3,6 @@
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;
@@ -171,14 +170,4 @@ class MerchantPortalSupportService extends BaseService
return $payTypeId > 0 ? '未知' : '';
}
/**
* 签名类型文案。
*
* @param int $signType 签名类型
* @return string 签名类型文本
*/
public function signTypeText(int $signType): string
{
return $this->textFromMap($signType, AuthConstant::signTypeMap());
}
}

View File

@@ -62,13 +62,11 @@ class MerchantApiCredentialQueryService extends BaseService
->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 = $this->textFromMap((int) $row->status, AuthConstant::credentialStatusMap());
$row->platform_public_key_preview = $this->maskCredentialValue(
trim((string) config('epay.v2.platform_public_key', '')),
false
);
$row->platform_sign_type_text = (string) config('epay.v2.sign_type', AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA);
return $row;
});
@@ -114,7 +112,6 @@ class MerchantApiCredentialQueryService extends BaseService
->select([
'c.id',
'c.merchant_id',
'c.sign_type',
'c.merchant_public_key',
'c.status',
'c.last_used_at',
@@ -151,11 +148,9 @@ class MerchantApiCredentialQueryService extends BaseService
$row->api_key_preview = $this->maskCredentialValue((string) ($row->api_key ?? ''), false);
$row->merchant_public_key_preview = $this->maskCredentialValue((string) ($row->merchant_public_key ?? ''), false);
$row->sign_type_text = $this->textFromMap((int) $row->sign_type, AuthConstant::signTypeMap());
$row->status_text = $this->textFromMap((int) $row->status, AuthConstant::credentialStatusMap());
$row->platform_public_key_full = trim((string) config('epay.v2.platform_public_key', ''));
$row->platform_public_key_preview = $this->maskCredentialValue((string) $row->platform_public_key_full, false);
$row->platform_sign_type_text = (string) config('epay.v2.sign_type', AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA);
return $row;
}

View File

@@ -96,7 +96,6 @@ class MerchantApiCredentialService extends BaseService
throw new ValidationException('请至少选择一种要生成的凭证类型');
}
$signType = (int) ($options['sign_type'] ?? ($current?->sign_type ?? AuthConstant::API_SIGN_TYPE_MD5));
$status = (int) ($options['status'] ?? ($current?->status ?? AuthConstant::CREDENTIAL_STATUS_ENABLED));
$credentialValue = $rotateV1 ? $this->generateCredentialValue() : trim((string) ($current?->api_key ?? ''));
$merchantPrivateKey = '';
@@ -112,7 +111,6 @@ class MerchantApiCredentialService extends BaseService
['merchant_id' => $merchantId],
[
'merchant_id' => $merchantId,
'sign_type' => $signType,
'status' => $status,
'api_key' => $credentialValue,
'merchant_public_key' => $merchantPublicKey,
@@ -274,19 +272,16 @@ class MerchantApiCredentialService extends BaseService
* @param bool $isUpdate 是否更新
* @param MerchantApiCredential|null $current 当前凭证
* 更新场景下,空字符串视为“不修改”,避免手动配置时误清空已有密钥。
* `sign_type` 在当前阶段只作为展示/默认接入说明,不再作为 V1/V2 互斥开关。
*
* @return array{merchant_id: int, sign_type: int, status: int, api_key?: string} 标准化后的写入数据
* @return array{merchant_id: int, status: int, api_key?: string} 标准化后的写入数据
*/
private function normalizePayload(array $data, bool $isUpdate, ?MerchantApiCredential $current = null): array
{
// 更新场景下以现有记录的 merchant_id 为准,避免把凭证误挂到别的商户。
$merchantId = (int) ($current?->merchant_id ?? ($data['merchant_id'] ?? 0));
$currentSignType = (int) ($current?->sign_type ?? AuthConstant::API_SIGN_TYPE_MD5);
$currentStatus = (int) ($current?->status ?? AuthConstant::CREDENTIAL_STATUS_ENABLED);
$payload = [
'merchant_id' => $merchantId,
'sign_type' => (int) ($data['sign_type'] ?? $currentSignType),
'status' => (int) ($data['status'] ?? $currentStatus),
];