feat(admin): store detail edit with login phone sync; tighten HQ UI
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
HQ store update now syncs store_account.phone for shop login; expand editable store fields. System version only for SUPER_ADMIN; permissions checklist layout polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,8 +52,18 @@ export default function DashboardPage() {
|
|||||||
request<DashboardStats>('/admin/dashboard/stats')
|
request<DashboardStats>('/admin/dashboard/stats')
|
||||||
.then(setStats)
|
.then(setStats)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
request<HqProfile>('/admin/auth/me')
|
||||||
|
.then((p) => {
|
||||||
|
setProfile(p);
|
||||||
|
if (p.adminRole === 'SUPER_ADMIN') {
|
||||||
void loadVersion();
|
void loadVersion();
|
||||||
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
} else {
|
||||||
|
setVersionLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setVersionLoading(false);
|
||||||
|
});
|
||||||
}, [loadVersion]);
|
}, [loadVersion]);
|
||||||
|
|
||||||
function handleDeploy() {
|
function handleDeploy() {
|
||||||
@@ -83,6 +93,7 @@ export default function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4}>数据概览</Typography.Title>
|
<Typography.Title level={4}>数据概览</Typography.Title>
|
||||||
|
|
||||||
|
{isSuperAdmin ? (
|
||||||
<Card
|
<Card
|
||||||
title="系统版本"
|
title="系统版本"
|
||||||
loading={versionLoading}
|
loading={versionLoading}
|
||||||
@@ -92,7 +103,6 @@ export default function DashboardPage() {
|
|||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadVersion()}>
|
<Button icon={<ReloadOutlined />} onClick={() => void loadVersion()}>
|
||||||
刷新版本
|
刷新版本
|
||||||
</Button>
|
</Button>
|
||||||
{isSuperAdmin && (
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<CloudUploadOutlined />}
|
icon={<CloudUploadOutlined />}
|
||||||
@@ -101,7 +111,6 @@ export default function DashboardPage() {
|
|||||||
>
|
>
|
||||||
发布更新
|
发布更新
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -122,6 +131,7 @@ export default function DashboardPage() {
|
|||||||
<Typography.Text type="secondary">尚未记录发版信息</Typography.Text>
|
<Typography.Text type="secondary">尚未记录发版信息</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
Divider,
|
|
||||||
Form,
|
Form,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
@@ -36,6 +35,13 @@ const ROLE_LABELS = Object.fromEntries(HQ_ADMIN_ROLES.map((r) => [r.value, r.lab
|
|||||||
|
|
||||||
const CATALOG_GROUPS = [...new Set(HQ_PERMISSION_CATALOG.map((p) => p.group ?? '其他'))];
|
const CATALOG_GROUPS = [...new Set(HQ_PERMISSION_CATALOG.map((p) => p.group ?? '其他'))];
|
||||||
|
|
||||||
|
function groupColSpan(itemCount: number): number {
|
||||||
|
if (itemCount <= 2) return 12;
|
||||||
|
if (itemCount === 3) return 8;
|
||||||
|
if (itemCount === 4) return 6;
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
function PermissionChecklist({
|
function PermissionChecklist({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -54,19 +60,39 @@ function PermissionChecklist({
|
|||||||
>
|
>
|
||||||
{CATALOG_GROUPS.map((group) => {
|
{CATALOG_GROUPS.map((group) => {
|
||||||
const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group);
|
const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group);
|
||||||
|
const span = groupColSpan(items.length);
|
||||||
|
const compact = items.length <= 3;
|
||||||
return (
|
return (
|
||||||
<div key={group} style={{ marginBottom: 16 }}>
|
<div
|
||||||
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
|
key={group}
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text strong style={{ display: 'block', marginBottom: 10 }}>
|
||||||
{group}
|
{group}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Row gutter={[8, 8]}>
|
{compact ? (
|
||||||
|
<Space size={[24, 8]} wrap>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Col key={item.key} span={8}>
|
<Checkbox key={item.key} value={item.key}>
|
||||||
|
{item.label}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[12, 10]}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Col key={item.key} xs={24} sm={12} md={span}>
|
||||||
<Checkbox value={item.key}>{item.label}</Checkbox>
|
<Checkbox value={item.key}>{item.label}</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
<Divider style={{ margin: '12px 0 0' }} />
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -88,6 +114,12 @@ export default function HqPermissionsPage() {
|
|||||||
const [accountLoading, setAccountLoading] = useState(false);
|
const [accountLoading, setAccountLoading] = useState(false);
|
||||||
const [accountSaving, setAccountSaving] = useState(false);
|
const [accountSaving, setAccountSaving] = useState(false);
|
||||||
|
|
||||||
|
const selectedAccount = useMemo(
|
||||||
|
() => accounts.find((a) => a.id === accountId),
|
||||||
|
[accounts, accountId],
|
||||||
|
);
|
||||||
|
const selectedIsSuperAdmin = selectedAccount?.adminRole === 'SUPER_ADMIN';
|
||||||
|
|
||||||
const previewEffectiveKeys = useMemo(
|
const previewEffectiveKeys = useMemo(
|
||||||
() => [...new Set([...roleInheritedKeys, ...accountKeys])],
|
() => [...new Set([...roleInheritedKeys, ...accountKeys])],
|
||||||
[roleInheritedKeys, accountKeys],
|
[roleInheritedKeys, accountKeys],
|
||||||
@@ -243,32 +275,57 @@ export default function HqPermissionsPage() {
|
|||||||
<Alert type="info" showIcon message="请先选择要配置的 HQ 账户" />
|
<Alert type="info" showIcon message="请先选择要配置的 HQ 账户" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Space wrap style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<span>角色继承:</span>
|
<span style={{ marginRight: 8 }}>角色继承:</span>
|
||||||
|
{selectedIsSuperAdmin ? (
|
||||||
|
<Tag color="blue">超级管理员基础权限(不含危险操作)</Tag>
|
||||||
|
) : (
|
||||||
|
<Space wrap size={[4, 4]}>
|
||||||
{roleInheritedKeys.map((key) => {
|
{roleInheritedKeys.map((key) => {
|
||||||
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
|
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
|
||||||
return (
|
return (
|
||||||
<Tag key={key} color="blue">
|
<Tag key={key} color="blue">
|
||||||
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
|
{item?.label || key}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{!roleInheritedKeys.length ? (
|
||||||
|
<Typography.Text type="secondary">无</Typography.Text>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
下方勾选为用户专属追加权限(保存后与角色权限合并生效)。危险操作(删除用户/订单/城市)默认不授予,需在此勾选。
|
下方勾选为用户专属追加权限(保存后与角色权限合并生效)。危险操作(删除用户/订单/城市)默认不授予,需在此勾选。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
|
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
|
||||||
<Space wrap style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<span>合并生效:</span>
|
<span style={{ marginRight: 8 }}>合并生效:</span>
|
||||||
{previewEffectiveKeys.map((key) => {
|
{selectedIsSuperAdmin ? (
|
||||||
|
<Space wrap size={[4, 4]}>
|
||||||
|
<Tag>基础权限(全部)</Tag>
|
||||||
|
{accountKeys.map((key) => {
|
||||||
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
|
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
|
||||||
return (
|
return (
|
||||||
<Tag key={key}>
|
<Tag key={key} color="orange">
|
||||||
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
|
{item?.label || key}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space wrap size={[4, 4]}>
|
||||||
|
{previewEffectiveKeys.map((key) => {
|
||||||
|
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
|
||||||
|
return (
|
||||||
|
<Tag key={key}>
|
||||||
|
{item?.label || key}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<Button type="primary" loading={accountSaving} onClick={() => void saveAccountPermissions()}>
|
<Button type="primary" loading={accountSaving} onClick={() => void saveAccountPermissions()}>
|
||||||
保存用户权限
|
保存用户权限
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Steps,
|
Steps,
|
||||||
Table,
|
Table,
|
||||||
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
@@ -352,11 +353,14 @@ export default function StoresPage() {
|
|||||||
const [cities, setCities] = useState<CityOption[]>([]);
|
const [cities, setCities] = useState<CityOption[]>([]);
|
||||||
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
|
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
|
||||||
const [optionsLoading, setOptionsLoading] = useState(false);
|
const [optionsLoading, setOptionsLoading] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [phoneMismatch, setPhoneMismatch] = useState<string | null>(null);
|
||||||
|
|
||||||
const selectedPartnerId = Form.useWatch('partnerAccountId', createForm);
|
const selectedPartnerId = Form.useWatch('partnerAccountId', createForm);
|
||||||
const selectedRegionCodes = Form.useWatch('regionCodes', createForm);
|
const selectedRegionCodes = Form.useWatch('regionCodes', createForm);
|
||||||
const selectedCityId = Form.useWatch('cityId', createForm);
|
const selectedCityId = Form.useWatch('cityId', createForm);
|
||||||
const selectedCategoryParentId = Form.useWatch('categoryParentId', createForm);
|
const selectedCategoryParentId = Form.useWatch('categoryParentId', createForm);
|
||||||
|
const editCategoryParentId = Form.useWatch('categoryParentId', editForm);
|
||||||
|
|
||||||
const categoryParentOptions = useMemo(
|
const categoryParentOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -372,6 +376,130 @@ export default function StoresPage() {
|
|||||||
.map((n) => ({ value: n.id, label: n.name }));
|
.map((n) => ({ value: n.id, label: n.name }));
|
||||||
}, [categoryTree, selectedCategoryParentId]);
|
}, [categoryTree, selectedCategoryParentId]);
|
||||||
|
|
||||||
|
const editCategoryChildOptions = useMemo(() => {
|
||||||
|
const parent = categoryTree.find((n) => n.id === editCategoryParentId);
|
||||||
|
return (parent?.children ?? [])
|
||||||
|
.filter((n) => n.status !== 'INACTIVE')
|
||||||
|
.map((n) => ({ value: n.id, label: n.name }));
|
||||||
|
}, [categoryTree, editCategoryParentId]);
|
||||||
|
|
||||||
|
async function openStoreDetail(row: StoreRow) {
|
||||||
|
const [d, cats] = await Promise.all([
|
||||||
|
request<Record<string, unknown>>(`/admin/stores/${row.id}`),
|
||||||
|
request<CategoryNode[]>('/admin/store-categories').catch(() => [] as CategoryNode[]),
|
||||||
|
]);
|
||||||
|
setCategoryTree(Array.isArray(cats) ? cats : []);
|
||||||
|
setDetail(d);
|
||||||
|
const category = d.category && typeof d.category === 'object'
|
||||||
|
? (d.category as { id?: string; parentId?: string | null })
|
||||||
|
: null;
|
||||||
|
const account = d.account && typeof d.account === 'object'
|
||||||
|
? (d.account as {
|
||||||
|
phone?: string | null;
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const categoryId = category?.id != null ? String(category.id) : undefined;
|
||||||
|
let parentId = category?.parentId != null ? String(category.parentId) : undefined;
|
||||||
|
if (!parentId && categoryId) {
|
||||||
|
for (const parent of cats) {
|
||||||
|
if (parent.id === categoryId) {
|
||||||
|
parentId = parent.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((parent.children ?? []).some((c) => c.id === categoryId)) {
|
||||||
|
parentId = parent.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loginPhone =
|
||||||
|
(typeof d.loginPhone === 'string' && d.loginPhone) ||
|
||||||
|
account?.phone ||
|
||||||
|
(typeof d.phone === 'string' ? d.phone : undefined);
|
||||||
|
const storePhone = typeof d.phone === 'string' ? d.phone : undefined;
|
||||||
|
const phoneMismatchNow = !!(loginPhone && storePhone && loginPhone !== storePhone);
|
||||||
|
setPhoneMismatch(phoneMismatchNow ? String(loginPhone) : null);
|
||||||
|
editForm.setFieldsValue({
|
||||||
|
name: d.name,
|
||||||
|
// 以门店手机号为准保存;若与账号登录号不一致,保存时会强制同步到登录账号
|
||||||
|
phone: storePhone || loginPhone,
|
||||||
|
intro: d.intro,
|
||||||
|
benefitUsageRule: d.benefitUsageRule,
|
||||||
|
coverUrl: d.coverUrl,
|
||||||
|
province: d.province,
|
||||||
|
city: d.cityName,
|
||||||
|
district: d.district,
|
||||||
|
address: d.address,
|
||||||
|
categoryParentId: parentId,
|
||||||
|
categoryId,
|
||||||
|
latitude: d.latitude != null ? Number(d.latitude) : undefined,
|
||||||
|
longitude: d.longitude != null ? Number(d.longitude) : undefined,
|
||||||
|
settlementRate: d.settlementRate != null ? Number(d.settlementRate) * 100 : 60,
|
||||||
|
openTime: d.openTime || '10:00',
|
||||||
|
closeTime: d.closeTime || '22:00',
|
||||||
|
openTime2: d.openTime2 || undefined,
|
||||||
|
closeTime2: d.closeTime2 || undefined,
|
||||||
|
avgPrice: d.avgPrice != null ? Number(d.avgPrice) : undefined,
|
||||||
|
bankAccountName: account?.bankAccountName || undefined,
|
||||||
|
bankAccountNo: account?.bankAccountNo || undefined,
|
||||||
|
bankBranch: account?.bankBranch || undefined,
|
||||||
|
});
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStoreDetail() {
|
||||||
|
if (!detail) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const v = await editForm.validateFields();
|
||||||
|
const hasCoords =
|
||||||
|
v.latitude != null &&
|
||||||
|
v.longitude != null &&
|
||||||
|
Number.isFinite(Number(v.latitude)) &&
|
||||||
|
Number.isFinite(Number(v.longitude));
|
||||||
|
const payload = {
|
||||||
|
name: v.name,
|
||||||
|
phone: v.phone,
|
||||||
|
coverUrl: v.coverUrl,
|
||||||
|
intro: v.intro,
|
||||||
|
benefitUsageRule: v.benefitUsageRule ?? null,
|
||||||
|
categoryId: v.categoryId,
|
||||||
|
province: v.province,
|
||||||
|
city: v.city,
|
||||||
|
district: v.district,
|
||||||
|
address: v.address,
|
||||||
|
avgPrice: v.avgPrice,
|
||||||
|
openTime: v.openTime,
|
||||||
|
closeTime: v.closeTime,
|
||||||
|
openTime2: v.openTime2 || null,
|
||||||
|
closeTime2: v.closeTime2 || null,
|
||||||
|
settlementRate: v.settlementRate != null ? Number(v.settlementRate) / 100 : undefined,
|
||||||
|
bankAccountName: v.bankAccountName ?? null,
|
||||||
|
bankAccountNo: v.bankAccountNo ?? null,
|
||||||
|
bankBranch: v.bankBranch ?? null,
|
||||||
|
...(hasCoords
|
||||||
|
? { latitude: Number(v.latitude), longitude: Number(v.longitude) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
const updated = await request<Record<string, unknown>>(`/admin/stores/${detail.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
message.success('门店信息已保存');
|
||||||
|
setDetail(updated);
|
||||||
|
setPhoneMismatch(null);
|
||||||
|
void reload();
|
||||||
|
} catch (e) {
|
||||||
|
if (e && typeof e === 'object' && 'errorFields' in e) return;
|
||||||
|
message.error(e instanceof Error ? e.message : '保存失败');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bindRegionSelection(codes: string[], partnerAccountId?: string) {
|
function bindRegionSelection(codes: string[], partnerAccountId?: string) {
|
||||||
const binding = resolveRegionBinding(codes, cities, partnerAccountId ?? selectedPartnerId);
|
const binding = resolveRegionBinding(codes, cities, partnerAccountId ?? selectedPartnerId);
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
@@ -398,7 +526,7 @@ export default function StoresPage() {
|
|||||||
return `区划 ${binding.cityCode} 暂未开城,请先在「开城 → 城市」添加`;
|
return `区划 ${binding.cityCode} 暂未开城,请先在「开城 → 城市」添加`;
|
||||||
}, [selectedRegionCodes, cities, selectedPartnerId]);
|
}, [selectedRegionCodes, cities, selectedPartnerId]);
|
||||||
|
|
||||||
async function loadOptions() {
|
async function loadOptions(opts?: { quiet?: boolean }) {
|
||||||
setOptionsLoading(true);
|
setOptionsLoading(true);
|
||||||
try {
|
try {
|
||||||
const qs = `pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`;
|
const qs = `pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`;
|
||||||
@@ -410,11 +538,13 @@ export default function StoresPage() {
|
|||||||
setPartners(p.items);
|
setPartners(p.items);
|
||||||
setCities(c.items);
|
setCities(c.items);
|
||||||
setCategoryTree(Array.isArray(cats) ? cats : []);
|
setCategoryTree(Array.isArray(cats) ? cats : []);
|
||||||
|
if (!opts?.quiet) {
|
||||||
if (!p.items.length) message.warning('暂无开城合伙人,请先在「开城 → 城市」详情中创建合伙人');
|
if (!p.items.length) message.warning('暂无开城合伙人,请先在「开城 → 城市」详情中创建合伙人');
|
||||||
if (!c.items.length) message.warning('暂无开城城市,请先在「开城 → 城市」中创建');
|
if (!c.items.length) message.warning('暂无开城城市,请先在「开城 → 城市」中创建');
|
||||||
if (!Array.isArray(cats) || !cats.length) {
|
if (!Array.isArray(cats) || !cats.length) {
|
||||||
message.warning('暂无门店分类,请先在「门店 → 门店分类」中配置');
|
message.warning('暂无门店分类,请先在「门店 → 门店分类」中配置');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e instanceof Error ? e.message : '加载合伙人/城市/分类失败');
|
message.error(e instanceof Error ? e.message : '加载合伙人/城市/分类失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -579,28 +709,7 @@ export default function StoresPage() {
|
|||||||
{
|
{
|
||||||
title: '操作', width: 80,
|
title: '操作', width: 80,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Button type="link" size="small" onClick={async () => {
|
<Button type="link" size="small" onClick={() => void openStoreDetail(row)}>详情</Button>
|
||||||
const d = await request<Record<string, unknown>>(`/admin/stores/${row.id}`);
|
|
||||||
setDetail(d);
|
|
||||||
editForm.setFieldsValue({
|
|
||||||
name: d.name,
|
|
||||||
phone: d.phone,
|
|
||||||
intro: d.intro,
|
|
||||||
benefitUsageRule: d.benefitUsageRule,
|
|
||||||
coverUrl: d.coverUrl,
|
|
||||||
address: d.address,
|
|
||||||
district: d.district,
|
|
||||||
latitude: d.latitude != null ? Number(d.latitude) : undefined,
|
|
||||||
longitude: d.longitude != null ? Number(d.longitude) : undefined,
|
|
||||||
settlementRate: d.settlementRate != null ? Number(d.settlementRate) * 100 : 60,
|
|
||||||
openTime: d.openTime || '10:00',
|
|
||||||
closeTime: d.closeTime || '22:00',
|
|
||||||
openTime2: d.openTime2 || undefined,
|
|
||||||
closeTime2: d.closeTime2 || undefined,
|
|
||||||
avgPrice: d.avgPrice != null ? Number(d.avgPrice) : undefined,
|
|
||||||
});
|
|
||||||
setDrawerOpen(true);
|
|
||||||
}}>详情</Button>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -670,7 +779,7 @@ export default function StoresPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<Select defaultValue={String(detail.status)} style={{ width: 120 }}
|
<Select value={String(detail.status)} style={{ width: 120 }}
|
||||||
options={Object.entries(STORE_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
options={Object.entries(STORE_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||||
onChange={async (status) => {
|
onChange={async (status) => {
|
||||||
await request(`/admin/stores/${detail.id}/status`, { method: 'PUT', body: JSON.stringify({ status }) });
|
await request(`/admin/stores/${detail.id}/status`, { method: 'PUT', body: JSON.stringify({ status }) });
|
||||||
@@ -678,54 +787,22 @@ export default function StoresPage() {
|
|||||||
setDetail({ ...detail, status });
|
setDetail({ ...detail, status });
|
||||||
void reload();
|
void reload();
|
||||||
}} />
|
}} />
|
||||||
<Button type="primary" onClick={async () => {
|
<Button type="primary" loading={saving} onClick={() => void saveStoreDetail()}>保存修改</Button>
|
||||||
const v = await editForm.validateFields();
|
|
||||||
const hasCoords =
|
|
||||||
v.latitude != null &&
|
|
||||||
v.longitude != null &&
|
|
||||||
Number.isFinite(Number(v.latitude)) &&
|
|
||||||
Number.isFinite(Number(v.longitude));
|
|
||||||
const payload = {
|
|
||||||
name: v.name,
|
|
||||||
phone: v.phone,
|
|
||||||
coverUrl: v.coverUrl,
|
|
||||||
intro: v.intro,
|
|
||||||
benefitUsageRule: v.benefitUsageRule ?? null,
|
|
||||||
district: v.district,
|
|
||||||
address: v.address,
|
|
||||||
avgPrice: v.avgPrice,
|
|
||||||
openTime: v.openTime,
|
|
||||||
closeTime: v.closeTime,
|
|
||||||
openTime2: v.openTime2 || null,
|
|
||||||
closeTime2: v.closeTime2 || null,
|
|
||||||
settlementRate: v.settlementRate != null ? Number(v.settlementRate) / 100 : undefined,
|
|
||||||
...(hasCoords
|
|
||||||
? { latitude: Number(v.latitude), longitude: Number(v.longitude) }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
await request(`/admin/stores/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
||||||
message.success('已保存');
|
|
||||||
setDetail({
|
|
||||||
...detail,
|
|
||||||
...payload,
|
|
||||||
settlementRate: payload.settlementRate,
|
|
||||||
latitude: hasCoords ? Number(v.latitude) : detail.latitude,
|
|
||||||
longitude: hasCoords ? Number(v.longitude) : detail.longitude,
|
|
||||||
});
|
|
||||||
void reload();
|
|
||||||
}}>保存</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
)}>
|
)}>
|
||||||
{detail && (
|
{detail && (
|
||||||
|
<Form form={editForm} layout="vertical">
|
||||||
|
<Tabs
|
||||||
|
destroyInactiveTabPane={false}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'basic',
|
||||||
|
label: '基本信息',
|
||||||
|
forceRender: true,
|
||||||
|
children: (
|
||||||
<>
|
<>
|
||||||
<StoreAuditMediaSection detail={detail} />
|
|
||||||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||||||
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
|
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="门店分类">
|
|
||||||
{detail.category && typeof detail.category === 'object' && 'name' in detail.category
|
|
||||||
? String((detail.category as { name?: string }).name || '—')
|
|
||||||
: '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="审核状态">
|
<Descriptions.Item label="审核状态">
|
||||||
<Tag color={
|
<Tag color={
|
||||||
String(detail.auditStatus) === 'PENDING' ? 'orange'
|
String(detail.auditStatus) === 'PENDING' ? 'orange'
|
||||||
@@ -737,45 +814,6 @@ export default function StoresPage() {
|
|||||||
{String(detail.auditStatus) === 'REJECTED' ? (
|
{String(detail.auditStatus) === 'REJECTED' ? (
|
||||||
<Descriptions.Item label="驳回原因">{String(detail.rejectReason || '—')}</Descriptions.Item>
|
<Descriptions.Item label="驳回原因">{String(detail.rejectReason || '—')}</Descriptions.Item>
|
||||||
) : null}
|
) : null}
|
||||||
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="经纬度">
|
|
||||||
{detail.latitude != null && detail.longitude != null
|
|
||||||
? `${Number(detail.latitude).toFixed(6)}, ${Number(detail.longitude).toFixed(6)}`
|
|
||||||
: '未设置'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="营业时间">
|
|
||||||
{[
|
|
||||||
detail.openTime && detail.closeTime
|
|
||||||
? `${String(detail.openTime)}-${String(detail.closeTime)}`
|
|
||||||
: null,
|
|
||||||
detail.openTime2 && detail.closeTime2
|
|
||||||
? `${String(detail.openTime2)}-${String(detail.closeTime2)}`
|
|
||||||
: null,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(',') || '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="人均费用">
|
|
||||||
{detail.avgPrice != null ? `¥${Number(detail.avgPrice).toFixed(0)}` : '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="核销结算比例">
|
|
||||||
{detail.settlementRate != null ? `${Math.round(Number(detail.settlementRate) * 100)}%` : '60%'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="结算户名">
|
|
||||||
{detail.account && typeof detail.account === 'object' && 'bankAccountName' in detail.account
|
|
||||||
? String((detail.account as { bankAccountName?: string | null }).bankAccountName || '—')
|
|
||||||
: '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="开户银行">
|
|
||||||
{detail.account && typeof detail.account === 'object' && 'bankBranch' in detail.account
|
|
||||||
? String((detail.account as { bankBranch?: string | null }).bankBranch || '—')
|
|
||||||
: '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="银行卡号">
|
|
||||||
{detail.account && typeof detail.account === 'object' && 'bankAccountNo' in detail.account
|
|
||||||
? String((detail.account as { bankAccountNo?: string | null }).bankAccountNo || '—')
|
|
||||||
: '—'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="核销数">{String(detail.redeemCount ?? 0)}</Descriptions.Item>
|
<Descriptions.Item label="核销数">{String(detail.redeemCount ?? 0)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="操作">
|
<Descriptions.Item label="操作">
|
||||||
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => navigate(`/logs/stores?storeId=${detail.id}`)}>
|
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => navigate(`/logs/stores?storeId=${detail.id}`)}>
|
||||||
@@ -794,10 +832,51 @@ export default function StoresPage() {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<Form form={editForm} layout="vertical">
|
<Typography.Title level={5} style={{ marginTop: 0 }}>编辑门店信息</Typography.Title>
|
||||||
|
{phoneMismatch ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message={`登录账号手机号仍为 ${phoneMismatch},与门店手机号不一致。请点击右上角「保存修改」同步,否则门店端无法用新号登录。`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
<Form.Item name="phone" label="电话" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item
|
||||||
<Form.Item name="coverUrl" label="封面图">
|
name="phone"
|
||||||
|
label="登录手机号"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
extra="门店端短信登录使用此号码;修改后需用新号重新登录"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="categoryParentId"
|
||||||
|
label="门店分类(大类)"
|
||||||
|
rules={[{ required: true, message: '请选择门店大类' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
loading={optionsLoading}
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={categoryParentOptions}
|
||||||
|
onChange={() => editForm.setFieldValue('categoryId', undefined)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="categoryId"
|
||||||
|
label="门店分类(细类)"
|
||||||
|
rules={[{ required: true, message: '请选择门店细类' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
placeholder={editCategoryParentId ? '选择细类' : '请先选大类'}
|
||||||
|
disabled={!editCategoryParentId}
|
||||||
|
options={editCategoryChildOptions}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="coverUrl" label="封面图 / 门头照">
|
||||||
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} showCount maxLength={500} /></Form.Item>
|
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} showCount maxLength={500} /></Form.Item>
|
||||||
@@ -808,8 +887,20 @@ export default function StoresPage() {
|
|||||||
>
|
>
|
||||||
<Input.TextArea rows={4} placeholder="选填,最多1000字" showCount maxLength={1000} />
|
<Input.TextArea rows={4} placeholder="选填,最多1000字" showCount maxLength={1000} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
<Space wrap style={{ width: '100%' }}>
|
||||||
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
|
<Form.Item name="province" label="省份" rules={[{ required: true }]}>
|
||||||
|
<Input style={{ width: 140 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="city" label="城市" rules={[{ required: true }]}>
|
||||||
|
<Input style={{ width: 140 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="district" label="区县" rules={[{ required: true }]}>
|
||||||
|
<Input style={{ width: 140 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Form.Item name="address" label="详细地址" rules={[{ required: true }]}>
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
<Space wrap style={{ width: '100%' }}>
|
<Space wrap style={{ width: '100%' }}>
|
||||||
<Form.Item name="latitude" label="纬度" style={{ marginBottom: 8 }}>
|
<Form.Item name="latitude" label="纬度" style={{ marginBottom: 8 }}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@@ -849,9 +940,6 @@ export default function StoresPage() {
|
|||||||
>
|
>
|
||||||
腾讯地图选点
|
腾讯地图选点
|
||||||
</Button>
|
</Button>
|
||||||
<Typography.Text type="secondary">
|
|
||||||
搜索或拖图确认位置后自动填入经纬度
|
|
||||||
</Typography.Text>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Form.Item name="avgPrice" label="人均费用(选填)">
|
<Form.Item name="avgPrice" label="人均费用(选填)">
|
||||||
<InputNumber min={0} precision={0} style={{ width: '100%' }} addonAfter="元" placeholder="用户端展示" />
|
<InputNumber min={0} precision={0} style={{ width: '100%' }} addonAfter="元" placeholder="用户端展示" />
|
||||||
@@ -872,11 +960,55 @@ export default function StoresPage() {
|
|||||||
<Input type="time" style={{ width: 140 }} />
|
<Input type="time" style={{ width: 140 }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'settlement',
|
||||||
|
label: '结算资质',
|
||||||
|
forceRender: true,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true }]}>
|
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true }]}>
|
||||||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
<Form.Item name="bankAccountName" label="结算户名">
|
||||||
|
<Input placeholder="开户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="bankBranch" label="开户银行">
|
||||||
|
<Input placeholder="如 中国工商银行某某支行" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="bankAccountNo"
|
||||||
|
label="银行卡号"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
const v = String(value || '').trim();
|
||||||
|
if (!v) return;
|
||||||
|
if (!/^\d{16,19}$/.test(v)) {
|
||||||
|
throw new Error('银行卡号须为 16–19 位数字');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="16–19 位数字" maxLength={19} />
|
||||||
|
</Form.Item>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
修改后点右上角「保存修改」一并提交。
|
||||||
|
</Typography.Paragraph>
|
||||||
</>
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'media',
|
||||||
|
label: '审核材料',
|
||||||
|
children: <StoreAuditMediaSection detail={detail} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||||
import { AdminDashboardService } from './admin-dashboard.service';
|
import { AdminDashboardService } from './admin-dashboard.service';
|
||||||
|
|
||||||
@Controller('admin/dashboard')
|
@Controller('admin/dashboard')
|
||||||
@@ -13,6 +14,7 @@ export class AdminDashboardController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('version')
|
@Get('version')
|
||||||
|
@UseGuards(SuperAdminGuard)
|
||||||
version() {
|
version() {
|
||||||
return this.dashboardService.getLatestVersion();
|
return this.dashboardService.getLatestVersion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export class AdminStoresService {
|
|||||||
return serializeBigInt(mapStoreCompat({
|
return serializeBigInt(mapStoreCompat({
|
||||||
...store,
|
...store,
|
||||||
account: store.bindings[0]?.storeAccount ?? null,
|
account: store.bindings[0]?.storeAccount ?? null,
|
||||||
|
/** 门店端登录手机号(store_account.phone),与 store.phone 应对齐 */
|
||||||
|
loginPhone: store.bindings[0]?.storeAccount?.phone ?? store.phone,
|
||||||
bindings: undefined,
|
bindings: undefined,
|
||||||
media,
|
media,
|
||||||
audits,
|
audits,
|
||||||
@@ -208,17 +210,89 @@ export class AdminStoresService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = await this.prisma.store.update({
|
let categoryId: bigint | undefined;
|
||||||
|
if (dto.categoryId !== undefined) {
|
||||||
|
if (!dto.categoryId?.trim()) {
|
||||||
|
throw new BadRequestException('请选择门店分类');
|
||||||
|
}
|
||||||
|
categoryId = BigInt(dto.categoryId);
|
||||||
|
await this.storeCategoryService.assertLeafCategoryId(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.phone !== undefined) {
|
||||||
|
const normalizedPhone = dto.phone.trim();
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
|
||||||
|
throw new BadRequestException('请输入正确的手机号码');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPhone = dto.phone !== undefined ? dto.phone.trim() : undefined;
|
||||||
|
const bankTouched =
|
||||||
|
dto.bankAccountName !== undefined ||
|
||||||
|
dto.bankAccountNo !== undefined ||
|
||||||
|
dto.bankBranch !== undefined;
|
||||||
|
const needAccountSync =
|
||||||
|
normalizedPhone !== undefined || dto.name !== undefined || bankTouched;
|
||||||
|
|
||||||
|
// 登录凭证在 store_account.phone;必须与门店展示手机号同步
|
||||||
|
let primaryBinding: {
|
||||||
|
storeAccount: { id: bigint; phone: string; name: string } | null;
|
||||||
|
} | null = null;
|
||||||
|
if (needAccountSync) {
|
||||||
|
primaryBinding = await this.prisma.storeAccountStore.findFirst({
|
||||||
|
where: { storeId: id, storeAccount: { isPrimary: 1 } },
|
||||||
|
include: {
|
||||||
|
storeAccount: { select: { id: true, phone: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!primaryBinding) {
|
||||||
|
primaryBinding = await this.prisma.storeAccountStore.findFirst({
|
||||||
|
where: { storeId: id },
|
||||||
|
include: {
|
||||||
|
storeAccount: { select: { id: true, phone: true, name: true } },
|
||||||
|
},
|
||||||
|
orderBy: { storeAccountId: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (normalizedPhone !== undefined && !primaryBinding?.storeAccount) {
|
||||||
|
throw new BadRequestException('门店未绑定登录账号,无法修改手机号');
|
||||||
|
}
|
||||||
|
const primaryAccount = primaryBinding?.storeAccount;
|
||||||
|
if (
|
||||||
|
normalizedPhone !== undefined &&
|
||||||
|
primaryAccount &&
|
||||||
|
normalizedPhone !== primaryAccount.phone
|
||||||
|
) {
|
||||||
|
const occupied = await this.prisma.storeAccount.findUnique({
|
||||||
|
where: { phone: normalizedPhone },
|
||||||
|
});
|
||||||
|
if (occupied && occupied.id !== primaryAccount.id) {
|
||||||
|
throw new BadRequestException('该手机号已被其他门店账号使用');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dto.bankAccountNo !== undefined) {
|
||||||
|
const no = dto.bankAccountNo?.trim() || null;
|
||||||
|
if (no && !/^\d{16,19}$/.test(no)) {
|
||||||
|
throw new BadRequestException('银行卡号须为 16–19 位数字');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.store.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||||
...(dto.phone !== undefined ? { phone: dto.phone } : {}),
|
...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}),
|
||||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||||
...(dto.benefitUsageRule !== undefined
|
...(dto.benefitUsageRule !== undefined
|
||||||
? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null }
|
? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null }
|
||||||
: {}),
|
: {}),
|
||||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
...(dto.province !== undefined ? { province: dto.province.trim() } : {}),
|
||||||
|
...(dto.city !== undefined ? { cityName: dto.city.trim() } : {}),
|
||||||
|
...(dto.district !== undefined ? { district: dto.district.trim() } : {}),
|
||||||
|
...(categoryId !== undefined ? { categoryId } : {}),
|
||||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||||
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
|
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
|
||||||
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
|
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
|
||||||
@@ -231,12 +305,12 @@ export class AdminStoresService {
|
|||||||
|
|
||||||
if (dto.coverUrl) {
|
if (dto.coverUrl) {
|
||||||
if (current.coverResourceId) {
|
if (current.coverResourceId) {
|
||||||
await this.prisma.commonResource.update({
|
await tx.commonResource.update({
|
||||||
where: { id: current.coverResourceId },
|
where: { id: current.coverResourceId },
|
||||||
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const cover = await this.prisma.commonResource.create({
|
const cover = await tx.commonResource.create({
|
||||||
data: {
|
data: {
|
||||||
ownerType: 'STORE',
|
ownerType: 'STORE',
|
||||||
ownerId: id,
|
ownerId: id,
|
||||||
@@ -247,10 +321,41 @@ export class AdminStoresService {
|
|||||||
url: dto.coverUrl,
|
url: dto.coverUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await this.prisma.store.update({ where: { id }, data: { coverResourceId: cover.id } });
|
await tx.store.update({ where: { id }, data: { coverResourceId: cover.id } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (primaryBinding?.storeAccount) {
|
||||||
|
const account = primaryBinding.storeAccount;
|
||||||
|
const accountData: {
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
} = {};
|
||||||
|
if (dto.name !== undefined) accountData.name = dto.name.trim();
|
||||||
|
if (normalizedPhone !== undefined && normalizedPhone !== account.phone) {
|
||||||
|
accountData.phone = normalizedPhone;
|
||||||
|
}
|
||||||
|
if (dto.bankAccountName !== undefined) {
|
||||||
|
accountData.bankAccountName = dto.bankAccountName?.trim() || null;
|
||||||
|
}
|
||||||
|
if (dto.bankAccountNo !== undefined) {
|
||||||
|
accountData.bankAccountNo = dto.bankAccountNo?.trim() || null;
|
||||||
|
}
|
||||||
|
if (dto.bankBranch !== undefined) {
|
||||||
|
accountData.bankBranch = dto.bankBranch?.trim() || null;
|
||||||
|
}
|
||||||
|
if (Object.keys(accountData).length) {
|
||||||
|
await tx.storeAccount.update({
|
||||||
|
where: { id: account.id },
|
||||||
|
data: accountData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return this.detailStore(id);
|
return this.detailStore(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,10 +162,22 @@ export class UpdateStoreDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
province?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
city?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
district?: string;
|
district?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
categoryId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@@ -191,6 +203,18 @@ export class UpdateStoreDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
avgPrice?: number | null;
|
avgPrice?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bankBranch?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateStoreAccountDto {
|
export class CreateStoreAccountDto {
|
||||||
|
|||||||
Reference in New Issue
Block a user