CaiWuXiTong/modules/EcommerceFinance/Repositories/CostFreightAnomalyRepository.php
renshen a9c43271d0 fix: 拆分成本与运费异常标记 (#1002)
合并 PR #1002:成本与运费按 0/空分别标记并独立监控。
2026-11-09 10:22:36 +08:00

190 lines
6.6 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\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\ShengsuanOrderProfitRowMapper;
class CostFreightAnomalyRepository
{
private const TABLE = ShengsuanOrderProfitRowMapper::TABLE;
public function ready(): bool
{
try {
$schema = Schema::connection($this->connectionName());
return $schema->hasTable(self::TABLE)
&& $schema->hasColumns(self::TABLE, [
'shop_name', 'online_order_no', 'internal_order_no',
'ship_time', 'product_cost', 'matched_freight_fee',
'matched_package_fee', 'matched_bill_fee',
]);
} catch (\Throwable) {
return false;
}
}
/** @return array{ready:bool,last_synced_at:?string,total:int,shops:list<string>} */
public function status(): array
{
try {
$query = $this->connection()->table(self::TABLE);
return [
'ready' => $this->ready(),
'last_synced_at' => (clone $query)->max('synced_at'),
'total' => (int) (clone $query)->count(),
'shops' => (clone $query)->whereNotNull('shop_name')
->where('shop_name', '<>', '')
->distinct()
->orderBy('shop_name')
->pluck('shop_name')
->map(static fn (mixed $shop): string => trim((string) $shop))
->filter()
->values()
->all(),
];
} catch (\Throwable) {
return [
'ready' => false,
'last_synced_at' => null,
'total' => 0,
'shops' => [],
];
}
}
/**
* @param array<string,mixed> $filters
* @return array{rows:list<array<string,mixed>>,total:int,has_more:bool}
*/
public function paginate(array $filters, int $page, int $limit): array
{
$query = $this->filtered($this->connection()->table(self::TABLE), $filters);
$total = (int) (clone $query)->count();
$rows = $query
->select([
'shop_name', 'online_order_no', 'internal_order_no', 'order_key',
'ship_time', 'product_cost', 'matched_package_fee',
'matched_freight_fee', 'matched_bill_fee', 'synced_at',
])
->orderByDesc('ship_time')
->orderByDesc('order_key')
->forPage(max($page, 1), min(max($limit, 1), 100))
->get()
->map(static function (object $row): array {
$anomalyTypes = self::anomalyTypes($row);
return [
'shop_name' => trim((string) ($row->shop_name ?? '')),
'online_order_no' => trim((string) ($row->online_order_no ?? '')),
'internal_order_no' => trim((string) ($row->internal_order_no ?? '')),
'order_key' => trim((string) ($row->order_key ?? '')),
'ship_time' => $row->ship_time,
'product_cost' => $row->product_cost === null ? null : (float) $row->product_cost,
'matched_package_fee' => $row->matched_package_fee === null ? null : (float) $row->matched_package_fee,
'matched_freight_fee' => $row->matched_freight_fee === null ? null : (float) $row->matched_freight_fee,
'matched_bill_fee' => $row->matched_bill_fee === null ? null : (float) $row->matched_bill_fee,
'synced_at' => $row->synced_at,
'anomaly_types' => $anomalyTypes,
'anomaly_type' => implode('、', $anomalyTypes),
];
})
->all();
return [
'rows' => $rows,
'total' => $total,
'has_more' => $page * min(max($limit, 1), 100) < $total,
];
}
private function filtered(\Illuminate\Database\Query\Builder $query, array $filters): \Illuminate\Database\Query\Builder
{
$query->where(function ($anomaly) {
$anomaly->where(function ($cost) {
$cost->whereNull('product_cost')->orWhere('product_cost', 0);
})->orWhere(function ($freight) {
$freight->whereNull('matched_freight_fee')->orWhere('matched_freight_fee', 0);
});
});
$from = trim((string) ($filters['from'] ?? ''));
$to = trim((string) ($filters['to'] ?? ''));
if ($from !== '') {
$query->where('ship_time', '>=', $from . ' 00:00:00');
}
if ($to !== '') {
$query->where('ship_time', '<=', $to . ' 23:59:59');
}
$shops = array_values(array_unique(array_filter(array_map(
static fn (mixed $value): string => trim((string) $value),
(array) ($filters['shop_names'] ?? []),
))));
if ($shops !== []) {
$query->whereIn('shop_name', array_slice($shops, 0, 50));
}
$orders = $this->batchValues((string) ($filters['order_no'] ?? ''));
if ($orders !== []) {
$query->where(function ($order) use ($orders): void {
$order->whereIn('online_order_no', $orders)
->orWhereIn('internal_order_no', $orders);
});
}
return $query;
}
/** @return list<string> */
private static function anomalyTypes(object $row): array
{
$types = [];
if (self::isZeroOrMissing($row->product_cost)) {
$types[] = '成本异常';
}
if (self::isZeroOrMissing($row->matched_freight_fee)) {
$types[] = '运费异常';
}
return $types;
}
private static function isZeroOrMissing(mixed $value): bool
{
return $value === null || (float) $value === 0.0;
}
/** @return list<string> */
private 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);
}
private function connection(): ConnectionInterface
{
return DB::connection($this->connectionName());
}
private function connectionName(): string
{
return (string) config('upstream_mirrors.connection', 'business_mirror');
}
}