Files
zyt/server/app/command/BackfillInheritVisitSlot.php
T
2026-10-15 11:03:54 +08:00

205 lines
9.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\command;
use app\common\service\tcm\RevisitVisitSlotService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Log;
/**
* 继承指派「继承时诊次」快照回填 / 对账(tcm_diagnosis_assign_log.inherit_visit_slot / inherit_visit_slot_calc_time)
*
* 使用方法:
* php think tcm:backfill-inherit-visit-slot --dry-run # 只计算、打印前 50 条拟写入,不写库
* php think tcm:backfill-inherit-visit-slot # 回填 calc_time=0 的继承行
* php think tcm:backfill-inherit-visit-slot --recalc # 全部继承行重算(含已回填)
* php think tcm:backfill-inherit-visit-slot --verify # 对账:快照 vs 实时重算,输出漂移清单,不写库
* php think tcm:backfill-inherit-visit-slot --month=2026-09 # 仅处理指派时间落在该月的继承行(可与以上组合)
*
* 口径(RevisitVisitSlotService::slotBefore,与 RevisitRateLogic / RevisitRateCalculator 完全一致):
* k = 继承时刻之前(严格小于 create_time)的有效业务订单数(剔除履约 4/9/10、软删)+ 诊单偏移(钳 0~20)。
* 统计口径永远实时重算,本列只用于指派记录面板展示与对账;事后退款/取消/改偏移会让快照与实时值漂移,属预期(--verify 列出)。
*
* 范围:is_inherit=1 AND to_assistant_id>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('<error>无效 --month,格式应为 YYYY-MM</error>');
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<int, list<int>> $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('<error>执行异常: ' . $e->getMessage() . '</error>');
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 '实时值更小:继承前订单被取消/退款/软删,或诊单偏移下调';
}
}