|
@@ -29,9 +29,6 @@ class CanvasService
|
|
|
/** 关联来源类型白名单 */
|
|
/** 关联来源类型白名单 */
|
|
|
const SOURCE_TYPES = ['none', 'product', 'episode'];
|
|
const SOURCE_TYPES = ['none', 'product', 'episode'];
|
|
|
|
|
|
|
|
- /** 当前支持的画布文档 schema 版本 */
|
|
|
|
|
- const SUPPORTED_SCHEMA_VERSION = 1;
|
|
|
|
|
-
|
|
|
|
|
/** 画布文档大小与图规模限制 */
|
|
/** 画布文档大小与图规模限制 */
|
|
|
const MAX_DOCUMENT_BYTES = 2097152; // 2MB
|
|
const MAX_DOCUMENT_BYTES = 2097152; // 2MB
|
|
|
const MAX_NODES = 2000;
|
|
const MAX_NODES = 2000;
|
|
@@ -52,6 +49,7 @@ class CanvasService
|
|
|
$cpid = Site::getCpid();
|
|
$cpid = Site::getCpid();
|
|
|
$anime_id = (int)getProp($data, 'anime_id', 0);
|
|
$anime_id = (int)getProp($data, 'anime_id', 0);
|
|
|
$episode_id = (int)getProp($data, 'episode_id', 0);
|
|
$episode_id = (int)getProp($data, 'episode_id', 0);
|
|
|
|
|
+ $title = getProp($data, 'title');
|
|
|
$page = max(1, (int)getProp($data, 'page', 1));
|
|
$page = max(1, (int)getProp($data, 'page', 1));
|
|
|
$page_size = min(100, max(1, (int)getProp($data, 'page_size', 20)));
|
|
$page_size = min(100, max(1, (int)getProp($data, 'page_size', 20)));
|
|
|
|
|
|
|
@@ -60,8 +58,7 @@ class CanvasService
|
|
|
->where('cpid', $cpid)
|
|
->where('cpid', $cpid)
|
|
|
->where('is_deleted', 0)
|
|
->where('is_deleted', 0)
|
|
|
->select('canvas_uuid as canvas_id', 'name as title', 'description', 'cover_url',
|
|
->select('canvas_uuid as canvas_id', 'name as title', 'description', 'cover_url',
|
|
|
- 'anime_id', 'episode_id', 'episode_number', 'schema_version', 'revision',
|
|
|
|
|
- 'created_at', 'updated_at');
|
|
|
|
|
|
|
+ 'anime_id', 'episode_id', 'episode_number', 'created_at', 'updated_at');
|
|
|
|
|
|
|
|
if ($anime_id) {
|
|
if ($anime_id) {
|
|
|
$query->where('anime_id', $anime_id);
|
|
$query->where('anime_id', $anime_id);
|
|
@@ -69,6 +66,9 @@ class CanvasService
|
|
|
if ($episode_id) {
|
|
if ($episode_id) {
|
|
|
$query->where('episode_id', $episode_id);
|
|
$query->where('episode_id', $episode_id);
|
|
|
}
|
|
}
|
|
|
|
|
+ if ($title) {
|
|
|
|
|
+ $query->where('name', 'like', "%$title%");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
$list = $query->orderByDesc('id')->paginate($page_size, ['*'], 'page', $page);
|
|
$list = $query->orderByDesc('id')->paginate($page_size, ['*'], 'page', $page);
|
|
|
|
|
|
|
@@ -92,9 +92,11 @@ class CanvasService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 创建画布(完整文档模式,兼容旧的 name-only 创建)
|
|
|
|
|
|
|
+ * 创建画布
|
|
|
*
|
|
*
|
|
|
- * @param array $data 完整画布文档或旧版 name/description/cover_url
|
|
|
|
|
|
|
+ * 必填:name、document;canvas_id 可选(缺省自动生成);可选:anime_id、episode_id、episode_number
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param array $data
|
|
|
* @return array
|
|
* @return array
|
|
|
*/
|
|
*/
|
|
|
public function createCanvas($data)
|
|
public function createCanvas($data)
|
|
@@ -103,54 +105,39 @@ class CanvasService
|
|
|
$cpid = Site::getCpid();
|
|
$cpid = Site::getCpid();
|
|
|
$now = date('Y-m-d H:i:s');
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
|
|
|
|
- if ($this->isDocumentRequest($data)) {
|
|
|
|
|
- $doc = $this->validateDocument($data, true);
|
|
|
|
|
- $canvas_uuid = $doc['canvas_id'];
|
|
|
|
|
- if (DB::table('mp_canvases')->where('canvas_uuid', $canvas_uuid)->exists()) {
|
|
|
|
|
- Utils::throwError('20003:画布ID已存在');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $row = [
|
|
|
|
|
- 'uid' => $uid,
|
|
|
|
|
- 'cpid' => $cpid,
|
|
|
|
|
- 'canvas_uuid' => $canvas_uuid,
|
|
|
|
|
- 'name' => $doc['title'],
|
|
|
|
|
- 'description' => (string)getProp($data, 'description', ''),
|
|
|
|
|
- 'cover_url' => (string)getProp($data, 'cover_url', ''),
|
|
|
|
|
- 'anime_id' => $doc['scope']['anime_id'],
|
|
|
|
|
- 'episode_id' => $doc['scope']['episode_id'],
|
|
|
|
|
- 'episode_number' => $doc['scope']['episode_number'],
|
|
|
|
|
- 'schema_version' => self::SUPPORTED_SCHEMA_VERSION,
|
|
|
|
|
- 'revision' => 1,
|
|
|
|
|
- 'document' => json_encode($doc, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
- 'is_deleted' => 0,
|
|
|
|
|
- 'created_at' => $now,
|
|
|
|
|
- 'updated_at' => $now
|
|
|
|
|
- ];
|
|
|
|
|
- $canvas_id = DB::table('mp_canvases')->insertGetId($row);
|
|
|
|
|
- if (!$canvas_id) {
|
|
|
|
|
- Utils::throwError('20003:创建画布失败');
|
|
|
|
|
- }
|
|
|
|
|
- $row['id'] = $canvas_id;
|
|
|
|
|
- return $this->buildDocument((object)$row);
|
|
|
|
|
|
|
+ $canvas_uuid = trim((string)getProp($data, 'canvas_id', ''));
|
|
|
|
|
+ if ($canvas_uuid === '') {
|
|
|
|
|
+ $canvas_uuid = $this->generateCanvasId();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mb_strlen($canvas_uuid) > 64) {
|
|
|
|
|
+ Utils::throwError('20003:canvas_id长度超出限制');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (DB::table('mp_canvases')->where('canvas_uuid', $canvas_uuid)->exists()) {
|
|
|
|
|
+ Utils::throwError('20003:画布ID已存在');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 旧版 name-only 创建,保留兼容
|
|
|
|
|
- $name = trim((string)getProp($data, 'name', ''));
|
|
|
|
|
|
|
+ $name = trim((string)$this->docProp($data, 'name', ''));
|
|
|
if ($name === '') {
|
|
if ($name === '') {
|
|
|
Utils::throwError('20003:请提供画布名称');
|
|
Utils::throwError('20003:请提供画布名称');
|
|
|
}
|
|
}
|
|
|
- $canvas_uuid = $this->generateCanvasId();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $document = $this->documentForStorage($this->extractDocument($data));
|
|
|
|
|
+ if ($document === null) {
|
|
|
|
|
+ Utils::throwError('20003:请提供document');
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->checkDocumentSize($document);
|
|
|
|
|
+
|
|
|
$canvas_id = DB::table('mp_canvases')->insertGetId([
|
|
$canvas_id = DB::table('mp_canvases')->insertGetId([
|
|
|
'uid' => $uid,
|
|
'uid' => $uid,
|
|
|
'cpid' => $cpid,
|
|
'cpid' => $cpid,
|
|
|
'canvas_uuid' => $canvas_uuid,
|
|
'canvas_uuid' => $canvas_uuid,
|
|
|
'name' => $name,
|
|
'name' => $name,
|
|
|
- 'description' => (string)getProp($data, 'description', ''),
|
|
|
|
|
- 'cover_url' => (string)getProp($data, 'cover_url', ''),
|
|
|
|
|
- 'schema_version' => self::SUPPORTED_SCHEMA_VERSION,
|
|
|
|
|
- 'revision' => 1,
|
|
|
|
|
- 'document' => null,
|
|
|
|
|
|
|
+ 'description' => (string)$this->docProp($data, 'description', ''),
|
|
|
|
|
+ 'cover_url' => (string)$this->docProp($data, 'cover_url', ''),
|
|
|
|
|
+ 'anime_id' => $this->scopeProp($data, 'anime_id'),
|
|
|
|
|
+ 'episode_id' => $this->scopeProp($data, 'episode_id'),
|
|
|
|
|
+ 'episode_number' => $this->scopeProp($data, 'episode_number'),
|
|
|
|
|
+ 'document' => $document,
|
|
|
'is_deleted' => 0,
|
|
'is_deleted' => 0,
|
|
|
'created_at' => $now,
|
|
'created_at' => $now,
|
|
|
'updated_at' => $now
|
|
'updated_at' => $now
|
|
@@ -159,13 +146,15 @@ class CanvasService
|
|
|
Utils::throwError('20003:创建画布失败');
|
|
Utils::throwError('20003:创建画布失败');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return $this->canvasDetail(['canvas_id' => $canvas_uuid]);
|
|
|
|
|
|
|
+ return $this->documentResult($canvas_uuid, $name, $document);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 保存画布完整文档;revision 不匹配时返回冲突标记
|
|
|
|
|
|
|
+ * 保存画布完整文档
|
|
|
*
|
|
*
|
|
|
- * @param array $data 完整画布文档
|
|
|
|
|
|
|
+ * 必填:canvas_id、name、document;可选:anime_id、episode_id、episode_number(不传保留原值)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param array $data
|
|
|
* @return array
|
|
* @return array
|
|
|
*/
|
|
*/
|
|
|
public function editCanvas($data)
|
|
public function editCanvas($data)
|
|
@@ -176,32 +165,33 @@ class CanvasService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$canvas = $this->checkCanvas($canvas_id);
|
|
$canvas = $this->checkCanvas($canvas_id);
|
|
|
- $doc = $this->validateDocument($data, false, $canvas);
|
|
|
|
|
|
|
|
|
|
- if ((int)$doc['revision'] !== (int)$canvas->revision) {
|
|
|
|
|
- return [
|
|
|
|
|
- 'revision_conflict' => true,
|
|
|
|
|
- 'canvas_id' => $this->canvasKey($canvas),
|
|
|
|
|
- 'revision' => (int)$canvas->revision
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ $name = trim((string)$this->docProp($data, 'name', ''));
|
|
|
|
|
+ if ($name === '') {
|
|
|
|
|
+ Utils::throwError('20003:请提供画布名称');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $next_revision = (int)$canvas->revision + 1;
|
|
|
|
|
- $doc['revision'] = $next_revision;
|
|
|
|
|
- $doc['canvas_id'] = $this->canvasKey($canvas);
|
|
|
|
|
-
|
|
|
|
|
- DB::table('mp_canvases')->where('id', $canvas->id)->update([
|
|
|
|
|
- 'name' => $doc['title'],
|
|
|
|
|
- 'anime_id' => $doc['scope']['anime_id'],
|
|
|
|
|
- 'episode_id' => $doc['scope']['episode_id'],
|
|
|
|
|
- 'episode_number' => $doc['scope']['episode_number'],
|
|
|
|
|
- 'schema_version' => self::SUPPORTED_SCHEMA_VERSION,
|
|
|
|
|
- 'revision' => $next_revision,
|
|
|
|
|
- 'document' => json_encode($doc, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
- 'updated_at' => date('Y-m-d H:i:s')
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ $document = $this->documentForStorage($this->extractDocument($data));
|
|
|
|
|
+ if ($document === null) {
|
|
|
|
|
+ Utils::throwError('20003:请提供document');
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->checkDocumentSize($document);
|
|
|
|
|
+
|
|
|
|
|
+ $update = [
|
|
|
|
|
+ 'name' => $name,
|
|
|
|
|
+ 'document' => $document,
|
|
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
|
|
+ ];
|
|
|
|
|
+ foreach (['anime_id', 'episode_id', 'episode_number'] as $field) {
|
|
|
|
|
+ $value = $this->scopeProp($data, $field, null);
|
|
|
|
|
+ if ($value !== null) {
|
|
|
|
|
+ $update[$field] = $value;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return $this->canvasDetail(['canvas_id' => $doc['canvas_id']]);
|
|
|
|
|
|
|
+ DB::table('mp_canvases')->where('id', $canvas->id)->update($update);
|
|
|
|
|
+
|
|
|
|
|
+ return $this->documentResult($this->canvasKey($canvas), $name, $document);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -241,7 +231,14 @@ class CanvasService
|
|
|
{
|
|
{
|
|
|
$canvas_id = (string)getProp($data, 'canvas_id', '');
|
|
$canvas_id = (string)getProp($data, 'canvas_id', '');
|
|
|
$canvas = $this->checkCanvas($canvas_id);
|
|
$canvas = $this->checkCanvas($canvas_id);
|
|
|
- return $this->buildDocument($canvas);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $raw = (string)getProp($canvas, 'document', '');
|
|
|
|
|
+ if ($raw !== '') {
|
|
|
|
|
+ return $this->documentResult($this->canvasKey($canvas), (string)$canvas->name, $raw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 旧画布无 document 时由节点/关联表兜底构建
|
|
|
|
|
+ return $this->documentResult($this->canvasKey($canvas), (string)$canvas->name, $this->buildLegacyDocument($canvas));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -578,270 +575,146 @@ class CanvasService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 判断请求是否为完整文档创建
|
|
|
|
|
|
|
+ * 提取前端传入的 document(必填字段)
|
|
|
*
|
|
*
|
|
|
- * @param mixed $data
|
|
|
|
|
- * @return bool
|
|
|
|
|
|
|
+ * @param array $data
|
|
|
|
|
+ * @return mixed
|
|
|
*/
|
|
*/
|
|
|
- private function isDocumentRequest($data)
|
|
|
|
|
|
|
+ private function extractDocument($data)
|
|
|
{
|
|
{
|
|
|
- return is_array($data) && (
|
|
|
|
|
- array_key_exists('graph', $data) ||
|
|
|
|
|
- array_key_exists('schema_version', $data) ||
|
|
|
|
|
- array_key_exists('title', $data) ||
|
|
|
|
|
- array_key_exists('scope', $data) ||
|
|
|
|
|
- array_key_exists('canvas_id', $data)
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ if (!is_array($data)) {
|
|
|
|
|
+ Utils::throwError('20003:请求体格式不正确');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!array_key_exists('document', $data)) {
|
|
|
|
|
+ Utils::throwError('20003:请提供document');
|
|
|
|
|
+ }
|
|
|
|
|
+ return $data['document'];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 校验并规范化完整画布文档
|
|
|
|
|
|
|
+ * 规范化 document 用于数据库存储:JSON 对象转字符串;JSON 字符串原样保存;空值存 null
|
|
|
*
|
|
*
|
|
|
- * @param array $data
|
|
|
|
|
- * @param bool $isCreate
|
|
|
|
|
- * @param object|null $canvas
|
|
|
|
|
- * @return array
|
|
|
|
|
|
|
+ * @param mixed $document
|
|
|
|
|
+ * @return string|null
|
|
|
*/
|
|
*/
|
|
|
- private function validateDocument($data, $isCreate, $canvas = null)
|
|
|
|
|
|
|
+ private function documentForStorage($document)
|
|
|
{
|
|
{
|
|
|
- if (!is_array($data)) {
|
|
|
|
|
- Utils::throwError('20003:请求体格式不正确');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $schema_version = (int)getProp($data, 'schema_version', self::SUPPORTED_SCHEMA_VERSION);
|
|
|
|
|
- if ($schema_version !== self::SUPPORTED_SCHEMA_VERSION) {
|
|
|
|
|
- Utils::throwError('20003:不支持的schema_version');
|
|
|
|
|
|
|
+ if ($document === null || $document === '') {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $canvas_id = (string)getProp($data, 'canvas_id', '');
|
|
|
|
|
- if ($isCreate) {
|
|
|
|
|
- if ($canvas_id === '') {
|
|
|
|
|
- $canvas_id = $this->generateCanvasId();
|
|
|
|
|
- }
|
|
|
|
|
- if (mb_strlen($canvas_id) > 64) {
|
|
|
|
|
- Utils::throwError('20003:canvas_id长度超出限制');
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- $key = $canvas ? $this->canvasKey($canvas) : '';
|
|
|
|
|
- if ($canvas_id !== '' && $canvas_id !== $key && $canvas_id !== (string)getProp($canvas, 'id', 0)) {
|
|
|
|
|
- Utils::throwError('20003:canvas_id与画布不一致');
|
|
|
|
|
|
|
+ if (is_array($document) || is_object($document)) {
|
|
|
|
|
+ $json = json_encode($document, JSON_UNESCAPED_UNICODE);
|
|
|
|
|
+ if ($json === false) {
|
|
|
|
|
+ Utils::throwError('20003:document格式不正确');
|
|
|
}
|
|
}
|
|
|
- $canvas_id = $key;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $revision = $isCreate ? 1 : (int)getProp($data, 'revision', -1);
|
|
|
|
|
- if (!$isCreate && $revision < 0) {
|
|
|
|
|
- Utils::throwError('20003:缺少revision');
|
|
|
|
|
|
|
+ return $json;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $title = trim((string)getProp($data, 'title', getProp($data, 'name', '')));
|
|
|
|
|
- if ($title === '') {
|
|
|
|
|
- $title = '未命名画布';
|
|
|
|
|
- }
|
|
|
|
|
- if (mb_strlen($title) > self::MAX_TITLE_LENGTH) {
|
|
|
|
|
- Utils::throwError('20003:画布标题长度超出限制');
|
|
|
|
|
|
|
+ if (is_string($document)) {
|
|
|
|
|
+ return $document;
|
|
|
}
|
|
}
|
|
|
|
|
+ Utils::throwError('20003:document格式不正确');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- $scope = getProp($data, 'scope', []);
|
|
|
|
|
- if (!is_array($scope)) {
|
|
|
|
|
- Utils::throwError('20003:scope格式不正确');
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 存储的 document 返回给前端:JSON 字符串解码为对象;非 JSON 字符串原样返回
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param mixed $document
|
|
|
|
|
+ * @return mixed
|
|
|
|
|
+ */
|
|
|
|
|
+ private function documentForResponse($document)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($document === null || $document === '') {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
- $scope = $this->checkScope($scope);
|
|
|
|
|
-
|
|
|
|
|
- $graph = getProp($data, 'graph', []);
|
|
|
|
|
- if (!is_array($graph)) {
|
|
|
|
|
- Utils::throwError('20003:graph格式不正确');
|
|
|
|
|
|
|
+ if (is_array($document) || is_object($document)) {
|
|
|
|
|
+ return $document;
|
|
|
}
|
|
}
|
|
|
- $graph = $this->normalizeGraph($graph);
|
|
|
|
|
-
|
|
|
|
|
- $preferences = getProp($data, 'preferences', []);
|
|
|
|
|
- if (!is_array($preferences)) {
|
|
|
|
|
- $preferences = [];
|
|
|
|
|
|
|
+ $raw = (string)$document;
|
|
|
|
|
+ $doc = json_decode($raw, true);
|
|
|
|
|
+ if ($doc === null && trim($raw) !== 'null') {
|
|
|
|
|
+ return $raw;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $doc = [
|
|
|
|
|
- 'schema_version' => self::SUPPORTED_SCHEMA_VERSION,
|
|
|
|
|
- 'canvas_id' => $canvas_id,
|
|
|
|
|
- 'revision' => $revision,
|
|
|
|
|
- 'scope' => $scope,
|
|
|
|
|
- 'title' => $title,
|
|
|
|
|
- 'graph' => $graph,
|
|
|
|
|
- 'preferences' => $preferences
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- if (strlen(json_encode($doc, JSON_UNESCAPED_UNICODE)) > self::MAX_DOCUMENT_BYTES) {
|
|
|
|
|
- Utils::throwError('20003:画布文档大小超出限制');
|
|
|
|
|
- }
|
|
|
|
|
- $this->checkTextAndUrlLimits($doc);
|
|
|
|
|
-
|
|
|
|
|
return $doc;
|
|
return $doc;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 校验 scope 归属:动漫属于当前用户,剧集属于该动漫
|
|
|
|
|
|
|
+ * 组装画布文档相关返回结构:document 原样返回,canvas_id 由服务端维护
|
|
|
*
|
|
*
|
|
|
- * @param array $scope
|
|
|
|
|
|
|
+ * @param string $canvas_id
|
|
|
|
|
+ * @param mixed $document
|
|
|
* @return array
|
|
* @return array
|
|
|
*/
|
|
*/
|
|
|
- private function checkScope($scope)
|
|
|
|
|
|
|
+ private function documentResult($canvas_id, $name, $document)
|
|
|
{
|
|
{
|
|
|
- $anime_id = (int)getProp($scope, 'anime_id', 0);
|
|
|
|
|
- $episode_id = (int)getProp($scope, 'episode_id', 0);
|
|
|
|
|
- if (!$anime_id || !$episode_id) {
|
|
|
|
|
- Utils::throwError('20003:scope.anime_id和scope.episode_id不能为空');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $anime = DB::table('mp_animes')
|
|
|
|
|
- ->where('id', $anime_id)
|
|
|
|
|
- ->where('user_id', Site::getUid())
|
|
|
|
|
- ->where('is_deleted', 0)
|
|
|
|
|
- ->first();
|
|
|
|
|
- if (!$anime) {
|
|
|
|
|
- Utils::throwError('20003:动漫不存在或无权访问');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $episode = DB::table('mp_anime_episodes')
|
|
|
|
|
- ->where('id', $episode_id)
|
|
|
|
|
- ->where('anime_id', $anime_id)
|
|
|
|
|
- ->first();
|
|
|
|
|
- if (!$episode) {
|
|
|
|
|
- Utils::throwError('20003:剧集不存在或不属于该动漫');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
return [
|
|
return [
|
|
|
- 'anime_id' => $anime_id,
|
|
|
|
|
- 'episode_id' => $episode_id,
|
|
|
|
|
- 'episode_number' => (int)$episode->episode_number
|
|
|
|
|
|
|
+ 'canvas_id' => (string)$canvas_id,
|
|
|
|
|
+ 'name' => (string)$name,
|
|
|
|
|
+ 'document' => $this->documentForResponse($document)
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 图结构最小校验:节点/边数量、ID 唯一、边端点存在
|
|
|
|
|
|
|
+ * 校验 document 存储大小(仅整体大小限制,不解析内部结构)
|
|
|
*
|
|
*
|
|
|
- * @param array $graph
|
|
|
|
|
- * @return array
|
|
|
|
|
|
|
+ * @param string|null $document
|
|
|
|
|
+ * @return void
|
|
|
*/
|
|
*/
|
|
|
- private function normalizeGraph($graph)
|
|
|
|
|
|
|
+ private function checkDocumentSize($document)
|
|
|
{
|
|
{
|
|
|
- $viewport = getProp($graph, 'viewport', ['x' => 0, 'y' => 0, 'zoom' => 1]);
|
|
|
|
|
- if (!is_array($viewport)) {
|
|
|
|
|
- $viewport = ['x' => 0, 'y' => 0, 'zoom' => 1];
|
|
|
|
|
- }
|
|
|
|
|
- $nodes = getProp($graph, 'nodes', []);
|
|
|
|
|
- $edges = getProp($graph, 'edges', []);
|
|
|
|
|
- if (!is_array($nodes)) {
|
|
|
|
|
- $nodes = [];
|
|
|
|
|
- }
|
|
|
|
|
- if (!is_array($edges)) {
|
|
|
|
|
- $edges = [];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (count($nodes) > self::MAX_NODES) {
|
|
|
|
|
- Utils::throwError('20003:节点数超出限制');
|
|
|
|
|
- }
|
|
|
|
|
- if (count($edges) > self::MAX_EDGES) {
|
|
|
|
|
- Utils::throwError('20003:边数超出限制');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $nodeIds = [];
|
|
|
|
|
- foreach ($nodes as $node) {
|
|
|
|
|
- if (!is_array($node)) {
|
|
|
|
|
- Utils::throwError('20003:节点格式不正确');
|
|
|
|
|
- }
|
|
|
|
|
- $id = getProp($node, 'id', '');
|
|
|
|
|
- if ($id === '' || $id === null || !is_scalar($id)) {
|
|
|
|
|
- Utils::throwError('20003:节点ID不能为空');
|
|
|
|
|
- }
|
|
|
|
|
- $idKey = (string)$id;
|
|
|
|
|
- if (isset($nodeIds[$idKey])) {
|
|
|
|
|
- Utils::throwError('20003:节点ID重复:' . $idKey);
|
|
|
|
|
- }
|
|
|
|
|
- $nodeIds[$idKey] = true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $edgeIds = [];
|
|
|
|
|
- foreach ($edges as $edge) {
|
|
|
|
|
- if (!is_array($edge)) {
|
|
|
|
|
- Utils::throwError('20003:边格式不正确');
|
|
|
|
|
- }
|
|
|
|
|
- $source = (string)getProp($edge, 'source', '');
|
|
|
|
|
- $target = (string)getProp($edge, 'target', '');
|
|
|
|
|
- if (!isset($nodeIds[$source]) || !isset($nodeIds[$target])) {
|
|
|
|
|
- Utils::throwError('20003:边端点不存在');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $edgeId = getProp($edge, 'id', '');
|
|
|
|
|
- if ($edgeId !== '' && $edgeId !== null) {
|
|
|
|
|
- if (!is_scalar($edgeId)) {
|
|
|
|
|
- Utils::throwError('20003:边ID格式不正确');
|
|
|
|
|
- }
|
|
|
|
|
- $edgeKey = (string)$edgeId;
|
|
|
|
|
- if (isset($edgeIds[$edgeKey])) {
|
|
|
|
|
- Utils::throwError('20003:边ID重复:' . $edgeKey);
|
|
|
|
|
- }
|
|
|
|
|
- $edgeIds[$edgeKey] = true;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if ($document !== null && strlen($document) > self::MAX_DOCUMENT_BYTES) {
|
|
|
|
|
+ Utils::throwError('20003:画布文档大小超出限制');
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return [
|
|
|
|
|
- 'viewport' => $viewport,
|
|
|
|
|
- 'nodes' => $nodes,
|
|
|
|
|
- 'edges' => $edges
|
|
|
|
|
- ];
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 递归限制文档内文本与 URL 长度
|
|
|
|
|
|
|
+ * 读取请求中 document 相关字段(兼容 document 放在顶层或 document 字段内)
|
|
|
*
|
|
*
|
|
|
- * @param mixed $value
|
|
|
|
|
- * @return void
|
|
|
|
|
|
|
+ * @param array $data
|
|
|
|
|
+ * @param string $key
|
|
|
|
|
+ * @param mixed $default
|
|
|
|
|
+ * @return mixed
|
|
|
*/
|
|
*/
|
|
|
- private function checkTextAndUrlLimits($value)
|
|
|
|
|
|
|
+ private function docProp($data, $key, $default = '')
|
|
|
{
|
|
{
|
|
|
- if (is_string($value)) {
|
|
|
|
|
- if (strlen($value) > self::MAX_TEXT_LENGTH) {
|
|
|
|
|
- Utils::throwError('20003:文本内容长度超出限制');
|
|
|
|
|
- }
|
|
|
|
|
- if (preg_match('#^https?://#i', $value) && strlen($value) > self::MAX_URL_LENGTH) {
|
|
|
|
|
- Utils::throwError('20003:URL长度超出限制');
|
|
|
|
|
- }
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ if (array_key_exists($key, $data)) {
|
|
|
|
|
+ return $data[$key];
|
|
|
}
|
|
}
|
|
|
- if (!is_array($value)) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- foreach ($value as $item) {
|
|
|
|
|
- $this->checkTextAndUrlLimits($item);
|
|
|
|
|
|
|
+ $doc = getProp($data, 'document', null);
|
|
|
|
|
+ if (is_array($doc) && array_key_exists($key, $doc)) {
|
|
|
|
|
+ return $doc[$key];
|
|
|
}
|
|
}
|
|
|
|
|
+ return $default;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 组装完整文档;旧画布无 document 时从节点/关联表构建
|
|
|
|
|
|
|
+ * 读取画布元数据列(anime_id/episode_id/episode_number),
|
|
|
|
|
+ * 兼容顶层字段、document 顶层字段、document.scope 三种位置;不影响 document 原样保存
|
|
|
*
|
|
*
|
|
|
- * @param object $canvas
|
|
|
|
|
- * @return array
|
|
|
|
|
|
|
+ * @param array $data
|
|
|
|
|
+ * @param string $key
|
|
|
|
|
+ * @param mixed $default
|
|
|
|
|
+ * @return int|null
|
|
|
*/
|
|
*/
|
|
|
- private function buildDocument($canvas)
|
|
|
|
|
|
|
+ private function scopeProp($data, $key, $default = 0)
|
|
|
{
|
|
{
|
|
|
- $raw = (string)getProp($canvas, 'document', '');
|
|
|
|
|
- if ($raw !== '') {
|
|
|
|
|
- $doc = json_decode($raw, true);
|
|
|
|
|
- if (is_array($doc)) {
|
|
|
|
|
- $doc['canvas_id'] = $this->canvasKey($canvas);
|
|
|
|
|
- $doc['revision'] = (int)getProp($canvas, 'revision', 1);
|
|
|
|
|
- $doc['schema_version'] = (int)getProp($canvas, 'schema_version', self::SUPPORTED_SCHEMA_VERSION);
|
|
|
|
|
- $doc['scope'] = $this->scopeFromRow($canvas);
|
|
|
|
|
- $doc['title'] = (string)getProp($canvas, 'name', getProp($doc, 'title', '未命名画布'));
|
|
|
|
|
- if (!isset($doc['graph']) || !is_array($doc['graph'])) {
|
|
|
|
|
- $doc['graph'] = ['viewport' => ['x' => 0, 'y' => 0, 'zoom' => 1], 'nodes' => [], 'edges' => []];
|
|
|
|
|
- }
|
|
|
|
|
- if (!isset($doc['preferences']) || !is_array($doc['preferences'])) {
|
|
|
|
|
- $doc['preferences'] = ['minimap_visible' => true, 'snap_to_grid_enabled' => false];
|
|
|
|
|
- }
|
|
|
|
|
- return $doc;
|
|
|
|
|
|
|
+ if (array_key_exists($key, $data)) {
|
|
|
|
|
+ return (int)$data[$key];
|
|
|
|
|
+ }
|
|
|
|
|
+ $doc = getProp($data, 'document', null);
|
|
|
|
|
+ if (is_array($doc)) {
|
|
|
|
|
+ if (array_key_exists($key, $doc)) {
|
|
|
|
|
+ return (int)$doc[$key];
|
|
|
|
|
+ }
|
|
|
|
|
+ $scope = getProp($doc, 'scope', []);
|
|
|
|
|
+ if (is_array($scope) && array_key_exists($key, $scope)) {
|
|
|
|
|
+ return (int)$scope[$key];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return $this->buildLegacyDocument($canvas);
|
|
|
|
|
|
|
+ $scope = getProp($data, 'scope', null);
|
|
|
|
|
+ if (is_array($scope) && array_key_exists($key, $scope)) {
|
|
|
|
|
+ return (int)$scope[$key];
|
|
|
|
|
+ }
|
|
|
|
|
+ return $default;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -888,9 +761,7 @@ class CanvasService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return [
|
|
return [
|
|
|
- 'schema_version' => self::SUPPORTED_SCHEMA_VERSION,
|
|
|
|
|
'canvas_id' => $this->canvasKey($canvas),
|
|
'canvas_id' => $this->canvasKey($canvas),
|
|
|
- 'revision' => (int)getProp($canvas, 'revision', 1),
|
|
|
|
|
'scope' => $this->scopeFromRow($canvas),
|
|
'scope' => $this->scopeFromRow($canvas),
|
|
|
'title' => (string)getProp($canvas, 'name', '未命名画布'),
|
|
'title' => (string)getProp($canvas, 'name', '未命名画布'),
|
|
|
'graph' => [
|
|
'graph' => [
|