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

@@ -64,4 +64,19 @@ class CashierController extends BaseController
$this->cashierService->payOrderDetail((string) ($payload['pay_no'] ?? ''))
);
}
/**
* 查询支付单状态。
*
* @param Request $request 请求对象
* @return Response 响应对象
*/
public function payOrderStatus(Request $request): Response
{
$payload = $this->validated($request->all(), CashierValidator::class, 'pay_order_status');
return $this->success(
$this->cashierService->payOrderStatus((string) ($payload['pay_no'] ?? ''))
);
}
}

View File

@@ -170,4 +170,16 @@ class EpayV2Controller extends BaseController
{
return $this->payOrderService->handlePluginCallback($payNo, $request);
}
/**
* 通道级通知入口。
*
* @param Request $request 请求对象
* @param int $chanId 通道ID
* @return string|Response
*/
public function channelNotify(Request $request, int $chanId): string|Response
{
return $this->payOrderService->handleChannelNotify($chanId, $request);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace app\http\api\controller\system;
use app\common\base\BaseController;
use app\service\system\config\SystemPublicConfigService;
use support\Request;
use support\Response;
/**
* 公开系统配置控制器。
*/
class SystemPublicConfigController extends BaseController
{
/**
* 构造方法。
*
* @param SystemPublicConfigService $systemPublicConfigService 系统公开配置服务
*/
public function __construct(
protected SystemPublicConfigService $systemPublicConfigService
) {
}
/**
* 查询收银台公开配置。
*
* @param Request $request 请求对象
* @return Response 响应对象
*/
public function cashier(Request $request): Response
{
return $this->success($this->systemPublicConfigService->cashier());
}
}

View File

@@ -27,6 +27,7 @@ class CashierValidator extends Validator
'context' => ['biz_no'],
'confirm' => ['biz_no', 'type'],
'pay_order' => ['pay_no'],
'pay_order_status' => ['pay_no'],
];
/**
@@ -65,4 +66,16 @@ class CashierValidator extends Validator
'pay_no' => 'required|string|max:32',
]);
}
/**
* 支付状态查询场景。
*
* @return static
*/
public function scenePayOrderStatus(): static
{
return $this->appendRules([
'pay_no' => 'required|string|max:32',
]);
}
}

View File

@@ -21,7 +21,7 @@ class EpayV1Validator extends Validator
'notify_url' => 'nullable|string|max:255',
'return_url' => 'nullable|string|max:255',
'name' => 'nullable|string|max:255',
'money' => 'nullable|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'nullable|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'param' => 'nullable',
'clientip' => 'nullable|ip',
'device' => 'nullable|string|in:pc,mobile,qq,wechat,alipay,jump',
@@ -73,7 +73,7 @@ class EpayV1Validator extends Validator
'notify_url' => 'required|string|max:255',
'return_url' => 'required|string|max:255',
'name' => 'required|string|max:255',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'sign_type' => 'required|string|in:MD5',
'sign' => 'required|string|max:255',
]);
@@ -92,7 +92,7 @@ class EpayV1Validator extends Validator
'notify_url' => 'required|string|max:255',
'return_url' => 'nullable|string|max:255',
'name' => 'required|string|max:255',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'clientip' => 'required|ip',
'sign_type' => 'required|string|in:MD5',
'sign' => 'required|string|max:255',
@@ -160,7 +160,7 @@ class EpayV1Validator extends Validator
{
return $this->appendRules([
'key' => 'required|string|max:128',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'trade_no' => 'nullable|string|max:64|required_without:out_trade_no',
'out_trade_no' => 'nullable|string|max:64|required_without:trade_no',
]);

View File

@@ -14,8 +14,7 @@ class EpayV2Validator extends Validator
protected array $rules = [
'pid' => 'required|integer|min:1',
'timestamp' => 'required|integer|min:1',
// 兼容旧版 SDK 里使用的 `RSA` 简写,同时内部统一按 SHA256WithRSA 验签。
'sign_type' => 'required|string|in:SHA256WithRSA,RSA',
'sign_type' => 'required|string|in:RSA',
// RSA 签名是 Base64 文本,长度会明显超过 MD5不能沿用 255 的短限制。
'sign' => 'required|string|max:2048',
'type' => 'nullable|string|max:32',
@@ -25,7 +24,7 @@ class EpayV2Validator extends Validator
'notify_url' => 'nullable|string|max:255',
'return_url' => 'nullable|string|max:255',
'name' => 'nullable|string|max:255',
'money' => 'nullable|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'nullable|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'param' => 'nullable',
'auth_code' => 'nullable|string|max:128',
'sub_openid' => 'nullable|string|max:128',
@@ -104,8 +103,8 @@ class EpayV2Validator extends Validator
'notify_url' => 'required|string|max:255',
'return_url' => 'required|string|max:255',
'name' => 'required|string|max:255',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'sign_type' => 'required|string|in:SHA256WithRSA,RSA',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'sign_type' => 'required|string|in:RSA',
'sign' => 'required|string|max:2048',
]);
}
@@ -124,9 +123,9 @@ class EpayV2Validator extends Validator
'notify_url' => 'required|string|max:255',
'return_url' => 'nullable|string|max:255',
'name' => 'required|string|max:255',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'device' => 'nullable|string|in:pc,mobile,qq,wechat,alipay',
'sign_type' => 'required|string|in:SHA256WithRSA,RSA',
'sign_type' => 'required|string|in:RSA',
'sign' => 'required|string|max:2048',
]);
}
@@ -139,11 +138,11 @@ class EpayV2Validator extends Validator
public function sceneRefund(): static
{
return $this->appendRules([
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'trade_no' => 'nullable|string|max:64|required_without:out_trade_no',
'out_trade_no' => 'nullable|string|max:64|required_without:trade_no',
'out_refund_no' => 'nullable|string|max:64',
'sign_type' => 'required|string|in:SHA256WithRSA,RSA',
'sign_type' => 'required|string|in:RSA',
'sign' => 'required|string|max:2048',
]);
}
@@ -208,8 +207,8 @@ class EpayV2Validator extends Validator
'type' => 'required|string|in:alipay,wxpay,qqpay,bank',
'account' => 'required|string|max:100',
'name' => 'required|string|max:100',
'money' => 'required|regex:/^\d+(?:\.\d{1,2})?$/',
'sign_type' => 'required|string|in:SHA256WithRSA,RSA',
'money' => 'required|regex:/^(?=.*[1-9])\d+(?:\.\d{1,2})?$/',
'sign_type' => 'required|string|in:RSA',
'sign' => 'required|string|max:2048',
]);
}