## 任务背景 订单数据页面已完成附件表头,本任务由 renshen 接入聚水潭上游真实只读数据。 ## 任务目标 - 订单售后费用明细:按附件字段展示订单、商品、SKU 成本及售后关联数据。 - 商品推广费用明细:按商品/款式汇总推广费用。 - 不硬编码附件演示值;缺失字段保持空值或零。 ## 预计修改路径 - modules/EcommerceFinance/Repositories/OrderDataRepository.php - modules/EcommerceFinance/View/Composers/OrderDataPageComposer.php - modules/EcommerceFinance/Providers/Main.php - resources/views/business/pages/order-data/*.blade.php - tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php - tests/Feature/Business/OrderDataPagesUpstreamTest.php ## 页面和路由 复用 order-data/after-sales-expenses、order-data/promotion-expenses 和 business.pages.show;不新增或修改 routes。 ## Controller / Repository 新增 OrderDataRepository 和专用 View Composer;不修改 app/Http/Controllers/Business/Pages.php。 ## 数据源、表和字段 source_key=operations_replica,数据库 JSReportReplica,schema=rpt。固定参数化只读查询使用 jst_Orders、jst_OrderItem、jst_sku、jst_skumap、jst_returnAndRefundInfo、jst_returnAndRefundItem、jst_warehouseInfo、ads_itemid_sales_cost_ratio_v2_di。字段限于订单/商品标识、时间、店铺、SKU 元数据、金额、售后数量金额及 push_fee。 ## scope - order-data.after-sales.list - order-data.promotion.list ## 权限、菜单和公共组件 复用现有页面权限、菜单、UpstreamReadOnlyQueryFactory 和 UpstreamQueryCacheFactory;不修改权限和导航。 ## 明确不修改 不修改 routes、Pages Controller、数据库迁移、上游连接配置、权限、菜单、京东钱包功能、共享数据库结构或任何上游数据。 ## 范围补充:后台快照刷新 - modules/EcommerceFinance/Services/OperationsReplicaSnapshotRefresher.php - tests/Unit/EcommerceFinance/OperationsReplicaSnapshotRefresherTest.php 刷新器仅为 order-data.after-sales.list 和 order-data.promotion.list 分派同一 OrderDataRepository 固定查询;其他 scope 继续复用现有 UpstreamBusinessInformation。 Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/27
523 lines
23 KiB
PHP
523 lines
23 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'] ?? '') !== '') {
|
|
$needle = $this->contains($filters['order_no']);
|
|
$where[] = "(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 (($filters['shop_name'] ?? '') !== '') {
|
|
$where[] = "o.[shop_name] LIKE ? ESCAPE '\\'";
|
|
$bindings[] = $this->contains($filters['shop_name']);
|
|
}
|
|
if (($filters['sku_id'] ?? '') !== '') {
|
|
$needle = $this->contains($filters['sku_id']);
|
|
$where[] = "(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);
|
|
}
|
|
|
|
$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 0 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];
|
|
if (($filters['shop_name'] ?? '') !== '') {
|
|
$where[] = "c.[shop_name] LIKE ? ESCAPE '\\'";
|
|
$bindings[] = $this->contains($filters['shop_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);
|
|
}
|
|
|
|
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) . '%';
|
|
}
|
|
}
|