Files
nl-admin-api/app/Service/WechatAccountService.php
李琦 6e2769c528
Some checks failed
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
优化部分代码,增加微信公众号模板
2026-08-13 19:02:05 +08:00

276 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service;
use App\BaseApp\BaseService;
use App\Models\wechat\WechatAccountModel;
use App\Service\common\FieldEncryptService;
use App\Service\common\wechat\WechatApiClient;
/**
* 公众号账号配置服务CRUDAppSecret 入库 AES 加密)、默认账号切换、连通性测试
* 对齐 OssConfigService 的加密模式list/detail 永不回显 secret 明文,只给 has_secret 标记
*/
class WechatAccountService extends BaseService
{
public function __construct()
{
parent::__construct();
$this->model = WechatAccountModel::class;
$this->selectField = [
'id', 'name', 'appid', 'app_secret', 'original_id',
'is_default', 'status', 'remark', 'sort', 'created_at', 'updated_at',
];
$this->queryField = [
'name' => 'like',
'appid' => 'like',
'status' => '=',
];
$this->orderBy = [
'name' => 'sort',
'sort' => 'desc',
];
}
/**
* 分页列表脱敏app_secret 整字段剔除,返回 has_secret 布尔)
*/
public function list(): array
{
$result = $this->getPageList();
$result['items'] = $this->maskRows($result['items'] ?? []);
return $result;
}
/**
* 下拉选项(发布弹窗选账号用):仅启用账号,默认账号排最前
*/
public function option(): mixed
{
return WechatAccountModel::where('deleted_at', 0)
->where('status', 1)
->orderByDesc('is_default')
->orderByDesc('sort')
->get(['id', 'name', 'appid', 'is_default']);
}
/**
* 详情(同样剔除 secret只给 has_secret
*/
public function detail($id): mixed
{
$info = WechatAccountModel::where('id', $id)->where('deleted_at', 0)->first($this->selectField);
if (empty($info)) {
return $this->utils->notFound('账号不存在');
}
$rows = $this->maskRows([$info->toArray()]);
return $rows[0];
}
/**
* 新增账号secret 用 AES 加密入库is_default=1 时清掉其它默认标记
*/
public function create($params): mixed
{
$name = trim((string) ($params['name'] ?? ''));
$appid = trim((string) ($params['appid'] ?? ''));
$secret = trim((string) ($params['app_secret'] ?? ''));
if ($name === '' || $appid === '' || $secret === '') {
$this->utils->errorThrow('请填写公众号名称、AppID 与 AppSecret');
}
// 同一 AppID 不允许重复配置(未删除范围内)
$exists = WechatAccountModel::where('appid', $appid)->where('deleted_at', 0)->exists();
if ($exists) {
$this->utils->errorThrow('该 AppID 已存在,请勿重复配置');
}
$now = time();
$isDefault = (int) ($params['is_default'] ?? 0);
if ($isDefault === 1) {
WechatAccountModel::where('deleted_at', 0)->update(['is_default' => 0, 'updated_at' => $now]);
}
$id = WechatAccountModel::insertGetId([
'name' => $name,
'appid' => $appid,
'app_secret' => FieldEncryptService::getInstance()->encryptForStorage($secret),
'original_id' => trim((string) ($params['original_id'] ?? '')),
'is_default' => $isDefault,
'status' => (int) ($params['status'] ?? 1),
'remark' => trim((string) ($params['remark'] ?? '')),
'sort' => (int) ($params['sort'] ?? 0),
'created_at' => $now,
'updated_at' => 0,
'deleted_at' => 0,
]);
return ['id' => $id];
}
/**
* 更新账号app_secret 传空 = 不改密钥(前端「已配置」占位不回传)
*/
public function update($id, $params): mixed
{
$id = (int) $id;
$info = WechatAccountModel::where('id', $id)->where('deleted_at', 0)->first();
if (empty($info)) {
return $this->utils->notFound('账号不存在');
}
$data = [];
if (array_key_exists('name', $params)) {
$name = trim((string) $params['name']);
if ($name === '') {
$this->utils->errorThrow('公众号名称不能为空');
}
$data['name'] = $name;
}
if (array_key_exists('appid', $params)) {
$appid = trim((string) $params['appid']);
if ($appid === '') {
$this->utils->errorThrow('AppID 不能为空');
}
// 改 AppID 时同样查重(排除自己)
$exists = WechatAccountModel::where('appid', $appid)
->where('deleted_at', 0)->where('id', '<>', $id)->exists();
if ($exists) {
$this->utils->errorThrow('该 AppID 已被其它账号使用');
}
$data['appid'] = $appid;
}
foreach (['original_id', 'remark'] as $field) {
if (array_key_exists($field, $params)) {
$data[$field] = trim((string) $params[$field]);
}
}
if (array_key_exists('status', $params)) {
$data['status'] = (int) $params['status'];
}
if (array_key_exists('sort', $params)) {
$data['sort'] = (int) $params['sort'];
}
// 密钥字段只在非空时更新(空串代表用户没改)
$secret = trim((string) ($params['app_secret'] ?? ''));
if ($secret !== '') {
$data['app_secret'] = FieldEncryptService::getInstance()->encryptForStorage($secret);
}
$now = time();
if (array_key_exists('is_default', $params) && (int) $params['is_default'] === 1) {
WechatAccountModel::where('deleted_at', 0)->where('id', '<>', $id)
->update(['is_default' => 0, 'updated_at' => $now]);
$data['is_default'] = 1;
} elseif (array_key_exists('is_default', $params)) {
$data['is_default'] = (int) $params['is_default'];
}
if (empty($data)) {
return true;
}
$data['updated_at'] = $now;
return WechatAccountModel::where('id', $id)->where('deleted_at', 0)->update($data);
}
/**
* 软删除(批量)
*/
public function delete($id): mixed
{
$ids = array_values(array_filter(array_map('intval', is_array($id) ? $id : [$id])));
if (empty($ids)) {
$this->utils->errorThrow('参数错误');
}
return WechatAccountModel::whereIn('id', $ids)->where('deleted_at', 0)->update([
'deleted_at' => time(),
'updated_at' => time(),
'is_default' => 0,
]);
}
/**
* 切换默认发布账号(全局仅一个)
*/
public function setDefault(array $params): array
{
$id = (int) ($params['id'] ?? 0);
if ($id <= 0) {
$this->utils->errorThrow('请选择账号');
}
$info = WechatAccountModel::where('id', $id)->where('deleted_at', 0)->first();
if (empty($info)) {
$this->utils->notFound('账号不存在');
}
if ((int) $info->status !== 1) {
$this->utils->errorThrow('该账号已禁用,无法设为默认');
}
$now = time();
WechatAccountModel::where('deleted_at', 0)->update(['is_default' => 0, 'updated_at' => $now]);
WechatAccountModel::where('id', $id)->update(['is_default' => 1, 'updated_at' => $now]);
return ['id' => $id];
}
/**
* 测试连通性:解密 secret 后直连微信换 token 验证(不落缓存)
* 支持两种入参:已有账号传 id表单未保存时直接传 appid+app_secret
*/
public function testConnection(array $params): array
{
$id = (int) ($params['id'] ?? 0);
$appid = trim((string) ($params['appid'] ?? ''));
$secret = trim((string) ($params['app_secret'] ?? ''));
if ($id > 0) {
$info = WechatAccountModel::where('id', $id)->where('deleted_at', 0)->first();
if (empty($info)) {
$this->utils->notFound('账号不存在');
}
$appid = $appid !== '' ? $appid : (string) $info->appid;
// 表单里没重填 secret 时,用库里已配置的密文解密测试
if ($secret === '') {
$secret = FieldEncryptService::getInstance()->decryptFromStorage((string) $info->app_secret);
}
}
if ($appid === '' || $secret === '') {
$this->utils->errorThrow('请填写 AppID 与 AppSecret');
}
return WechatApiClient::getInstance()->testCredential($appid, $secret);
}
/**
* 发布链路取账号:指定 id 优先,缺省回落默认账号;返回含解密后 secret 的数组
* 为什么放这里:解密动作收敛在账号服务内,文章服务只拿可用凭据
*/
public function getPublishAccount(int $accountId = 0): array
{
$query = WechatAccountModel::where('deleted_at', 0)->where('status', 1);
if ($accountId > 0) {
$info = (clone $query)->where('id', $accountId)->first();
if (empty($info)) {
$this->utils->errorThrow('指定的公众号账号不存在或已禁用');
}
} else {
$info = (clone $query)->where('is_default', 1)->first();
if (empty($info)) {
$this->utils->errorThrow('未配置默认公众号账号,请先到「公众号账号」设置默认账号');
}
}
$secret = FieldEncryptService::getInstance()->decryptFromStorage((string) $info->app_secret);
if ($secret === '') {
$this->utils->errorThrow('该账号的 AppSecret 解密失败,请重新保存密钥');
}
return [
'id' => (int) $info->id,
'name' => (string) $info->name,
'appid' => (string) $info->appid,
'secret' => $secret,
];
}
/**
* 行脱敏secret 密文剔除,替换为 has_secret 布尔标记
*/
private function maskRows(array $rows): array
{
foreach ($rows as &$row) {
$row['has_secret'] = trim((string) ($row['app_secret'] ?? '')) !== '';
unset($row['app_secret']);
}
unset($row);
return $rows;
}
}