fix: 优化换货
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m0s

This commit is contained in:
sexygoat
2026-05-14 14:06:52 +08:00
parent b611808316
commit fded2fe12d

View File

@@ -7,30 +7,59 @@ const GENERIC_CITY_LABELS = new Set([
'省直辖县级行政区划' '省直辖县级行政区划'
]); ]);
const NAME_LABEL_REG = /(收货人|收件人|联系人|姓名)/g; const NAME_LABELS = ['收货人', '收件人', '联系人', '姓名'];
const PHONE_LABEL_REG = /(手机号码|手机号|联系电话|联系方式|电话|手机)/g; const PHONE_LABELS = ['手机号码', '手机号', '联系电话', '联系方式', '电话', '手机'];
const ADDRESS_LABEL_REG = /(收货地址|详细地址|地址|所在地区|地区)/g; const REGION_LABELS = ['所在地区', '地区'];
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/; const DETAIL_LABELS = ['详细地址', '收货地址', '地址'];
const ALL_FIELD_LABELS = [
...new Set([...NAME_LABELS, ...PHONE_LABELS, ...REGION_LABELS, ...DETAIL_LABELS])
];
const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/; const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/;
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
const FIELD_LABEL_PATTERN = createLabelPattern(ALL_FIELD_LABELS);
const STRIP_FIELD_LABEL_REG = new RegExp(
`(^|[\\s,;\\n])(?:${FIELD_LABEL_PATTERN})\\s*[:]`,
'g'
);
const provinceOptions = regionData.map(({ label, value }) => ({ label, value })); const provinceOptions = regionData.map(({ label, value }) => ({ label, value }));
const districtRecords = regionData.flatMap((province) => const districtRecords = regionData.flatMap((province) =>
(province.children || []).flatMap((city) => (province.children || []).flatMap((city) =>
(city.children || []).map((district) => ({ (city.children || []).map((district) => {
province, const provinceAliases = createProvinceAliases(province.label);
city, const cityAliases = createCityAliases(getDisplayCityLabel(city.label));
district, const districtAliases = createDistrictAliases(district.label);
provinceSearchLabel: normalizeForSearch(province.label),
citySearchLabel: normalizeForSearch(getDisplayCityLabel(city.label)), return {
districtSearchLabel: normalizeForSearch(district.label), province,
fullSearchLabel: normalizeForSearch( city,
`${province.label}${getDisplayCityLabel(city.label)}${district.label}` district,
) provinceAliases,
})) cityAliases,
districtAliases,
provinceSearchAliases: provinceAliases.map(normalizeForSearch).filter(Boolean),
citySearchAliases: cityAliases.map(normalizeForSearch).filter(Boolean),
districtSearchAliases: districtAliases.map(normalizeForSearch).filter(Boolean),
fullSearchLabels: buildFullSearchLabels(provinceAliases, cityAliases, districtAliases)
};
})
) )
); );
function escapeRegExp(value = '') {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function createLabelPattern(labels = []) {
return labels
.slice()
.sort((left, right) => right.length - left.length)
.map((label) => escapeRegExp(label))
.join('|');
}
function normalizeText(value = '') { function normalizeText(value = '') {
return String(value) return String(value)
.replace(/\r/g, '\n') .replace(/\r/g, '\n')
@@ -43,6 +72,64 @@ function normalizeForSearch(value = '') {
return normalizeText(value).replace(/[\s,.。;::、\-_/]/g, ''); return normalizeText(value).replace(/[\s,.。;::、\-_/]/g, '');
} }
function stripProvinceSuffix(label = '') {
return normalizeText(label).replace(
/(特别行政区|维吾尔自治区|壮族自治区|回族自治区|自治区|省|市)$/u,
''
);
}
function stripCitySuffix(label = '') {
return normalizeText(label).replace(/(自治州|地区|盟|市)$/u, '');
}
function stripDistrictSuffix(label = '') {
return normalizeText(label).replace(/(自治县|自治旗|区|县|旗|市)$/u, '');
}
function createAliases(label = '', stripFn = null) {
const rawLabel = normalizeText(label);
const aliases = new Set([rawLabel]);
const simplifiedLabel = stripFn ? normalizeText(stripFn(rawLabel)) : '';
if (simplifiedLabel && simplifiedLabel !== rawLabel) {
aliases.add(simplifiedLabel);
}
return [...aliases].filter(Boolean);
}
function createProvinceAliases(label = '') {
return createAliases(label, stripProvinceSuffix);
}
function createCityAliases(label = '') {
if (!label) return [''];
return createAliases(label, stripCitySuffix);
}
function createDistrictAliases(label = '') {
return createAliases(label, stripDistrictSuffix);
}
function buildFullSearchLabels(provinceAliases = [], cityAliases = [], districtAliases = []) {
const fullLabels = new Set();
const normalizedCityAliases = cityAliases.length ? cityAliases : [''];
provinceAliases.forEach((provinceAlias) => {
normalizedCityAliases.forEach((cityAlias) => {
districtAliases.forEach((districtAlias) => {
const combinedLabel = normalizeForSearch(`${provinceAlias}${cityAlias}${districtAlias}`);
if (combinedLabel) {
fullLabels.add(combinedLabel);
}
});
});
});
return [...fullLabels];
}
function getDisplayCityLabel(label = '') { function getDisplayCityLabel(label = '') {
return GENERIC_CITY_LABELS.has(label) ? '' : label; return GENERIC_CITY_LABELS.has(label) ? '' : label;
} }
@@ -90,17 +177,32 @@ function getLabelParts(regionCodes = []) {
return [province.label, displayCityLabel, district.label].filter(Boolean); return [province.label, displayCityLabel, district.label].filter(Boolean);
} }
function stripFieldLabels(value = '') {
return normalizeText(value).replace(STRIP_FIELD_LABEL_REG, '$1');
}
function cleanupAddressText(value = '') { function cleanupAddressText(value = '') {
return normalizeText(value) return stripFieldLabels(value)
.replace(NAME_LABEL_REG, ' ')
.replace(PHONE_LABEL_REG, ' ')
.replace(ADDRESS_LABEL_REG, ' ')
.replace(/[:]/g, ' ') .replace(/[:]/g, ' ')
.replace(/[,;]/g, ' ') .replace(/[,;]/g, ' ')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .trim();
} }
function cleanupName(value = '') {
return normalizeText(value)
.replace(/^[\s,;:]+/, '')
.replace(/[\s,;:]+$/, '')
.trim();
}
function findBestAliasInText(text = '', aliases = []) {
return aliases
.slice()
.sort((left, right) => right.length - left.length)
.find((alias) => alias && text.includes(alias)) || '';
}
function extractPhone(rawText = '') { function extractPhone(rawText = '') {
const text = normalizeText(rawText); const text = normalizeText(rawText);
const match = text.match(PHONE_REG); const match = text.match(PHONE_REG);
@@ -125,12 +227,16 @@ function pickBestMatch(records, matcher) {
.filter(matcher) .filter(matcher)
.sort((left, right) => { .sort((left, right) => {
return ( return (
right.fullSearchLabel.length - left.fullSearchLabel.length || right.fullSearchLabels[0]?.length - left.fullSearchLabels[0]?.length ||
right.districtSearchLabel.length - left.districtSearchLabel.length right.district.label.length - left.district.label.length
); );
})[0] || null; })[0] || null;
} }
function matchAnyAlias(searchText = '', aliases = []) {
return aliases.some((alias) => alias && searchText.includes(alias));
}
function findRegionMatch(rawText = '') { function findRegionMatch(rawText = '') {
const searchText = normalizeForSearch(rawText); const searchText = normalizeForSearch(rawText);
if (!searchText) { if (!searchText) {
@@ -138,16 +244,24 @@ function findRegionMatch(rawText = '') {
} }
let match = pickBestMatch(districtRecords, (record) => { let match = pickBestMatch(districtRecords, (record) => {
return record.fullSearchLabel && searchText.includes(record.fullSearchLabel); return record.fullSearchLabels.some((label) => searchText.includes(label));
}); });
if (!match) { if (!match) {
match = pickBestMatch(districtRecords, (record) => { match = pickBestMatch(districtRecords, (record) => {
return ( return (
record.provinceSearchLabel && matchAnyAlias(searchText, record.provinceSearchAliases) &&
record.districtSearchLabel && matchAnyAlias(searchText, record.districtSearchAliases) &&
searchText.includes(record.provinceSearchLabel) && (!record.citySearchAliases.length || matchAnyAlias(searchText, record.citySearchAliases))
searchText.includes(record.districtSearchLabel) );
});
}
if (!match) {
match = pickBestMatch(districtRecords, (record) => {
return (
matchAnyAlias(searchText, record.citySearchAliases) &&
matchAnyAlias(searchText, record.districtSearchAliases)
); );
}); });
} }
@@ -156,8 +270,11 @@ function findRegionMatch(rawText = '') {
} }
function getRegionStartIndex(text, match) { function getRegionStartIndex(text, match) {
const cityLabel = getDisplayCityLabel(match.city.label); const candidates = [
const candidates = [match.province.label, cityLabel, match.district.label] findBestAliasInText(text, match.provinceAliases),
findBestAliasInText(text, match.cityAliases),
findBestAliasInText(text, match.districtAliases)
]
.filter(Boolean) .filter(Boolean)
.map((label) => text.indexOf(label)) .map((label) => text.indexOf(label))
.filter((index) => index >= 0); .filter((index) => index >= 0);
@@ -166,25 +283,26 @@ function getRegionStartIndex(text, match) {
} }
function stripMatchedRegion(addressText, match) { function stripMatchedRegion(addressText, match) {
const cityLabel = getDisplayCityLabel(match.city.label);
let detailAddress = normalizeText(addressText); let detailAddress = normalizeText(addressText);
const matchedProvince = findBestAliasInText(detailAddress, match.provinceAliases);
const matchedCity = findBestAliasInText(detailAddress, match.cityAliases);
const matchedDistrict = findBestAliasInText(detailAddress, match.districtAliases);
[match.province.label, cityLabel, match.district.label] [matchedProvince, matchedCity, matchedDistrict]
.filter(Boolean) .filter(Boolean)
.forEach((label) => { .forEach((label) => {
detailAddress = detailAddress.replace(label, ''); detailAddress = detailAddress.replace(label, '');
}); });
return detailAddress.replace(/^[,;:\s]+/, '').trim(); return cleanupAddressText(detailAddress);
} }
function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) { function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) {
const sourceText = regionStartIndex > 0 const sourceText =
? textWithoutPhone.slice(0, regionStartIndex) regionStartIndex > 0 ? textWithoutPhone.slice(0, regionStartIndex) : textWithoutPhone || rawText;
: textWithoutPhone || rawText;
const directNameMatch = normalizeText(rawText).match( const directNameMatch = normalizeText(rawText).match(
/(?:收货人|收件人|联系人|姓名)\s*[:]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/ /(?:收货人|收件人|联系人|姓名)\s*[:]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/u
); );
if (directNameMatch?.[1]) { if (directNameMatch?.[1]) {
return directNameMatch[1].trim(); return directNameMatch[1].trim();
@@ -196,7 +314,7 @@ function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartInd
.trim(); .trim();
const tokens = cleaned.split(/\s+/).filter(Boolean); const tokens = cleaned.split(/\s+/).filter(Boolean);
const nameToken = tokens.find((token) => { const nameToken = tokens.find((token) => {
return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/.test(token); return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/u.test(token);
}); });
return nameToken || ''; return nameToken || '';
@@ -223,6 +341,78 @@ function formatRegionCodes(match) {
return [match.province.value, match.city.value, match.district.value]; return [match.province.value, match.city.value, match.district.value];
} }
function combineDetailParts(parts = []) {
return parts
.map((item) => cleanupAddressText(item))
.filter(Boolean)
.join('');
}
function extractFieldValue(text = '', labels = []) {
if (!text || !labels.length) {
return '';
}
const labelPattern = createLabelPattern(labels);
const fieldReg = new RegExp(
`(?:${labelPattern})\\s*[:]\\s*([\\s\\S]*?)(?=(?:[\\n,]\\s*)?(?:${FIELD_LABEL_PATTERN})\\s*[:]|$)`,
'u'
);
const match = normalizeText(text).match(fieldReg);
if (!match?.[1]) {
return '';
}
return normalizeText(match[1]).replace(/^[,\s]+|[,\s]+$/g, '');
}
function extractLabeledFields(text = '') {
return {
name: extractFieldValue(text, NAME_LABELS),
phone: extractFieldValue(text, PHONE_LABELS),
region: extractFieldValue(text, REGION_LABELS),
detail: extractFieldValue(text, DETAIL_LABELS)
};
}
function parseFieldBasedShippingText(text = '') {
const fields = extractLabeledFields(text);
const hasAnyField = Object.values(fields).some(Boolean);
if (!hasAnyField) {
return null;
}
const recipientName = cleanupName(fields.name);
const recipientPhone = extractPhone(fields.phone || text).phone;
const regionSource = normalizeText(fields.region);
const detailSource = normalizeText(fields.detail);
let regionMatch = findRegionMatch(regionSource || `${detailSource}${text}`);
if (!regionMatch) {
regionMatch = findRegionMatch(text);
}
const detailParts = [];
if (regionSource) {
const regionRemainder = regionMatch ? stripMatchedRegion(regionSource, regionMatch) : regionSource;
if (regionRemainder) {
detailParts.push(regionRemainder);
}
}
if (detailSource) {
detailParts.push(detailSource);
}
return {
recipient_name: recipientName,
recipient_phone: recipientPhone,
regionCodes: regionMatch ? formatRegionCodes(regionMatch) : [],
detailAddress: combineDetailParts(detailParts)
};
}
export function buildRegionColumns(regionCodes = []) { export function buildRegionColumns(regionCodes = []) {
const provinceCode = const provinceCode =
getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || ''; getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || '';
@@ -291,6 +481,11 @@ export function parseShippingText(rawText = '') {
}; };
} }
const fieldBasedResult = parseFieldBasedShippingText(text);
if (fieldBasedResult) {
return fieldBasedResult;
}
const { phone, textWithoutPhone } = extractPhone(text); const { phone, textWithoutPhone } = extractPhone(text);
const regionMatch = findRegionMatch(textWithoutPhone); const regionMatch = findRegionMatch(textWithoutPhone);
const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1; const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1;