276 lines
10 KiB
PHP
276 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Service;
|
||
|
||
use App\BaseApp\BaseService;
|
||
use App\Models\wechat\WechatAccountModel;
|
||
use App\Service\common\FieldEncryptService;
|
||
use App\Service\common\wechat\WechatApiClient;
|
||
|
||
/**
|
||
* 公众号账号配置服务:CRUD(AppSecret 入库 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;
|
||
}
|
||
}
|