272 lines
9.9 KiB
PHP
272 lines
9.9 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use Illuminate\Database\Query\Builder;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* 发货明细只读仓库。读取扁平表 jst_oms_shipping(对齐工作平台发货数据 47 列),
|
||
* 供“发货数据”页面展示与后续快递运费核算使用。
|
||
*/
|
||
class ShippingOrderRepository
|
||
{
|
||
private readonly ConnectionInterface $db;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->db = DB::connection((string) config('jushuitan.connection', 'jushuitan_local'));
|
||
}
|
||
|
||
/**
|
||
* @param array<string,string> $filters
|
||
* @return array{data:array,total:int}
|
||
*/
|
||
public function paginate(array $filters, int $page, int $perPage, string $mode = 'merged'): array
|
||
{
|
||
$page = max($page, 1);
|
||
$perPage = min(max($perPage, 1), 100);
|
||
|
||
if ($mode === 'detail') {
|
||
$q = $this->filteredQuery($filters);
|
||
$total = (clone $q)->count();
|
||
$rows = (clone $q)
|
||
->orderByDesc('ship_time')
|
||
->orderByDesc('source_id')
|
||
->orderBy('sku_source_id')
|
||
->forPage($page, $perPage)
|
||
->get()->map(fn ($r) => (array) $r)->all();
|
||
|
||
return ['data' => $rows, 'total' => $total];
|
||
}
|
||
|
||
// 合并:按包裹(source_id)归并
|
||
$base = $this->filteredQuery($filters);
|
||
$total = (clone $base)->distinct()->count('source_id');
|
||
$rows = (clone $base)
|
||
->groupBy('source_id')
|
||
->selectRaw($this->mergedSelectRaw())
|
||
->orderByDesc('ship_time')
|
||
->orderByDesc('source_id')
|
||
->forPage($page, $perPage)
|
||
->get()->map(fn ($r) => (array) $r)->all();
|
||
|
||
return ['data' => $rows, 'total' => $total];
|
||
}
|
||
|
||
/** 合并聚合 SELECT:条码/商品ID 逗号拼接、数量/重量/体积/金额/运费求和、长宽高取最大、其余取值。 */
|
||
private function mergedSelectRaw(): string
|
||
{
|
||
$sum = ['qty', 'freight', 'online_weight', 'actual_weight', 'volume_cm3', 'weight_kg',
|
||
'settle_weight_kg', 'settle_volume_cm3', 'recon_weight_kg', 'recon_volume_cm3', 'amount', 'settle_freight'];
|
||
$concat = ['item_id', 'barcode'];
|
||
$min = ['product_name', 'supplier_name', 'product_code'];
|
||
$all = ['express_no', 'trade_no', 'item_id', 'barcode', 'product_name', 'qty', 'pkg_count',
|
||
'receiver_state', 'buyer_nick', 'receiver_name', 'shop_name', 'express_name1', 'warehouse_name',
|
||
'ship_time', 'freight', 'online_weight', 'actual_weight', 'length_cm', 'width_cm', 'height_cm',
|
||
'volume_cm3', 'receiver_city', 'receiver_district', 'supplier_name', 'receiver_address',
|
||
'receiver_mobile', 'receiver_phone', 'express_name2', 'platform', 'buyer_memo', 'seller_memo',
|
||
'package_no', 'amount', 'product_code', 'download_time', 'pay_time', 'freight_rate', 'freight_formula',
|
||
'weight_kg', 'settle_weight_kg', 'settle_volume_cm3', 'recon_weight_kg', 'recon_volume_cm3',
|
||
'finished_barcode', 'finished_name', 'finished_supplier', 'settle_freight', 'front_no'];
|
||
|
||
$parts = ['MAX(id) as id', 'source_id'];
|
||
foreach ($all as $c) {
|
||
if (in_array($c, $sum, true)) {
|
||
$parts[] = "SUM(`{$c}`) as `{$c}`";
|
||
} elseif (in_array($c, $concat, true)) {
|
||
$parts[] = "GROUP_CONCAT(DISTINCT `{$c}` SEPARATOR ',') as `{$c}`";
|
||
} elseif (in_array($c, $min, true)) {
|
||
$parts[] = "MIN(`{$c}`) as `{$c}`";
|
||
} else {
|
||
$parts[] = "MAX(`{$c}`) as `{$c}`";
|
||
}
|
||
}
|
||
|
||
return implode(', ', $parts);
|
||
}
|
||
|
||
/**
|
||
* @param array<string,string> $filters
|
||
* @return \Generator<int,array<string,mixed>>
|
||
*/
|
||
public function exportRows(array $filters, int $batchSize = 1000, string $mode = 'merged'): \Generator
|
||
{
|
||
$batchSize = min(max($batchSize, 1), 5000);
|
||
$last = null;
|
||
|
||
if ($mode === 'detail') {
|
||
while (true) {
|
||
$query = $this->filteredQuery($filters);
|
||
if ($last !== null) {
|
||
$query->where('id', '>', $last);
|
||
}
|
||
$rows = $query->orderBy('id')->limit($batchSize)->get()->all();
|
||
if ($rows === []) {
|
||
return;
|
||
}
|
||
foreach ($rows as $row) {
|
||
yield (array) $row;
|
||
}
|
||
$last = (int) $rows[array_key_last($rows)]->id;
|
||
if (count($rows) < $batchSize) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 合并导出:按 source_id 键集分页 + 归并
|
||
while (true) {
|
||
$query = $this->filteredQuery($filters);
|
||
if ($last !== null) {
|
||
$query->where('source_id', '>', $last);
|
||
}
|
||
$rows = $query->groupBy('source_id')->selectRaw($this->mergedSelectRaw())
|
||
->orderBy('source_id')->limit($batchSize)->get()->all();
|
||
if ($rows === []) {
|
||
return;
|
||
}
|
||
foreach ($rows as $row) {
|
||
yield (array) $row;
|
||
}
|
||
$last = (int) $rows[array_key_last($rows)]->source_id;
|
||
if (count($rows) < $batchSize) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** @param array<string,string> $filters */
|
||
private function filteredQuery(array $filters): Builder
|
||
{
|
||
$query = $this->db->table('jst_oms_shipping');
|
||
|
||
if (($filters['express_no'] ?? '') !== '') {
|
||
$query->where('express_no', '=', trim((string) $filters['express_no']));
|
||
}
|
||
if (($filters['sourceno'] ?? '') !== '') {
|
||
$query->where('trade_no', '=', trim((string) $filters['sourceno']));
|
||
}
|
||
|
||
// 条形码:单值关键词
|
||
if (($filters['barcode'] ?? '') !== '') {
|
||
$query->whereRaw("barcode LIKE ? ESCAPE '!'", [$this->contains((string) $filters['barcode'])]);
|
||
}
|
||
|
||
// 供应商/店铺/仓库:多选,精确匹配所选的任意一个
|
||
foreach (['supplier_name', 'shop_name', 'warehouse_name'] as $column) {
|
||
$values = $this->multi($filters[$column] ?? null);
|
||
if ($values !== []) {
|
||
$query->whereIn($column, $values);
|
||
}
|
||
}
|
||
|
||
// 快递公司:多选,匹配快递名称1/2 任意一个
|
||
$couriers = $this->multi($filters['express_company'] ?? null);
|
||
if ($couriers !== []) {
|
||
$query->where(function (Builder $group) use ($couriers): void {
|
||
$group->whereIn('express_name1', $couriers)
|
||
->orWhereIn('express_name2', $couriers);
|
||
});
|
||
}
|
||
|
||
if (($filters['ship_from'] ?? '') !== '') {
|
||
$query->where('ship_time', '>=', $filters['ship_from'] . ' 00:00:00');
|
||
}
|
||
if (($filters['ship_to'] ?? '') !== '') {
|
||
$query->where('ship_time', '<', date('Y-m-d 00:00:00', strtotime($filters['ship_to'] . ' +1 day')));
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
private function contains(string $value): string
|
||
{
|
||
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
|
||
}
|
||
|
||
/**
|
||
* 归一多选筛选值为字符串数组(兼容单个字符串)。
|
||
* @return array<int,string>
|
||
*/
|
||
private function multi(mixed $value): array
|
||
{
|
||
if (is_string($value)) {
|
||
$value = $value === '' ? [] : [$value];
|
||
}
|
||
if (! is_array($value)) {
|
||
return [];
|
||
}
|
||
$out = [];
|
||
foreach ($value as $item) {
|
||
$item = trim((string) $item);
|
||
if ($item !== '' && ! in_array($item, $out, true)) {
|
||
$out[] = $item;
|
||
}
|
||
}
|
||
|
||
return array_slice($out, 0, 50);
|
||
}
|
||
|
||
/**
|
||
* 取某字段去重候选值(供筛选框自动补全)。
|
||
* @return array<int,string>
|
||
*/
|
||
public function distinctValues(string $field, string $keyword, int $limit = 20): array
|
||
{
|
||
$map = [
|
||
'supplier_name' => ['supplier_name'],
|
||
'shop_name' => ['shop_name'],
|
||
'warehouse_name' => ['warehouse_name'],
|
||
'express_company' => ['express_name1', 'express_name2'],
|
||
];
|
||
if (! isset($map[$field])) {
|
||
return [];
|
||
}
|
||
$limit = min(max($limit, 1), 500);
|
||
$like = $this->contains($keyword);
|
||
$values = [];
|
||
foreach ($map[$field] as $col) {
|
||
$rows = $this->db->table('jst_oms_shipping')
|
||
->selectRaw($col . ' as v')
|
||
->whereRaw($col . " LIKE ? ESCAPE '!'", [$like])
|
||
->whereNotNull($col)
|
||
->where($col, '<>', '')
|
||
->groupBy($col)
|
||
->orderBy($col)
|
||
->limit($limit)
|
||
->pluck('v');
|
||
foreach ($rows as $v) {
|
||
$v = trim((string) $v);
|
||
// 过滤空值与占位脏值(如自动发货仓 send_warehouse 为 "0")
|
||
if ($v !== '' && $v !== '0') {
|
||
$values[$v] = true;
|
||
}
|
||
}
|
||
}
|
||
$values = array_keys($values);
|
||
sort($values, SORT_FLAG_CASE | SORT_STRING);
|
||
|
||
return array_slice($values, 0, $limit);
|
||
}
|
||
|
||
/** 同步状态(供页面显示同步中/最近完成)。 */
|
||
public function syncStatus(): array
|
||
{
|
||
try {
|
||
$row = $this->db->table('jst_sync_state')->where('dataset_key', 'jst.sctrade')->first();
|
||
if (! $row) {
|
||
return ['code' => 'unavailable', 'last_succeeded_at' => null];
|
||
}
|
||
|
||
return [
|
||
'code' => (string) ($row->status ?? 'unavailable'),
|
||
'last_succeeded_at' => $row->last_succeeded_at ?? null,
|
||
];
|
||
} catch (\Throwable) {
|
||
return ['code' => 'unavailable', 'last_succeeded_at' => null];
|
||
}
|
||
}
|
||
}
|