短信验证调试成功
This commit is contained in:
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { groupResourcesByProductId, mapProductMedia } from '../catalog/catalog.mapper';
|
||||
import type { AdminProductsQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateProductDto, UpdateProductDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@@ -27,11 +28,23 @@ export class AdminProductsService {
|
||||
}),
|
||||
this.prisma.commonProductItem.count({ where }),
|
||||
]);
|
||||
|
||||
const productIds = items.map((p) => p.id);
|
||||
const resources = productIds.length
|
||||
? await this.prisma.commonResource.findMany({
|
||||
where: {
|
||||
ownerType: 'PRODUCT',
|
||||
ownerId: { in: productIds },
|
||||
status: 'ACTIVE',
|
||||
bizType: { in: ['CAROUSEL', 'DETAIL'] },
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
: [];
|
||||
const resourceMap = groupResourcesByProductId(resources);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
...p,
|
||||
mainImageUrl: p.coverResource?.url ?? null,
|
||||
})),
|
||||
items: items.map((p) => this.formatProduct(p, resourceMap.get(p.id.toString()) ?? [])),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -44,7 +57,18 @@ export class AdminProductsService {
|
||||
include: { coverResource: true },
|
||||
});
|
||||
if (!product) throw new NotFoundException('商品不存在');
|
||||
return serializeBigInt({ ...product, mainImageUrl: product.coverResource?.url ?? null });
|
||||
|
||||
const resources = await this.prisma.commonResource.findMany({
|
||||
where: {
|
||||
ownerType: 'PRODUCT',
|
||||
ownerId: id,
|
||||
status: 'ACTIVE',
|
||||
bizType: { in: ['CAROUSEL', 'DETAIL'] },
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
|
||||
return serializeBigInt(this.formatProduct(product, resources));
|
||||
}
|
||||
|
||||
async create(dto: CreateProductDto) {
|
||||
@@ -65,26 +89,19 @@ export class AdminProductsService {
|
||||
benefitAmount: dto.benefitAmount ?? dto.price,
|
||||
status: (dto.status ?? 'DRAFT') as 'DRAFT' | 'ON_SALE' | 'OFF_SALE',
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
...(dto.detailContent !== undefined
|
||||
? { detailContent: dto.detailContent as Prisma.InputJsonValue }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (dto.coverUrl) {
|
||||
const cover = await this.prisma.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'PRODUCT',
|
||||
ownerId: product.id,
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: dto.coverUrl,
|
||||
url: dto.coverUrl,
|
||||
},
|
||||
});
|
||||
await this.prisma.commonProductItem.update({
|
||||
where: { id: product.id },
|
||||
data: { coverResourceId: cover.id },
|
||||
});
|
||||
await this.syncCover(product.id, dto.coverUrl);
|
||||
}
|
||||
await this.syncProductMedia(product.id, {
|
||||
carouselUrls: dto.carouselUrls,
|
||||
detailImageUrls: dto.detailImageUrls,
|
||||
});
|
||||
|
||||
return this.detail(product.id);
|
||||
}
|
||||
@@ -101,35 +118,96 @@ export class AdminProductsService {
|
||||
...(dto.benefitAmount !== undefined ? { benefitAmount: dto.benefitAmount } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'DRAFT' | 'ON_SALE' | 'OFF_SALE' } : {}),
|
||||
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
||||
...(dto.detailContent !== undefined
|
||||
? { detailContent: dto.detailContent as Prisma.InputJsonValue }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (dto.coverUrl) {
|
||||
const product = await this.prisma.commonProductItem.findUniqueOrThrow({ where: { id } });
|
||||
if (product.coverResourceId) {
|
||||
await this.prisma.commonResource.update({
|
||||
where: { id: product.coverResourceId },
|
||||
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
||||
});
|
||||
} else {
|
||||
const cover = await this.prisma.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'PRODUCT',
|
||||
ownerId: id,
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: dto.coverUrl,
|
||||
url: dto.coverUrl,
|
||||
},
|
||||
});
|
||||
await this.prisma.commonProductItem.update({
|
||||
where: { id },
|
||||
data: { coverResourceId: cover.id },
|
||||
});
|
||||
}
|
||||
await this.syncCover(id, dto.coverUrl);
|
||||
}
|
||||
await this.syncProductMedia(id, {
|
||||
carouselUrls: dto.carouselUrls,
|
||||
detailImageUrls: dto.detailImageUrls,
|
||||
});
|
||||
|
||||
return this.detail(id);
|
||||
}
|
||||
|
||||
private formatProduct(
|
||||
product: Prisma.CommonProductItemGetPayload<{ include: { coverResource: true } }>,
|
||||
extraResources: Prisma.CommonResourceGetPayload<object>[],
|
||||
) {
|
||||
const media = mapProductMedia(product, extraResources);
|
||||
return {
|
||||
...product,
|
||||
price: Number(product.price),
|
||||
benefitAmount: Number(product.benefitAmount ?? product.price),
|
||||
...media,
|
||||
};
|
||||
}
|
||||
|
||||
private async syncCover(productId: bigint, coverUrl: string) {
|
||||
const product = await this.prisma.commonProductItem.findUniqueOrThrow({ where: { id: productId } });
|
||||
if (product.coverResourceId) {
|
||||
await this.prisma.commonResource.update({
|
||||
where: { id: product.coverResourceId },
|
||||
data: { url: coverUrl, ossKey: coverUrl },
|
||||
});
|
||||
} else {
|
||||
const cover = await this.prisma.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'PRODUCT',
|
||||
ownerId: productId,
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: coverUrl,
|
||||
url: coverUrl,
|
||||
},
|
||||
});
|
||||
await this.prisma.commonProductItem.update({
|
||||
where: { id: productId },
|
||||
data: { coverResourceId: cover.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async syncProductMedia(
|
||||
productId: bigint,
|
||||
dto: { carouselUrls?: string[]; detailImageUrls?: string[] },
|
||||
) {
|
||||
if (dto.carouselUrls !== undefined) {
|
||||
await this.replaceProductResources(productId, 'CAROUSEL', dto.carouselUrls);
|
||||
}
|
||||
if (dto.detailImageUrls !== undefined) {
|
||||
await this.replaceProductResources(productId, 'DETAIL', dto.detailImageUrls);
|
||||
}
|
||||
}
|
||||
|
||||
private async replaceProductResources(
|
||||
productId: bigint,
|
||||
bizType: 'CAROUSEL' | 'DETAIL',
|
||||
urls: string[],
|
||||
) {
|
||||
const cleaned = urls.map((u) => u?.trim()).filter(Boolean);
|
||||
await this.prisma.commonResource.deleteMany({
|
||||
where: { ownerType: 'PRODUCT', ownerId: productId, bizType },
|
||||
});
|
||||
if (cleaned.length === 0) return;
|
||||
await this.prisma.commonResource.createMany({
|
||||
data: cleaned.map((url, sortOrder) => ({
|
||||
ownerType: 'PRODUCT' as const,
|
||||
ownerId: productId,
|
||||
bizType,
|
||||
mediaType: 'IMAGE' as const,
|
||||
ossBucket: 'legacy',
|
||||
ossKey: url,
|
||||
url,
|
||||
sortOrder,
|
||||
status: 'ACTIVE' as const,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminUserLogsService } from './admin-user-logs.service';
|
||||
import { AdminUserLogsQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Controller('admin/logs/users')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminUserLogsController {
|
||||
constructor(private readonly service: AdminUserLogsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminUserLogsQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { eventNamesForUserLogCategory, resolveUserLogCategory } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminUserLogsQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminUserLogsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminUserLogsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.LogUserAnalyticsWhereInput = {};
|
||||
|
||||
if (query.userId) {
|
||||
where.userId = BigInt(query.userId);
|
||||
} else if (query.phone || query.userNo) {
|
||||
const userWhere: Prisma.UserWhereInput = {};
|
||||
if (query.phone) userWhere.phone = { contains: query.phone };
|
||||
if (query.userNo) userWhere.userNo = { contains: query.userNo };
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: userWhere,
|
||||
select: { id: true },
|
||||
take: 100,
|
||||
});
|
||||
if (users.length === 0) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
where.userId = { in: users.map((u) => u.id) };
|
||||
}
|
||||
|
||||
if (query.eventName) {
|
||||
where.eventName = query.eventName;
|
||||
} else if (query.category) {
|
||||
const names = eventNamesForUserLogCategory(query.category);
|
||||
if (names?.length) {
|
||||
where.eventName = { in: names };
|
||||
}
|
||||
}
|
||||
|
||||
if (query.from || query.to) {
|
||||
where.createdAt = {
|
||||
...(query.from ? { gte: new Date(query.from) } : {}),
|
||||
...(query.to ? { lte: new Date(query.to) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.logUserAnalytics.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.logUserAnalytics.count({ where }),
|
||||
]);
|
||||
|
||||
const userIds = [...new Set(rows.map((r) => r.userId).filter((id): id is bigint => id != null))];
|
||||
const users = userIds.length
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, userNo: true, phone: true, nickname: true },
|
||||
})
|
||||
: [];
|
||||
const userMap = new Map(users.map((u) => [u.id.toString(), u]));
|
||||
|
||||
return serializeBigInt({
|
||||
items: rows.map((row) => {
|
||||
const user = row.userId ? userMap.get(row.userId.toString()) : undefined;
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.userId,
|
||||
userNo: user?.userNo ?? null,
|
||||
phone: user?.phone ?? null,
|
||||
nickname: user?.nickname ?? null,
|
||||
category: resolveUserLogCategory(row.eventName),
|
||||
eventName: row.eventName,
|
||||
clientApp: row.clientApp,
|
||||
refType: row.refType,
|
||||
refId: row.refId,
|
||||
extraJson: row.extraJson,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const row = await this.prisma.logUserAnalytics.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('日志不存在');
|
||||
|
||||
const user = row.userId
|
||||
? await this.prisma.user.findUnique({
|
||||
where: { id: row.userId },
|
||||
select: { id: true, userNo: true, phone: true, nickname: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
return serializeBigInt({
|
||||
...row,
|
||||
userNo: user?.userNo ?? null,
|
||||
phone: user?.phone ?? null,
|
||||
nickname: user?.nickname ?? null,
|
||||
category: resolveUserLogCategory(row.eventName),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsArray, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { IsArray, IsIn, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateStoreStatusDto {
|
||||
@IsString()
|
||||
@@ -391,6 +391,20 @@ export class CreateProductDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
carouselUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
detailImageUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
detailContent?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class UpdateProductDto {
|
||||
@@ -425,4 +439,18 @@ export class UpdateProductDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
carouselUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
detailImageUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
detailContent?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,36 @@ export class AdminProductsQueryDto extends PaginationQueryDto {
|
||||
aromaType?: string;
|
||||
}
|
||||
|
||||
export class AdminUserLogsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
category?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
eventName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
from?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -21,6 +21,8 @@ import { AdminHqAccountsController } from './admin-hq-accounts.controller';
|
||||
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
||||
import { AdminProductsController } from './admin-products.controller';
|
||||
import { AdminProductsService } from './admin-products.service';
|
||||
import { AdminUserLogsController } from './admin-user-logs.controller';
|
||||
import { AdminUserLogsService } from './admin-user-logs.service';
|
||||
import { AdminTicketsController } from './admin-tickets.controller';
|
||||
import { AdminTicketsService } from './admin-tickets.service';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
@@ -45,6 +47,7 @@ import { CommonModule } from '../common/common.module';
|
||||
AdminDeliveriesController,
|
||||
AdminHqAccountsController,
|
||||
AdminProductsController,
|
||||
AdminUserLogsController,
|
||||
AdminTicketsController,
|
||||
],
|
||||
providers: [
|
||||
@@ -59,6 +62,7 @@ import { CommonModule } from '../common/common.module';
|
||||
AdminDeliveriesService,
|
||||
AdminHqAccountsService,
|
||||
AdminProductsService,
|
||||
AdminUserLogsService,
|
||||
AdminTicketsService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user