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

@@ -522,7 +522,10 @@ class EpayMapiTest extends Command
$extJson = (array) ($payOrder['ext_json'] ?? []);
$presentation = (array) ($extJson['presentation'] ?? []);
$summary = $this->summarizePayParamsSnapshot((array) ($presentation['params_snapshot'] ?? []));
$summary = $this->summarizePayParams(
(array) ($presentation['pay_params'] ?? []),
(string) ($presentation['pay_page'] ?? '')
);
if ($summary !== []) {
$output->writeln(' 插件返回:');
$output->writeln(' ' . $this->formatJson($summary));
@@ -533,15 +536,16 @@ class EpayMapiTest extends Command
* 归纳支付参数快照。
*
* @param array $snapshot 支付参数快照
* @param string $payPage 承接页类型
* @return array 归纳结果
*/
private function summarizePayParamsSnapshot(array $snapshot): array
private function summarizePayParams(array $snapshot, string $payPage = ''): array
{
if ($snapshot === []) {
return [];
}
$summary = ['type' => (string) ($snapshot['type'] ?? '')];
$summary = ['pay_page' => $payPage];
if (isset($snapshot['pay_product'])) {
$summary['pay_product'] = (string) $snapshot['pay_product'];
}
@@ -549,20 +553,20 @@ class EpayMapiTest extends Command
$summary['pay_action'] = (string) $snapshot['pay_action'];
}
switch ((string) ($snapshot['type'] ?? '')) {
case 'form':
switch ($summary['pay_page']) {
case 'html':
$html = $this->stringifyValue($snapshot['html'] ?? '');
$summary['html_length'] = strlen($html);
$summary['html_head'] = $this->limitString($this->normalizeWhitespace($html), 160);
break;
case 'qrcode':
$summary['qrcode_url'] = $this->stringifyValue($snapshot['qrcode_url'] ?? $snapshot['qrcode_data'] ?? '');
$summary['qrcode'] = $this->stringifyValue($snapshot['qrcode'] ?? '');
break;
case 'urlscheme':
$summary['urlscheme'] = $this->stringifyValue($snapshot['urlscheme'] ?? $snapshot['order_str'] ?? '');
$summary['urlscheme'] = $this->stringifyValue($snapshot['urlscheme'] ?? '');
break;
case 'url':
$summary['payurl'] = $this->stringifyValue($snapshot['payurl'] ?? '');
case 'jump':
$summary['url'] = $this->stringifyValue($snapshot['url'] ?? '');
break;
default:
if (isset($snapshot['raw']) && is_array($snapshot['raw'])) {
@@ -1075,7 +1079,7 @@ class EpayMapiTest extends Command
private function resolve(string $class): object
{
try {
$instance = container_make($class, []);
$instance = container_get($class);
} catch (\Throwable $e) {
throw new CommandException("无法解析 {$class}", 0, $e);
}

View File

@@ -404,7 +404,6 @@ class EpayMockChainTest extends Command
'merchant_id' => (int) $merchant->id,
'api_key' => 'mock-v1-api-key-20260423',
'merchant_public_key' => '',
'sign_type' => AuthConstant::API_SIGN_TYPE_MD5,
'status' => AuthConstant::CREDENTIAL_STATUS_ENABLED,
]
);
@@ -477,7 +476,6 @@ class EpayMockChainTest extends Command
'merchant_id' => (int) $merchant->id,
'api_key' => 'mock-v2-api-key-20260423',
'merchant_public_key' => $merchantPair['public_key'],
'sign_type' => AuthConstant::API_SIGN_TYPE_SHA256_WITH_RSA,
'status' => AuthConstant::CREDENTIAL_STATUS_ENABLED,
]
);
@@ -847,10 +845,10 @@ class EpayMockChainTest extends Command
/** @var EpaySignerManager $signerManager */
$signerManager = $this->resolve(EpaySignerManager::class);
$payload['timestamp'] = (string) time();
$payload['sign_type'] = AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA;
$payload['sign_type'] = AuthConstant::API_SIGN_NAME_RSA;
$signPayload = $payload;
unset($signPayload['sign'], $signPayload['sign_type']);
$payload['sign'] = $signerManager->sign($signPayload, AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA, $privateKey);
$payload['sign'] = $signerManager->sign($signPayload, AuthConstant::API_SIGN_NAME_RSA, $privateKey);
return $payload;
}
@@ -870,11 +868,10 @@ class EpayMockChainTest extends Command
return ['passed' => false, 'message' => '响应缺少 sign'];
}
$signType = $signerManager->normalizeSignType((string) ($responseData['sign_type'] ?? AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA));
$verifyPayload = $responseData;
unset($verifyPayload['sign'], $verifyPayload['sign_type']);
if (!$signerManager->verify($verifyPayload, $signType, $sign, $platformPublicKey)) {
if (!$signerManager->verify($verifyPayload, (string) ($responseData['sign_type'] ?? AuthConstant::API_SIGN_NAME_RSA), $sign, $platformPublicKey)) {
return ['passed' => false, 'message' => '响应验签失败'];
}
@@ -942,11 +939,11 @@ class EpayMockChainTest extends Command
'param' => (string) (((array) ($bizOrder->ext_json['merchant'] ?? []))['param'] ?? ''),
'timestamp' => (string) time(),
'endtime' => FormatHelper::dateTime($this->nowDateTime()),
'sign_type' => AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA,
'sign_type' => AuthConstant::API_SIGN_NAME_RSA,
];
$signPayload = $payload;
unset($signPayload['sign'], $signPayload['sign_type']);
$payload['sign'] = $signerManager->sign($signPayload, AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA, $mockPlatformPrivateKey);
$payload['sign'] = $signerManager->sign($signPayload, AuthConstant::API_SIGN_NAME_RSA, $mockPlatformPrivateKey);
return $payload;
}
@@ -1300,7 +1297,7 @@ class EpayMockChainTest extends Command
private function resolve(string $class): object
{
try {
$instance = container_make($class, []);
$instance = container_get($class);
} catch (Throwable $e) {
throw new CommandException('无法解析 ' . $class, 50002, $e);
}

View File

@@ -506,8 +506,8 @@ class EpayV2ApiTest extends Command
$signerManager = $this->resolve(EpaySignerManager::class);
$payload['pid'] = (int) ($payload['pid'] ?? 0);
$payload['timestamp'] = (string) time();
$payload['sign_type'] = AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA;
$payload['sign'] = $signerManager->sign($payload, AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA, $privateKey);
$payload['sign_type'] = AuthConstant::API_SIGN_NAME_RSA;
$payload['sign'] = $signerManager->sign($payload, AuthConstant::API_SIGN_NAME_RSA, $privateKey);
return $payload;
}
@@ -762,8 +762,8 @@ class EpayV2ApiTest extends Command
'pid' => 1,
'timestamp' => (string) time(),
];
$sign = $signerManager->sign($probePayload, AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA, $merchantPrivateKey);
if (!$signerManager->verify($probePayload, AuthConstant::API_SIGN_NAME_SHA256_WITH_RSA, $sign, $merchantPublicKey)) {
$sign = $signerManager->sign($probePayload, AuthConstant::API_SIGN_NAME_RSA, $merchantPrivateKey);
if (!$signerManager->verify($probePayload, AuthConstant::API_SIGN_NAME_RSA, $sign, $merchantPublicKey)) {
throw new CommandException('提供的商户私钥与后台配置的商户公钥不匹配。');
}
}
@@ -1203,7 +1203,7 @@ class EpayV2ApiTest extends Command
private function resolve(string $class): object
{
try {
$instance = container_make($class, []);
$instance = container_get($class);
} catch (\Throwable $e) {
throw new CommandException("无法解析 {$class}", 0, $e);
}

View File

@@ -143,7 +143,6 @@ class EpayV2Bootstrap extends Command
[
'merchant_id' => (int) $merchant->id,
'status' => AuthConstant::CREDENTIAL_STATUS_ENABLED,
'sign_type' => (int) ($credential?->sign_type ?? AuthConstant::API_SIGN_TYPE_MD5),
'api_key' => (string) ($credential?->api_key ?: bin2hex(random_bytes(16))),
'merchant_public_key' => $pair['public_key'],
]
@@ -476,7 +475,7 @@ POWERSHELL;
private function resolve(string $class): object
{
try {
$instance = container_make($class, []);
$instance = container_get($class);
} catch (\Throwable $e) {
throw new CommandException("无法解析 {$class}", 0, $e);
}

View File

@@ -144,6 +144,10 @@ class MpayTest extends Command
'pay_amount' => $payAmount,
'subject' => $this->envString('MPAY_TEST_PAYMENT_SUBJECT', 'mpay smoke payment'),
'body' => $this->envString('MPAY_TEST_PAYMENT_BODY', 'mpay smoke payment'),
'notify_url' => $this->envString('MPAY_TEST_PAYMENT_NOTIFY_URL', ''),
'return_url' => $this->envString('MPAY_TEST_PAYMENT_RETURN_URL', ''),
'client_ip' => $this->envString('MPAY_TEST_PAYMENT_CLIENT_IP', '127.0.0.1'),
'device' => $this->envString('MPAY_TEST_PAYMENT_DEVICE', 'pc'),
'ext_json' => $this->envJson('MPAY_TEST_PAYMENT_EXT_JSON', []),
]);
@@ -164,7 +168,6 @@ class MpayTest extends Command
$message .= ', 已标记超时';
} elseif ($this->envBool('MPAY_TEST_PAYMENT_MARK_SUCCESS', false)) {
$service->markPaySuccess((string) $payOrder->pay_no, [
'fee_actual_amount' => $this->envInt('MPAY_TEST_PAYMENT_FEE_AMOUNT', (int) $payOrder->fee_estimated_amount),
'channel_trade_no' => $this->envString('MPAY_TEST_PAYMENT_CHANNEL_TRADE_NO', $this->generateTestNo('CH-')),
'channel_order_no' => $this->envString('MPAY_TEST_PAYMENT_CHANNEL_ORDER_NO', $this->generateTestNo('CO-')),
]);
@@ -273,10 +276,10 @@ class MpayTest extends Command
'pay_no' => (string) $payOrder->pay_no,
'refund_no' => '',
'pay_amount' => (int) $payOrder->pay_amount,
'fee_amount' => (int) $payOrder->fee_actual_amount,
'fee_amount' => (int) $payOrder->service_fee_amount,
'refund_amount' => 0,
'fee_reverse_amount' => 0,
'net_amount' => max(0, (int) $payOrder->pay_amount - (int) $payOrder->fee_actual_amount),
'net_amount' => max(0, (int) $payOrder->pay_amount - (int) $payOrder->service_fee_amount),
'item_status' => TradeConstant::SETTLEMENT_STATUS_PENDING,
]];
$merchantId = (int) $payOrder->merchant_id;
@@ -442,7 +445,7 @@ class MpayTest extends Command
private function resolve(string $class): object
{
try {
$instance = container_make($class, []);
$instance = container_get($class);
} catch (\Throwable $e) {
throw new CommandException("无法解析 {$class}", 0, $e);
}

View File

@@ -39,32 +39,23 @@ class SystemConfigSync extends Command
{
try {
/** @var SystemConfigDefinitionService $definitionService */
$definitionService = container_make(SystemConfigDefinitionService::class, []);
$definitionService = container_get(SystemConfigDefinitionService::class);
/** @var SystemConfigRepository $repository */
$repository = container_make(SystemConfigRepository::class, []);
$repository = container_get(SystemConfigRepository::class);
/** @var SystemConfigRuntimeService $runtimeService */
$runtimeService = container_make(SystemConfigRuntimeService::class, []);
$runtimeService = container_get(SystemConfigRuntimeService::class);
$tabs = $definitionService->tabs();
$written = 0;
foreach ($tabs as $tab) {
$groupCode = (string) ($tab['key'] ?? '');
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$configKey = strtolower(trim((string) ($rule['field'] ?? '')));
if ($configKey === '' || str_starts_with($configKey, '__')) {
continue;
}
foreach ($definitionService->defaultStorageValues($tab) as $configKey => $configValue) {
$repository->updateOrCreate(
['config_key' => $configKey],
[
'group_code' => $groupCode,
'config_value' => (string) ($rule['value'] ?? ''),
'config_value' => $configValue,
]
);
@@ -84,4 +75,3 @@ class SystemConfigSync extends Command
}
}
}