feat(customer): 添加客户报备重复检查功能
- 新增 DUP_CHECK_LIMIT 和 DUP_CHECK_MAX_PAGES 常量配置 - 实现三要素重复检查逻辑,支持地址、楼栋号、单元号、房号、姓名、电话等字段组合匹配 - 添加 normalizeText 函数处理文本标准化 - 实现 combinationsOf3 函数生成三个元素的组合 - 添加 checkDuplicateBeforeSubmit 函数执行重复检查 - 在提交前验证重复报备并显示提示信息 - 移除用户页面中的 IsDealer 组件 - 更新用户组件删除实名认证和收货地址相关功能
This commit is contained in:
@@ -26,6 +26,8 @@ const AddShopDealerApply = () => {
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
const [referee, setReferee] = useState<ShopDealerUser>()
|
||||
const PROTECTION_DAYS = 15;
|
||||
const DUP_CHECK_LIMIT = 200;
|
||||
const DUP_CHECK_MAX_PAGES = 50;
|
||||
|
||||
// 房号信息:用 dealerCode 存储唯一键,dealerName 存储展示文案
|
||||
const buildHouseKey = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||
@@ -159,6 +161,107 @@ const AddShopDealerApply = () => {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
type DupKey = 'address' | 'buildingNo' | 'unitNo' | 'roomNo' | 'realName' | 'mobile';
|
||||
const DUP_LABELS: Record<DupKey, string> = {
|
||||
address: '小区名称',
|
||||
buildingNo: '楼栋号',
|
||||
unitNo: '单元号',
|
||||
roomNo: '房号',
|
||||
realName: '姓名',
|
||||
mobile: '电话',
|
||||
};
|
||||
|
||||
const normalizeText = (v: any) => (v ?? '').toString().trim();
|
||||
|
||||
const getApplyDupFields = (apply: ShopDealerApply): Record<DupKey, string> => {
|
||||
const parsed = parseHouseKey(apply.dealerCode);
|
||||
return {
|
||||
address: normalizeText(parsed.community || apply.address),
|
||||
buildingNo: normalizeText(parsed.buildingNo),
|
||||
unitNo: normalizeText(parsed.unitNo),
|
||||
roomNo: normalizeText(parsed.roomNo),
|
||||
realName: normalizeText(apply.realName),
|
||||
mobile: normalizeText(apply.mobile),
|
||||
};
|
||||
};
|
||||
|
||||
const getNewDupFields = (values: any): Record<DupKey, string> => ({
|
||||
address: normalizeText(values.address),
|
||||
buildingNo: normalizeText(values.buildingNo),
|
||||
unitNo: normalizeText(values.unitNo),
|
||||
roomNo: normalizeText(values.roomNo),
|
||||
realName: normalizeText(values.realName),
|
||||
mobile: normalizeText(values.mobile),
|
||||
});
|
||||
|
||||
const combinationsOf3 = <T,>(arr: T[]): [T, T, T][] => {
|
||||
const res: [T, T, T][] = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
for (let k = j + 1; k < arr.length; k++) {
|
||||
res.push([arr[i], arr[j], arr[k]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const findMatchedTriad = (a: Record<DupKey, string>, b: Record<DupKey, string>) => {
|
||||
const availableKeys = (Object.keys(a) as DupKey[]).filter((k) => a[k] !== '');
|
||||
if (availableKeys.length < 3) return null;
|
||||
const triads = combinationsOf3(availableKeys);
|
||||
for (const triad of triads) {
|
||||
if (triad.every((k) => a[k] === b[k] && b[k] !== '')) return triad;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkDuplicateBeforeSubmit = async (values: any, opts?: { skipDealerCode?: string }) => {
|
||||
const inputFields = getNewDupFields(values);
|
||||
const nonEmptyCount = Object.values(inputFields).filter((v) => v !== '').length;
|
||||
if (nonEmptyCount < 3) return null;
|
||||
|
||||
const seen = new Set<number>();
|
||||
const scanPages = async (params: any) => {
|
||||
for (let page = 1; page <= DUP_CHECK_MAX_PAGES; page++) {
|
||||
const res = await pageShopDealerApply({...params, page, limit: DUP_CHECK_LIMIT});
|
||||
const list = res?.list || [];
|
||||
|
||||
for (const item of list) {
|
||||
if (opts?.skipDealerCode && item.dealerCode === opts.skipDealerCode) continue;
|
||||
if (item.applyId && seen.has(item.applyId)) continue;
|
||||
if (item.applyId) seen.add(item.applyId);
|
||||
|
||||
const triad = findMatchedTriad(inputFields, getApplyDupFields(item));
|
||||
if (triad) return {item, triad};
|
||||
}
|
||||
|
||||
if (list.length < DUP_CHECK_LIMIT) break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 优先按手机号(精确)查询,数据量更小
|
||||
if (inputFields.mobile) {
|
||||
const hit = await scanPages({type: 4, mobile: inputFields.mobile});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
// 再按小区关键字查询,覆盖房号相关组合
|
||||
if (inputFields.address) {
|
||||
const hit = await scanPages({type: 4, keywords: inputFields.address});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
// 最后按姓名关键字兜底(用于覆盖不包含“小区/电话”的组合)
|
||||
if (inputFields.realName) {
|
||||
const hit = await scanPages({type: 4, keywords: inputFields.realName});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const submitSucceed = async (values: any) => {
|
||||
|
||||
try {
|
||||
@@ -272,6 +375,20 @@ const AddShopDealerApply = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增报备:提交前做“三要素”重复校验(小区/楼栋/单元/房号/姓名/电话 任一三要素重复提示已报备)
|
||||
if (!isEditMode) {
|
||||
const dup = await checkDuplicateBeforeSubmit(values, {skipDealerCode: houseKey});
|
||||
if (dup) {
|
||||
const triadLabels = dup.triad.map((k: DupKey) => DUP_LABELS[k]).join('、');
|
||||
const existingDisplay = dup.item.dealerName || dup.item.address || '地址未知';
|
||||
Taro.showToast({
|
||||
title: `疑似重复报备:${triadLabels}一致(${existingDisplay}),已报备`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算过期时间
|
||||
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||
|
||||
@@ -2,11 +2,11 @@ import {Cell} from '@nutui/nutui-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ArrowRight, ShieldCheck, LogisticsError, Location, Tips, Ask} from '@nutui/icons-react-taro'
|
||||
import {ArrowRight, LogisticsError, Tips, Ask} from '@nutui/icons-react-taro'
|
||||
import {useUser} from '@/hooks/useUser'
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser, isCertified} = useUser();
|
||||
const {logoutUser} = useUser();
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
@@ -50,37 +50,37 @@ const UserCell = () => {
|
||||
navTo('/user/wallet/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Location size={16}/>
|
||||
<Text className={'pl-3 text-sm'}>收货地址</Text>
|
||||
</View>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/address/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
|
||||
<Text className={'pl-3 text-sm'}>实名认证</Text>
|
||||
{isCertified() && (
|
||||
<Text className={'pl-2 text-xs text-green-500'}>已认证</Text>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/userVerify/index', true)
|
||||
}}
|
||||
/>
|
||||
{/*<Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title={*/}
|
||||
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <Location size={16}/>*/}
|
||||
{/* <Text className={'pl-3 text-sm'}>收货地址</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* }*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/user/address/index', true)*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
{/*<Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title={*/}
|
||||
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>*/}
|
||||
{/* <Text className={'pl-3 text-sm'}>实名认证</Text>*/}
|
||||
{/* {isCertified() && (*/}
|
||||
{/* <Text className={'pl-2 text-xs text-green-500'}>已认证</Text>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/* }*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/user/userVerify/index', true)*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
|
||||
@@ -8,7 +8,6 @@ import {useUser} from "@/hooks/useUser";
|
||||
import {NavBar} from '@nutui/nutui-react-taro';
|
||||
import {Home} from '@nutui/icons-react-taro'
|
||||
import './user.scss'
|
||||
import IsDealer from "./components/IsDealer";
|
||||
|
||||
function User() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
@@ -34,7 +33,6 @@ function User() {
|
||||
}}>
|
||||
<UserCard/>
|
||||
<UserOrder/>
|
||||
<IsDealer/>
|
||||
<UserCell/>
|
||||
<UserFooter/>
|
||||
</div>
|
||||
@@ -70,7 +68,6 @@ function User() {
|
||||
}}>
|
||||
<UserCard/>
|
||||
{/*<UserOrder/>*/}
|
||||
<IsDealer/>
|
||||
<UserCell/>
|
||||
<UserFooter/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user