webadmin增加显示子账号显示列表

修复api服务端报错
This commit is contained in:
2026-07-10 10:36:01 +08:00
parent b1d5b3214f
commit 2368121972
6 changed files with 401 additions and 45 deletions
+1
View File
@@ -5,6 +5,7 @@ export const HQ_OPERATION_ACTION_OPTIONS = [
{ value: 'PARTNER_UPDATE', label: '编辑城市合伙人' }, { value: 'PARTNER_UPDATE', label: '编辑城市合伙人' },
{ value: 'PARTNER_ACCOUNT_CREATE', label: '新增合伙人账户' }, { value: 'PARTNER_ACCOUNT_CREATE', label: '新增合伙人账户' },
{ value: 'PARTNER_ACCOUNT_UPDATE', label: '编辑合伙人账户' }, { value: 'PARTNER_ACCOUNT_UPDATE', label: '编辑合伙人账户' },
{ value: 'PARTNER_ACCOUNT_DELETE', label: '删除合伙人子账号' },
{ value: 'HQ_ACCOUNT_CREATE', label: '新增 HQ 管理员' }, { value: 'HQ_ACCOUNT_CREATE', label: '新增 HQ 管理员' },
{ value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员' }, { value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员' },
{ value: 'HQ_PERMISSION_UPDATE', label: '配置 HQ 权限' }, { value: 'HQ_PERMISSION_UPDATE', label: '配置 HQ 权限' },
+273 -39
View File
@@ -1,15 +1,27 @@
import { useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
Button, Drawer, Form, Input, Modal, Select, Space, Table, Tabs, Tag, Typography, message, Button, Drawer, Form, Input, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Typography, message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { PARTNER_STAFF_ROLE_LABELS } from '@dukang/shared-types';
import { request, type Paginated } from '../lib/api'; import { request, type Paginated } from '../lib/api';
import { AdminCellLine } from '../components/AdminCellLine';
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; 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 Row = { type PartnerOption = { id: string; companyName: string };
id: string; phone: string; name: string; status: string; isPrimary: number; createdAt: string;
type AccountTreeRow = {
id: string;
phone: string;
name: string;
status: string;
isPrimary: number;
staffRole: string | null;
parentAccountId: string | null;
createdAt: string;
lastLoginAt: string | null;
partner?: { id: string; companyName: string }; partner?: { id: string; companyName: string };
children?: AccountTreeRow[];
}; };
type BillRow = { type BillRow = {
@@ -22,40 +34,94 @@ type OrderRow = {
user?: { userNo: string; phone: string | null }; user?: { userNo: string; phone: string | null };
}; };
type PartnerOption = { id: string; companyName: string }; type Detail = AccountTreeRow & { bills?: BillRow[]; orders?: OrderRow[] };
type Detail = Row & { bills?: BillRow[]; orders?: OrderRow[] }; const STAFF_ROLE_OPTIONS = Object.entries(PARTNER_STAFF_ROLE_LABELS).map(([value, label]) => ({
value,
label,
}));
function filterTree(rows: AccountTreeRow[], phone?: string, status?: string): AccountTreeRow[] {
const phoneQ = phone?.trim();
const match = (row: AccountTreeRow) => {
const phoneOk = !phoneQ || row.phone.includes(phoneQ);
const statusOk = !status || row.status === status;
return phoneOk && statusOk;
};
const walk = (list: AccountTreeRow[]): AccountTreeRow[] =>
list
.map((row) => {
const children = row.children?.length ? walk(row.children) : undefined;
if (match(row) || (children && children.length > 0)) {
return { ...row, children: children?.length ? children : undefined };
}
return null;
})
.filter((row): row is AccountTreeRow => row !== null);
return walk(rows);
}
export default function PartnerAccountsPage() { export default function PartnerAccountsPage() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const [createForm] = Form.useForm(); const [createForm] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({}); const [subForm] = Form.useForm();
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>( const [filters, setFilters] = useState<{ phone?: string; status?: string; partnerId?: string }>({});
'/admin/partner-accounts', const [treeData, setTreeData] = useState<AccountTreeRow[]>([]);
() => { const [loading, setLoading] = useState(false);
const qs = new URLSearchParams(); const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
if (filters.phone) qs.set('phone', filters.phone);
if (filters.status) qs.set('status', filters.status);
return qs;
},
[filters],
);
const [detail, setDetail] = useState<Detail | null>(null); const [detail, setDetail] = useState<Detail | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [subOpen, setSubOpen] = useState(false);
const [subParent, setSubParent] = useState<AccountTreeRow | null>(null);
const [partners, setPartners] = useState<PartnerOption[]>([]); const [partners, setPartners] = useState<PartnerOption[]>([]);
const loadTree = useCallback(async () => {
setLoading(true);
try {
const qs = new URLSearchParams();
if (filters.partnerId) qs.set('partnerId', filters.partnerId);
const path = qs.toString() ? `/admin/partner-accounts/tree?${qs}` : '/admin/partner-accounts/tree';
const res = await request<AccountTreeRow[]>(path);
setTreeData(filterTree(res, filters.phone, filters.status));
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => {
void loadPartners();
}, []);
useEffect(() => {
void loadTree();
}, [loadTree]);
useEffect(() => {
if (!drawerOpen || !detail) return;
editForm.setFieldsValue({
name: detail.name,
phone: detail.phone,
status: detail.status,
});
}, [drawerOpen, detail, editForm]);
async function loadPartners() { async function loadPartners() {
const res = await request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`); const res = await request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
setPartners(res.items); setPartners(res.items);
} }
async function openAccount(id: string) { async function openAccount(id: string) {
const d = await request<Detail>(`/admin/partner-accounts/${id}`); try {
setDetail(d); const d = await request<Detail>(`/admin/partner-accounts/${id}`);
editForm.setFieldsValue({ name: d.name, phone: d.phone, status: d.status }); setDetail(d);
setDrawerOpen(true); setDrawerOpen(true);
} catch (e) {
message.error(e instanceof Error ? e.message : '加载账户详情失败');
}
} }
async function saveAccount() { async function saveAccount() {
@@ -64,22 +130,86 @@ export default function PartnerAccountsPage() {
await request(`/admin/partner-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) }); await request(`/admin/partner-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
message.success('已保存'); message.success('已保存');
setDrawerOpen(false); setDrawerOpen(false);
void reload(); void loadTree();
} }
const columns: ColumnsType<Row> = [ async function deleteSubAccount(row: AccountTreeRow) {
{ title: '姓名', dataIndex: 'name', width: 100 }, await request(`/admin/partner-accounts/${row.id}`, { method: 'DELETE' });
message.success('子账号已删除');
void loadTree();
}
function openAddSub(parent: AccountTreeRow) {
setSubParent(parent);
subForm.resetFields();
subForm.setFieldsValue({ staffRole: 'INTERNAL' });
setSubOpen(true);
}
function collectExpandableKeys(rows: AccountTreeRow[]): string[] {
const keys: string[] = [];
for (const row of rows) {
if (row.children?.length) {
keys.push(row.id);
keys.push(...collectExpandableKeys(row.children));
}
}
return keys;
}
const columns: ColumnsType<AccountTreeRow> = [
{
title: '姓名 / 类型',
width: 200,
ellipsis: true,
render: (_, row) => (
<AdminCellLine
primary={row.name}
secondary={
row.parentAccountId
? `子账号 · ${PARTNER_STAFF_ROLE_LABELS[row.staffRole as keyof typeof PARTNER_STAFF_ROLE_LABELS] || row.staffRole || '—'}`
: row.isPrimary === 1
? '主账号'
: '合伙人账号'
}
/>
),
},
{ title: '手机', dataIndex: 'phone', width: 120 }, { title: '手机', dataIndex: 'phone', width: 120 },
{ title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 140 }, {
{ title: '主账号', dataIndex: 'isPrimary', width: 80, render: (v) => (v === 1 ? '是' : '否') }, title: '开城合伙人',
width: 140,
ellipsis: true,
render: (_, row) => row.partner?.companyName || '—',
},
{
title: '标识',
width: 100,
render: (_, row) =>
row.parentAccountId ? (
<Tag></Tag>
) : row.isPrimary === 1 ? (
<Tag color="blue"></Tag>
) : (
<Tag></Tag>
),
},
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> }, { title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
{ {
title: '操作', width: 120, title: '操作',
width: 180,
render: (_, row) => ( render: (_, row) => (
<Space> <Space size={0} wrap onClick={(e) => e.stopPropagation()}>
<Button type="link" size="small" onClick={() => void openAccount(row.id)}></Button> <Button type="link" size="small" onClick={() => void openAccount(row.id)}></Button>
<Button type="link" size="small" onClick={() => void openAccount(row.id)}></Button> {!row.parentAccountId && row.isPrimary === 1 ? (
<Button type="link" size="small" onClick={() => openAddSub(row)}></Button>
) : null}
{row.parentAccountId ? (
<Popconfirm title="确定删除该子账号?" onConfirm={() => void deleteSubAccount(row)}>
<Button type="link" size="small" danger></Button>
</Popconfirm>
) : null}
</Space> </Space>
), ),
}, },
@@ -105,23 +235,70 @@ export default function PartnerAccountsPage() {
return ( return (
<div> <div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> <div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">/</Typography.Text>
</div>
<Button type="primary" onClick={() => { void loadPartners(); setCreateOpen(true); }}></Button> <Button type="primary" onClick={() => { void loadPartners(); setCreateOpen(true); }}></Button>
</Space> </Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form
form={form}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(v) => {
setFilters({
phone: v.phone?.trim() || undefined,
status: v.status || undefined,
partnerId: v.partnerId || undefined,
});
}}
>
<Form.Item name="partnerId" label="开城合伙人">
<Select
allowClear
showSearch
optionFilterProp="label"
style={{ width: 180 }}
placeholder="全部"
options={partners.map((p) => ({ value: p.id, label: p.companyName }))}
onFocus={() => void loadPartners()}
/>
</Form.Item>
<Form.Item name="phone" label="手机"><Input allowClear /></Form.Item> <Form.Item name="phone" label="手机"><Input allowClear /></Form.Item>
<Form.Item name="status" label="状态"> <Form.Item name="status" label="状态">
<Select allowClear style={{ width: 100 }} options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} /> <Select allowClear style={{ width: 100 }} options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item> </Form.Item>
<Form.Item><Button type="primary" htmlType="submit"></Button></Form.Item> <Form.Item>
<Space>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={() => setExpandedRowKeys(collectExpandableKeys(treeData))}></Button>
<Button onClick={() => setExpandedRowKeys([])}></Button>
</Space>
</Form.Item>
</Form> </Form>
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 900 }} <Table<AccountTreeRow>
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} /> rowKey="id"
className="admin-table-nowrap"
loading={loading}
columns={columns}
dataSource={treeData}
scroll={{ x: 1100 }}
pagination={false}
expandable={{
expandedRowKeys,
onExpandedRowsChange: (keys) => setExpandedRowKeys(keys as string[]),
defaultExpandAllRows: false,
}}
/>
<Drawer <Drawer
title="编辑开城合伙人账户" title={detail?.parentAccountId ? '合伙人子账号详情' : '开城合伙人账户详情'}
width={720} width={720}
open={drawerOpen} open={drawerOpen}
onClose={() => setDrawerOpen(false)} onClose={() => {
setDrawerOpen(false);
setDetail(null);
}}
destroyOnClose
extra={<Button type="primary" onClick={() => void saveAccount()}></Button>} extra={<Button type="primary" onClick={() => void saveAccount()}></Button>}
> >
{detail && ( {detail && (
@@ -130,10 +307,27 @@ export default function PartnerAccountsPage() {
key: 'info', key: 'info',
label: '基本信息', label: '基本信息',
children: ( children: (
<Form form={editForm} layout="vertical"> <Form
form={editForm}
layout="vertical"
key={detail.id}
initialValues={{
name: detail.name,
phone: detail.phone,
status: detail.status,
}}
>
<Form.Item label="开城合伙人"> <Form.Item label="开城合伙人">
<Input value={detail.partner?.companyName} disabled /> <Input value={detail.partner?.companyName} disabled />
</Form.Item> </Form.Item>
{detail.parentAccountId ? (
<Form.Item label="账号类型">
<Input
value={`子账号 · ${PARTNER_STAFF_ROLE_LABELS[detail.staffRole as keyof typeof PARTNER_STAFF_ROLE_LABELS] || detail.staffRole || '—'}`}
disabled
/>
</Form.Item>
) : 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 <Form.Item
name="phone" name="phone"
@@ -179,7 +373,7 @@ export default function PartnerAccountsPage() {
message.success('已创建'); message.success('已创建');
setCreateOpen(false); setCreateOpen(false);
createForm.resetFields(); createForm.resetFields();
void reload(); void loadTree();
}}> }}>
<Form form={createForm} layout="vertical"> <Form form={createForm} layout="vertical">
<Form.Item name="partnerId" label="开城合伙人" rules={[{ required: true }]}> <Form.Item name="partnerId" label="开城合伙人" rules={[{ required: true }]}>
@@ -198,6 +392,46 @@ export default function PartnerAccountsPage() {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
<Modal
title={subParent ? `添加子账号 · ${subParent.name}` : '添加子账号'}
open={subOpen}
onCancel={() => setSubOpen(false)}
onOk={async () => {
if (!subParent) return;
const v = await subForm.validateFields();
await request('/admin/partner-accounts', {
method: 'POST',
body: JSON.stringify({
partnerId: subParent.partner?.id,
parentAccountId: subParent.id,
name: v.name,
phone: v.phone,
staffRole: v.staffRole,
}),
});
message.success('子账号已创建');
setSubOpen(false);
setExpandedRowKeys((keys) => [...new Set([...keys, subParent.id])]);
void loadTree();
}}
>
<Form form={subForm} layout="vertical">
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item
name="phone"
label="登录手机"
rules={[
{ required: true },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
]}
>
<Input maxLength={11} />
</Form.Item>
<Form.Item name="staffRole" label="角色" rules={[{ required: true }]}>
<Select options={STAFF_ROLE_OPTIONS.filter((o) => o.value !== 'PARTNER')} />
</Form.Item>
</Form>
</Modal>
</div> </div>
); );
} }
@@ -6,6 +6,7 @@ export const HqOperationAction = {
PARTNER_UPDATE: 'PARTNER_UPDATE', PARTNER_UPDATE: 'PARTNER_UPDATE',
PARTNER_ACCOUNT_CREATE: 'PARTNER_ACCOUNT_CREATE', PARTNER_ACCOUNT_CREATE: 'PARTNER_ACCOUNT_CREATE',
PARTNER_ACCOUNT_UPDATE: 'PARTNER_ACCOUNT_UPDATE', PARTNER_ACCOUNT_UPDATE: 'PARTNER_ACCOUNT_UPDATE',
PARTNER_ACCOUNT_DELETE: 'PARTNER_ACCOUNT_DELETE',
HQ_ACCOUNT_CREATE: 'HQ_ACCOUNT_CREATE', HQ_ACCOUNT_CREATE: 'HQ_ACCOUNT_CREATE',
HQ_ACCOUNT_UPDATE: 'HQ_ACCOUNT_UPDATE', HQ_ACCOUNT_UPDATE: 'HQ_ACCOUNT_UPDATE',
HQ_PERMISSION_UPDATE: 'HQ_PERMISSION_UPDATE', HQ_PERMISSION_UPDATE: 'HQ_PERMISSION_UPDATE',
@@ -50,6 +51,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
[HqOperationAction.PARTNER_UPDATE]: '编辑城市合伙人', [HqOperationAction.PARTNER_UPDATE]: '编辑城市合伙人',
[HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户', [HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户',
[HqOperationAction.PARTNER_ACCOUNT_UPDATE]: '编辑合伙人账户', [HqOperationAction.PARTNER_ACCOUNT_UPDATE]: '编辑合伙人账户',
[HqOperationAction.PARTNER_ACCOUNT_DELETE]: '删除合伙人子账号',
[HqOperationAction.HQ_ACCOUNT_CREATE]: '新增 HQ 管理员', [HqOperationAction.HQ_ACCOUNT_CREATE]: '新增 HQ 管理员',
[HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员', [HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员',
[HqOperationAction.HQ_PERMISSION_UPDATE]: '配置 HQ 权限', [HqOperationAction.HQ_PERMISSION_UPDATE]: '配置 HQ 权限',
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
@@ -54,6 +54,11 @@ export class AdminPartnersController {
export class AdminPartnerAccountsController { export class AdminPartnerAccountsController {
constructor(private readonly service: AdminPartnersService) {} constructor(private readonly service: AdminPartnersService) {}
@Get('tree')
tree(@Query('partnerId') partnerId?: string) {
return this.service.listPartnerAccountTree(partnerId ? BigInt(partnerId) : undefined);
}
@Get() @Get()
list(@Query() query: AdminPartnerAccountsQueryDto) { list(@Query() query: AdminPartnerAccountsQueryDto) {
return this.service.listPartnerAccounts(query); return this.service.listPartnerAccounts(query);
@@ -85,4 +90,14 @@ export class AdminPartnerAccountsController {
update(@Param('id') id: string, @Body() dto: UpdatePartnerAccountDto) { update(@Param('id') id: string, @Body() dto: UpdatePartnerAccountDto) {
return this.service.updatePartnerAccount(BigInt(id), dto); return this.service.updatePartnerAccount(BigInt(id), dto);
} }
@Delete(':id')
@HqOperation({
action: HqOperationAction.PARTNER_ACCOUNT_DELETE,
refType: 'PARTNER_ACCOUNT',
refIdParam: 'id',
})
remove(@Param('id') id: string) {
return this.service.deletePartnerSubAccount(BigInt(id));
}
} }
@@ -94,10 +94,65 @@ export class AdminPartnersService {
return serializeBigInt({ items, total, page, pageSize }); return serializeBigInt({ items, total, page, pageSize });
} }
async listPartnerAccountTree(partnerId?: bigint) {
const where: Prisma.PartnerAccountWhereInput = {};
if (partnerId) where.partnerId = partnerId;
const accounts = await this.prisma.partnerAccount.findMany({
where,
orderBy: [{ isPrimary: 'desc' }, { createdAt: 'asc' }],
include: { partner: { select: { id: true, companyName: true } } },
});
type TreeNode = (typeof accounts)[number] & { children: TreeNode[] };
const nodeMap = new Map<string, TreeNode>();
const roots: TreeNode[] = [];
for (const account of accounts) {
nodeMap.set(account.id.toString(), { ...account, children: [] });
}
for (const account of accounts) {
const node = nodeMap.get(account.id.toString())!;
if (account.parentAccountId) {
const parent = nodeMap.get(account.parentAccountId.toString());
if (parent) parent.children.push(node);
else roots.push(node);
} else {
roots.push(node);
}
}
const mapNode = (node: TreeNode) => ({
id: node.id,
phone: node.phone,
name: node.name,
status: node.status,
isPrimary: node.isPrimary,
staffRole: node.staffRole,
parentAccountId: node.parentAccountId,
partner: node.partner,
createdAt: node.createdAt,
lastLoginAt: node.lastLoginAt,
children: node.children.length ? node.children.map(mapNode) : undefined,
});
return serializeBigInt(roots.map(mapNode));
}
async detailPartnerAccount(id: bigint) { async detailPartnerAccount(id: bigint) {
const account = await this.prisma.partnerAccount.findUnique({ const account = await this.prisma.partnerAccount.findUnique({
where: { id }, where: { id },
include: { partner: true }, include: {
partner: {
select: {
id: true,
companyName: true,
contactPhone: true,
address: true,
},
},
},
}); });
if (!account) throw new NotFoundException('开城合伙人账号不存在'); if (!account) throw new NotFoundException('开城合伙人账号不存在');
@@ -126,16 +181,49 @@ export class AdminPartnersService {
} }
async createPartnerAccount(dto: CreatePartnerAccountDto) { async createPartnerAccount(dto: CreatePartnerAccountDto) {
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } }); const phone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
throw new BadRequestException('请输入正确的手机号码');
}
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
if (phoneTaken) throw new BadRequestException('该手机号已被使用');
if (dto.parentAccountId) {
const parent = await this.prisma.partnerAccount.findUnique({
where: { id: BigInt(dto.parentAccountId) },
});
if (!parent) throw new BadRequestException('主账号不存在');
if (parent.isPrimary !== 1) throw new BadRequestException('仅可向主账号添加子账号');
if (dto.partnerId && dto.partnerId !== parent.partnerId.toString()) {
throw new BadRequestException('开城合伙人与主账号不匹配');
}
const account = await this.prisma.partnerAccount.create({
data: {
partnerId: parent.partnerId,
phone,
name: dto.name.trim(),
staffRole: (dto.staffRole ?? 'INTERNAL') as 'PARTNER' | 'INTERNAL' | 'PROMOTER',
isPrimary: 0,
parentAccountId: parent.id,
status: 'ACTIVE',
},
include: { partner: { select: { id: true, companyName: true } } },
});
return serializeBigInt(account);
}
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId!) } });
if (!partner) throw new BadRequestException('开城合伙人不存在'); if (!partner) throw new BadRequestException('开城合伙人不存在');
const account = await this.prisma.partnerAccount.create({ const account = await this.prisma.partnerAccount.create({
data: { data: {
partnerId: partner.id, partnerId: partner.id,
phone: dto.phone, phone,
name: dto.name, name: dto.name.trim(),
staffRole: dto.staffRole ? (dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER') : undefined, staffRole: dto.staffRole ? (dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER') : undefined,
isPrimary: 0, isPrimary: 0,
}, },
include: { partner: { select: { id: true, companyName: true } } },
}); });
return serializeBigInt(account); return serializeBigInt(account);
} }
@@ -162,4 +250,14 @@ export class AdminPartnersService {
const account = await this.prisma.partnerAccount.update({ where: { id }, data }); const account = await this.prisma.partnerAccount.update({ where: { id }, data });
return serializeBigInt(account); return serializeBigInt(account);
} }
async deletePartnerSubAccount(id: bigint) {
const account = await this.prisma.partnerAccount.findUnique({ where: { id } });
if (!account) throw new NotFoundException('开城合伙人账号不存在');
if (!account.parentAccountId) {
throw new BadRequestException('仅可删除子账号');
}
await this.prisma.partnerAccount.delete({ where: { id } });
return { ok: true };
}
} }
@@ -211,9 +211,10 @@ export class UpdatePartnerAccountDto {
} }
export class CreatePartnerAccountDto { export class CreatePartnerAccountDto {
@ValidateIf((o: CreatePartnerAccountDto) => !o.parentAccountId)
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
partnerId: string; partnerId?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -226,6 +227,11 @@ export class CreatePartnerAccountDto {
@IsOptional() @IsOptional()
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER']) @IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
staffRole?: string; staffRole?: string;
/** 主账号 ID;传入则创建子账号 */
@IsOptional()
@IsString()
parentAccountId?: string;
} }
export class CreateCityDto { export class CreateCityDto {