first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\common\model\tcm\PrescriptionOrder;
use app\common\service\ExpressTrackingService;
use app\common\service\gancao\GancaoLogisticsRouteService;
use app\common\service\gancao\GancaoScmRecipelService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 同步甘草订单的物流路由信息到本地物流追踪表
*
* 数据流:
* zyt_tcm_prescription_order (甘草已上传)
* ↓ 调用 igc_scm.logistics.client_opt.pull / GET_TASK_ROUTE_LIST
* ↓ 甘草报快递任务不存在等(如 10101)且业务单已有运单号 → 降级快递100
* zyt_express_tracking + zyt_express_trace + zyt_express_state_log + zyt_express_query_log
*
* 使用方法:
* php think gancao:sync-logistics 默认拉 2000 单(跳过已完成/已签收)
* php think gancao:sync-logistics --limit=500 自定义拉取上限
* php think gancao:sync-logistics --order-id=1424 只跑指定订单(数字 id)
* php think gancao:sync-logistics --order-id=PO20260530165158645023 或 PO 业务单号
* php think gancao:sync-logistics -t SF1234567890 按快递单号走快递100 查询并落库
* php think gancao:sync-logistics --detail 打印每单详细
*
* 建议 crontab(每 30 分钟执行一次):
* 0,30 * * * * cd /path/to/server && php think gancao:sync-logistics >> runtime/log/gancao_sync.log 2>&1
*
* 跳过履约状态为已完成(3)、已签收(6) 的业务订单,不再拉取甘草路由。
* 已对「待释放医助非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/
class GancaoSyncLogisticsRoute extends Command
{
protected function configure()
{
$this->setName('gancao:sync-logistics')
->setDescription('同步甘草订单的物流路由(GET_TASK_ROUTE_LIST)到本地物流追踪表')
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '本次最多处理多少条订单(跳过已完成/已签收)', 2000)
->addOption('order-id', null, Option::VALUE_OPTIONAL, '只同步指定订单:prescription_order.id 或 PO 业务单号 order_no', null)
->addOption('tracking-number', 't', Option::VALUE_REQUIRED, '按快递单号走快递100 查询并落库')
->addOption('detail', 'd', Option::VALUE_NONE, '打印每单详细结果');
}
protected function execute(Input $input, Output $output)
{
$limit = max(1, (int) $input->getOption('limit'));
$onlyOrderId = null;
$onlyOrderNo = '';
$orderIdArg = $input->getOption('order-id');
if ($orderIdArg !== null && trim((string) $orderIdArg) !== '') {
$resolved = self::resolvePrescriptionOrderId((string) $orderIdArg);
if ($resolved === null) {
$output->error('未找到订单:' . trim((string) $orderIdArg) . '--order-id 支持数字 id 或 PO 业务单号)');
return 1;
}
$onlyOrderId = $resolved['id'];
$onlyOrderNo = $resolved['order_no'];
}
$trackingNumber = trim((string) $input->getOption('tracking-number'));
$verbose = (bool) $input->getOption('detail');
if ($trackingNumber !== '' && $onlyOrderId !== null) {
$output->error('请勿同时使用 --tracking-number 与 --order-id');
return 1;
}
$output->writeln('========================================');
$output->writeln($trackingNumber !== '' ? '快递100 物流查询' : '甘草物流路由同步');
$output->writeln('========================================');
if ($trackingNumber === '' && !GancaoScmRecipelService::isConfigured()) {
$output->error('甘草 SCM 未配置:' . GancaoScmRecipelService::whyNotConfigured());
return 1;
}
$start = microtime(true);
if ($trackingNumber !== '') {
$output->writeln('开始查询...(快递100tracking_number=' . $trackingNumber . '');
} else {
if ($onlyOrderId !== null) {
$output->writeln('开始同步...(指定单 order_id=' . $onlyOrderId . ($onlyOrderNo !== '' ? ', order_no=' . $onlyOrderNo : '') . '');
} else {
$output->writeln('开始同步...limit=' . $limit . '');
}
}
try {
if ($trackingNumber !== '') {
$stats = ExpressTrackingService::queryKuaidiByTrackingNumber($trackingNumber);
$stats['reconcile_cleared'] = 0;
$stats['reconcile_scanned'] = 0;
$stats['reconcile_lines'] = [];
} else {
$stats = GancaoLogisticsRouteService::syncBatch($limit, $onlyOrderId);
if ($onlyOrderId !== null) {
$stats['reconcile_cleared'] = 0;
$stats['reconcile_scanned'] = 0;
$stats['reconcile_lines'] = [];
} else {
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(2000);
$stats['reconcile_cleared'] = (int) ($recon['cleared'] ?? 0);
$stats['reconcile_scanned'] = (int) ($recon['scanned'] ?? 0);
$stats['reconcile_lines'] = $recon['lines'] ?? [];
}
}
} catch (\Throwable $e) {
$output->error('同步异常:' . $e->getMessage());
return 1;
}
$duration = round(microtime(true) - $start, 2);
if ($verbose && !empty($stats['details'])) {
$output->writeln('');
$output->writeln('--- 详细 ---');
foreach ($stats['details'] as $row) {
$tag = !empty($row['success']) ? '[OK]' : '[FAIL]';
$line = sprintf(
'%s order_id=%s order_no=%s app_order_no=%s tn=%s state=%s traces=+%s source=%s msg=%s',
$tag,
$row['order_id'] ?? '',
$row['order_no'] ?? '',
$row['app_order_no'] ?? '',
$row['tracking_number'] ?? '',
$row['state'] ?? '',
$row['traces'] ?? 0,
$row['source'] ?? '',
$row['message'] ?? ''
);
$output->writeln($line);
}
}
$output->writeln('');
$output->writeln('========================================');
$output->writeln('同步完成');
$output->writeln('========================================');
$output->writeln('总数:' . $stats['total']);
if (isset($stats['skipped'])) {
$output->writeln('跳过:' . (int) $stats['skipped'] . '(已完成/已签收)');
}
$output->writeln('成功:' . $stats['success']);
$output->writeln('失败:' . $stats['failed']);
$output->writeln('医助已移除(甘草同步+指派日志):' . (int) ($stats['assistant_cleared'] ?? 0));
$output->writeln('医助已移除(履约已发货核对):' . (int) ($stats['reconcile_cleared'] ?? 0) . '(扫描 ' . (int) ($stats['reconcile_scanned'] ?? 0) . ' 单)');
$lines = array_merge($stats['assistant_lines'] ?? [], $stats['reconcile_lines'] ?? []);
if ($lines === []) {
$output->writeln('医助明细:(无)');
} else {
$output->writeln('医助明细:');
foreach ($lines as $line) {
$output->writeln(' ' . $line);
}
}
$output->writeln('耗时:' . $duration . 's');
return 0;
}
/**
* @return array{id:int, order_no:string}|null
*/
private static function resolvePrescriptionOrderId(string $raw): ?array
{
$raw = trim($raw);
if ($raw === '') {
return null;
}
$q = PrescriptionOrder::whereNull('delete_time');
if (preg_match('/^\d+$/', $raw) === 1) {
$id = (int) $raw;
if ($id <= 0) {
return null;
}
$row = (clone $q)->where('id', $id)->field('id,order_no')->find();
} else {
$row = (clone $q)->where('order_no', $raw)->field('id,order_no')->find();
}
if (!$row) {
return null;
}
return [
'id' => (int) $row->id,
'order_no' => (string) $row->order_no,
];
}
}