新增
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,68 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 物流自动更新定时任务
|
||||
*
|
||||
* 使用方法:
|
||||
* php think express:auto-update
|
||||
*
|
||||
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货」核对释放诊单医助(跳过已完成/已签收业务单,与是否甘草单无关)
|
||||
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
|
||||
*
|
||||
* 已对「待释放医助非二中心」执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at,
|
||||
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
|
||||
*/
|
||||
class ExpressAutoUpdate extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('express:auto-update')
|
||||
->setDescription('自动更新物流追踪信息');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始自动更新物流信息...');
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$result = ExpressTrackingService::autoUpdateBatch(1000);
|
||||
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000);
|
||||
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln("更新完成!");
|
||||
$output->writeln("总数: {$result['total']}");
|
||||
$output->writeln("成功: {$result['success']}");
|
||||
$output->writeln("失败: {$result['failed']}");
|
||||
$output->writeln("医助已移除(物流任务+指派日志): " . (int) ($result['assistant_cleared'] ?? 0));
|
||||
$output->writeln("医助已移除(履约已发货核对): " . (int) ($recon['cleared'] ?? 0) . " (扫描 " . (int) ($recon['scanned'] ?? 0) . " 单)");
|
||||
$lines = array_merge($result['assistant_lines'] ?? [], $recon['lines'] ?? []);
|
||||
if ($lines === []) {
|
||||
$output->writeln('医助明细: (无)');
|
||||
} else {
|
||||
$output->writeln('医助明细:');
|
||||
foreach ($lines as $line) {
|
||||
$output->writeln(' ' . $line);
|
||||
}
|
||||
}
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$output->error("更新失败: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 物流自动更新定时任务
|
||||
*
|
||||
* 使用方法:
|
||||
* php think express:auto-update
|
||||
*
|
||||
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货」核对释放诊单医助(跳过已完成/已签收业务单,与是否甘草单无关)
|
||||
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
|
||||
*
|
||||
* 已对「待释放医助非二中心」执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at,
|
||||
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
|
||||
*/
|
||||
class ExpressAutoUpdate extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('express:auto-update')
|
||||
->setDescription('自动更新物流追踪信息');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始自动更新物流信息...');
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$result = ExpressTrackingService::autoUpdateBatch(1000);
|
||||
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000);
|
||||
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln("更新完成!");
|
||||
$output->writeln("总数: {$result['total']}");
|
||||
$output->writeln("成功: {$result['success']}");
|
||||
$output->writeln("失败: {$result['failed']}");
|
||||
$output->writeln("医助已移除(物流任务+指派日志): " . (int) ($result['assistant_cleared'] ?? 0));
|
||||
$output->writeln("医助已移除(履约已发货核对): " . (int) ($recon['cleared'] ?? 0) . " (扫描 " . (int) ($recon['scanned'] ?? 0) . " 单)");
|
||||
$lines = array_merge($result['assistant_lines'] ?? [], $recon['lines'] ?? []);
|
||||
if ($lines === []) {
|
||||
$output->writeln('医助明细: (无)');
|
||||
} else {
|
||||
$output->writeln('医助明细:');
|
||||
foreach ($lines as $line) {
|
||||
$output->writeln(' ' . $line);
|
||||
}
|
||||
}
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$output->error("更新失败: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +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('开始查询...(快递100,tracking_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,
|
||||
];
|
||||
}
|
||||
}
|
||||
<?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('开始查询...(快递100,tracking_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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +1,361 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 一次性把 zyt_qywx_external_contact.follow_users JSON 中的 tags:
|
||||
* 1. 合并去重后回填到 zyt_qywx_external_contact.tags(JSON 字段,详情页用)
|
||||
* 2. 拍平按 (external_userid, follow_user_id, tag_id) 三元组同步到关系表
|
||||
* zyt_qywx_external_contact_tag(用于检索/统计/聚合)
|
||||
*
|
||||
* 使用方法:
|
||||
* php think qywx:backfill-customer-tags
|
||||
* php think qywx:backfill-customer-tags --all (强制刷新所有行,不仅是 tags 为空的)
|
||||
*
|
||||
* 不调企微 API、纯本地解析;新加 tags 字段或关系表后跑一次即可(后续 UPSERT 自动维护)。
|
||||
*/
|
||||
class QywxBackfillCustomerTags extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('qywx:backfill-customer-tags')
|
||||
->addOption(
|
||||
'all',
|
||||
'a',
|
||||
\think\console\input\Option::VALUE_NONE,
|
||||
'强制刷新所有行(默认只处理 tags 为空 / NULL / [] 的行)'
|
||||
)
|
||||
->addOption(
|
||||
'fast',
|
||||
'f',
|
||||
\think\console\input\Option::VALUE_NONE,
|
||||
'快速模式:批量 INSERT IGNORE 关系表,CASE-WHEN 批量 UPDATE tags(首次回填/远程库网络延迟时用)'
|
||||
)
|
||||
->setDescription('回填外部联系人 tags 字段(从本地 follow_users JSON 提取)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$all = (bool) $input->getOption('all');
|
||||
$fast = (bool) $input->getOption('fast');
|
||||
$startTime = microtime(true);
|
||||
|
||||
if ($fast) {
|
||||
return $this->executeFast($input, $output, $all, $startTime);
|
||||
}
|
||||
|
||||
$output->writeln('开始回填 qywx_external_contact.tags ...');
|
||||
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行'));
|
||||
|
||||
$query = Db::name('qywx_external_contact')
|
||||
->whereNull('delete_time')
|
||||
->where('follow_users', '<>', '')
|
||||
->where('follow_users', '<>', '[]');
|
||||
|
||||
if (!$all) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('tags')
|
||||
->whereOr('tags', '')
|
||||
->whereOr('tags', '[]');
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$output->writeln("候选 {$total} 条");
|
||||
|
||||
if ($total === 0) {
|
||||
$output->writeln('无需回填');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$updated = 0;
|
||||
$unchanged = 0;
|
||||
$emptyTags = 0;
|
||||
$relationSynced = 0;
|
||||
|
||||
// 分页处理避免内存爆
|
||||
$pageSize = 500;
|
||||
$lastId = 0;
|
||||
|
||||
while (true) {
|
||||
$rows = (clone $query)
|
||||
->where('id', '>', $lastId)
|
||||
->order('id', 'asc')
|
||||
->limit($pageSize)
|
||||
->field(['id', 'external_userid', 'follow_users', 'tags'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if ($rows === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 拿到本批 id 对应 external_userid,用于同步关系表
|
||||
$idToExt = [];
|
||||
foreach ($rows as $row) {
|
||||
$idToExt[(int) $row['id']] = (string) ($row['external_userid'] ?? '');
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$lastId = (int) $row['id'];
|
||||
$processed++;
|
||||
|
||||
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
|
||||
if (!is_array($followUsers)) {
|
||||
$followUsers = [];
|
||||
}
|
||||
|
||||
// —— 关系表(每行都同步,不依赖 JSON 字段是否变化;--all 模式下也会全量重写)
|
||||
$extId = $idToExt[$lastId] ?? '';
|
||||
if ($extId !== '') {
|
||||
CustomerLogic::syncContactTagsRelation($extId, $followUsers);
|
||||
$relationSynced++;
|
||||
}
|
||||
|
||||
// —— tags JSON 字段(值未变的跳过 UPDATE,省 IO)
|
||||
$newTags = CustomerLogic::extractFollowUserTags($followUsers);
|
||||
$oldTags = (string) ($row['tags'] ?? '');
|
||||
|
||||
if ($newTags === '[]') {
|
||||
$emptyTags++;
|
||||
}
|
||||
|
||||
if ($newTags === $oldTags) {
|
||||
$unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Db::name('qywx_external_contact')
|
||||
->where('id', $lastId)
|
||||
->update([
|
||||
'tags' => $newTags,
|
||||
// 不刷 update_time,避免误触发"最近活跃"类排序
|
||||
]);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (($processed % 2000) === 0) {
|
||||
$output->writeln(sprintf('进度: %d / %d,已更新 %d', $processed, $total, $updated));
|
||||
}
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('回填完成');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("处理: {$processed}");
|
||||
$output->writeln("tags JSON 更新: {$updated}");
|
||||
$output->writeln("tags JSON 未变: {$unchanged}");
|
||||
$output->writeln("空 tags 行数: {$emptyTags} (follow_user 内无任何 tag)");
|
||||
$output->writeln("关系表同步: {$relationSynced} 行");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速模式:批量 INSERT IGNORE + 批量 CASE-WHEN UPDATE,远程库网络延迟下推荐用此模式。
|
||||
* 注意:不会删除已在关系表中、但当前 follow_users 已不再存在的"过时"关系;首次回填场景安全。
|
||||
*/
|
||||
private function executeFast(Input $input, Output $output, bool $all, float $startTime): int
|
||||
{
|
||||
$output->writeln('开始[快速]回填 qywx_external_contact.tags ...');
|
||||
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行') . ' + fast');
|
||||
|
||||
$query = Db::name('qywx_external_contact')
|
||||
->whereNull('delete_time')
|
||||
->where('follow_users', '<>', '')
|
||||
->where('follow_users', '<>', '[]');
|
||||
|
||||
if (!$all) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('tags')
|
||||
->whereOr('tags', '')
|
||||
->whereOr('tags', '[]');
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$output->writeln("候选 {$total} 条");
|
||||
if ($total === 0) {
|
||||
$output->writeln('无需回填');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$tagRowsInserted = 0;
|
||||
$jsonUpdated = 0;
|
||||
$pageSize = 1000;
|
||||
$lastId = 0;
|
||||
$now = time();
|
||||
|
||||
while (true) {
|
||||
$rows = (clone $query)
|
||||
->where('id', '>', $lastId)
|
||||
->order('id', 'asc')
|
||||
->limit($pageSize)
|
||||
->field(['id', 'external_userid', 'follow_users'])
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
$tagBatch = [];
|
||||
$tagJsonByExtId = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$lastId = (int) $row['id'];
|
||||
$processed++;
|
||||
$extId = (string) ($row['external_userid'] ?? '');
|
||||
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
|
||||
if (!is_array($followUsers)) {
|
||||
$followUsers = [];
|
||||
}
|
||||
|
||||
$tagJsonByExtId[$lastId] = CustomerLogic::extractFollowUserTags($followUsers);
|
||||
|
||||
if ($extId === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($followUsers as $fu) {
|
||||
if (!is_array($fu)) {
|
||||
continue;
|
||||
}
|
||||
$followUserId = mb_substr(trim((string) ($fu['userid'] ?? '')), 0, 64);
|
||||
$tags = $fu['tags'] ?? [];
|
||||
if (!is_array($tags)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tags as $t) {
|
||||
if (!is_array($t)) {
|
||||
continue;
|
||||
}
|
||||
$tagId = mb_substr(trim((string) ($t['tag_id'] ?? '')), 0, 64);
|
||||
if ($tagId === '') {
|
||||
continue;
|
||||
}
|
||||
$tagBatch[] = [
|
||||
'external_userid' => $extId,
|
||||
'follow_user_id' => $followUserId,
|
||||
'tag_id' => $tagId,
|
||||
'tag_name' => mb_substr((string) ($t['tag_name'] ?? ''), 0, 128),
|
||||
'group_name' => mb_substr((string) ($t['group_name'] ?? ''), 0, 128),
|
||||
'type' => isset($t['type']) ? (int) $t['type'] : 1,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($tagBatch !== []) {
|
||||
$tagRowsInserted += $this->batchInsertIgnoreTags($tagBatch);
|
||||
}
|
||||
if ($tagJsonByExtId !== []) {
|
||||
$jsonUpdated += $this->batchUpdateTagsJson($tagJsonByExtId);
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('进度: %d / %d 关系累计 %d tags JSON 累计 %d', $processed, $total, $tagRowsInserted, $jsonUpdated));
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('[快速]回填完成');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("处理: {$processed}");
|
||||
$output->writeln("关系表 INSERT IGNORE: {$tagRowsInserted}(含可能被忽略的重复行)");
|
||||
$output->writeln("tags JSON 批量 UPDATE: {$jsonUpdated}");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量 INSERT IGNORE 到关系表。返回受影响(实际新插入)行数。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function batchInsertIgnoreTags(array $rows): int
|
||||
{
|
||||
if ($rows === []) {
|
||||
return 0;
|
||||
}
|
||||
$chunks = array_chunk($rows, 500);
|
||||
$affected = 0;
|
||||
foreach ($chunks as $chunk) {
|
||||
$values = [];
|
||||
$params = [];
|
||||
foreach ($chunk as $r) {
|
||||
$values[] = '(?,?,?,?,?,?,?,?)';
|
||||
$params[] = $r['external_userid'];
|
||||
$params[] = $r['follow_user_id'];
|
||||
$params[] = $r['tag_id'];
|
||||
$params[] = $r['tag_name'];
|
||||
$params[] = $r['group_name'];
|
||||
$params[] = $r['type'];
|
||||
$params[] = $r['create_time'];
|
||||
$params[] = $r['update_time'];
|
||||
}
|
||||
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
|
||||
$sql = "INSERT IGNORE INTO {$prefix}qywx_external_contact_tag "
|
||||
. '(external_userid, follow_user_id, tag_id, tag_name, group_name, type, create_time, update_time) VALUES '
|
||||
. implode(',', $values);
|
||||
Db::execute($sql, $params);
|
||||
$affected += count($chunk);
|
||||
}
|
||||
|
||||
return $affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 CASE WHEN id THEN val 一条 SQL 批量 UPDATE tags JSON。
|
||||
*
|
||||
* @param array<int, string> $idToTagsJson
|
||||
*/
|
||||
private function batchUpdateTagsJson(array $idToTagsJson): int
|
||||
{
|
||||
if ($idToTagsJson === []) {
|
||||
return 0;
|
||||
}
|
||||
$chunks = array_chunk($idToTagsJson, 500, true);
|
||||
$affected = 0;
|
||||
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
|
||||
foreach ($chunks as $chunk) {
|
||||
$cases = [];
|
||||
$ids = [];
|
||||
$params = [];
|
||||
foreach ($chunk as $id => $tagsJson) {
|
||||
$cases[] = 'WHEN ? THEN ?';
|
||||
$params[] = $id;
|
||||
$params[] = $tagsJson;
|
||||
$ids[] = (int) $id;
|
||||
}
|
||||
$idList = implode(',', $ids);
|
||||
$sql = "UPDATE {$prefix}qywx_external_contact SET tags = CASE id "
|
||||
. implode(' ', $cases)
|
||||
. " END WHERE id IN ({$idList})";
|
||||
Db::execute($sql, $params);
|
||||
$affected += count($chunk);
|
||||
}
|
||||
|
||||
return $affected;
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 一次性把 zyt_qywx_external_contact.follow_users JSON 中的 tags:
|
||||
* 1. 合并去重后回填到 zyt_qywx_external_contact.tags(JSON 字段,详情页用)
|
||||
* 2. 拍平按 (external_userid, follow_user_id, tag_id) 三元组同步到关系表
|
||||
* zyt_qywx_external_contact_tag(用于检索/统计/聚合)
|
||||
*
|
||||
* 使用方法:
|
||||
* php think qywx:backfill-customer-tags
|
||||
* php think qywx:backfill-customer-tags --all (强制刷新所有行,不仅是 tags 为空的)
|
||||
*
|
||||
* 不调企微 API、纯本地解析;新加 tags 字段或关系表后跑一次即可(后续 UPSERT 自动维护)。
|
||||
*/
|
||||
class QywxBackfillCustomerTags extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('qywx:backfill-customer-tags')
|
||||
->addOption(
|
||||
'all',
|
||||
'a',
|
||||
\think\console\input\Option::VALUE_NONE,
|
||||
'强制刷新所有行(默认只处理 tags 为空 / NULL / [] 的行)'
|
||||
)
|
||||
->addOption(
|
||||
'fast',
|
||||
'f',
|
||||
\think\console\input\Option::VALUE_NONE,
|
||||
'快速模式:批量 INSERT IGNORE 关系表,CASE-WHEN 批量 UPDATE tags(首次回填/远程库网络延迟时用)'
|
||||
)
|
||||
->setDescription('回填外部联系人 tags 字段(从本地 follow_users JSON 提取)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$all = (bool) $input->getOption('all');
|
||||
$fast = (bool) $input->getOption('fast');
|
||||
$startTime = microtime(true);
|
||||
|
||||
if ($fast) {
|
||||
return $this->executeFast($input, $output, $all, $startTime);
|
||||
}
|
||||
|
||||
$output->writeln('开始回填 qywx_external_contact.tags ...');
|
||||
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行'));
|
||||
|
||||
$query = Db::name('qywx_external_contact')
|
||||
->whereNull('delete_time')
|
||||
->where('follow_users', '<>', '')
|
||||
->where('follow_users', '<>', '[]');
|
||||
|
||||
if (!$all) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('tags')
|
||||
->whereOr('tags', '')
|
||||
->whereOr('tags', '[]');
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$output->writeln("候选 {$total} 条");
|
||||
|
||||
if ($total === 0) {
|
||||
$output->writeln('无需回填');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$updated = 0;
|
||||
$unchanged = 0;
|
||||
$emptyTags = 0;
|
||||
$relationSynced = 0;
|
||||
|
||||
// 分页处理避免内存爆
|
||||
$pageSize = 500;
|
||||
$lastId = 0;
|
||||
|
||||
while (true) {
|
||||
$rows = (clone $query)
|
||||
->where('id', '>', $lastId)
|
||||
->order('id', 'asc')
|
||||
->limit($pageSize)
|
||||
->field(['id', 'external_userid', 'follow_users', 'tags'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if ($rows === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 拿到本批 id 对应 external_userid,用于同步关系表
|
||||
$idToExt = [];
|
||||
foreach ($rows as $row) {
|
||||
$idToExt[(int) $row['id']] = (string) ($row['external_userid'] ?? '');
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$lastId = (int) $row['id'];
|
||||
$processed++;
|
||||
|
||||
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
|
||||
if (!is_array($followUsers)) {
|
||||
$followUsers = [];
|
||||
}
|
||||
|
||||
// —— 关系表(每行都同步,不依赖 JSON 字段是否变化;--all 模式下也会全量重写)
|
||||
$extId = $idToExt[$lastId] ?? '';
|
||||
if ($extId !== '') {
|
||||
CustomerLogic::syncContactTagsRelation($extId, $followUsers);
|
||||
$relationSynced++;
|
||||
}
|
||||
|
||||
// —— tags JSON 字段(值未变的跳过 UPDATE,省 IO)
|
||||
$newTags = CustomerLogic::extractFollowUserTags($followUsers);
|
||||
$oldTags = (string) ($row['tags'] ?? '');
|
||||
|
||||
if ($newTags === '[]') {
|
||||
$emptyTags++;
|
||||
}
|
||||
|
||||
if ($newTags === $oldTags) {
|
||||
$unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Db::name('qywx_external_contact')
|
||||
->where('id', $lastId)
|
||||
->update([
|
||||
'tags' => $newTags,
|
||||
// 不刷 update_time,避免误触发"最近活跃"类排序
|
||||
]);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (($processed % 2000) === 0) {
|
||||
$output->writeln(sprintf('进度: %d / %d,已更新 %d', $processed, $total, $updated));
|
||||
}
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('回填完成');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("处理: {$processed}");
|
||||
$output->writeln("tags JSON 更新: {$updated}");
|
||||
$output->writeln("tags JSON 未变: {$unchanged}");
|
||||
$output->writeln("空 tags 行数: {$emptyTags} (follow_user 内无任何 tag)");
|
||||
$output->writeln("关系表同步: {$relationSynced} 行");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速模式:批量 INSERT IGNORE + 批量 CASE-WHEN UPDATE,远程库网络延迟下推荐用此模式。
|
||||
* 注意:不会删除已在关系表中、但当前 follow_users 已不再存在的"过时"关系;首次回填场景安全。
|
||||
*/
|
||||
private function executeFast(Input $input, Output $output, bool $all, float $startTime): int
|
||||
{
|
||||
$output->writeln('开始[快速]回填 qywx_external_contact.tags ...');
|
||||
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行') . ' + fast');
|
||||
|
||||
$query = Db::name('qywx_external_contact')
|
||||
->whereNull('delete_time')
|
||||
->where('follow_users', '<>', '')
|
||||
->where('follow_users', '<>', '[]');
|
||||
|
||||
if (!$all) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('tags')
|
||||
->whereOr('tags', '')
|
||||
->whereOr('tags', '[]');
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$output->writeln("候选 {$total} 条");
|
||||
if ($total === 0) {
|
||||
$output->writeln('无需回填');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$tagRowsInserted = 0;
|
||||
$jsonUpdated = 0;
|
||||
$pageSize = 1000;
|
||||
$lastId = 0;
|
||||
$now = time();
|
||||
|
||||
while (true) {
|
||||
$rows = (clone $query)
|
||||
->where('id', '>', $lastId)
|
||||
->order('id', 'asc')
|
||||
->limit($pageSize)
|
||||
->field(['id', 'external_userid', 'follow_users'])
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
$tagBatch = [];
|
||||
$tagJsonByExtId = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$lastId = (int) $row['id'];
|
||||
$processed++;
|
||||
$extId = (string) ($row['external_userid'] ?? '');
|
||||
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
|
||||
if (!is_array($followUsers)) {
|
||||
$followUsers = [];
|
||||
}
|
||||
|
||||
$tagJsonByExtId[$lastId] = CustomerLogic::extractFollowUserTags($followUsers);
|
||||
|
||||
if ($extId === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($followUsers as $fu) {
|
||||
if (!is_array($fu)) {
|
||||
continue;
|
||||
}
|
||||
$followUserId = mb_substr(trim((string) ($fu['userid'] ?? '')), 0, 64);
|
||||
$tags = $fu['tags'] ?? [];
|
||||
if (!is_array($tags)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tags as $t) {
|
||||
if (!is_array($t)) {
|
||||
continue;
|
||||
}
|
||||
$tagId = mb_substr(trim((string) ($t['tag_id'] ?? '')), 0, 64);
|
||||
if ($tagId === '') {
|
||||
continue;
|
||||
}
|
||||
$tagBatch[] = [
|
||||
'external_userid' => $extId,
|
||||
'follow_user_id' => $followUserId,
|
||||
'tag_id' => $tagId,
|
||||
'tag_name' => mb_substr((string) ($t['tag_name'] ?? ''), 0, 128),
|
||||
'group_name' => mb_substr((string) ($t['group_name'] ?? ''), 0, 128),
|
||||
'type' => isset($t['type']) ? (int) $t['type'] : 1,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($tagBatch !== []) {
|
||||
$tagRowsInserted += $this->batchInsertIgnoreTags($tagBatch);
|
||||
}
|
||||
if ($tagJsonByExtId !== []) {
|
||||
$jsonUpdated += $this->batchUpdateTagsJson($tagJsonByExtId);
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('进度: %d / %d 关系累计 %d tags JSON 累计 %d', $processed, $total, $tagRowsInserted, $jsonUpdated));
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('[快速]回填完成');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("处理: {$processed}");
|
||||
$output->writeln("关系表 INSERT IGNORE: {$tagRowsInserted}(含可能被忽略的重复行)");
|
||||
$output->writeln("tags JSON 批量 UPDATE: {$jsonUpdated}");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量 INSERT IGNORE 到关系表。返回受影响(实际新插入)行数。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function batchInsertIgnoreTags(array $rows): int
|
||||
{
|
||||
if ($rows === []) {
|
||||
return 0;
|
||||
}
|
||||
$chunks = array_chunk($rows, 500);
|
||||
$affected = 0;
|
||||
foreach ($chunks as $chunk) {
|
||||
$values = [];
|
||||
$params = [];
|
||||
foreach ($chunk as $r) {
|
||||
$values[] = '(?,?,?,?,?,?,?,?)';
|
||||
$params[] = $r['external_userid'];
|
||||
$params[] = $r['follow_user_id'];
|
||||
$params[] = $r['tag_id'];
|
||||
$params[] = $r['tag_name'];
|
||||
$params[] = $r['group_name'];
|
||||
$params[] = $r['type'];
|
||||
$params[] = $r['create_time'];
|
||||
$params[] = $r['update_time'];
|
||||
}
|
||||
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
|
||||
$sql = "INSERT IGNORE INTO {$prefix}qywx_external_contact_tag "
|
||||
. '(external_userid, follow_user_id, tag_id, tag_name, group_name, type, create_time, update_time) VALUES '
|
||||
. implode(',', $values);
|
||||
Db::execute($sql, $params);
|
||||
$affected += count($chunk);
|
||||
}
|
||||
|
||||
return $affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 CASE WHEN id THEN val 一条 SQL 批量 UPDATE tags JSON。
|
||||
*
|
||||
* @param array<int, string> $idToTagsJson
|
||||
*/
|
||||
private function batchUpdateTagsJson(array $idToTagsJson): int
|
||||
{
|
||||
if ($idToTagsJson === []) {
|
||||
return 0;
|
||||
}
|
||||
$chunks = array_chunk($idToTagsJson, 500, true);
|
||||
$affected = 0;
|
||||
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
|
||||
foreach ($chunks as $chunk) {
|
||||
$cases = [];
|
||||
$ids = [];
|
||||
$params = [];
|
||||
foreach ($chunk as $id => $tagsJson) {
|
||||
$cases[] = 'WHEN ? THEN ?';
|
||||
$params[] = $id;
|
||||
$params[] = $tagsJson;
|
||||
$ids[] = (int) $id;
|
||||
}
|
||||
$idList = implode(',', $ids);
|
||||
$sql = "UPDATE {$prefix}qywx_external_contact SET tags = CASE id "
|
||||
. implode(' ', $cases)
|
||||
. " END WHERE id IN ({$idList})";
|
||||
Db::execute($sql, $params);
|
||||
$affected += count($chunk);
|
||||
}
|
||||
|
||||
return $affected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 同步现有订单快递单号到物流追踪表
|
||||
*
|
||||
* 使用方法:
|
||||
* php think express:sync
|
||||
*/
|
||||
class SyncTrackingNumbers extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('express:sync')
|
||||
->setDescription('同步现有订单快递单号到物流追踪表');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始同步现有订单快递单号...');
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// 终态订单不再触发查件:已完成(3)/已取消(4)/已签收(6)/暂不制药(8)/拒收(9)/退款(10)/保留药方(11)/制药缓发(12)
|
||||
$terminalFulfillmentStatus = [3, 4, 6, 8, 9, 10, 11, 12];
|
||||
|
||||
// 查询所有有快递单号、未结案、且未上传甘草的订单
|
||||
// 已上传甘草(gancao_reciperl_order_no 非空)的物流由甘草侧 GancaoLogisticsRouteService 拉取,不重复走快递100
|
||||
$orders = Db::name('tcm_prescription_order')
|
||||
->where('tracking_number', '<>', '')
|
||||
->whereNull('delete_time')
|
||||
->whereNotIn('fulfillment_status', $terminalFulfillmentStatus)
|
||||
->whereRaw("TRIM(COALESCE(gancao_reciperl_order_no, '')) = ''")
|
||||
->field([
|
||||
'id',
|
||||
'tracking_number',
|
||||
'express_company',
|
||||
'recipient_name',
|
||||
'recipient_phone',
|
||||
'shipping_address',
|
||||
])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$total = count($orders);
|
||||
$success = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$output->writeln("找到 {$total} 个有快递单号、未结案、未上传甘草的订单(已跳过已完成/已取消/已签收等终态及甘草已托管订单)");
|
||||
|
||||
foreach ($orders as $order) {
|
||||
try {
|
||||
// 检查是否已存在
|
||||
$exists = Db::name('express_tracking')
|
||||
->where('tracking_number', $order['tracking_number'])
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
|
||||
if ($exists > 0) {
|
||||
$skipped++;
|
||||
$output->writeln("跳过: {$order['tracking_number']} (已存在)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建追踪记录
|
||||
$result = ExpressTrackingService::createOrUpdate([
|
||||
'order_id' => $order['id'],
|
||||
'order_type' => 'prescription',
|
||||
'tracking_number' => $order['tracking_number'],
|
||||
'express_company' => $order['express_company'] ?: 'auto',
|
||||
'recipient_phone' => $order['recipient_phone'],
|
||||
'recipient_name' => $order['recipient_name'],
|
||||
'recipient_address' => $order['shipping_address'],
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
$success++;
|
||||
$output->writeln("成功: {$order['tracking_number']}");
|
||||
} else {
|
||||
$failed++;
|
||||
$output->writeln("失败: {$order['tracking_number']}");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$output->error("错误: {$order['tracking_number']} - {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('同步完成!');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("总数: {$total}");
|
||||
$output->writeln("成功: {$success}");
|
||||
$output->writeln("跳过: {$skipped}");
|
||||
$output->writeln("失败: {$failed}");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
$output->writeln('');
|
||||
$output->writeln('现在可以运行定时任务测试:');
|
||||
$output->writeln(' php think express:auto-update');
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$output->error("同步失败: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 同步现有订单快递单号到物流追踪表
|
||||
*
|
||||
* 使用方法:
|
||||
* php think express:sync
|
||||
*/
|
||||
class SyncTrackingNumbers extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('express:sync')
|
||||
->setDescription('同步现有订单快递单号到物流追踪表');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('开始同步现有订单快递单号...');
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// 终态订单不再触发查件:已完成(3)/已取消(4)/已签收(6)/暂不制药(8)/拒收(9)/退款(10)/保留药方(11)/制药缓发(12)
|
||||
$terminalFulfillmentStatus = [3, 4, 6, 8, 9, 10, 11, 12];
|
||||
|
||||
// 查询所有有快递单号、未结案、且未上传甘草的订单
|
||||
// 已上传甘草(gancao_reciperl_order_no 非空)的物流由甘草侧 GancaoLogisticsRouteService 拉取,不重复走快递100
|
||||
$orders = Db::name('tcm_prescription_order')
|
||||
->where('tracking_number', '<>', '')
|
||||
->whereNull('delete_time')
|
||||
->whereNotIn('fulfillment_status', $terminalFulfillmentStatus)
|
||||
->whereRaw("TRIM(COALESCE(gancao_reciperl_order_no, '')) = ''")
|
||||
->field([
|
||||
'id',
|
||||
'tracking_number',
|
||||
'express_company',
|
||||
'recipient_name',
|
||||
'recipient_phone',
|
||||
'shipping_address',
|
||||
])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$total = count($orders);
|
||||
$success = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$output->writeln("找到 {$total} 个有快递单号、未结案、未上传甘草的订单(已跳过已完成/已取消/已签收等终态及甘草已托管订单)");
|
||||
|
||||
foreach ($orders as $order) {
|
||||
try {
|
||||
// 检查是否已存在
|
||||
$exists = Db::name('express_tracking')
|
||||
->where('tracking_number', $order['tracking_number'])
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
|
||||
if ($exists > 0) {
|
||||
$skipped++;
|
||||
$output->writeln("跳过: {$order['tracking_number']} (已存在)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建追踪记录
|
||||
$result = ExpressTrackingService::createOrUpdate([
|
||||
'order_id' => $order['id'],
|
||||
'order_type' => 'prescription',
|
||||
'tracking_number' => $order['tracking_number'],
|
||||
'express_company' => $order['express_company'] ?: 'auto',
|
||||
'recipient_phone' => $order['recipient_phone'],
|
||||
'recipient_name' => $order['recipient_name'],
|
||||
'recipient_address' => $order['shipping_address'],
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
$success++;
|
||||
$output->writeln("成功: {$order['tracking_number']}");
|
||||
} else {
|
||||
$failed++;
|
||||
$output->writeln("失败: {$order['tracking_number']}");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$output->error("错误: {$order['tracking_number']} - {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln('同步完成!');
|
||||
$output->writeln('========================================');
|
||||
$output->writeln("总数: {$total}");
|
||||
$output->writeln("成功: {$success}");
|
||||
$output->writeln("跳过: {$skipped}");
|
||||
$output->writeln("失败: {$failed}");
|
||||
$output->writeln("耗时: {$duration}秒");
|
||||
$output->writeln('');
|
||||
$output->writeln('现在可以运行定时任务测试:');
|
||||
$output->writeln(' php think express:auto-update');
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$output->error("同步失败: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user