CaiWuXiTong/modules/EcommerceFinance/Repositories/ProductMaterialBarcodeMappingRepository.php
chenxi 20de1a28bb 91 料号改用普通商品启用状态 (#680)
91 料号从系统普通商品资料判断启用状态,并用外部条码创建时间选择最新唯一编码。
2026-10-27 11:51:05 +08:00

203 lines
8.1 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 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,
];
}
}