Files
zyt/server/app/command/MobileRecordingCosWork.php
2026-11-09 14:51:31 +08:00

73 lines
4.6 KiB
PHP

<?php
declare(strict_types=1);
namespace app\command;
use app\adminapi\logic\tcm\MobileRecordingMediaLogic;
use app\common\service\mobilerecording\MobileRecordingObjects;
use app\common\service\mobilerecording\MobileRecordingWork;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Db;
/** Schedule once per minute with a non-overlap lock; contains no cloud credentials in arguments. */
final class MobileRecordingCosWork extends Command
{
protected function configure()
{
$this->setName('mobile-recording:cos-work')->setDescription('推进手机录音COS提交及私有对象删除重试')
->addOption('dry-run', null, Option::VALUE_NONE, '只统计,不调用COS')
->addOption('limit', null, Option::VALUE_REQUIRED, '最多处理的上传数,1至100', '5')
->addOption('seconds', null, Option::VALUE_REQUIRED, '上传调度时间预算,1至300秒;单个COS请求仍有10秒超时', '20');
}
protected function execute(Input $input, Output $output): int
{
try {
$limit = max(1, min(100, (int) $input->getOption('limit')));
$seconds = max(1, min(300, (int) $input->getOption('seconds')));
$dry = (bool) $input->getOption('dry-run');
$deadline = microtime(true) + $seconds;
$counts = MobileRecordingObjects::cleanup(1, $dry);
$counts += ['uploads_selected' => 0, 'uploads_completed' => 0, 'uploads_deferred' => 0,
'uploads_denied' => 0, 'staging_cleaned' => 0];
$rows = Db::name('mobile_recording_upload')->alias('u')
->join([Db::name('mobile_recording_binding')->getTable() => 'b'], 'b.id=u.binding_id')
->leftJoin([Db::name('mobile_recording_object')->getTable() => 'o'], 'o.upload_id=u.upload_id')
->leftJoin([Db::name('mobile_recording_asset')->getTable() => 'a'], 'a.upload_id=u.upload_id AND a.binding_id=u.binding_id AND a.kind=u.kind')
->leftJoin([Db::name('mobile_recording_work')->getTable() => 'w'], 'w.upload_id=u.upload_id')
->whereRaw("((u.state='uploading' AND u.received_bytes=u.size_bytes) OR (u.state='complete' AND o.state IN ('pending','finalizing','verifying')) OR (u.state='complete' AND o.state='ready' AND a.upload_id IS NOT NULL AND COALESCE(w.staging_cleaned,0)=0))")
->whereRaw('COALESCE(w.next_retry_at,0) <= ' . time())
->field('u.upload_id,u.state,o.state AS object_state,b.admin_id,b.diagnosis_id')
->orderRaw('COALESCE(w.last_attempt_at,0),u.updated_at,u.upload_id')->limit($limit)->select()->toArray();
$counts['uploads_selected'] = count($rows);
if (!$dry) {
foreach ($rows as $row) {
if (microtime(true) >= $deadline) { break; }
try {
if ($row['state'] === 'complete' && $row['object_state'] === 'ready') {
if (!MobileRecordingObjects::cleanupStaging($row['upload_id'], true)) { throw new \DomainException('Media publication no longer active'); }
$counts['staging_cleaned']++;
} elseif ($row['state'] === 'complete') { MobileRecordingMediaLogic::migrateLocal($row['upload_id']); $counts['uploads_completed']++; }
else { MobileRecordingMediaLogic::complete(['upload_id' => $row['upload_id'], 'diagnosis_id' => (int) $row['diagnosis_id']], (int) $row['admin_id']); }
if ($row['state'] === 'uploading') {
// complete may publish successfully while best-effort unlink failed. Keep that task retryable.
if (!MobileRecordingObjects::cleanupStaging($row['upload_id'], true)) { throw new \DomainException('Media publication no longer active'); }
$counts['uploads_completed']++;
}
MobileRecordingWork::attempted($row['upload_id']);
} catch (\Throwable $e) {
MobileRecordingWork::attempted($row['upload_id'], $e);
$counts['uploads_deferred']++;
if ($e instanceof \DomainException) { $counts['uploads_denied']++; }
}
}
}
$output->writeln('MOBILE_RECORDING_COS_WORK ' . json_encode($counts));
return $counts['errors'] > 0 || $counts['uploads_deferred'] > 0 ? 1 : 0;
} catch (\Throwable $e) { $output->writeln('MOBILE_RECORDING_COS_WORK storage_or_configuration_error'); return 1; }
}
}