203 lines
8.1 KiB
PHP
203 lines
8.1 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 商品成本使用的基础料号 → 内部商品编码映射。
|
||
*
|
||
* 条码、创建时间读取 product_barcode_catalog;商品启用状态读取系统普通商品资料。
|
||
*/
|
||
class ProductMaterialBarcodeMappingRepository
|
||
{
|
||
public const SOURCE_KEY = 'product_barcode_catalog';
|
||
|
||
/**
|
||
* 「91.C数字.数字」是条码映射库里的基础料号,后面的规格尾缀不参与查询。
|
||
*/
|
||
public static function normalizeMaterialCode(string $code): ?string
|
||
{
|
||
$code = mb_strtoupper(trim($code));
|
||
if (preg_match('/^(91\.C\d+\.\d+)(?:-.*)?$/i', $code, $matches) !== 1) {
|
||
return null;
|
||
}
|
||
|
||
return $matches[1];
|
||
}
|
||
|
||
/**
|
||
* @param list<string> $materialCodes
|
||
* @return array{
|
||
* unique:array<string,string>,
|
||
* ambiguous:array<string,list<string>>,
|
||
* missing:list<string>,
|
||
* source_rows:int
|
||
* }
|
||
*/
|
||
public function resolve(
|
||
array $materialCodes,
|
||
?UpstreamReadOnlyQuery $query = null,
|
||
?callable $enabledSkuLookup = null,
|
||
): array
|
||
{
|
||
$normalized = [];
|
||
foreach ($materialCodes as $code) {
|
||
$materialCode = self::normalizeMaterialCode((string) $code);
|
||
if ($materialCode !== null) {
|
||
$normalized[$materialCode] = true;
|
||
}
|
||
}
|
||
$materialCodes = array_keys($normalized);
|
||
sort($materialCodes, SORT_STRING);
|
||
|
||
if ($materialCodes === []) {
|
||
return [
|
||
'unique' => [],
|
||
'ambiguous' => [],
|
||
'missing' => [],
|
||
'source_rows' => 0,
|
||
];
|
||
}
|
||
|
||
$query ??= new UpstreamReadOnlyQuery(self::SOURCE_KEY, 15);
|
||
$enabledSkuLookup ??= static fn (array $skuCodes): array => app(
|
||
ProductCatalogMirrorRepository::class,
|
||
)->enabledNormalSkuCodes($skuCodes);
|
||
/** @var array<string,array<string,string>> $candidates */
|
||
$candidates = [];
|
||
$sourceRows = 0;
|
||
|
||
try {
|
||
foreach (array_chunk($materialCodes, 200) as $chunk) {
|
||
$placeholders = implode(', ', array_fill(0, count($chunk), '?'));
|
||
$rows = $query->select(
|
||
'WITH material_skus AS ('
|
||
. 'SELECT DISTINCT '
|
||
. "UPPER(BTRIM(updates->>'materialNo')) AS material_no, "
|
||
. 'COALESCE(NULLIF(BTRIM("normalizedSku"), \'\'), NULLIF(BTRIM("skuCode"), \'\')) AS sku_code '
|
||
. 'FROM public."BarcodePrintImportRecord" '
|
||
. "WHERE UPPER(BTRIM(updates->>'materialNo')) IN ({$placeholders}) "
|
||
. 'AND COALESCE(NULLIF(BTRIM("normalizedSku"), \'\'), NULLIF(BTRIM("skuCode"), \'\')) IS NOT NULL'
|
||
. '), barcode_matches AS ('
|
||
. 'SELECT DISTINCT material_skus.material_no, '
|
||
. "UPPER(NULLIF(BTRIM(product_rows.updates->>'barcode'), '')) AS mapped_sku, "
|
||
. 'product_rows."createdAt" AS import_record_created_at '
|
||
. 'FROM material_skus '
|
||
. 'JOIN public."BarcodePrintImportRecord" product_rows '
|
||
. 'ON COALESCE(NULLIF(BTRIM(product_rows."normalizedSku"), \'\'), '
|
||
. 'NULLIF(BTRIM(product_rows."skuCode"), \'\')) = material_skus.sku_code '
|
||
. "WHERE NULLIF(BTRIM(product_rows.updates->>'barcode'), '') IS NOT NULL"
|
||
. '), barcode_candidates AS ('
|
||
. 'SELECT barcode_matches.material_no, barcode_matches.mapped_sku, '
|
||
. 'COALESCE(archive."createdAt", barcode_matches.import_record_created_at) '
|
||
. 'AS candidate_created_at '
|
||
. 'FROM barcode_matches '
|
||
. 'LEFT JOIN public."SupplyChainProductArchive" archive '
|
||
. 'ON UPPER(BTRIM(archive.barcode)) = barcode_matches.mapped_sku '
|
||
. '), base_sku_candidates AS ('
|
||
. 'SELECT material_skus.material_no, '
|
||
. 'UPPER(BTRIM(material_skus.sku_code)) AS mapped_sku, '
|
||
. 'archive."createdAt" AS candidate_created_at '
|
||
. 'FROM material_skus '
|
||
. 'LEFT JOIN public."SupplyChainProductArchive" archive '
|
||
. 'ON UPPER(BTRIM(archive.sku::text)) = UPPER(BTRIM(material_skus.sku_code)) '
|
||
. 'WHERE NOT EXISTS ('
|
||
. 'SELECT 1 FROM barcode_matches '
|
||
. 'WHERE barcode_matches.material_no = material_skus.material_no'
|
||
. ')'
|
||
. ') '
|
||
. 'SELECT material_no, mapped_sku, candidate_created_at '
|
||
. 'FROM barcode_candidates '
|
||
. 'UNION '
|
||
. 'SELECT material_no, mapped_sku, candidate_created_at '
|
||
. 'FROM base_sku_candidates',
|
||
$chunk,
|
||
);
|
||
foreach ($rows as $row) {
|
||
$sourceRows++;
|
||
$materialCode = self::normalizeMaterialCode((string) ($row['material_no'] ?? ''));
|
||
$mappedSku = mb_strtoupper(trim((string) ($row['mapped_sku'] ?? '')));
|
||
if ($materialCode === null || $mappedSku === '') {
|
||
continue;
|
||
}
|
||
$createdAt = trim((string) (
|
||
$row['candidate_created_at']
|
||
?? $row['import_record_created_at']
|
||
?? ''
|
||
));
|
||
if (
|
||
! isset($candidates[$materialCode][$mappedSku])
|
||
|| $createdAt > $candidates[$materialCode][$mappedSku]
|
||
) {
|
||
$candidates[$materialCode][$mappedSku] = $createdAt;
|
||
}
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
throw new RuntimeException(
|
||
'91 基础料号条码映射读取失败,请检查商品条码映射库只读连接:' . $exception->getMessage(),
|
||
0,
|
||
$exception,
|
||
);
|
||
}
|
||
|
||
$candidateSkus = [];
|
||
foreach ($candidates as $mappedCandidates) {
|
||
foreach (array_keys($mappedCandidates) as $mappedSku) {
|
||
$candidateSkus[(string) $mappedSku] = true;
|
||
}
|
||
}
|
||
ksort($candidateSkus, SORT_STRING);
|
||
|
||
try {
|
||
$enabledSkus = $enabledSkuLookup(array_keys($candidateSkus));
|
||
} catch (Throwable $exception) {
|
||
throw new RuntimeException(
|
||
'91 基础料号普通商品状态读取失败:' . $exception->getMessage(),
|
||
0,
|
||
$exception,
|
||
);
|
||
}
|
||
|
||
$unique = [];
|
||
$ambiguous = [];
|
||
$missing = [];
|
||
foreach ($materialCodes as $materialCode) {
|
||
$mappedCandidates = array_filter(
|
||
$candidates[$materialCode] ?? [],
|
||
static fn (string $createdAt, string $mappedSku): bool => isset($enabledSkus[$mappedSku]),
|
||
ARRAY_FILTER_USE_BOTH,
|
||
);
|
||
if ($mappedCandidates === []) {
|
||
$missing[] = $materialCode;
|
||
continue;
|
||
}
|
||
|
||
$latestCreatedAt = max(array_values($mappedCandidates));
|
||
$mappedSkus = [];
|
||
foreach ($mappedCandidates as $mappedSku => $createdAt) {
|
||
if ($createdAt === $latestCreatedAt) {
|
||
$mappedSkus[] = (string) $mappedSku;
|
||
}
|
||
}
|
||
sort($mappedSkus, SORT_STRING);
|
||
|
||
if (count($mappedSkus) === 1) {
|
||
$unique[$materialCode] = $mappedSkus[0];
|
||
} else {
|
||
$ambiguous[$materialCode] = $mappedSkus;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'unique' => $unique,
|
||
'ambiguous' => $ambiguous,
|
||
'missing' => $missing,
|
||
'source_rows' => $sourceRows,
|
||
];
|
||
}
|
||
}
|