webadmin增加oss日志
This commit is contained in:
@@ -243,6 +243,7 @@ enum ThirdPartyProvider {
|
||||
WECHAT_REFUND
|
||||
WECHAT_AUTH
|
||||
WECHAT_MAP
|
||||
ALIYUN_OSS
|
||||
XFX
|
||||
SMS
|
||||
LOGISTICS
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
export type OssActorRef = {
|
||||
refType: string;
|
||||
refId: bigint;
|
||||
};
|
||||
|
||||
export function ossActorRefFromAuth(
|
||||
actorType?: string,
|
||||
actorId?: bigint,
|
||||
): OssActorRef | undefined {
|
||||
if (!actorType || actorId == null) return undefined;
|
||||
return { refType: actorType, refId: actorId };
|
||||
}
|
||||
|
||||
type LogOssUploadInput = {
|
||||
scene: 'UPLOAD_PUT_OBJECT' | 'UPLOAD_TOKEN';
|
||||
requestBody?: Record<string, unknown>;
|
||||
responseBody?: Record<string, unknown>;
|
||||
externalNo?: string;
|
||||
status: 'SUCCESS' | 'FAILED';
|
||||
errorMessage?: string;
|
||||
actorRef?: OssActorRef;
|
||||
};
|
||||
|
||||
export async function logOssUpload(prisma: PrismaService, input: LogOssUploadInput) {
|
||||
const row = await prisma.logThirdParty.create({
|
||||
data: {
|
||||
provider: 'ALIYUN_OSS',
|
||||
scene: input.scene,
|
||||
refType: input.actorRef?.refType,
|
||||
refId: input.actorRef?.refId,
|
||||
requestBody: input.requestBody as never,
|
||||
responseBody: input.responseBody as never,
|
||||
externalNo: input.externalNo?.slice(0, 128),
|
||||
status: input.status,
|
||||
errorMessage: input.errorMessage?.slice(0, 512),
|
||||
},
|
||||
});
|
||||
return row.id;
|
||||
}
|
||||
@@ -12,21 +12,31 @@ import {
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { ResourceService } from './resource.service';
|
||||
import { JwtAuthGuard, type AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { ResourceService, type OssUploadActor } from './resource.service';
|
||||
import { ResourceListQueryDto } from './dto/common-query.dto';
|
||||
import { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
||||
|
||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
function resolveUploadActor(user?: AuthUser): OssUploadActor | undefined {
|
||||
if (!user) return undefined;
|
||||
return {
|
||||
refType: user.actorType,
|
||||
refId: user.actorId,
|
||||
clientApp: user.clientApp,
|
||||
};
|
||||
}
|
||||
|
||||
@Controller('common/resources')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ResourceController {
|
||||
constructor(private readonly service: ResourceService) {}
|
||||
|
||||
@Post('upload-token')
|
||||
uploadToken(@Body() dto: UploadTokenDto) {
|
||||
return this.service.getUploadToken(dto);
|
||||
uploadToken(@CurrentUser() user: AuthUser, @Body() dto: UploadTokenDto) {
|
||||
return this.service.getUploadToken(dto, resolveUploadActor(user));
|
||||
}
|
||||
|
||||
@Post('upload')
|
||||
@@ -35,8 +45,12 @@ export class ResourceController {
|
||||
limits: { fileSize: Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES) },
|
||||
}),
|
||||
)
|
||||
upload(@UploadedFile() file: Express.Multer.File, @Body() dto: UploadFileDto) {
|
||||
return this.service.uploadFile(file, dto);
|
||||
upload(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body() dto: UploadFileDto,
|
||||
) {
|
||||
return this.service.uploadFile(file, dto, resolveUploadActor(user));
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -5,11 +5,16 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||
import { logOssUpload, type OssActorRef } from '../../integrations/oss/oss-log.util';
|
||||
import type { ResourceListQueryDto } from './dto/common-query.dto';
|
||||
import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
||||
|
||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
export type OssUploadActor = OssActorRef & {
|
||||
clientApp?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ResourceService {
|
||||
constructor(
|
||||
@@ -17,25 +22,120 @@ export class ResourceService {
|
||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||
) {}
|
||||
|
||||
getUploadToken(dto: UploadTokenDto) {
|
||||
return this.oss.getUploadToken(dto);
|
||||
getUploadToken(dto: UploadTokenDto, actor?: OssUploadActor) {
|
||||
try {
|
||||
const result = this.oss.getUploadToken(dto);
|
||||
void logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_TOKEN',
|
||||
actorRef: actor,
|
||||
requestBody: {
|
||||
bizType: dto.bizType,
|
||||
mediaType: dto.mediaType,
|
||||
fileName: dto.fileName,
|
||||
clientApp: actor?.clientApp,
|
||||
},
|
||||
responseBody: {
|
||||
bucket: result.bucket,
|
||||
ossKey: result.ossKey,
|
||||
url: result.url,
|
||||
mock: result.mock ?? false,
|
||||
},
|
||||
externalNo: result.ossKey,
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
void logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_TOKEN',
|
||||
actorRef: actor,
|
||||
requestBody: {
|
||||
bizType: dto.bizType,
|
||||
mediaType: dto.mediaType,
|
||||
fileName: dto.fileName,
|
||||
clientApp: actor?.clientApp,
|
||||
},
|
||||
status: 'FAILED',
|
||||
errorMessage: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(file: Express.Multer.File | undefined, dto: UploadFileDto) {
|
||||
if (!file) {
|
||||
throw new BadRequestException('请选择要上传的文件');
|
||||
}
|
||||
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
||||
if (file.size > maxUploadBytes) {
|
||||
throw new BadRequestException(`文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`);
|
||||
}
|
||||
return this.oss.putObject({
|
||||
async uploadFile(
|
||||
file: Express.Multer.File | undefined,
|
||||
dto: UploadFileDto,
|
||||
actor?: OssUploadActor,
|
||||
) {
|
||||
const baseRequest = {
|
||||
bizType: dto.bizType,
|
||||
mediaType: dto.mediaType,
|
||||
clientApp: actor?.clientApp,
|
||||
};
|
||||
|
||||
if (!file) {
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody: baseRequest,
|
||||
status: 'FAILED',
|
||||
errorMessage: '请选择要上传的文件',
|
||||
});
|
||||
throw new BadRequestException('请选择要上传的文件');
|
||||
}
|
||||
|
||||
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
||||
const requestBody = {
|
||||
...baseRequest,
|
||||
fileName: file.originalname || 'upload.bin',
|
||||
buffer: file.buffer,
|
||||
fileSize: file.size,
|
||||
mimeType: file.mimetype,
|
||||
});
|
||||
};
|
||||
|
||||
if (file.size > maxUploadBytes) {
|
||||
const message = `文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`;
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody,
|
||||
status: 'FAILED',
|
||||
errorMessage: message,
|
||||
});
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.oss.putObject({
|
||||
bizType: dto.bizType,
|
||||
mediaType: dto.mediaType,
|
||||
fileName: file.originalname || 'upload.bin',
|
||||
buffer: file.buffer,
|
||||
mimeType: file.mimetype,
|
||||
});
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody,
|
||||
responseBody: {
|
||||
bucket: result.bucket,
|
||||
region: result.region,
|
||||
ossKey: result.ossKey,
|
||||
url: result.url,
|
||||
mock: result.mock ?? false,
|
||||
},
|
||||
externalNo: result.ossKey,
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody,
|
||||
status: 'FAILED',
|
||||
errorMessage: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async register(dto: RegisterResourceDto) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminOssLogsService } from './admin-oss-logs.service';
|
||||
import { AdminOssLogsQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Controller('admin/logs/oss')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminOssLogsController {
|
||||
constructor(private readonly service: AdminOssLogsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminOssLogsQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { 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 { AdminOssLogsQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
function mapOssLogRow(row: {
|
||||
id: bigint;
|
||||
scene: string;
|
||||
refType: string | null;
|
||||
refId: bigint | null;
|
||||
requestBody: unknown;
|
||||
responseBody: unknown;
|
||||
externalNo: string | null;
|
||||
status: string;
|
||||
errorMessage: string | null;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
const req = (row.requestBody ?? {}) as Record<string, unknown>;
|
||||
const res = (row.responseBody ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
id: row.id,
|
||||
scene: row.scene,
|
||||
status: row.status,
|
||||
actorType: row.refType,
|
||||
actorId: row.refId,
|
||||
clientApp: (req.clientApp as string | undefined) ?? null,
|
||||
bizType: (req.bizType as string | undefined) ?? null,
|
||||
mediaType: (req.mediaType as string | undefined) ?? null,
|
||||
fileName: (req.fileName as string | undefined) ?? null,
|
||||
fileSize: (req.fileSize as number | undefined) ?? null,
|
||||
mimeType: (req.mimeType as string | undefined) ?? null,
|
||||
ossKey: (res.ossKey as string | undefined) ?? row.externalNo ?? null,
|
||||
url: (res.url as string | undefined) ?? null,
|
||||
bucket: (res.bucket as string | undefined) ?? null,
|
||||
mock: (res.mock as boolean | undefined) ?? null,
|
||||
errorMessage: row.errorMessage,
|
||||
createdAt: row.createdAt,
|
||||
requestBody: row.requestBody,
|
||||
responseBody: row.responseBody,
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminOssLogsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminOssLogsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.LogThirdPartyWhereInput = {
|
||||
provider: 'ALIYUN_OSS',
|
||||
};
|
||||
if (query.scene) where.scene = query.scene;
|
||||
if (query.status) where.status = query.status as Prisma.EnumThirdPartyLogStatusFilter['equals'];
|
||||
if (query.refType) where.refType = query.refType;
|
||||
if (query.refId) where.refId = BigInt(query.refId);
|
||||
|
||||
const andFilters: Prisma.LogThirdPartyWhereInput[] = [];
|
||||
if (query.bizType) {
|
||||
andFilters.push({
|
||||
requestBody: { string_contains: `"bizType":"${query.bizType}"` },
|
||||
});
|
||||
}
|
||||
if (query.clientApp) {
|
||||
andFilters.push({
|
||||
requestBody: { string_contains: `"clientApp":"${query.clientApp}"` },
|
||||
});
|
||||
}
|
||||
if (andFilters.length) {
|
||||
where.AND = andFilters;
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.logThirdParty.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.logThirdParty.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: rows.map(mapOssLogRow),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const row = await this.prisma.logThirdParty.findFirst({
|
||||
where: { id, provider: 'ALIYUN_OSS' },
|
||||
});
|
||||
if (!row) throw new NotFoundException('OSS 上传日志不存在');
|
||||
return serializeBigInt(mapOssLogRow(row));
|
||||
}
|
||||
}
|
||||
@@ -399,6 +399,32 @@ export class AdminHqLogsQueryDto extends PaginationQueryDto {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export class AdminOssLogsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
scene?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['SUCCESS', 'FAILED', 'PENDING'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bizType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
clientApp?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
refType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
refId?: string;
|
||||
}
|
||||
|
||||
export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -31,6 +31,8 @@ import { AdminPartnerLogsController } from './admin-partner-logs.controller';
|
||||
import { AdminPartnerLogsService } from './admin-partner-logs.service';
|
||||
import { AdminHqLogsController } from './admin-hq-logs.controller';
|
||||
import { AdminHqLogsService } from './admin-hq-logs.service';
|
||||
import { AdminOssLogsController } from './admin-oss-logs.controller';
|
||||
import { AdminOssLogsService } from './admin-oss-logs.service';
|
||||
import { AdminTicketsController } from './admin-tickets.controller';
|
||||
import { AdminTicketsService } from './admin-tickets.service';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
@@ -77,6 +79,7 @@ import { AdminDeployService } from './admin-deploy.service';
|
||||
AdminStoreLogsController,
|
||||
AdminPartnerLogsController,
|
||||
AdminHqLogsController,
|
||||
AdminOssLogsController,
|
||||
AdminTicketsController,
|
||||
AdminXiaofeixiaController,
|
||||
AdminProductDetailTemplatesController,
|
||||
@@ -101,6 +104,7 @@ import { AdminDeployService } from './admin-deploy.service';
|
||||
AdminStoreLogsService,
|
||||
AdminPartnerLogsService,
|
||||
AdminHqLogsService,
|
||||
AdminOssLogsService,
|
||||
AdminTicketsService,
|
||||
AdminXiaofeixiaService,
|
||||
AdminProductDetailTemplatesService,
|
||||
|
||||
Reference in New Issue
Block a user