行数据 */ private static $segmentRowCache = []; /** @var array 分镜号 => 行数据 */ private static $segmentNoCache = []; /** @var array 动漫ID => 动漫行数据(anime_name、script_id) */ private static $animeRowCache = []; /** @var array 剧本分集分组ID => 剧本ID */ private static $groupScriptCache = []; /** @var array 剧本ID => 剧本名 */ private static $scriptNameCache = []; /** @var array 动漫ID-集数 => 分集ID */ private static $episodeIdCache = []; /** @var array 分集ID => 分集行数据 */ private static $episodeRowCache = []; /** @var array 任务表锚点缓存 */ private static $taskAnchorCache = []; /** * 空锚点(仅记录归属所需的 ID 锚点,编号等展示字段不入 charge_info) * * @return array */ public static function emptyAnchor(): array { return [ 'anime_id' => 0, 'episode_id' => 0, 'act_id' => 0, 'segment_id' => '', 'script_id' => 0, ]; } /** * 由上下文(任务创建参数或 charge_info)解析完整锚点 * * @param array $context * @return array 见 emptyAnchor(),缺失项为 0 / 空串 */ public static function resolve(array $context): array { $anchor = self::emptyAnchor(); try { // 1. 上下文显式传入的锚点 $animeId = (int)getProp($context, 'anime_id', 0); $episodeId = (int)getProp($context, 'episode_id', 0); $episodeNumber = (int)getProp($context, 'episode_number', 0); $scriptId = (int)getProp($context, 'script_id', 0); $actId = (int)getProp($context, 'act_id', 0); if ($actId <= 0) { $actId = (int)getProp($context, 'alias_act_id', 0); } $segmentId = self::normalizeSegmentNo(getProp($context, 'segment_id', '')); if ($segmentId === '') { $segmentId = self::normalizeSegmentNo(getProp($context, 'alias_segment_id', '')); } // 2. 由分镜行/分镜号补全动漫、剧集、剧幕/分镜 $segmentRow = []; if ($actId > 0) { $segmentRow = self::fromSegmentRowId($actId); } elseif ($segmentId !== '') { $segmentRow = self::fromSegmentNo($segmentId); } if (!empty($segmentRow)) { $animeId = $animeId ?: (int)$segmentRow['anime_id']; $episodeId = $episodeId ?: (int)$segmentRow['episode_id']; $episodeNumber = $episodeNumber ?: (int)$segmentRow['episode_number']; $actId = (int)$segmentRow['id']; if ($segmentId === '') { $segmentId = (string)$segmentRow['segment_id']; } } // 3. 只有动漫 + 集数时补全分集ID if ($episodeId <= 0 && $animeId > 0 && $episodeNumber > 0) { $episodeId = self::episodeIdByNumber($animeId, $episodeNumber); } // 4. 只有分集ID时补全动漫(用于取动漫绑定的剧本) if ($episodeId > 0 && ($animeId <= 0 || $episodeNumber <= 0)) { $episodeRow = self::fromEpisodeId($episodeId); if ($episodeRow) { $animeId = $animeId ?: (int)$episodeRow['anime_id']; } } // 5. 剧本:动漫绑定优先,其次上下文脚本ID $animeScriptId = $animeId > 0 ? self::scriptIdByAnime($animeId) : 0; $scriptId = $animeScriptId > 0 ? $animeScriptId : $scriptId; $anchor['anime_id'] = $animeId; $anchor['episode_id'] = $episodeId; $anchor['act_id'] = $actId; $anchor['segment_id'] = $segmentId; $anchor['script_id'] = $scriptId; return $anchor; } catch (\Throwable $e) { dLog('points')->warning('锚点解析失败(不影响主流程): ' . $e->getMessage()); return $anchor; } } /** * 将锚点合并进 charge_info * * - act_id / segment_id 统一按 alias_ 前缀记录:alias_act_id / alias_segment_id; * - charge_info 中已有的值优先(尤其任务参数已带的 alias_act_id / alias_segment_id),缺失的才补充; * - 未解析到的锚点不写入,避免冗余空值。 * * @param array $chargeInfo 已构建好的计费信息 * @param array $context 任务创建参数 / 计费上下文 * @return array 合并后的计费信息 */ public static function mergeIntoChargeInfo(array $chargeInfo, array $context): array { $anchor = self::resolve($context); // act_id / segment_id 统一转换成 alias_ 前缀 $anchor['alias_act_id'] = $anchor['act_id']; $anchor['alias_segment_id'] = $anchor['segment_id']; unset($anchor['act_id'], $anchor['segment_id']); foreach ($anchor as $key => $value) { if ($value === 0 || $value === '' || $value === null) { continue; } // 已有值优先(alias_ 开头的历史字段保留原值) if (!empty(getProp($chargeInfo, $key, ''))) { continue; } $chargeInfo[$key] = $value; } return $chargeInfo; } /** * 用已解析出的锚点补写 charge_info(已有值优先,仅补 anime_id / script_id) * * 用于积分明细落库时(PointsService):保证明细列与 charge_info 口径一致, * 已经写入过锚点的任务(视频/图片)不会重复写入。 * * @param array $chargeInfo * @param int|null $animeId * @param int|null $scriptId * @return array */ public static function fillChargeInfoAnchors(array $chargeInfo, $animeId, $scriptId): array { if ((int)$animeId > 0 && empty(getProp($chargeInfo, 'anime_id', ''))) { $chargeInfo['anime_id'] = (int)$animeId; } if ((int)$scriptId > 0 && empty(getProp($chargeInfo, 'script_id', ''))) { $chargeInfo['script_id'] = (int)$scriptId; } return $chargeInfo; } /** * 分镜行ID(mp_episode_segments.id)→ 分镜行数据 * * @param int $rowId * @return array */ public static function fromSegmentRowId(int $rowId): array { if ($rowId <= 0) { return []; } if (!array_key_exists($rowId, self::$segmentRowCache)) { $row = DB::table('mp_episode_segments')->where('id', $rowId)->first(self::SEGMENT_COLUMNS); self::$segmentRowCache[$rowId] = $row ? (array)$row : []; } return self::$segmentRowCache[$rowId]; } /** * 分镜号(mp_episode_segments.segment_id)→ 分镜行数据 * * @param string $segmentNo * @return array */ public static function fromSegmentNo(string $segmentNo): array { $segmentNo = self::normalizeSegmentNo($segmentNo); if ($segmentNo === '') { return []; } if (!array_key_exists($segmentNo, self::$segmentNoCache)) { $row = DB::table('mp_episode_segments')->where('segment_id', $segmentNo)->first(self::SEGMENT_COLUMNS); self::$segmentNoCache[$segmentNo] = $row ? (array)$row : []; } return self::$segmentNoCache[$segmentNo]; } /** * 视频/图片任务ID → 锚点(回查任务表保存的关联片段,兼容未写锚点的历史任务) * * @param string $type 明细类型(video/image) * @param int $taskId 任务表主键 * @return array */ public static function anchorByTask(string $type, int $taskId): array { if ($taskId <= 0 || !in_array($type, [MpUserPointsDetail::TYPE_VIDEO, MpUserPointsDetail::TYPE_IMAGE], true)) { return self::emptyAnchor(); } $cacheKey = $type . ':' . $taskId; if (!array_key_exists($cacheKey, self::$taskAnchorCache)) { $table = $type === MpUserPointsDetail::TYPE_VIDEO ? 'mp_generate_video_tasks' : 'mp_generate_pic_tasks'; $task = DB::table($table)->where('id', $taskId)->first(['alias_act_id', 'alias_segment_id']); $anchor = self::emptyAnchor(); if ($task) { $context = []; if ((int)getProp($task, 'alias_act_id', 0) > 0) { $context['alias_act_id'] = (int)getProp($task, 'alias_act_id', 0); } $segmentNo = self::normalizeSegmentNo(getProp($task, 'alias_segment_id', '')); if ($segmentNo !== '') { $context['alias_segment_id'] = $segmentNo; } if ($context) { $anchor = self::resolve($context); } } self::$taskAnchorCache[$cacheKey] = $anchor; } return self::$taskAnchorCache[$cacheKey]; } /** * 动漫ID → 绑定剧本ID(未绑定返回 0) * * @param int $animeId * @return int */ public static function scriptIdByAnime(int $animeId): int { $animeRow = self::animeRowById($animeId); return (int)($animeRow['script_id'] ?? 0); } /** * 动漫ID → 动漫名(动漫不存在返回空串) * * @param int $animeId * @return string */ public static function animeNameById(int $animeId): string { $animeRow = self::animeRowById($animeId); return trim((string)($animeRow['anime_name'] ?? '')); } /** * 动漫ID → 动漫行数据 * * @param int $animeId * @return array */ private static function animeRowById(int $animeId): array { if ($animeId <= 0) { return []; } if (!array_key_exists($animeId, self::$animeRowCache)) { $row = DB::table('mp_animes')->where('id', $animeId)->first(['id', 'anime_name', 'script_id']); self::$animeRowCache[$animeId] = $row ? (array)$row : []; } return self::$animeRowCache[$animeId]; } /** * 剧本分集分组ID(mp_script_episode_group.id)→ 剧本ID * * @param int $groupId * @return int */ public static function scriptIdByGroup(int $groupId): int { if ($groupId <= 0) { return 0; } if (!array_key_exists($groupId, self::$groupScriptCache)) { self::$groupScriptCache[$groupId] = (int)DB::table('mp_script_episode_group')->where('id', $groupId)->value('script_id'); } return (int)self::$groupScriptCache[$groupId]; } /** * 剧本ID → 剧本名(剧本不存在返回空串) * * @param int $scriptId * @return string */ public static function scriptNameById(int $scriptId): string { if ($scriptId <= 0) { return ''; } if (!array_key_exists($scriptId, self::$scriptNameCache)) { self::$scriptNameCache[$scriptId] = trim((string)DB::table('mp_scripts')->where('id', $scriptId)->value('script_name')); } return (string)self::$scriptNameCache[$scriptId]; } /** * 动漫ID + 集数 → 分集ID * * @param int $animeId * @param int $episodeNumber * @return int */ public static function episodeIdByNumber(int $animeId, int $episodeNumber): int { if ($animeId <= 0 || $episodeNumber <= 0) { return 0; } $cacheKey = $animeId . '-' . $episodeNumber; if (!array_key_exists($cacheKey, self::$episodeIdCache)) { self::$episodeIdCache[$cacheKey] = (int)DB::table('mp_anime_episodes') ->where('anime_id', $animeId) ->where('episode_number', $episodeNumber) ->value('id'); } return (int)self::$episodeIdCache[$cacheKey]; } /** * 分集ID → 分集行数据(mp_anime_episodes) * * @param int $episodeId * @return array */ public static function fromEpisodeId(int $episodeId): array { if ($episodeId <= 0) { return []; } if (!array_key_exists($episodeId, self::$episodeRowCache)) { $row = DB::table('mp_anime_episodes')->where('id', $episodeId)->first(['id', 'anime_id', 'episode_number']); self::$episodeRowCache[$episodeId] = $row ? (array)$row : []; } return self::$episodeRowCache[$episodeId]; } /** * 归一化分镜号:空、0、字符串 "0" 均视为未传 * * @param mixed $segmentNo * @return string */ private static function normalizeSegmentNo($segmentNo): string { $segmentNo = trim((string)$segmentNo); if ($segmentNo === '' || $segmentNo === '0') { return ''; } return $segmentNo; } }