185 lines
7.9 KiB
PHP
185 lines
7.9 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): 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);
|
|
/** @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"
|
|
. '), active_barcode_matches AS ('
|
|
. 'SELECT barcode_matches.material_no, barcode_matches.mapped_sku, '
|
|
. 'archive."createdAt" AS candidate_created_at, '
|
|
. 'archive."businessCreatedAt" AS business_created_at, '
|
|
. 'archive."productStatus" AS product_status, '
|
|
. 'barcode_matches.import_record_created_at '
|
|
. 'FROM barcode_matches '
|
|
. 'JOIN public."SupplyChainProductArchive" archive '
|
|
. 'ON UPPER(BTRIM(archive.barcode)) = barcode_matches.mapped_sku '
|
|
. "WHERE archive.\"productStatus\" = '销售'"
|
|
. '), base_sku_matches AS ('
|
|
. 'SELECT material_skus.material_no, '
|
|
. 'UPPER(BTRIM(archive.sku::text)) AS mapped_sku, '
|
|
. 'archive."createdAt" AS candidate_created_at, '
|
|
. 'archive."businessCreatedAt" AS business_created_at, '
|
|
. 'archive."productStatus" AS product_status, '
|
|
. 'NULL::timestamp AS import_record_created_at '
|
|
. 'FROM material_skus '
|
|
. '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'
|
|
. ') '
|
|
. "AND archive.\"productStatus\" = '销售'"
|
|
. ') '
|
|
. 'SELECT material_no, mapped_sku, candidate_created_at, business_created_at, '
|
|
. 'product_status, import_record_created_at '
|
|
. 'FROM active_barcode_matches '
|
|
. 'UNION '
|
|
. 'SELECT material_no, mapped_sku, candidate_created_at, business_created_at, '
|
|
. 'product_status, import_record_created_at '
|
|
. 'FROM base_sku_matches',
|
|
$chunk,
|
|
);
|
|
foreach ($rows as $row) {
|
|
$sourceRows++;
|
|
$materialCode = self::normalizeMaterialCode((string) ($row['material_no'] ?? ''));
|
|
$mappedSku = mb_strtoupper(trim((string) ($row['mapped_sku'] ?? '')));
|
|
$productStatus = trim((string) ($row['product_status'] ?? ''));
|
|
if ($materialCode === null || $mappedSku === '' || $productStatus !== '销售') {
|
|
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,
|
|
);
|
|
}
|
|
|
|
$unique = [];
|
|
$ambiguous = [];
|
|
$missing = [];
|
|
foreach ($materialCodes as $materialCode) {
|
|
$mappedCandidates = $candidates[$materialCode] ?? [];
|
|
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,
|
|
];
|
|
}
|
|
}
|