实现订单费用上游数据读取
This commit is contained in:
parent
68f1faac46
commit
f04e935eca
@ -15,8 +15,508 @@ class OrderDataRepository
|
||||
{
|
||||
}
|
||||
|
||||
/** @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) . '%';
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use App\Business\UpstreamBusinessInformation;
|
||||
use Modules\EcommerceFinance\Contracts\UpstreamSnapshotRefresher;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
|
||||
/**
|
||||
* 将经营分析副本库已登记的业务信息查询还原为共享快照刷新任务。
|
||||
@ -15,6 +16,7 @@ class OperationsReplicaSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
public function __construct(
|
||||
private readonly UpstreamQueryCacheFactory $caches,
|
||||
private readonly UpstreamBusinessInformation $information,
|
||||
private readonly OrderDataRepository $orders,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -31,7 +33,19 @@ class OperationsReplicaSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
return $this->caches->forSource(self::SOURCE)->refresh(
|
||||
$scope,
|
||||
$parameters,
|
||||
fn () => $this->information->snapshot($scope, $parameters),
|
||||
fn () => match ($scope) {
|
||||
'order-data.after-sales.list' => $this->orders->afterSales(
|
||||
(array) ($parameters['filters'] ?? []),
|
||||
(int) ($parameters['page'] ?? 1),
|
||||
(int) ($parameters['limit'] ?? 25),
|
||||
),
|
||||
'order-data.promotion.list' => $this->orders->promotionExpenses(
|
||||
(array) ($parameters['filters'] ?? []),
|
||||
(int) ($parameters['page'] ?? 1),
|
||||
(int) ($parameters['limit'] ?? 25),
|
||||
),
|
||||
default => $this->information->snapshot($scope, $parameters),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
135
modules/EcommerceFinance/View/OrderDataPageData.php
Normal file
135
modules/EcommerceFinance/View/OrderDataPageData.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\View;
|
||||
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Throwable;
|
||||
|
||||
class OrderDataPageData
|
||||
{
|
||||
private const SOURCE = 'operations_replica';
|
||||
|
||||
public function __construct(
|
||||
private readonly OrderDataRepository $orders,
|
||||
private readonly UpstreamQueryCacheFactory $caches,
|
||||
) {
|
||||
}
|
||||
|
||||
public function afterSales(): array
|
||||
{
|
||||
$filters = [
|
||||
'from' => $this->date(request('from')) ?: now()->subDays(6)->toDateString(),
|
||||
'to' => $this->date(request('to')) ?: now()->toDateString(),
|
||||
'order_no' => $this->text(request('order_no')),
|
||||
'shop_name' => $this->text(request('shop_name')),
|
||||
'sku_id' => $this->text(request('sku_id')),
|
||||
];
|
||||
[$page, $limit] = $this->pageSettings();
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
'order-data.after-sales.list',
|
||||
compact('filters', 'page', 'limit'),
|
||||
fn () => $this->orders->afterSales($filters, $page, $limit),
|
||||
);
|
||||
|
||||
return [
|
||||
'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit),
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => $cache->responseHeaders(),
|
||||
'orderDataError' => null,
|
||||
];
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
return $this->failed($filters, $page, $limit, $exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return $this->failed($filters, $page, $limit, '订单售后费用数据暂时无法读取,请稍后重试。');
|
||||
}
|
||||
}
|
||||
|
||||
public function promotionExpenses(): array
|
||||
{
|
||||
$filters = [
|
||||
'date' => $this->date(request('date')),
|
||||
'shop_name' => $this->text(request('shop_name')),
|
||||
'sku_id' => $this->text(request('sku_id')),
|
||||
];
|
||||
[$page, $limit] = $this->pageSettings();
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
'order-data.promotion.list',
|
||||
compact('filters', 'page', 'limit'),
|
||||
fn () => $this->orders->promotionExpenses($filters, $page, $limit),
|
||||
);
|
||||
|
||||
return [
|
||||
'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit),
|
||||
'orderDataFilters' => array_replace($filters, ['date' => $result['date'] ?? $filters['date']]),
|
||||
'orderDataStatus' => $cache->responseHeaders(),
|
||||
'orderDataError' => null,
|
||||
];
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
return $this->failed($filters, $page, $limit, $exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return $this->failed($filters, $page, $limit, '商品推广费用数据暂时无法读取,请稍后重试。');
|
||||
}
|
||||
}
|
||||
|
||||
private function failed(array $filters, int $page, int $limit, string $message): array
|
||||
{
|
||||
return [
|
||||
'orderDataRows' => $this->paginator([], $page, $limit),
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => [],
|
||||
'orderDataError' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
private function paginator(array $rows, int $page, int $limit): Paginator
|
||||
{
|
||||
return new Paginator(
|
||||
Collection::make($rows)->map(static fn (array $row): object => (object) $row),
|
||||
$limit,
|
||||
$page,
|
||||
[
|
||||
'path' => request()->url(),
|
||||
'query' => request()->except('page'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{0:int,1:int} */
|
||||
private function pageSettings(): array
|
||||
{
|
||||
return [
|
||||
max((int) request('page', 1), 1),
|
||||
min(max((int) setting('default.list_limit', 25), 1), 100),
|
||||
];
|
||||
}
|
||||
|
||||
private function text(mixed $value): string
|
||||
{
|
||||
return mb_substr(trim((string) $value), 0, 100);
|
||||
}
|
||||
|
||||
private function date(mixed $value): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $matches) !== 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1]) ? $value : '';
|
||||
}
|
||||
}
|
||||
@ -1,134 +1,238 @@
|
||||
@php
|
||||
$headers = [
|
||||
'内部订单号',
|
||||
'线上订单号',
|
||||
'子订单号',
|
||||
'售后单号',
|
||||
'支付时间',
|
||||
'订单完成时间',
|
||||
'发货时间',
|
||||
'订单时间',
|
||||
'店铺名称',
|
||||
'店铺编码',
|
||||
'商品编码',
|
||||
'款式编码(参考)',
|
||||
'店铺商品编码',
|
||||
'店铺款式编码',
|
||||
'链接',
|
||||
'品牌',
|
||||
'分类',
|
||||
'虚拟分类',
|
||||
'颜色及规格',
|
||||
'商品名称',
|
||||
'线下客户编号',
|
||||
'线下客户名称',
|
||||
'唯一码',
|
||||
'商品数量',
|
||||
'商品金额',
|
||||
'商品成本',
|
||||
'其中:红冲订单成本',
|
||||
'商家优惠',
|
||||
'运费收入',
|
||||
'付款金额',
|
||||
'其中:平台优惠',
|
||||
'其中:红冲订单金额',
|
||||
'实发金额',
|
||||
'实发数量',
|
||||
'实发成本',
|
||||
'订单类型',
|
||||
'登记时间',
|
||||
'确认时间',
|
||||
'进仓时间',
|
||||
'线上商品名称',
|
||||
'发货前申请数量',
|
||||
'发货前申请金额',
|
||||
'发货前申请成本',
|
||||
'发货前退回数量',
|
||||
'发货前退回金额',
|
||||
'发货前退回成本',
|
||||
'发货前退款金额',
|
||||
'发货前退款成本',
|
||||
'发货前退平台补贴',
|
||||
'发货后申请数量',
|
||||
'发货后申请金额',
|
||||
'发货后申请成本',
|
||||
'发货后退回数量',
|
||||
'发货后退回金额',
|
||||
'发货后退回成本',
|
||||
'发货后退款金额',
|
||||
'发货后退款成本',
|
||||
'发货后退平台补贴',
|
||||
'其中:次品商品数量',
|
||||
'退款类型',
|
||||
'问题类型',
|
||||
'明细退货类型',
|
||||
'分类单',
|
||||
'赠品',
|
||||
'组合装',
|
||||
'订单标签',
|
||||
'售后单标签',
|
||||
'商品标签',
|
||||
'发货仓编号',
|
||||
'发货仓库',
|
||||
'运营云仓',
|
||||
'收货仓编号',
|
||||
'收货仓',
|
||||
'供销商编号',
|
||||
'供销商',
|
||||
'快递单号',
|
||||
'快递公司',
|
||||
'省',
|
||||
'市',
|
||||
'业务员',
|
||||
'500112冲减收入',
|
||||
'60031erp快递费',
|
||||
'800101其他服务费',
|
||||
'800103其他活动费用',
|
||||
'800104违约赔付',
|
||||
'800105佣金',
|
||||
'800106保证金',
|
||||
'800107推广充值',
|
||||
'800108品牌新享',
|
||||
'800109淘宝客/多多进宝',
|
||||
'800110运费险',
|
||||
'800114公益宝贝',
|
||||
'8001201直通车/钻展/快车等',
|
||||
'8001202推广红包冲减',
|
||||
'800122售后补偿',
|
||||
'800125红包',
|
||||
'800127退货运费',
|
||||
'800129退款',
|
||||
'800130订单',
|
||||
'800132其他费用',
|
||||
'800304售后补偿',
|
||||
'800501种菜佣金',
|
||||
'800503发空包/赠品费用',
|
||||
$orderData = app(\Modules\EcommerceFinance\View\OrderDataPageData::class)->afterSales();
|
||||
$columns = [
|
||||
'internal_order_no' => '内部订单号',
|
||||
'online_order_no' => '线上订单号',
|
||||
'sub_order_no' => '子订单号',
|
||||
'after_sales_no' => '售后单号',
|
||||
'pay_time' => '支付时间',
|
||||
'order_complete_time' => '订单完成时间',
|
||||
'send_time' => '发货时间',
|
||||
'order_time' => '订单时间',
|
||||
'shop_name' => '店铺名称',
|
||||
'shop_code' => '店铺编码',
|
||||
'product_code' => '商品编码',
|
||||
'style_code' => '款式编码(参考)',
|
||||
'shop_product_code' => '店铺商品编码',
|
||||
'shop_style_code' => '店铺款式编码',
|
||||
'product_link' => '链接',
|
||||
'brand' => '品牌',
|
||||
'category' => '分类',
|
||||
'virtual_category' => '虚拟分类',
|
||||
'color_spec' => '颜色及规格',
|
||||
'product_name' => '商品名称',
|
||||
'offline_customer_code' => '线下客户编号',
|
||||
'offline_customer_name' => '线下客户名称',
|
||||
'unique_code' => '唯一码',
|
||||
'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' => '实发成本',
|
||||
'order_type' => '订单类型',
|
||||
'registration_time' => '登记时间',
|
||||
'confirmation_time' => '确认时间',
|
||||
'warehouse_entry_time' => '进仓时间',
|
||||
'online_product_name' => '线上商品名称',
|
||||
'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' => '其中:次品商品数量',
|
||||
'refund_type' => '退款类型',
|
||||
'question_type' => '问题类型',
|
||||
'detail_return_type' => '明细退货类型',
|
||||
'classification_order' => '分类单',
|
||||
'is_gift' => '赠品',
|
||||
'combine_type' => '组合装',
|
||||
'order_tags' => '订单标签',
|
||||
'after_sales_tags' => '售后单标签',
|
||||
'item_tags' => '商品标签',
|
||||
'shipping_warehouse_code' => '发货仓编号',
|
||||
'shipping_warehouse' => '发货仓库',
|
||||
'operation_cloud_warehouse' => '运营云仓',
|
||||
'receiving_warehouse_code' => '收货仓编号',
|
||||
'receiving_warehouse' => '收货仓',
|
||||
'supplier_code' => '供销商编号',
|
||||
'supplier_name' => '供销商',
|
||||
'express_no' => '快递单号',
|
||||
'express_company' => '快递公司',
|
||||
'province' => '省',
|
||||
'city' => '市',
|
||||
'salesperson' => '业务员',
|
||||
'account_500112' => '500112冲减收入',
|
||||
'account_60031' => '60031erp快递费',
|
||||
'account_800101' => '800101其他服务费',
|
||||
'account_800103' => '800103其他活动费用',
|
||||
'account_800104' => '800104违约赔付',
|
||||
'account_800105' => '800105佣金',
|
||||
'account_800106' => '800106保证金',
|
||||
'account_800107' => '800107推广充值',
|
||||
'account_800108' => '800108品牌新享',
|
||||
'account_800109' => '800109淘宝客/多多进宝',
|
||||
'account_800110' => '800110运费险',
|
||||
'account_800114' => '800114公益宝贝',
|
||||
'account_8001201' => '8001201直通车/钻展/快车等',
|
||||
'account_8001202' => '8001202推广红包冲减',
|
||||
'account_800122' => '800122售后补偿',
|
||||
'account_800125' => '800125红包',
|
||||
'account_800127' => '800127退货运费',
|
||||
'account_800129' => '800129退款',
|
||||
'account_800130' => '800130订单',
|
||||
'account_800132' => '800132其他费用',
|
||||
'account_800304' => '800304售后补偿',
|
||||
'account_800501' => '800501种菜佣金',
|
||||
'account_800503' => '800503发空包/赠品费用',
|
||||
];
|
||||
$numberFields = array_flip([
|
||||
'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',
|
||||
'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',
|
||||
]);
|
||||
$rows = $orderData['orderDataRows'];
|
||||
$filters = $orderData['orderDataFilters'];
|
||||
$status = data_get($orderData, 'orderDataStatus.X-Caiwu-Data-Status');
|
||||
$orderDataError = $orderData['orderDataError'];
|
||||
@endphp
|
||||
|
||||
<div class="upstream-data-page upstream-data-page--spaced">
|
||||
@include('business.pages.partials.upstream-data-styles')
|
||||
|
||||
<style>
|
||||
.order-data-table--wide { min-width: 590rem; }
|
||||
.order-data-link { color: var(--vip-content-accent-dark, #856632); }
|
||||
.order-data-link .material-icons-outlined { font-size: 1.125rem; }
|
||||
</style>
|
||||
|
||||
<form method="GET" class="upstream-data-toolbar" aria-label="订单售后费用明细筛选">
|
||||
<div class="upstream-data-field">
|
||||
<label for="order-data-from">订单开始日期</label>
|
||||
<input id="order-data-from" name="from" type="date" value="{{ $filters['from'] ?? '' }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="order-data-to">订单结束日期</label>
|
||||
<input id="order-data-to" name="to" type="date" value="{{ $filters['to'] ?? '' }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="order-data-order-no">订单号</label>
|
||||
<input id="order-data-order-no" name="order_no" type="text" value="{{ $filters['order_no'] ?? '' }}" placeholder="内部、线上或子订单号">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="order-data-shop">店铺名称</label>
|
||||
<input id="order-data-shop" name="shop_name" type="text" value="{{ $filters['shop_name'] ?? '' }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="order-data-sku">商品编码</label>
|
||||
<input id="order-data-sku" name="sku_id" type="text" value="{{ $filters['sku_id'] ?? '' }}" placeholder="商品、款式或店铺编码">
|
||||
</div>
|
||||
<div class="upstream-data-actions">
|
||||
<button type="submit" class="upstream-data-button upstream-data-button--primary">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a href="{{ route('business.pages.show', ['section' => $section['slug'], 'page' => $page['slug']]) }}" class="upstream-data-button">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($orderDataError ?? null)
|
||||
<div class="upstream-data-alert" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $orderDataError }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="upstream-data-table-panel">
|
||||
<div class="upstream-data-table-scroll">
|
||||
<table class="upstream-data-table order-data-table" aria-label="订单售后费用明细">
|
||||
<table class="upstream-data-table order-data-table order-data-table--wide" aria-label="订单售后费用明细">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($headers as $header)
|
||||
<th>{{ $header }}</th>
|
||||
@foreach ($columns as $field => $header)
|
||||
<th @class(['is-number' => isset($numberFields[$field])])>{{ $header }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="is-empty">
|
||||
<td colspan="{{ count($headers) }}">暂无数据</td>
|
||||
</tr>
|
||||
@forelse ($rows ?? [] as $row)
|
||||
<tr>
|
||||
@foreach ($columns as $field => $header)
|
||||
@php $value = data_get($row, $field); @endphp
|
||||
<td @class(['is-number' => isset($numberFields[$field])])>
|
||||
@if ($field === 'product_link' && $value)
|
||||
<a class="order-data-link" href="{{ $value }}" target="_blank" rel="noopener" title="打开商品链接" aria-label="打开商品链接">
|
||||
<span class="material-icons-outlined" aria-hidden="true">open_in_new</span>
|
||||
</a>
|
||||
@elseif (isset($numberFields[$field]))
|
||||
{{ $value === null || $value === '' ? '-' : number_format((float) $value, 2) }}
|
||||
@else
|
||||
{{ $value === null || $value === '' ? '-' : $value }}
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="{{ count($columns) }}">暂无数据</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="upstream-data-pagination">
|
||||
<span>共 0 条记录</span>
|
||||
<span>第 1 / 1 页</span>
|
||||
<span>
|
||||
当前页 {{ $rows?->count() ?? 0 }} 条
|
||||
@if ($status) · {{ strtoupper($status) }} @endif
|
||||
</span>
|
||||
<div class="upstream-data-pagination__controls">
|
||||
<a
|
||||
href="{{ $rows?->previousPageUrl() ?? '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => !$rows || $rows->onFirstPage()])
|
||||
aria-label="上一页"
|
||||
title="上一页"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<span>第 {{ $rows?->currentPage() ?? 1 }} 页</span>
|
||||
<a
|
||||
href="{{ $rows?->nextPageUrl() ?? '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => !$rows?->hasMorePages()])
|
||||
aria-label="下一页"
|
||||
title="下一页"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,38 +1,107 @@
|
||||
@php
|
||||
$headers = [
|
||||
'店铺',
|
||||
'商品编码',
|
||||
'款式编码',
|
||||
'店铺商品编码',
|
||||
'店铺款式编码',
|
||||
'线上颜色规格',
|
||||
'8001201直通车/钻展/快车等',
|
||||
$orderData = app(\Modules\EcommerceFinance\View\OrderDataPageData::class)->promotionExpenses();
|
||||
$columns = [
|
||||
'shop_name' => '店铺',
|
||||
'product_code' => '商品编码',
|
||||
'style_code' => '款式编码',
|
||||
'shop_product_code' => '店铺商品编码',
|
||||
'shop_style_code' => '店铺款式编码',
|
||||
'online_color_spec' => '线上颜色规格',
|
||||
'promotion_fee' => '8001201直通车/钻展/快车等',
|
||||
];
|
||||
$rows = $orderData['orderDataRows'];
|
||||
$filters = $orderData['orderDataFilters'];
|
||||
$status = data_get($orderData, 'orderDataStatus.X-Caiwu-Data-Status');
|
||||
$orderDataError = $orderData['orderDataError'];
|
||||
@endphp
|
||||
|
||||
<div class="upstream-data-page upstream-data-page--spaced">
|
||||
@include('business.pages.partials.upstream-data-styles')
|
||||
|
||||
<form method="GET" class="upstream-data-toolbar" aria-label="商品推广费用明细筛选">
|
||||
<div class="upstream-data-field">
|
||||
<label for="promotion-expense-date">统计日期</label>
|
||||
<input id="promotion-expense-date" name="date" type="date" value="{{ $filters['date'] ?? '' }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="promotion-expense-shop">店铺名称</label>
|
||||
<input id="promotion-expense-shop" name="shop_name" type="text" value="{{ $filters['shop_name'] ?? '' }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="promotion-expense-sku">商品编码</label>
|
||||
<input id="promotion-expense-sku" name="sku_id" type="text" value="{{ $filters['sku_id'] ?? '' }}" placeholder="商品、款式或店铺编码">
|
||||
</div>
|
||||
<div class="upstream-data-actions">
|
||||
<button type="submit" class="upstream-data-button upstream-data-button--primary">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a href="{{ route('business.pages.show', ['section' => $section['slug'], 'page' => $page['slug']]) }}" class="upstream-data-button">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($orderDataError ?? null)
|
||||
<div class="upstream-data-alert" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $orderDataError }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="upstream-data-table-panel">
|
||||
<div class="upstream-data-table-scroll">
|
||||
<table class="upstream-data-table order-data-table" aria-label="商品推广费用明细">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($headers as $header)
|
||||
<th>{{ $header }}</th>
|
||||
@foreach ($columns as $field => $header)
|
||||
<th @class(['is-number' => $field === 'promotion_fee'])>{{ $header }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="is-empty">
|
||||
<td colspan="{{ count($headers) }}">暂无数据</td>
|
||||
</tr>
|
||||
@forelse ($rows ?? [] as $row)
|
||||
<tr>
|
||||
@foreach ($columns as $field => $header)
|
||||
@php $value = data_get($row, $field); @endphp
|
||||
<td @class(['is-number' => $field === 'promotion_fee'])>
|
||||
@if ($field === 'promotion_fee')
|
||||
{{ $value === null || $value === '' ? '-' : number_format((float) $value, 2) }}
|
||||
@else
|
||||
{{ $value === null || $value === '' ? '-' : $value }}
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="{{ count($columns) }}">暂无数据</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="upstream-data-pagination">
|
||||
<span>共 0 条记录</span>
|
||||
<span>第 1 / 1 页</span>
|
||||
<span>
|
||||
当前页 {{ $rows?->count() ?? 0 }} 条
|
||||
@if ($status) · {{ strtoupper($status) }} @endif
|
||||
</span>
|
||||
<div class="upstream-data-pagination__controls">
|
||||
<a
|
||||
href="{{ $rows?->previousPageUrl() ?? '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => !$rows || $rows->onFirstPage()])
|
||||
aria-label="上一页"
|
||||
title="上一页"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<span>第 {{ $rows?->currentPage() ?? 1 }} 页</span>
|
||||
<a
|
||||
href="{{ $rows?->nextPageUrl() ?? '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => !$rows?->hasMorePages()])
|
||||
aria-label="下一页"
|
||||
title="下一页"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
102
tests/Feature/Business/OrderDataPagesUpstreamTest.php
Normal file
102
tests/Feature/Business/OrderDataPagesUpstreamTest.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class OrderDataPagesUpstreamTest extends FeatureTestCase
|
||||
{
|
||||
public function test_order_data_pages_render_cached_upstream_rows_and_filters(): void
|
||||
{
|
||||
$this->allowOrderDataPages();
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')->twice()->andReturnUsing(
|
||||
function (string $scope, array $parameters): array {
|
||||
if ($scope === 'order-data.after-sales.list') {
|
||||
$this->assertSame('ORDER-1', $parameters['filters']['order_no']);
|
||||
|
||||
return ['rows' => [[
|
||||
'internal_order_no' => 'ORDER-1',
|
||||
'shop_name' => '测试店铺',
|
||||
'product_amount' => 123.45,
|
||||
]]];
|
||||
}
|
||||
|
||||
$this->assertSame('order-data.promotion.list', $scope);
|
||||
|
||||
return [
|
||||
'date' => '2026-08-09',
|
||||
'rows' => [[
|
||||
'shop_name' => '推广测试店铺',
|
||||
'product_code' => 'SKU-1',
|
||||
'promotion_fee' => 9.05,
|
||||
]],
|
||||
];
|
||||
},
|
||||
);
|
||||
$cache->shouldReceive('responseHeaders')->twice()->andReturn([
|
||||
'X-Caiwu-Data-Status' => 'direct',
|
||||
]);
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')->twice()->with('operations_replica')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'after-sales-expenses',
|
||||
'order_no' => 'ORDER-1',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-09',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('ORDER-1')
|
||||
->assertSeeText('测试店铺')
|
||||
->assertSeeText('123.45')
|
||||
->assertSeeText('DIRECT');
|
||||
|
||||
$this->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'promotion-expenses',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('推广测试店铺')
|
||||
->assertSeeText('SKU-1')
|
||||
->assertSeeText('9.05')
|
||||
->assertSee('value="2026-08-09"', false);
|
||||
}
|
||||
|
||||
public function test_order_data_page_keeps_rendering_when_the_upstream_is_unavailable(): void
|
||||
{
|
||||
$this->allowOrderDataPages();
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')->once()->andThrow(
|
||||
new ServiceUnavailableHttpException(30, '首份共享快照正在准备。')
|
||||
);
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')->once()->with('operations_replica')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'promotion-expenses',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('首份共享快照正在准备。')
|
||||
->assertSeeText('暂无数据');
|
||||
}
|
||||
|
||||
private function allowOrderDataPages(): void
|
||||
{
|
||||
$this->user->update(['locale' => 'zh-CN']);
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ namespace Tests\Unit\EcommerceFinance;
|
||||
use App\Business\UpstreamBusinessInformation;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Services\OperationsReplicaSnapshotRefresher;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
@ -68,7 +69,8 @@ class OperationsReplicaSnapshotRefresherTest extends TestCase
|
||||
}
|
||||
};
|
||||
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information);
|
||||
$orders = $this->createMock(OrderDataRepository::class);
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
|
||||
|
||||
$this->assertSame('operations_replica', $refresher->sourceKey());
|
||||
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
|
||||
@ -78,6 +80,66 @@ class OperationsReplicaSnapshotRefresherTest extends TestCase
|
||||
$this->assertSame(['scope' => $scope, 'parameters' => $parameters], $cache->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider orderDataScopes
|
||||
*/
|
||||
public function test_order_data_scopes_use_the_order_data_repository(
|
||||
string $scope,
|
||||
string $method,
|
||||
array $parameters,
|
||||
): void {
|
||||
$cache = new class extends UpstreamQueryCache {
|
||||
public mixed $value = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function refresh(string $scope, array $parameters, Closure $resolver): string
|
||||
{
|
||||
$this->value = $resolver();
|
||||
|
||||
return 'fresh';
|
||||
}
|
||||
};
|
||||
$caches = new class($cache) extends UpstreamQueryCacheFactory {
|
||||
public function __construct(private readonly UpstreamQueryCache $cache)
|
||||
{
|
||||
}
|
||||
|
||||
public function forSource(string $source): UpstreamQueryCache
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
};
|
||||
$information = $this->createMock(UpstreamBusinessInformation::class);
|
||||
$information->expects($this->never())->method('snapshot');
|
||||
$orders = $this->createMock(OrderDataRepository::class);
|
||||
$orders->expects($this->once())
|
||||
->method($method)
|
||||
->with($parameters['filters'], $parameters['page'], $parameters['limit'])
|
||||
->willReturn(['rows' => [['id' => 1]]]);
|
||||
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
|
||||
|
||||
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
|
||||
$this->assertSame(['rows' => [['id' => 1]]], $cache->value);
|
||||
}
|
||||
|
||||
public static function orderDataScopes(): array
|
||||
{
|
||||
$parameters = [
|
||||
'filters' => ['date' => '2026-08-09'],
|
||||
'page' => 1,
|
||||
'limit' => 25,
|
||||
];
|
||||
|
||||
return [
|
||||
'after sales' => ['order-data.after-sales.list', 'afterSales', $parameters],
|
||||
'promotion' => ['order-data.promotion.list', 'promotionExpenses', $parameters],
|
||||
];
|
||||
}
|
||||
|
||||
public static function snapshotScopes(): array
|
||||
{
|
||||
return [
|
||||
|
||||
173
tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
Normal file
173
tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class OrderDataRepositoryTest extends TestCase
|
||||
{
|
||||
public function test_after_sales_uses_fixed_joins_parameterized_filters_and_overfetch_pagination(): void
|
||||
{
|
||||
$query = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$call = 0;
|
||||
$query->expects($this->exactly(2))->method('select')->willReturnCallback(
|
||||
function (string $sql, array $bindings) use (&$call): array {
|
||||
$call++;
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
|
||||
if ($call === 2) {
|
||||
$this->assertStringContainsString('[rpt].[jst_returnAndRefundInfo]', $sql);
|
||||
$this->assertSame(['1'], $bindings);
|
||||
|
||||
return [[
|
||||
'internal_order_no' => '1',
|
||||
'after_sales_id' => 'AF-1',
|
||||
'after_sales_time' => '2026-08-02 12:00:00',
|
||||
'sub_order_no' => 'SUB-1',
|
||||
'product_code' => 'SKU-1',
|
||||
'application_quantity' => '1',
|
||||
'application_amount' => '6.00',
|
||||
'return_quantity' => '1',
|
||||
'return_amount' => '6.00',
|
||||
]];
|
||||
}
|
||||
|
||||
$this->assertStringContainsString('[rpt].[jst_Orders]', $sql);
|
||||
$this->assertStringContainsString('[rpt].[jst_OrderItem]', $sql);
|
||||
$this->assertStringContainsString('OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY', $sql);
|
||||
$this->assertStringNotContainsString('COUNT(*) OVER', $sql);
|
||||
$this->assertSame([
|
||||
'2026-08-01',
|
||||
'2026-08-02',
|
||||
'%SO\%\_%',
|
||||
'%SO\%\_%',
|
||||
'%SO\%\_%',
|
||||
'%JD%',
|
||||
'%SKU%',
|
||||
'%SKU%',
|
||||
'%SKU%',
|
||||
], $bindings);
|
||||
|
||||
return [[
|
||||
'internal_order_no' => '1',
|
||||
'sub_order_no' => 'SUB-1',
|
||||
'product_code' => 'SKU-1',
|
||||
'shop_site' => 'jd',
|
||||
'shop_style_code' => '1001',
|
||||
'send_time' => '2026-08-02 10:00:00',
|
||||
'unit_cost' => '3.00',
|
||||
'product_amount' => '12.34',
|
||||
'product_quantity' => '2',
|
||||
'actual_shipped_amount' => '12.34',
|
||||
'actual_shipped_quantity' => '2',
|
||||
'actual_shipped_cost' => '6.00',
|
||||
'pre_application_quantity' => '0',
|
||||
'pre_application_amount' => '0',
|
||||
'pre_application_cost' => '0',
|
||||
'pre_return_quantity' => '0',
|
||||
'pre_return_amount' => '0',
|
||||
'pre_return_cost' => '0',
|
||||
'pre_refund_amount' => '0',
|
||||
'pre_refund_cost' => '0',
|
||||
'pre_platform_subsidy' => '0',
|
||||
'post_application_quantity' => '0',
|
||||
'post_application_amount' => '0',
|
||||
'post_application_cost' => '0',
|
||||
'post_return_quantity' => '0',
|
||||
'post_return_amount' => '0',
|
||||
'post_return_cost' => '0',
|
||||
'post_refund_amount' => '0',
|
||||
'post_refund_cost' => '0',
|
||||
'post_platform_subsidy' => '0',
|
||||
'defective_quantity' => '0',
|
||||
]];
|
||||
},
|
||||
);
|
||||
|
||||
$result = (new OrderDataRepository($this->factory($query)))->afterSales([
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-02',
|
||||
'order_no' => 'SO%_',
|
||||
'shop_name' => 'JD',
|
||||
'sku_id' => 'SKU',
|
||||
], 2, 2);
|
||||
|
||||
$this->assertCount(1, $result['rows']);
|
||||
$this->assertSame(12.34, $result['rows'][0]['product_amount']);
|
||||
$this->assertSame(2.0, $result['rows'][0]['product_quantity']);
|
||||
$this->assertSame('https://item.jd.com/1001.html', $result['rows'][0]['product_link']);
|
||||
$this->assertSame('AF-1', $result['rows'][0]['after_sales_no']);
|
||||
$this->assertSame(1.0, $result['rows'][0]['post_return_quantity']);
|
||||
$this->assertNull($result['rows'][0]['account_8001201']);
|
||||
}
|
||||
|
||||
public function test_promotion_expenses_use_one_statistics_date_and_map_only_the_page_rows(): void
|
||||
{
|
||||
$query = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$call = 0;
|
||||
$query->expects($this->exactly(2))->method('select')->willReturnCallback(
|
||||
function (string $sql, array $bindings) use (&$call): array {
|
||||
$call++;
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
|
||||
if ($call === 2) {
|
||||
$this->assertStringContainsString('[rpt].[jst_skumap]', $sql);
|
||||
$this->assertSame(['ITEM-1'], $bindings);
|
||||
|
||||
return [[
|
||||
'shop_style_code' => 'ITEM-1',
|
||||
'shop_name' => 'JD',
|
||||
'product_code' => 'SKU-1',
|
||||
'style_code' => 'STYLE-1',
|
||||
'shop_product_code' => 'SHOP-SKU-1',
|
||||
'online_color_spec' => '黑色',
|
||||
]];
|
||||
}
|
||||
|
||||
$this->assertStringContainsString('[rpt].[ads_itemid_sales_cost_ratio_v2_di]', $sql);
|
||||
$this->assertStringContainsString('OFFSET 3 ROWS FETCH NEXT 4 ROWS ONLY', $sql);
|
||||
$this->assertStringContainsString('FROM paged p', $sql);
|
||||
$this->assertStringNotContainsString('SUM(c.[push_fee])', $sql);
|
||||
$this->assertStringNotContainsString('COUNT(*) OVER', $sql);
|
||||
$this->assertSame([
|
||||
'2026-08-09',
|
||||
'%JD%',
|
||||
'%SKU\_%',
|
||||
'%SKU\_%',
|
||||
'%SKU\_%',
|
||||
'%SKU\_%',
|
||||
], $bindings);
|
||||
|
||||
return [[
|
||||
'shop_name' => 'JD',
|
||||
'shop_style_code' => 'ITEM-1',
|
||||
'promotion_fee' => '9.05',
|
||||
]];
|
||||
},
|
||||
);
|
||||
|
||||
$result = (new OrderDataRepository($this->factory($query)))->promotionExpenses([
|
||||
'date' => '2026-08-09',
|
||||
'shop_name' => 'JD',
|
||||
'sku_id' => 'SKU_',
|
||||
], 2, 3);
|
||||
|
||||
$this->assertSame('2026-08-09', $result['date']);
|
||||
$this->assertSame(9.05, $result['rows'][0]['promotion_fee']);
|
||||
$this->assertSame('SKU-1', $result['rows'][0]['product_code']);
|
||||
}
|
||||
|
||||
private function factory(UpstreamReadOnlyQuery $query): UpstreamReadOnlyQueryFactory
|
||||
{
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->expects($this->once())
|
||||
->method('forSource')
|
||||
->with('operations_replica')
|
||||
->willReturn($query);
|
||||
|
||||
return $factory;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user