Files
dukang/server/dukang-api/src/modules/ops/admin-users.service.ts
T
jacy f0b99ea9da webadmin端
批量删除用户
模板上传图片数量限制改成30
小飞侠接口配置
2026-07-06 18:04:02 +08:00

276 lines
8.0 KiB
TypeScript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import type { AdminUsersQueryDto } from './dto/admin-query.dto';
const FINISHED_ORDER_STATUSES = ['COMPLETED', 'CANCELLED', 'REFUNDED'] as const;
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()
export class AdminUsersService {
constructor(private readonly prisma: PrismaService) {}
async list(query: AdminUsersQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.UserWhereInput = {};
if (query.phone) where.phone = { contains: query.phone };
if (query.userNo) where.userNo = { contains: query.userNo };
if (query.deviceKey) where.deviceKey = query.deviceKey;
if (query.status !== undefined) where.status = query.status;
if (query.phoneVerified === '1') where.phoneVerifiedAt = { not: null };
if (query.phoneVerified === '0') where.phoneVerifiedAt = null;
const [items, total] = await Promise.all([
this.prisma.user.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
select: {
id: true,
userNo: true,
deviceKey: true,
phone: true,
phoneVerifiedAt: true,
mergedIntoUserId: true,
wxOpenId: true,
nickname: true,
status: true,
createdAt: true,
updatedAt: true,
_count: { select: { orders: true } },
},
}),
this.prisma.user.count({ where }),
]);
return serializeBigInt({
items: items.map((u) => mapAdminUserRow(u)),
total,
page,
pageSize,
});
}
async detail(id: bigint) {
const user = await this.prisma.user.findUnique({
where: { id },
include: {
cityPreference: true,
mergedInto: { select: { id: true, userNo: true, phone: true, nickname: true } },
orders: {
orderBy: { createdAt: 'desc' },
select: {
id: true,
orderNo: true,
status: true,
payAmount: true,
payStatus: true,
createdAt: true,
},
},
_count: { select: { mergedFrom: true, orders: true, addresses: true } },
},
});
if (!user) throw new NotFoundException('用户不存在');
return serializeBigInt({
...user,
wechatVerified: !!user.wxOpenId,
mergedFromCount: user._count.mergedFrom,
orderCount: user._count.orders,
addressCount: user._count.addresses,
_count: undefined,
});
}
async previewBatchDelete(ids: bigint[]) {
const uniqueIds = [...new Set(ids)];
if (!uniqueIds.length) {
return { items: [], hasRisk: false, total: 0 };
}
const users = await this.prisma.user.findMany({
where: { id: { in: uniqueIds } },
select: {
id: true,
userNo: true,
nickname: true,
phone: true,
orders: {
where: { status: { notIn: [...FINISHED_ORDER_STATUSES] } },
select: { id: true, orderNo: true, status: true, payAmount: true, createdAt: true },
orderBy: { createdAt: 'desc' },
},
redeemRecords: {
select: {
id: true,
redeemNo: true,
amount: true,
createdAt: true,
payout: { select: { status: true } },
},
orderBy: { createdAt: 'desc' },
},
},
orderBy: { createdAt: 'desc' },
});
const foundIds = new Set(users.map((u) => u.id.toString()));
const missing = uniqueIds.filter((id) => !foundIds.has(id.toString()));
if (missing.length) {
throw new NotFoundException('部分用户不存在');
}
const items = users.map((u) => {
const unfinishedOrders = u.orders;
const redeemRecords = u.redeemRecords.map((r) => ({
id: r.id,
redeemNo: r.redeemNo,
amount: r.amount,
createdAt: r.createdAt,
payoutStatus: r.payout?.status ?? null,
}));
const hasRisk = unfinishedOrders.length > 0 || redeemRecords.length > 0;
return {
id: u.id,
userNo: u.userNo,
nickname: u.nickname,
phone: u.phone,
unfinishedOrders,
redeemRecords,
hasRisk,
};
});
return serializeBigInt({
items,
hasRisk: items.some((i) => i.hasRisk),
total: items.length,
});
}
async batchDeleteUsers(ids: bigint[], confirmRisk: boolean) {
const preview = await this.previewBatchDelete(ids);
if (preview.hasRisk && !confirmRisk) {
const riskyUsers = preview.items.filter((i) => i.hasRisk);
throw new BadRequestException({
message: '所选用户存在未完成订单或核销记录,需二次确认后删除',
code: 'USER_DELETE_RISK',
riskyUsers: riskyUsers.map((u) => ({
id: u.id,
userNo: u.userNo,
unfinishedOrderCount: u.unfinishedOrders.length,
redeemRecordCount: u.redeemRecords.length,
})),
});
}
const userIds = preview.items.map((i) => BigInt(String(i.id)));
await this.prisma.$transaction(async (tx) => {
for (const id of userIds) {
await this.deleteUserInTx(tx, id);
}
});
return {
ok: true,
deleted: userIds.length,
message: '用户及关联业务数据已删除,行为日志已保留',
};
}
async deleteUser(id: bigint) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException('用户不存在');
await this.prisma.$transaction(async (tx) => {
await this.deleteUserInTx(tx, id);
});
return {
ok: true,
message: '用户及关联业务数据已删除,行为日志已保留',
};
}
private async deleteUserInTx(tx: Prisma.TransactionClient, id: bigint) {
const user = await tx.user.findUnique({ where: { id } });
if (!user) return;
await tx.user.updateMany({
where: { mergedIntoUserId: id },
data: { mergedIntoUserId: null },
});
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 } });
}
}