') —— 核心配置(既有部署可能把密钥写在核心 config/pay.php)。 * * 这样插件单独分发时能独立工作,装在有核心配置的站点上也不会取到两套冲突值。 */ class PayConfig { /** 进程内缓存 */ private static array $cache = []; public static function get(string $channel = ''): array { $static = self::staticConfig(); if ($channel === '') { return [ 'mock_enable' => $static['mock_enable'] ?? false, 'alipay' => self::channel('alipay'), 'wechat' => self::channel('wechat'), ]; } return self::channel($channel); } public static function mockEnabled(): bool { return (bool) (self::staticConfig()['mock_enable'] ?? false); } private static function channel(string $channel): array { if (isset(self::$cache[$channel])) { return self::$cache[$channel]; } $static = self::staticConfig(); $config = (array) ($static[$channel] ?? []); // 核心配置覆盖(只覆盖非空的标量值,避免把插件解析好的绝对路径冲成空串) foreach ((array) config('pay.' . $channel, []) as $key => $value) { if ($value === null || $value === '' || $value === []) { continue; } $config[$key] = $value; } // 私钥兜底归一化:核心配置可能直接给了裸 base64(未做 PKCS8 判定) $secretKey = $channel === 'wechat' ? 'mch_secret_cert' : 'app_secret_cert'; if (!empty($config[$secretKey]) && is_string($config[$secretKey])) { $config[$secretKey] = self::normalizePrivateKey($config[$secretKey]); } return self::$cache[$channel] = $config; } private static function staticConfig(): array { if (!isset(self::$cache['__static'])) { $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'paycenter.php'; self::$cache['__static'] = is_file($file) ? (array) include $file : []; } return self::$cache['__static']; } public static function clear(): void { self::$cache = []; } /** * 私钥归一化(与 config/paycenter.php 内逻辑一致,见那里逐条注释): * PKCS8 必须自带 'PRIVATE KEY' 头,PKCS1 必须交裸 base64 让 SDK 去包 'RSA PRIVATE KEY' 头。 */ public static function normalizePrivateKey(?string $secret): string { $value = trim((string) $secret); if ($value === '') { return ''; } if (is_file($value)) { $value = (string) file_get_contents($value); } $body = (string) preg_replace('/-----[^-]+-----|\s+/', '', $value); if ($body === '') { return ''; } $der = base64_decode($body, true) ?: ''; $isPkcs8 = str_contains(substr($der, 0, 64), "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01"); return $isPkcs8 ? "-----BEGIN PRIVATE KEY-----\n" . wordwrap($body, 64, "\n", true) . "\n-----END PRIVATE KEY-----" : $body; } /** * 私钥 → openssl 可直接解析的 PEM。 * * 注意与 normalizePrivateKey() 的区别:后者是喂给 SDK 的形态(PKCS1 必须留裸 base64, * 由 SDK 的 get_private_cert() 去包 RSA 头),直接交给 openssl_pkey_get_private() 会失败。 * 这里按 DER 判定给出正确的 PEM 头,供「真的签一次名」这类本地校验使用。 */ public static function toPem(?string $secret): string { $value = trim((string) $secret); if ($value === '') { return ''; } if (is_file($value)) { $value = (string) file_get_contents($value); } $body = (string) preg_replace('/-----[^-]+-----|\s+/', '', $value); if ($body === '') { return ''; } $der = base64_decode($body, true) ?: ''; $isPkcs8 = str_contains(substr($der, 0, 64), "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01"); $header = $isPkcs8 ? 'PRIVATE KEY' : 'RSA PRIVATE KEY'; return "-----BEGIN {$header}-----\n" . wordwrap($body, 64, "\n", true) . "\n-----END {$header}-----"; } /** * 相对路径按项目根目录解析(SDK 按进程 CWD 解析,Web 入口 CWD 是 public/) */ public static function resolvePath(?string $path): string { $path = trim((string) $path); if ($path === '') { return ''; } if (str_starts_with($path, '/') || str_starts_with($path, '\\') || preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) { return $path; } $root = function_exists('root_path') ? root_path() : dirname(__DIR__, 3) . DIRECTORY_SEPARATOR; return $root . ltrim($path, '/\\'); } }