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