Jelajahi Sumber

积分明细新增chat类型的动漫、剧本ID等信息记录

lh 2 hari lalu
induk
melakukan
108a6e0d04

+ 108 - 10
app/Console/Commands/BackfillPointsScriptCommand.php

@@ -18,6 +18,8 @@ class BackfillPointsScriptCommand extends Command
                             {--type= : 只回填指定类型(video/image/chat),默认三种类型全部回填}
                             {--type= : 只回填指定类型(video/image/chat),默认三种类型全部回填}
                             {--dry-run : 只统计将回填的记录数,不实际更新}
                             {--dry-run : 只统计将回填的记录数,不实际更新}
                             {--force : 重算已有锚点的记录(默认只处理缺失动漫/剧本的记录)}
                             {--force : 重算已有锚点的记录(默认只处理缺失动漫/剧本的记录)}
+                            {--skip-charge-info : 只回填明细列,不改写 charge_info}
+                            {--charge-info-only : 只补齐 charge_info 缺失的 anime_id/script_id(依据明细列已回填的值)}
                             {--limit= : 最多处理记录数(默认全部)}';
                             {--limit= : 最多处理记录数(默认全部)}';
 
 
     /**
     /**
@@ -25,7 +27,7 @@ class BackfillPointsScriptCommand extends Command
      *
      *
      * @var string
      * @var string
      */
      */
-    protected $description = '回填积分明细的动漫ID/动漫名与剧本ID/剧本名(历史数据无动漫/剧本维度时为空)';
+    protected $description = '回填积分明细的动漫ID/动漫名、剧本ID/剧本名,并同步补写 charge_info 的 anime_id/script_id';
 
 
     /**
     /**
      * 每批处理条数
      * 每批处理条数
@@ -48,11 +50,17 @@ class BackfillPointsScriptCommand extends Command
 
 
         $dryRun = (bool)$this->option('dry-run');
         $dryRun = (bool)$this->option('dry-run');
         $force = (bool)$this->option('force');
         $force = (bool)$this->option('force');
+        $skipChargeInfo = (bool)$this->option('skip-charge-info');
+        $chargeInfoOnly = (bool)$this->option('charge-info-only');
         $limit = (int)$this->option('limit');
         $limit = (int)$this->option('limit');
         if ($limit < 0) {
         if ($limit < 0) {
             $limit = 0;
             $limit = 0;
         }
         }
 
 
+        if ($chargeInfoOnly) {
+            return $this->fillChargeInfoOnly($dryRun);
+        }
+
         $type = trim((string)$this->option('type'));
         $type = trim((string)$this->option('type'));
         $types = in_array($type, ['video', 'image', 'chat'], true) ? [$type] : ['video', 'image', 'chat'];
         $types = in_array($type, ['video', 'image', 'chat'], true) ? [$type] : ['video', 'image', 'chat'];
 
 
@@ -82,14 +90,16 @@ class BackfillPointsScriptCommand extends Command
         $scanned = 0;
         $scanned = 0;
         $filled = 0;
         $filled = 0;
         $skipped = 0;
         $skipped = 0;
+        $chargeInfoUpdated = 0;
 
 
-        $query->chunkById(self::CHUNK_SIZE, function ($rows) use ($pointsService, $dryRun, $limit, &$scanned, &$filled, &$skipped) {
-            /** @var array 剧本 => 待更新明细ID */
+        $query->chunkById(self::CHUNK_SIZE, function ($rows) use ($pointsService, $dryRun, $skipChargeInfo, $limit, &$scanned, &$filled, &$skipped, &$chargeInfoUpdated) {
+            /** @var array 锚点 => 待更新明细ID */
             $pending = [];
             $pending = [];
 
 
             foreach ($rows as $row) {
             foreach ($rows as $row) {
                 if ($limit > 0 && $scanned >= $limit) {
                 if ($limit > 0 && $scanned >= $limit) {
-                    return false;
+                    // 达到上限:先跳出明细循环,保证已解析的数据在下方统一落库
+                    break;
                 }
                 }
                 $scanned++;
                 $scanned++;
 
 
@@ -110,12 +120,20 @@ class BackfillPointsScriptCommand extends Command
                     continue;
                     continue;
                 }
                 }
 
 
-                $key = (int)$context['anime_id'] . '|' . (string)$context['anime_name'] . '|' . (int)$context['script_id'] . '|' . (string)$context['script_name'];
+                // charge_info 中缺失的锚点才补(已有值优先,不改写历史原始值)
+                $needChargeInfoAnime = !$skipChargeInfo && !empty($context['anime_id']) && empty(getProp($chargeInfo, 'anime_id', ''));
+                $needChargeInfoScript = !$skipChargeInfo && !empty($context['script_id']) && empty(getProp($chargeInfo, 'script_id', ''));
+                $mask = ($needChargeInfoAnime ? 1 : 0) + ($needChargeInfoScript ? 2 : 0);
+
+                $key = (int)$context['anime_id'] . '|' . (string)$context['anime_name'] . '|' . (int)$context['script_id'] . '|' . (string)$context['script_name'] . '|' . $mask;
                 $pending[$key][] = (int)$row->id;
                 $pending[$key][] = (int)$row->id;
             }
             }
 
 
             foreach ($pending as $key => $ids) {
             foreach ($pending as $key => $ids) {
-                list($animeId, $animeName, $scriptId, $scriptName) = explode('|', $key, 4);
+                list($animeId, $animeName, $scriptId, $scriptName, $mask) = explode('|', $key, 5);
+                $animeId = (int)$animeId;
+                $scriptId = (int)$scriptId;
+                $mask = (int)$mask;
                 $filled += count($ids);
                 $filled += count($ids);
 
 
                 if ($dryRun) {
                 if ($dryRun) {
@@ -123,19 +141,99 @@ class BackfillPointsScriptCommand extends Command
                 }
                 }
 
 
                 DB::table('mp_user_points_details')->whereIn('id', $ids)->update([
                 DB::table('mp_user_points_details')->whereIn('id', $ids)->update([
-                    'anime_id' => (int)$animeId ?: null,
+                    'anime_id' => $animeId ?: null,
                     'anime_name' => $animeName !== '' ? $animeName : null,
                     'anime_name' => $animeName !== '' ? $animeName : null,
-                    'script_id' => (int)$scriptId ?: null,
+                    'script_id' => $scriptId ?: null,
                     'script_name' => $scriptName !== '' ? $scriptName : null,
                     'script_name' => $scriptName !== '' ? $scriptName : null,
                 ]);
                 ]);
+
+                // charge_info 补写 anime_id / script_id(JSON 原子写入,不影响其他键)
+                if ($mask > 0 && ($animeId || $scriptId)) {
+                    $this->updateChargeInfoAnchors($ids, $animeId, $scriptId, $mask);
+                    $chargeInfoUpdated += count($ids);
+                }
             }
             }
 
 
-            return true;
+            // 达到处理上限时结束分片遍历
+            return !($limit > 0 && $scanned >= $limit);
         });
         });
 
 
-        $this->info('扫描明细: ' . $scanned . ' 条,可回填: ' . $filled . ' 条,与剧本无关: ' . $skipped . ' 条');
+        $this->info('扫描明细: ' . $scanned . ' 条,可回填: ' . $filled . ' 条,无动漫/剧本线索: ' . $skipped . ' 条');
+        if (!$skipChargeInfo) {
+            $this->info('charge_info 补写: ' . $chargeInfoUpdated . ' 条' . ($dryRun ? '(dry-run 未写入)' : ''));
+        }
         $this->info($dryRun ? 'dry-run 结束,未更新任何数据' : '回填完成');
         $this->info($dryRun ? 'dry-run 结束,未更新任何数据' : '回填完成');
 
 
         return 0;
         return 0;
     }
     }
+
+    /**
+     * 只补齐 charge_info 缺失的锚点(依据明细列已有值,不做锚点解析)
+     *
+     * 用于修正“列已回填但 charge_info 未补写”的行,速度快、可重复执行。
+     *
+     * @param bool $dryRun
+     * @return int
+     */
+    private function fillChargeInfoOnly(bool $dryRun): int
+    {
+        $targets = [
+            'anime_id' => "anime_id is not null and JSON_CONTAINS_PATH(charge_info, 'one', '$.anime_id') = 0",
+            'script_id' => "script_id is not null and JSON_CONTAINS_PATH(charge_info, 'one', '$.script_id') = 0",
+        ];
+
+        foreach ($targets as $column => $condition) {
+            $count = DB::table('mp_user_points_details')->whereRaw($condition)->count();
+            if ($dryRun) {
+                $this->info('charge_info 待补 ' . $column . ': ' . $count . ' 条(dry-run 未写入)');
+                continue;
+            }
+            if ($count <= 0) {
+                $this->info('charge_info 待补 ' . $column . ': 0 条');
+                continue;
+            }
+
+            $affected = DB::update(
+                'UPDATE mp_user_points_details SET charge_info = JSON_SET(charge_info, \'$.' . $column . '\', ' . $column . ')'
+                . ' WHERE ' . $condition
+            );
+            $this->info('charge_info 补写 ' . $column . ': ' . $affected . ' 条');
+        }
+
+        return 0;
+    }
+
+    /**
+     * 补写 charge_info 的 anime_id / script_id(只补缺失的键,已有值保留)
+     *
+     * @param array $ids      明细ID
+     * @param int   $animeId
+     * @param int   $scriptId
+     * @param int   $mask     1=补 anime_id,2=补 script_id,3=两者都补
+     * @return void
+     */
+    private function updateChargeInfoAnchors(array $ids, int $animeId, int $scriptId, int $mask): void
+    {
+        $paths = [];
+        $bindings = [];
+
+        if (($mask & 1) === 1 && $animeId > 0) {
+            $paths[] = "'$.anime_id', ?";
+            $bindings[] = $animeId;
+        }
+        if (($mask & 2) === 2 && $scriptId > 0) {
+            $paths[] = "'$.script_id', ?";
+            $bindings[] = $scriptId;
+        }
+        if (!$paths) {
+            return;
+        }
+
+        // 用 JSON_SET 原子补写锚点:只影响指定键,charge_info 其他内容保持不变
+        $sql = 'UPDATE mp_user_points_details'
+            . ' SET charge_info = JSON_SET(COALESCE(charge_info, JSON_OBJECT()), ' . implode(', ', $paths) . ')'
+            . ' WHERE id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')';
+
+        DB::update($sql, array_merge($bindings, array_map('intval', $ids)));
+    }
 }
 }

+ 23 - 0
app/Libs/ScriptAnchor.php

@@ -189,6 +189,29 @@ class ScriptAnchor
     }
     }
 
 
     /**
     /**
+     * 用已解析出的锚点补写 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)→ 分镜行数据
      * 分镜行ID(mp_episode_segments.id)→ 分镜行数据
      *
      *
      * @param int $rowId
      * @param int $rowId

+ 4 - 0
app/Services/PointsService.php

@@ -1515,6 +1515,8 @@ class PointsService
             }
             }
 
 
             $scriptContext = $this->resolveScriptContext($chargeInfo, null, $type);
             $scriptContext = $this->resolveScriptContext($chargeInfo, null, $type);
+            // charge_info 与明细列口径一致:补写缺失的 anime_id / script_id(已有值保留)
+            $chargeInfo = ScriptAnchor::fillChargeInfoAnchors($chargeInfo, $scriptContext['anime_id'], $scriptContext['script_id']);
 
 
             DB::table('mp_user_points_details')->insert([
             DB::table('mp_user_points_details')->insert([
                 'uid' => $uid,
                 'uid' => $uid,
@@ -1669,6 +1671,8 @@ class PointsService
 
 
             // 剧本维度(解析失败或与剧本无关时为空,不影响计费主流程)
             // 剧本维度(解析失败或与剧本无关时为空,不影响计费主流程)
             $scriptContext = $this->resolveScriptContext($chargeInfo, $taskId, $type);
             $scriptContext = $this->resolveScriptContext($chargeInfo, $taskId, $type);
+            // charge_info 与明细列口径一致:补写缺失的 anime_id / script_id(已有值保留)
+            $chargeInfo = ScriptAnchor::fillChargeInfoAnchors($chargeInfo, $scriptContext['anime_id'], $scriptContext['script_id']);
 
 
             // 记录积分使用明细
             // 记录积分使用明细
             DB::table('mp_user_points_details')->insert([
             DB::table('mp_user_points_details')->insert([