feat;提交管理端和城市合伙人端
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminPromoCodesService } from './admin-promo-codes.service';
|
||||
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
||||
import { CreatePromoCodeDto, UpdatePromoCodeStatusDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/promo-codes')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminPromoCodesController {
|
||||
constructor(private readonly service: AdminPromoCodesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminPromoCodesQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id/stats')
|
||||
stats(@Param('id') id: string) {
|
||||
return this.service.stats(BigInt(id));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreatePromoCodeDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
updateStatus(@Param('id') id: string, @Body() dto: UpdatePromoCodeStatusDto) {
|
||||
return this.service.updateStatus(BigInt(id), dto.status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { CreatePromoCodeDto } from './dto/admin-mutate.dto';
|
||||
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
function userH5Base(): string {
|
||||
return (process.env.USER_H5_URL || 'http://localhost:5173').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function buildLandingUrl(code: string): string {
|
||||
return `${userH5Base()}/?promo=${encodeURIComponent(code)}`;
|
||||
}
|
||||
|
||||
function randomCode(): string {
|
||||
const n = Math.random().toString(36).slice(2, 8).toUpperCase();
|
||||
return `DK${n}`;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminPromoCodesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
private mapRow(row: {
|
||||
id: bigint;
|
||||
code: string;
|
||||
name: string;
|
||||
status: string;
|
||||
scanCount: number;
|
||||
orderCount: number;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
status: row.status,
|
||||
scanCount: row.scanCount,
|
||||
orderCount: row.orderCount,
|
||||
landingUrl: buildLandingUrl(row.code),
|
||||
createdAt: row.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
async list(query: AdminPromoCodesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: {
|
||||
status?: 'ACTIVE' | 'DISABLED';
|
||||
name?: { contains: string };
|
||||
code?: { contains: string };
|
||||
} = {};
|
||||
if (query.status) where.status = query.status as 'ACTIVE' | 'DISABLED';
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.code) where.code = { contains: query.code };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonPromoCode.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.commonPromoCode.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((r) => this.mapRow(r)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const row = await this.prisma.commonPromoCode.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('推广码不存在');
|
||||
const stats = this.statsFromRow(row);
|
||||
return serializeBigInt({ ...this.mapRow(row), stats });
|
||||
}
|
||||
|
||||
async create(dto: CreatePromoCodeDto) {
|
||||
let code = dto.code?.trim().toUpperCase();
|
||||
if (code) {
|
||||
const exists = await this.prisma.commonPromoCode.findUnique({ where: { code } });
|
||||
if (exists) throw new BadRequestException('推广码已存在');
|
||||
} else {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const candidate = randomCode();
|
||||
const exists = await this.prisma.commonPromoCode.findUnique({ where: { code: candidate } });
|
||||
if (!exists) {
|
||||
code = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!code) throw new BadRequestException('生成推广码失败,请重试');
|
||||
}
|
||||
|
||||
const row = await this.prisma.commonPromoCode.create({
|
||||
data: {
|
||||
code,
|
||||
name: dto.name.trim(),
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
async updateStatus(id: bigint, status: 'ACTIVE' | 'DISABLED') {
|
||||
const row = await this.prisma.commonPromoCode.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
});
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
async stats(id: bigint) {
|
||||
const row = await this.prisma.commonPromoCode.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('推广码不存在');
|
||||
return serializeBigInt(this.statsFromRow(row));
|
||||
}
|
||||
|
||||
private statsFromRow(row: { scanCount: number; orderCount: number }) {
|
||||
const scanCount = row.scanCount;
|
||||
const orderCount = row.orderCount;
|
||||
const conversionRate =
|
||||
scanCount > 0 ? Math.round((orderCount / scanCount) * 1000) / 10 : 0;
|
||||
return { scanCount, orderCount, conversionRate };
|
||||
}
|
||||
}
|
||||
@@ -426,3 +426,19 @@ export class UpdateProductDto {
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
export class CreatePromoCodeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export class UpdatePromoCodeStatusDto {
|
||||
@IsString()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status: 'ACTIVE' | 'DISABLED';
|
||||
}
|
||||
|
||||
@@ -236,3 +236,17 @@ export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||
@IsString()
|
||||
mediaType?: string;
|
||||
}
|
||||
|
||||
export class AdminPromoCodesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@@ -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 { AdminPromoCodesController } from './admin-promo-codes.controller';
|
||||
import { AdminPromoCodesService } from './admin-promo-codes.service';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
|
||||
@Module({
|
||||
@@ -41,6 +43,7 @@ import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
AdminDeliveriesController,
|
||||
AdminHqAccountsController,
|
||||
AdminProductsController,
|
||||
AdminPromoCodesController,
|
||||
],
|
||||
providers: [
|
||||
AdminDashboardService,
|
||||
@@ -54,6 +57,7 @@ import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
AdminDeliveriesService,
|
||||
AdminHqAccountsService,
|
||||
AdminProductsService,
|
||||
AdminPromoCodesService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user