Compare commits

...

2 Commits

Author SHA1 Message Date
jacy e2fd28a35b fix(partner): drop store-create SMS verify; keep qualification aspect ratio
CI / verify (pull_request) Has been cancelled
Partner H5 no longer requires store phone SMS on create; qualification overlay uses height auto with widthFix.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 00:34:31 +08:00
jacy 15c35c49de 分享功能和资质文件展示 2026-07-28 00:31:31 +08:00
11 changed files with 97 additions and 200 deletions
+1 -5
View File
@@ -6,7 +6,6 @@ export type StoreDraftForm = {
district: string; district: string;
name: string; name: string;
phone: string; phone: string;
storeSmsCode: string;
address: string; address: string;
/** 门店坐标(定位或地理编码) */ /** 门店坐标(定位或地理编码) */
latitude: string; latitude: string;
@@ -50,7 +49,6 @@ export const defaultStoreForm = (): StoreDraftForm => ({
cityId: '', cityId: '',
name: '', name: '',
phone: '', phone: '',
storeSmsCode: '',
address: '', address: '',
latitude: '', latitude: '',
longitude: '', longitude: '',
@@ -218,7 +216,7 @@ export function patchEnvPhotoAt(urls: string[], index: number, url: string): str
export function validateStoreStep3( export function validateStoreStep3(
form: Pick< form: Pick<
StoreDraftForm, StoreDraftForm,
'bankAccountName' | 'bankAccountNo' | 'bankBranch' | 'phone' | 'storeSmsCode' 'bankAccountName' | 'bankAccountNo' | 'bankBranch' | 'phone'
>, >,
): string | null { ): string | null {
if (!form.bankAccountName.trim()) return '请填写户主姓名'; if (!form.bankAccountName.trim()) return '请填写户主姓名';
@@ -227,7 +225,5 @@ export function validateStoreStep3(
if (!form.bankBranch.trim()) return '请填写开户支行'; if (!form.bankBranch.trim()) return '请填写开户支行';
if (!form.phone.trim()) return '请填写联系电话'; if (!form.phone.trim()) return '请填写联系电话';
if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号'; if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号';
if (!form.storeSmsCode.trim()) return '请输入门店手机号验证码';
if (!/^\d{4,6}$/.test(form.storeSmsCode.trim())) return '验证码格式不正确';
return null; return null;
} }
-8
View File
@@ -8,12 +8,4 @@ export function checkStorePhoneAvailable(phone: string) {
); );
} }
export function sendStorePhoneSms(phone: string) {
return request<{ ok: boolean; maskedPhone: string }>('PARTNER_H5', '/partner/stores/send-phone-sms', {
method: 'POST',
body: JSON.stringify({ phone: phone.trim() }),
silent: true,
});
}
export type { PartnerStorePhoneAvailableResponse }; export type { PartnerStorePhoneAvailableResponse };
+6 -164
View File
@@ -11,7 +11,7 @@ import { toastError, toastSuccess } from '../lib/toast';
import { resolveRegionBinding } from '../lib/china-region'; import { resolveRegionBinding } from '../lib/china-region';
import { checkStorePhoneAvailable, sendStorePhoneSms } from '../lib/storePhone'; import { checkStorePhoneAvailable } from '../lib/storePhone';
import { formatStoreCoords, locateStorePosition } from '../lib/storeLocate'; import { formatStoreCoords, locateStorePosition } from '../lib/storeLocate';
@@ -55,7 +55,6 @@ type StoreCategoryNode = {
type FieldErrors = { type FieldErrors = {
phone?: string; phone?: string;
storeSmsCode?: string;
}; };
@@ -70,7 +69,7 @@ function isPhoneConflictMessage(message: string) {
function isPhoneValidationMessage(message: string) { function isPhoneValidationMessage(message: string) {
return message.includes('联系电话') || message.includes('手机号') || message.includes('验证码'); return message.includes('联系电话') || message.includes('手机号');
} }
@@ -100,10 +99,6 @@ export default function StoreCreatePage() {
const [citiesError, setCitiesError] = useState(''); const [citiesError, setCitiesError] = useState('');
const [smsCooldown, setSmsCooldown] = useState(0);
const [smsHint, setSmsHint] = useState('');
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]); const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
const [locating, setLocating] = useState(false); const [locating, setLocating] = useState(false);
@@ -230,25 +225,13 @@ export default function StoreCreatePage() {
setSubmitError(''); setSubmitError('');
let nextPatch = patch;
if ('phone' in patch) { if ('phone' in patch) {
setFieldErrors((prev) => ({ ...prev, phone: undefined, storeSmsCode: undefined })); setFieldErrors((prev) => ({ ...prev, phone: undefined }));
setSmsHint('');
nextPatch = { ...patch, storeSmsCode: '' };
} }
if ('storeSmsCode' in patch) { setForm((prev) => ({ ...prev, ...patch }));
setFieldErrors((prev) => ({ ...prev, storeSmsCode: undefined }));
}
setForm((prev) => ({ ...prev, ...nextPatch }));
} }
@@ -308,72 +291,6 @@ export default function StoreCreatePage() {
} }
async function sendStorePhoneCode() {
const phone = form.phone.trim();
if (!/^1\d{10}$/.test(phone)) {
setFieldErrors({ phone: '请先填写正确的11位手机号' });
return;
}
setSmsHint('');
setFieldErrors((prev) => ({ ...prev, phone: undefined }));
try {
const phoneCheck = await checkStorePhoneAvailable(phone);
if (!phoneCheck.available) {
setFieldErrors({ phone: phoneCheck.message ?? '该手机号不可用于门店账号' });
return;
}
const res = await sendStorePhoneSms(phone);
setSmsHint(`验证码已发送至 ${res.maskedPhone}`);
setSmsCooldown(60);
const timer = setInterval(() => {
setSmsCooldown((s) => {
if (s <= 1) {
clearInterval(timer);
return 0;
}
return s - 1;
});
}, 1000);
} catch (e) {
const msg = e instanceof Error ? e.message : '验证码发送失败';
setFieldErrors({ phone: msg });
}
}
async function handleNext() { async function handleNext() {
if (step === 1) { if (step === 1) {
@@ -410,11 +327,7 @@ export default function StoreCreatePage() {
if (msg) { if (msg) {
if (isPhoneValidationMessage(msg)) { if (isPhoneValidationMessage(msg)) {
if (msg.includes('验证码')) { setFieldErrors({ phone: msg });
setFieldErrors({ storeSmsCode: msg });
} else {
setFieldErrors({ phone: msg });
}
return; return;
} }
reportFormError(msg); reportFormError(msg);
@@ -510,8 +423,6 @@ export default function StoreCreatePage() {
phone: form.phone.trim(), phone: form.phone.trim(),
smsCode: form.storeSmsCode.trim(),
district: form.district.trim(), district: form.district.trim(),
address: form.address.trim(), address: form.address.trim(),
@@ -561,7 +472,6 @@ export default function StoreCreatePage() {
setForm(defaultStoreForm()); setForm(defaultStoreForm());
setFieldErrors({}); setFieldErrors({});
setSubmitError(''); setSubmitError('');
setSmsHint('');
setParams({ step: '1' }, { replace: true }); setParams({ step: '1' }, { replace: true });
toastSuccess('门店录入成功'); toastSuccess('门店录入成功');
navigate(`/stores/${result.store.id}`); navigate(`/stores/${result.store.id}`);
@@ -571,11 +481,6 @@ export default function StoreCreatePage() {
setFieldErrors({ phone: message }); setFieldErrors({ phone: message });
return; return;
} }
if (/验证码/.test(message)) {
setFieldErrors({ storeSmsCode: message });
goStep(3);
return;
}
setSubmitError(message); setSubmitError(message);
toastError(message); toastError(message);
} finally { } finally {
@@ -1201,74 +1106,11 @@ export default function StoreCreatePage() {
<p className="label-md text-muted" style={{ marginTop: 8 }}> <p className="label-md text-muted" style={{ marginTop: 8 }}>
该手机号将作为门店端登录账号,验证码发送至该号确认后方可提交。 该手机号将作为门店端登录账号。
</p> </p>
</div> </div>
<div className="partner-field">
<label>门店账号验证码 <span className="text-primary">*</span></label>
<div className="partner-input-row">
<div className="partner-input-wrap" style={{ flex: 1 }}>
<span className="material-symbols-outlined partner-input-icon">shield</span>
<input
className="partner-input"
type="text"
inputMode="numeric"
maxLength={6}
placeholder="请输入短信验证码"
value={form.storeSmsCode}
onChange={(e) => patchForm({ storeSmsCode: e.target.value.replace(/\D/g, '') })}
/>
</div>
<button
type="button"
className="partner-code-btn"
disabled={smsCooldown > 0 || submitting}
onClick={() => void sendStorePhoneCode()}
>
{smsCooldown > 0 ? `${smsCooldown}s` : '获取验证码'}
</button>
</div>
{smsHint && (
<p className="label-md text-muted" style={{ marginTop: 8 }}>{smsHint}</p>
)}
{fieldErrors.storeSmsCode && (
<p className="partner-field-error" role="alert">{fieldErrors.storeSmsCode}</p>
)}
</div>
</section> </section>
</> </>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

@@ -2,4 +2,6 @@ export default definePageConfig({
navigationBarTitleText: '我的', navigationBarTitleText: '我的',
enablePullDownRefresh: true, enablePullDownRefresh: true,
backgroundTextStyle: 'dark', backgroundTextStyle: 'dark',
enableShareAppMessage: true,
enableShareTimeline: true,
}); });
+32 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components'; import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import { import {
BRAND_LOGO_MARK_URL, BRAND_LOGO_MARK_URL,
QUALIFICATION_DISCLOSURE_URL, QUALIFICATION_DISCLOSURE_URL,
@@ -9,6 +9,7 @@ import {
} from '@dukang/shared-types'; } from '@dukang/shared-types';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader'; import TabMainHeader from '../../components/TabMainHeader';
import WechatShareReady from '../../components/WechatShareReady';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { goLogin } from '../../lib/auth-nav'; import { goLogin } from '../../lib/auth-nav';
import { bindWechatForUser } from '../../lib/wechat-auth'; import { bindWechatForUser } from '../../lib/wechat-auth';
@@ -22,6 +23,11 @@ import {
} from '../../lib/mini-wechat-profile'; } from '../../lib/mini-wechat-profile';
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api'; import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
import { isWechatEnv } from '../../lib/weixin'; import { isWechatEnv } from '../../lib/weixin';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
const ORDER_SHORTCUTS = [ const ORDER_SHORTCUTS = [
{ tab: 'pending_pay', icon: '付', label: '待付款' }, { tab: 'pending_pay', icon: '付', label: '待付款' },
@@ -132,6 +138,21 @@ export default function MinePage() {
.catch(() => setWxAuthorize(true)); .catch(() => setWxAuthorize(true));
}, []); }, []);
const sharePayload = useMemo(
() => ({
title: '杜康好客 · 我的',
desc: DEFAULT_SHARE_DESC,
path: '/pages/mine/index',
}),
[],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
}));
async function ensureWechatBound(): Promise<boolean> { async function ensureWechatBound(): Promise<boolean> {
if (profile?.hasWechat) return true; if (profile?.hasWechat) return true;
if (!wxAuthorize) { if (!wxAuthorize) {
@@ -279,6 +300,7 @@ export default function MinePage() {
if (!authed) { if (!authed) {
return ( return (
<PageShell variant="tab" className="mine-page no-tab-header"> <PageShell variant="tab" className="mine-page no-tab-header">
<WechatShareReady payload={sharePayload} />
<TabMainHeader title="我的" /> <TabMainHeader title="我的" />
<View className="mine-header"> <View className="mine-header">
<View className="mine-header-texture" /> <View className="mine-header-texture" />
@@ -331,6 +353,7 @@ export default function MinePage() {
return ( return (
<PageShell variant="tab" className="mine-page no-tab-header"> <PageShell variant="tab" className="mine-page no-tab-header">
<WechatShareReady payload={sharePayload} />
<TabMainHeader title="我的" /> <TabMainHeader title="我的" />
<View className="mine-header"> <View className="mine-header">
<View className="mine-header-texture" /> <View className="mine-header-texture" />
@@ -533,7 +556,13 @@ export default function MinePage() {
className="mine-qualification-mask" className="mine-qualification-mask"
onClick={() => setQualificationOpen(false)} onClick={() => setQualificationOpen(false)}
> >
<ScrollView scrollY className="mine-qualification-scroll" enhanced showScrollbar> <ScrollView
scrollY
className="mine-qualification-scroll"
style={{ height: '100%' }}
enhanced
showScrollbar
>
<Image <Image
className="mine-qualification-img" className="mine-qualification-img"
src={QUALIFICATION_DISCLOSURE_URL} src={QUALIFICATION_DISCLOSURE_URL}
@@ -2,4 +2,6 @@ export default definePageConfig({
navigationBarTitleText: '门店', navigationBarTitleText: '门店',
enablePullDownRefresh: true, enablePullDownRefresh: true,
backgroundTextStyle: 'dark', backgroundTextStyle: 'dark',
enableShareAppMessage: true,
enableShareTimeline: true,
}); });
+23 -1
View File
@@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { View, Text, Image, Input } from '@tarojs/components'; import { View, Text, Image, Input } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader'; import TabMainHeader from '../../components/TabMainHeader';
import WechatShareReady from '../../components/WechatShareReady';
import RegionPicker from '../../components/RegionPicker'; import RegionPicker from '../../components/RegionPicker';
import CategoryPicker, { import CategoryPicker, {
EMPTY_CATEGORY, EMPTY_CATEGORY,
@@ -27,6 +28,11 @@ import {
import { FALLBACK_CITY_CODE } from '../../lib/product-images'; import { FALLBACK_CITY_CODE } from '../../lib/product-images';
import { formatDistanceMeters } from '../../lib/geo'; import { formatDistanceMeters } from '../../lib/geo';
import { request, toast } from '../../lib/api'; import { request, toast } from '../../lib/api';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
type Store = { type Store = {
id: string; id: string;
@@ -176,8 +182,24 @@ export default function StoresPage() {
return parts.length ? `营业时间: ${parts.join(',')}` : '营业时间: 10:00-22:00'; return parts.length ? `营业时间: ${parts.join(',')}` : '营业时间: 10:00-22:00';
} }
const sharePayload = useMemo(
() => ({
title: '杜康好客门店',
desc: DEFAULT_SHARE_DESC,
path: '/pages/stores/index',
}),
[],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: '',
}));
return ( return (
<PageShell variant="tab" className="store-page no-tab-header"> <PageShell variant="tab" className="store-page no-tab-header">
<WechatShareReady payload={sharePayload} />
<TabMainHeader title="门店" /> <TabMainHeader title="门店" />
<View className="store-filter"> <View className="store-filter">
+18 -9
View File
@@ -505,31 +505,40 @@
z-index: 1200; z-index: 1200;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; width: 100%;
justify-content: center; height: 100%;
gap: 10px; padding: 0;
padding: 24px 16px; margin: 0;
background: rgba(0, 0, 0, 0.72); background: #000;
box-sizing: border-box; box-sizing: border-box;
} }
.mine-qualification-scroll { .mine-qualification-scroll {
flex: 1;
width: 100%; width: 100%;
max-width: 420px; height: 100%;
max-height: calc(86vh - 36px); max-width: none;
border-radius: 8px; max-height: none;
border-radius: 0;
overflow: hidden; overflow: hidden;
background: #fff; background: #000;
} }
.mine-qualification-img { .mine-qualification-img {
display: block; display: block;
width: 100%; width: 100%;
height: auto;
pointer-events: none; pointer-events: none;
} }
.mine-qualification-hint { .mine-qualification-hint {
position: absolute;
left: 0;
right: 0;
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
z-index: 1;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.82); color: rgba(255, 255, 255, 0.82);
pointer-events: none;
} }
@@ -1,9 +1,11 @@
/** /**
* 上传小程序「资质公示」静态图到 OSS:static/mini-user/qualification-disclosure.png * 上传小程序「资质公示」静态图到 OSS:static/mini-user/qualification-disclosure.png
* 用法(在 server/dukang-api): node scripts/upload-qualification-disclosure.mjs * 用法(在 server/dukang-api):
* node scripts/upload-qualification-disclosure.mjs [本地 png 路径]
* 也可设环境变量 QUALIFICATION_DISCLOSURE_LOCAL。
*/ */
import { readFileSync, existsSync } from 'fs'; import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path'; import { resolve, dirname, isAbsolute } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createRequire } from 'module'; import { createRequire } from 'module';
@@ -12,7 +14,6 @@ const OSS = require('ali-oss');
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const apiRoot = resolve(__dirname, '..'); const apiRoot = resolve(__dirname, '..');
const repoRoot = resolve(apiRoot, '../..');
function loadEnvFile(path) { function loadEnvFile(path) {
if (!existsSync(path)) return {}; if (!existsSync(path)) return {};
@@ -49,10 +50,15 @@ if (!accessKeyId || !accessKeySecret || !bucket) {
process.exit(1); process.exit(1);
} }
const localFile = resolve( const argPath = process.argv[2]?.trim() || env.QUALIFICATION_DISCLOSURE_LOCAL?.trim() || '';
repoRoot, if (!argPath) {
'apps/mini-user/src/assets/qualification-disclosure.png', console.error(
); '请传入本地 png 路径,例如:\n node scripts/upload-qualification-disclosure.mjs D:/tmp/qualification-disclosure.png',
);
process.exit(1);
}
const localFile = isAbsolute(argPath) ? argPath : resolve(process.cwd(), argPath);
if (!existsSync(localFile)) { if (!existsSync(localFile)) {
console.error('本地文件不存在:', localFile); console.error('本地文件不存在:', localFile);
process.exit(1); process.exit(1);
@@ -260,9 +260,6 @@ export class StoreService {
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) { async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
const { primaryId } = await this.resolvePartnerScope(partnerAccountId); const { primaryId } = await this.resolvePartnerScope(partnerAccountId);
const normalizedPhone = String(body.phone).trim(); const normalizedPhone = String(body.phone).trim();
const smsCode = body.smsCode ? String(body.smsCode).trim() : '';
if (!smsCode) throw new BadRequestException('请输入门店手机号验证码');
await this.authService.verifySmsCode(normalizedPhone, smsCode, SmsScene.PARTNER_STORE_OPEN);
const confirmBindExisting = body.confirmBindExisting === true || body.confirmBindExisting === 'true'; const confirmBindExisting = body.confirmBindExisting === true || body.confirmBindExisting === 'true';
await this.assertStorePhoneAvailable(normalizedPhone, confirmBindExisting); await this.assertStorePhoneAvailable(normalizedPhone, confirmBindExisting);