新增「账单流水数据 › 聚水潭订单标签明细」(紧邻种菜明细表):四列(线上订单号/店铺名称/下单时间/标签),按标签拆行(一个标签一行)、订单不去重、标签不汇总,无标签订单记为「(无标签)」。新增 jst_order_labels 表(含公司/时间/标签/店铺索引)、JstOrderLabel 模型、JstOrderLabelImporter(从镜像 ops_jst_orders 按区间拆行导入,--replace 只清同区间)与 jst-order-labels:import 命令、导出路由与用例。页面筛选:下单时间区间(点整块开日历、可单独清除)、店铺、标签、线上订单号。
155 lines
5.4 KiB
PHP
155 lines
5.4 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');
|
||
|
||
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;
|
||
}
|
||
}
|