eb961c3a74
Serve user/shop/partner H5 under /user/, /shop/, /partner/ on a single authorized domain; update nginx, SSL script, and router base paths. Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
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/user').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 };
|
|
}
|
|
}
|