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;
|
|||
|
|
}
|
|||
|
|
}
|