Files
nl-admin-api/app/Service/WechatAccountService.php

276 lines
10 KiB
PHP
Raw Normal View History

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