feat(promo): generate WeChat mini-program codes on create
Use getwxacodeunlimit with activity id as scene, upload PNG to OSS qrcode/, and show in HQ admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,7 +26,7 @@ export class PromoController {
|
||||
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||
return this.promoCodeService.touch(
|
||||
{ promoCode: dto.promoCode, qrcodeId: dto.qrcodeId },
|
||||
{ promoCode: dto.promoCode, qrcodeId: dto.qrcodeId, promoId: dto.promoId },
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class PromoTouchDto {
|
||||
@IsOptional()
|
||||
@@ -8,4 +8,9 @@ export class PromoTouchDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
qrcodeId?: string;
|
||||
|
||||
/** 小程序码 scene 中的推广活动 ID */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoId?: string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { randomBytes } from 'crypto';
|
||||
import * as QRCode from 'qrcode';
|
||||
import {
|
||||
PROMO_CODE_SCENE_LABELS,
|
||||
PromoCodeScene,
|
||||
@@ -14,8 +13,9 @@ import {
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { OSS_PROVIDER, WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||
import type {
|
||||
CreatePromoCodeDto,
|
||||
PromoCodeListQueryDto,
|
||||
@@ -70,6 +70,7 @@ export class PromoCodeService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
|
||||
) {}
|
||||
|
||||
listScenes() {
|
||||
@@ -231,18 +232,25 @@ export class PromoCodeService {
|
||||
throw new BadRequestException('生成二维码 ID 失败,请重试');
|
||||
}
|
||||
|
||||
private async createQrcodeResource(promoId: bigint, code: string, qrcodeId: string) {
|
||||
const landingUrl = buildLandingUrl(code, qrcodeId);
|
||||
const pngBuffer = await QRCode.toBuffer(landingUrl, {
|
||||
width: 512,
|
||||
margin: 1,
|
||||
type: 'png',
|
||||
color: { dark: '#1f1a17', light: '#ffffff' },
|
||||
/**
|
||||
* 调用微信 getwxacodeunlimit,scene=推广活动 ID,PNG 上传 OSS uploads/qrcode/
|
||||
*/
|
||||
private async createQrcodeResource(promoId: bigint, code: string) {
|
||||
const scene = promoId.toString();
|
||||
if (scene.length > 32) {
|
||||
throw new BadRequestException('推广活动 ID 过长,无法写入小程序码 scene');
|
||||
}
|
||||
const pngBuffer = await this.wechat.getWxaCodeUnlimited({
|
||||
scene,
|
||||
page: process.env.WX_MINI_PROMO_PAGE || 'pages/home/index',
|
||||
width: 430,
|
||||
checkPath: false,
|
||||
});
|
||||
const fileName = `promo-${code}-${scene}.png`;
|
||||
const uploaded = await this.oss.putObject({
|
||||
bizType: 'QRCODE',
|
||||
mediaType: 'IMAGE',
|
||||
fileName: `promo-${code}.png`,
|
||||
fileName,
|
||||
buffer: pngBuffer,
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
@@ -255,7 +263,7 @@ export class PromoCodeService {
|
||||
ossBucket: uploaded.bucket,
|
||||
ossKey: uploaded.ossKey,
|
||||
url: uploaded.url,
|
||||
fileName: `promo-${code}.png`,
|
||||
fileName,
|
||||
fileSize: BigInt(pngBuffer.length),
|
||||
mimeType: 'image/png',
|
||||
},
|
||||
@@ -281,13 +289,18 @@ export class PromoCodeService {
|
||||
},
|
||||
});
|
||||
|
||||
const resource = await this.createQrcodeResource(row.id, code, qrcodeId);
|
||||
const updated = await this.prisma.commonPromoCode.update({
|
||||
where: { id: row.id },
|
||||
data: { qrcodeResourceId: resource.id },
|
||||
include: this.includeRelations,
|
||||
});
|
||||
return this.mapRow(updated);
|
||||
try {
|
||||
const resource = await this.createQrcodeResource(row.id, code);
|
||||
const updated = await this.prisma.commonPromoCode.update({
|
||||
where: { id: row.id },
|
||||
data: { qrcodeResourceId: resource.id },
|
||||
include: this.includeRelations,
|
||||
});
|
||||
return this.mapRow(updated);
|
||||
} catch (err) {
|
||||
await this.prisma.commonPromoCode.delete({ where: { id: row.id } }).catch(() => undefined);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdatePromoCodeDto) {
|
||||
@@ -349,9 +362,13 @@ export class PromoCodeService {
|
||||
};
|
||||
}
|
||||
|
||||
async findByCodeOrQrcodeId(input: { code?: string; qrcodeId?: string }) {
|
||||
async findByCodeOrQrcodeId(input: { code?: string; qrcodeId?: string; promoId?: string }) {
|
||||
const code = input.code?.trim().toUpperCase();
|
||||
const qrcodeId = input.qrcodeId?.trim();
|
||||
const promoId = input.promoId?.trim();
|
||||
if (promoId && /^\d+$/.test(promoId)) {
|
||||
return this.prisma.commonPromoCode.findUnique({ where: { id: BigInt(promoId) } });
|
||||
}
|
||||
if (code) {
|
||||
return this.prisma.commonPromoCode.findUnique({ where: { code } });
|
||||
}
|
||||
@@ -362,14 +379,18 @@ export class PromoCodeService {
|
||||
}
|
||||
|
||||
/** C 端扫码/带参进入:累加 scan_count、归因、标记用户来源 */
|
||||
async touch(input: { promoCode?: string; qrcodeId?: string }, userId?: bigint) {
|
||||
async touch(
|
||||
input: { promoCode?: string; qrcodeId?: string; promoId?: string },
|
||||
userId?: bigint,
|
||||
) {
|
||||
const promoCode = input.promoCode?.trim().toUpperCase();
|
||||
const qrcodeId = input.qrcodeId?.trim();
|
||||
if (!promoCode && !qrcodeId) {
|
||||
throw new BadRequestException('请提供 promoCode 或 qrcodeId');
|
||||
const promoId = input.promoId?.trim();
|
||||
if (!promoCode && !qrcodeId && !promoId) {
|
||||
throw new BadRequestException('请提供 promoId、promoCode 或 qrcodeId');
|
||||
}
|
||||
|
||||
const promo = await this.findByCodeOrQrcodeId({ code: promoCode, qrcodeId });
|
||||
const promo = await this.findByCodeOrQrcodeId({ code: promoCode, qrcodeId, promoId });
|
||||
if (!promo || promo.status !== 'ACTIVE') {
|
||||
throw new NotFoundException('推广码无效或已停用');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user