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

@@ -76,13 +76,15 @@ class PaymentRuntimeProcess
)
);
$this->runIfDue(
'order_timeout',
$this->intConfig('pay_order_timeout_scan_interval_seconds', 60, 5),
fn (): array => $this->maintenanceService()->timeoutExpiredPayOrders(
$this->intConfig('pay_order_timeout_batch_size', 100, 1)
)
);
if ($this->boolConfig('pay_order_timeout_enabled', true)) {
$this->runIfDue(
'order_timeout',
$this->intConfig('pay_order_timeout_scan_interval_seconds', 60, 5),
fn (): array => $this->maintenanceService()->timeoutExpiredPayOrders(
$this->intConfig('pay_order_timeout_batch_size', 100, 1)
)
);
}
if ($this->boolConfig('pay_active_query_enabled', true)) {
$this->runIfDue(
@@ -161,7 +163,7 @@ class PaymentRuntimeProcess
*/
private function maintenanceService(): PaymentRuntimeMaintenanceService
{
return container_make(PaymentRuntimeMaintenanceService::class, []);
return container_get(PaymentRuntimeMaintenanceService::class);
}
/**
@@ -216,6 +218,6 @@ class PaymentRuntimeProcess
*/
private function runtimeConfig(): SystemConfigRuntimeService
{
return container_make(SystemConfigRuntimeService::class, []);
return container_get(SystemConfigRuntimeService::class);
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace app\process;
use app\service\payment\receipt\ReceiptWatcherService;
use support\Log;
use Workerman\Timer;
use Workerman\Worker;
/**
* 网页流水监听调度进程。
*
* 该进程不访问第三方平台,只负责把当前需要查询流水的账号和订单同步到 Redis。
*/
class ReceiptWatcherProcess
{
/**
* 上次执行时间。
*
* @var array<string, int>
*/
private array $lastRunAt = [];
/**
* 运行锁。
*
* @var array<string, bool>
*/
private array $running = [];
/**
* 构造方法。
*
* @param array<string, mixed> $options 进程选项
*/
public function __construct(
private array $options = []
) {
}
/**
* Worker 启动。
*
* @param Worker $worker Worker 实例
* @return void
*/
public function onWorkerStart(Worker $worker): void
{
try {
$this->watcherService()->refreshChannelCache();
} catch (\Throwable $e) {
Log::warning('[ReceiptWatcherProcess] 启动刷新账号缓存失败:' . $e->getMessage());
}
$heartbeat = $this->intOption('heartbeat_seconds', 1, 1, 60);
Timer::add($heartbeat, function (): void {
$this->tick();
});
Log::info(sprintf('[ReceiptWatcherProcess] 网页流水监听调度进程已启动 heartbeat=%s', $heartbeat));
}
/**
* 心跳调度入口。
*
* @return void
*/
private function tick(): void
{
try {
$this->runIfDue('refresh_channels', 60, function (): array {
return $this->watcherService()->refreshChannelCache();
});
$this->runIfDue('sync_pending_orders', $this->scanIntervalSeconds(), function (): array {
return $this->watcherService()->syncPendingOrders($this->scanBatchSize());
});
} catch (\Throwable $e) {
Log::warning('[ReceiptWatcherProcess] 心跳调度失败:' . $e->getMessage());
}
}
/**
* 到期后执行任务。
*
* @param string $key 任务键
* @param int $intervalSeconds 间隔秒数
* @param callable $callback 任务回调
* @return void
*/
private function runIfDue(string $key, int $intervalSeconds, callable $callback): void
{
$now = time();
$lastRunAt = (int) ($this->lastRunAt[$key] ?? 0);
if ($lastRunAt > 0 && $now - $lastRunAt < $intervalSeconds) {
return;
}
if (!empty($this->running[$key])) {
return;
}
$this->lastRunAt[$key] = $now;
$this->running[$key] = true;
try {
$summary = $callback();
if ($this->hasWork($summary)) {
Log::info(sprintf(
'[ReceiptWatcherProcess] %s 执行完成 %s',
$key,
json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
));
}
} catch (\Throwable $e) {
Log::warning(sprintf('[ReceiptWatcherProcess] %s 执行失败:%s', $key, $e->getMessage()));
} finally {
$this->running[$key] = false;
}
}
/**
* @param array<string, int> $summary 任务摘要
* @return bool 是否有实际工作量
*/
private function hasWork(array $summary): bool
{
foreach ($summary as $value) {
if ((int) $value > 0) {
return true;
}
}
return false;
}
/**
* @return int 待支付订单扫描间隔
*/
private function scanIntervalSeconds(): int
{
return max(2, (int) sys_config('receipt_watcher_order_scan_interval_seconds', 3));
}
/**
* @return int 待支付订单扫描批量
*/
private function scanBatchSize(): int
{
return max(1, (int) sys_config('receipt_watcher_order_scan_batch_size', 500));
}
/**
* @param string $key 配置键
* @param int $default 默认值
* @param int $min 最小值
* @param int $max 最大值
* @return int 配置值
*/
private function intOption(string $key, int $default, int $min, int $max): int
{
$value = (int) ($this->options[$key] ?? $default);
return min($max, max($min, $value));
}
/**
* @return ReceiptWatcherService 网页流水监听服务
*/
private function watcherService(): ReceiptWatcherService
{
return container_get(ReceiptWatcherService::class);
}
}