121 lines
6.0 KiB
PHP
121 lines
6.0 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository as Shipping;
|
||
use Throwable;
|
||
|
||
/** 缺供应商的工单,先还原普通商品编码,再按商品资料补充供应商。 */
|
||
class SupplierBookkeepingProductResolver
|
||
{
|
||
public function __construct(
|
||
private readonly ProductCatalogMirrorRepository $products,
|
||
private readonly Shipping $shipping,
|
||
) {}
|
||
|
||
public function apply(array $rows): array
|
||
{
|
||
$missing = array_filter($rows, static fn ($row) => trim((string) ($row['supplier'] ?? '')) === '');
|
||
if ($missing === []) return $rows;
|
||
try {
|
||
$plans = [];
|
||
$codes = [];
|
||
foreach ($missing as $key => $row) {
|
||
$original = array_map([Shipping::class, 'sku'], Shipping::numbers($row['product_code'] ?? ''));
|
||
$plans[$key] = [];
|
||
foreach ($original as $code) {
|
||
$parts = self::encodedComponents($code);
|
||
$plans[$key][$code] = $parts;
|
||
array_push($codes, $code, ...$parts);
|
||
}
|
||
}
|
||
$catalog = $this->catalog($codes);
|
||
$needsShipping = [];
|
||
foreach ($plans as $key => $plan) {
|
||
// 运营套装的编号本身不含组件,必须有原单发货明细作依据。
|
||
if (count($plan) === 1) {
|
||
$code = (string) array_key_first($plan);
|
||
if (!isset($catalog[$code]) && preg_match('/^(?:MKH-)?运营组合套装/u', $code)) $needsShipping[$key] = $missing[$key];
|
||
}
|
||
}
|
||
if ($needsShipping !== []) {
|
||
$shipments = $this->shipping->shipmentsFor(array_values($needsShipping));
|
||
$extraCodes = [];
|
||
foreach ($needsShipping as $key => $row) {
|
||
$parts = $this->shippedComponents($row, $shipments);
|
||
$plans[$key][array_key_first($plans[$key])] = $parts;
|
||
array_push($extraCodes, ...$parts);
|
||
}
|
||
$catalog += $this->catalog($extraCodes);
|
||
}
|
||
foreach ($plans as $key => $plan) {
|
||
$resolved = [];
|
||
$suppliers = [];
|
||
$unresolved = [];
|
||
$details = [];
|
||
foreach ($plan as $code => $parts) {
|
||
// 普通商品精确命中优先,不拆合法商品编码内部的连字符。
|
||
foreach (isset($catalog[$code]) ? [$code] : ($parts !== [] ? $parts : [$code]) as $part) {
|
||
$resolved[$part] = true;
|
||
$supplier = trim((string) ($catalog[$part]['supplier_name'] ?? ''));
|
||
$details[$part] = $part . '→' . ($supplier !== '' ? $supplier : '未匹配供应商');
|
||
if ($supplier === '') $unresolved[] = $part;
|
||
else $suppliers[$supplier] = true;
|
||
}
|
||
}
|
||
$rows[$key]['resolved_product_code'] = implode("\n", array_keys($resolved));
|
||
$rows[$key]['supplier_match_basis'] = '按商品编码匹配商品资料:' . implode(';', $details);
|
||
// 任一组件未匹配时不使用部分供应商作扣款判定。
|
||
if ($resolved !== [] && $unresolved === []) $rows[$key]['supplier'] = implode("\n", array_keys($suppliers));
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
foreach (array_keys($missing) as $key) $rows[$key]['supplier_match_basis'] = '商品资料或原单发货明细暂不可读取,供应商待确认';
|
||
}
|
||
return $rows;
|
||
}
|
||
|
||
private function catalog(array $codes): array
|
||
{
|
||
$result = [];
|
||
foreach (array_chunk(array_values(array_unique($codes)), 150) as $chunk) {
|
||
foreach ($this->products->normalProductsBySkuCodes($chunk) as $product) {
|
||
$code = Shipping::sku($product->sku_id);
|
||
if (!isset($result[$code]) || (int) $product->autoid > (int) $result[$code]['autoid']) $result[$code] = (array) $product;
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/** 仅识别完整的“ZH-/MKH- 商品编码-数量”结构,组件仍须逐个命中普通商品资料。 */
|
||
private static function encodedComponents(string $code): array
|
||
{
|
||
if (!preg_match('/^(?:ZH-|MKH-)(.+)$/u', $code, $prefix)) return [];
|
||
preg_match_all('/(.+?)-([1-9]\d*)(?:-|$)/u', $prefix[1], $matches, PREG_SET_ORDER);
|
||
if (count($matches) < 2 || implode('', array_column($matches, 0)) !== $prefix[1]) return [];
|
||
return array_values(array_unique(array_map(static fn ($match) => Shipping::sku($match[1]), $matches)));
|
||
}
|
||
|
||
private function shippedComponents(array $row, array $shipments): array
|
||
{
|
||
[$from, $to] = Shipping::window($row);
|
||
$tracking = Shipping::numbers($row['tracking_no'] ?? '');
|
||
$orders = Shipping::numbers($row['online_order_no'] ?? '');
|
||
$wanted = $tracking !== [] ? $tracking : $orders;
|
||
if ($wanted === []) return [];
|
||
$matched = [];
|
||
$codes = [];
|
||
foreach ($shipments as $shipment) {
|
||
$references = $tracking !== [] ? $shipment['tracking_numbers'] : $shipment['order_numbers'];
|
||
if (array_intersect($wanted, $references) === []) continue;
|
||
if ($orders !== [] && array_intersect($orders, $shipment['order_numbers']) === []) continue;
|
||
if ($shipment['ship_time'] === null || $shipment['ship_time'] < $from || $shipment['ship_time'] >= $to) continue;
|
||
if (!in_array(trim((string) ($shipment['unshipped'] ?? '')), ['', '#N/A'], true)) return [];
|
||
array_push($matched, ...$references);
|
||
array_push($codes, ...array_map([Shipping::class, 'sku'], Shipping::numbers($shipment['barcode'])));
|
||
}
|
||
return array_diff($wanted, $matched) === [] ? array_values(array_unique($codes)) : [];
|
||
}
|
||
}
|