小飞侠接口调通测试
This commit is contained in:
@@ -33,7 +33,16 @@ export async function request<T>(path: string, options: RequestInit = {}): Promi
|
|||||||
if (token) headers.Authorization = `Bearer ${token}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||||
const json = await res.json();
|
const text = await res.text();
|
||||||
|
if (!text.trim()) {
|
||||||
|
throw new Error(`接口空响应(HTTP ${res.status} ${res.statusText || ''})`);
|
||||||
|
}
|
||||||
|
let json: { code: number; message?: string; data?: T };
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text) as { code: number; message?: string; data?: T };
|
||||||
|
} catch {
|
||||||
|
throw new Error(`接口返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
||||||
|
}
|
||||||
if (json.code === 401) {
|
if (json.code === 401) {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
@@ -70,6 +79,8 @@ export type AdminUserRow = {
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
phoneVerifiedAt: string | null;
|
phoneVerifiedAt: string | null;
|
||||||
mergedIntoUserId: string | null;
|
mergedIntoUserId: string | null;
|
||||||
|
wxOpenId: string | null;
|
||||||
|
wechatVerified: boolean;
|
||||||
nickname: string | null;
|
nickname: string | null;
|
||||||
status: number;
|
status: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { request, type AdminUserRow, type Paginated } from '../lib/api';
|
import { request, type AdminUserRow, type Paginated } from '../lib/api';
|
||||||
|
import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||||
|
|
||||||
|
type UserOrderRow = {
|
||||||
|
id: string;
|
||||||
|
orderNo: string;
|
||||||
|
status: string;
|
||||||
|
payAmount: number;
|
||||||
|
payStatus?: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
type UserDetail = AdminUserRow & {
|
type UserDetail = AdminUserRow & {
|
||||||
|
wxUnionId?: string | null;
|
||||||
cityPref?: Record<string, unknown> | null;
|
cityPref?: Record<string, unknown> | null;
|
||||||
mergedInto?: { id: string; userNo: string; phone: string | null; nickname: string | null } | null;
|
mergedInto?: { id: string; userNo: string; phone: string | null; nickname: string | null } | null;
|
||||||
orders?: Array<{ id: string; orderNo: string; status: string; payAmount: number; createdAt: string }>;
|
orders?: UserOrderRow[];
|
||||||
mergedFromCount?: number;
|
mergedFromCount?: number;
|
||||||
orderCount?: number;
|
|
||||||
addressCount?: number;
|
addressCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,6 +46,9 @@ export default function UsersPage() {
|
|||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [detail, setDetail] = useState<UserDetail | null>(null);
|
const [detail, setDetail] = useState<UserDetail | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState('');
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -65,6 +81,44 @@ export default function UsersPage() {
|
|||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDeleteModal() {
|
||||||
|
setDeleteConfirm('');
|
||||||
|
setDeleteOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!detail) return;
|
||||||
|
if (deleteConfirm !== detail.userNo) {
|
||||||
|
message.error('请输入正确的用户编号以确认删除');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await request(`/admin/users/${detail.id}`, { method: 'DELETE' });
|
||||||
|
message.success('用户已删除(行为日志已保留)');
|
||||||
|
setDeleteOpen(false);
|
||||||
|
setDrawerOpen(false);
|
||||||
|
setDetail(null);
|
||||||
|
void load();
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '删除失败');
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderColumns: ColumnsType<UserOrderRow> = [
|
||||||
|
{ title: '订单号', dataIndex: 'orderNo', width: 170 },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (s) => <Tag>{ORDER_STATUS_LABELS[s] || s}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` },
|
||||||
|
{ title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
|
];
|
||||||
|
|
||||||
const columns: ColumnsType<AdminUserRow> = [
|
const columns: ColumnsType<AdminUserRow> = [
|
||||||
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
|
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
|
||||||
{ title: '昵称', dataIndex: 'nickname', width: 100 },
|
{ title: '昵称', dataIndex: 'nickname', width: 100 },
|
||||||
@@ -80,6 +134,12 @@ export default function UsersPage() {
|
|||||||
width: 90,
|
width: 90,
|
||||||
render: (v) => (v ? <Tag color="green">已验证</Tag> : <Tag color="orange">访客</Tag>),
|
render: (v) => (v ? <Tag color="green">已验证</Tag> : <Tag color="orange">访客</Tag>),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '微信',
|
||||||
|
dataIndex: 'wechatVerified',
|
||||||
|
width: 100,
|
||||||
|
render: (v) => (v ? <Tag color="blue">微信已验证</Tag> : <Tag>未绑定</Tag>),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'deviceKey',
|
title: 'deviceKey',
|
||||||
dataIndex: 'deviceKey',
|
dataIndex: 'deviceKey',
|
||||||
@@ -153,7 +213,7 @@ export default function UsersPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1200 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -166,7 +226,15 @@ export default function UsersPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Drawer title="用户详情" width={560} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
<Drawer
|
||||||
|
title="用户详情"
|
||||||
|
width={640}
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
extra={detail && (
|
||||||
|
<Button danger onClick={openDeleteModal}>删除用户</Button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
{detail && (
|
{detail && (
|
||||||
<>
|
<>
|
||||||
<Descriptions column={1} bordered size="small">
|
<Descriptions column={1} bordered size="small">
|
||||||
@@ -177,6 +245,11 @@ export default function UsersPage() {
|
|||||||
<Descriptions.Item label="验手机时间">
|
<Descriptions.Item label="验手机时间">
|
||||||
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="微信验证">
|
||||||
|
{detail.wechatVerified ? <Tag color="blue">微信已验证</Tag> : <Tag>未绑定</Tag>}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="wxOpenId">{detail.wxOpenId || '—'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="wxUnionId">{detail.wxUnionId || '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="合并至">
|
<Descriptions.Item label="合并至">
|
||||||
{detail.mergedInto
|
{detail.mergedInto
|
||||||
@@ -189,22 +262,20 @@ export default function UsersPage() {
|
|||||||
{new Date(detail.createdAt).toLocaleString('zh-CN')}
|
{new Date(detail.createdAt).toLocaleString('zh-CN')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
{detail.orders && detail.orders.length > 0 && (
|
|
||||||
<>
|
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||||
<Typography.Title level={5} style={{ marginTop: 16 }}>最近订单</Typography.Title>
|
全部订单({detail.orders?.length ?? 0})
|
||||||
<Table
|
</Typography.Title>
|
||||||
size="small"
|
<Table
|
||||||
rowKey="id"
|
size="small"
|
||||||
pagination={false}
|
rowKey="id"
|
||||||
dataSource={detail.orders}
|
pagination={false}
|
||||||
columns={[
|
scroll={{ x: 520, y: 240 }}
|
||||||
{ title: '订单号', dataIndex: 'orderNo' },
|
dataSource={detail.orders ?? []}
|
||||||
{ title: '状态', dataIndex: 'status' },
|
columns={orderColumns}
|
||||||
{ title: '金额', dataIndex: 'payAmount', render: (v) => `¥${v}` },
|
locale={{ emptyText: '暂无订单' }}
|
||||||
]}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
@@ -215,6 +286,58 @@ export default function UsersPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="确认删除用户"
|
||||||
|
open={deleteOpen}
|
||||||
|
okText="确认删除"
|
||||||
|
okButtonProps={{
|
||||||
|
danger: true,
|
||||||
|
disabled: !detail || deleteConfirm !== detail.userNo,
|
||||||
|
loading: deleting,
|
||||||
|
}}
|
||||||
|
onOk={() => void confirmDelete()}
|
||||||
|
onCancel={() => setDeleteOpen(false)}
|
||||||
|
width={720}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{detail && (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message="此操作不可恢复"
|
||||||
|
description={(
|
||||||
|
<>
|
||||||
|
将删除用户 <strong>{detail.userNo}</strong> 及其地址、订单、权益券、核销记录等业务数据。
|
||||||
|
<br />
|
||||||
|
用户行为日志(埋点)与第三方调用日志将<strong>保留</strong>,不随用户删除。
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Typography.Text strong>关联订单({detail.orders?.length ?? 0} 笔)</Typography.Text>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
style={{ marginTop: 8, marginBottom: 16 }}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 520, y: 200 }}
|
||||||
|
dataSource={detail.orders ?? []}
|
||||||
|
columns={orderColumns}
|
||||||
|
locale={{ emptyText: '无订单' }}
|
||||||
|
/>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
请输入用户编号 <Typography.Text code>{detail.userNo}</Typography.Text> 以确认删除:
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Input
|
||||||
|
value={deleteConfirm}
|
||||||
|
placeholder={detail.userNo}
|
||||||
|
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class CourierConfigService {
|
|||||||
provider,
|
provider,
|
||||||
xiaofeixia: {
|
xiaofeixia: {
|
||||||
apiUrl:
|
apiUrl:
|
||||||
this.config.get<string>('XIAOFEIXIA_API_URL') ??
|
this.config.get<string>('XIAOFEIXIA_API_URL')?.trim() ||
|
||||||
'https://beta.51xiaoju.cn/app/api/interface.do',
|
'https://beta.51xiaoju.cn/app/api/interface.do',
|
||||||
appId: this.config.get<string>('XIAOFEIXIA_APP_ID') || undefined,
|
appId: this.config.get<string>('XIAOFEIXIA_APP_ID') || undefined,
|
||||||
mchId: this.config.get<string>('XIAOFEIXIA_MCH_ID') ?? '',
|
mchId: this.config.get<string>('XIAOFEIXIA_MCH_ID') ?? '',
|
||||||
|
|||||||
@@ -66,7 +66,22 @@ export class XiaofeixiaClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as XiaofeixiaApiResponse<T>;
|
const rawText = await response.text();
|
||||||
|
let payload: XiaofeixiaApiResponse<T>;
|
||||||
|
try {
|
||||||
|
payload = rawText ? (JSON.parse(rawText) as XiaofeixiaApiResponse<T>) : (null as unknown as XiaofeixiaApiResponse<T>);
|
||||||
|
} catch {
|
||||||
|
throw new CourierApiError(
|
||||||
|
`小飞侠响应非 JSON(HTTP ${response.status}): ${rawText.slice(0, 200) || '(空)'}`,
|
||||||
|
'200000',
|
||||||
|
'XIAOFEIXIA',
|
||||||
|
rawText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
throw new CourierApiError('小飞侠返回空响应', '200000', 'XIAOFEIXIA');
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) {
|
if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) {
|
||||||
throw new CourierApiError(
|
throw new CourierApiError(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
import { Controller, Delete, Get, Param, Query, 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 { AdminUsersService } from './admin-users.service';
|
import { AdminUsersService } from './admin-users.service';
|
||||||
import { AdminUsersQueryDto } from './dto/admin-query.dto';
|
import { AdminUsersQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
@@ -17,4 +18,10 @@ export class AdminUsersController {
|
|||||||
detail(@Param('id') id: string) {
|
detail(@Param('id') id: string) {
|
||||||
return this.usersService.detail(BigInt(id));
|
return this.usersService.detail(BigInt(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(SuperAdminGuard)
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.usersService.deleteUser(BigInt(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,37 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
|||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
import type { AdminUsersQueryDto } from './dto/admin-query.dto';
|
import type { AdminUsersQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
|
function mapAdminUserRow(u: {
|
||||||
|
id: bigint;
|
||||||
|
userNo: string;
|
||||||
|
deviceKey: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
phoneVerifiedAt: Date | null;
|
||||||
|
mergedIntoUserId: bigint | null;
|
||||||
|
wxOpenId: string | null;
|
||||||
|
nickname: string | null;
|
||||||
|
status: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: { orders: number };
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
userNo: u.userNo,
|
||||||
|
deviceKey: u.deviceKey,
|
||||||
|
phone: u.phone,
|
||||||
|
phoneVerifiedAt: u.phoneVerifiedAt,
|
||||||
|
mergedIntoUserId: u.mergedIntoUserId,
|
||||||
|
wxOpenId: u.wxOpenId,
|
||||||
|
wechatVerified: !!u.wxOpenId,
|
||||||
|
nickname: u.nickname,
|
||||||
|
status: u.status,
|
||||||
|
createdAt: u.createdAt,
|
||||||
|
updatedAt: u.updatedAt,
|
||||||
|
orderCount: u._count.orders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminUsersService {
|
export class AdminUsersService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
@@ -33,6 +64,7 @@ export class AdminUsersService {
|
|||||||
phone: true,
|
phone: true,
|
||||||
phoneVerifiedAt: true,
|
phoneVerifiedAt: true,
|
||||||
mergedIntoUserId: true,
|
mergedIntoUserId: true,
|
||||||
|
wxOpenId: true,
|
||||||
nickname: true,
|
nickname: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
@@ -44,11 +76,7 @@ export class AdminUsersService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
items: items.map((u) => ({
|
items: items.map((u) => mapAdminUserRow(u)),
|
||||||
...u,
|
|
||||||
orderCount: u._count.orders,
|
|
||||||
_count: undefined,
|
|
||||||
})),
|
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -63,12 +91,12 @@ export class AdminUsersService {
|
|||||||
mergedInto: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
mergedInto: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||||
orders: {
|
orders: {
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 5,
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
orderNo: true,
|
orderNo: true,
|
||||||
status: true,
|
status: true,
|
||||||
payAmount: true,
|
payAmount: true,
|
||||||
|
payStatus: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -79,10 +107,59 @@ export class AdminUsersService {
|
|||||||
|
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
...user,
|
...user,
|
||||||
|
wechatVerified: !!user.wxOpenId,
|
||||||
mergedFromCount: user._count.mergedFrom,
|
mergedFromCount: user._count.mergedFrom,
|
||||||
orderCount: user._count.orders,
|
orderCount: user._count.orders,
|
||||||
addressCount: user._count.addresses,
|
addressCount: user._count.addresses,
|
||||||
_count: undefined,
|
_count: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteUser(id: bigint) {
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!user) throw new NotFoundException('用户不存在');
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
const orderIds = (
|
||||||
|
await tx.order.findMany({ where: { userId: id }, select: { id: true } })
|
||||||
|
).map((o) => o.id);
|
||||||
|
|
||||||
|
const redeemIds = (
|
||||||
|
await tx.redeemRecord.findMany({ where: { userId: id }, select: { id: true } })
|
||||||
|
).map((r) => r.id);
|
||||||
|
|
||||||
|
if (redeemIds.length) {
|
||||||
|
await tx.storePayout.deleteMany({ where: { redeemRecordId: { in: redeemIds } } });
|
||||||
|
await tx.storeRating.deleteMany({ where: { redeemRecordId: { in: redeemIds } } });
|
||||||
|
await tx.redeemRecord.deleteMany({ where: { id: { in: redeemIds } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.benefitCoupon.deleteMany({ where: { userId: id } });
|
||||||
|
|
||||||
|
if (orderIds.length) {
|
||||||
|
await tx.order.updateMany({
|
||||||
|
where: { originOrderId: { in: orderIds } },
|
||||||
|
data: { originOrderId: null },
|
||||||
|
});
|
||||||
|
await tx.commonTicket.deleteMany({
|
||||||
|
where: { refType: 'ORDER', refId: { in: orderIds } },
|
||||||
|
});
|
||||||
|
await tx.order.deleteMany({ where: { id: { in: orderIds } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.avatarResourceId) {
|
||||||
|
await tx.commonResource.updateMany({
|
||||||
|
where: { id: user.avatarResourceId, ownerType: 'USER', ownerId: id },
|
||||||
|
data: { status: 'DELETED' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.user.delete({ where: { id } });
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
message: '用户及关联业务数据已删除,行为日志已保留',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,12 @@ export class AdminXiaofeixiaService {
|
|||||||
raw: err.raw,
|
raw: err.raw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw err;
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
elapsedMs: Date.now() - startedAt,
|
||||||
|
error: message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class XiaofeixiaEstimateFreightDto {
|
export class XiaofeixiaEstimateFreightDto {
|
||||||
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0.01)
|
@Min(0.01)
|
||||||
weight: number;
|
weight: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user