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

@@ -4,6 +4,7 @@ namespace app\service\system\config;
use app\common\base\BaseService;
use app\exception\ConflictException;
use app\exception\ValidationException;
/**
* 系统配置定义解析服务。
@@ -119,6 +120,92 @@ class SystemConfigDefinitionService extends BaseService
return $this->tabMapCache[$groupCode] ?? null;
}
/**
* 获取标签页内的实际配置字段。
*
* @param array $tab 标签页定义
* @return array<int, string> 配置字段列表
*/
public function fields(array $tab): array
{
$fields = [];
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field === '' || $this->isVirtualField($field)) {
continue;
}
$fields[$field] = true;
}
return array_keys($fields);
}
/**
* 获取全部实际配置字段。
*
* @return array<int, string> 配置字段列表
*/
public function allFields(): array
{
$fields = [];
foreach ($this->tabs() as $tab) {
foreach ($this->fields($tab) as $field) {
$fields[$field] = true;
}
}
return array_keys($fields);
}
/**
* 获取标签页内配置项的默认落库值。
*
* @param array $tab 标签页定义
* @return array<string, string> 字段到默认值的映射
* @throws ValidationException
*/
public function defaultStorageValues(array $tab): array
{
$defaults = [];
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field === '' || $this->isVirtualField($field)) {
continue;
}
$defaults[$field] = $this->stringifyValue($rule['value'] ?? '');
}
return $defaults;
}
/**
* 获取全部配置项默认落库值。
*
* @return array<string, string> 字段到默认值的映射
* @throws ValidationException
*/
public function allDefaultStorageValues(): array
{
$defaults = [];
foreach ($this->tabs() as $tab) {
foreach ($this->defaultStorageValues($tab) as $field => $value) {
$defaults[$field] = $value;
}
}
return $defaults;
}
/**
* 使用当前值回填标签页规则。
*
@@ -140,7 +227,10 @@ class SystemConfigDefinitionService extends BaseService
}
if (!$this->isVirtualField($field)) {
$rule['value'] = array_key_exists($field, $values) ? $values[$field] : ($rule['value'] ?? '');
$rule['value'] = $this->normalizeValueForForm(
$rule,
array_key_exists($field, $values) ? $values[$field] : ($rule['value'] ?? '')
);
}
$rules[] = $rule;
}
@@ -168,7 +258,10 @@ class SystemConfigDefinitionService extends BaseService
continue;
}
$data[$field] = array_key_exists($field, $values) ? $values[$field] : ($rule['value'] ?? '');
$data[$field] = $this->normalizeValueForForm(
$rule,
array_key_exists($field, $values) ? $values[$field] : ($rule['value'] ?? '')
);
}
return $data;
@@ -208,6 +301,26 @@ class SystemConfigDefinitionService extends BaseService
return $messages;
}
/**
* 将配置值转换为可落库字符串。
*
* @param mixed $value 配置值
* @return string 可落库字符串
* @throws ValidationException
*/
public function stringifyValue(mixed $value): string
{
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_array($value) || is_object($value)) {
throw new ValidationException('系统配置值暂不支持复杂类型');
}
return (string) $value;
}
/**
* 标准化单个标签页定义。
*
@@ -268,7 +381,7 @@ class SystemConfigDefinitionService extends BaseService
$options[] = [
'label' => (string) ($option['label'] ?? ''),
'value' => (string) ($option['value'] ?? ''),
'value' => $option['value'] ?? '',
];
}
@@ -285,7 +398,7 @@ class SystemConfigDefinitionService extends BaseService
$normalized['type'] = (string) ($rule['type'] ?? 'input');
$normalized['field'] = $field;
$normalized['title'] = (string) ($rule['title'] ?? $field);
$normalized['value'] = (string) ($rule['value'] ?? '');
$normalized['value'] = $rule['value'] ?? '';
$normalized['props'] = is_array($rule['props'] ?? null) ? $rule['props'] : [];
$normalized['options'] = $options;
$normalized['validate'] = $validate;
@@ -293,14 +406,39 @@ class SystemConfigDefinitionService extends BaseService
return $normalized;
}
/**
* 按表单组件类型整理前端模型值。
*
* @param array<string, mixed> $rule 配置项定义
* @param mixed $value 原始值
* @return mixed 前端表单值
*/
private function normalizeValueForForm(array $rule, mixed $value): mixed
{
$type = strtolower(trim((string) ($rule['type'] ?? '')));
if ($type !== 'inputnumber') {
return $value;
}
if ($value === null || $value === '') {
return null;
}
if (is_numeric($value)) {
return str_contains((string) $value, '.') ? (float) $value : (int) $value;
}
return null;
}
/**
* 判断是否为虚拟字段。
*
* @param string $field 字段名
* @return bool 是否为虚拟字段
*/
private function isVirtualField(string $field): bool
public function isVirtualField(string $field): bool
{
return str_starts_with($field, self::VIRTUAL_FIELD_PREFIX);
return str_starts_with(trim($field), self::VIRTUAL_FIELD_PREFIX);
}
}

View File

@@ -76,31 +76,9 @@ class SystemConfigPageService extends BaseService
throw new ValidationException('系统配置标签不存在');
}
$keys = [];
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field !== '' && !str_starts_with($field, '__')) {
$keys[] = $field;
}
}
$keys = array_values(array_unique($keys));
if ($keys === []) {
$rowMap = [];
} else {
$rows = $this->systemConfigRepository->query()
->whereIn('config_key', $keys)
->get(['config_key', 'config_value']);
$rowMap = [];
foreach ($rows as $row) {
$rowMap[strtolower((string) $row->config_key)] = (string) ($row->config_value ?? '');
}
}
$rowMap = $this->systemConfigRepository->valueMapByKeys(
$this->systemConfigDefinitionService->fields($tab)
);
$tab['rules'] = $this->systemConfigDefinitionService->hydrateRules($tab, $rowMap);
$tab['formData'] = $this->systemConfigDefinitionService->extractFormData($tab, $rowMap);
@@ -127,17 +105,8 @@ class SystemConfigPageService extends BaseService
$this->validateRequiredValues($tab, $formData);
$this->transaction(function () use ($tab, $formData): void {
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field === '' || str_starts_with($field, '__')) {
continue;
}
$value = $this->stringifyValue($formData[$field] ?? '');
foreach ($this->systemConfigDefinitionService->fields($tab) as $field) {
$value = $this->systemConfigDefinitionService->stringifyValue($formData[$field] ?? '');
$this->systemConfigRepository->updateOrCreate(
['config_key' => $field],
[
@@ -194,23 +163,4 @@ class SystemConfigPageService extends BaseService
return $value === null || $value === '';
}
/**
* 将配置值转换为可保存字符串。
*
* @param array|object|bool|float|int|string|null $value 配置值
* @return string 可保存字符串
* @throws ValidationException
*/
protected function stringifyValue(array|object|bool|float|int|string|null $value): string
{
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_array($value) || is_object($value)) {
throw new ValidationException('系统配置值暂不支持复杂类型');
}
return (string) $value;
}
}

View File

@@ -90,52 +90,13 @@ class SystemConfigRuntimeService extends BaseService
*/
protected function buildValueMap(): array
{
$values = [];
$tabs = $this->systemConfigDefinitionService->tabs();
$keys = [];
foreach ($tabs as $tab) {
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field !== '' && !str_starts_with($field, '__')) {
$keys[] = $field;
}
}
}
$keys = array_values(array_unique($keys));
if ($keys === []) {
$values = $this->systemConfigDefinitionService->allDefaultStorageValues();
if ($values === []) {
return [];
}
$rows = $this->systemConfigRepository->query()
->whereIn('config_key', $keys)
->get(['config_key', 'config_value']);
$rowMap = [];
foreach ($rows as $row) {
$rowMap[strtolower((string) $row->config_key)] = (string) ($row->config_value ?? '');
}
foreach ($tabs as $tab) {
foreach ((array) ($tab['rules'] ?? []) as $rule) {
if (!is_array($rule)) {
continue;
}
$field = strtolower(trim((string) ($rule['field'] ?? '')));
if ($field === '' || str_starts_with($field, '__')) {
continue;
}
$values[$field] = array_key_exists($field, $rowMap)
? (string) $rowMap[$field]
: (string) ($rule['value'] ?? '');
}
foreach ($this->systemConfigRepository->valueMapByKeys(array_keys($values)) as $field => $value) {
$values[$field] = $value;
}
return $values;

View File

@@ -0,0 +1,135 @@
<?php
namespace app\service\system\config;
use app\common\base\BaseService;
/**
* 系统公开配置服务。
*
* 只整理前端可以安全读取的展示类配置,不返回密钥、对象存储凭证等敏感信息。
*/
class SystemPublicConfigService extends BaseService
{
private const DEFAULT_SITE_LOGO = '/assets/brand/mpay-logo.svg';
private const DEFAULT_SITE_LOGO_COMPACT = '/assets/brand/mpay-mark.svg';
/**
* 管理后台展示配置。
*
* @return array<string, mixed>
*/
public function adminPortal(): array
{
return $this->portalConfig('admin_portal_name', '支付中台管理后台');
}
/**
* 商户后台展示配置。
*
* @return array<string, mixed>
*/
public function merchantPortal(): array
{
return array_replace($this->portalConfig('merchant_portal_name', '支付中台商户后台'), [
'merchant_announcement_enabled' => $this->boolConfig('merchant_announcement_enabled', false),
'merchant_announcement' => $this->textConfig('merchant_announcement'),
]);
}
/**
* 收银台展示配置。
*
* @return array<string, mixed>
*/
public function cashier(): array
{
$siteName = $this->textConfig('site_name', 'MPAY 支付中台');
$siteLogo = $this->textConfig('site_logo', self::DEFAULT_SITE_LOGO);
$cashierLogo = $this->textConfig('cashier_logo');
return [
'enabled' => $this->boolConfig('cashier_enabled', true),
'site_name' => $siteName,
'title' => $this->textConfig('cashier_title', 'MPAY 收银台'),
'logo' => $cashierLogo !== '' ? $cashierLogo : $siteLogo,
'notice_enabled' => $this->boolConfig('cashier_notice_enabled', true),
'notice' => $this->textConfig('cashier_notice', '确认支付方式后,系统会创建本次支付尝试并跳转支付页。'),
'show_merchant_name' => $this->boolConfig('cashier_show_merchant_name', true),
'show_order_no' => $this->boolConfig('cashier_show_order_no', true),
'show_pay_type_desc' => $this->boolConfig('cashier_show_pay_type_desc', true),
'poll_interval_seconds' => $this->intConfig('cashier_poll_interval_seconds', 2, 1, 60),
'poll_timeout_seconds' => $this->intConfig('cashier_poll_timeout_seconds', 300, 30, 3600),
'customer_service_enabled' => $this->boolConfig('customer_service_enabled', false),
'customer_service_name' => $this->textConfig('customer_service_name'),
'customer_service_phone' => $this->textConfig('customer_service_phone'),
'customer_service_email' => $this->textConfig('customer_service_email'),
];
}
/**
* 按端整理门户通用展示配置。
*
* @param string $portalNameKey 门户名称配置 key
* @param string $portalNameDefault 门户名称默认值
* @return array<string, mixed>
*/
private function portalConfig(string $portalNameKey, string $portalNameDefault): array
{
return [
'site_name' => $this->textConfig('site_name', 'MPAY 支付中台'),
'site_url' => rtrim($this->textConfig('site_url'), '/'),
'site_logo' => $this->textConfig('site_logo', self::DEFAULT_SITE_LOGO),
'site_logo_compact' => $this->textConfig('site_logo_compact', self::DEFAULT_SITE_LOGO_COMPACT),
'portal_name' => $this->textConfig($portalNameKey, $portalNameDefault),
'customer_service_enabled' => $this->boolConfig('customer_service_enabled', false),
'customer_service_name' => $this->textConfig('customer_service_name'),
'customer_service_phone' => $this->textConfig('customer_service_phone'),
'customer_service_email' => $this->textConfig('customer_service_email'),
];
}
/**
* 读取文本配置。
*
* @param string $key 配置键
* @param string $default 默认值
* @return string 文本值
*/
private function textConfig(string $key, string $default = ''): string
{
$value = trim((string) sys_config($key, $default));
return $value !== '' ? $value : $default;
}
/**
* 读取布尔配置。
*
* @param string $key 配置键
* @param bool $default 默认值
* @return bool 布尔值
*/
private function boolConfig(string $key, bool $default): bool
{
$value = strtolower(trim((string) sys_config($key, $default ? '1' : '0')));
return in_array($value, ['1', 'true', 'yes', 'on', 'enabled'], true);
}
/**
* 读取整数配置。
*
* @param string $key 配置键
* @param int $default 默认值
* @param int $min 最小值
* @param int $max 最大值
* @return int 整数值
*/
private function intConfig(string $key, int $default, int $min, int $max): int
{
$value = (int) sys_config($key, $default);
return min($max, max($min, $value));
}
}