+ {row.unfinishedOrders.length > 0 && (
+ <>
+
未完成订单({row.unfinishedOrders.length})
+
+ >
+ )}
+ {row.redeemRecords.length > 0 && (
+ <>
+
核销记录({row.redeemRecords.length})
+
`¥${v}` },
+ {
+ title: '打款状态',
+ dataIndex: 'payoutStatus',
+ width: 100,
+ render: (v) => (v === 'PENDING' ? 待打款 : v === 'PAID' ? 已打款 : '—'),
+ },
+ { title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
+ ]}
+ />
+ >
+ )}
+
+ ),
+ }}
+ columns={[
+ { title: '用户编号', dataIndex: 'userNo', width: 120 },
+ { title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => v || '—' },
+ { title: '手机', dataIndex: 'phone', width: 120, render: (v) => v || '—' },
+ {
+ title: '风险',
+ width: 200,
+ render: (_, row) => (row.hasRisk ? (
+
+ 需关注
+ {row.unfinishedOrders.length > 0 && (
+ 未完成订单 {row.unfinishedOrders.length}
+ )}
+ {row.redeemRecords.length > 0 && (
+ 核销 {row.redeemRecords.length}
+ )}
+
+ ) : (
+ 无关联风险
+ )),
+ },
+ ]}
+ />
+ >
+ )}
+ {!batchPreviewLoading && batchPreview && batchDeleteStep === 2 && (
+ <>
+
+ 以下 {batchPreview.items.filter((i) => i.hasRisk).length} 名用户存在未完成订单或核销记录。
+ 确认后将永久删除这些订单、核销记录、权益券及门店打款关联数据,且不可恢复。
+ >
+ )}
+ />
+ i.hasRisk)}
+ columns={[
+ { title: '用户编号', dataIndex: 'userNo', width: 120 },
+ { title: '未完成订单', width: 110, render: (_, row) => row.unfinishedOrders.length },
+ { title: '核销记录', width: 90, render: (_, row) => row.redeemRecords.length },
+ ]}
+ />
+ setBatchRiskAck(e.target.checked)}>
+ 我确认删除上述用户的未完成订单、核销记录及相关业务数据
+
+ >
+ )}
+
);
}
diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example
index 86061b8..98dfcc9 100644
--- a/server/dukang-api/.env.example
+++ b/server/dukang-api/.env.example
@@ -67,3 +67,10 @@ XIAOFEIXIA_MCH_ID=
XIAOFEIXIA_API_KEY=
XIAOFEIXIA_SIGN_TYPE=MD5
# XIAOFEIXIA_APP_ID=
+# HQ 订单发货默认寄件信息
+SHIP_FROM_NAME=杜康仓库
+SHIP_FROM_MOBILE=13800000000
+SHIP_FROM_ADDRESS=河南省郑州市金水区
+SHIP_FROM_ADDRESS_DETAIL=杜康酒业仓
+SHIP_FROM_LNG=113.665
+SHIP_FROM_LAT=34.757
diff --git a/server/dukang-api/src/modules/ops/admin-orders.controller.ts b/server/dukang-api/src/modules/ops/admin-orders.controller.ts
index 7bab823..f6f74bc 100644
--- a/server/dukang-api/src/modules/ops/admin-orders.controller.ts
+++ b/server/dukang-api/src/modules/ops/admin-orders.controller.ts
@@ -1,7 +1,8 @@
-import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/common';
+import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
+import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
import { AdminOrdersService } from './admin-orders.service';
-import { UpdateOrderStatusDto } from './dto/admin-mutate.dto';
+import { AdminShipOrderDto, BatchDeleteOrdersDto, UpdateOrderStatusDto } from './dto/admin-mutate.dto';
import { AdminOrdersQueryDto } from './dto/admin-query.dto';
@Controller('admin/orders')
@@ -14,11 +15,28 @@ export class AdminOrdersController {
return this.ordersService.list(query);
}
+ @Post('batch-delete')
+ @UseGuards(SuperAdminGuard)
+ batchDelete(@Body() dto: BatchDeleteOrdersDto) {
+ return this.ordersService.batchDeleteOrders(dto.ids.map((id) => BigInt(id)));
+ }
+
+ @Get('ship-defaults')
+ shipDefaults() {
+ return this.ordersService.getShipDefaults();
+ }
+
@Get(':id')
detail(@Param('id') id: string) {
return this.ordersService.detail(BigInt(id));
}
+ /** HQ 发货:调用小飞侠创建运单并更新配送信息 */
+ @Post(':id/ship')
+ ship(@Param('id') id: string, @Body() dto: AdminShipOrderDto) {
+ return this.ordersService.shipOrder(BigInt(id), dto);
+ }
+
/** preV1 调试:直接改订单状态,不走业务校验 */
@Put(':id/status')
updateStatus(@Param('id') id: string, @Body() dto: UpdateOrderStatusDto) {
diff --git a/server/dukang-api/src/modules/ops/admin-orders.service.ts b/server/dukang-api/src/modules/ops/admin-orders.service.ts
index 7b974b8..8a1f0b9 100644
--- a/server/dukang-api/src/modules/ops/admin-orders.service.ts
+++ b/server/dukang-api/src/modules/ops/admin-orders.service.ts
@@ -1,17 +1,23 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { orderStatusLogWhere } from '../../common/event/event.helpers';
import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat';
import { TradeService } from '../trade/trade.service';
+import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
import type { AdminOrdersQueryDto } from './dto/admin-query.dto';
+import type { AdminShipOrderDto } from './dto/admin-mutate.dto';
+import type { XiaofeixiaCreateShipmentDto } from './dto/admin-courier.dto';
@Injectable()
export class AdminOrdersService {
constructor(
private readonly prisma: PrismaService,
private readonly tradeService: TradeService,
+ private readonly xiaofeixiaService: AdminXiaofeixiaService,
+ private readonly config: ConfigService,
) {}
async list(query: AdminOrdersQueryDto) {
@@ -87,4 +93,156 @@ export class AdminOrdersService {
await this.tradeService.applyStatusTransition(id, order.status, status, 'HQ_DEBUG');
return this.detail(id);
}
+
+ getShipDefaults() {
+ return {
+ provider: 'XFX',
+ providerLabel: '小飞侠',
+ fromName: this.config.get('SHIP_FROM_NAME') || '杜康仓库',
+ fromMobile: this.config.get('SHIP_FROM_MOBILE') || '13800000000',
+ fromAddress: this.config.get('SHIP_FROM_ADDRESS') || '河南省郑州市金水区',
+ fromAddressDetail: this.config.get('SHIP_FROM_ADDRESS_DETAIL') || '杜康酒业仓',
+ fromLng: Number(this.config.get('SHIP_FROM_LNG') || 113.665),
+ fromLat: Number(this.config.get('SHIP_FROM_LAT') || 34.757),
+ weight: 2,
+ payMode: '1',
+ };
+ }
+
+ async shipOrder(id: bigint, dto: AdminShipOrderDto) {
+ if (dto.provider !== 'XFX') {
+ throw new BadRequestException('暂仅支持小飞侠配送');
+ }
+
+ const order = await this.prisma.order.findUnique({
+ where: { id },
+ include: { delivery: true },
+ });
+ if (!order) throw new NotFoundException('订单不存在');
+
+ if (!['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(order.status)) {
+ throw new BadRequestException('当前订单状态不可发货');
+ }
+ if (order.delivery?.trackingNo) {
+ throw new BadRequestException('该订单已有运单号,请勿重复发货');
+ }
+
+ const defaults = this.getShipDefaults();
+ const shipmentDto: XiaofeixiaCreateShipmentDto = {
+ outNumber: order.orderNo,
+ fromName: dto.fromName || defaults.fromName,
+ fromMobile: dto.fromMobile || defaults.fromMobile,
+ fromAddress: dto.fromAddress || defaults.fromAddress,
+ fromAddressDetail: dto.fromAddressDetail || defaults.fromAddressDetail,
+ fromLng: dto.fromLng ?? defaults.fromLng,
+ fromLat: dto.fromLat ?? defaults.fromLat,
+ toName: order.receiverName,
+ toMobile: order.receiverPhone,
+ toAddress: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`,
+ toAddressDetail: order.receiverAddress,
+ goodsName: order.productName,
+ goodsNum: order.quantity,
+ weight: dto.weight ?? defaults.weight,
+ payMode: dto.payMode || defaults.payMode,
+ remark: dto.remark || `HQ发货 ${order.orderNo}`,
+ };
+
+ const result = await this.xiaofeixiaService.createShipment(shipmentDto);
+ if (!result.ok || !result.data) {
+ throw new BadRequestException(result.error || '小飞侠创建运单失败');
+ }
+
+ const { providerShipmentId, trackingNumber } = result.data;
+ const now = new Date();
+
+ await this.prisma.$transaction(async (tx) => {
+ if (order.delivery) {
+ await tx.orderDelivery.update({
+ where: { orderId: id },
+ data: {
+ provider: 'XFX',
+ trackingNo: trackingNumber,
+ providerOrderNo: String(providerShipmentId),
+ shippingAt: now,
+ },
+ });
+ } else {
+ await tx.orderDelivery.create({
+ data: {
+ orderId: id,
+ provider: 'XFX',
+ trackingNo: trackingNumber,
+ providerOrderNo: String(providerShipmentId),
+ shippingAt: now,
+ },
+ });
+ }
+ });
+
+ await this.tradeService.applyStatusTransition(id, order.status, 'SHIPPING', 'HQ_SHIP');
+ return this.detail(id);
+ }
+
+ async batchDeleteOrders(ids: bigint[]) {
+ const uniqueIds = [...new Set(ids)];
+ if (!uniqueIds.length) {
+ return { ok: true, deleted: 0, message: '未选择订单' };
+ }
+
+ const orders = await this.prisma.order.findMany({
+ where: { id: { in: uniqueIds } },
+ select: { id: true, orderNo: true },
+ });
+ if (!orders.length) throw new NotFoundException('订单不存在');
+
+ const orderIds = orders.map((o) => o.id);
+
+ await this.prisma.$transaction(async (tx) => {
+ await this.deleteOrdersInTx(tx, orderIds);
+ });
+
+ return {
+ ok: true,
+ deleted: orderIds.length,
+ orderNos: orders.map((o) => o.orderNo),
+ message: '订单及关联业务数据已删除,状态流转等业务日志已保留',
+ };
+ }
+
+ private async deleteOrdersInTx(tx: Prisma.TransactionClient, orderIds: bigint[]) {
+ if (!orderIds.length) return;
+
+ const couponIds = (
+ await tx.benefitCoupon.findMany({
+ where: { orderId: { in: orderIds } },
+ select: { id: true },
+ })
+ ).map((c) => c.id);
+
+ if (couponIds.length) {
+ const redeemIds = (
+ await tx.redeemRecord.findMany({
+ where: { couponId: { in: couponIds } },
+ 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: { id: { in: couponIds } } });
+ }
+
+ 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 } } });
+ }
}
diff --git a/server/dukang-api/src/modules/ops/admin-product-detail-templates.service.ts b/server/dukang-api/src/modules/ops/admin-product-detail-templates.service.ts
index d648fa6..7005c33 100644
--- a/server/dukang-api/src/modules/ops/admin-product-detail-templates.service.ts
+++ b/server/dukang-api/src/modules/ops/admin-product-detail-templates.service.ts
@@ -25,7 +25,7 @@ type TemplateRow = {
updatedAt: Date;
};
-const MAX_TEMPLATE_DETAIL_IMAGES = 20;
+const MAX_TEMPLATE_DETAIL_IMAGES = 30;
@Injectable()
export class AdminProductDetailTemplatesService {
diff --git a/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts b/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts
new file mode 100644
index 0000000..b560da5
--- /dev/null
+++ b/server/dukang-api/src/modules/ops/admin-redeem-debug.controller.ts
@@ -0,0 +1,31 @@
+import { Body, Controller, Post, UseGuards } from '@nestjs/common';
+import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
+import { AdminRedeemDebugService } from './admin-redeem-debug.service';
+import type {
+ AdminRedeemDebugCreateTokenDto,
+ AdminRedeemDebugStoreTokenDto,
+} from './dto/admin-mutate.dto';
+
+@Controller('admin/redeem/debug')
+@UseGuards(HqAuthGuard)
+export class AdminRedeemDebugController {
+ constructor(private readonly service: AdminRedeemDebugService) {}
+
+ /** preV1 调试:为用户生成核销码 */
+ @Post('create-token')
+ createToken(@Body() dto: AdminRedeemDebugCreateTokenDto) {
+ return this.service.createToken(dto);
+ }
+
+ /** preV1 调试:门店侧预览核销 */
+ @Post('preview')
+ preview(@Body() dto: AdminRedeemDebugStoreTokenDto) {
+ return this.service.preview(dto);
+ }
+
+ /** preV1 调试:门店侧确认核销 */
+ @Post('confirm')
+ confirm(@Body() dto: AdminRedeemDebugStoreTokenDto) {
+ return this.service.confirm(dto);
+ }
+}
diff --git a/server/dukang-api/src/modules/ops/admin-redeem-debug.service.ts b/server/dukang-api/src/modules/ops/admin-redeem-debug.service.ts
new file mode 100644
index 0000000..a99478c
--- /dev/null
+++ b/server/dukang-api/src/modules/ops/admin-redeem-debug.service.ts
@@ -0,0 +1,45 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { PrismaService } from '../../common/prisma/prisma.module';
+import { RedeemService } from '../redeem/redeem.service';
+import type {
+ AdminRedeemDebugCreateTokenDto,
+ AdminRedeemDebugStoreTokenDto,
+} from './dto/admin-mutate.dto';
+
+@Injectable()
+export class AdminRedeemDebugService {
+ constructor(
+ private readonly prisma: PrismaService,
+ private readonly redeemService: RedeemService,
+ ) {}
+
+ private async resolveStoreAccountId(storeId: string): Promise {
+ const account = await this.prisma.storeAccount.findFirst({
+ where: { storeId: BigInt(storeId), status: 'ACTIVE' },
+ orderBy: { id: 'asc' },
+ select: { id: true, store: { select: { name: true } } },
+ });
+ if (!account) {
+ throw new NotFoundException('该门店无可用账户,请先创建门店账户');
+ }
+ return account.id;
+ }
+
+ async createToken(dto: AdminRedeemDebugCreateTokenDto) {
+ return this.redeemService.createToken(BigInt(dto.userId), {
+ amount: dto.amount,
+ couponId: dto.couponId,
+ storeId: dto.storeId,
+ });
+ }
+
+ async preview(dto: AdminRedeemDebugStoreTokenDto) {
+ const storeAccountId = await this.resolveStoreAccountId(dto.storeId);
+ return this.redeemService.previewRedeem(storeAccountId, dto.token);
+ }
+
+ async confirm(dto: AdminRedeemDebugStoreTokenDto) {
+ const storeAccountId = await this.resolveStoreAccountId(dto.storeId);
+ return this.redeemService.confirmRedeem(storeAccountId, { token: dto.token });
+ }
+}
diff --git a/server/dukang-api/src/modules/ops/admin-users.controller.ts b/server/dukang-api/src/modules/ops/admin-users.controller.ts
index 0acc807..54848ea 100644
--- a/server/dukang-api/src/modules/ops/admin-users.controller.ts
+++ b/server/dukang-api/src/modules/ops/admin-users.controller.ts
@@ -1,8 +1,9 @@
-import { Controller, Delete, Get, Param, Query, UseGuards } from '@nestjs/common';
+import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
import { AdminUsersService } from './admin-users.service';
import { AdminUsersQueryDto } from './dto/admin-query.dto';
+import { BatchDeleteUsersConfirmDto, BatchDeleteUsersDto } from './dto/admin-mutate.dto';
@Controller('admin/users')
@UseGuards(HqAuthGuard)
@@ -14,6 +15,21 @@ export class AdminUsersController {
return this.usersService.list(query);
}
+ @Post('batch-delete/preview')
+ @UseGuards(SuperAdminGuard)
+ previewBatchDelete(@Body() dto: BatchDeleteUsersDto) {
+ return this.usersService.previewBatchDelete(dto.ids.map((id) => BigInt(id)));
+ }
+
+ @Post('batch-delete')
+ @UseGuards(SuperAdminGuard)
+ batchDelete(@Body() dto: BatchDeleteUsersConfirmDto) {
+ return this.usersService.batchDeleteUsers(
+ dto.ids.map((id) => BigInt(id)),
+ dto.confirmRisk,
+ );
+ }
+
@Get(':id')
detail(@Param('id') id: string) {
return this.usersService.detail(BigInt(id));
diff --git a/server/dukang-api/src/modules/ops/admin-users.service.ts b/server/dukang-api/src/modules/ops/admin-users.service.ts
index 18d3cff..9637995 100644
--- a/server/dukang-api/src/modules/ops/admin-users.service.ts
+++ b/server/dukang-api/src/modules/ops/admin-users.service.ts
@@ -1,9 +1,11 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+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;
@@ -115,46 +117,108 @@ export class AdminUsersService {
});
}
+ 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) => {
- 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 } });
+ await this.deleteUserInTx(tx, id);
});
return {
@@ -162,4 +226,50 @@ export class AdminUsersService {
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 } });
+ }
}
diff --git a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
index b7d58fd..d2dc4e2 100644
--- a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
+++ b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
@@ -1,4 +1,5 @@
-import { IsArray, IsIn, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
+import { Type } from 'class-transformer';
+import { IsArray, IsBoolean, IsIn, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, Min } from 'class-validator';
export class UpdateStoreStatusDto {
@IsString()
@@ -307,6 +308,96 @@ export class UpdateOrderStatusDto {
status: string;
}
+export class BatchDeleteOrdersDto {
+ @IsArray()
+ @IsString({ each: true })
+ ids: string[];
+}
+
+export class BatchDeleteUsersDto {
+ @IsArray()
+ @IsString({ each: true })
+ ids: string[];
+}
+
+export class BatchDeleteUsersConfirmDto extends BatchDeleteUsersDto {
+ @IsBoolean()
+ confirmRisk: boolean;
+}
+
+/** HQ 订单发货(目前仅小飞侠 XFX) */
+export class AdminShipOrderDto {
+ @IsIn(['XFX'])
+ provider: string;
+
+ @IsOptional()
+ @IsString()
+ fromName?: string;
+
+ @IsOptional()
+ @IsString()
+ fromMobile?: string;
+
+ @IsOptional()
+ @IsString()
+ fromAddress?: string;
+
+ @IsOptional()
+ @IsString()
+ fromAddressDetail?: string;
+
+ @IsOptional()
+ @IsNumber()
+ fromLng?: number;
+
+ @IsOptional()
+ @IsNumber()
+ fromLat?: number;
+
+ @IsOptional()
+ @Type(() => Number)
+ @IsNumber()
+ @Min(0.01)
+ weight?: number;
+
+ @IsOptional()
+ @IsIn(['1', '2'])
+ payMode?: string;
+
+ @IsOptional()
+ @IsString()
+ remark?: string;
+}
+
+export class AdminRedeemDebugCreateTokenDto {
+ @IsString()
+ @IsNotEmpty()
+ userId: string;
+
+ @Type(() => Number)
+ @IsNumber()
+ @Min(0.01)
+ amount: number;
+
+ @IsOptional()
+ @IsString()
+ couponId?: string;
+
+ @IsOptional()
+ @IsString()
+ storeId?: string;
+}
+
+export class AdminRedeemDebugStoreTokenDto {
+ @IsString()
+ @IsNotEmpty()
+ storeId: string;
+
+ @IsString()
+ @IsNotEmpty()
+ token: string;
+}
+
export class UpdateDeliveryDto {
@IsOptional()
@IsString()
diff --git a/server/dukang-api/src/modules/ops/ops.module.ts b/server/dukang-api/src/modules/ops/ops.module.ts
index e89392b..c32711f 100644
--- a/server/dukang-api/src/modules/ops/ops.module.ts
+++ b/server/dukang-api/src/modules/ops/ops.module.ts
@@ -33,9 +33,12 @@ import { AdminXiaofeixiaController } from './admin-xiaofeixia.controller';
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
import { AdminProductDetailTemplatesController } from './admin-product-detail-templates.controller';
import { AdminProductDetailTemplatesService } from './admin-product-detail-templates.service';
+import { RedeemModule } from '../redeem/redeem.module';
+import { AdminRedeemDebugController } from './admin-redeem-debug.controller';
+import { AdminRedeemDebugService } from './admin-redeem-debug.service';
@Module({
- imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule],
+ imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
controllers: [
AdminDashboardController,
AdminUsersController,
@@ -56,6 +59,7 @@ import { AdminProductDetailTemplatesService } from './admin-product-detail-templ
AdminTicketsController,
AdminXiaofeixiaController,
AdminProductDetailTemplatesController,
+ AdminRedeemDebugController,
],
providers: [
AdminDashboardService,
@@ -73,6 +77,7 @@ import { AdminProductDetailTemplatesService } from './admin-product-detail-templ
AdminTicketsService,
AdminXiaofeixiaService,
AdminProductDetailTemplatesService,
+ AdminRedeemDebugService,
SuperAdminGuard,
],
})