fix(h5-partner): resolve merge conflicts for auth and sub-accounts
Merge partner session model (AuthGate, ensureSession, WeChat OAuth in Context) with sub-account routing, admin partner-account tree CRUD, upload lock, and partner staff flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { AdminCellLine } from '../components/AdminCellLine';
|
||||
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
|
||||
type PartnerOption = { id: string; companyName: string };
|
||||
type ParentAccount = { id: string; name: string; phone: string };
|
||||
|
||||
type AccountTreeRow = {
|
||||
id: string;
|
||||
@@ -16,10 +17,11 @@ type AccountTreeRow = {
|
||||
name: string;
|
||||
status: string;
|
||||
isPrimary: number;
|
||||
staffRole: string | null;
|
||||
parentAccountId: string | null;
|
||||
staffRole?: string | null;
|
||||
parentAccountId?: string | null;
|
||||
parent?: ParentAccount | null;
|
||||
createdAt: string;
|
||||
lastLoginAt: string | null;
|
||||
lastLoginAt?: string | null;
|
||||
partner?: { id: string; companyName: string };
|
||||
children?: AccountTreeRow[];
|
||||
};
|
||||
@@ -49,20 +51,29 @@ function filterTree(rows: AccountTreeRow[], phone?: string, status?: string): Ac
|
||||
return phoneOk && statusOk;
|
||||
};
|
||||
|
||||
const walk = (list: AccountTreeRow[]): AccountTreeRow[] =>
|
||||
list
|
||||
.map((row) => {
|
||||
const walk = (list: AccountTreeRow[]): AccountTreeRow[] => {
|
||||
const result: AccountTreeRow[] = [];
|
||||
for (const row of list) {
|
||||
const children = row.children?.length ? walk(row.children) : undefined;
|
||||
if (match(row) || (children && children.length > 0)) {
|
||||
return { ...row, children: children?.length ? children : undefined };
|
||||
result.push({ ...row, children: children?.length ? children : undefined });
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((row): row is AccountTreeRow => row !== null);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return walk(rows);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -167,7 +178,7 @@ export default function PartnerAccountsPage() {
|
||||
primary={row.name}
|
||||
secondary={
|
||||
row.parentAccountId
|
||||
? `子账号 · ${PARTNER_STAFF_ROLE_LABELS[row.staffRole as keyof typeof PARTNER_STAFF_ROLE_LABELS] || row.staffRole || '—'}`
|
||||
? `子账号 · ${staffRoleLabel(row.staffRole)}`
|
||||
: row.isPrimary === 1
|
||||
? '主账号'
|
||||
: '合伙人账号'
|
||||
@@ -183,15 +194,28 @@ export default function PartnerAccountsPage() {
|
||||
render: (_, row) => row.partner?.companyName || '—',
|
||||
},
|
||||
{
|
||||
title: '标识',
|
||||
title: '账号类型',
|
||||
dataIndex: 'isPrimary',
|
||||
width: 90,
|
||||
render: (v, row) => (
|
||||
<Tag color={row.parentAccountId ? 'default' : v === 1 ? 'blue' : 'default'}>
|
||||
{row.parentAccountId ? '子账号' : accountTypeLabel(v)}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'staffRole',
|
||||
width: 100,
|
||||
render: (_, row) =>
|
||||
row.parentAccountId ? (
|
||||
<Tag>子账号</Tag>
|
||||
) : row.isPrimary === 1 ? (
|
||||
<Tag color="blue">主账号</Tag>
|
||||
) : (
|
||||
<Tag>账号</Tag>
|
||||
render: (role) => staffRoleLabel(role),
|
||||
},
|
||||
{
|
||||
title: '所属主账号',
|
||||
width: 140,
|
||||
render: (_, row) => (
|
||||
row.isPrimary === 1 || !row.parentAccountId
|
||||
? '-'
|
||||
: (row.parent ? `${row.parent.name} / ${row.parent.phone}` : '-')
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
|
||||
@@ -282,7 +306,7 @@ export default function PartnerAccountsPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={treeData}
|
||||
scroll={{ x: 1100 }}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowKeys,
|
||||
@@ -320,13 +344,25 @@ export default function PartnerAccountsPage() {
|
||||
<Form.Item label="开城合伙人">
|
||||
<Input value={detail.partner?.companyName} disabled />
|
||||
</Form.Item>
|
||||
{detail.parentAccountId ? (
|
||||
<Form.Item label="账号类型">
|
||||
<Input
|
||||
value={`子账号 · ${PARTNER_STAFF_ROLE_LABELS[detail.staffRole as keyof typeof PARTNER_STAFF_ROLE_LABELS] || detail.staffRole || '—'}`}
|
||||
value={
|
||||
detail.parentAccountId
|
||||
? `子账号 · ${staffRoleLabel(detail.staffRole)}`
|
||||
: accountTypeLabel(detail.isPrimary)
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
</Form.Item>
|
||||
{detail.staffRole ? (
|
||||
<Form.Item label="角色">
|
||||
<Input value={staffRoleLabel(detail.staffRole)} disabled />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{detail.parentAccountId && detail.parent ? (
|
||||
<Form.Item label="所属主账号">
|
||||
<Input value={`${detail.parent.name} / ${detail.parent.phone}`} disabled />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
||||
import { toastError } from '../lib/toast';
|
||||
import { enqueueUpload } from '../lib/upload-lock';
|
||||
import {
|
||||
authorizePartnerWechat,
|
||||
fetchClientConfig,
|
||||
@@ -92,13 +92,12 @@ export default function OssUploadField({
|
||||
if (file.size > 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 : '上传失败');
|
||||
@@ -116,7 +115,6 @@ export default function OssUploadField({
|
||||
} catch (e) {
|
||||
const text = e instanceof Error ? e.message : '微信授权失败';
|
||||
setError(text);
|
||||
toastError(text);
|
||||
setAuthorizing(false);
|
||||
}
|
||||
}
|
||||
@@ -126,17 +124,24 @@ export default function OssUploadField({
|
||||
}
|
||||
|
||||
async function pickWechatImage() {
|
||||
setUploading(true);
|
||||
setError('');
|
||||
try {
|
||||
await weixinSdk.init();
|
||||
await enqueueUpload(async () => {
|
||||
const files = await weixinSdk.chooseImages({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
});
|
||||
if (files?.[0]) {
|
||||
await uploadSelectedFile(files[0]);
|
||||
return;
|
||||
}
|
||||
if (!files?.[0]) {
|
||||
throw new Error('未能获取图片,请重试');
|
||||
}
|
||||
await uploadSelectedFile(files[0]);
|
||||
});
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickFile() {
|
||||
if (uploading || authorizing) return;
|
||||
@@ -145,7 +150,6 @@ export default function OssUploadField({
|
||||
if (useWechatPicker && needsAuth) {
|
||||
const text = '请先完成微信授权后再上传照片';
|
||||
setError(text);
|
||||
toastError(text);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'react';
|
||||
import { stripOAuthParamsFromLocation, toAppPath } from '@dukang/weixin-sdk';
|
||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
|
||||
import {
|
||||
clearAuth,
|
||||
ensureSession,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
type PartnerSessionPayload,
|
||||
type PartnerSessionProfile,
|
||||
} from '../lib/api';
|
||||
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
|
||||
import { fetchClientConfig, processPartnerWechatOAuthCallback } from '../lib/wechat-auth';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
|
||||
@@ -40,6 +40,17 @@ type PartnerSessionValue = {
|
||||
|
||||
const PartnerSessionContext = createContext<PartnerSessionValue | null>(null);
|
||||
|
||||
function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
phone: profile.phone,
|
||||
companyName: profile.companyName ?? '',
|
||||
isPrimary: profile.isPrimary ?? true,
|
||||
staffRole: profile.staffRole,
|
||||
};
|
||||
}
|
||||
|
||||
export function PartnerSessionProvider({ children }: { children: ReactNode }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
@@ -49,13 +60,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
|
||||
saveAuth(session);
|
||||
setAuthenticated(true);
|
||||
if (session.partner) {
|
||||
setAccount({
|
||||
id: session.partner.id,
|
||||
name: session.partner.name,
|
||||
phone: session.partner.phone,
|
||||
companyName: session.partner.companyName,
|
||||
isPrimary: session.partner.isPrimary,
|
||||
});
|
||||
setAccount(accountFromProfile(session.partner));
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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';
|
||||
import { showPartnerToast } from './toast';
|
||||
|
||||
@@ -22,7 +22,9 @@ const AUTH_RECOVERY_EXEMPT_PATHS = [
|
||||
'/partner/auth/login/wechat',
|
||||
];
|
||||
|
||||
export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName' | 'isPrimary'>;
|
||||
export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName' | 'isPrimary'> & {
|
||||
staffRole?: PartnerStaffRole;
|
||||
};
|
||||
|
||||
export type PartnerSessionPayload = {
|
||||
accessToken: string;
|
||||
@@ -31,7 +33,7 @@ export type PartnerSessionPayload = {
|
||||
};
|
||||
|
||||
export type ApiRequestOptions = RequestInit & {
|
||||
/** 为 true 时不弹出 toast(由调用方自行展示) */
|
||||
/** 保留字段,兼容旧调用;错误提示由页面自行处理 */
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
@@ -101,6 +103,7 @@ function profileFromMe(me: PartnerMe): PartnerSessionProfile {
|
||||
phone: me.phone,
|
||||
companyName: me.companyName,
|
||||
isPrimary: me.isPrimary,
|
||||
staffRole: me.staffRole ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type {
|
||||
CreatePartnerStaffRequest,
|
||||
PartnerStaffItem,
|
||||
UpdatePartnerStaffRequest,
|
||||
import {
|
||||
PartnerStaffRole,
|
||||
type CreatePartnerStaffRequest,
|
||||
type PartnerStaffItem,
|
||||
type UpdatePartnerStaffRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { request } from './api';
|
||||
|
||||
@@ -12,7 +13,11 @@ export function listPartnerStaff() {
|
||||
export function createPartnerStaff(body: CreatePartnerStaffRequest) {
|
||||
return request<PartnerStaffItem>('PARTNER_H5', '/partner/staff', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify({
|
||||
name: body.name,
|
||||
phone: body.phone,
|
||||
staffRole: PartnerStaffRole.INTERNAL,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,10 +33,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' }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,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('');
|
||||
@@ -138,6 +138,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<StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch'>,
|
||||
): string | null {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 串行化上传,避免微信 JSSDK 并发选图/读图导致卡死 */
|
||||
let uploadChain: Promise<unknown> = Promise.resolve();
|
||||
|
||||
export function enqueueUpload<T>(task: () => Promise<T>): Promise<T> {
|
||||
const next = uploadChain.then(task, task);
|
||||
uploadChain = next.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
return next;
|
||||
}
|
||||
@@ -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<UploadFileResult> {
|
||||
@@ -25,19 +25,22 @@ export async function uploadFileToOss(
|
||||
const headers: Record<string, string> = { 'X-Client-App': 'PARTNER_H5' };
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
|
||||
|
||||
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');
|
||||
showPartnerToast('未登录', 'error');
|
||||
throw new Error('未登录');
|
||||
}
|
||||
if (json.code !== 0) {
|
||||
showPartnerToast(json.message || '上传失败', 'error');
|
||||
throw new Error(json.message || '上传失败');
|
||||
}
|
||||
|
||||
@@ -48,6 +51,22 @@ export async function uploadFileToOss(
|
||||
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<UploadFileResult> {
|
||||
return uploadFileToOssInner(file, options);
|
||||
}
|
||||
|
||||
export type OpenCityOption = {
|
||||
|
||||
@@ -107,6 +107,6 @@ export async function processPartnerWechatOAuthCallback(): Promise<PartnerSessio
|
||||
}
|
||||
|
||||
/** @deprecated 使用 handlePartnerWechatLoginResult */
|
||||
export function savePartnerWechatAuth(result: WechatLoginResult): boolean {
|
||||
return !!handlePartnerWechatLoginResult(result);
|
||||
export function savePartnerWechatAuth(result: WechatLoginResult): PartnerSessionPayload | null {
|
||||
return handlePartnerWechatLoginResult(result);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
loginPartnerWithWechat,
|
||||
} from '../lib/wechat-auth';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
import { toastError } from '../lib/toast';
|
||||
|
||||
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
||||
const REMEMBER_FLAG_KEY = 'partner_remember_account';
|
||||
@@ -86,9 +85,7 @@ export default function LoginPage() {
|
||||
|
||||
function ensureAgreed() {
|
||||
if (!agreed) {
|
||||
const text = '请先勾选并同意用户协议';
|
||||
setMsg(text);
|
||||
toastError(text);
|
||||
setMsg('请先勾选并同意用户协议');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -129,9 +126,7 @@ export default function LoginPage() {
|
||||
});
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
const text = formatPartnerError(e);
|
||||
setMsg(text);
|
||||
toastError(text);
|
||||
setMsg(formatPartnerError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,9 +155,7 @@ export default function LoginPage() {
|
||||
}
|
||||
await finishLoginNavigate();
|
||||
} catch (e) {
|
||||
const text = formatPartnerError(e);
|
||||
setMsg(text);
|
||||
toastError(text);
|
||||
setMsg(formatPartnerError(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -173,7 +166,6 @@ export default function LoginPage() {
|
||||
setMsg('');
|
||||
if (!isWechatEnv()) {
|
||||
setMsg(WECHAT_INAPP_REQUIRED_MSG);
|
||||
toastError(WECHAT_INAPP_REQUIRED_MSG);
|
||||
return;
|
||||
}
|
||||
setWxLoading(true);
|
||||
@@ -184,9 +176,7 @@ export default function LoginPage() {
|
||||
await finishLoginNavigate();
|
||||
}
|
||||
} catch (e) {
|
||||
const text = formatWechatError(e);
|
||||
setMsg(text);
|
||||
toastError(text);
|
||||
setMsg(formatWechatError(e));
|
||||
} finally {
|
||||
setWxLoading(false);
|
||||
}
|
||||
|
||||
@@ -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>(PartnerStaffRole.INTERNAL);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
|
||||
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 (
|
||||
<div className="page-no-tab partner-staff-create-page">
|
||||
<div className="partner-page-sticky">
|
||||
<PageHeader title="添加子账号" onBack={() => navigate('/center/staff')} />
|
||||
|
||||
<main style={{ padding: '16px 20px 120px' }}>
|
||||
{msg && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{msg}</p>}
|
||||
{submitError && (
|
||||
<p className="partner-form-error" role="alert">{submitError}</p>
|
||||
)}
|
||||
|
||||
<div className="partner-form-card">
|
||||
<label className="partner-form-label">真实姓名 *</label>
|
||||
<div className="partner-form-input-wrap">
|
||||
<section className="partner-form-card">
|
||||
<div className="partner-section-title">
|
||||
<div className="partner-section-bar" />
|
||||
<h2 className="headline-md">子账号信息</h2>
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>真实姓名 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">person</span>
|
||||
<input
|
||||
className="partner-form-input"
|
||||
placeholder="请输入员工姓名"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onChange={(e) => patchName(e.target.value)}
|
||||
/>
|
||||
<span className="material-symbols-outlined">person</span>
|
||||
</div>
|
||||
{fieldErrors.name && (
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="partner-form-card">
|
||||
<label className="partner-form-label">手机号码 *</label>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div className="partner-field">
|
||||
<label>手机号码 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">call</span>
|
||||
<input
|
||||
className="partner-form-input"
|
||||
type="tel"
|
||||
placeholder="请输入11位手机号"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<button type="button" className="btn btn-outline" disabled={sending} onClick={() => void sendCode()}>
|
||||
{sending ? '发送中' : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
className="partner-form-input"
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder="验证码"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
inputMode="numeric"
|
||||
onChange={(e) => patchPhone(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="partner-form-card">
|
||||
<label className="partner-form-label">分配角色 *</label>
|
||||
<div className="partner-form-input-wrap">
|
||||
<select
|
||||
className="partner-form-input"
|
||||
value={staffRole}
|
||||
onChange={(e) => setStaffRole(e.target.value as PartnerStaffRole)}
|
||||
>
|
||||
{ROLE_OPTIONS.map((role) => (
|
||||
<option key={role} value={role}>{PARTNER_STAFF_ROLE_LABELS[role]}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="material-symbols-outlined">expand_more</span>
|
||||
</div>
|
||||
{fieldErrors.phone && (
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="label-md text-muted" style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
创建即代表同意合伙人管理协议。子账号默认禁用,需手动启用。
|
||||
<div className="partner-info-banner">
|
||||
<div className="partner-bills-icon">
|
||||
<span className="material-symbols-outlined">info</span>
|
||||
</div>
|
||||
<p className="label-md text-variant" style={{ marginTop: 0, lineHeight: 1.5 }}>
|
||||
子账号创建后默认禁用,需在子账号列表中手动启用后方可登录。
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<div className="partner-sticky-footer">
|
||||
<button type="button" className="partner-btn-primary" disabled={submitting} onClick={() => void submit()}>
|
||||
<span className="material-symbols-outlined">person_add</span>
|
||||
{submitting ? '创建中...' : '确认创建'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer className="partner-sticky-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
disabled={submitting}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>person_add</span>
|
||||
{submitting ? '创建中…' : '确认创建'}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string | null>(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);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
|
||||
validateStoreStep3,
|
||||
|
||||
patchEnvPhotoAt,
|
||||
|
||||
} from '../lib/storeDraft';
|
||||
|
||||
|
||||
@@ -63,6 +65,14 @@ function isPhoneConflictMessage(message: string) {
|
||||
|
||||
|
||||
|
||||
function isPhoneValidationMessage(message: string) {
|
||||
|
||||
return message.includes('联系电话') || message.includes('手机号');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function StoreCreatePage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -93,7 +103,6 @@ export default function StoreCreatePage() {
|
||||
|
||||
function reportFormError(message: string) {
|
||||
setSubmitError(message);
|
||||
toastError(message);
|
||||
}
|
||||
|
||||
const stepFromUrl = Number(params.get('step') || 0);
|
||||
@@ -153,7 +162,6 @@ export default function StoreCreatePage() {
|
||||
.catch((e) => {
|
||||
const msg = e instanceof Error ? e.message : '加载开城城市失败';
|
||||
setCitiesError(msg);
|
||||
toastError(msg);
|
||||
});
|
||||
|
||||
}, []);
|
||||
@@ -196,6 +204,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);
|
||||
@@ -260,11 +284,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 {
|
||||
@@ -304,6 +328,10 @@ export default function StoreCreatePage() {
|
||||
const step1Msg = validateStoreStep1(form);
|
||||
|
||||
if (step1Msg) {
|
||||
if (isPhoneValidationMessage(step1Msg)) {
|
||||
setFieldErrors({ phone: step1Msg });
|
||||
return;
|
||||
}
|
||||
reportFormError(step1Msg);
|
||||
goStep(1);
|
||||
return;
|
||||
@@ -341,13 +369,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);
|
||||
|
||||
@@ -396,11 +427,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);
|
||||
@@ -677,15 +707,7 @@ export default function StoreCreatePage() {
|
||||
|
||||
onWechatReadyChange={() => { void refresh(); }}
|
||||
|
||||
onChange={(nextUrl) => {
|
||||
|
||||
const envPhotoUrls = [...form.envPhotoUrls];
|
||||
|
||||
envPhotoUrls[index] = nextUrl;
|
||||
|
||||
patchForm({ envPhotoUrls });
|
||||
|
||||
}}
|
||||
onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)}
|
||||
|
||||
/>
|
||||
|
||||
@@ -757,21 +779,43 @@ export default function StoreCreatePage() {
|
||||
|
||||
</div>
|
||||
|
||||
<section className="partner-form-card partner-store-phone-summary">
|
||||
<section className="partner-form-card">
|
||||
|
||||
<div className="partner-store-phone-summary-main">
|
||||
<div className="partner-field">
|
||||
|
||||
<span className="label-md text-muted">门店登录手机号</span>
|
||||
<label>门店登录手机号 <span className="text-primary">*</span></label>
|
||||
|
||||
<span className="body-md">{form.phone.trim() || '未填写'}</span>
|
||||
<div className="partner-field-input">
|
||||
|
||||
<span className="material-symbols-outlined">call</span>
|
||||
|
||||
<input
|
||||
|
||||
type="tel"
|
||||
|
||||
placeholder="请输入11位手机号"
|
||||
|
||||
value={form.phone}
|
||||
|
||||
onChange={(e) => patchForm({ phone: e.target.value })}
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="button" className="partner-weekly-link" onClick={() => goStep(1)}>
|
||||
{fieldErrors.phone && (
|
||||
|
||||
修改
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
|
||||
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
|
||||
该手机号将作为门店端登录账号,提交前会再次校验是否已被占用。
|
||||
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export default function StoreDetailPage() {
|
||||
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
|
||||
const [statusSaving, setStatusSaving] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [actionError, setActionError] = useState('');
|
||||
|
||||
function applyStore(data: Record<string, unknown>) {
|
||||
setStore(data);
|
||||
@@ -52,6 +53,7 @@ export default function StoreDetailPage() {
|
||||
if (!ok) return;
|
||||
}
|
||||
setStatusSaving(true);
|
||||
setActionError('');
|
||||
try {
|
||||
const updated = await request<Record<string, unknown>>('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<Record<string, unknown>>('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() {
|
||||
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
|
||||
|
||||
<main style={{ padding: '16px 20px' }}>
|
||||
{actionError && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{actionError}</p>}
|
||||
{!subReadonly && (
|
||||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
|
||||
@@ -22,8 +22,6 @@ export interface PartnerStaffItem {
|
||||
export interface CreatePartnerStaffRequest {
|
||||
phone: string;
|
||||
name: string;
|
||||
staffRole: PartnerStaffRole;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface UpdatePartnerStaffRequest {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -10,13 +10,11 @@ export class CreatePartnerStaffDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
/** 未传时服务端默认 INTERNAL */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
staffRole: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
staffRole?: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerStaffDto {
|
||||
|
||||
@@ -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: (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'DISABLED',
|
||||
|
||||
@@ -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 }),
|
||||
@@ -152,6 +156,7 @@ export class AdminPartnersService {
|
||||
address: true,
|
||||
},
|
||||
},
|
||||
parent: { select: { id: true, name: true, phone: true } },
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
|
||||
@@ -123,6 +123,10 @@ export class AdminPartnerAccountsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
isPrimary?: string;
|
||||
}
|
||||
|
||||
export class AdminBenefitCouponsQueryDto extends PaginationQueryDto {
|
||||
|
||||
Reference in New Issue
Block a user