0(to=0 的释放行 is_inherit 恒为 0)。按诊单分块 2000 批量取订单序列,不逐行查库。 * 异常:整体 try/catch 并 return 0(Crontab::start 捕获异常会把 dev_crontab.status 置 ERROR 停跑)。 */ class BackfillInheritVisitSlot extends Command { /** 每批处理的日志行数(按诊单分块取订单) */ private const CHUNK_SIZE = 2000; /** dry-run / verify 最多打印条数 */ private const PRINT_LIMIT = 50; protected function configure() { $this->setName('tcm:backfill-inherit-visit-slot') ->setDescription('继承指派「继承时诊次」快照回填 / 对账(inherit_visit_slot)') ->addOption('dry-run', null, Option::VALUE_NONE, '只计算不写库,打印前 50 条拟写入') ->addOption('recalc', null, Option::VALUE_NONE, '重算全部继承行(默认只回填 calc_time=0 的行)') ->addOption('verify', null, Option::VALUE_NONE, '对账模式:输出快照 vs 实时漂移清单,不写库') ->addOption('month', null, Option::VALUE_OPTIONAL, '仅处理指派时间落在该月(YYYY-MM)的继承行', ''); } protected function execute(Input $input, Output $output): int { $startedAt = microtime(true); $now = time(); try { $dryRun = (bool) $input->getOption('dry-run'); $recalc = (bool) $input->getOption('recalc'); $verify = (bool) $input->getOption('verify'); $month = trim((string) $input->getOption('month')); if ($month !== '' && preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) !== 1) { $output->writeln('无效 --month,格式应为 YYYY-MM'); return 0; } $mode = $verify ? 'verify' : ($dryRun ? 'dry-run' : ($recalc ? 'recalc' : 'backfill')); $output->writeln(sprintf( '[%s] 继承时诊次快照 %s 开始%s', date('Y-m-d H:i:s', $now), $mode, $month !== '' ? ',月份=' . $month : '' )); $query = Db::name('tcm_diagnosis_assign_log') ->where('is_inherit', 1) ->where('to_assistant_id', '>', 0) ->where('diagnosis_id', '>', 0); if ($month !== '') { $startTs = (int) strtotime($month . '-01 00:00:00'); $endTs = (int) strtotime(date('Y-m-t', $startTs) . ' 23:59:59'); $query->where('create_time', '>=', $startTs)->where('create_time', '<=', $endTs); } if (!$verify && !$recalc) { $query->where('inherit_visit_slot_calc_time', 0); } $rows = $query ->field(['id', 'diagnosis_id', 'to_assistant_id', 'create_time', 'inherit_visit_slot', 'inherit_visit_slot_calc_time']) ->order(['diagnosis_id' => 'asc', 'id' => 'asc']) ->select() ->toArray(); $total = \count($rows); $output->writeln(sprintf('待处理继承行 %d 条', $total)); if ($total === 0) { return 0; } $processed = 0; $written = 0; $drift = 0; $printed = 0; foreach (array_chunk($rows, self::CHUNK_SIZE) as $chunk) { $diagIds = array_values(array_unique(array_map(static fn (array $r): int => (int) $r['diagnosis_id'], $chunk))); $offsetMap = RevisitVisitSlotService::fetchRevisitSlotStartOffsetMap($diagIds); /** @var array> $ctsByDiag */ $ctsByDiag = []; foreach (RevisitVisitSlotService::fetchOrderSeqRows($diagIds, ['o.diagnosis_id', 'o.create_time']) as $o) { $ctsByDiag[(int) $o['diagnosis_id']][] = (int) $o['create_time']; } foreach ($chunk as $r) { $processed++; $logId = (int) $r['id']; $did = (int) $r['diagnosis_id']; $t = (int) $r['create_time']; $realtime = min(255, RevisitVisitSlotService::slotBefore( $ctsByDiag[$did] ?? [], RevisitVisitSlotService::resolveRevisitSlotStartOffset($did, $offsetMap), $t )); $snapshot = (int) $r['inherit_visit_slot']; $calcTime = (int) $r['inherit_visit_slot_calc_time']; if ($verify) { if ($calcTime > 0 && $snapshot === $realtime) { continue; } $drift++; if ($printed < self::PRINT_LIMIT) { $printed++; $output->writeln(sprintf( ' 漂移 log_id=%d diagnosis_id=%d 指派时间=%s 快照=%d 实时=%d 原因=%s', $logId, $did, date('Y-m-d H:i:s', $t), $snapshot, $realtime, self::driftReason($calcTime, $snapshot, $realtime) )); } continue; } if ($dryRun) { if ($printed < self::PRINT_LIMIT) { $printed++; $output->writeln(sprintf( ' [演练] log_id=%d diagnosis_id=%d 指派时间=%s 当前快照=%d → 拟写入=%d', $logId, $did, date('Y-m-d H:i:s', $t), $snapshot, $realtime )); } continue; } Db::name('tcm_diagnosis_assign_log') ->where('id', $logId) ->update([ 'inherit_visit_slot' => $realtime, 'inherit_visit_slot_calc_time' => $now, ]); $written++; } } if ($verify) { $output->writeln(sprintf( '对账完成:检查 %d 条,漂移 %d 条%s,耗时 %.2fs', $processed, $drift, $drift > self::PRINT_LIMIT ? sprintf('(仅打印前 %d 条)', self::PRINT_LIMIT) : '', microtime(true) - $startedAt )); } elseif ($dryRun) { $output->writeln(sprintf('演练完成:应回填 %d 条(未落库),耗时 %.2fs', $processed, microtime(true) - $startedAt)); } else { $output->writeln(sprintf('回填完成:处理 %d 条,写入 %d 条,耗时 %.2fs', $processed, $written, microtime(true) - $startedAt)); } return 0; } catch (\Throwable $e) { Log::error('继承时诊次快照回填异常: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); $output->writeln('执行异常: ' . $e->getMessage() . ''); return 0; } } /** 漂移原因(无历史快照时只能按方向推断) */ private static function driftReason(int $calcTime, int $snapshot, int $realtime): string { if ($calcTime <= 0) { return '未回填(calc_time=0,加列前的历史继承行)'; } if ($realtime > $snapshot) { return '实时值更大:继承前订单被恢复/新增有效订单,或诊单偏移上调'; } return '实时值更小:继承前订单被取消/退款/软删,或诊单偏移下调'; } }