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 PHONE_LABEL_REG = /(手机号码|手机号|联系电话|联系方式|电话|手机)/g;
const ADDRESS_LABEL_REG = /(收货地址|详细地址|地址|所在地区|地区)/g;
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
const NAME_LABELS = ['收货人', '收件人', '联系人', '姓名'];
const PHONE_LABELS = ['手机号码', '手机号', '联系电话', '联系方式', '电话', '手机'];
const REGION_LABELS = ['所在地区', '地区'];
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 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 districtRecords = regionData.flatMap((province) =>
(province.children || []).flatMap((city) =>
(city.children || []).map((district) => ({
province,
city,
district,
provinceSearchLabel: normalizeForSearch(province.label),
citySearchLabel: normalizeForSearch(getDisplayCityLabel(city.label)),
districtSearchLabel: normalizeForSearch(district.label),
fullSearchLabel: normalizeForSearch(
`${province.label}${getDisplayCityLabel(city.label)}${district.label}`
)
}))
(city.children || []).map((district) => {
const provinceAliases = createProvinceAliases(province.label);
const cityAliases = createCityAliases(getDisplayCityLabel(city.label));
const districtAliases = createDistrictAliases(district.label);
return {
province,
city,
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 = '') {
return String(value)
.replace(/\r/g, '\n')
@@ -43,6 +72,64 @@ function normalizeForSearch(value = '') {
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 = '') {
return GENERIC_CITY_LABELS.has(label) ? '' : label;
}
@@ -90,17 +177,32 @@ function getLabelParts(regionCodes = []) {
return [province.label, displayCityLabel, district.label].filter(Boolean);
}
function stripFieldLabels(value = '') {
return normalizeText(value).replace(STRIP_FIELD_LABEL_REG, '$1');
}
function cleanupAddressText(value = '') {
return normalizeText(value)
.replace(NAME_LABEL_REG, ' ')
.replace(PHONE_LABEL_REG, ' ')
.replace(ADDRESS_LABEL_REG, ' ')
return stripFieldLabels(value)
.replace(/[:]/g, ' ')
.replace(/[,;]/g, ' ')
.replace(/\s+/g, ' ')
.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 = '') {
const text = normalizeText(rawText);
const match = text.match(PHONE_REG);
@@ -125,12 +227,16 @@ function pickBestMatch(records, matcher) {
.filter(matcher)
.sort((left, right) => {
return (
right.fullSearchLabel.length - left.fullSearchLabel.length ||
right.districtSearchLabel.length - left.districtSearchLabel.length
right.fullSearchLabels[0]?.length - left.fullSearchLabels[0]?.length ||
right.district.label.length - left.district.label.length
);
})[0] || null;
}
function matchAnyAlias(searchText = '', aliases = []) {
return aliases.some((alias) => alias && searchText.includes(alias));
}
function findRegionMatch(rawText = '') {
const searchText = normalizeForSearch(rawText);
if (!searchText) {
@@ -138,16 +244,24 @@ function findRegionMatch(rawText = '') {
}
let match = pickBestMatch(districtRecords, (record) => {
return record.fullSearchLabel && searchText.includes(record.fullSearchLabel);
return record.fullSearchLabels.some((label) => searchText.includes(label));
});
if (!match) {
match = pickBestMatch(districtRecords, (record) => {
return (
record.provinceSearchLabel &&
record.districtSearchLabel &&
searchText.includes(record.provinceSearchLabel) &&
searchText.includes(record.districtSearchLabel)
matchAnyAlias(searchText, record.provinceSearchAliases) &&
matchAnyAlias(searchText, record.districtSearchAliases) &&
(!record.citySearchAliases.length || matchAnyAlias(searchText, record.citySearchAliases))
);
});
}
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) {
const cityLabel = getDisplayCityLabel(match.city.label);
const candidates = [match.province.label, cityLabel, match.district.label]
const candidates = [
findBestAliasInText(text, match.provinceAliases),
findBestAliasInText(text, match.cityAliases),
findBestAliasInText(text, match.districtAliases)
]
.filter(Boolean)
.map((label) => text.indexOf(label))
.filter((index) => index >= 0);
@@ -166,25 +283,26 @@ function getRegionStartIndex(text, match) {
}
function stripMatchedRegion(addressText, match) {
const cityLabel = getDisplayCityLabel(match.city.label);
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)
.forEach((label) => {
detailAddress = detailAddress.replace(label, '');
});
return detailAddress.replace(/^[,;:\s]+/, '').trim();
return cleanupAddressText(detailAddress);
}
function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) {
const sourceText = regionStartIndex > 0
? textWithoutPhone.slice(0, regionStartIndex)
: textWithoutPhone || rawText;
const sourceText =
regionStartIndex > 0 ? textWithoutPhone.slice(0, regionStartIndex) : textWithoutPhone || rawText;
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]) {
return directNameMatch[1].trim();
@@ -196,7 +314,7 @@ function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartInd
.trim();
const tokens = cleaned.split(/\s+/).filter(Boolean);
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 || '';
@@ -223,6 +341,78 @@ function formatRegionCodes(match) {
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 = []) {
const provinceCode =
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 regionMatch = findRegionMatch(textWithoutPhone);
const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1;