CaiWuXiTong/modules/EcommerceFinance/Repositories/ProductMaterialBarcodeMappingRepository.php
chenxi dd8dfddacf 商品成本:按91料号映射规格条码
将91基础料号映射为唯一规格条码后继续沿用三级价格链计算成本;多规格候选保持待核验。
2026-10-25 03:28:05 +08:00

138 lines
5.3 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);
$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 '
. "TRIM(updates->>'materialNo') AS material_no, "
. 'COALESCE(NULLIF(BTRIM("normalizedSku"), \'\'), NULLIF(BTRIM("skuCode"), \'\')) AS sku_code '
. 'FROM public."BarcodePrintImportRecord" '
. "WHERE TRIM(updates->>'materialNo') IN ({$placeholders}) "
. 'AND COALESCE(NULLIF(BTRIM("normalizedSku"), \'\'), NULLIF(BTRIM("skuCode"), \'\')) IS NOT NULL'
. '), barcode_matches AS ('
. 'SELECT DISTINCT material_skus.material_no, '
. "NULLIF(BTRIM(product_rows.updates->>'barcode'), '') AS barcode "
. '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"
. ') '
. 'SELECT barcode_matches.material_no, barcode_matches.barcode AS mapped_sku '
. 'FROM barcode_matches '
. 'UNION '
. 'SELECT material_skus.material_no, material_skus.sku_code '
. 'FROM material_skus '
. 'WHERE NOT EXISTS ('
. 'SELECT 1 FROM barcode_matches '
. 'WHERE barcode_matches.material_no = material_skus.material_no'
. ')',
$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;
}
$candidates[$materialCode][$mappedSku] = true;
}
}
} catch (Throwable $exception) {
throw new RuntimeException(
'91 基础料号条码映射读取失败,请检查商品条码映射库只读连接:' . $exception->getMessage(),
0,
$exception,
);
}
$unique = [];
$ambiguous = [];
$missing = [];
foreach ($materialCodes as $materialCode) {
$mappedSkus = array_keys($candidates[$materialCode] ?? []);
sort($mappedSkus, SORT_STRING);
if (count($mappedSkus) === 1) {
$unique[$materialCode] = $mappedSkus[0];
} elseif (count($mappedSkus) > 1) {
$ambiguous[$materialCode] = $mappedSkus;
} else {
$missing[] = $materialCode;
}
}
return [
'unique' => $unique,
'ambiguous' => $ambiguous,
'missing' => $missing,
'source_rows' => $sourceRows,
];
}
}