| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- <?php
- namespace App\Libs;
- use App\Models\MpUserPointsDetail;
- use Illuminate\Support\Facades\DB;
- /**
- * 积分明细锚点解析
- *
- * 统一从任务/计费上下文推导「动漫 → 剧集 → 剧幕/分镜 → 剧本」锚点,用于:
- * - 任务创建时写入 charge_info(AIVideoGenerationService / AIImageGenerationService),
- * 只写入 ID 锚点:anime_id / episode_id / act_id(= alias_act_id)/ segment_id(= alias_segment_id)/ script_id,
- * 剧集序号、剧幕序号、分镜序号等展示字段不入 charge_info;
- * - 积分明细落库时写入 anime_id / anime_name / script_id / script_name(PointsService)。
- *
- * 上下文键名兼容两类写法:
- * - 标准锚点:anime_id / episode_id / episode_number / act_id / segment_id / script_id / group_id
- * - 历史参数:alias_act_id(mp_episode_segments.id)、alias_segment_id(mp_episode_segments.segment_id / 分镜号)
- *
- * 规则:
- * - 有分镜行ID或分镜号时,回查 mp_episode_segments 补全动漫、剧集、剧幕/分镜锚点;
- * - 剧本优先取动漫绑定的剧本(mp_animes.script_id),其次取上下文显式传入的 script_id;
- * - 推导不到的一律返回 0 / 空串,异常只记日志,绝不影响任务创建与计费主流程。
- */
- class ScriptAnchor
- {
- /**
- * 分镜表读取字段(只取锚点链路需要的列)
- */
- const SEGMENT_COLUMNS = [
- 'id',
- 'anime_id',
- 'episode_id',
- 'episode_number',
- 'segment_id',
- ];
- /** @var array 分镜行ID => 行数据 */
- 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;
- }
- }
|