685 lines
25 KiB
PHP
685 lines
25 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Modules\EcommerceFinance\Services\GoodsProfitAfsLookupSchema;
|
||
|
||
/**
|
||
* 订单售后费用明细:聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」原表。
|
||
*
|
||
* 原表 rpt.jst_goods_profit_kpi_items_afs 的列名是 undefined1..undefinedN,
|
||
* 中文表头放在 rpt.jst_goods_profit_kpi_items_afs_schemas(column → name)。
|
||
* 两张表都镜像到共享业务库后,页面按 schemas 映射读取字段。
|
||
*/
|
||
class GoodsProfitAfsRepository
|
||
{
|
||
public const MAIN_DATASET = 'operations_replica.rpt.jst_goods_profit_kpi_items_afs';
|
||
|
||
public const SCHEMA_DATASET = 'operations_replica.rpt.jst_goods_profit_kpi_items_afs_schemas';
|
||
|
||
private const MAIN_TABLE = 'ops_jst_goods_profit_kpi_items_afs';
|
||
|
||
private const SCHEMA_TABLE = 'ops_jst_goods_profit_kpi_items_afs_schemas';
|
||
|
||
/** @var array<string,string>|null */
|
||
private ?array $fieldColumns = null;
|
||
|
||
private ?bool $hasDateLookup = null;
|
||
|
||
public function ready(): bool
|
||
{
|
||
try {
|
||
return Schema::connection($this->connectionName())->hasTable(self::MAIN_TABLE)
|
||
&& Schema::connection($this->connectionName())->hasTable(self::SCHEMA_TABLE)
|
||
&& $this->initialSyncCompleted()
|
||
&& $this->connection()->table(self::MAIN_TABLE)->exists();
|
||
} catch (\Throwable) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** 首次全量同步完成前不对外使用,避免页面读到不完整的数据。 */
|
||
private function initialSyncCompleted(): bool
|
||
{
|
||
$state = $this->connection()->table('business_mirror_table_states')
|
||
->where('dataset_key', self::MAIN_DATASET)
|
||
->first(['initial_completed_at']);
|
||
|
||
return $state !== null && $state->initial_completed_at !== null;
|
||
}
|
||
|
||
/**
|
||
* 页面字段键 => 上游列名(undefinedN)。
|
||
*
|
||
* @return array<string,string>
|
||
*/
|
||
public function fieldColumns(): array
|
||
{
|
||
if ($this->fieldColumns !== null) {
|
||
return $this->fieldColumns;
|
||
}
|
||
|
||
$headers = (array) config('order_after_sales_cost.headers', []);
|
||
$map = [];
|
||
$existing = array_map(
|
||
'strtolower',
|
||
Schema::connection($this->connectionName())->getColumnListing(self::MAIN_TABLE),
|
||
);
|
||
|
||
foreach ($this->connection()->table(self::SCHEMA_TABLE)->orderBy('id')->get(['column', 'name']) as $schema) {
|
||
$label = self::normalizeLabel((string) $schema->name);
|
||
$field = $headers[$label] ?? null;
|
||
$column = (string) $schema->column;
|
||
|
||
// 同一科目出现多次(如 800105佣金 / 800105平台佣金)时保留第一次映射。
|
||
// schemas 里有 170 条,但原表只有 160 列,提前过滤掉不存在的列。
|
||
if ($field !== null && ! isset($map[$field]) && in_array(strtolower($column), $existing, true)) {
|
||
$map[$field] = $column;
|
||
}
|
||
}
|
||
|
||
return $this->fieldColumns = $map;
|
||
}
|
||
|
||
/**
|
||
* 按「店铺 + 店铺商品编码 + 店铺款式编码」批量读取售后镜像中的内部商品编码。
|
||
*
|
||
* 仅作为商品成本历史拆分映射失败后的补充证据,不直接修改售后页面数据。
|
||
*
|
||
* @param list<array{shop_name:string,shop_sku_id:string,shop_i_id:string}> $candidates
|
||
* @return list<array{shop_name:string,shop_sku_id:string,shop_i_id:string,product_code:string}>
|
||
*/
|
||
public function platformSkuMappings(array $candidates): array
|
||
{
|
||
$keys = [];
|
||
foreach ($candidates as $candidate) {
|
||
$shopName = trim((string) ($candidate['shop_name'] ?? ''));
|
||
$shopSkuId = trim((string) ($candidate['shop_sku_id'] ?? ''));
|
||
$shopItemId = trim((string) ($candidate['shop_i_id'] ?? ''));
|
||
if ($shopName === '' || $shopSkuId === '' || $shopItemId === '') {
|
||
continue;
|
||
}
|
||
$keys[$shopName . "\0" . $shopSkuId . "\0" . $shopItemId] = [
|
||
'shop_name' => $shopName,
|
||
'shop_sku_id' => $shopSkuId,
|
||
'shop_i_id' => $shopItemId,
|
||
];
|
||
}
|
||
if ($keys === []) {
|
||
return [];
|
||
}
|
||
|
||
$columns = $this->fieldColumns();
|
||
foreach (['shop_name', 'shop_product_code', 'shop_style_code', 'product_code'] as $field) {
|
||
if (! isset($columns[$field])
|
||
|| preg_match('/^[A-Za-z0-9_]+$/', (string) $columns[$field]) !== 1) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
$connection = $this->connection();
|
||
$table = '`' . self::MAIN_TABLE . '`';
|
||
$shopName = '`' . $columns['shop_name'] . '`';
|
||
$shopProductCode = '`' . $columns['shop_product_code'] . '`';
|
||
$shopStyleCode = '`' . $columns['shop_style_code'] . '`';
|
||
$productCode = '`' . $columns['product_code'] . '`';
|
||
|
||
$connection->statement('DROP TEMPORARY TABLE IF EXISTS tmp_cost_afs_sku_keys');
|
||
$connection->statement(
|
||
'CREATE TEMPORARY TABLE tmp_cost_afs_sku_keys ('
|
||
. 'shop_name varchar(191) NOT NULL, '
|
||
. 'shop_sku_id varchar(128) NOT NULL, '
|
||
. 'shop_i_id varchar(128) NOT NULL, '
|
||
. 'PRIMARY KEY (shop_name, shop_sku_id, shop_i_id)'
|
||
. ') ENGINE=InnoDB',
|
||
);
|
||
|
||
$rows = [];
|
||
try {
|
||
foreach (array_chunk(array_values($keys), 500) as $chunk) {
|
||
$connection->table('tmp_cost_afs_sku_keys')->insertOrIgnore($chunk);
|
||
}
|
||
|
||
$records = $connection->select(
|
||
"SELECT DISTINCT
|
||
m.{$shopName} AS shop_name,
|
||
m.{$shopProductCode} AS shop_sku_id,
|
||
m.{$shopStyleCode} AS shop_i_id,
|
||
m.{$productCode} AS product_code
|
||
FROM {$table} m
|
||
STRAIGHT_JOIN tmp_cost_afs_sku_keys k
|
||
ON k.shop_name = m.{$shopName}
|
||
AND k.shop_sku_id = m.{$shopProductCode}
|
||
AND k.shop_i_id = m.{$shopStyleCode}
|
||
WHERE COALESCE(TRIM(m.{$productCode}), '') <> ''",
|
||
);
|
||
foreach ($records as $record) {
|
||
$rows[] = [
|
||
'shop_name' => trim((string) $record->shop_name),
|
||
'shop_sku_id' => trim((string) $record->shop_sku_id),
|
||
'shop_i_id' => trim((string) $record->shop_i_id),
|
||
'product_code' => mb_strtoupper(trim((string) $record->product_code)),
|
||
];
|
||
}
|
||
} finally {
|
||
$connection->statement('DROP TEMPORARY TABLE IF EXISTS tmp_cost_afs_sku_keys');
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/**
|
||
* 商品成本补码:读取指定订单中数量、金额均为正的商品利润组件。
|
||
*
|
||
* @param list<string> $onlineOrderNos
|
||
* @return list<array{
|
||
* online_order_no:string,
|
||
* shop_name:string,
|
||
* shop_sku_id:string,
|
||
* shop_i_id:string,
|
||
* product_code:string,
|
||
* product_quantity:float,
|
||
* product_amount:float
|
||
* }>
|
||
*/
|
||
public function positiveComponentRowsForOrders(array $onlineOrderNos): array
|
||
{
|
||
$onlineOrderNos = array_values(array_unique(array_filter(array_map(
|
||
static fn (mixed $value): string => trim((string) $value),
|
||
$onlineOrderNos,
|
||
), static fn (string $value): bool => $value !== '')));
|
||
sort($onlineOrderNos, SORT_STRING);
|
||
|
||
if ($onlineOrderNos === [] || ! $this->ready()) {
|
||
return [];
|
||
}
|
||
|
||
$columns = $this->fieldColumns();
|
||
foreach ([
|
||
'online_order_no', 'shop_name', 'shop_product_code', 'shop_style_code',
|
||
'product_code', 'product_quantity', 'product_amount',
|
||
] as $field) {
|
||
if (! isset($columns[$field])) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
$select = array_values(array_unique(array_values(array_intersect_key(
|
||
$columns,
|
||
array_flip([
|
||
'online_order_no', 'shop_name', 'shop_product_code', 'shop_style_code',
|
||
'product_code', 'product_quantity', 'product_amount',
|
||
]),
|
||
))));
|
||
$rows = [];
|
||
foreach (array_chunk($onlineOrderNos, 500) as $chunk) {
|
||
foreach ($this->connection()->table(self::MAIN_TABLE)
|
||
->whereIn($columns['online_order_no'], $chunk)
|
||
->orderBy('id')
|
||
->get($select) as $record) {
|
||
$productQuantity = (float) ($record->{$columns['product_quantity']} ?? 0);
|
||
$productAmount = (float) ($record->{$columns['product_amount']} ?? 0);
|
||
$productCode = mb_strtoupper(trim((string) ($record->{$columns['product_code']} ?? '')));
|
||
if ($productQuantity <= 0 || $productAmount <= 0 || $productCode === '') {
|
||
continue;
|
||
}
|
||
$rows[] = [
|
||
'online_order_no' => trim((string) ($record->{$columns['online_order_no']} ?? '')),
|
||
'shop_name' => trim((string) ($record->{$columns['shop_name']} ?? '')),
|
||
'shop_sku_id' => trim((string) ($record->{$columns['shop_product_code']} ?? '')),
|
||
'shop_i_id' => trim((string) ($record->{$columns['shop_style_code']} ?? '')),
|
||
'product_code' => $productCode,
|
||
'product_quantity' => $productQuantity,
|
||
'product_amount' => $productAmount,
|
||
];
|
||
}
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/** @param array<string,mixed> $filters */
|
||
public function covers(array $filters): bool
|
||
{
|
||
$from = trim((string) ($filters['from'] ?? ''));
|
||
$start = (string) config('order_after_sales_cost.goods_profit_from', '2026-07-01');
|
||
|
||
// 更早月份由导入表负责,先按日期边界退出,避免每次查询都检查宽表同步状态。
|
||
if ($start !== '' && $from !== '' && $from < $start) {
|
||
return false;
|
||
}
|
||
|
||
if (! $this->ready()) {
|
||
return false;
|
||
}
|
||
|
||
$columns = $this->fieldColumns();
|
||
$to = trim((string) ($filters['to'] ?? ''));
|
||
$orderNo = trim((string) ($filters['order_no'] ?? ''));
|
||
$orderNos = $this->orderNumbers($orderNo);
|
||
|
||
// 只按单号(不限日期)查询:原表命中该单就用原表,否则交给导入历史数据兜底。
|
||
if ($from === '' && $to === '' && $orderNos !== []) {
|
||
$dateColumn = $columns['order_time'] ?? null;
|
||
|
||
foreach (['online_order_no', 'internal_order_no'] as $field) {
|
||
if (! isset($columns[$field])) {
|
||
continue;
|
||
}
|
||
|
||
$values = $field === 'internal_order_no'
|
||
? array_values(array_filter($orderNos, static fn (string $value): bool => mb_strlen($value) < 12))
|
||
: $orderNos;
|
||
if ($values === []) {
|
||
continue;
|
||
}
|
||
|
||
$query = $this->connection()->table(self::MAIN_TABLE)
|
||
->whereIn($columns[$field], $values);
|
||
|
||
// 只有带订单时间的行才是完整明细;只有费用行的历史订单交给导入数据。
|
||
if ($dateColumn !== null) {
|
||
$query->whereNotNull($dateColumn)->where($dateColumn, '<>', '');
|
||
}
|
||
|
||
if ($query->limit(1)->exists()) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// “是否覆盖”只看日期范围,不掺业务筛选:筛选条件由 paginate 应用,
|
||
// 筛不到就该显示空结果,而不是回退到旧订单镜像(否则筛选会被忽略)。
|
||
if ($from === '' && $to === '') {
|
||
return true;
|
||
}
|
||
|
||
if (! isset($columns['order_time'])) {
|
||
return false;
|
||
}
|
||
|
||
$expression = $this->dateExpression($columns['order_time']);
|
||
$range = $this->connection()->table(self::MAIN_TABLE)
|
||
->selectRaw("MAX({$expression}) as max_time")
|
||
->first();
|
||
|
||
if (! $range || $range->max_time === null) {
|
||
return false;
|
||
}
|
||
|
||
// 起始日受 goods_profit_from 约束,结束日按原表实际最大日期判断(今天往往晚于最后一次同步)。
|
||
$anchor = $from !== '' ? $from : $to;
|
||
|
||
return $anchor === '' || substr((string) $range->max_time, 0, 10) >= $anchor;
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $filters
|
||
* @return array{rows:list<array<string,mixed>>,has_more:bool}
|
||
*/
|
||
public function paginate(array $filters, int $page, int $limit): array
|
||
{
|
||
$cacheKey = 'goods-profit-afs:page:' . sha1((string) json_encode([$filters, $page, $limit]));
|
||
|
||
return cache()->remember($cacheKey, now()->addMinutes(2), fn (): array => $this->paginateUncached($filters, $page, $limit));
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $filters
|
||
* @return array{rows:list<array<string,mixed>>,has_more:bool}
|
||
*/
|
||
private function paginateUncached(array $filters, int $page, int $limit): array
|
||
{
|
||
$page = max($page, 1);
|
||
$limit = min(max($limit, 1), 100);
|
||
$columns = $this->fieldColumns();
|
||
$select = array_values(array_unique(array_merge(['id'], array_values($columns))));
|
||
$orderColumn = $columns['order_time'] ?? null;
|
||
$orderNo = trim((string) ($filters['order_no'] ?? ''));
|
||
$orderNos = $this->orderNumbers($orderNo);
|
||
|
||
if (count($orderNos) > 1) {
|
||
return $this->paginateByOrderNumbers($orderNos, $filters, $columns, $select, $page, $limit);
|
||
}
|
||
|
||
if ($orderNo !== '') {
|
||
return $this->paginateByOrderNumber($orderNo, $filters, $columns, $select, $page, $limit);
|
||
}
|
||
|
||
$query = $this->filtered($filters, $columns)->select($select);
|
||
|
||
if ($orderColumn !== null) {
|
||
$query->orderByRaw($this->dateExpression($orderColumn) . ' desc');
|
||
}
|
||
|
||
$records = $query->orderByDesc('id')->offset(($page - 1) * $limit)->limit($limit + 1)->get();
|
||
$numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', []));
|
||
$rows = [];
|
||
|
||
foreach ($records as $record) {
|
||
$row = [];
|
||
|
||
foreach ($columns as $field => $column) {
|
||
$value = $record->{$column} ?? null;
|
||
|
||
if (isset($numberKeys[$field])) {
|
||
$value = $value === null ? null : (float) $value;
|
||
}
|
||
|
||
$row[$field] = is_string($value) ? trim($value) : $value;
|
||
}
|
||
|
||
$rows[] = $row;
|
||
}
|
||
|
||
return [
|
||
'rows' => array_slice($rows, 0, $limit),
|
||
'has_more' => count($rows) > $limit,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 单号查询:分别走 online/internal 两个索引,合并后在 PHP 排序分页。
|
||
*
|
||
* @param array<string,mixed> $filters
|
||
* @param array<string,string> $columns
|
||
* @param list<string> $select
|
||
* @return array{rows:list<array<string,mixed>>,has_more:bool}
|
||
*/
|
||
private function paginateByOrderNumber(
|
||
string $orderNo,
|
||
array $filters,
|
||
array $columns,
|
||
array $select,
|
||
int $page,
|
||
int $limit,
|
||
): array {
|
||
$records = [];
|
||
$cap = max($limit * 4, 400);
|
||
|
||
foreach ($this->orderNumberFields($orderNo) as $field) {
|
||
if (! isset($columns[$field])) {
|
||
continue;
|
||
}
|
||
|
||
$query = $this->connection()->table(self::MAIN_TABLE)
|
||
->where($columns[$field], 'like', $orderNo . '%');
|
||
$this->applyDateRange($query, $filters, $columns);
|
||
|
||
// 先只取主键(走索引、窄查询),再按主键取宽行,避免宽表全列扫描。
|
||
$ids = $query->limit($cap)->pluck('id')->all();
|
||
|
||
if ($ids !== []) {
|
||
foreach ($this->connection()->table(self::MAIN_TABLE)->whereIn('id', $ids)->get($select) as $record) {
|
||
$records[(int) $record->id] = $record;
|
||
}
|
||
}
|
||
}
|
||
|
||
$timeColumn = $columns['order_time'] ?? null;
|
||
usort($records, static function ($left, $right) use ($timeColumn): int {
|
||
$leftTime = $timeColumn !== null ? strtotime(str_replace('/', '-', (string) ($left->{$timeColumn} ?? ''))) : false;
|
||
$rightTime = $timeColumn !== null ? strtotime(str_replace('/', '-', (string) ($right->{$timeColumn} ?? ''))) : false;
|
||
|
||
return [$rightTime ?: 0, (int) $right->id] <=> [$leftTime ?: 0, (int) $left->id];
|
||
});
|
||
|
||
$numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', []));
|
||
$rows = [];
|
||
|
||
foreach ($records as $record) {
|
||
$row = [];
|
||
|
||
foreach ($columns as $field => $column) {
|
||
$value = $record->{$column} ?? null;
|
||
|
||
if (isset($numberKeys[$field])) {
|
||
$value = $value === null ? null : (float) $value;
|
||
}
|
||
|
||
$row[$field] = is_string($value) ? trim($value) : $value;
|
||
}
|
||
|
||
$rows[] = $row;
|
||
}
|
||
|
||
$offset = ($page - 1) * $limit;
|
||
|
||
return [
|
||
'rows' => array_slice($rows, $offset, $limit),
|
||
'has_more' => count($rows) > $offset + $limit,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 批量单号(一行一个/逗号/空格分隔):按两列索引 IN 查询后 SQL 分页。
|
||
*
|
||
* @param list<string> $orderNos
|
||
* @param array<string,mixed> $filters
|
||
* @param array<string,string> $columns
|
||
* @param list<string> $select
|
||
* @return array{rows:list<array<string,mixed>>,has_more:bool}
|
||
*/
|
||
private function paginateByOrderNumbers(
|
||
array $orderNos,
|
||
array $filters,
|
||
array $columns,
|
||
array $select,
|
||
int $page,
|
||
int $limit,
|
||
): array {
|
||
$short = array_values(array_filter($orderNos, static fn (string $value): bool => mb_strlen($value) < 12));
|
||
|
||
$query = $this->connection()->table(self::MAIN_TABLE);
|
||
$query->where(function ($inner) use ($columns, $orderNos, $short): void {
|
||
$matched = false;
|
||
|
||
if (isset($columns['online_order_no'])) {
|
||
$inner->whereIn($columns['online_order_no'], $orderNos);
|
||
$matched = true;
|
||
}
|
||
|
||
if ($short !== [] && isset($columns['internal_order_no'])) {
|
||
$matched ? $inner->orWhereIn($columns['internal_order_no'], $short)
|
||
: $inner->whereIn($columns['internal_order_no'], $short);
|
||
$matched = true;
|
||
}
|
||
|
||
if (! $matched) {
|
||
$inner->whereRaw('1 = 0');
|
||
}
|
||
});
|
||
|
||
$this->applyDateRange($query, $filters, $columns);
|
||
// 不额外排序(走单号索引直接取这 N 条);按 id 倒序会把主键反向全表扫描。
|
||
$ids = $query->forPage($page, $limit + 1)->pluck('id')->all();
|
||
$fetched = $ids === []
|
||
? collect()
|
||
: $this->connection()->table(self::MAIN_TABLE)->whereIn('id', $ids)->get($select)->keyBy('id');
|
||
$records = array_values(array_filter(array_map(
|
||
static fn (int $id) => $fetched[$id] ?? null,
|
||
$ids,
|
||
)));
|
||
$numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', []));
|
||
$rows = [];
|
||
|
||
foreach ($records as $record) {
|
||
$row = [];
|
||
|
||
foreach ($columns as $field => $column) {
|
||
$value = $record->{$column} ?? null;
|
||
|
||
if (isset($numberKeys[$field])) {
|
||
$value = $value === null ? null : (float) $value;
|
||
}
|
||
|
||
$row[$field] = is_string($value) ? trim($value) : $value;
|
||
}
|
||
|
||
$rows[] = $row;
|
||
}
|
||
|
||
$hasMore = count($rows) > $limit;
|
||
$rows = array_slice($rows, 0, $limit);
|
||
|
||
return ['rows' => $rows, 'has_more' => $hasMore];
|
||
}
|
||
|
||
/**
|
||
* 批量单号拆成数组(与订单镜像的批量规则一致)。
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
private function orderNumbers(string $raw): array
|
||
{
|
||
$raw = trim($raw);
|
||
|
||
if ($raw === '') {
|
||
return [];
|
||
}
|
||
|
||
$parts = preg_split('/[\s,;,;、]+/u', $raw) ?: [];
|
||
$values = [];
|
||
|
||
foreach ($parts as $part) {
|
||
$part = trim((string) $part);
|
||
|
||
if ($part !== '' && ! isset($values[$part])) {
|
||
$values[$part] = true;
|
||
}
|
||
}
|
||
|
||
return array_slice(array_keys($values), 0, 200);
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $filters
|
||
* @param array<string,string> $columns
|
||
*/
|
||
private function filtered(array $filters, array $columns): \Illuminate\Database\Query\Builder
|
||
{
|
||
$query = $this->connection()->table(self::MAIN_TABLE);
|
||
$from = trim((string) ($filters['from'] ?? ''));
|
||
$to = trim((string) ($filters['to'] ?? ''));
|
||
$orderNo = trim((string) ($filters['order_no'] ?? ''));
|
||
$sku = trim((string) ($filters['sku_id'] ?? ''));
|
||
$shops = array_values(array_filter((array) ($filters['shop_names'] ?? []), static fn ($v) => trim((string) $v) !== ''));
|
||
|
||
// 原表的订单时间是字符串(如 2026/8/23 14:58:51,不补零),不能直接和 ISO 日期做字符串比较。
|
||
if (($from !== '' || $to !== '') && isset($columns['order_time'])) {
|
||
$expression = $this->dateExpression($columns['order_time']);
|
||
|
||
if ($from !== '') {
|
||
$query->whereRaw($expression . ' >= ?', [$from . ' 00:00:00']);
|
||
}
|
||
|
||
if ($to !== '') {
|
||
$query->whereRaw($expression . ' <= ?', [$to . ' 23:59:59']);
|
||
}
|
||
}
|
||
|
||
if ($shops !== [] && isset($columns['shop_name'])) {
|
||
$query->whereIn($columns['shop_name'], $shops);
|
||
}
|
||
|
||
if ($orderNo !== '') {
|
||
$query->where(function ($inner) use ($columns, $orderNo): void {
|
||
$matched = false;
|
||
|
||
foreach (['online_order_no', 'internal_order_no'] as $field) {
|
||
if (! isset($columns[$field])) {
|
||
continue;
|
||
}
|
||
|
||
$inner->orWhere($columns[$field], 'like', $orderNo . '%');
|
||
$matched = true;
|
||
}
|
||
|
||
if (! $matched) {
|
||
$inner->whereRaw('1 = 0');
|
||
}
|
||
});
|
||
}
|
||
|
||
if ($sku !== '' && isset($columns['product_code'])) {
|
||
$query->where($columns['product_code'], 'like', '%' . addcslashes($sku, '%_\\') . '%');
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $filters
|
||
* @param array<string,string> $columns
|
||
*/
|
||
private function applyDateRange(\Illuminate\Database\Query\Builder $query, array $filters, array $columns): void
|
||
{
|
||
$from = trim((string) ($filters['from'] ?? ''));
|
||
$to = trim((string) ($filters['to'] ?? ''));
|
||
|
||
if (($from === '' && $to === '') || ! isset($columns['order_time'])) {
|
||
return;
|
||
}
|
||
|
||
$expression = $this->dateExpression($columns['order_time']);
|
||
|
||
if ($from !== '') {
|
||
$query->whereRaw($expression . ' >= ?', [$from . ' 00:00:00']);
|
||
}
|
||
|
||
if ($to !== '') {
|
||
$query->whereRaw($expression . ' <= ?', [$to . ' 23:59:59']);
|
||
}
|
||
}
|
||
|
||
private function dateExpression(string $column): string
|
||
{
|
||
// Dynamic schemas can move the source field. Keep the original parser
|
||
// until the matching additive lookup has been installed.
|
||
if ($column === 'undefined7') {
|
||
$this->hasDateLookup ??= Schema::connection($this->connectionName())
|
||
->hasColumn(self::MAIN_TABLE, GoodsProfitAfsLookupSchema::DATE_COLUMN);
|
||
if ($this->hasDateLookup) {
|
||
return '`' . GoodsProfitAfsLookupSchema::DATE_COLUMN . '`';
|
||
}
|
||
}
|
||
|
||
return "STR_TO_DATE(REPLACE(`{$column}`, '/', '-'), '%Y-%m-%d %H:%i:%s')";
|
||
}
|
||
|
||
private function connection(): \Illuminate\Database\ConnectionInterface
|
||
{
|
||
return DB::connection($this->connectionName());
|
||
}
|
||
|
||
private function connectionName(): string
|
||
{
|
||
return (string) config('upstream_mirrors.connection', 'business_mirror');
|
||
}
|
||
|
||
private static function normalizeLabel(string $label): string
|
||
{
|
||
$label = str_replace(["\u{3000}", "\u{00A0}"], ' ', $label);
|
||
$label = preg_replace('/\s+/u', ' ', $label) ?? $label;
|
||
|
||
return trim($label);
|
||
}
|
||
|
||
/**
|
||
* 线上订单号和内部订单号长度差很多(19 位 vs 7~8 位)。长单号只查线上订单号,
|
||
* 避免用内部订单号列做 `like '长单号%'` 的全表扫描。
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
private function orderNumberFields(string $orderNo): array
|
||
{
|
||
return mb_strlen($orderNo) >= 12
|
||
? ['online_order_no']
|
||
: ['online_order_no', 'internal_order_no'];
|
||
}
|
||
}
|