355 lines
18 KiB
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)) . '%';
|
|
}
|
|
}
|