更新
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 批量回填处方业务订单「签收时间」到物流库(express_tracking / express_trace)。
|
||||
*
|
||||
* 背景:导出列表的「签收日期」来自库表(ExpressTrackingService::batchResolveSignUnixFromDbForOrders,无 HTTP)。
|
||||
* 终态订单(已完成3/已签收6)被 express:auto-update 排除、或从未创建过物流追踪记录时,库内就没有签收时间,
|
||||
* 导出便显示为空。本命令对这些订单逐单走一次快递100 并落库(queryAndUpdate 会写入 sign_time + 轨迹),
|
||||
* 之后导出直接读库即可,既快又不空。
|
||||
*
|
||||
* 使用方法:
|
||||
* php think tcm:backfill-sign-time --dry-run
|
||||
* php think tcm:backfill-sign-time --limit=300 --sleep-ms=200
|
||||
* php think tcm:backfill-sign-time --fulfillment-status=3,5,6 --days=90
|
||||
* php think tcm:backfill-sign-time --order-id=474
|
||||
* php think tcm:backfill-sign-time --force # 库内已有签收时间也重查
|
||||
* php think tcm:backfill-sign-time --include-gancao # 同时处理甘草托管单(默认跳过,由甘草侧拉取)
|
||||
*
|
||||
* 履约状态代码(fulfillment_status):1待双审通过 2待发货 3已完成 4已取消 5已发货 6已签收
|
||||
* 7进行中 8暂不制药 9拒收 10退款 11保留药方 12制药缓发
|
||||
*
|
||||
* 建议配 crontab(如每小时一次,限量推进历史欠数据):
|
||||
* 0 * * * * cd /path/to/server && php think tcm:backfill-sign-time --limit=300 >> /dev/null 2>&1
|
||||
*/
|
||||
class TcmBackfillPrescriptionOrderSignTime extends Command
|
||||
{
|
||||
/**
|
||||
* 默认不按履约状态过滤:只要订单有运单号(tracking_number)即视为已发货,纳入回填。
|
||||
* 按 fulfillment_status 过滤会漏掉「物流已送达、但业务状态滞后(如进行中)」的单子。
|
||||
* 仍可用 --fulfillment-status 显式收窄。
|
||||
*/
|
||||
private const DEFAULT_FULFILLMENT_STATUSES = [];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('tcm:backfill-sign-time')
|
||||
->setDescription('批量回填处方业务订单签收时间到物流库(导出读库即可,不再逐单 HTTP)')
|
||||
->addOption('dry-run', null, Option::VALUE_NONE, '只列出待处理订单,不发起查询/不落库')
|
||||
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '本次最多补查(HTTP)订单数', 300)
|
||||
->addOption('order-id', null, Option::VALUE_OPTIONAL, '仅处理指定 prescription_order.id', null)
|
||||
->addOption('min-id', null, Option::VALUE_OPTIONAL, '仅 id>=该值', null)
|
||||
->addOption('max-id', null, Option::VALUE_OPTIONAL, '仅 id<=该值', null)
|
||||
->addOption('days', null, Option::VALUE_OPTIONAL, '仅处理近 N 天创建的订单(0=不限)', 0)
|
||||
->addOption(
|
||||
'fulfillment-status',
|
||||
null,
|
||||
Option::VALUE_OPTIONAL,
|
||||
'仅处理指定履约状态,逗号分隔数字(如 3,5,6);默认不限(有运单号即处理)',
|
||||
null
|
||||
)
|
||||
->addOption('repair-db', null, Option::VALUE_NONE, '仅按库内轨迹重算并修复 sign_time 列(零 HTTP,修复历史「拉取当天」污染)')
|
||||
->addOption('include-gancao', null, Option::VALUE_NONE, '同时处理甘草托管单(默认跳过)')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '库内已有签收时间也重新查询落库')
|
||||
->addOption('sleep-ms', null, Option::VALUE_OPTIONAL, '每次 HTTP 查询后休眠毫秒,减轻快递100 压力', 200)
|
||||
->addOption('detail', 'd', Option::VALUE_NONE, '打印每条明细');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]|null 解析失败(传了非法值)返回 null;未传返回默认集
|
||||
*/
|
||||
private static function parseFulfillmentStatusOption(?string $raw): ?array
|
||||
{
|
||||
if ($raw === null || trim($raw) === '') {
|
||||
return self::DEFAULT_FULFILLMENT_STATUSES;
|
||||
}
|
||||
$seen = [];
|
||||
foreach (explode(',', $raw) as $part) {
|
||||
$n = (int) trim($part);
|
||||
if ($n >= 1 && $n <= 12) {
|
||||
$seen[$n] = true;
|
||||
}
|
||||
}
|
||||
$ids = array_keys($seen);
|
||||
sort($ids);
|
||||
|
||||
return $ids !== [] ? $ids : null;
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$dryRun = (bool) $input->getOption('dry-run');
|
||||
$verbose = (bool) $input->getOption('detail');
|
||||
$force = (bool) $input->getOption('force');
|
||||
$includeGancao = (bool) $input->getOption('include-gancao');
|
||||
$limit = max(1, (int) $input->getOption('limit'));
|
||||
$sleepMs = max(0, (int) $input->getOption('sleep-ms'));
|
||||
$days = max(0, (int) $input->getOption('days'));
|
||||
|
||||
$onlyOrderId = $input->getOption('order-id');
|
||||
$onlyOrderId = $onlyOrderId !== null && $onlyOrderId !== '' ? (int) $onlyOrderId : null;
|
||||
$minId = $input->getOption('min-id');
|
||||
$minId = $minId !== null && $minId !== '' ? (int) $minId : null;
|
||||
$maxId = $input->getOption('max-id');
|
||||
$maxId = $maxId !== null && $maxId !== '' ? (int) $maxId : null;
|
||||
|
||||
$fsRaw = $input->getOption('fulfillment-status');
|
||||
$fulfillmentStatuses = self::parseFulfillmentStatusOption($fsRaw !== null ? (string) $fsRaw : null);
|
||||
if ($fulfillmentStatuses === null) {
|
||||
$output->error('--fulfillment-status 格式无效,请传入 1–12 的数字,英文逗号分隔,例如:3,5,6');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('批量回填业务订单签收时间(快递100 落库)');
|
||||
$output->writeln('========================================');
|
||||
|
||||
// 纯库内修复模式:用现有轨迹重算签收时间,覆盖被写成「同步当天」的 sign_time 列(零 HTTP)
|
||||
if ((bool) $input->getOption('repair-db')) {
|
||||
$output->writeln('模式:仅修复 sign_time 列(按库内轨迹重算,不查快递100)...');
|
||||
$stat = ExpressTrackingService::repairSignTimeFromDbTraces($dryRun, $dryRun ? 0 : $limit);
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln(sprintf(
|
||||
'修复完成:扫描已签收运单 %d,修正 sign_time %d,无需修正 %d(dry-run=%s)',
|
||||
$stat['scanned'],
|
||||
$stat['updated'],
|
||||
$stat['skipped'],
|
||||
$dryRun ? 'yes' : 'no'
|
||||
));
|
||||
$output->writeln('========================================');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$q = PrescriptionOrder::whereNull('delete_time')
|
||||
->whereRaw("TRIM(COALESCE(`tracking_number`, '')) <> ''")
|
||||
->order('id', 'desc');
|
||||
|
||||
if ($onlyOrderId !== null && $onlyOrderId > 0) {
|
||||
$q->where('id', $onlyOrderId);
|
||||
} else {
|
||||
// 空集 = 不按履约状态过滤(默认):有运单号即纳入,避免漏掉业务状态滞后的已送达单
|
||||
if ($fulfillmentStatuses !== []) {
|
||||
$q->whereIn('fulfillment_status', $fulfillmentStatuses);
|
||||
}
|
||||
if (!$includeGancao) {
|
||||
$q->whereRaw("TRIM(COALESCE(`gancao_reciperl_order_no`, '')) = ''");
|
||||
}
|
||||
if ($minId !== null && $minId > 0) {
|
||||
$q->where('id', '>=', $minId);
|
||||
}
|
||||
if ($maxId !== null && $maxId > 0) {
|
||||
$q->where('id', '<=', $maxId);
|
||||
}
|
||||
if ($days > 0) {
|
||||
$q->where('create_time', '>=', time() - $days * 86400);
|
||||
}
|
||||
// 多取一些候选,DB 已可解析的会被跳过,确保「需要补查」的能凑够 limit
|
||||
$q->limit($limit * 5);
|
||||
}
|
||||
|
||||
$rows = $q->field(['id', 'order_no', 'tracking_number', 'fulfillment_status', 'create_time'])
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
$output->warning('没有符合条件的订单。');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 先批量从库表解析,跳过已有签收时间的(除非 --force),避免无谓 HTTP
|
||||
$dbSignByPo = [];
|
||||
if (!$force) {
|
||||
$signItems = [];
|
||||
foreach ($rows as $r) {
|
||||
$signItems[] = [
|
||||
'order_id' => (int) ($r['id'] ?? 0),
|
||||
'tracking_number' => trim((string) ($r['tracking_number'] ?? '')),
|
||||
];
|
||||
}
|
||||
try {
|
||||
$dbSignByPo = ExpressTrackingService::batchResolveSignUnixFromDbForOrders($signItems);
|
||||
} catch (\Throwable $e) {
|
||||
$output->warning('批量库表解析失败,将逐单补查:' . $e->getMessage());
|
||||
$dbSignByPo = [];
|
||||
}
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
$alreadyInDb = 0;
|
||||
foreach ($rows as $r) {
|
||||
$id = (int) ($r['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!$force && (int) ($dbSignByPo[$id] ?? 0) > 0) {
|
||||
$alreadyInDb++;
|
||||
continue;
|
||||
}
|
||||
$candidates[] = $r;
|
||||
if (count($candidates) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln(sprintf(
|
||||
'候选 %d 条(库内已有签收 %d 条已跳过);本次最多补查 %d 条(dry-run=%s force=%s sleep-ms=%d)',
|
||||
count($rows),
|
||||
$alreadyInDb,
|
||||
$limit,
|
||||
$dryRun ? 'yes' : 'no',
|
||||
$force ? 'yes' : 'no',
|
||||
$sleepMs
|
||||
));
|
||||
|
||||
if ($candidates === []) {
|
||||
$output->writeln('无需补查:候选订单签收时间均已在库。');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$resolved = 0; // 查到并落库了签收时间
|
||||
$empty = 0; // 查询成功但暂无签收(在途等)
|
||||
$failed = 0;
|
||||
|
||||
$totalToDo = count($candidates);
|
||||
// 每单一次快递100 HTTP + sleep,串行较慢;定期打印进度,避免看起来卡住。
|
||||
$progressEvery = max(1, (int) min(20, max(5, (int) ceil($totalToDo / 20))));
|
||||
if (!$dryRun) {
|
||||
$output->writeln(sprintf(
|
||||
'开始逐单补查(共 %d 单,每单一次快递100 查询 + %dms 间隔,请耐心等待,每 %d 单报告一次进度)...',
|
||||
$totalToDo,
|
||||
$sleepMs,
|
||||
$progressEvery
|
||||
));
|
||||
}
|
||||
$startTs = microtime(true);
|
||||
|
||||
$done = 0;
|
||||
foreach ($candidates as $r) {
|
||||
$id = (int) ($r['id'] ?? 0);
|
||||
$tn = trim((string) ($r['tracking_number'] ?? ''));
|
||||
|
||||
if ($dryRun) {
|
||||
if ($verbose) {
|
||||
$output->writeln(sprintf('#%d %s 运单=%s (dry-run,跳过查询)', $id, (string) ($r['order_no'] ?? ''), $tn));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$signTs = ExpressTrackingService::syncSignUnixFromLogisticsForPrescriptionOrder($id, true);
|
||||
if ($signTs > 0) {
|
||||
$resolved++;
|
||||
if ($verbose) {
|
||||
$output->writeln(sprintf('#%d 运单=%s → 签收 %s', $id, $tn, date('Y-m-d H:i:s', $signTs)));
|
||||
}
|
||||
} else {
|
||||
$empty++;
|
||||
if ($verbose) {
|
||||
$output->writeln(sprintf('#%d 运单=%s → 暂无签收(在途/未匹配)', $id, $tn));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
if ($verbose) {
|
||||
$output->writeln("<error>#{$id} 运单={$tn} 失败:{$e->getMessage()}</error>");
|
||||
}
|
||||
}
|
||||
|
||||
$done++;
|
||||
// 非明细模式也给出心跳进度(明细模式已逐单打印,无需重复)
|
||||
if (!$verbose && ($done % $progressEvery === 0 || $done === $totalToDo)) {
|
||||
$elapsed = max(0.001, microtime(true) - $startTs);
|
||||
$rate = $done / $elapsed;
|
||||
$eta = $rate > 0 ? (int) round(($totalToDo - $done) / $rate) : 0;
|
||||
$output->writeln(sprintf(
|
||||
' 进度 %d/%d(落库 %d,暂无 %d,失败 %d)已用 %ds,预计剩余 %ds',
|
||||
$done,
|
||||
$totalToDo,
|
||||
$resolved,
|
||||
$empty,
|
||||
$failed,
|
||||
(int) round($elapsed),
|
||||
$eta
|
||||
));
|
||||
}
|
||||
|
||||
if ($sleepMs > 0) {
|
||||
usleep($sleepMs * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln(sprintf('完成:落库签收 %d,暂无签收 %d,失败 %d', $resolved, $empty, $failed));
|
||||
$output->writeln('========================================');
|
||||
|
||||
return $failed > 0 ? 2 : 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user