feat: 同步本地全量开发到 GitHub(前端/后端/超管/开发文档规则)
含 AI 助手、抖音 OAuth、流量池推送、支付获客、SuperAdmin 设备与场景管理等本地迭代;以本地为准单向推送,未拉取远程。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
/**
|
||||
* 开放接口 · 内容库
|
||||
*
|
||||
* 鉴权:HTTP Header `X-API-KEY: <library.externalApiKey>`
|
||||
* (也允许 query/body 参数 `apiKey`,但仅推荐 header 方式)
|
||||
*
|
||||
* 提供两类外部能力:
|
||||
* POST /v1/open/content-library/push-item — 第三方把素材写入指定内容库
|
||||
* GET /v1/open/content-library/items — 第三方按 libraryId 拉取素材列表
|
||||
*
|
||||
* 真源:开发文档/5、接口/03-业务板块接口/06-内容库接口.md
|
||||
*
|
||||
* @package app\common\controller
|
||||
*/
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\cunkebao\model\ContentLibrary;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
class OpenContentLibraryController extends Controller
|
||||
{
|
||||
/** 单页最大数量 */
|
||||
const MAX_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* 解析 apiKey 并定位内容库;失败抛 401/403/404
|
||||
* @return array 内容库行(含 companyId、id、externalApiKey 等)
|
||||
*/
|
||||
private function ensureExternalApiSchemaReady()
|
||||
{
|
||||
if (!ContentLibrary::externalApiColumnsReady()) {
|
||||
ResponseHelper::error(
|
||||
503,
|
||||
'外部接口字段未就绪,请先执行迁移:Server/database/migrations/20260526_content_library_external_api.sql'
|
||||
)->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function authLibrary(): array
|
||||
{
|
||||
$this->ensureExternalApiSchemaReady();
|
||||
|
||||
$apiKey = (string) Request::header('X-API-KEY', '');
|
||||
if ($apiKey === '') {
|
||||
$apiKey = (string) Request::param('apiKey', '');
|
||||
}
|
||||
$apiKey = trim($apiKey);
|
||||
if ($apiKey === '') {
|
||||
ResponseHelper::unauthorized('缺少 apiKey(请在 Header X-API-KEY 中传入)')->send();
|
||||
exit;
|
||||
}
|
||||
|
||||
// 先按 libraryId+apiKey 精确匹配(更高效,并避免歧义)
|
||||
$libraryId = intval(Request::param('libraryId', 0));
|
||||
$where = [
|
||||
['externalApiKey', '=', $apiKey],
|
||||
['externalApiEnabled', '=', 1],
|
||||
['isDel', '=', 0],
|
||||
];
|
||||
if ($libraryId > 0) {
|
||||
$where[] = ['id', '=', $libraryId];
|
||||
}
|
||||
$library = ContentLibrary::where($where)->find();
|
||||
if (!$library) {
|
||||
ResponseHelper::unauthorized('apiKey 无效或对应的内容库未启用外部接口')->send();
|
||||
exit;
|
||||
}
|
||||
return $library->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入审计日志(失败静默,不影响主流程)
|
||||
*/
|
||||
private function logCall(array $library, string $action, int $code, string $msg, $payloadDigest = null): void
|
||||
{
|
||||
try {
|
||||
Db::name('content_library_external_log')->insert([
|
||||
'libraryId' => (int)($library['id'] ?? 0),
|
||||
'companyId' => (int)($library['companyId'] ?? 0),
|
||||
'action' => $action,
|
||||
'clientIp' => substr((string) Request::ip(), 0, 64),
|
||||
'apiKey' => substr((string)($library['externalApiKey'] ?? ''), 0, 8),
|
||||
'payload' => is_array($payloadDigest) ? json_encode($payloadDigest, JSON_UNESCAPED_UNICODE) : (is_string($payloadDigest) ? $payloadDigest : null),
|
||||
'responseCode' => $code,
|
||||
'responseMsg' => mb_substr((string) $msg, 0, 240),
|
||||
'createTime' => time(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查(公开)
|
||||
* GET /v1/open/content-library/health
|
||||
*/
|
||||
public function health()
|
||||
{
|
||||
return ResponseHelper::success([
|
||||
'service' => 'cunkebao-open-content-library',
|
||||
'version' => '1.0',
|
||||
'time' => time(),
|
||||
], 'ok');
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入素材到指定内容库
|
||||
* POST /v1/open/content-library/push-item
|
||||
* Header: X-API-KEY: <apiKey>
|
||||
* Body : libraryId(int) contentType(int) content(string) title(string?) resUrls(array?) urls(array?) coverImage(string?)
|
||||
*
|
||||
* contentType 取值:1 图片 / 2 链接 / 3 视频 / 4 文本 / 5 小程序 / 6 图文
|
||||
*/
|
||||
public function pushItem()
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return ResponseHelper::error(400, '请使用 POST 方法');
|
||||
}
|
||||
$library = $this->authLibrary();
|
||||
if (empty($library['externalAllowPushIn'])) {
|
||||
$this->logCall($library, 'push_in', 403, '该内容库未开放外部推入');
|
||||
return ResponseHelper::error(403, '该内容库未开放外部推入');
|
||||
}
|
||||
|
||||
$param = $this->request->post();
|
||||
$contentType = isset($param['contentType']) ? intval($param['contentType']) : 0;
|
||||
$allowedTypes = [1, 2, 3, 4, 5, 6];
|
||||
if (!in_array($contentType, $allowedTypes, true)) {
|
||||
$this->logCall($library, 'push_in', 400, 'contentType 不合法');
|
||||
return ResponseHelper::error(400, 'contentType 不合法(合法值:1图片 2链接 3视频 4文本 5小程序 6图文)');
|
||||
}
|
||||
$content = trim((string)($param['content'] ?? ''));
|
||||
if ($content === '') {
|
||||
$this->logCall($library, 'push_in', 400, 'content 不能为空');
|
||||
return ResponseHelper::error(400, 'content 不能为空');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$row = [
|
||||
'libraryId' => (int) $library['id'],
|
||||
'companyId' => (int) $library['companyId'],
|
||||
'userId' => (int) ($library['userId'] ?? 0),
|
||||
'contentType' => $contentType,
|
||||
'title' => mb_substr((string)($param['title'] ?? ''), 0, 200),
|
||||
'content' => $content,
|
||||
'resUrls' => isset($param['resUrls']) ? json_encode($param['resUrls'], JSON_UNESCAPED_UNICODE) : json_encode([]),
|
||||
'urls' => isset($param['urls']) ? json_encode($param['urls'], JSON_UNESCAPED_UNICODE) : json_encode([]),
|
||||
'coverImage' => (string)($param['coverImage'] ?? ''),
|
||||
'sendTime' => $now,
|
||||
'createTime' => $now,
|
||||
'updateTime' => $now,
|
||||
'isDel' => 0,
|
||||
'senderNickname' => mb_substr((string)($param['senderNickname'] ?? '外部接口'), 0, 50),
|
||||
'senderAvatar' => (string)($param['senderAvatar'] ?? ''),
|
||||
];
|
||||
|
||||
try {
|
||||
$insertId = Db::name('content_item')->insertGetId($row);
|
||||
$this->logCall($library, 'push_in', 200, 'ok', ['contentType' => $contentType, 'len' => mb_strlen($content)]);
|
||||
return ResponseHelper::success([
|
||||
'id' => (int) $insertId,
|
||||
'libraryId' => (int) $library['id'],
|
||||
], 'ok');
|
||||
} catch (\Throwable $e) {
|
||||
$this->logCall($library, 'push_in', 500, $e->getMessage());
|
||||
return ResponseHelper::error(500, '写入失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取指定内容库下的素材列表(仅未删除)
|
||||
* GET /v1/open/content-library/items?libraryId=&page=1&limit=20&contentType=
|
||||
* Header: X-API-KEY: <apiKey>
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
$library = $this->authLibrary();
|
||||
if (empty($library['externalAllowPullOut'])) {
|
||||
$this->logCall($library, 'pull_out', 403, '该内容库未开放外部拉取');
|
||||
return ResponseHelper::error(403, '该内容库未开放外部拉取');
|
||||
}
|
||||
|
||||
$page = max(1, intval(Request::param('page', 1)));
|
||||
$limit = max(1, intval(Request::param('limit', 20)));
|
||||
if ($limit > self::MAX_LIMIT) {
|
||||
$limit = self::MAX_LIMIT;
|
||||
}
|
||||
$contentType = Request::param('contentType', '');
|
||||
|
||||
$where = [
|
||||
['libraryId', '=', (int) $library['id']],
|
||||
['isDel', '=', 0],
|
||||
];
|
||||
if ($contentType !== '' && $contentType !== null) {
|
||||
$where[] = ['contentType', '=', intval($contentType)];
|
||||
}
|
||||
|
||||
$query = Db::name('content_item')->where($where);
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query
|
||||
->field('id,libraryId,contentType,title,content,resUrls,urls,coverImage,senderNickname,senderAvatar,sendTime,createTime')
|
||||
->order('id DESC')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
$list = array_map(function ($r) {
|
||||
$r['resUrls'] = json_decode($r['resUrls'] ?? '[]', true) ?: [];
|
||||
$r['urls'] = json_decode($r['urls'] ?? '[]', true) ?: [];
|
||||
return $r;
|
||||
}, $rows);
|
||||
|
||||
$this->logCall($library, 'pull_out', 200, 'ok', ['page' => $page, 'limit' => $limit]);
|
||||
return ResponseHelper::success([
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'libraryId' => (int) $library['id'],
|
||||
'list' => $list,
|
||||
], 'ok');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user