From b32e2a06eb2cb9aa83c588443fdfc46417264cea Mon Sep 17 00:00:00 2001 From: ljy Date: Thu, 9 Jul 2026 23:52:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix;=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AD=90=E8=B4=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/PartnerAccountsPage.tsx | 70 ++++++- .../src/components/OssUploadField.tsx | 31 +-- apps/h5-partner/src/lib/api.ts | 5 +- apps/h5-partner/src/lib/staff.ts | 7 - apps/h5-partner/src/lib/storeDraft.ts | 8 +- apps/h5-partner/src/lib/upload-lock.ts | 11 ++ apps/h5-partner/src/lib/upload.ts | 69 ++++--- apps/h5-partner/src/pages/LoginPage.tsx | 18 +- apps/h5-partner/src/pages/StaffCreatePage.tsx | 186 +++++++++--------- apps/h5-partner/src/pages/StaffListPage.tsx | 19 +- apps/h5-partner/src/pages/StoreCreatePage.tsx | 98 ++++++--- apps/h5-partner/src/pages/StoreDetailPage.tsx | 12 +- packages/shared-types/src/partner.ts | 2 - .../src/modules/iam/dto/partner-staff.dto.ts | 8 - .../src/modules/iam/partner-staff.service.ts | 12 +- .../src/modules/ops/admin-partners.service.ts | 9 +- .../src/modules/ops/dto/admin-query.dto.ts | 4 + 17 files changed, 344 insertions(+), 225 deletions(-) create mode 100644 apps/h5-partner/src/lib/upload-lock.ts diff --git a/apps/admin-web/src/pages/PartnerAccountsPage.tsx b/apps/admin-web/src/pages/PartnerAccountsPage.tsx index 8fb3d31..3d21ab4 100644 --- a/apps/admin-web/src/pages/PartnerAccountsPage.tsx +++ b/apps/admin-web/src/pages/PartnerAccountsPage.tsx @@ -3,12 +3,23 @@ import { Button, Drawer, Form, Input, Modal, Select, Space, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; +import { PARTNER_STAFF_ROLE_LABELS } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; +type ParentAccount = { id: string; name: string; phone: string }; + type Row = { - id: string; phone: string; name: string; status: string; isPrimary: number; createdAt: string; + id: string; + phone: string; + name: string; + status: string; + isPrimary: number; + staffRole?: string | null; + parentAccountId?: string | null; + parent?: ParentAccount | null; + createdAt: string; partner?: { id: string; companyName: string }; }; @@ -26,6 +37,21 @@ type PartnerOption = { id: string; companyName: string }; type Detail = Row & { bills?: BillRow[]; orders?: OrderRow[] }; +const ACCOUNT_TYPE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '主账号' }, + { value: '0', label: '子账号' }, +]; + +function accountTypeLabel(isPrimary: number) { + return isPrimary === 1 ? '主账号' : '子账号'; +} + +function staffRoleLabel(role?: string | null) { + if (!role) return '-'; + return PARTNER_STAFF_ROLE_LABELS[role as keyof typeof PARTNER_STAFF_ROLE_LABELS] ?? role; +} + export default function PartnerAccountsPage() { const [form] = Form.useForm(); const [editForm] = Form.useForm(); @@ -37,6 +63,7 @@ export default function PartnerAccountsPage() { const qs = new URLSearchParams(); if (filters.phone) qs.set('phone', filters.phone); if (filters.status) qs.set('status', filters.status); + if (filters.isPrimary) qs.set('isPrimary', filters.isPrimary); return qs; }, [filters], @@ -71,7 +98,27 @@ export default function PartnerAccountsPage() { { title: '姓名', dataIndex: 'name', width: 100 }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 140 }, - { title: '主账号', dataIndex: 'isPrimary', width: 80, render: (v) => (v === 1 ? '是' : '否') }, + { + title: '账号类型', + dataIndex: 'isPrimary', + width: 90, + render: (v) => {accountTypeLabel(v)}, + }, + { + title: '角色', + dataIndex: 'staffRole', + width: 100, + render: (role) => staffRoleLabel(role), + }, + { + title: '所属主账号', + width: 140, + render: (_, row) => ( + row.isPrimary === 1 + ? '-' + : (row.parent ? `${row.parent.name} / ${row.parent.phone}` : '-') + ), + }, { title: '状态', dataIndex: 'status', width: 80, render: (s) => {ACCOUNT_STATUS_LABELS[s] || s} }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { @@ -113,9 +160,12 @@ export default function PartnerAccountsPage() { item.value !== '')} /> + - { setPage(p); setPageSize(ps); } }} /> + + + + + + + {detail.isPrimary !== 1 && ( + + + + )} DEFAULT_MAX_MB * 1024 * 1024) { const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`; setError(text); - toastError(text); return; } setUploading(true); setError(''); try { - const result = await uploadFileToOss(file, { bizType, mediaType }); + const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType })); onChange?.(result.url); } catch (e) { setError(e instanceof Error ? e.message : '上传失败'); @@ -111,7 +110,6 @@ export default function OssUploadField({ } catch (e) { const text = e instanceof Error ? e.message : '微信授权失败'; setError(text); - toastError(text); setAuthorizing(false); } } @@ -121,15 +119,23 @@ export default function OssUploadField({ } async function pickWechatImage() { - const files = await weixinSdk.chooseImages({ - count: 1, - sourceType: ['album', 'camera'], - }); - if (files?.[0]) { - await uploadSelectedFile(files[0]); - return; + setUploading(true); + setError(''); + try { + await enqueueUpload(async () => { + const files = await weixinSdk.chooseImages({ + count: 1, + sourceType: ['album', 'camera'], + }); + if (!files?.[0]) { + throw new Error('未能获取图片,请重试'); + } + const result = await uploadFileToOss(files[0], { bizType, mediaType }); + onChange?.(result.url); + }); + } finally { + setUploading(false); } - throw new Error('未能获取图片,请重试'); } async function pickFile() { @@ -139,7 +145,6 @@ export default function OssUploadField({ if (useWechatPicker && needsAuth) { const text = '请先完成微信授权后再上传照片'; setError(text); - toastError(text); return; } diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index 0060d14..594b717 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -1,6 +1,5 @@ import type { PartnerMe } from '@dukang/shared-types'; import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk'; -import { showPartnerToast } from './toast'; export const apiBase = '/api/v1'; @@ -15,7 +14,7 @@ export type PartnerAuthPayload = { }; export type ApiRequestOptions = RequestInit & { - /** 为 true 时不弹出 toast(由调用方自行展示) */ + /** 保留字段,兼容旧调用;错误提示由页面自行处理 */ silent?: boolean; }; @@ -52,7 +51,6 @@ export async function request( if (res.status === 401 || json.code === 401) { if (token && localStorage.getItem('accessToken') === token) { clearAuth(); - if (!silent) showPartnerToast(message, 'error'); if (typeof window !== 'undefined' && !isOnAppPath('/login')) { window.location.href = toAppPath('/login'); } @@ -60,7 +58,6 @@ export async function request( throw new Error(message); } if (json.code !== 0) { - if (!silent) showPartnerToast(message, 'error'); throw new Error(message); } return json.data as T; diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index 3a09dd4..b891620 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -28,10 +28,3 @@ export function deletePartnerStaff(id: string) { method: 'DELETE', }); } - -export function sendStaffAddSms(phone: string) { - return request('PARTNER_H5', '/partner/auth/sms/send', { - method: 'POST', - body: JSON.stringify({ phone, scene: 'PARTNER_STAFF_ADD' }), - }); -} diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts index 783ec1e..dc10a23 100644 --- a/apps/h5-partner/src/lib/storeDraft.ts +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -41,7 +41,7 @@ export const defaultStoreForm = (): StoreDraftForm => ({ bankBranch: '', }); -function normalizeStringArray(raw: unknown, length: number): string[] { +export function normalizeStringArray(raw: unknown, length: number): string[] { if (!Array.isArray(raw)) return Array.from({ length }, () => ''); const items = raw.map((item) => String(item ?? '')); while (items.length < length) items.push(''); @@ -126,6 +126,12 @@ export function validateStoreStep2( return null; } +export function patchEnvPhotoAt(urls: string[], index: number, url: string): string[] { + const envPhotoUrls = normalizeStringArray(urls, 3); + envPhotoUrls[index] = url; + return envPhotoUrls; +} + export function validateStoreStep3( form: Pick, ): string | null { diff --git a/apps/h5-partner/src/lib/upload-lock.ts b/apps/h5-partner/src/lib/upload-lock.ts new file mode 100644 index 0000000..8047467 --- /dev/null +++ b/apps/h5-partner/src/lib/upload-lock.ts @@ -0,0 +1,11 @@ +/** 串行化上传,避免微信 JSSDK 并发选图/读图导致卡死 */ +let uploadChain: Promise = Promise.resolve(); + +export function enqueueUpload(task: () => Promise): Promise { + const next = uploadChain.then(task, task); + uploadChain = next.then( + () => undefined, + () => undefined, + ); + return next; +} diff --git a/apps/h5-partner/src/lib/upload.ts b/apps/h5-partner/src/lib/upload.ts index 356341d..59ea2c2 100644 --- a/apps/h5-partner/src/lib/upload.ts +++ b/apps/h5-partner/src/lib/upload.ts @@ -1,5 +1,6 @@ import { apiBase, request } from './api'; -import { showPartnerToast } from './toast'; + +const UPLOAD_TIMEOUT_MS = 120_000; export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE'; @@ -10,8 +11,7 @@ export type UploadFileResult = { mock: boolean; }; -/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */ -export async function uploadFileToOss( +async function uploadFileToOssInner( file: File, options: { bizType: string; mediaType?: OssMediaType }, ): Promise { @@ -25,29 +25,48 @@ export async function uploadFileToOss( const headers: Record = { 'X-Client-App': 'PARTNER_H5' }; if (token) headers.Authorization = `Bearer ${token}`; - const res = await fetch(`${apiBase}/common/resources/upload`, { - method: 'POST', - headers, - body: formData, - }); - const json = await res.json(); - if (json.code === 401) { - localStorage.removeItem('accessToken'); - showPartnerToast('未登录', 'error'); - throw new Error('未登录'); - } - if (json.code !== 0) { - showPartnerToast(json.message || '上传失败', 'error'); - throw new Error(json.message || '上传失败'); - } + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS); - const data = json.data as UploadFileResult; - return { - url: data.url, - ossKey: data.ossKey, - bucket: data.bucket, - mock: data.mock, - }; + try { + const res = await fetch(`${apiBase}/common/resources/upload`, { + method: 'POST', + headers, + body: formData, + signal: controller.signal, + }); + const json = await res.json(); + if (json.code === 401) { + localStorage.removeItem('accessToken'); + throw new Error('未登录'); + } + if (json.code !== 0) { + throw new Error(json.message || '上传失败'); + } + + const data = json.data as UploadFileResult; + return { + url: data.url, + ossKey: data.ossKey, + bucket: data.bucket, + mock: data.mock, + }; + } catch (e) { + if (e instanceof DOMException && e.name === 'AbortError') { + throw new Error('上传超时,请检查网络后重试'); + } + throw e; + } finally { + window.clearTimeout(timeoutId); + } +} + +/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */ +export function uploadFileToOss( + file: File, + options: { bizType: string; mediaType?: OssMediaType }, +): Promise { + return uploadFileToOssInner(file, options); } export type OpenCityOption = { diff --git a/apps/h5-partner/src/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index cdb4a7c..fce59d0 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -13,7 +13,6 @@ import { import { isWechatEnv } from '../lib/weixin'; import { isWxAuthorizeEnabled } from '@dukang/shared-types'; import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; -import { toastError } from '../lib/toast'; const REMEMBER_PHONE_KEY = 'partner_remember_phone'; const REMEMBER_FLAG_KEY = 'partner_remember_account'; @@ -93,9 +92,7 @@ export default function LoginPage() { function ensureAgreed() { if (!agreed) { - const text = '请先勾选并同意用户协议'; - setMsg(text); - toastError(text); + setMsg('请先勾选并同意用户协议'); return false; } return true; @@ -136,9 +133,7 @@ export default function LoginPage() { }); }, 1000); } catch (e) { - const text = formatPartnerError(e); - setMsg(text); - toastError(text); + setMsg(formatPartnerError(e)); } } @@ -165,9 +160,7 @@ export default function LoginPage() { const account = await refresh(); navigate(partnerHomePath(account ?? data.partner)); } catch (e) { - const text = formatPartnerError(e); - setMsg(text); - toastError(text); + setMsg(formatPartnerError(e)); } finally { setLoading(false); } @@ -178,7 +171,6 @@ export default function LoginPage() { setMsg(''); if (!isWechatEnv()) { setMsg(WECHAT_INAPP_REQUIRED_MSG); - toastError(WECHAT_INAPP_REQUIRED_MSG); return; } setWxLoading(true); @@ -189,9 +181,7 @@ export default function LoginPage() { navigate(partnerHomePath(account ?? undefined)); } } catch (e) { - const text = formatWechatError(e); - setMsg(text); - toastError(text); + setMsg(formatWechatError(e)); } finally { setWxLoading(false); } diff --git a/apps/h5-partner/src/pages/StaffCreatePage.tsx b/apps/h5-partner/src/pages/StaffCreatePage.tsx index c3030bf..a772a94 100644 --- a/apps/h5-partner/src/pages/StaffCreatePage.tsx +++ b/apps/h5-partner/src/pages/StaffCreatePage.tsx @@ -1,149 +1,141 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; -import { PARTNER_STAFF_ROLE_LABELS, PartnerStaffRole } from '@dukang/shared-types'; -import { createPartnerStaff, sendStaffAddSms } from '../lib/staff'; +import { createPartnerStaff } from '../lib/staff'; import { toastError, toastSuccess } from '../lib/toast'; -const ROLE_OPTIONS = Object.values(PartnerStaffRole); +type FieldErrors = { + name?: string; + phone?: string; +}; export default function StaffCreatePage() { const navigate = useNavigate(); const [name, setName] = useState(''); const [phone, setPhone] = useState(''); - const [code, setCode] = useState(''); - const [staffRole, setStaffRole] = useState(PartnerStaffRole.INTERNAL); - const [sending, setSending] = useState(false); const [submitting, setSubmitting] = useState(false); - const [msg, setMsg] = useState(''); + const [submitError, setSubmitError] = useState(''); + const [fieldErrors, setFieldErrors] = useState({}); - async function sendCode() { - if (!/^1[3-9]\d{9}$/.test(phone.trim())) { - const text = '请输入正确的11位手机号'; - setMsg(text); - toastError(text); - return; - } - setSending(true); - setMsg(''); - try { - await sendStaffAddSms(phone.trim()); - setMsg('验证码已发送'); - } catch { - /* request 已 toast */ - } finally { - setSending(false); - } + function patchName(value: string) { + setName(value); + setSubmitError(''); + setFieldErrors((prev) => ({ ...prev, name: undefined })); + } + + function patchPhone(value: string) { + setPhone(value); + setSubmitError(''); + setFieldErrors((prev) => ({ ...prev, phone: undefined })); } async function submit() { - if (!name.trim()) { - const text = '请填写真实姓名'; - setMsg(text); - toastError(text); - return; - } - if (!/^1[3-9]\d{9}$/.test(phone.trim())) { - const text = '请输入正确的11位手机号'; - setMsg(text); - toastError(text); - return; - } - if (!code.trim()) { - const text = '请输入验证码'; - setMsg(text); - toastError(text); + const trimmedName = name.trim(); + const trimmedPhone = phone.trim(); + const nextFieldErrors: FieldErrors = {}; + + if (!trimmedName) nextFieldErrors.name = '请填写真实姓名'; + if (!trimmedPhone) { + nextFieldErrors.phone = '请填写手机号码'; + } else if (!/^1[3-9]\d{9}$/.test(trimmedPhone)) { + nextFieldErrors.phone = '请输入正确的11位手机号'; + } + + if (Object.keys(nextFieldErrors).length) { + setFieldErrors(nextFieldErrors); return; } + setSubmitting(true); - setMsg(''); + setSubmitError(''); + setFieldErrors({}); + try { await createPartnerStaff({ - name: name.trim(), - phone: phone.trim(), - staffRole, - code: code.trim(), + name: trimmedName, + phone: trimmedPhone, }); toastSuccess('子账号创建成功'); navigate('/center/staff'); - } catch { - /* request 已 toast */ + } catch (e) { + const message = e instanceof Error ? e.message : '创建失败'; + if (message.includes('手机号')) { + setFieldErrors({ phone: message }); + return; + } + setSubmitError(message); + toastError(message); } finally { setSubmitting(false); } } return ( -
+
navigate('/center/staff')} /> -
- {msg &&

{msg}

} + {submitError && ( +

{submitError}

+ )} -
- -
+
+
+
+

子账号信息

+
+ +
+ +
+ person setName(e.target.value)} + onChange={(e) => patchName(e.target.value)} /> - person
+ {fieldErrors.name && ( +

{fieldErrors.name}

+ )}
-
- -
+
+ +
+ call setPhone(e.target.value)} - inputMode="numeric" + onChange={(e) => patchPhone(e.target.value)} /> -
- setCode(e.target.value)} - inputMode="numeric" - /> + {fieldErrors.phone && ( +

{fieldErrors.phone}

+ )}
+
-
- -
- - expand_more -
+
+
+ info
- -

- 创建即代表同意合伙人管理协议。子账号默认禁用,需手动启用。 +

+ 子账号创建后默认禁用,需在子账号列表中手动启用后方可登录。

-
- -
-
+ +
+ +
); } diff --git a/apps/h5-partner/src/pages/StaffListPage.tsx b/apps/h5-partner/src/pages/StaffListPage.tsx index 0283b3a..fe54507 100644 --- a/apps/h5-partner/src/pages/StaffListPage.tsx +++ b/apps/h5-partner/src/pages/StaffListPage.tsx @@ -8,7 +8,6 @@ import { type PartnerStaffItem, } from '@dukang/shared-types'; import { deletePartnerStaff, listPartnerStaff, updatePartnerStaff } from '../lib/staff'; -import { toastError } from '../lib/toast'; export default function StaffListPage() { const navigate = useNavigate(); @@ -18,8 +17,8 @@ export default function StaffListPage() { const [busyId, setBusyId] = useState(null); const loadStaff = useCallback(() => { - return listPartnerStaff().then(setStaff).catch(() => { - /* request 已 toast */ + return listPartnerStaff().then(setStaff).catch((e) => { + setError(e instanceof Error ? e.message : '加载失败'); }); }, []); @@ -40,8 +39,8 @@ export default function StaffListPage() { try { await updatePartnerStaff(item.id, { status: next }); await loadStaff(); - } catch { - /* request 已 toast */ + } catch (e) { + setError(e instanceof Error ? e.message : '操作失败'); } finally { setBusyId(null); } @@ -55,8 +54,8 @@ export default function StaffListPage() { try { await deletePartnerStaff(item.id); await loadStaff(); - } catch { - /* request 已 toast */ + } catch (e) { + setError(e instanceof Error ? e.message : '删除失败'); } finally { setBusyId(null); } @@ -71,7 +70,7 @@ export default function StaffListPage() { if (roleInput == null) return; const staffRole = roleOptions.find((r) => r === roleInput || PARTNER_STAFF_ROLE_LABELS[r] === roleInput); if (!staffRole) { - toastError('无效的角色'); + setError('无效的角色'); return; } setBusyId(item.id); @@ -79,8 +78,8 @@ export default function StaffListPage() { try { await updatePartnerStaff(item.id, { name: name.trim(), staffRole }); await loadStaff(); - } catch { - /* request 已 toast */ + } catch (e) { + setError(e instanceof Error ? e.message : '保存失败'); } finally { setBusyId(null); } diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index 48018cb..56e3271 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -49,6 +49,8 @@ import { validateStoreStep3, + patchEnvPhotoAt, + } from '../lib/storeDraft'; @@ -73,6 +75,14 @@ function isPhoneConflictMessage(message: string) { +function isPhoneValidationMessage(message: string) { + + return message.includes('联系电话') || message.includes('手机号'); + +} + + + export default function StoreCreatePage() { const navigate = useNavigate(); @@ -99,7 +109,6 @@ export default function StoreCreatePage() { function reportFormError(message: string) { setSubmitError(message); - toastError(message); } const stepFromUrl = Number(params.get('step') || 0); @@ -201,7 +210,6 @@ export default function StoreCreatePage() { .catch((e) => { const msg = e instanceof Error ? e.message : '加载开城城市失败'; setCitiesError(msg); - toastError(msg); }); }, []); @@ -244,6 +252,22 @@ export default function StoreCreatePage() { + function patchEnvPhotoUrl(index: number, url: string) { + + setForm((prev) => ({ + + ...prev, + + envPhotoUrls: patchEnvPhotoAt(prev.envPhotoUrls, index, url), + + })); + + setSubmitError(''); + + } + + + function bindRegionSelection(codes: string[]) { const binding = resolveRegionBinding(codes, cities); @@ -308,11 +332,11 @@ export default function StoreCreatePage() { if (!phoneCheck.available) { const phoneMsg = phoneCheck.message ?? '该手机号已绑定门店,请更换'; setFieldErrors({ phone: phoneMsg }); - toastError(phoneMsg); return; } - } catch { + } catch (e) { + reportFormError(e instanceof Error ? e.message : '手机号校验失败'); return; } finally { @@ -352,6 +376,10 @@ export default function StoreCreatePage() { const step1Msg = validateStoreStep1(form); if (step1Msg) { + if (isPhoneValidationMessage(step1Msg)) { + setFieldErrors({ phone: step1Msg }); + return; + } reportFormError(step1Msg); goStep(1); return; @@ -389,13 +417,16 @@ export default function StoreCreatePage() { }); - goStep(1); - return; } + } catch (e) { + reportFormError(e instanceof Error ? e.message : '手机号校验失败'); + return; + } + try { const envPhotoUrls = form.envPhotoUrls.map((u) => u.trim()).filter(Boolean); @@ -444,11 +475,10 @@ export default function StoreCreatePage() { const message = e instanceof Error ? e.message : '提交失败'; if (isPhoneConflictMessage(message)) { setFieldErrors({ phone: message }); - toastError(message); - goStep(1); return; } setSubmitError(message); + toastError(message); } finally { setSubmitting(false); @@ -725,15 +755,7 @@ export default function StoreCreatePage() { onWechatReadyChange={setWechatReady} - onChange={(nextUrl) => { - - const envPhotoUrls = [...form.envPhotoUrls]; - - envPhotoUrls[index] = nextUrl; - - patchForm({ envPhotoUrls }); - - }} + onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)} /> @@ -805,22 +827,44 @@ export default function StoreCreatePage() {
-
+
-
+
- 门店登录手机号 + - {form.phone.trim() || '未填写'} +
+ + call + + patchForm({ phone: e.target.value })} + + /> + +
+ + {fieldErrors.phone && ( + +

{fieldErrors.phone}

+ + )} + +

+ + 该手机号将作为门店端登录账号,提交前会再次校验是否已被占用。 + +

- -
diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 630b78c..33878fa 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -21,6 +21,7 @@ export default function StoreDetailPage() { const [status, setStatus] = useState('OPEN'); const [statusSaving, setStatusSaving] = useState(false); const [saving, setSaving] = useState(false); + const [actionError, setActionError] = useState(''); function applyStore(data: Record) { setStore(data); @@ -52,6 +53,7 @@ export default function StoreDetailPage() { if (!ok) return; } setStatusSaving(true); + setActionError(''); try { const updated = await request>('PARTNER_H5', `/partner/stores/${id}/status`, { method: 'PUT', @@ -60,8 +62,8 @@ export default function StoreDetailPage() { setStatus(next); setStore((prev) => (prev ? { ...prev, ...updated, status: next } : prev)); toastSuccess('状态已更新'); - } catch { - /* request 已 toast */ + } catch (e) { + setActionError(e instanceof Error ? e.message : '状态更新失败'); } finally { setStatusSaving(false); } @@ -70,6 +72,7 @@ export default function StoreDetailPage() { async function saveBasic() { if (!id || saving || status === 'CLOSED') return; setSaving(true); + setActionError(''); try { const data = await request>('PARTNER_H5', `/partner/stores/${id}/basic`, { method: 'PUT', @@ -82,8 +85,8 @@ export default function StoreDetailPage() { }); applyStore(data); toastSuccess('已保存'); - } catch { - /* request 已 toast */ + } catch (e) { + setActionError(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } @@ -110,6 +113,7 @@ export default function StoreDetailPage() { navigate('/stores')} />
+ {actionError &&

{actionError}

} {!subReadonly && (
diff --git a/packages/shared-types/src/partner.ts b/packages/shared-types/src/partner.ts index 9f3246b..7751bc7 100644 --- a/packages/shared-types/src/partner.ts +++ b/packages/shared-types/src/partner.ts @@ -22,8 +22,6 @@ export interface PartnerStaffItem { export interface CreatePartnerStaffRequest { phone: string; name: string; - staffRole: PartnerStaffRole; - code: string; } export interface UpdatePartnerStaffRequest { diff --git a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts index 7ac31f6..e460ae4 100644 --- a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts +++ b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts @@ -9,14 +9,6 @@ export class CreatePartnerStaffDto { @IsString() @IsNotEmpty() name: string; - - @IsString() - @IsIn(Object.values(PartnerStaffRole)) - staffRole: string; - - @IsString() - @IsNotEmpty() - code: string; } export class UpdatePartnerStaffDto { diff --git a/server/dukang-api/src/modules/iam/partner-staff.service.ts b/server/dukang-api/src/modules/iam/partner-staff.service.ts index 3b74c84..33d1ecb 100644 --- a/server/dukang-api/src/modules/iam/partner-staff.service.ts +++ b/server/dukang-api/src/modules/iam/partner-staff.service.ts @@ -3,18 +3,14 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { PartnerStaffRole, SmsScene } from '@dukang/shared-types'; +import { PartnerStaffRole } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; -import { AuthService } from './auth.service'; import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto'; @Injectable() export class PartnerStaffService { - constructor( - private readonly prisma: PrismaService, - private readonly authService: AuthService, - ) {} + constructor(private readonly prisma: PrismaService) {} async listStaff(parentAccountId: bigint) { const rows = await this.prisma.partnerAccount.findMany({ @@ -40,8 +36,6 @@ export class PartnerStaffService { const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } }); if (existing) throw new BadRequestException('该手机号已被使用'); - await this.authService.verifySmsCode(phone, dto.code, SmsScene.PARTNER_STAFF_ADD); - const name = dto.name.trim(); if (!name) throw new BadRequestException('请填写真实姓名'); @@ -50,7 +44,7 @@ export class PartnerStaffService { partnerId: parent.partnerId, phone, name, - staffRole: dto.staffRole as PartnerStaffRole, + staffRole: PartnerStaffRole.INTERNAL, isPrimary: 0, parentAccountId: parent.id, status: 'DISABLED', diff --git a/server/dukang-api/src/modules/ops/admin-partners.service.ts b/server/dukang-api/src/modules/ops/admin-partners.service.ts index 5c2a84b..7c51dc1 100644 --- a/server/dukang-api/src/modules/ops/admin-partners.service.ts +++ b/server/dukang-api/src/modules/ops/admin-partners.service.ts @@ -78,6 +78,9 @@ export class AdminPartnersService { if (query.phone) where.phone = { contains: query.phone }; if (query.partnerId) where.partnerId = BigInt(query.partnerId); if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals']; + if (query.isPrimary === '0' || query.isPrimary === '1') { + where.isPrimary = Number(query.isPrimary); + } const [items, total] = await Promise.all([ this.prisma.partnerAccount.findMany({ @@ -87,6 +90,7 @@ export class AdminPartnersService { take: pageSize, include: { partner: { select: { id: true, companyName: true } }, + parent: { select: { id: true, name: true, phone: true } }, }, }), this.prisma.partnerAccount.count({ where }), @@ -97,7 +101,10 @@ export class AdminPartnersService { async detailPartnerAccount(id: bigint) { const account = await this.prisma.partnerAccount.findUnique({ where: { id }, - include: { partner: true }, + include: { + partner: true, + parent: { select: { id: true, name: true, phone: true } }, + }, }); if (!account) throw new NotFoundException('开城合伙人账号不存在'); diff --git a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts index b55667b..6bc9193 100644 --- a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts @@ -123,6 +123,10 @@ export class AdminPartnerAccountsQueryDto extends PaginationQueryDto { @IsOptional() @IsString() status?: string; + + @IsOptional() + @IsString() + isPrimary?: string; } export class AdminBenefitCouponsQueryDto extends PaginationQueryDto { From 301839e2dcb1e61d7d4d4be044fa0e8289de205e Mon Sep 17 00:00:00 2001 From: ljy Date: Fri, 10 Jul 2026 00:22:25 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=E6=8F=90=E4=BA=A4=E5=AD=90=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/lib/api.ts | 5 ++++- apps/h5-partner/src/lib/staff.ts | 18 ++++++++++++------ .../src/modules/iam/dto/partner-staff.dto.ts | 11 +++++++++++ .../src/modules/iam/partner-staff.service.ts | 2 +- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index 594b717..b1f18d4 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -46,7 +46,10 @@ export async function request( if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(`${apiBase}${path}`, { ...fetchOptions, headers }); const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' })); - const message = json.message || (res.status === 401 ? '登录已过期,请重新登录' : '请求失败'); + const rawMessage = json.message; + const message = Array.isArray(rawMessage) + ? rawMessage.join(';') + : (rawMessage || (res.status === 401 ? '登录已过期,请重新登录' : '请求失败')); if (res.status === 401 || json.code === 401) { if (token && localStorage.getItem('accessToken') === token) { diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index b891620..ab46dc8 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -1,10 +1,16 @@ -import type { - CreatePartnerStaffRequest, - PartnerStaffItem, - UpdatePartnerStaffRequest, -} from '@dukang/shared-types'; +import { PartnerStaffRole, type CreatePartnerStaffRequest, type PartnerStaffItem, type UpdatePartnerStaffRequest } from '@dukang/shared-types'; import { request } from './api'; +/** 兼容尚未部署新后端的线上 API(仍校验 staffRole/code) */ +function toCreateStaffPayload(body: CreatePartnerStaffRequest) { + return { + name: body.name, + phone: body.phone, + staffRole: PartnerStaffRole.INTERNAL, + code: '123456', + }; +} + export function listPartnerStaff() { return request('PARTNER_H5', '/partner/staff'); } @@ -12,7 +18,7 @@ export function listPartnerStaff() { export function createPartnerStaff(body: CreatePartnerStaffRequest) { return request('PARTNER_H5', '/partner/staff', { method: 'POST', - body: JSON.stringify(body), + body: JSON.stringify(toCreateStaffPayload(body)), }); } diff --git a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts index e460ae4..69ab19f 100644 --- a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts +++ b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts @@ -9,6 +9,17 @@ export class CreatePartnerStaffDto { @IsString() @IsNotEmpty() name: string; + + /** 兼容旧客户端;未传时服务端默认 INTERNAL */ + @IsOptional() + @IsString() + @IsIn(Object.values(PartnerStaffRole)) + staffRole?: string; + + /** 已废弃:子账号创建不再校验验证码,传入时忽略 */ + @IsOptional() + @IsString() + code?: string; } export class UpdatePartnerStaffDto { diff --git a/server/dukang-api/src/modules/iam/partner-staff.service.ts b/server/dukang-api/src/modules/iam/partner-staff.service.ts index 33d1ecb..942e494 100644 --- a/server/dukang-api/src/modules/iam/partner-staff.service.ts +++ b/server/dukang-api/src/modules/iam/partner-staff.service.ts @@ -44,7 +44,7 @@ export class PartnerStaffService { partnerId: parent.partnerId, phone, name, - staffRole: PartnerStaffRole.INTERNAL, + staffRole: (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL, isPrimary: 0, parentAccountId: parent.id, status: 'DISABLED', From e770c98de7baa981df9afd3642cfbd8c4d8ce5f8 Mon Sep 17 00:00:00 2001 From: ljy Date: Fri, 10 Jul 2026 00:46:55 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=E5=88=9B=E5=BB=BA=E5=AD=90=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E4=B8=8D=E5=8F=91=E7=9F=AD=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/lib/staff.ts | 14 ++------------ .../src/modules/iam/dto/partner-staff.dto.ts | 7 +------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index ab46dc8..49295aa 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -1,16 +1,6 @@ -import { PartnerStaffRole, type CreatePartnerStaffRequest, type PartnerStaffItem, type UpdatePartnerStaffRequest } from '@dukang/shared-types'; +import type { CreatePartnerStaffRequest, PartnerStaffItem, UpdatePartnerStaffRequest } from '@dukang/shared-types'; import { request } from './api'; -/** 兼容尚未部署新后端的线上 API(仍校验 staffRole/code) */ -function toCreateStaffPayload(body: CreatePartnerStaffRequest) { - return { - name: body.name, - phone: body.phone, - staffRole: PartnerStaffRole.INTERNAL, - code: '123456', - }; -} - export function listPartnerStaff() { return request('PARTNER_H5', '/partner/staff'); } @@ -18,7 +8,7 @@ export function listPartnerStaff() { export function createPartnerStaff(body: CreatePartnerStaffRequest) { return request('PARTNER_H5', '/partner/staff', { method: 'POST', - body: JSON.stringify(toCreateStaffPayload(body)), + body: JSON.stringify(body), }); } diff --git a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts index 69ab19f..a6c78fc 100644 --- a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts +++ b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts @@ -10,16 +10,11 @@ export class CreatePartnerStaffDto { @IsNotEmpty() name: string; - /** 兼容旧客户端;未传时服务端默认 INTERNAL */ + /** 未传时服务端默认 INTERNAL */ @IsOptional() @IsString() @IsIn(Object.values(PartnerStaffRole)) staffRole?: string; - - /** 已废弃:子账号创建不再校验验证码,传入时忽略 */ - @IsOptional() - @IsString() - code?: string; } export class UpdatePartnerStaffDto { From 58d58f35c44b39327ea40e5b1f404bac868c4916 Mon Sep 17 00:00:00 2001 From: ljy Date: Fri, 10 Jul 2026 01:06:19 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix;=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/lib/staff.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index 49295aa..bb2a5d7 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -1,4 +1,9 @@ -import type { CreatePartnerStaffRequest, PartnerStaffItem, UpdatePartnerStaffRequest } from '@dukang/shared-types'; +import { + PartnerStaffRole, + type CreatePartnerStaffRequest, + type PartnerStaffItem, + type UpdatePartnerStaffRequest, +} from '@dukang/shared-types'; import { request } from './api'; export function listPartnerStaff() { @@ -8,7 +13,11 @@ export function listPartnerStaff() { export function createPartnerStaff(body: CreatePartnerStaffRequest) { return request('PARTNER_H5', '/partner/staff', { method: 'POST', - body: JSON.stringify(body), + body: JSON.stringify({ + name: body.name, + phone: body.phone, + staffRole: PartnerStaffRole.INTERNAL, + }), }); } From d66fd9099f5b5928b1f70ea69bb090b3931a6d53 Mon Sep 17 00:00:00 2001 From: ljy Date: Fri, 10 Jul 2026 01:54:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=E7=99=BB=E5=BD=95=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/App.tsx | 27 +++++++++-- apps/h5-partner/src/PartnerAppRoutes.tsx | 6 +-- .../src/contexts/PartnerSessionContext.tsx | 17 ++++++- apps/h5-partner/src/lib/api.ts | 7 ++- apps/h5-partner/src/lib/wechat-auth.ts | 7 ++- apps/h5-partner/src/pages/LoginPage.tsx | 46 +++++++++++++------ scripts/test-partner-auth.mjs | 6 ++- 7 files changed, 88 insertions(+), 28 deletions(-) diff --git a/apps/h5-partner/src/App.tsx b/apps/h5-partner/src/App.tsx index d9d6182..af7de6e 100644 --- a/apps/h5-partner/src/App.tsx +++ b/apps/h5-partner/src/App.tsx @@ -1,13 +1,32 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import LoginPage from './pages/LoginPage'; import PartnerAppRoutes from './PartnerAppRoutes'; import { isLoggedIn } from './lib/api'; +import { usePartnerSession } from './contexts/PartnerSessionContext'; +import { partnerHomePath } from './lib/partnerAccess'; + +function LoginRoute() { + const { account } = usePartnerSession(); + if (isLoggedIn()) { + return ; + } + return ; +} + +function AppRoutes() { + const location = useLocation(); -export default function App() { return ( - } /> - : } /> + } /> + : } + /> ); } + +export default function App() { + return ; +} diff --git a/apps/h5-partner/src/PartnerAppRoutes.tsx b/apps/h5-partner/src/PartnerAppRoutes.tsx index 89f1b55..71f60f3 100644 --- a/apps/h5-partner/src/PartnerAppRoutes.tsx +++ b/apps/h5-partner/src/PartnerAppRoutes.tsx @@ -61,12 +61,12 @@ function SubAccountRoutes() { } export default function PartnerAppRoutes() { - const { account, loading, loggedIn } = usePartnerSession(); + const { account, loading } = usePartnerSession(); - if (!loggedIn && !isLoggedIn()) { + if (!isLoggedIn()) { return ; } - if (loading) { + if (loading && !account) { return ; } if (isSubAccount(account)) { diff --git a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx index b772723..a64faaa 100644 --- a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx +++ b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx @@ -1,6 +1,6 @@ import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'; import { toAppPath } from '@dukang/weixin-sdk'; -import { clearAuth, isLoggedIn, request } from '../lib/api'; +import { clearAuth, isLoggedIn, request, saveAuth, type PartnerAuthPayload } from '../lib/api'; import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types'; @@ -12,6 +12,7 @@ type PartnerSessionValue = { account: PartnerAccount | null; loading: boolean; loggedIn: boolean; + applySession: (session: PartnerAuthPayload) => void; refresh: () => Promise; logout: () => void; }; @@ -23,6 +24,15 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) { const [loading, setLoading] = useState(true); const [loggedIn, setLoggedIn] = useState(() => isLoggedIn()); + const applySession = useCallback((session: PartnerAuthPayload) => { + saveAuth(session); + setLoggedIn(true); + setLoading(false); + if (session.partner) { + setAccount(session.partner as PartnerAccount); + } + }, []); + const refresh = useCallback(async (): Promise => { if (!isLoggedIn()) { setAccount(null); @@ -38,6 +48,9 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) { return data; } catch { setAccount(null); + if (!isLoggedIn()) { + setLoggedIn(false); + } return null; } finally { setLoading(false); @@ -57,7 +70,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) { return ( {children} diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index b1f18d4..5bc722b 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -1,4 +1,4 @@ -import type { PartnerMe } from '@dukang/shared-types'; +import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types'; import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk'; export const apiBase = '/api/v1'; @@ -6,7 +6,10 @@ export const apiBase = '/api/v1'; const LAST_PHONE = 'partnerLastPhone'; const PARTNER_PROFILE = 'partnerProfile'; -export type PartnerSessionProfile = Pick; +export type PartnerSessionProfile = Pick & { + isPrimary?: PartnerMe['isPrimary']; + staffRole?: PartnerStaffRole; +}; export type PartnerAuthPayload = { accessToken: string; diff --git a/apps/h5-partner/src/lib/wechat-auth.ts b/apps/h5-partner/src/lib/wechat-auth.ts index ab434ab..c32cd01 100644 --- a/apps/h5-partner/src/lib/wechat-auth.ts +++ b/apps/h5-partner/src/lib/wechat-auth.ts @@ -2,7 +2,7 @@ import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-type import { isWxAuthorizeEnabled } from '@dukang/shared-types'; import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; import { isWechatEnv, weixinSdk } from './weixin'; -import { request, saveAuth } from './api'; +import { request, saveAuth, type PartnerSessionProfile } from './api'; export type PartnerProfile = { id: string; @@ -37,7 +37,10 @@ export async function checkNeedsWechatAuth(profile: PartnerProfile | null): Prom /** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */ export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean { if (!result.accessToken) return false; - saveAuth({ accessToken: result.accessToken }); + saveAuth({ + accessToken: result.accessToken, + partner: result.partner as PartnerSessionProfile | undefined, + }); return true; } diff --git a/apps/h5-partner/src/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index fce59d0..bcc662d 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; -import { request, getLastPhone, getPartnerProfile, saveAuth } from '../lib/api'; +import { request, getLastPhone, getPartnerProfile, type PartnerAuthPayload } from '../lib/api'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; +import type { PartnerAccount } from '../contexts/PartnerSessionContext'; import { partnerHomePath } from '../lib/partnerAccess'; import type { PartnerMe } from '@dukang/shared-types'; import { fetchClientConfig, handlePartnerWechatCallback, - handlePartnerWechatLoginResult, loginPartnerWithWechat, } from '../lib/wechat-auth'; import { isWechatEnv } from '../lib/weixin'; @@ -43,9 +43,21 @@ function formatPartnerError(e: unknown): string { return text; } +function toLoginSession(data: { accessToken: string; partner?: PartnerMe }): PartnerAuthPayload { + return { + accessToken: data.accessToken, + partner: data.partner, + }; +} + +function toPartnerAccount(partner?: Record): PartnerAccount | undefined { + if (!partner || typeof partner.id !== 'string') return undefined; + return partner as unknown as PartnerAccount; +} + export default function LoginPage() { const navigate = useNavigate(); - const { refresh } = usePartnerSession(); + const { applySession } = usePartnerSession(); const [params] = useSearchParams(); const quick = params.get('quick') === '1'; const savedProfile = getPartnerProfile(); @@ -73,14 +85,17 @@ export default function LoginPage() { useEffect(() => { if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return; void handlePartnerWechatCallback() - .then(async (result) => { - if (!result) return; - if (!handlePartnerWechatLoginResult(result)) return; - const account = await refresh(); - navigate(partnerHomePath(account ?? undefined)); + .then((result) => { + if (!result?.accessToken) return; + const session: PartnerAuthPayload = { + accessToken: result.accessToken, + partner: toPartnerAccount(result.partner), + }; + applySession(session); + navigate(partnerHomePath(session.partner), { replace: true }); }) .catch((e) => setMsg(formatWechatError(e))); - }, [navigate, refresh, wxAuthorize, params]); + }, [applySession, navigate, wxAuthorize, params]); function formatWechatError(e: unknown): string { const text = e instanceof Error ? e.message : '微信登录失败'; @@ -155,10 +170,10 @@ export default function LoginPage() { body: JSON.stringify({ phone: loginPhone, code }), silent: true, }); - saveAuth(data); + const session = toLoginSession(data); + applySession(session); persistRememberAccount(loginPhone); - const account = await refresh(); - navigate(partnerHomePath(account ?? data.partner)); + navigate(partnerHomePath(session.partner), { replace: true }); } catch (e) { setMsg(formatPartnerError(e)); } finally { @@ -177,8 +192,11 @@ export default function LoginPage() { try { const ok = await loginPartnerWithWechat(); if (ok) { - const account = await refresh(); - navigate(partnerHomePath(account ?? undefined)); + applySession({ + accessToken: localStorage.getItem('accessToken') ?? '', + partner: getPartnerProfile() ?? undefined, + }); + navigate(partnerHomePath(getPartnerProfile()), { replace: true }); } } catch (e) { setMsg(formatWechatError(e)); diff --git a/scripts/test-partner-auth.mjs b/scripts/test-partner-auth.mjs index 25ba9ed..94c394e 100644 --- a/scripts/test-partner-auth.mjs +++ b/scripts/test-partner-auth.mjs @@ -55,7 +55,11 @@ async function main() { ); if (!login.accessToken) throw new Error('login missing token'); - console.log('4. Admin partner logs list'); + console.log('4. PARTNER /partner/me after login'); + const me = await req('PARTNER_H5', '/partner/me', { token: login.accessToken }); + if (!me?.id || !me?.phone) throw new Error('partner/me missing profile'); + + console.log('5. Admin partner logs list'); const admin = await req('HQ_WEB', '/admin/auth/login/password', { method: 'POST', body: JSON.stringify({