131 lines
5.4 KiB
PHP
131 lines
5.4 KiB
PHP
<?php
|
||
|
||
namespace App\Service\wx;
|
||
|
||
use App\Models\WxAppModel;
|
||
use App\Service\common\FieldEncryptService;
|
||
use App\Service\common\UtilsService;
|
||
|
||
/**
|
||
* 小程序应用凭证读取
|
||
*
|
||
* 请求头 X-App-Code(或参数 app_code)决定用哪个品牌的 AppID。
|
||
* 没带就用 .env 里 WX_DEFAULT_APP_CODE 兜底;都没有就取唯一启用的那条。
|
||
* 库表(nl_wx_app)始终是主源,.env 只在「表里完全没有匹配行」时回落兜底,
|
||
* 这样单品牌部署可以不进后台配置就跑起来,双品牌共用进程仍以表 + 请求头分流为主。
|
||
*/
|
||
class WxAppService
|
||
{
|
||
private static mixed $_instance;
|
||
|
||
public static function getInstance(): null|static
|
||
{
|
||
$name = get_called_class();
|
||
if (!isset(self::$_instance[$name])) {
|
||
self::$_instance[$name] = new static();
|
||
}
|
||
return self::$_instance[$name];
|
||
}
|
||
|
||
/**
|
||
* 取当前请求对应的应用配置(含解密后的密钥)
|
||
*
|
||
* 解析顺序:
|
||
* 1. 按 code(请求头 → .env 默认值)查 nl_wx_app
|
||
* 2. 查不到且 .env 配了 AppID:返回仅含 app_id/name/code 的兜底配置,
|
||
* 密钥仍尝试从表里「按 app_id 匹配」的那行解密;表里也没有就抛错
|
||
* 指引运维去后台填写——密钥不能从 .env 读,安全红线
|
||
* 3. .env 也没配:直接抛错
|
||
*/
|
||
public function current(): array
|
||
{
|
||
$code = $this->currentCode();
|
||
$query = WxAppModel::where('deleted_at', 0)->where('status', 0);
|
||
$app = $code !== '' ? $query->where('code', $code)->first() : $query->first();
|
||
|
||
// 库表命中:解密密钥列后返回
|
||
if (!empty($app)) {
|
||
return $this->loadWithSecrets($app);
|
||
}
|
||
|
||
// 库表未命中,回落 .env(仅 AppID/名称,不含密钥)
|
||
$envAppId = trim((string) config('nl.wx.env_app_id', ''));
|
||
if ($envAppId === '') {
|
||
// .env 没配 AppID 就没法兜底了,直接抛错
|
||
UtilsService::getInstance()->errorThrow('未配置小程序应用(nl_wx_app),请先在后台添加或在 .env 配 WX_APP_ID');
|
||
}
|
||
|
||
$envCode = trim((string) config('nl.wx.default_app_code', ''));
|
||
$envName = trim((string) config('nl.wx.env_app_name', ''));
|
||
|
||
// 兜底场景:AppID 来自 .env,但密钥仍只能从库里按 AppID 找一行来解密
|
||
// 这样老项目硬编码密钥的脏习惯不会回到 .env,密钥始终加密入库
|
||
$secretRow = WxAppModel::where('app_id', $envAppId)
|
||
->where('deleted_at', 0)
|
||
->first(['app_secret', 'mch_key', 'mch_private_key']);
|
||
if (empty($secretRow) || trim((string) ($secretRow['app_secret'] ?? '')) === '') {
|
||
// 表里既没匹配行、或行里没填密钥,只能报错让运维去后台补
|
||
UtilsService::getInstance()->errorThrow(
|
||
'小程序 AppSecret 未配置:请打开侧栏「小程序 → 应用配置」,新增/编辑 AppID=' .
|
||
$envAppId .
|
||
' 的记录并填写 AppSecret(密钥加密入库,不要写进 .env)'
|
||
);
|
||
}
|
||
|
||
$encrypt = FieldEncryptService::getInstance();
|
||
return [
|
||
'id' => 0,
|
||
'code' => $envCode,
|
||
'name' => $envName,
|
||
'app_id' => $envAppId,
|
||
'app_secret' => $encrypt->decryptFromStorage((string) $secretRow['app_secret'], true),
|
||
'mch_id' => '',
|
||
'mch_key' => $encrypt->decryptFromStorage((string) ($secretRow['mch_key'] ?? ''), true),
|
||
'mch_serial_no' => '',
|
||
'mch_private_key' => $encrypt->decryptFromStorage((string) ($secretRow['mch_private_key'] ?? ''), true),
|
||
'platform_public_key' => '',
|
||
'notify_url' => '',
|
||
'template_code' => '',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 当前请求声明的品牌标识
|
||
*
|
||
* 优先级:请求头 X-App-Code > 请求参数 app_code > .env 的 WX_DEFAULT_APP_CODE
|
||
* 全部为空时返回空串,调用方会改走「取唯一启用行」逻辑
|
||
*/
|
||
public function currentCode(): string
|
||
{
|
||
$code = (string) (request()->header('X-App-Code') ?: request()->input('app_code', ''));
|
||
$code = trim($code);
|
||
if ($code !== '') {
|
||
return $code;
|
||
}
|
||
// 请求没带品牌标识时回落 .env 默认值(单品牌部署兜底)
|
||
return trim((string) config('nl.wx.default_app_code', ''));
|
||
}
|
||
|
||
/**
|
||
* 把模型行的密钥列解密后转数组返回
|
||
*/
|
||
private function loadWithSecrets($app): array
|
||
{
|
||
$app = $app->toArray();
|
||
$raw = WxAppModel::where('id', $app['id'])->first(['app_secret', 'mch_key', 'mch_private_key']);
|
||
$encrypt = FieldEncryptService::getInstance();
|
||
$secret = $encrypt->decryptFromStorage($raw['app_secret'] ?? '', true);
|
||
if (trim((string) $secret) === '') {
|
||
UtilsService::getInstance()->errorThrow(
|
||
'小程序 AppSecret 未配置:请打开侧栏「小程序 → 应用配置」,编辑 code=' .
|
||
($app['code'] ?? '') .
|
||
'(AppID=' . ($app['app_id'] ?? '') . ')并填写 AppSecret'
|
||
);
|
||
}
|
||
$app['app_secret'] = $secret;
|
||
$app['mch_key'] = $encrypt->decryptFromStorage($raw['mch_key'] ?? '', true);
|
||
$app['mch_private_key'] = $encrypt->decryptFromStorage($raw['mch_private_key'] ?? '', true);
|
||
return $app;
|
||
}
|
||
}
|