setName('mobile-recording:cos-migrate')->setDescription('分批迁移历史本地完整录音/文字至COS') ->addOption('dry-run', null, Option::VALUE_NONE, '只统计,不读取内容、不调用COS') ->addOption('limit', null, Option::VALUE_REQUIRED, '每批1至100条', '20') ->addOption('after', null, Option::VALUE_REQUIRED, '上一批输出的32位不透明cursor', ''); } protected function execute(Input $input, Output $output): int { try { $after = (string) $input->getOption('after'); if ($after !== '' && !preg_match('/^[a-f0-9]{32}$/D', $after)) { throw new \InvalidArgumentException('Invalid cursor'); } $query = Db::name('mobile_recording_upload')->alias('u') ->leftJoin([Db::name('mobile_recording_object')->getTable() => 'o'], 'o.upload_id=u.upload_id') ->where('u.state', 'complete')->whereRaw('(o.upload_id IS NULL OR o.state IN (\'pending\',\'finalizing\',\'verifying\'))'); if ($after !== '') { $query->where('u.upload_id', '>', $after); } $ids = $query->order('u.upload_id')->limit(max(1, min(100, (int) $input->getOption('limit'))))->column('u.upload_id'); $counts = ['selected' => count($ids), 'migrated' => 0, 'deferred' => 0, 'next_cursor' => $ids ? end($ids) : '']; if (!$input->getOption('dry-run')) { foreach ($ids as $id) { try { MobileRecordingMediaLogic::migrateLocal($id); $counts['migrated']++; } catch (\Throwable $e) { $counts['deferred']++; } } } $output->writeln('MOBILE_RECORDING_COS_MIGRATE ' . json_encode($counts)); return 0; } catch (\Throwable $e) { $output->writeln('MOBILE_RECORDING_COS_MIGRATE storage_or_configuration_error'); return 1; } } }