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

@@ -0,0 +1,73 @@
<?php
namespace app\queue\support;
use app\common\interface\QueueJobInterface;
use RuntimeException;
use support\Log;
use Throwable;
/**
* 队列任务基类。
*
* 提供消息字段校验、布尔值解析和统一失败日志,具体业务 Job 只需要实现 handle。
*/
abstract class AbstractQueueJob implements QueueJobInterface
{
/**
* 默认消费失败处理。
*
* @param Throwable $exception 异常
* @param array<string, mixed> $package 原始队列包
* @return void
*/
public function failed(Throwable $exception, array $package): void
{
Log::warning(sprintf(
'[%s] 消费失败 queue=%s attempts=%s error=%s',
$this->logName(),
(string) ($package['queue'] ?? ''),
(string) ($package['attempts'] ?? ''),
$exception->getMessage()
));
}
/**
* 读取必填字符串字段。
*
* @param array<string, mixed> $data 队列消息
* @param string $key 字段名
* @param string $label 字段显示名
* @return string 字段值
*/
protected function requireString(array $data, string $key, string $label = ''): string
{
$value = trim((string) ($data[$key] ?? ''));
if ($value === '') {
throw new RuntimeException(($label !== '' ? $label : $key) . ' 不能为空');
}
return $value;
}
/**
* 解析布尔字段。
*
* @param mixed $value 字段值
* @return bool 布尔结果
*/
protected function boolValue(mixed $value): bool
{
return filter_var($value, FILTER_VALIDATE_BOOL);
}
/**
* 获取日志名称。
*
* @return string 日志名称
*/
protected function logName(): string
{
return static::class;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace app\queue\support;
use app\common\interface\QueueJobInterface;
use RuntimeException;
use support\Log;
use Throwable;
use Webman\RedisQueue\Consumer;
/**
* Redis 队列消费者基类。
*
* 统一把 webman/redis-queue 的 Consumer 协议适配到业务 Job具体消费者只需要声明
* 队列名和 Job 类名。
*/
abstract class AbstractRedisConsumer implements Consumer
{
/**
* Redis 队列连接名。
*
* @var string
*/
public $connection = 'default';
/**
* 获取任务类名。
*
* @return class-string<QueueJobInterface> 任务类名
*/
abstract protected function jobClass(): string;
/**
* 消费队列消息。
*
* @param mixed $data 队列消息
* @return void
*/
public function consume($data): void
{
$this->job()->handle(is_array($data) ? $data : []);
}
/**
* 处理消费失败。
*
* @param Throwable $exception 异常
* @param array<string, mixed> $package 原始队列包
* @return void
*/
public function onConsumeFailure(Throwable $exception, array $package): void
{
try {
$this->job()->failed($exception, $package);
} catch (Throwable $failureException) {
Log::warning(sprintf(
'[QueueConsumer] 失败处理异常 job=%s queue=%s error=%s failure_error=%s',
$this->jobClass(),
(string) ($package['queue'] ?? ''),
$exception->getMessage(),
$failureException->getMessage()
));
}
}
/**
* 从容器中获取任务实例。
*
* Job 不保存单次消费的可变状态,使用 container_get 复用实例,避免每条消息重复构造依赖。
*
* @return QueueJobInterface 任务实例
*/
private function job(): QueueJobInterface
{
$job = container_get($this->jobClass());
if (!$job instanceof QueueJobInterface) {
throw new RuntimeException('队列任务必须实现 QueueJobInterface' . $this->jobClass());
}
return $job;
}
}