feat(store): separate login phone from public contactPhone

Store.phone stays login/boss; contactPhone is C-end dial. Public store APIs keep phone compat via publicDial mapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 07:50:40 +08:00
parent a598d1cd30
commit 07630c9046
11 changed files with 265 additions and 67 deletions
+37 -8
View File
@@ -368,12 +368,18 @@ export default function StoresPage() {
account?.phone ||
(typeof d.phone === 'string' ? d.phone : undefined);
const storePhone = typeof d.phone === 'string' ? d.phone : undefined;
const contactPhone =
(typeof d.contactPhone === 'string' && d.contactPhone.trim()) ||
storePhone ||
loginPhone ||
'';
const phoneMismatchNow = !!(loginPhone && storePhone && loginPhone !== storePhone);
setPhoneMismatch(phoneMismatchNow ? String(loginPhone) : null);
editForm.setFieldsValue({
name: d.name,
// 以门店手机号为准保存;若与账号登录号不一致,保存时会强制同步到登录账号
phone: storePhone || loginPhone,
// 登录手机号(老板);与 StoreAccount 同步
phone: loginPhone || storePhone,
contactPhone,
intro: d.intro,
benefitUsageRule:
d.benefitUsageRule != null &&
@@ -431,6 +437,7 @@ export default function StoresPage() {
const payload = {
name: v.name,
phone: v.phone,
contactPhone: String(v.contactPhone || '').trim() || v.phone,
coverUrl: v.coverUrl ?? '',
envPhotoUrls: Array.isArray(v.envPhotoUrls)
? v.envPhotoUrls.map((u: string) => String(u || '').trim()).filter(Boolean)
@@ -608,6 +615,7 @@ export default function StoresPage() {
city: values.city,
name: values.name.trim(),
phone: values.phone.trim(),
contactPhone: String(values.contactPhone || values.phone || '').trim(),
district: values.district.trim(),
address: values.address.trim(),
...(values.latitude != null &&
@@ -673,7 +681,13 @@ export default function StoresPage() {
render: (_, row) => row.category?.name || '—',
},
{ title: '城市', dataIndex: 'cityName', width: 80 },
{ title: '电话', dataIndex: 'phone', width: 120 },
{ title: '登录号', dataIndex: 'phone', width: 120 },
{
title: '联系电话',
dataIndex: 'contactPhone',
width: 120,
render: (v: string | null | undefined, row) => v || row.phone,
},
{
title: '营业状态', dataIndex: 'status', width: 90,
render: (s) => <Tag>{STORE_STATUS_LABELS[s] || s}</Tag>,
@@ -884,18 +898,26 @@ export default function StoresPage() {
type="warning"
showIcon
style={{ marginBottom: 16 }}
message={`登录账号手机号仍${phoneMismatch},与门店手机号不一致。请点击右上角「保存修改」同步,否则门店端无法用新号登录`}
message={`主账号登录号${phoneMismatch},与门店登录字段不一致。保存「登录手机号」将同步到门店端登录账号`}
/>
) : null}
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item
name="phone"
label="登录手机号"
label="登录手机号(老板)"
rules={[{ required: true }]}
extra="门店端短信登录使用此号码;修改后需用新号重新登录"
extra="门店端主账号短信登录;修改后需用新号重新登录"
>
<Input />
</Form.Item>
<Form.Item
name="contactPhone"
label="联系电话(店长/对外)"
rules={[{ required: true, message: '请填写对外联系电话' }]}
extra="用户端门店详情展示与拨号使用此号码,可与登录号不同"
>
<Input placeholder="11位手机号" />
</Form.Item>
<Form.Item
name="categoryParentId"
label="门店分类(大类)"
@@ -1171,8 +1193,15 @@ export default function StoresPage() {
<Form.Item name="name" label="门店名称" rules={[{ required: true, message: '请填写门店名称' }]}>
<Input placeholder="请输入门店名称" />
</Form.Item>
<Form.Item name="phone" label="门店手机号(登录账号" rules={[{ required: true, message: '请填写门店手机号' }]}>
<Input placeholder="11位手机号" />
<Form.Item name="phone" label="登录手机号(老板" rules={[{ required: true, message: '请填写登录手机号' }]}>
<Input placeholder="门店端主账号登录" />
</Form.Item>
<Form.Item
name="contactPhone"
label="联系电话(店长/对外)"
extra="用户端拨号展示;留空则与登录号相同"
>
<Input placeholder="11位手机号,可与登录号不同" />
</Form.Item>
<Form.Item name="sortOrder" label="排序" extra="数值越小越靠前">
<InputNumber min={0} precision={0} style={{ width: '100%' }} placeholder="0" />
+12 -3
View File
@@ -5,7 +5,10 @@ export type StoreDraftForm = {
city: string;
district: string;
name: string;
/** 门店登录手机号(老板主账号) */
phone: string;
/** 对外联系电话(店长);可与登录号不同 */
contactPhone: string;
address: string;
/** 门店坐标(定位或地理编码) */
latitude: string;
@@ -51,6 +54,7 @@ export const defaultStoreForm = (): StoreDraftForm => ({
cityId: '',
name: '',
phone: '',
contactPhone: '',
address: '',
latitude: '',
longitude: '',
@@ -102,6 +106,9 @@ export function normalizeStoreDraftForm(raw: Partial<StoreDraftForm> | null | un
...raw,
regionCodes: Array.isArray(raw.regionCodes) ? raw.regionCodes.map(String) : base.regionCodes,
cityId: String(raw.cityId ?? base.cityId),
phone: String(raw.phone ?? base.phone),
contactPhone:
String(raw.contactPhone ?? '').trim() || String(raw.phone ?? '').trim() || base.contactPhone,
latitude: raw.latitude != null && raw.latitude !== '' ? String(raw.latitude) : base.latitude,
longitude: raw.longitude != null && raw.longitude !== '' ? String(raw.longitude) : base.longitude,
openTime: String(raw.openTime ?? base.openTime),
@@ -236,14 +243,16 @@ export function patchEnvPhotoAt(urls: string[], index: number, url: string): str
export function validateStoreStep3(
form: Pick<
StoreDraftForm,
'bankAccountName' | 'bankAccountNo' | 'bankBranch' | 'phone'
'bankAccountName' | 'bankAccountNo' | 'bankBranch' | 'phone' | 'contactPhone'
>,
): string | null {
if (!form.bankAccountName.trim()) return '请填写户主姓名';
if (!form.bankAccountNo.trim()) return '请填写银行卡号';
if (!BANK_RE.test(form.bankAccountNo.replace(/\s/g, ''))) return '银行卡号须为 16~19 位数字';
if (!form.bankBranch.trim()) return '请填写开户支行';
if (!form.phone.trim()) return '请填写联系电话';
if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号';
if (!form.phone.trim()) return '请填写门店登录手机号';
if (!PHONE_RE.test(form.phone.trim())) return '门店登录手机号须为11位手机号';
if (!form.contactPhone.trim()) return '请填写联系电话';
if (!PHONE_RE.test(form.contactPhone.trim())) return '联系电话须为11位手机号';
return null;
}
+56 -5
View File
@@ -56,6 +56,7 @@ type StoreCategoryNode = {
type FieldErrors = {
phone?: string;
contactPhone?: string;
};
@@ -237,9 +238,17 @@ export default function StoreCreatePage() {
setSubmitError('');
if ('phone' in patch) {
if ('phone' in patch || 'contactPhone' in patch) {
setFieldErrors((prev) => ({ ...prev, phone: undefined }));
setFieldErrors((prev) => ({
...prev,
...('phone' in patch ? { phone: undefined } : {}),
...('contactPhone' in patch ? { contactPhone: undefined } : {}),
}));
}
@@ -315,7 +324,9 @@ export default function StoreCreatePage() {
const msg = validateStoreStep3(form);
if (msg) {
if (isPhoneValidationMessage(msg)) {
setFieldErrors({ phone: msg });
setFieldErrors(
msg.includes('联系电话') ? { contactPhone: msg } : { phone: msg },
);
reportStepError(msg);
return;
}
@@ -336,7 +347,9 @@ export default function StoreCreatePage() {
if (msg) {
if (isPhoneValidationMessage(msg)) {
setFieldErrors({ phone: msg });
setFieldErrors(
msg.includes('联系电话') ? { contactPhone: msg } : { phone: msg },
);
return;
}
reportFormError(msg);
@@ -441,6 +454,8 @@ export default function StoreCreatePage() {
phone: form.phone.trim(),
contactPhone: form.contactPhone.trim() || form.phone.trim(),
district: form.district.trim(),
address: form.address.trim(),
@@ -1089,7 +1104,43 @@ export default function StoreCreatePage() {
<p className="label-md text-muted" style={{ marginTop: 8 }}>
</p>
</div>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">phone_in_talk</span>
<input
type="tel"
placeholder="店长或对外可拨打号码"
value={form.contactPhone}
onChange={(e) => patchForm({ contactPhone: e.target.value })}
/>
</div>
{fieldErrors.contactPhone && (
<p className="partner-field-error" role="alert">{fieldErrors.contactPhone}</p>
)}
<p className="label-md text-muted" style={{ marginTop: 8 }}>
使
</p>
+25 -5
View File
@@ -43,7 +43,8 @@ export default function StoreDetailPage() {
const [loadError, setLoadError] = useState('');
const [form, setForm] = useState({
name: '',
phone: '',
loginPhone: '',
contactPhone: '',
address: '',
intro: '',
benefitUsageRule: '',
@@ -64,7 +65,8 @@ export default function StoreDetailPage() {
setStore(data);
setForm({
name: String(data.name || ''),
phone: String(data.phone || ''),
loginPhone: String(data.phone || ''),
contactPhone: String(data.contactPhone || data.phone || ''),
address: String(data.address || ''),
intro: String(data.intro || ''),
benefitUsageRule: (() => {
@@ -164,7 +166,7 @@ export default function StoreDetailPage() {
method: 'PUT',
body: JSON.stringify({
name: form.name.trim(),
phone: form.phone.trim(),
contactPhone: form.contactPhone.trim(),
address: form.address.trim(),
intro: form.intro.trim(),
benefitUsageRule: form.benefitUsageRule.trim() || null,
@@ -346,12 +348,30 @@ export default function StoreDetailPage() {
<label></label>
<input disabled={readOnly} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} style={{ width: '100%', padding: '12px 16px', border: '1px solid rgba(226,190,188,0.5)', borderRadius: 8, fontSize: 16, fontWeight: 500 }} />
</div>
<div className="partner-field">
<label></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">lock</span>
<input disabled type="tel" value={form.loginPhone} readOnly />
</div>
<p className="label-md text-muted" style={{ marginTop: 8 }}>
</p>
</div>
<div className="partner-field">
<label></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">call</span>
<input disabled={readOnly} type="tel" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
<span className="material-symbols-outlined">phone_in_talk</span>
<input
disabled={readOnly}
type="tel"
value={form.contactPhone}
onChange={(e) => setForm({ ...form, contactPhone: e.target.value })}
/>
</div>
<p className="label-md text-muted" style={{ marginTop: 8 }}>
使
</p>
</div>
<div className="partner-field">
<label></label>