batchValues() 原来拿值本身当数组键去重,纯数字订单号(如 3317080368285022883)会被 PHP 转成 int,后续 whereIn 就成了数字和长文本比较,走不上 so_id / outer_oi_id 的 前缀索引,实测退化成全表扫描(70 秒以上)。 改成键加 v 前缀、返回原字符串,ctype_digit 显式传字符串。新增用例覆盖前导零单号 0001234 与 1234,防止再回归。
584 lines
25 KiB
PHP
584 lines
25 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
|
||
class OrderDataRepository
|
||
{
|
||
private const SOURCE = 'operations_replica';
|
||
|
||
private ?UpstreamReadOnlyQuery $query = null;
|
||
|
||
public function __construct(private readonly UpstreamReadOnlyQueryFactory $queries)
|
||
{
|
||
}
|
||
|
||
/** @return array{rows:array<int,array<string,mixed>>,date:?string} */
|
||
public function afterSales(array $filters, int $page, int $limit): array
|
||
{
|
||
$where = [];
|
||
$bindings = [];
|
||
|
||
if (($filters['from'] ?? '') !== '') {
|
||
$where[] = 'o.[order_date] >= CAST(? AS date)';
|
||
$bindings[] = $filters['from'];
|
||
}
|
||
if (($filters['to'] ?? '') !== '') {
|
||
$where[] = 'o.[order_date] < DATEADD(day, 1, CAST(? AS date))';
|
||
$bindings[] = $filters['to'];
|
||
}
|
||
if (($filters['order_no'] ?? '') !== '') {
|
||
// 支持批量订单号:每个单号一组匹配,多组取并集。
|
||
$groups = [];
|
||
foreach ($this->batchValues($filters['order_no']) as $value) {
|
||
$needle = $this->contains($value);
|
||
$groups[] = "(CONVERT(varchar(30), o.[o_id]) LIKE ? ESCAPE '\\'"
|
||
. " OR o.[so_id] LIKE ? ESCAPE '\\'"
|
||
. " OR i.[outer_oi_id] LIKE ? ESCAPE '\\')";
|
||
array_push($bindings, $needle, $needle, $needle);
|
||
}
|
||
|
||
if ($groups !== []) {
|
||
$where[] = '(' . implode(' OR ', $groups) . ')';
|
||
}
|
||
}
|
||
if (($filters['shop_name'] ?? '') !== '') {
|
||
$where[] = "o.[shop_name] LIKE ? ESCAPE '\\'";
|
||
$bindings[] = $this->contains($filters['shop_name']);
|
||
}
|
||
if (($filters['sku_id'] ?? '') !== '') {
|
||
$groups = [];
|
||
foreach ($this->batchValues($filters['sku_id']) as $value) {
|
||
$needle = $this->contains($value);
|
||
$groups[] = "(i.[sku_id] LIKE ? ESCAPE '\\'"
|
||
. " OR i.[shop_sku_id] LIKE ? ESCAPE '\\'"
|
||
. " OR i.[shop_i_id] LIKE ? ESCAPE '\\')";
|
||
array_push($bindings, $needle, $needle, $needle);
|
||
}
|
||
|
||
if ($groups !== []) {
|
||
$where[] = '(' . implode(' OR ', $groups) . ')';
|
||
}
|
||
}
|
||
|
||
$whereSql = $where === [] ? '1 = 1' : implode(' AND ', $where);
|
||
[$offset, $fetch] = $this->pageBounds($page, $limit);
|
||
|
||
$sql = "WITH paged_rows AS (
|
||
SELECT
|
||
o.[o_id] AS internal_order_no,
|
||
o.[so_id] AS online_order_no,
|
||
i.[outer_oi_id] AS sub_order_no,
|
||
o.[pay_date],
|
||
o.[end_time],
|
||
o.[send_date],
|
||
o.[order_date],
|
||
o.[shop_name],
|
||
o.[shop_id],
|
||
o.[shop_site],
|
||
o.[type] AS order_type,
|
||
o.[labels] AS order_tags,
|
||
o.[wms_co_id],
|
||
o.[l_id] AS express_no,
|
||
o.[logistics_company] AS express_company,
|
||
o.[receiver_state] AS province,
|
||
o.[receiver_city] AS city,
|
||
o.[creator_name] AS salesperson,
|
||
o.[free_amount] AS merchant_discount,
|
||
o.[freight] AS freight_income,
|
||
i.[oi_id],
|
||
i.[sku_id] AS product_code,
|
||
i.[i_id] AS style_code,
|
||
i.[shop_sku_id] AS shop_product_code,
|
||
i.[shop_i_id] AS shop_style_code,
|
||
i.[properties_value] AS color_spec,
|
||
i.[name] AS online_product_name,
|
||
i.[qty] AS product_quantity,
|
||
i.[amount] AS product_amount,
|
||
i.[buyer_paid_amount] AS payment_amount,
|
||
i.[seller_income_amount],
|
||
i.[is_gift],
|
||
i.[src_combine_sku_id],
|
||
i.[sku_type],
|
||
i.[send_warehouse]
|
||
FROM [rpt].[jst_Orders] o
|
||
INNER JOIN [rpt].[jst_OrderItem] i ON i.[o_id] = o.[o_id]
|
||
WHERE {$whereSql}
|
||
ORDER BY o.[order_date] DESC, o.[o_id] DESC, i.[oi_id] DESC
|
||
OFFSET {$offset} ROWS FETCH NEXT {$fetch} ROWS ONLY
|
||
)
|
||
SELECT
|
||
p.[internal_order_no],
|
||
p.[online_order_no],
|
||
p.[sub_order_no],
|
||
CAST(NULL AS nvarchar(100)) AS after_sales_no,
|
||
CONVERT(varchar(19), p.[pay_date], 120) AS pay_time,
|
||
CONVERT(varchar(19), p.[end_time], 120) AS order_complete_time,
|
||
CONVERT(varchar(19), p.[send_date], 120) AS send_time,
|
||
CONVERT(varchar(19), p.[order_date], 120) AS order_time,
|
||
p.[shop_name],
|
||
p.[shop_id] AS shop_code,
|
||
p.[product_code],
|
||
p.[style_code],
|
||
p.[shop_product_code],
|
||
p.[shop_style_code],
|
||
p.[shop_site],
|
||
s.[brand],
|
||
s.[category],
|
||
s.[vc_name] AS virtual_category,
|
||
s.[cost_price] AS unit_cost,
|
||
p.[color_spec],
|
||
COALESCE(NULLIF(s.[name], ''), p.[color_spec]) AS product_name,
|
||
CAST(p.[product_quantity] AS decimal(18,2)) AS product_quantity,
|
||
CAST(ISNULL(p.[product_amount], 0) AS decimal(18,2)) AS product_amount,
|
||
CAST(ISNULL(s.[cost_price], 0) * ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS product_cost,
|
||
CAST(CASE WHEN ISNULL(p.[product_amount], 0) < 0
|
||
THEN ISNULL(s.[cost_price], 0) * ABS(ISNULL(p.[product_quantity], 0)) ELSE 0 END AS decimal(18,2))
|
||
AS reversal_order_cost,
|
||
CAST(ISNULL(p.[merchant_discount], 0) AS decimal(18,2)) AS merchant_discount,
|
||
CAST(ISNULL(p.[freight_income], 0) AS decimal(18,2)) AS freight_income,
|
||
CAST(COALESCE(p.[payment_amount], p.[product_amount], 0) AS decimal(18,2)) AS payment_amount,
|
||
CAST(CASE WHEN ISNULL(p.[seller_income_amount], 0) > ISNULL(p.[payment_amount], 0)
|
||
THEN p.[seller_income_amount] - ISNULL(p.[payment_amount], 0) ELSE 0 END AS decimal(18,2))
|
||
AS platform_discount,
|
||
CAST(CASE WHEN ISNULL(p.[product_amount], 0) < 0 THEN ABS(p.[product_amount]) ELSE NULL END AS decimal(18,2))
|
||
AS reversal_order_amount,
|
||
CAST(ISNULL(p.[product_amount], 0) AS decimal(18,2)) AS actual_shipped_amount,
|
||
CAST(ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS actual_shipped_quantity,
|
||
CAST(ISNULL(s.[cost_price], 0) * ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS actual_shipped_cost,
|
||
p.[order_type],
|
||
p.[online_product_name],
|
||
CAST(0 AS decimal(18,2)) AS pre_application_quantity,
|
||
CAST(0 AS decimal(18,2)) AS pre_application_amount,
|
||
CAST(0 AS decimal(18,2)) AS pre_application_cost,
|
||
CAST(0 AS decimal(18,2)) AS pre_return_quantity,
|
||
CAST(0 AS decimal(18,2)) AS pre_return_amount,
|
||
CAST(0 AS decimal(18,2)) AS pre_return_cost,
|
||
CAST(0 AS decimal(18,2)) AS pre_refund_amount,
|
||
CAST(0 AS decimal(18,2)) AS pre_refund_cost,
|
||
CAST(0 AS decimal(18,2)) AS pre_platform_subsidy,
|
||
CAST(0 AS decimal(18,2)) AS post_application_quantity,
|
||
CAST(0 AS decimal(18,2)) AS post_application_amount,
|
||
CAST(0 AS decimal(18,2)) AS post_application_cost,
|
||
CAST(0 AS decimal(18,2)) AS post_return_quantity,
|
||
CAST(0 AS decimal(18,2)) AS post_return_amount,
|
||
CAST(0 AS decimal(18,2)) AS post_return_cost,
|
||
CAST(0 AS decimal(18,2)) AS post_refund_amount,
|
||
CAST(0 AS decimal(18,2)) AS post_refund_cost,
|
||
CAST(0 AS decimal(18,2)) AS post_platform_subsidy,
|
||
CAST(0 AS decimal(18,2)) AS defective_quantity,
|
||
CAST(NULL AS nvarchar(100)) AS refund_type,
|
||
CAST(NULL AS nvarchar(100)) AS question_type,
|
||
CAST(NULL AS nvarchar(100)) AS detail_return_type,
|
||
CASE WHEN p.[is_gift] = 1 THEN N'是' ELSE N'否' END AS is_gift,
|
||
CASE WHEN NULLIF(p.[src_combine_sku_id], '') IS NULL THEN N'非组合' ELSE N'组合装' END AS combine_type,
|
||
p.[order_tags],
|
||
CAST(NULL AS nvarchar(500)) AS after_sales_tags,
|
||
s.[labels] AS item_tags,
|
||
COALESCE(p.[send_warehouse], p.[wms_co_id]) AS shipping_warehouse_code,
|
||
wh.[name] AS shipping_warehouse,
|
||
wh.[remark1] AS operation_cloud_warehouse,
|
||
s.[supplier_id] AS supplier_code,
|
||
s.[supplier_name] AS supplier_name,
|
||
p.[express_no],
|
||
p.[express_company],
|
||
p.[province],
|
||
p.[city],
|
||
p.[salesperson]
|
||
FROM paged_rows p
|
||
LEFT JOIN [rpt].[jst_sku] s ON s.[sku_id] = p.[product_code]
|
||
LEFT JOIN [rpt].[jst_warehouseInfo] wh
|
||
ON wh.[wms_co_id] = COALESCE(p.[send_warehouse], p.[wms_co_id])
|
||
ORDER BY p.[order_date] DESC, p.[internal_order_no] DESC, p.[oi_id] DESC";
|
||
|
||
$rows = $this->hydrateAfterSales($this->query()->select($sql, $bindings));
|
||
|
||
return $this->rowsResult($rows, fn (array $row): array => $this->normalizeAfterSalesRow($row));
|
||
}
|
||
|
||
/** @return array{rows:array<int,array<string,mixed>>} */
|
||
public function promotionExpenses(array $filters, int $page, int $limit): array
|
||
{
|
||
$date = $this->promotionDate($filters['date'] ?? null);
|
||
|
||
if ($date === null) {
|
||
return ['rows' => [], 'date' => null];
|
||
}
|
||
|
||
$where = ['c.[stat_date] = CAST(? AS date)', 'ISNULL(c.[push_fee], 0) <> 0'];
|
||
$bindings = [$date];
|
||
$shopNames = array_values(array_filter(array_map(
|
||
static fn (mixed $name): string => trim((string) $name),
|
||
(array) ($filters['shop_names'] ?? []),
|
||
), static fn (string $name): bool => $name !== ''));
|
||
if ($shopNames === [] && ($filters['shop_name'] ?? '') !== '') {
|
||
$shopNames = [trim((string) $filters['shop_name'])];
|
||
}
|
||
if ($shopNames !== []) {
|
||
$where[] = '(' . implode(' OR ', array_fill(0, count($shopNames), "c.[shop_name] LIKE ? ESCAPE '\\'")) . ')';
|
||
foreach ($shopNames as $name) {
|
||
$bindings[] = $this->contains($name);
|
||
}
|
||
}
|
||
|
||
if (($filters['sku_id'] ?? '') !== '') {
|
||
$needle = $this->contains($filters['sku_id']);
|
||
$where[] = "(c.[item_id] LIKE ? ESCAPE '\\' OR EXISTS ("
|
||
. 'SELECT 1 FROM [rpt].[jst_skumap] filter_map '
|
||
. 'WHERE filter_map.[shop_i_id] = c.[item_id] '
|
||
. "AND (filter_map.[sku_id] LIKE ? ESCAPE '\\'"
|
||
. " OR filter_map.[i_id] LIKE ? ESCAPE '\\'"
|
||
. " OR filter_map.[shop_sku_id] LIKE ? ESCAPE '\\')))";
|
||
array_push($bindings, $needle, $needle, $needle, $needle);
|
||
}
|
||
|
||
$whereSql = implode(' AND ', $where);
|
||
[$offset, $fetch] = $this->pageBounds($page, $limit);
|
||
|
||
$sql = "WITH paged AS (
|
||
SELECT
|
||
c.[shop_name],
|
||
c.[item_id],
|
||
CAST(c.[push_fee] AS decimal(18,2)) AS promotion_fee
|
||
FROM [rpt].[ads_itemid_sales_cost_ratio_v2_di] c
|
||
WHERE {$whereSql}
|
||
ORDER BY c.[push_fee] DESC, c.[shop_name], c.[item_id]
|
||
OFFSET {$offset} ROWS FETCH NEXT {$fetch} ROWS ONLY
|
||
)
|
||
SELECT
|
||
p.[shop_name],
|
||
p.[item_id] AS shop_style_code,
|
||
p.[promotion_fee]
|
||
FROM paged p
|
||
ORDER BY p.[promotion_fee] DESC, p.[shop_name], p.[item_id]";
|
||
|
||
$rows = $this->mapPromotionRows($this->query()->select($sql, $bindings));
|
||
|
||
$result = $this->rowsResult($rows, function (array $row): array {
|
||
$row['promotion_fee'] = $this->decimal($row['promotion_fee'] ?? null) ?? 0.0;
|
||
|
||
return $row;
|
||
});
|
||
|
||
return array_merge($result, ['date' => $date]);
|
||
}
|
||
|
||
private function query(): UpstreamReadOnlyQuery
|
||
{
|
||
return $this->query ??= $this->queries->forSource(self::SOURCE);
|
||
}
|
||
|
||
private function normalizeAfterSalesRow(array $row): array
|
||
{
|
||
$link = $this->productLink(
|
||
(string) ($row['shop_site'] ?? ''),
|
||
(string) ($row['shop_name'] ?? ''),
|
||
(string) ($row['shop_style_code'] ?? ''),
|
||
);
|
||
unset($row['shop_site']);
|
||
unset($row['unit_cost']);
|
||
|
||
$row['product_link'] = $link;
|
||
$row['offline_customer_code'] = null;
|
||
$row['offline_customer_name'] = null;
|
||
$row['unique_code'] = null;
|
||
$row['registration_time'] = null;
|
||
$row['confirmation_time'] = null;
|
||
$row['warehouse_entry_time'] = null;
|
||
$row['classification_order'] = null;
|
||
$row['receiving_warehouse_code'] = null;
|
||
$row['receiving_warehouse'] = null;
|
||
|
||
foreach ($this->expenseFields() as $field) {
|
||
$row[$field] = $this->decimal($row[$field] ?? null);
|
||
}
|
||
|
||
// 500112 冲减收入:暂以「退货红冲金额(reversal_order_amount)」作为收入冲减口径,
|
||
// 待财务确认最终取数规则后替换。其余 account_* 科目字段仍为空(科目归集未接入)。
|
||
$row['account_500112'] = $this->decimal($row['reversal_order_amount'] ?? null);
|
||
|
||
foreach ($this->amountFields() as $field) {
|
||
if (array_key_exists($field, $row)) {
|
||
$row[$field] = $this->decimal($row[$field]) ?? 0.0;
|
||
}
|
||
}
|
||
|
||
return $row;
|
||
}
|
||
|
||
/** @param array<int,array<string,mixed>> $rows */
|
||
private function hydrateAfterSales(array $rows): array
|
||
{
|
||
$orderIds = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => (string) ($row['internal_order_no'] ?? ''),
|
||
$rows,
|
||
))));
|
||
|
||
if ($orderIds === []) {
|
||
return $rows;
|
||
}
|
||
|
||
$placeholders = implode(', ', array_fill(0, count($orderIds), '?'));
|
||
$refunds = $this->query()->select(
|
||
"SELECT
|
||
a.[o_id] AS internal_order_no,
|
||
CONVERT(nvarchar(100), a.[as_id]) AS after_sales_id,
|
||
a.[outer_as_id] AS outside_after_sales_id,
|
||
CONVERT(varchar(19), a.[as_date], 120) AS after_sales_time,
|
||
a.[type] AS refund_type,
|
||
a.[question_type],
|
||
a.[labels] AS after_sales_tags,
|
||
a.[refund] AS refund_amount,
|
||
a.[free_amount] AS platform_subsidy,
|
||
ri.[outer_oi_id] AS sub_order_no,
|
||
ri.[sku_id] AS product_code,
|
||
ri.[type] AS detail_return_type,
|
||
ri.[qty] AS application_quantity,
|
||
ri.[amount] AS application_amount,
|
||
ri.[r_qty] AS return_quantity,
|
||
ri.[shop_amount] AS return_amount,
|
||
TRY_CONVERT(decimal(18,2), ri.[defective_qty]) AS defective_quantity
|
||
FROM [rpt].[jst_returnAndRefundInfo] a
|
||
INNER JOIN [rpt].[jst_returnAndRefundItem] ri ON ri.[as_id] = a.[as_id]
|
||
WHERE a.[o_id] IN ({$placeholders})
|
||
ORDER BY a.[as_date] DESC, a.[as_id] DESC",
|
||
$orderIds,
|
||
);
|
||
$byOrder = [];
|
||
|
||
foreach ($refunds as $refund) {
|
||
$byOrder[(string) ($refund['internal_order_no'] ?? '')][] = $refund;
|
||
}
|
||
|
||
foreach ($rows as &$row) {
|
||
$matches = array_values(array_filter(
|
||
$byOrder[(string) ($row['internal_order_no'] ?? '')] ?? [],
|
||
static function (array $refund) use ($row): bool {
|
||
$refundSubOrder = (string) ($refund['sub_order_no'] ?? '');
|
||
|
||
return $refundSubOrder !== ''
|
||
? $refundSubOrder === (string) ($row['sub_order_no'] ?? '')
|
||
: (string) ($refund['product_code'] ?? '') === (string) ($row['product_code'] ?? '');
|
||
},
|
||
));
|
||
$first = $matches[0] ?? [];
|
||
$row['after_sales_no'] = $first['outside_after_sales_id'] ?? $first['after_sales_id'] ?? null;
|
||
$row['refund_type'] = $first['refund_type'] ?? null;
|
||
$row['question_type'] = $first['question_type'] ?? null;
|
||
$row['detail_return_type'] = $first['detail_return_type'] ?? null;
|
||
$row['after_sales_tags'] = $first['after_sales_tags'] ?? null;
|
||
$unitCost = (float) ($row['unit_cost'] ?? 0);
|
||
$preReturnQuantity = 0.0;
|
||
|
||
foreach ($matches as $refund) {
|
||
$phase = empty($row['send_time'])
|
||
|| (string) ($refund['after_sales_time'] ?? '') < (string) $row['send_time']
|
||
? 'pre'
|
||
: 'post';
|
||
$applicationQuantity = (float) ($refund['application_quantity'] ?? 0);
|
||
$returnQuantity = (float) ($refund['return_quantity'] ?? 0);
|
||
$row[$phase . '_application_quantity'] += $applicationQuantity;
|
||
$row[$phase . '_application_amount'] += (float) ($refund['application_amount'] ?? 0);
|
||
$row[$phase . '_return_quantity'] += $returnQuantity;
|
||
$row[$phase . '_return_amount'] += (float) ($refund['return_amount'] ?? 0);
|
||
$row[$phase . '_refund_amount'] += (float) ($refund['refund_amount'] ?? 0);
|
||
$row[$phase . '_platform_subsidy'] += (float) ($refund['platform_subsidy'] ?? 0);
|
||
$row[$phase . '_application_cost'] += $applicationQuantity * $unitCost;
|
||
$row[$phase . '_return_cost'] += $returnQuantity * $unitCost;
|
||
$row[$phase . '_refund_cost'] += ($applicationQuantity - $returnQuantity) * $unitCost;
|
||
$row['defective_quantity'] += (float) ($refund['defective_quantity'] ?? 0);
|
||
|
||
if ($phase === 'pre') {
|
||
$preReturnQuantity += $returnQuantity;
|
||
}
|
||
}
|
||
|
||
$row['actual_shipped_quantity'] = (float) ($row['product_quantity'] ?? 0) - $preReturnQuantity;
|
||
$row['actual_shipped_amount'] = (float) ($row['product_amount'] ?? 0)
|
||
- (float) ($row['pre_return_amount'] ?? 0);
|
||
$row['actual_shipped_cost'] = $row['actual_shipped_quantity'] * $unitCost;
|
||
}
|
||
unset($row);
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/** @param array<int,array<string,mixed>> $rows */
|
||
private function mapPromotionRows(array $rows): array
|
||
{
|
||
$itemIds = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => (string) ($row['shop_style_code'] ?? ''),
|
||
$rows,
|
||
))));
|
||
|
||
if ($itemIds === []) {
|
||
return $rows;
|
||
}
|
||
|
||
$placeholders = implode(', ', array_fill(0, count($itemIds), '?'));
|
||
$mappings = $this->query()->select(
|
||
"SELECT
|
||
m.[shop_i_id] AS shop_style_code,
|
||
m.[sku_id] AS product_code,
|
||
m.[i_id] AS style_code,
|
||
m.[shop_sku_id] AS shop_product_code,
|
||
m.[properties_value] AS online_color_spec,
|
||
shop.[shop_name],
|
||
m.[modified]
|
||
FROM [rpt].[jst_skumap] m
|
||
LEFT JOIN [rpt].[jst_shopInfo] shop ON shop.[shop_id] = m.[shop_id]
|
||
WHERE m.[shop_i_id] IN ({$placeholders})
|
||
ORDER BY m.[shop_i_id], m.[modified] DESC",
|
||
$itemIds,
|
||
);
|
||
$byItem = [];
|
||
|
||
foreach ($mappings as $mapping) {
|
||
$byItem[(string) ($mapping['shop_style_code'] ?? '')][] = $mapping;
|
||
}
|
||
|
||
foreach ($rows as &$row) {
|
||
$candidates = $byItem[(string) ($row['shop_style_code'] ?? '')] ?? [];
|
||
$mapping = collect($candidates)->firstWhere('shop_name', $row['shop_name'] ?? null)
|
||
?? ($candidates[0] ?? []);
|
||
|
||
foreach (['product_code', 'style_code', 'shop_product_code', 'online_color_spec'] as $field) {
|
||
$row[$field] = $mapping[$field] ?? null;
|
||
}
|
||
}
|
||
unset($row);
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/** @return array<int,string> */
|
||
private function amountFields(): array
|
||
{
|
||
return [
|
||
'product_quantity', 'product_amount', 'product_cost', 'reversal_order_cost',
|
||
'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount',
|
||
'reversal_order_amount', 'actual_shipped_amount', 'actual_shipped_quantity',
|
||
'actual_shipped_cost', 'pre_application_quantity', 'pre_application_amount',
|
||
'pre_application_cost', 'pre_return_quantity', 'pre_return_amount', 'pre_return_cost',
|
||
'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy',
|
||
'post_application_quantity', 'post_application_amount', 'post_application_cost',
|
||
'post_return_quantity', 'post_return_amount', 'post_return_cost',
|
||
'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy', 'defective_quantity',
|
||
];
|
||
}
|
||
|
||
/** @return array<int,string> */
|
||
private function expenseFields(): array
|
||
{
|
||
return [
|
||
'account_500112', 'account_60031', 'account_800101', 'account_800103',
|
||
'account_800104', 'account_800105', 'account_800106', 'account_800107',
|
||
'account_800108', 'account_800109', 'account_800110', 'account_800114',
|
||
'account_8001201', 'account_8001202', 'account_800122', 'account_800125',
|
||
'account_800127', 'account_800129', 'account_800130', 'account_800132',
|
||
'account_800304', 'account_800501', 'account_800503',
|
||
];
|
||
}
|
||
|
||
/** @return array{0:int,1:int} */
|
||
private function pageBounds(int $page, int $limit): array
|
||
{
|
||
$page = max($page, 1);
|
||
$limit = min(max($limit, 1), 100);
|
||
$offset = ($page - 1) * $limit;
|
||
|
||
return [$offset, $limit + 1];
|
||
}
|
||
|
||
/** @return array{rows:array<int,array<string,mixed>>} */
|
||
private function rowsResult(array $rows, callable $normalizer): array
|
||
{
|
||
foreach ($rows as &$row) {
|
||
$row = $normalizer($row);
|
||
}
|
||
unset($row);
|
||
|
||
return ['rows' => $rows];
|
||
}
|
||
|
||
private function productLink(string $site, string $shopName, string $itemId): ?string
|
||
{
|
||
if ($itemId === '') {
|
||
return null;
|
||
}
|
||
|
||
$platform = mb_strtolower($site . ' ' . $shopName);
|
||
|
||
if (str_contains($platform, '京东') || str_contains($platform, '京喜') || str_contains($platform, 'jd')) {
|
||
return 'https://item.jd.com/' . rawurlencode($itemId) . '.html';
|
||
}
|
||
|
||
if (str_contains($platform, '抖音')) {
|
||
return 'https://haohuo.jinritemai.com/ecommerce/trade/detail/index.html?id=' . rawurlencode($itemId);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function decimal(mixed $value): ?float
|
||
{
|
||
return $value === null || $value === '' ? null : round((float) $value, 2);
|
||
}
|
||
|
||
private function promotionDate(mixed $value): ?string
|
||
{
|
||
$date = trim((string) $value);
|
||
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1) {
|
||
return $date;
|
||
}
|
||
|
||
$row = $this->query()->first(
|
||
'SELECT TOP 1 CONVERT(varchar(10), [stat_date], 23) AS [latest_date] '
|
||
. 'FROM [rpt].[ads_itemid_sales_cost_ratio_v2_di] ORDER BY [stat_date] DESC'
|
||
);
|
||
|
||
return isset($row['latest_date']) ? (string) $row['latest_date'] : null;
|
||
}
|
||
|
||
private function contains(mixed $value): string
|
||
{
|
||
return '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], (string) $value) . '%';
|
||
}
|
||
|
||
/**
|
||
* 批量筛选值:支持换行、逗号、分号、顿号、制表符和空格分隔,去重后最多 50 个。
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
private function batchValues(mixed $raw): array
|
||
{
|
||
$raw = trim((string) $raw);
|
||
if ($raw === '') {
|
||
return [];
|
||
}
|
||
|
||
$parts = preg_split('/[\s,;,;、]+/u', $raw) ?: [];
|
||
$values = [];
|
||
|
||
foreach ($parts as $part) {
|
||
$part = trim((string) $part);
|
||
if ($part === '') {
|
||
continue;
|
||
}
|
||
|
||
// 键加 'v' 前缀:纯数字单号当数组键会被 PHP 转成 int,会丢掉原字符串。
|
||
if (! isset($values['v' . $part])) {
|
||
$values['v' . $part] = $part;
|
||
}
|
||
if (count($values) >= 50) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return array_values($values);
|
||
}
|
||
}
|