138 lines
6.6 KiB
PHP
138 lines
6.6 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||
use Modules\EcommerceFinance\Services\SupplierBookkeepingShippingImporter as Shipping;
|
||
use RuntimeException;
|
||
|
||
/** 厂家记账只读核价:工作平台月度导出镜像 + 当前普通商品资料成本。 */
|
||
class SupplierBookkeepingValuationRepository
|
||
{
|
||
public function __construct(
|
||
private readonly GenericBusinessMirrorRepository $mirror,
|
||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||
) {}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
public function shipmentsFor(array $rows): array
|
||
{
|
||
if ($rows === []) return [];
|
||
$db = $this->mirror->connection();
|
||
$from = null;
|
||
$to = null;
|
||
$tracking = [];
|
||
$orders = [];
|
||
foreach ($rows as $row) {
|
||
[$start, $end] = self::window($row);
|
||
$from = $from === null || $start < $from ? $start : $from;
|
||
$to = $to === null || $end > $to ? $end : $to;
|
||
$numbers = self::numbers($row['tracking_no'] ?? '');
|
||
if ($numbers !== []) {
|
||
array_push($tracking, ...$numbers);
|
||
} else {
|
||
array_push($orders, ...self::numbers($row['online_order_no'] ?? ''));
|
||
}
|
||
}
|
||
$found = [];
|
||
// 只读每月已完成的代次;单号独立索引,合并订单也按完整单号匹配。
|
||
foreach (['tracking' => $tracking, 'order' => $orders] as $kind => $values) {
|
||
foreach (array_chunk(array_values(array_unique($values)), 150) as $chunk) {
|
||
$query = $db->table(Shipping::REFS . ' AS f')
|
||
->join(Shipping::ROWS . ' AS r', function ($join) {
|
||
$join->on('r.generation', '=', 'f.generation')->on('r.source_row', '=', 'f.source_row');
|
||
})
|
||
->join(Shipping::MONTHS . ' AS m', 'm.active_generation', '=', 'r.generation')
|
||
->where('f.kind', $kind)->whereIn('f.reference', $chunk)
|
||
->where('m.source_month', '>=', substr($from, 0, 10))->where('m.source_month', '<', substr($to, 0, 10))
|
||
->where(function ($q) use ($from, $to) {
|
||
$q->whereNull('r.ship_time')->orWhere(function ($q) use ($from, $to) {
|
||
$q->where('r.ship_time', '>=', $from)->where('r.ship_time', '<', $to);
|
||
});
|
||
})->select('r.*');
|
||
foreach ($query->cursor() as $record) {
|
||
$shipment = (array) $record;
|
||
$shipment['id'] = $record->generation . ':' . $record->source_row;
|
||
$shipment['tracking_numbers'] = array_values(array_unique(array_merge(self::numbers($record->express_no), self::numbers($record->old_express_no))));
|
||
$shipment['order_numbers'] = self::numbers($record->trade_no);
|
||
$shipment['source_id'] = hash('sha256', self::identifier($record->express_no) . '|' . $record->package_no);
|
||
$found[$shipment['id']] = $shipment;
|
||
if (count($found) > 20000) throw new RuntimeException('匹配发货记录过多,须缩小工单范围');
|
||
}
|
||
}
|
||
}
|
||
return array_values($found);
|
||
}
|
||
|
||
public function readyMonths(): array
|
||
{
|
||
return $this->mirror->connection()->table(Shipping::MONTHS)->whereNotNull('active_generation')
|
||
->pluck('source_month')->map(static fn ($date) => substr($date, 0, 7))->all();
|
||
}
|
||
|
||
/** @return array<string,?string> 规范化 SKU => 当前商品资料成本单价 */
|
||
public function costsFor(array $skus): array
|
||
{
|
||
if ($skus === []) return [];
|
||
$dataset = $this->datasets->get(ProductCatalogMirrorRepository::DATASET_KEY);
|
||
if (!$this->mirror->ready($dataset)) {
|
||
throw new RuntimeException('商品资料尚未完成首次同步');
|
||
}
|
||
$db = $this->mirror->connection();
|
||
$table = $dataset->targetTable();
|
||
$values = [];
|
||
foreach ($skus as $sku) {
|
||
$normalized = self::sku($sku);
|
||
foreach ([$normalized, mb_strtolower($normalized), str_replace('+', '_X002B_', $normalized)] as $value) {
|
||
if ($value !== '') $values[$value] = true;
|
||
}
|
||
}
|
||
$result = [];
|
||
$latest = [];
|
||
$exclusions = Schema::connection($db->getName())->hasTable('product_catalog_exclusions');
|
||
foreach (array_chunk(array_keys($values), 150) as $chunk) {
|
||
$query = $db->table($table)->where('sku_type', 'normal')->whereIn('sku_id', $chunk);
|
||
if ($exclusions) {
|
||
$query->whereNotExists(static function ($q) use ($table): void {
|
||
$q->selectRaw('1')->from('product_catalog_exclusions AS e')
|
||
->whereColumn('e.mirror_key', $table . '._mirror_key')
|
||
->whereRaw("e.sku_id_normalized = REPLACE(UPPER(TRIM({$table}.sku_id)), '_X002B_', '+')");
|
||
});
|
||
}
|
||
foreach ($query->orderByDesc('autoid')->get(['autoid', 'sku_id', 'cost_price']) as $record) {
|
||
$key = self::sku($record->sku_id);
|
||
if (!isset($latest[$key]) || (int) $record->autoid > $latest[$key]) {
|
||
$latest[$key] = (int) $record->autoid;
|
||
$result[$key] = $record->cost_price === null ? null : (string) $record->cost_price;
|
||
}
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/** 记账所在月份及前五个月,右端为下月1日(不含)。 */
|
||
public static function window(array $row): array
|
||
{
|
||
$month = Carbon::parse((string) $row['started_at'])->startOfMonth();
|
||
return [$month->copy()->subMonthsNoOverflow(5)->format('Y-m-d H:i:s'), $month->addMonth()->format('Y-m-d H:i:s')];
|
||
}
|
||
|
||
public static function numbers(mixed $value): array
|
||
{
|
||
return array_values(array_unique(array_filter(array_map(static fn ($v) => self::identifier($v), preg_split('/[\r\n,,、;;]+/u', (string) $value) ?: []), static fn ($v) => $v !== '')));
|
||
}
|
||
|
||
public static function sku(mixed $value): string
|
||
{
|
||
return str_replace('_X002B_', '+', mb_strtoupper(self::identifier($value)));
|
||
}
|
||
|
||
/** 只移除单号/商品编码外围的复制附加符,保留内部符号及前导0。 */
|
||
private static function identifier(mixed $value): string
|
||
{
|
||
return preg_replace('/^[\s\p{Cf}\'"‘’“”'"@@]+|[\s\p{Cf}\'"‘’“”'"@@]+$/u', '', (string) $value) ?? trim((string) $value);
|
||
}
|
||
}
|