前端配置迁至 config/local.js,完善支付、模板库与部署脚本
- 页面直接引用 local.js 设置 SOON_DEPLOY_CONFIG,移除 deploy-config - Docker sync-config 生成 local.js;更新 README 与 agent-core 说明 - 模板库自动扫描 .soon;新增后端部署/种子/排查脚本 - 完善支付配置、订阅弹窗与后台支付管理页 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Soon\Api\Services;
|
||||
|
||||
use Soon\Api\Core\Config;
|
||||
use Soon\Api\Core\Db;
|
||||
use Soon\Api\Core\Json;
|
||||
|
||||
/**
|
||||
* 支付参数、密钥就绪检查与 Admin 读写。
|
||||
*/
|
||||
final class PaymentConfig
|
||||
{
|
||||
/** @var array<string, list<string>> */
|
||||
private const REQUIRED = [
|
||||
'alipay' => ['site.base_url', 'alipay.app_id', 'alipay.private_key', 'alipay.public_key'],
|
||||
'wechat' => [
|
||||
'site.base_url',
|
||||
'wechat.app_id',
|
||||
'wechat.mch_id',
|
||||
'wechat.mch_serial_no',
|
||||
'wechat.mch_private_key',
|
||||
'wechat.api_v3_key',
|
||||
'wechat.platform_cert',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var array<string, array{label: string, hint: string}> */
|
||||
public const PARAM_FIELDS = [
|
||||
'site.base_url' => [
|
||||
'label' => 'API 外网地址',
|
||||
'hint' => '须 HTTPS 外网可达,用于支付 notify 回调',
|
||||
],
|
||||
'site.front_base_url' => [
|
||||
'label' => '前端站点地址',
|
||||
'hint' => '支付宝支付完成后的 return_url 跳转',
|
||||
],
|
||||
'alipay.app_id' => [
|
||||
'label' => '支付宝 APPID',
|
||||
'hint' => '开放平台应用 ID',
|
||||
],
|
||||
'alipay.sandbox' => [
|
||||
'label' => '支付宝环境',
|
||||
'hint' => '沙箱仅联调使用',
|
||||
],
|
||||
'wechat.app_id' => [
|
||||
'label' => '微信 AppID',
|
||||
'hint' => '关联支付的公众号/小程序/移动应用 AppID',
|
||||
],
|
||||
'wechat.mch_id' => [
|
||||
'label' => '微信商户号',
|
||||
'hint' => '微信支付商户号 mch_id',
|
||||
],
|
||||
'wechat.mch_serial_no' => [
|
||||
'label' => '商户证书序列号',
|
||||
'hint' => '商户 API 证书序列号,用于 V3 签名',
|
||||
],
|
||||
'wechat.sandbox' => [
|
||||
'label' => '微信环境',
|
||||
'hint' => '当前实现共用正式网关,仅作标记',
|
||||
],
|
||||
'payment.display_channels' => [
|
||||
'label' => '前端展示渠道',
|
||||
'hint' => '用户端可选的支付方式,默认仅支付宝',
|
||||
],
|
||||
];
|
||||
|
||||
/** @return list<string> */
|
||||
public static function displayChannels(): array
|
||||
{
|
||||
$raw = Config::get('payment.display_channels');
|
||||
if ($raw === null || $raw === '') {
|
||||
return ['alipay'];
|
||||
}
|
||||
$parts = is_array($raw)
|
||||
? $raw
|
||||
: (preg_split('/[\s,]+/', (string)$raw, -1, PREG_SPLIT_NO_EMPTY) ?: []);
|
||||
$allowed = ['alipay', 'wechat'];
|
||||
$out = [];
|
||||
foreach ($parts as $p) {
|
||||
$p = strtolower(trim((string)$p));
|
||||
if ($p !== '' && in_array($p, $allowed, true) && !in_array($p, $out, true)) {
|
||||
$out[] = $p;
|
||||
}
|
||||
}
|
||||
return $out !== [] ? $out : ['alipay'];
|
||||
}
|
||||
|
||||
public static function assertChannelAllowed(string $channel): void
|
||||
{
|
||||
if (!in_array($channel, self::displayChannels(), true)) {
|
||||
Json::fail('bad_request', '该支付方式暂未开放', 400);
|
||||
}
|
||||
}
|
||||
|
||||
public static function reloadRuntime(): void
|
||||
{
|
||||
$config = Config::all();
|
||||
PaymentConfigLoader::merge($config);
|
||||
SettingsConfigLoader::merge($config);
|
||||
Config::init($config);
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public static function paramValues(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (SettingsConfigLoader::PAYMENT_KEYS as $key) {
|
||||
$out[$key] = self::stringValue($key, Config::get($key));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @param list<array{key: string, value: string}> $items */
|
||||
public static function saveParams(int $adminId, array $items): array
|
||||
{
|
||||
$allowed = array_flip(SettingsConfigLoader::PAYMENT_KEYS);
|
||||
$pdo = Db::pdo();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO settings (`key`, `value`, updated_at) VALUES (:k, :v, :ts) '
|
||||
. 'ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), updated_at=VALUES(updated_at)'
|
||||
);
|
||||
$saved = [];
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$key = trim((string)($item['key'] ?? ''));
|
||||
if ($key === '' || !isset($allowed[$key])) {
|
||||
continue;
|
||||
}
|
||||
$value = trim((string)($item['value'] ?? ''));
|
||||
if ($key === 'site.base_url' && $value !== '' && !str_starts_with(strtolower($value), 'https://')) {
|
||||
Json::fail('bad_request', 'API 外网地址须以 https:// 开头', 400);
|
||||
}
|
||||
if (str_ends_with($key, '.sandbox')) {
|
||||
$value = in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true) ? '1' : '0';
|
||||
}
|
||||
if ($key === 'payment.display_channels') {
|
||||
$parsed = self::parseDisplayChannelsValue($value);
|
||||
if ($parsed === []) {
|
||||
Json::fail('bad_request', '至少选择一种前端展示支付方式', 400);
|
||||
}
|
||||
$value = implode(',', $parsed);
|
||||
}
|
||||
$stmt->execute(['k' => $key, 'v' => $value, 'ts' => $now]);
|
||||
$saved[] = $key;
|
||||
}
|
||||
if ($saved === []) {
|
||||
Json::fail('bad_request', '无有效配置项', 400);
|
||||
}
|
||||
AuditService::log($adminId, 'payment.config.save', 'payment', ['keys' => $saved]);
|
||||
self::reloadRuntime();
|
||||
return ['keys' => $saved];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function adminSnapshot(): array
|
||||
{
|
||||
$baseUrl = rtrim((string)Config::get('site.base_url', ''), '/');
|
||||
return [
|
||||
'params' => self::paramValues(),
|
||||
'display_channels' => self::displayChannels(),
|
||||
'readiness' => self::readiness(),
|
||||
'notify_urls' => [
|
||||
'alipay' => $baseUrl !== '' ? $baseUrl . '/api/v1/pay/alipay/notify' : '',
|
||||
'wechat' => $baseUrl !== '' ? $baseUrl . '/api/v1/pay/wechat/notify' : '',
|
||||
],
|
||||
'return_url_hint' => rtrim((string)Config::get('site.front_base_url', Config::get('site.base_url', '')), '/')
|
||||
. '/pages/member.web.html?paid={order_no}',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, array{ready: bool, missing: list<string>}> */
|
||||
public static function readiness(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::REQUIRED as $channel => $keys) {
|
||||
$missing = [];
|
||||
foreach ($keys as $key) {
|
||||
if (!self::isPresent($key)) {
|
||||
$missing[] = self::missingLabel($key);
|
||||
}
|
||||
}
|
||||
$out[$channel] = ['ready' => $missing === [], 'missing' => $missing];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function isPresent(string $key): bool
|
||||
{
|
||||
$val = Config::get($key);
|
||||
if (is_bool($val)) {
|
||||
return true;
|
||||
}
|
||||
if (is_string($val)) {
|
||||
return trim($val) !== '';
|
||||
}
|
||||
return $val !== null && $val !== '';
|
||||
}
|
||||
|
||||
private static function missingLabel(string $key): string
|
||||
{
|
||||
if (isset(self::PARAM_FIELDS[$key])) {
|
||||
return self::PARAM_FIELDS[$key]['label'];
|
||||
}
|
||||
$map = [
|
||||
'alipay.private_key' => '应用私钥',
|
||||
'alipay.public_key' => '支付宝公钥',
|
||||
'wechat.mch_private_key' => '商户私钥',
|
||||
'wechat.api_v3_key' => 'APIv3 密钥',
|
||||
'wechat.platform_cert' => '平台证书',
|
||||
];
|
||||
return $map[$key] ?? $key;
|
||||
}
|
||||
|
||||
private static function stringValue(string $key, mixed $value): string
|
||||
{
|
||||
if (str_ends_with($key, '.sandbox')) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
if ($key === 'payment.display_channels') {
|
||||
if (is_array($value)) {
|
||||
return implode(',', self::parseDisplayChannelsList($value));
|
||||
}
|
||||
$parsed = self::parseDisplayChannelsValue((string)($value ?? ''));
|
||||
return $parsed !== [] ? implode(',', $parsed) : 'alipay';
|
||||
}
|
||||
return is_string($value) ? $value : (string)($value ?? '');
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function parseDisplayChannelsValue(string $value): array
|
||||
{
|
||||
return self::parseDisplayChannelsList(
|
||||
preg_split('/[\s,]+/', trim($value), -1, PREG_SPLIT_NO_EMPTY) ?: []
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<string> $parts @return list<string> */
|
||||
private static function parseDisplayChannelsList(array $parts): array
|
||||
{
|
||||
$allowed = ['alipay', 'wechat'];
|
||||
$out = [];
|
||||
foreach ($parts as $p) {
|
||||
$p = strtolower(trim((string)$p));
|
||||
if ($p !== '' && in_array($p, $allowed, true) && !in_array($p, $out, true)) {
|
||||
$out[] = $p;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user