mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-05-17 06:20:25 +08:00
feat: 完善支付通道和收款监听链路
新增 ChannelNotifyPayloadInterface 等支付插件通知契约,规范 pay_no 定位和插件返回校验。 新增微信、支付宝、收钱吧、Postar 个人收款插件适配,支持余额识别与备注识别。 新增 receipt-watcher 后端进程、Redis 队列 job 和平台事件监听,覆盖收款流水通知、商户通知、退款派发、转账派发与清算完成。 补齐个人收款监听相关系统配置、仓储、服务费冻结明细、订单后台操作和通道测试能力。 重构支付单创建、回调、费用、风控、结算和通道统计链路,统一状态流转与幂等处理。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user