v3数据表更改

This commit is contained in:
2026-07-01 14:36:50 +08:00
parent 638898b71e
commit aeb4ecfc84
46 changed files with 4553 additions and 918 deletions
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { IamModule } from '../iam/iam.module';
import { ResourceService } from './resource.service';
import { EventService } from './event.service';
import { TicketService } from './ticket.service';
import { ThirdPartyLogService } from './third-party-log.service';
import { ResourceController } from './resource.controller';
import { EventController } from './event.controller';
import { TicketController } from './ticket.controller';
import { ThirdPartyLogController } from './third-party-log.controller';
@Module({
imports: [IamModule],
controllers: [ResourceController, EventController, TicketController, ThirdPartyLogController],
providers: [ResourceService, EventService, TicketService, ThirdPartyLogService],
exports: [ResourceService, EventService, TicketService],
})
export class CommonModule {}
@@ -0,0 +1,178 @@
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class UploadTokenDto {
@IsString()
@IsNotEmpty()
bizType: string;
@IsString()
@IsIn(['IMAGE', 'VIDEO', 'FILE'])
mediaType: string;
@IsString()
@IsNotEmpty()
fileName: string;
}
export class RegisterResourceDto {
@IsString()
@IsNotEmpty()
ownerType: string;
@IsString()
@IsNotEmpty()
ownerId: string;
@IsString()
@IsNotEmpty()
bizType: string;
@IsString()
@IsIn(['IMAGE', 'VIDEO', 'FILE'])
mediaType: string;
@IsString()
@IsNotEmpty()
ossKey: string;
@IsString()
@IsNotEmpty()
url: string;
@IsOptional()
@IsString()
ossBucket?: string;
@IsOptional()
@IsString()
fileName?: string;
@IsOptional()
sortOrder?: number;
}
export class UpdateResourceDto {
@IsOptional()
@IsString()
url?: string;
@IsOptional()
@IsIn(['IMAGE', 'VIDEO', 'FILE'])
mediaType?: string;
@IsOptional()
sortOrder?: number;
@IsOptional()
@IsIn(['ACTIVE', 'DELETED'])
status?: string;
}
export class CreateEventDto {
@IsString()
@IsNotEmpty()
eventType: string;
@IsString()
@IsNotEmpty()
refType: string;
@IsString()
@IsNotEmpty()
refId: string;
@IsOptional()
@IsString()
actorType?: string;
@IsOptional()
@IsString()
actorId?: string;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
param1?: string;
@IsOptional()
@IsString()
param1Desc?: string;
@IsOptional()
@IsString()
param2?: string;
@IsOptional()
@IsString()
param2Desc?: string;
@IsOptional()
@IsString()
param3?: string;
@IsOptional()
@IsString()
param3Desc?: string;
@IsOptional()
amount1?: number;
@IsOptional()
amount2?: number;
@IsOptional()
@IsString()
remark?: string;
@IsOptional()
extraJson?: Record<string, unknown>;
}
export class CreateTicketDto {
@IsString()
@IsIn(['REFUND', 'RESHIPMENT', 'ALERT'])
ticketType: string;
@IsString()
@IsNotEmpty()
refType: string;
@IsString()
@IsNotEmpty()
refId: string;
@IsOptional()
@IsString()
remark?: string;
@IsOptional()
@IsString()
param1?: string;
@IsOptional()
@IsString()
param1Desc?: string;
}
export class UpdateTicketStatusDto {
@IsString()
@IsNotEmpty()
status: string;
@IsOptional()
@IsString()
remark?: string;
}
export class AssignTicketDto {
@IsString()
@IsIn(['HQ', 'PARTNER', 'SYSTEM'])
operatorType: string;
@IsString()
@IsNotEmpty()
operatorId: string;
}
@@ -0,0 +1,93 @@
import { Type } from 'class-transformer';
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
pageSize?: number = 20;
}
export class ResourceListQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
ownerType?: string;
@IsOptional()
@IsString()
ownerId?: string;
@IsOptional()
@IsString()
bizType?: string;
@IsOptional()
@IsIn(['ACTIVE', 'DELETED'])
status?: string;
}
export class EventListQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
refType?: string;
@IsOptional()
@IsString()
refId?: string;
@IsOptional()
@IsString()
eventType?: string;
}
export class EventTimelineQueryDto {
@IsString()
refType: string;
@IsString()
refId: string;
}
export class TicketListQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
ticketType?: string;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
refType?: string;
@IsOptional()
@IsString()
refId?: string;
}
export class ThirdPartyLogQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
provider?: string;
@IsOptional()
@IsString()
scene?: string;
@IsOptional()
@IsString()
refType?: string;
@IsOptional()
@IsString()
refId?: string;
}
@@ -0,0 +1,31 @@
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { EventService } from './event.service';
import { EventListQueryDto, EventTimelineQueryDto } from './dto/common-query.dto';
import { CreateEventDto } from './dto/common-mutate.dto';
@Controller('common/events')
@UseGuards(JwtAuthGuard)
export class EventController {
constructor(private readonly service: EventService) {}
@Post()
create(@Body() dto: CreateEventDto) {
return this.service.create(dto);
}
@Get()
list(@Query() query: EventListQueryDto) {
return this.service.list(query);
}
@Get('timeline')
timeline(@Query() query: EventTimelineQueryDto) {
return this.service.timeline(query);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.service.detail(BigInt(id));
}
}
@@ -0,0 +1,71 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import type { ActorType, EventType, ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import type { EventListQueryDto, EventTimelineQueryDto } from './dto/common-query.dto';
import type { CreateEventDto } from './dto/common-mutate.dto';
@Injectable()
export class EventService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateEventDto) {
const event = await this.prisma.commonEvent.create({
data: {
eventType: dto.eventType as EventType,
refType: dto.refType,
refId: BigInt(dto.refId),
actorType: dto.actorType as ActorType | undefined,
actorId: dto.actorId ? BigInt(dto.actorId) : undefined,
status: dto.status,
param1: dto.param1,
param1Desc: dto.param1Desc,
param2: dto.param2,
param2Desc: dto.param2Desc,
param3: dto.param3,
param3Desc: dto.param3Desc,
amount1: dto.amount1,
amount2: dto.amount2,
remark: dto.remark,
extraJson: dto.extraJson as Prisma.InputJsonValue | undefined,
},
});
return serializeBigInt(event);
}
async list(query: EventListQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.CommonEventWhereInput = {};
if (query.refType) where.refType = query.refType;
if (query.refId) where.refId = BigInt(query.refId);
if (query.eventType) where.eventType = query.eventType as Prisma.EnumEventTypeFilter['equals'];
const [items, total] = await Promise.all([
this.prisma.commonEvent.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.commonEvent.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async timeline(query: EventTimelineQueryDto) {
const items = await this.prisma.commonEvent.findMany({
where: { refType: query.refType, refId: BigInt(query.refId) },
orderBy: { createdAt: 'asc' },
take: 200,
});
return serializeBigInt(items);
}
async detail(id: bigint) {
const event = await this.prisma.commonEvent.findUnique({ where: { id } });
if (!event) throw new NotFoundException('事件不存在');
return serializeBigInt(event);
}
}
@@ -0,0 +1,41 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { ResourceService } from './resource.service';
import { ResourceListQueryDto } from './dto/common-query.dto';
import { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto';
@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);
}
@Post()
register(@Body() dto: RegisterResourceDto) {
return this.service.register(dto);
}
@Get()
list(@Query() query: ResourceListQueryDto) {
return this.service.list(query);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.service.detail(BigInt(id));
}
@Put(':id')
update(@Param('id') id: string, @Body() dto: UpdateResourceDto) {
return this.service.update(BigInt(id), dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.service.remove(BigInt(id));
}
}
@@ -0,0 +1,98 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import type { ResourceListQueryDto } from './dto/common-query.dto';
import type { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto';
@Injectable()
export class ResourceService {
constructor(private readonly prisma: PrismaService) {}
getUploadToken(dto: UploadTokenDto) {
const bucket = process.env.OSS_BUCKET || 'mock-dukang';
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
const key = `uploads/${dto.bizType.toLowerCase()}/${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
const cdnBase = process.env.OSS_CDN_BASE || 'https://mock-cdn.dukang.local';
return {
bucket,
region: process.env.OSS_REGION || 'oss-cn-hangzhou',
ossKey: key,
url: `${cdnBase}/${key}`,
mock: true,
expireAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
mediaType: dto.mediaType,
bizType: dto.bizType,
};
}
async register(dto: RegisterResourceDto) {
const resource = await this.prisma.commonResource.create({
data: {
ownerType: dto.ownerType as ResourceOwnerType,
ownerId: BigInt(dto.ownerId),
bizType: dto.bizType as ResourceBizType,
mediaType: dto.mediaType as ResourceMediaType,
ossBucket: dto.ossBucket ?? process.env.OSS_BUCKET ?? 'mock-dukang',
ossKey: dto.ossKey,
url: dto.url,
fileName: dto.fileName,
sortOrder: dto.sortOrder ?? 0,
},
});
return serializeBigInt(resource);
}
async list(query: ResourceListQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.CommonResourceWhereInput = {
status: (query.status ?? 'ACTIVE') as Prisma.EnumResourceStatusFilter['equals'],
};
if (query.ownerType) where.ownerType = query.ownerType as Prisma.EnumResourceOwnerTypeFilter['equals'];
if (query.ownerId) where.ownerId = BigInt(query.ownerId);
if (query.bizType) where.bizType = query.bizType as Prisma.EnumResourceBizTypeFilter['equals'];
const [items, total] = await Promise.all([
this.prisma.commonResource.findMany({
where,
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.commonResource.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async detail(id: bigint) {
const resource = await this.prisma.commonResource.findUnique({ where: { id } });
if (!resource) throw new NotFoundException('资源不存在');
return serializeBigInt(resource);
}
async update(id: bigint, dto: UpdateResourceDto) {
await this.detail(id);
const resource = await this.prisma.commonResource.update({
where: { id },
data: {
...(dto.url !== undefined ? { url: dto.url, ossKey: dto.url } : {}),
...(dto.mediaType !== undefined ? { mediaType: dto.mediaType as 'IMAGE' | 'VIDEO' | 'FILE' } : {}),
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DELETED' } : {}),
},
});
return serializeBigInt(resource);
}
async remove(id: bigint) {
await this.detail(id);
await this.prisma.commonResource.update({
where: { id },
data: { status: 'DELETED' },
});
return { ok: true };
}
}
@@ -0,0 +1,20 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { ThirdPartyLogService } from './third-party-log.service';
import { ThirdPartyLogQueryDto } from './dto/common-query.dto';
@Controller('common/third-party-logs')
@UseGuards(HqAuthGuard)
export class ThirdPartyLogController {
constructor(private readonly service: ThirdPartyLogService) {}
@Get()
list(@Query() query: ThirdPartyLogQueryDto) {
return this.service.list(query);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.service.detail(BigInt(id));
}
}
@@ -0,0 +1,37 @@
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 { ThirdPartyLogQueryDto } from './dto/common-query.dto';
@Injectable()
export class ThirdPartyLogService {
constructor(private readonly prisma: PrismaService) {}
async list(query: ThirdPartyLogQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.LogThirdPartyWhereInput = {};
if (query.provider) where.provider = query.provider as Prisma.EnumThirdPartyProviderFilter['equals'];
if (query.scene) where.scene = { contains: query.scene };
if (query.refType) where.refType = query.refType;
if (query.refId) where.refId = BigInt(query.refId);
const [items, 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, total, page, pageSize });
}
async detail(id: bigint) {
const log = await this.prisma.logThirdParty.findUnique({ where: { id } });
if (!log) throw new NotFoundException('日志不存在');
return serializeBigInt(log);
}
}
@@ -0,0 +1,38 @@
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { TicketService } from './ticket.service';
import { TicketListQueryDto } from './dto/common-query.dto';
import { AssignTicketDto, CreateTicketDto, UpdateTicketStatusDto } from './dto/common-mutate.dto';
@Controller('common/tickets')
@UseGuards(JwtAuthGuard)
export class TicketController {
constructor(private readonly service: TicketService) {}
@Post()
create(@Body() dto: CreateTicketDto) {
return this.service.create(dto);
}
@Get()
list(@Query() query: TicketListQueryDto) {
return this.service.list(query);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.service.detail(BigInt(id));
}
@Put(':id/status')
updateStatus(@Param('id') id: string, @Body() dto: UpdateTicketStatusDto) {
return this.service.updateStatus(BigInt(id), dto);
}
@Put(':id/assign')
@UseGuards(HqAuthGuard)
assign(@Param('id') id: string, @Body() dto: AssignTicketDto) {
return this.service.assign(BigInt(id), dto);
}
}
@@ -0,0 +1,83 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import type { ActorType, TicketType } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import type { TicketListQueryDto } from './dto/common-query.dto';
import type { AssignTicketDto, CreateTicketDto, UpdateTicketStatusDto } from './dto/common-mutate.dto';
function generateTicketNo() {
return `TK${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
}
@Injectable()
export class TicketService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateTicketDto) {
const ticket = await this.prisma.commonTicket.create({
data: {
ticketNo: generateTicketNo(),
ticketType: dto.ticketType as TicketType,
refType: dto.refType,
refId: BigInt(dto.refId),
remark: dto.remark,
param1: dto.param1,
param1Desc: dto.param1Desc,
},
});
return serializeBigInt(ticket);
}
async list(query: TicketListQueryDto) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.CommonTicketWhereInput = {};
if (query.ticketType) where.ticketType = query.ticketType as Prisma.EnumTicketTypeFilter['equals'];
if (query.status) where.status = query.status;
if (query.refType) where.refType = query.refType;
if (query.refId) where.refId = BigInt(query.refId);
const [items, total] = await Promise.all([
this.prisma.commonTicket.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.commonTicket.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async detail(id: bigint) {
const ticket = await this.prisma.commonTicket.findUnique({ where: { id } });
if (!ticket) throw new NotFoundException('工单不存在');
return serializeBigInt(ticket);
}
async updateStatus(id: bigint, dto: UpdateTicketStatusDto) {
await this.detail(id);
const ticket = await this.prisma.commonTicket.update({
where: { id },
data: {
status: dto.status,
remark: dto.remark,
completedAt: ['COMPLETED', 'CLOSED', 'RESOLVED'].includes(dto.status) ? new Date() : undefined,
},
});
return serializeBigInt(ticket);
}
async assign(id: bigint, dto: AssignTicketDto) {
await this.detail(id);
const ticket = await this.prisma.commonTicket.update({
where: { id },
data: {
operatorType: dto.operatorType as ActorType,
operatorId: BigInt(dto.operatorId),
},
});
return serializeBigInt(ticket);
}
}