按采购单号+条码+日期、采购单号+条码、条码+数量、快递单号+条码的顺序逐层匹配。 - 每层只处理上一轮剩余原始行 - 后三层允许日期差异 - 跨采购单号匹配时展示双方原单号 Co-authored-by: 苏木 <594326199@qq.com> Co-committed-by: 苏木 <594326199@qq.com>
513 lines
20 KiB
PHP
513 lines
20 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Carbon\CarbonImmutable;
|
||
|
||
/**
|
||
* 厂家对账单 vs 我方结算数据:
|
||
* 按四层顺序匹配,每层只处理上一层剩余的原始行:
|
||
* 1. 采购单号 + 商品编码 + 日期;
|
||
* 2. 采购单号 + 商品编码,忽略日期;
|
||
* 3. 商品编码 + 数量,忽略日期;
|
||
* 4. 快递单号 + 商品编码。
|
||
*
|
||
* 商品编码会比较我方 productCode/skuCode 与厂家条码去掉厂家编号/版本后缀后的值。
|
||
* 匹配过程不删除或改写双方原始行,未匹配行全部保留为厂家多或厂家少。
|
||
*/
|
||
class SupplierStatementMatcher
|
||
{
|
||
public const TYPE_MATCH = '一致';
|
||
|
||
public const TYPE_SUPPLIER_EXTRA = '厂家多';
|
||
|
||
public const TYPE_MISMATCH = '数量/金额不一致';
|
||
|
||
public const TYPE_SUPPLIER_MISSING = '厂家少';
|
||
|
||
/** 厂家条码后缀:供应商编号 + 版本,如 N1097V01、N1269V01。 */
|
||
private const SUPPLIER_SUFFIX = '/[A-Z]{1,3}\d{3,6}V\d{1,3}$/i';
|
||
|
||
private const LAYER_PURCHASE_DATE = 1;
|
||
|
||
private const LAYER_PURCHASE = 2;
|
||
|
||
private const LAYER_BARCODE_QTY = 3;
|
||
|
||
private const LAYER_EXPRESS = 4;
|
||
|
||
/**
|
||
* @param list<array<string,mixed>> $mine 我方数据(expressNo/purchaseNo/date/productCode/unitPrice/qty/amount;productCode 为空才回退货号)
|
||
* @param list<array<string,mixed>> $theirs 厂家数据(expressNo/purchaseNo/createdAt/barcode/unitPrice/qty/amount)
|
||
* @return array{rows:list<array<string,mixed>>,summary:array<string,mixed>}
|
||
*/
|
||
public function compare(array $mine, array $theirs): array
|
||
{
|
||
$mineRemaining = array_values($mine);
|
||
$theirsRemaining = array_values($theirs);
|
||
$pairs = [];
|
||
|
||
foreach ([
|
||
self::LAYER_PURCHASE_DATE,
|
||
self::LAYER_PURCHASE,
|
||
self::LAYER_BARCODE_QTY,
|
||
self::LAYER_EXPRESS,
|
||
] as $layer) {
|
||
$mineGroups = $this->groupForLayer($mineRemaining, true, $layer);
|
||
$theirsGroups = $this->groupForLayer($theirsRemaining, false, $layer);
|
||
$matchedKeys = array_intersect_key($mineGroups, $theirsGroups);
|
||
if ($matchedKeys === []) {
|
||
continue;
|
||
}
|
||
|
||
foreach (array_keys($matchedKeys) as $key) {
|
||
$pairs[] = [
|
||
'mine' => $mineGroups[$key],
|
||
'theirs' => $theirsGroups[$key],
|
||
'layer' => $layer,
|
||
];
|
||
}
|
||
|
||
$mineRemaining = $this->withoutMatchedLayerRows($mineRemaining, true, $layer, $matchedKeys);
|
||
$theirsRemaining = $this->withoutMatchedLayerRows($theirsRemaining, false, $layer, $matchedKeys);
|
||
}
|
||
|
||
$rows = [];
|
||
$summary = [
|
||
'match' => 0,
|
||
'supplierExtra' => 0,
|
||
'mismatch' => 0,
|
||
'supplierMissing' => 0,
|
||
'mineAmount' => 0.0,
|
||
'theirsAmount' => 0.0,
|
||
'supplierOverQty' => 0.0,
|
||
'supplierOverAmount' => 0.0,
|
||
'settleAmount' => 0.0,
|
||
];
|
||
|
||
foreach ($pairs as $pair) {
|
||
$rows[] = $this->comparisonRow($pair['mine'], $pair['theirs'], $pair['layer'], $summary);
|
||
}
|
||
|
||
$mineLeftovers = $this->groupForLayer($mineRemaining, true, self::LAYER_PURCHASE_DATE, true);
|
||
$theirsLeftovers = $this->groupForLayer($theirsRemaining, false, self::LAYER_PURCHASE_DATE, true);
|
||
foreach ($mineLeftovers as $group) {
|
||
$rows[] = $this->comparisonRow($group, null, null, $summary);
|
||
}
|
||
foreach ($theirsLeftovers as $group) {
|
||
$rows[] = $this->comparisonRow(null, $group, null, $summary);
|
||
}
|
||
|
||
usort($rows, static fn (array $a, array $b): int => [
|
||
$a['type'] === self::TYPE_MATCH ? 1 : 0,
|
||
$a['date'],
|
||
$a['purchaseNo'],
|
||
$a['barcode'],
|
||
] <=> [
|
||
$b['type'] === self::TYPE_MATCH ? 1 : 0,
|
||
$b['date'],
|
||
$b['purchaseNo'],
|
||
$b['barcode'],
|
||
]);
|
||
|
||
$summary['mineAmount'] = round($summary['mineAmount'], 2);
|
||
$summary['theirsAmount'] = round($summary['theirsAmount'], 2);
|
||
$summary['supplierOverQty'] = round($summary['supplierOverQty'], 4);
|
||
$summary['supplierOverAmount'] = round($summary['supplierOverAmount'], 2);
|
||
$summary['settleAmount'] = round($summary['settleAmount'], 2);
|
||
$summary['differenceAmount'] = round($summary['mineAmount'] - $summary['settleAmount'], 2);
|
||
|
||
return ['rows' => $rows, 'summary' => $summary];
|
||
}
|
||
|
||
/**
|
||
* 按当前匹配层对原始行归组;同键数量、金额相加,但仍记录来源行数和全部日期/采购单号。
|
||
*
|
||
* @param list<array<string,mixed>> $rows
|
||
* @return array<string,array<string,mixed>>
|
||
*/
|
||
private function groupForLayer(array $rows, bool $mineSide, int $layer, bool $singletonFallback = false): array
|
||
{
|
||
$grouped = [];
|
||
foreach ($rows as $index => $row) {
|
||
$key = $this->matchLayerKey($row, $mineSide, $layer);
|
||
if ($key === null) {
|
||
if (! $singletonFallback) {
|
||
continue;
|
||
}
|
||
$key = "source\0" . $layer . "\0" . $index;
|
||
}
|
||
|
||
$barcode = $this->barcodeValue($row, $mineSide ? ['productCode', 'skuCode'] : ['barcode']);
|
||
$barcodeKey = $this->normalizeBarcode($barcode);
|
||
$purchaseNo = $this->normalizePurchaseNo($row['purchaseNo'] ?? '');
|
||
$date = $this->normalizeDate($row[$mineSide ? 'date' : 'createdAt'] ?? '');
|
||
$expressNo = trim((string) $this->firstValue($row, ['expressNo', 'express_no']));
|
||
$unitPrice = (float) ($row['unitPrice'] ?? 0);
|
||
|
||
$grouped[$key] ??= [
|
||
'purchaseNo' => '',
|
||
'purchaseNos' => [],
|
||
'date' => '',
|
||
'dates' => [],
|
||
'dateCount' => 0,
|
||
'dateEnd' => '',
|
||
'barcode' => $barcode,
|
||
'barcodeKey' => $barcodeKey,
|
||
'supplierBarcode' => $mineSide ? '' : $barcode,
|
||
'expressNo' => '',
|
||
'expressNos' => [],
|
||
'unitPrice' => 0.0,
|
||
'unitPrices' => [],
|
||
'qty' => 0.0,
|
||
'amount' => 0.0,
|
||
'sourceRows' => 0,
|
||
];
|
||
|
||
if ($purchaseNo !== '') {
|
||
$grouped[$key]['purchaseNos'][$purchaseNo] = true;
|
||
}
|
||
if ($date !== '') {
|
||
$grouped[$key]['dates'][$date] = true;
|
||
}
|
||
if ($expressNo !== '') {
|
||
$grouped[$key]['expressNos'][$expressNo] = true;
|
||
}
|
||
if ($unitPrice > 0) {
|
||
$grouped[$key]['unitPrices'][number_format($unitPrice, 4, '.', '')] = true;
|
||
}
|
||
if ($grouped[$key]['barcode'] === '' && $barcode !== '') {
|
||
$grouped[$key]['barcode'] = $barcode;
|
||
}
|
||
if (! $mineSide && $grouped[$key]['supplierBarcode'] === '' && $barcode !== '') {
|
||
$grouped[$key]['supplierBarcode'] = $barcode;
|
||
}
|
||
|
||
$grouped[$key]['qty'] += (float) ($row['qty'] ?? 0);
|
||
$grouped[$key]['amount'] += (float) ($row['amount'] ?? 0);
|
||
$grouped[$key]['sourceRows']++;
|
||
}
|
||
|
||
foreach ($grouped as &$group) {
|
||
$purchaseNos = array_keys((array) $group['purchaseNos']);
|
||
sort($purchaseNos, SORT_NATURAL);
|
||
$group['purchaseNos'] = $purchaseNos;
|
||
$group['purchaseNo'] = (string) ($purchaseNos[0] ?? '');
|
||
|
||
$dates = array_keys((array) $group['dates']);
|
||
sort($dates);
|
||
$group['dates'] = $dates;
|
||
$group['date'] = (string) ($dates[0] ?? '');
|
||
$group['dateCount'] = count($dates);
|
||
$group['dateEnd'] = count($dates) > 1 ? (string) $dates[array_key_last($dates)] : '';
|
||
|
||
$expressNos = array_keys((array) $group['expressNos']);
|
||
sort($expressNos);
|
||
$group['expressNos'] = $expressNos;
|
||
$group['expressNo'] = (string) ($expressNos[0] ?? '');
|
||
|
||
$group = $this->finalizeGroup($group);
|
||
}
|
||
unset($group);
|
||
|
||
return $grouped;
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows @param array<string,mixed> $matchedKeys */
|
||
private function withoutMatchedLayerRows(array $rows, bool $mineSide, int $layer, array $matchedKeys): array
|
||
{
|
||
$remaining = [];
|
||
foreach ($rows as $row) {
|
||
$key = $this->matchLayerKey($row, $mineSide, $layer);
|
||
if ($key !== null && isset($matchedKeys[$key])) {
|
||
continue;
|
||
}
|
||
$remaining[] = $row;
|
||
}
|
||
|
||
return $remaining;
|
||
}
|
||
|
||
/** @param array<string,mixed> $row */
|
||
private function matchLayerKey(array $row, bool $mineSide, int $layer): ?string
|
||
{
|
||
$barcode = $this->barcodeValue($row, $mineSide ? ['productCode', 'skuCode'] : ['barcode']);
|
||
if ($barcode === '') {
|
||
return null;
|
||
}
|
||
|
||
$barcodeKey = $this->normalizeBarcode($barcode);
|
||
$purchaseNo = $this->normalizePurchaseNo($row['purchaseNo'] ?? '');
|
||
$date = $this->normalizeDate($row[$mineSide ? 'date' : 'createdAt'] ?? '');
|
||
$qtyRaw = trim((string) ($row['qty'] ?? ''));
|
||
$expressNo = $this->normalizeExpressNo($this->firstValue($row, ['expressNo', 'express_no']));
|
||
|
||
return match ($layer) {
|
||
self::LAYER_PURCHASE_DATE => $purchaseNo !== ''
|
||
? "purchase-date\0" . $purchaseNo . "\0" . $barcodeKey . "\0" . $date
|
||
: null,
|
||
self::LAYER_PURCHASE => $purchaseNo !== ''
|
||
? "purchase\0" . $purchaseNo . "\0" . $barcodeKey
|
||
: null,
|
||
self::LAYER_BARCODE_QTY => $qtyRaw !== ''
|
||
? "barcode-qty\0" . $barcodeKey . "\0" . number_format((float) $qtyRaw, 4, '.', '')
|
||
: null,
|
||
self::LAYER_EXPRESS => $expressNo !== ''
|
||
? "express\0" . $expressNo . "\0" . $barcodeKey
|
||
: null,
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
/** @param array<string,mixed> $row @param list<string> $fields */
|
||
private function barcodeValue(array $row, array $fields): string
|
||
{
|
||
return $this->firstValue($row, $fields);
|
||
}
|
||
|
||
/** @param array<string,mixed> $row @param list<string> $fields */
|
||
private function firstValue(array $row, array $fields): string
|
||
{
|
||
foreach ($fields as $field) {
|
||
$value = trim((string) ($row[$field] ?? ''));
|
||
if ($value !== '') {
|
||
return $value;
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed>|null $mineRow
|
||
* @param array<string,mixed>|null $theirsRow
|
||
* @param array<string,int|float> $summary
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function comparisonRow(?array $mineRow, ?array $theirsRow, ?int $matchedLayer, array &$summary): array
|
||
{
|
||
$minePurchaseNos = array_values((array) ($mineRow['purchaseNos'] ?? []));
|
||
$theirsPurchaseNos = array_values((array) ($theirsRow['purchaseNos'] ?? []));
|
||
$minePurchaseNo = implode('、', $minePurchaseNos);
|
||
$theirsPurchaseNo = implode('、', $theirsPurchaseNos);
|
||
|
||
$purchaseNo = $minePurchaseNo !== '' ? $minePurchaseNo : $theirsPurchaseNo;
|
||
$date = (string) ($mineRow['date'] ?? $theirsRow['date'] ?? '');
|
||
$theirsDate = (string) ($theirsRow['date'] ?? '');
|
||
$barcode = (string) ($mineRow['barcode'] ?? $theirsRow['barcode'] ?? '');
|
||
$supplierBarcode = (string) ($theirsRow['supplierBarcode'] ?? '');
|
||
$expressNo = (string) (
|
||
($theirsRow['expressNo'] ?? '') !== ''
|
||
? $theirsRow['expressNo']
|
||
: ($mineRow['expressNo'] ?? '')
|
||
);
|
||
$mineUnitPrice = (float) ($mineRow['unitPrice'] ?? 0);
|
||
$theirsUnitPrice = (float) ($theirsRow['unitPrice'] ?? 0);
|
||
$unitPrice = $mineUnitPrice > 0 ? $mineUnitPrice : $theirsUnitPrice;
|
||
$mineQty = (float) ($mineRow['qty'] ?? 0);
|
||
$mineAmount = (float) ($mineRow['amount'] ?? 0);
|
||
$theirsQty = (float) ($theirsRow['qty'] ?? 0);
|
||
$theirsAmount = (float) ($theirsRow['amount'] ?? 0);
|
||
$qtyDifference = $theirsQty - $mineQty;
|
||
$amountDifference = $theirsAmount - $mineAmount;
|
||
$quantityDifference = $this->describeDifference($qtyDifference, $amountDifference);
|
||
$dateDifferenceDays = $this->dateDifferenceDays($date, $theirsDate);
|
||
$matchNote = $matchedLayer === null
|
||
? ''
|
||
: $this->matchLayerNote(
|
||
$matchedLayer,
|
||
$minePurchaseNos,
|
||
$theirsPurchaseNos,
|
||
$date,
|
||
$theirsDate,
|
||
);
|
||
|
||
if ($mineRow === null) {
|
||
$type = self::TYPE_SUPPLIER_EXTRA;
|
||
$settle = 0.0;
|
||
$note = '厂家整条多报:' . $quantityDifference . ';四层匹配后仍未找到我方对应记录,不结算';
|
||
$summary['supplierExtra']++;
|
||
} elseif ($theirsRow === null) {
|
||
$type = self::TYPE_SUPPLIER_MISSING;
|
||
$settle = 0.0;
|
||
$note = '厂家漏报:我方数量 ' . $this->number($mineQty)
|
||
. ',单价 ¥' . number_format($mineUnitPrice, 2)
|
||
. ',金额 ¥' . number_format($mineAmount, 2)
|
||
. ';四层匹配后仍未找到厂家对应记录,按厂家数据结算(不付款)';
|
||
$summary['supplierMissing']++;
|
||
} elseif (abs($mineAmount - $theirsAmount) < 0.005 && abs($mineQty - $theirsQty) < 0.005) {
|
||
$type = self::TYPE_MATCH;
|
||
$settle = $mineAmount;
|
||
$note = $matchNote . '数量、金额一致,正常结算';
|
||
$summary['match']++;
|
||
} else {
|
||
$type = self::TYPE_MISMATCH;
|
||
$settle = min($mineAmount, $theirsAmount);
|
||
$note = $matchNote . $quantityDifference . ';按金额低的一方结算,请厂家修改';
|
||
$summary['mismatch']++;
|
||
}
|
||
|
||
$summary['mineAmount'] += $mineAmount;
|
||
$summary['theirsAmount'] += $theirsAmount;
|
||
$summary['supplierOverQty'] += max($qtyDifference, 0);
|
||
$summary['supplierOverAmount'] += max($amountDifference, 0);
|
||
$summary['settleAmount'] += $settle;
|
||
|
||
return [
|
||
'purchaseNo' => $purchaseNo,
|
||
'minePurchaseNo' => $minePurchaseNo,
|
||
'theirsPurchaseNo' => $theirsPurchaseNo,
|
||
'purchaseNoDifference' => $minePurchaseNo !== ''
|
||
&& $theirsPurchaseNo !== ''
|
||
&& $minePurchaseNo !== $theirsPurchaseNo,
|
||
'date' => $date,
|
||
'mineDate' => (string) ($mineRow['date'] ?? ''),
|
||
'theirsDate' => $theirsDate,
|
||
'dateDifference' => $date !== '' && $theirsDate !== '' && $date !== $theirsDate,
|
||
'barcode' => $barcode,
|
||
'supplierBarcode' => $supplierBarcode !== '' && $supplierBarcode !== $barcode ? $supplierBarcode : '',
|
||
'expressNo' => $expressNo,
|
||
'unitPrice' => $unitPrice,
|
||
'mineUnitPrice' => $mineUnitPrice,
|
||
'theirsUnitPrice' => $theirsUnitPrice,
|
||
'mineQty' => $mineQty,
|
||
'mineAmount' => $mineAmount,
|
||
'theirsQty' => $theirsQty,
|
||
'theirsAmount' => $theirsAmount,
|
||
'qtyDifference' => round($qtyDifference, 4),
|
||
'amountDifference' => round($amountDifference, 2),
|
||
'supplierOverQty' => round(max($qtyDifference, 0), 4),
|
||
'supplierOverAmount' => round(max($amountDifference, 0), 2),
|
||
'type' => $type,
|
||
'settleAmount' => $settle,
|
||
'dateToleranceDays' => $dateDifferenceDays ?? 0,
|
||
'dateDifferenceDays' => $dateDifferenceDays,
|
||
'dateCount' => max((int) ($mineRow['dateCount'] ?? 0), (int) ($theirsRow['dateCount'] ?? 0)),
|
||
'dateEnd' => (string) ($mineRow['dateEnd'] ?? $theirsRow['dateEnd'] ?? ''),
|
||
'matchedByLayer' => $matchedLayer,
|
||
'note' => $note,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param list<string> $minePurchaseNos
|
||
* @param list<string> $theirsPurchaseNos
|
||
*/
|
||
private function matchLayerNote(
|
||
int $layer,
|
||
array $minePurchaseNos,
|
||
array $theirsPurchaseNos,
|
||
string $mineDate,
|
||
string $theirsDate,
|
||
): string {
|
||
$dateNote = '';
|
||
if ($mineDate !== '' || $theirsDate !== '') {
|
||
$dateNote = '(我方日期 '
|
||
. ($mineDate !== '' ? $mineDate : '空')
|
||
. ' / 厂家日期 '
|
||
. ($theirsDate !== '' ? $theirsDate : '空')
|
||
. ')';
|
||
}
|
||
|
||
return match ($layer) {
|
||
self::LAYER_PURCHASE_DATE => '',
|
||
self::LAYER_PURCHASE => '采购单号+商品编码匹配,已忽略日期差异' . $dateNote . ';',
|
||
self::LAYER_BARCODE_QTY => (
|
||
implode('、', $minePurchaseNos) !== implode('、', $theirsPurchaseNos)
|
||
? '采购单号不一致(我方 '
|
||
. (implode('、', $minePurchaseNos) ?: '空')
|
||
. ' / 厂家 '
|
||
. (implode('、', $theirsPurchaseNos) ?: '空')
|
||
. '),'
|
||
: ''
|
||
) . '商品编码+数量匹配,已忽略日期差异' . $dateNote . ';',
|
||
self::LAYER_EXPRESS => '快递单号+商品编码匹配;',
|
||
default => '',
|
||
};
|
||
}
|
||
|
||
private function dateDifferenceDays(string $left, string $right): ?int
|
||
{
|
||
try {
|
||
$leftDate = CarbonImmutable::createFromFormat('!Y-m-d', $left);
|
||
$rightDate = CarbonImmutable::createFromFormat('!Y-m-d', $right);
|
||
} catch (\Throwable) {
|
||
return null;
|
||
}
|
||
if (! $leftDate instanceof CarbonImmutable || ! $rightDate instanceof CarbonImmutable) {
|
||
return null;
|
||
}
|
||
|
||
return (int) $leftDate->diffInDays($rightDate);
|
||
}
|
||
|
||
/** @param array<string,mixed> $group @return array<string,mixed> */
|
||
private function finalizeGroup(array $group): array
|
||
{
|
||
$prices = array_keys((array) ($group['unitPrices'] ?? []));
|
||
if (count($prices) === 1) {
|
||
$group['unitPrice'] = (float) $prices[0];
|
||
} elseif ((float) $group['qty'] != 0.0) {
|
||
$group['unitPrice'] = round((float) $group['amount'] / (float) $group['qty'], 4);
|
||
}
|
||
|
||
return $group;
|
||
}
|
||
|
||
private function normalizePurchaseNo(mixed $value): string
|
||
{
|
||
$purchaseNo = ltrim(trim((string) $value), "'");
|
||
|
||
return preg_match('/^(\d+)\.0+$/', $purchaseNo, $matches) === 1 ? $matches[1] : $purchaseNo;
|
||
}
|
||
|
||
private function normalizeExpressNo(mixed $value): string
|
||
{
|
||
$expressNo = preg_replace('/\s+/u', '', trim((string) $value)) ?? '';
|
||
$expressNo = ltrim($expressNo, "'@");
|
||
|
||
return preg_match('/^(\d+)\.0+$/', $expressNo, $matches) === 1 ? $matches[1] : strtoupper($expressNo);
|
||
}
|
||
|
||
private function normalizeDate(mixed $value): string
|
||
{
|
||
$raw = trim((string) $value);
|
||
if (preg_match('/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})/', $raw, $matches) === 1) {
|
||
$year = (int) $matches[1];
|
||
$month = (int) $matches[2];
|
||
$day = (int) $matches[3];
|
||
if (checkdate($month, $day, $year)) {
|
||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||
}
|
||
}
|
||
|
||
return $raw;
|
||
}
|
||
|
||
private function describeDifference(float $qtyDifference, float $amountDifference): string
|
||
{
|
||
$parts = [];
|
||
if (abs($qtyDifference) >= 0.005) {
|
||
$parts[] = '厂家数量' . ($qtyDifference > 0 ? '多 ' : '少 ') . $this->number(abs($qtyDifference));
|
||
}
|
||
if (abs($amountDifference) >= 0.005) {
|
||
$parts[] = '厂家金额' . ($amountDifference > 0 ? '多 ¥' : '少 ¥') . number_format(abs($amountDifference), 2);
|
||
}
|
||
|
||
return $parts === [] ? '数量、金额一致' : implode(',', $parts);
|
||
}
|
||
|
||
private function number(float $value): string
|
||
{
|
||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||
}
|
||
|
||
/** 去掉条码末尾的「厂家编号 + 版本」后缀,双方口径统一后再匹配。 */
|
||
private function normalizeBarcode(string $barcode): string
|
||
{
|
||
$normalized = preg_replace(self::SUPPLIER_SUFFIX, '', $barcode);
|
||
|
||
return strtoupper($normalized === null || $normalized === '' ? $barcode : $normalized);
|
||
}
|
||
}
|