233 万行上做 distinct 扫描要几秒;新增 JstOrderLabelOptions(按公司缓存 24 小时),页面读取缓存,导入命令结束时失效并预热,避免每次打开页面都等待。
159 lines
5.6 KiB
PHP
159 lines
5.6 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use Illuminate\Database\DatabaseManager;
|
||
use InvalidArgumentException;
|
||
use RuntimeException;
|
||
|
||
/**
|
||
* 聚水潭订单标签明细导入(从共享镜像 ops_jst_orders 拆标签写入 jst_order_labels)。
|
||
*
|
||
* 一个标签一行:一单有 N 个标签就写 N 行;订单不去重、标签不汇总。
|
||
* 无标签的订单保留一行,标签记为「(无标签)」,方便与订单总数核对。
|
||
*/
|
||
class JstOrderLabelImporter
|
||
{
|
||
public const NO_LABEL = '(无标签)';
|
||
|
||
private const READ_CHUNK = 2000;
|
||
|
||
private const INSERT_CHUNK = 2000;
|
||
|
||
public function __construct(private readonly DatabaseManager $databases) {}
|
||
|
||
/**
|
||
* @return array{from:string,to:string,orders:int,rows:int,no_label_orders:int,labels:int,seconds:float}
|
||
*/
|
||
public function import(string $from, string $to, int $companyId = 1, bool $replace = false, string $source = 'jst-order-label-import'): array
|
||
{
|
||
$from = $this->normalizeDate($from, '起始日期');
|
||
$to = $this->normalizeDate($to, '结束日期');
|
||
if ($from > $to) {
|
||
throw new InvalidArgumentException('起始日期不能晚于结束日期。');
|
||
}
|
||
|
||
$start = $from . ' 00:00:00';
|
||
$end = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00';
|
||
|
||
$mirror = $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror'));
|
||
if (! $mirror->getSchemaBuilder()->hasTable('ops_jst_orders')) {
|
||
throw new RuntimeException('聚水潭订单镜像尚未建立(ops_jst_orders)。');
|
||
}
|
||
|
||
$target = $this->databases->connection();
|
||
$startedAt = microtime(true);
|
||
|
||
if ($replace) {
|
||
$target->table('jst_order_labels')
|
||
->where('company_id', $companyId)
|
||
->where('ordered_at', '>=', $start)
|
||
->where('ordered_at', '<', $end)
|
||
->delete();
|
||
}
|
||
|
||
$now = now();
|
||
$orders = 0;
|
||
$rows = 0;
|
||
$noLabelOrders = 0;
|
||
$buffer = [];
|
||
|
||
$flush = function () use (&$buffer, &$rows, $target): void {
|
||
foreach (array_chunk($buffer, self::INSERT_CHUNK) as $chunk) {
|
||
$target->table('jst_order_labels')->insert($chunk);
|
||
$rows += count($chunk);
|
||
}
|
||
$buffer = [];
|
||
};
|
||
|
||
$mirror->table('ops_jst_orders')
|
||
->where('order_date', '>=', $start)
|
||
->where('order_date', '<', $end)
|
||
->orderBy('Id')
|
||
->chunkById(self::READ_CHUNK, function ($records) use (&$buffer, &$orders, &$noLabelOrders, $flush, $companyId, $source, $now): void {
|
||
foreach ($records as $record) {
|
||
$orders++;
|
||
$orderedAt = $record->order_date === null ? null : substr((string) $record->order_date, 0, 19);
|
||
if ($orderedAt === null || $orderedAt === '') {
|
||
continue;
|
||
}
|
||
|
||
$labels = $this->splitLabels((string) ($record->labels ?? ''));
|
||
if ($labels === []) {
|
||
$labels = [self::NO_LABEL];
|
||
$noLabelOrders++;
|
||
}
|
||
|
||
foreach ($labels as $label) {
|
||
$buffer[] = [
|
||
'company_id' => $companyId,
|
||
'order_no' => mb_substr((string) $record->so_id, 0, 100),
|
||
'shop_name' => mb_substr((string) ($record->shop_name ?? ''), 0, 191),
|
||
'ordered_at' => $orderedAt,
|
||
'label' => $label,
|
||
'created_from' => $source,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
];
|
||
}
|
||
|
||
if (count($buffer) >= self::INSERT_CHUNK * 2) {
|
||
$flush();
|
||
}
|
||
}
|
||
}, 'Id');
|
||
|
||
$flush();
|
||
|
||
$labels = $target->table('jst_order_labels')
|
||
->where('company_id', $companyId)
|
||
->where('ordered_at', '>=', $start)
|
||
->where('ordered_at', '<', $end)
|
||
->distinct()
|
||
->count('label');
|
||
|
||
// 导入会改变筛选选项,重新预热缓存,避免页面下次打开时做几秒的 distinct 扫描。
|
||
app(JstOrderLabelOptions::class)->forget($companyId);
|
||
app(JstOrderLabelOptions::class)->warm($companyId);
|
||
|
||
return [
|
||
'from' => $from,
|
||
'to' => $to,
|
||
'orders' => $orders,
|
||
'rows' => $rows,
|
||
'no_label_orders' => $noLabelOrders,
|
||
'labels' => (int) $labels,
|
||
'seconds' => round(microtime(true) - $startedAt, 1),
|
||
];
|
||
}
|
||
|
||
/** @return list<string> */
|
||
private function splitLabels(string $labels): array
|
||
{
|
||
$out = [];
|
||
foreach (explode(',', $labels) as $label) {
|
||
$label = trim($label);
|
||
if ($label !== '') {
|
||
$out[] = mb_substr($label, 0, 191);
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($out));
|
||
}
|
||
|
||
private function normalizeDate(string $value, string $label): string
|
||
{
|
||
$value = trim($value);
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
|
||
throw new InvalidArgumentException($label . '必须是 YYYY-MM-DD。');
|
||
}
|
||
|
||
[$year, $month, $day] = array_map('intval', explode('-', $value));
|
||
if (! checkdate($month, $day, $year)) {
|
||
throw new InvalidArgumentException($label . '不是有效日期。');
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
}
|