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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
135
app/service/system/config/SystemPublicConfigService.php
Normal file
135
app/service/system/config/SystemPublicConfigService.php
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user