|
@@ -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)));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|