CaiWuXiTong/modules/EcommerceFinance/Repositories/OrderProfitRepository.php

418 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\GoodsProfitAfsLookupSchema;
/**
* 聚水潭「订单利润」页面数据,按订单聚合商品利润镜像明细。
*
* 商品成本、退款、退平台补贴等来自订单利润明细镜像;包材费、快递费和
* 账单费用属于聚水潭页面侧的自动匹配结果,源表中没有字段,未确认规则前
* 保持为空,不能用 0 代替。
*/
class OrderProfitRepository
{
private const TABLE = GoodsProfitAfsLookupSchema::TABLE;
private const NUMERIC_FIELDS = [
'product_quantity', 'product_amount', 'product_cost',
'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost',
'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy',
'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy',
];
private const ITEM_FIELDS = [
'internal_order_no', 'online_order_no', 'sub_order_no', 'after_sales_no',
'order_time', 'shop_name', 'product_code', 'style_code',
'shop_product_code', 'shop_style_code', 'product_name', 'color_spec',
'product_quantity', 'product_amount', 'product_cost',
'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost',
'refund_type', 'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy',
'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy',
'classification_order', 'combine_type', 'shipping_warehouse', 'express_no',
'express_company', 'order_tags', 'after_sales_tags', 'item_tags',
];
private ?bool $readyCache = null;
public function __construct(private readonly ?GoodsProfitAfsRepository $fields = null)
{
}
public function ready(): bool
{
return $this->readyCache ??= ($this->fields ?? app(GoodsProfitAfsRepository::class))->ready();
}
public function status(): array
{
try {
$state = $this->connection()->table('business_mirror_table_states')
->where('dataset_key', GoodsProfitAfsRepository::MAIN_DATASET)
->first(['initial_completed_at', 'last_succeeded_at', 'last_error_code']);
} catch (\Throwable) {
$state = null;
}
return [
'ready' => $this->ready(),
'initial_completed_at' => $state->initial_completed_at ?? null,
'last_succeeded_at' => $state->last_succeeded_at ?? null,
'last_error_code' => $state->last_error_code ?? null,
];
}
/**
* @param array<string,mixed> $filters
* @return array{rows:list<array<string,mixed>>,has_more:bool}
*/
public function paginate(array $filters, int $page, int $limit): array
{
if (! $this->hasActiveFilters($filters)) {
return ['rows' => [], 'has_more' => false];
}
$columns = $this->columns();
foreach ([
'shop_name', 'online_order_no', 'internal_order_no', 'order_time',
'product_cost', 'refund_type', 'post_refund_amount', 'post_platform_subsidy',
'pre_refund_amount', 'pre_platform_subsidy', 'classification_order',
] as $field) {
if (! isset($columns[$field])) {
return ['rows' => [], 'has_more' => false];
}
}
$page = max($page, 1);
$limit = min(max($limit, 1), 100);
$orderValues = self::batchValues((string) ($filters['order_no'] ?? ''));
if ($orderValues !== []) {
$ids = $this->orderNumberIds($orderValues, $columns);
if ($ids === []) {
return ['rows' => [], 'has_more' => false];
}
$query = $this->connection()->table(self::TABLE)->whereIn('_mirror_key', $ids);
$query = $this->filtered(
$query,
array_replace($filters, ['order_no' => '', 'from' => '', 'to' => '']),
$columns,
);
} else {
$query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns);
}
$shop = $this->quote($columns['shop_name']);
$online = $this->quote($columns['online_order_no']);
$internal = $this->quote($columns['internal_order_no']);
$orderTime = $this->quote($columns['order_time']);
$refundType = $this->quote($columns['refund_type']);
$categoryOrder = $this->quote($columns['classification_order']);
$cost = $this->sumExpression($columns, ['product_cost']);
$refund = $this->sumExpression($columns, ['pre_refund_amount', 'post_refund_amount']);
$platformSubsidy = $this->sumExpression($columns, ['pre_platform_subsidy', 'post_platform_subsidy']);
$quantity = $this->sumExpression($columns, ['product_quantity']);
$amount = $this->sumExpression($columns, ['product_amount']);
$records = $query
->selectRaw("{$shop} AS shop_name")
->selectRaw("{$online} AS online_order_no")
->selectRaw("{$internal} AS internal_order_no")
->selectRaw("MAX({$orderTime}) AS order_time")
->selectRaw("{$cost} AS cost_amount")
->selectRaw("{$refund} AS refund_amount")
->selectRaw("{$platformSubsidy} AS platform_subsidy")
->selectRaw("{$quantity} AS product_quantity")
->selectRaw("{$amount} AS product_amount")
->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type")
->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order")
->selectRaw('COUNT(*) AS source_row_count')
->groupBy($columns['shop_name'], $columns['online_order_no'], $columns['internal_order_no'])
->orderByRaw("MAX({$orderTime}) DESC")
->orderByDesc($columns['internal_order_no'])
->forPage($page, $limit + 1)
->get();
$rows = $records->map(static fn (object $record): array => [
'shop_name' => trim((string) ($record->shop_name ?? '')),
'online_order_no' => trim((string) ($record->online_order_no ?? '')),
'internal_order_no' => trim((string) ($record->internal_order_no ?? '')),
'order_time' => $record->order_time,
'cost_amount' => round((float) ($record->cost_amount ?? 0), 2),
'refund_amount' => round((float) ($record->refund_amount ?? 0), 2),
'platform_subsidy' => round((float) ($record->platform_subsidy ?? 0), 2),
'product_quantity' => round((float) ($record->product_quantity ?? 0), 2),
'product_amount' => round((float) ($record->product_amount ?? 0), 2),
'refund_type' => $record->refund_type,
'classification_order' => $record->classification_order,
'source_row_count' => (int) ($record->source_row_count ?? 0),
'matched_package_fee' => null,
'matched_freight_fee' => null,
'matched_bill_fee' => null,
'items' => [],
])->all();
$hasMore = count($rows) > $limit;
$rows = array_slice($rows, 0, $limit);
$this->hydrateItems($rows, $columns);
return ['rows' => $rows, 'has_more' => $hasMore];
}
/** @return list<string> */
public function shopNames(): array
{
$cacheKey = 'order-profit.shop-options:undefined8';
$cached = Cache::get($cacheKey);
if (is_array($cached)) {
return $cached;
}
if (! $this->ready()) {
return [];
}
$columns = $this->columns();
$shopColumn = $columns['shop_name'] ?? null;
if ($shopColumn === null) {
return [];
}
$names = $this->connection()->table(self::TABLE)
->select($shopColumn)
->whereNotNull($shopColumn)
->where($shopColumn, '<>', '')
->distinct()
->orderBy($shopColumn)
->pluck($shopColumn)
->map(static fn (mixed $name): string => trim((string) $name))
->filter()
->values()
->all();
Cache::put($cacheKey, $names, now()->addHours(24));
return $names;
}
/** @return array<string,string> */
private function columns(): array
{
return ($this->fields ?? app(GoodsProfitAfsRepository::class))->fieldColumns();
}
/**
* @param list<string> $orderValues
* @param array<string,string> $columns
* @return list<string>
*/
private function orderNumberIds(array $orderValues, array $columns): array
{
$ids = [];
$single = count($orderValues) === 1 ? $orderValues[0] : null;
foreach (['online_order_no', 'internal_order_no'] as $field) {
if (! isset($columns[$field])) {
continue;
}
if ($field === 'internal_order_no') {
$orderValues = array_values(array_filter(
$orderValues,
static fn (string $value): bool => mb_strlen($value) <= 11,
));
if ($orderValues === []) {
continue;
}
}
$query = $this->connection()->table(self::TABLE)->select('_mirror_key')->limit(10000);
if ($single !== null) {
$query->where($columns[$field], 'like', $single . '%');
} else {
$query->whereIn($columns[$field], $orderValues);
}
foreach ($query->pluck('_mirror_key') as $id) {
$ids[(string) $id] = true;
}
}
return array_keys($ids);
}
/**
* @param array<string,mixed> $filters
* @param array<string,string> $columns
*/
private function filtered(Builder $query, array $filters, array $columns): Builder
{
$dateColumn = $this->queryDateColumn($columns);
$from = trim((string) ($filters['from'] ?? ''));
$to = trim((string) ($filters['to'] ?? ''));
if ($dateColumn !== null && $from !== '') {
$query->where($dateColumn, '>=', $from . ' 00:00:00');
}
if ($dateColumn !== null && $to !== '') {
$query->where($dateColumn, '<=', $to . ' 23:59:59');
}
$shops = self::batchValues((string) ($filters['shop_name'] ?? ''));
if ($shops !== [] && isset($columns['shop_name'])) {
$query->whereIn($columns['shop_name'], $shops);
}
$orderValues = self::batchValues((string) ($filters['order_no'] ?? ''));
if ($orderValues !== []) {
$query->where(function (Builder $order) use ($orderValues, $columns): void {
foreach (['online_order_no', 'internal_order_no'] as $field) {
if (! isset($columns[$field])) {
continue;
}
$order->orWhereIn($columns[$field], $orderValues);
}
});
}
$skuValues = self::batchValues((string) ($filters['sku_id'] ?? ''));
if ($skuValues !== [] && isset($columns['product_code'])) {
$query->whereIn($columns['product_code'], $skuValues);
}
return $query;
}
/**
* @param list<array<string,mixed>> $rows
* @param array<string,string> $columns
*/
private function hydrateItems(array &$rows, array $columns): void
{
if ($rows === []) {
return;
}
$itemFields = array_values(array_filter(
self::ITEM_FIELDS,
static fn (string $field): bool => isset($columns[$field]),
));
if ($itemFields === []) {
return;
}
$onlineValues = array_values(array_filter(array_column($rows, 'online_order_no')));
$internalValues = array_values(array_filter(array_column($rows, 'internal_order_no')));
if ($onlineValues === [] && $internalValues === []) {
return;
}
$query = $this->connection()->table(self::TABLE);
$query->where(function (Builder $orders) use ($columns, $onlineValues, $internalValues): void {
if ($onlineValues !== [] && isset($columns['online_order_no'])) {
$orders->whereIn($columns['online_order_no'], $onlineValues);
}
if ($internalValues !== [] && isset($columns['internal_order_no'])) {
$method = $onlineValues === [] ? 'whereIn' : 'orWhereIn';
$orders->{$method}($columns['internal_order_no'], $internalValues);
}
});
$select = [];
foreach ($itemFields as $field) {
$select[] = $columns[$field] . ' AS ' . $field;
}
$itemsByOrder = [];
foreach ($query->get($select) as $record) {
$item = [];
foreach ($itemFields as $field) {
$value = $record->{$field} ?? null;
$item[$field] = in_array($field, self::NUMERIC_FIELDS, true)
? ($value === null || $value === '' ? null : (float) $value)
: $value;
}
$online = trim((string) ($item['online_order_no'] ?? ''));
$internal = trim((string) ($item['internal_order_no'] ?? ''));
$itemsByOrder[$online . "\0" . $internal][] = $item;
}
foreach ($rows as &$row) {
$key = trim((string) $row['online_order_no']) . "\0" . trim((string) $row['internal_order_no']);
$row['items'] = $itemsByOrder[$key] ?? [];
}
unset($row);
}
/** @param list<string> $fields */
private function sumExpression(array $columns, array $fields): string
{
$parts = [];
foreach ($fields as $field) {
if (isset($columns[$field])) {
$parts[] = 'COALESCE(CAST(NULLIF(TRIM(' . $this->quote($columns[$field])
. "), '') AS DECIMAL(18,2)), 0)";
}
}
return $parts === [] ? '0' : 'SUM(' . implode(' + ', $parts) . ')';
}
private function queryDateColumn(array $columns): ?string
{
return Schema::connection($this->connectionName())->hasColumn(
self::TABLE,
GoodsProfitAfsLookupSchema::DATE_COLUMN,
)
? GoodsProfitAfsLookupSchema::DATE_COLUMN
: ($columns['order_time'] ?? null);
}
private function connection(): ConnectionInterface
{
return DB::connection($this->connectionName());
}
private function connectionName(): string
{
return (string) config('upstream_mirrors.connection', 'business_mirror');
}
private function quote(string $column): string
{
if (preg_match('/^[A-Za-z0-9_]+$/', $column) !== 1) {
throw new \InvalidArgumentException('订单利润字段名不安全。');
}
return '`' . $column . '`';
}
/** @param array<string,mixed> $filters */
private function hasActiveFilters(array $filters): bool
{
foreach (['from', 'to', 'order_no', 'shop_name', 'sku_id'] as $key) {
if (trim((string) ($filters[$key] ?? '')) !== '') {
return true;
}
}
return false;
}
/** @return list<string> */
private static function batchValues(string $value): array
{
$parts = preg_split('/[\s,,;;]+/u', trim($value)) ?: [];
$values = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '') {
$values[$part] = true;
}
if (count($values) >= 100) {
break;
}
}
return array_keys($values);
}
}