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 [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||||
const [referee, setReferee] = useState<ShopDealerUser>()
|
const [referee, setReferee] = useState<ShopDealerUser>()
|
||||||
const PROTECTION_DAYS = 15;
|
const PROTECTION_DAYS = 15;
|
||||||
|
const DUP_CHECK_LIMIT = 200;
|
||||||
|
const DUP_CHECK_MAX_PAGES = 50;
|
||||||
|
|
||||||
// 房号信息:用 dealerCode 存储唯一键,dealerName 存储展示文案
|
// 房号信息:用 dealerCode 存储唯一键,dealerName 存储展示文案
|
||||||
const buildHouseKey = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
const buildHouseKey = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||||
@@ -159,6 +161,107 @@ const AddShopDealerApply = () => {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
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) => {
|
const submitSucceed = async (values: any) => {
|
||||||
|
|
||||||
try {
|
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();
|
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import {Cell} from '@nutui/nutui-react-taro'
|
|||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {View, Text} from '@tarojs/components'
|
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'
|
import {useUser} from '@/hooks/useUser'
|
||||||
|
|
||||||
const UserCell = () => {
|
const UserCell = () => {
|
||||||
const {logoutUser, isCertified} = useUser();
|
const {logoutUser} = useUser();
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
@@ -50,37 +50,37 @@ const UserCell = () => {
|
|||||||
navTo('/user/wallet/index', true)
|
navTo('/user/wallet/index', true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Cell
|
{/*<Cell*/}
|
||||||
className="nutui-cell-clickable"
|
{/* className="nutui-cell-clickable"*/}
|
||||||
title={
|
{/* title={*/}
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||||
<Location size={16}/>
|
{/* <Location size={16}/>*/}
|
||||||
<Text className={'pl-3 text-sm'}>收货地址</Text>
|
{/* <Text className={'pl-3 text-sm'}>收货地址</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
}
|
{/* }*/}
|
||||||
align="center"
|
{/* align="center"*/}
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
navTo('/user/address/index', true)
|
{/* navTo('/user/address/index', true)*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
/>
|
{/*/>*/}
|
||||||
<Cell
|
{/*<Cell*/}
|
||||||
className="nutui-cell-clickable"
|
{/* className="nutui-cell-clickable"*/}
|
||||||
title={
|
{/* title={*/}
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||||
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
|
{/* <ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>*/}
|
||||||
<Text className={'pl-3 text-sm'}>实名认证</Text>
|
{/* <Text className={'pl-3 text-sm'}>实名认证</Text>*/}
|
||||||
{isCertified() && (
|
{/* {isCertified() && (*/}
|
||||||
<Text className={'pl-2 text-xs text-green-500'}>已认证</Text>
|
{/* <Text className={'pl-2 text-xs text-green-500'}>已认证</Text>*/}
|
||||||
)}
|
{/* )}*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
}
|
{/* }*/}
|
||||||
align="center"
|
{/* align="center"*/}
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
navTo('/user/userVerify/index', true)
|
{/* navTo('/user/userVerify/index', true)*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
/>
|
{/*/>*/}
|
||||||
<Cell
|
<Cell
|
||||||
className="nutui-cell-clickable"
|
className="nutui-cell-clickable"
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {useUser} from "@/hooks/useUser";
|
|||||||
import {NavBar} from '@nutui/nutui-react-taro';
|
import {NavBar} from '@nutui/nutui-react-taro';
|
||||||
import {Home} from '@nutui/icons-react-taro'
|
import {Home} from '@nutui/icons-react-taro'
|
||||||
import './user.scss'
|
import './user.scss'
|
||||||
import IsDealer from "./components/IsDealer";
|
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||||
@@ -34,7 +33,6 @@ function User() {
|
|||||||
}}>
|
}}>
|
||||||
<UserCard/>
|
<UserCard/>
|
||||||
<UserOrder/>
|
<UserOrder/>
|
||||||
<IsDealer/>
|
|
||||||
<UserCell/>
|
<UserCell/>
|
||||||
<UserFooter/>
|
<UserFooter/>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +68,6 @@ function User() {
|
|||||||
}}>
|
}}>
|
||||||
<UserCard/>
|
<UserCard/>
|
||||||
{/*<UserOrder/>*/}
|
{/*<UserOrder/>*/}
|
||||||
<IsDealer/>
|
|
||||||
<UserCell/>
|
<UserCell/>
|
||||||
<UserFooter/>
|
<UserFooter/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user