更新
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
<?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 '实时值更小:继承前订单被取消/退款/软删,或诊单偏移下调';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user