CaiWuXiTong/modules/EcommerceFinance/Repositories/OrderDataMirrorRepository.php

355 lines
18 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
class OrderDataMirrorRepository
{
private const DATASETS = [
'operations_replica.rpt.jst_Orders',
'operations_replica.rpt.jst_OrderItem',
'operations_replica.rpt.jst_sku',
'operations_replica.rpt.jst_warehouseInfo',
'operations_replica.rpt.jst_returnAndRefundInfo',
'operations_replica.rpt.jst_returnAndRefundItem',
];
private const PROMOTION_DATASETS = [
'operations_replica.rpt.ads_itemid_sales_cost_ratio_v2_di',
'operations_replica.rpt.jst_skumap',
'operations_replica.rpt.jst_shopInfo',
];
private readonly ConnectionInterface $connection;
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {
$this->connection = $mirror->connection();
}
public function ready(): bool
{
return $this->datasetsReady(self::DATASETS);
}
public function promotionReady(): bool
{
return $this->datasetsReady(self::PROMOTION_DATASETS);
}
private function datasetsReady(array $datasets): bool
{
try {
foreach ($datasets as $dataset) {
if (! $this->mirror->ready($this->datasets->get($dataset))) {
return false;
}
}
return true;
} catch (\Throwable) {
return false;
}
}
/** @return array{rows:array<int,array<string,mixed>>} */
public function afterSales(array $filters, int $page, int $limit): array
{
$limit = min(max($limit, 1), 100);
$query = $this->baseQuery();
$this->applyFilters($query, $filters);
$rows = $query
->select($this->columns())
->orderByDesc('o.order_date')->orderByDesc('o.o_id')->orderByDesc('i.oi_id')
->offset((max($page, 1) - 1) * $limit)
->limit($limit + 1)
->get()->map(static fn (object $row): array => (array) $row)->all();
$rows = array_map(fn (array $row): array => $this->baseRow($row), $rows);
$this->hydrateRefunds($rows);
return ['rows' => $rows];
}
/** @return array{rows:array<int,array<string,mixed>>,date:?string} */
public function promotionExpenses(array $filters, int $page, int $limit): array
{
$limit = min(max($limit, 1), 100);
$date = trim((string) ($filters['date'] ?? ''));
if ($date === '') {
$date = (string) ($this->connection->table('ops_item_sales_cost_ratio')->max('stat_date') ?? '');
$date = substr($date, 0, 10);
}
if ($date === '') {
return ['rows' => [], 'date' => null];
}
$query = $this->connection->table('ops_item_sales_cost_ratio as c')
->where('c.stat_date', '>=', $date . ' 00:00:00')
->where('c.stat_date', '<', date('Y-m-d 00:00:00', strtotime($date . ' +1 day')))
->where('c.push_fee', '<>', 0);
if (($filters['shop_name'] ?? '') !== '') {
$query->whereRaw("c.shop_name LIKE ? ESCAPE '!'", [$this->contains((string) $filters['shop_name'])]);
}
if (($filters['sku_id'] ?? '') !== '') {
$needle = $this->contains((string) $filters['sku_id']);
$query->where(function (Builder $items) use ($needle): void {
$items->whereRaw("c.item_id LIKE ? ESCAPE '!'", [$needle])
->orWhereExists(function (Builder $mapping) use ($needle): void {
$mapping->selectRaw('1')->from('ops_jst_sku_map as filter_map')
->whereColumn('filter_map.shop_i_id', 'c.item_id')
->where(function (Builder $codes) use ($needle): void {
$codes->whereRaw("filter_map.sku_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("filter_map.i_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("filter_map.shop_sku_id LIKE ? ESCAPE '!'", [$needle]);
});
});
});
}
$rows = $query->select(['c.shop_name', 'c.item_id as shop_style_code', 'c.push_fee as promotion_fee'])
->orderByDesc('c.push_fee')->orderBy('c.shop_name')->orderBy('c.item_id')
->offset((max($page, 1) - 1) * $limit)->limit($limit + 1)
->get()->map(static fn (object $row): array => (array) $row)->all();
$itemIds = array_values(array_unique(array_filter(array_column($rows, 'shop_style_code'))));
$mappings = $itemIds === [] ? collect() : $this->connection->table('ops_jst_sku_map as m')
->leftJoin('ops_jst_shop_info as shop', 'shop.shop_id', '=', 'm.shop_id')
->whereIn('m.shop_i_id', $itemIds)->orderByDesc('m.modified')
->get(['m.shop_i_id', 'm.sku_id', 'm.i_id', 'm.shop_sku_id', 'm.properties_value', 'shop.shop_name'])
->groupBy('shop_i_id');
foreach ($rows as &$row) {
$candidates = $mappings->get($row['shop_style_code'], collect());
$mapping = $candidates->firstWhere('shop_name', $row['shop_name']) ?? $candidates->first();
$row['product_code'] = $mapping->sku_id ?? null;
$row['style_code'] = $mapping->i_id ?? null;
$row['shop_product_code'] = $mapping->shop_sku_id ?? null;
$row['online_color_spec'] = $mapping->properties_value ?? null;
$row['promotion_fee'] = round((float) $row['promotion_fee'], 2);
}
unset($row);
return ['rows' => $rows, 'date' => $date];
}
private function baseQuery(): Builder
{
$latestSkus = $this->connection->table('ops_jst_sku')
->selectRaw('sku_id, MAX(autoid) AS latest_autoid')
->where('sku_type', 'normal')->groupBy('sku_id');
return $this->connection->table('ops_jst_orders as o')
->join('ops_jst_order_item as i', 'i.o_id', '=', 'o.o_id')
->leftJoinSub($latestSkus, 'sku_keys', 'sku_keys.sku_id', '=', 'i.sku_id')
->leftJoin('ops_jst_sku as s', 's.autoid', '=', 'sku_keys.latest_autoid')
->leftJoin('ops_jst_warehouse_info as wh', function ($join): void {
$join->on('wh.wms_co_id', '=', $this->connection->raw('COALESCE(i.send_warehouse, o.wms_co_id)'));
});
}
private function applyFilters(Builder $query, array $filters): void
{
if (($filters['from'] ?? '') !== '') {
$query->where('o.order_date', '>=', $filters['from'] . ' 00:00:00');
}
if (($filters['to'] ?? '') !== '') {
$query->where('o.order_date', '<', date('Y-m-d 00:00:00', strtotime($filters['to'] . ' +1 day')));
}
if (($filters['order_no'] ?? '') !== '') {
$needle = $this->contains((string) $filters['order_no']);
$query->where(function (Builder $order) use ($needle): void {
$order->whereRaw("CAST(o.o_id AS CHAR) LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("o.so_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("i.outer_oi_id LIKE ? ESCAPE '!'", [$needle]);
});
}
if (($filters['shop_name'] ?? '') !== '') {
$query->whereRaw("o.shop_name LIKE ? ESCAPE '!'", [$this->contains((string) $filters['shop_name'])]);
}
if (($filters['sku_id'] ?? '') !== '') {
$needle = $this->contains((string) $filters['sku_id']);
$query->where(function (Builder $sku) use ($needle): void {
$sku->whereRaw("i.sku_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("i.shop_sku_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("i.shop_i_id LIKE ? ESCAPE '!'", [$needle]);
});
}
}
/** @return list<string> */
private function columns(): array
{
return [
'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 as pay_time',
'o.end_time as order_complete_time', 'o.send_date as send_time',
'o.order_date as order_time', 'o.shop_name', 'o.shop_id as shop_code',
'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',
'o.shop_site', 's.brand', 's.category', 's.vc_name as virtual_category',
's.cost_price as unit_cost', 'i.properties_value as color_spec',
'i.name as online_product_name', 's.name as sku_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',
'o.type as order_type', 'o.labels as order_tags',
'o.free_amount as merchant_discount', 'o.freight as freight_income',
'i.send_warehouse', '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',
'wh.name as shipping_warehouse', 'wh.remark1 as operation_cloud_warehouse',
's.supplier_id as supplier_code', 's.supplier_name',
];
}
private function baseRow(array $row): array
{
$quantity = (float) ($row['product_quantity'] ?? 0);
$amount = (float) ($row['product_amount'] ?? 0);
$cost = (float) ($row['unit_cost'] ?? 0);
$payment = (float) ($row['payment_amount'] ?? $amount);
$sellerIncome = (float) ($row['seller_income_amount'] ?? 0);
$row += [
'after_sales_no' => null, 'refund_type' => null, 'question_type' => null,
'detail_return_type' => null, 'after_sales_tags' => null,
];
$row['product_name'] = ($row['sku_name'] ?? '') !== '' ? $row['sku_name'] : ($row['color_spec'] ?? null);
$row['product_quantity'] = $quantity;
$row['product_amount'] = $amount;
$row['product_cost'] = $cost * $quantity;
$row['reversal_order_cost'] = $amount < 0 ? $cost * abs($quantity) : 0.0;
$row['merchant_discount'] = (float) ($row['merchant_discount'] ?? 0);
$row['freight_income'] = (float) ($row['freight_income'] ?? 0);
$row['payment_amount'] = $payment;
$row['platform_discount'] = $sellerIncome > $payment ? $sellerIncome - $payment : 0.0;
$row['reversal_order_amount'] = $amount < 0 ? abs($amount) : 0.0;
$row['actual_shipped_amount'] = $amount;
$row['actual_shipped_quantity'] = $quantity;
$row['actual_shipped_cost'] = $cost * $quantity;
foreach (['pre', 'post'] as $phase) {
foreach (['application_quantity', 'application_amount', 'application_cost', 'return_quantity',
'return_amount', 'return_cost', 'refund_amount', 'refund_cost', 'platform_subsidy'] as $field) {
$row[$phase . '_' . $field] = 0.0;
}
}
$row['defective_quantity'] = 0.0;
$row['is_gift'] = (int) ($row['is_gift'] ?? 0) === 1 ? '是' : '否';
$row['combine_type'] = empty($row['src_combine_sku_id']) ? '非组合' : '组合装';
$row['shipping_warehouse_code'] = $row['send_warehouse'] ?: ($row['wms_co_id'] ?? null);
$row['product_link'] = $this->productLink(
(string) ($row['shop_site'] ?? ''), (string) ($row['shop_name'] ?? ''),
(string) ($row['shop_style_code'] ?? ''),
);
foreach (['offline_customer_code', 'offline_customer_name', 'unique_code', 'registration_time',
'confirmation_time', 'warehouse_entry_time', 'classification_order',
'receiving_warehouse_code', 'receiving_warehouse'] as $field) {
$row[$field] = null;
}
foreach ($this->expenseFields() as $field) {
$row[$field] = null;
}
unset($row['shop_site'], $row['unit_cost'], $row['sku_name'], $row['seller_income_amount'],
$row['src_combine_sku_id'], $row['send_warehouse'], $row['wms_co_id']);
return $row;
}
/** @param array<int,array<string,mixed>> $rows */
private function hydrateRefunds(array &$rows): void
{
$orderIds = array_values(array_unique(array_filter(array_column($rows, 'internal_order_no'))));
if ($orderIds === []) {
return;
}
$refunds = $this->connection->table('ops_jst_return_refund_info as a')
->join('ops_jst_return_refund_item as ri', 'ri.as_id', '=', 'a.as_id')
->whereIn('a.o_id', $orderIds)
->orderByDesc('a.as_date')->orderByDesc('a.as_id')
->get([
'a.o_id as internal_order_no', 'a.as_id as after_sales_id',
'a.outer_as_id as outside_after_sales_id', 'a.as_date 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', 'ri.defective_qty as defective_quantity',
])->groupBy('internal_order_no');
foreach ($rows as &$row) {
$matches = collect($refunds->get($row['internal_order_no'], collect()))->filter(
static function (object $refund) use ($row): bool {
return (string) ($refund->sub_order_no ?? '') !== ''
? (string) $refund->sub_order_no === (string) ($row['sub_order_no'] ?? '')
: (string) ($refund->product_code ?? '') === (string) ($row['product_code'] ?? '');
},
)->values();
$first = $matches->first();
if ($first !== null) {
$row['after_sales_no'] = $first->outside_after_sales_id ?: $first->after_sales_id;
$row['refund_type'] = $first->refund_type;
$row['question_type'] = $first->question_type;
$row['detail_return_type'] = $first->detail_return_type;
$row['after_sales_tags'] = $first->after_sales_tags;
}
$unitCost = $row['product_quantity'] == 0 ? 0 : $row['product_cost'] / $row['product_quantity'];
$preReturned = 0.0;
foreach ($matches as $refund) {
$phase = empty($row['send_time']) || (string) $refund->after_sales_time < (string) $row['send_time']
? 'pre' : 'post';
$application = (float) ($refund->application_quantity ?? 0);
$returned = (float) ($refund->return_quantity ?? 0);
$row[$phase . '_application_quantity'] += $application;
$row[$phase . '_application_amount'] += (float) ($refund->application_amount ?? 0);
$row[$phase . '_application_cost'] += $application * $unitCost;
$row[$phase . '_return_quantity'] += $returned;
$row[$phase . '_return_amount'] += (float) ($refund->return_amount ?? 0);
$row[$phase . '_return_cost'] += $returned * $unitCost;
$row[$phase . '_refund_amount'] += (float) ($refund->refund_amount ?? 0);
$row[$phase . '_refund_cost'] += ($application - $returned) * $unitCost;
$row[$phase . '_platform_subsidy'] += (float) ($refund->platform_subsidy ?? 0);
$row['defective_quantity'] += (float) ($refund->defective_quantity ?? 0);
if ($phase === 'pre') {
$preReturned += $returned;
}
}
$row['actual_shipped_quantity'] = $row['product_quantity'] - $preReturned;
$row['actual_shipped_amount'] = $row['product_amount'] - $row['pre_return_amount'];
$row['actual_shipped_cost'] = $row['actual_shipped_quantity'] * $unitCost;
}
unset($row);
}
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;
}
/** @return list<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',
];
}
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
}
}