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:
@@ -3,7 +3,12 @@ import { BadRequestException, Injectable, InternalServerErrorException, Logger }
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import type { IWechatProvider, WechatCodeSession, WechatOAuthSession } from './wechat.interface';
|
||||
import type {
|
||||
IWechatProvider,
|
||||
WechatCodeSession,
|
||||
WechatOAuthSession,
|
||||
WechatWxaCodeUnlimitedInput,
|
||||
} from './wechat.interface';
|
||||
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
|
||||
import {
|
||||
decryptPayResource,
|
||||
@@ -252,6 +257,52 @@ export class WechatApiProvider implements IWechatProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||
const scene = (input.scene ?? '').trim();
|
||||
if (!scene || scene.length > 32) {
|
||||
throw new BadRequestException('小程序码 scene 须为 1~32 个可见字符');
|
||||
}
|
||||
const accessToken = await this.getMiniAccessToken();
|
||||
const page = (input.page ?? process.env.WX_MINI_PROMO_PAGE ?? 'pages/home/index').replace(
|
||||
/^\//,
|
||||
'',
|
||||
);
|
||||
const envFromCfg = process.env.WX_MINI_ENV_VERSION;
|
||||
const envVersion: 'release' | 'trial' | 'develop' =
|
||||
input.envVersion ??
|
||||
(envFromCfg === 'trial' || envFromCfg === 'develop' || envFromCfg === 'release'
|
||||
? envFromCfg
|
||||
: 'release');
|
||||
const body = {
|
||||
scene,
|
||||
page,
|
||||
width: input.width ?? 430,
|
||||
check_path: input.checkPath ?? false,
|
||||
env_version: envVersion,
|
||||
is_hyaline: input.isHyaline ?? false,
|
||||
};
|
||||
const apiUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`;
|
||||
const res = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
// 失败时微信返回 JSON(以 { 开头),成功为 PNG 二进制
|
||||
if (buf.length >= 1 && buf[0] === 0x7b /* '{' */) {
|
||||
let errMsg = '生成小程序码失败';
|
||||
try {
|
||||
const err = JSON.parse(buf.toString('utf8')) as { errcode?: number; errmsg?: string };
|
||||
errMsg = err.errmsg || errMsg;
|
||||
this.logger.error(`getwxacodeunlimit failed: ${err.errcode} ${err.errmsg}`);
|
||||
} catch {
|
||||
this.logger.error(`getwxacodeunlimit non-image response: ${buf.toString('utf8').slice(0, 200)}`);
|
||||
}
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
async getPhoneNumberByCode(code: string, platform: 'mini' | 'h5', actorRef?: WechatActorRef): Promise<string> {
|
||||
if (platform === 'h5') {
|
||||
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, NotImplementedException } from '@nestjs/common';
|
||||
import type { IWechatProvider } from './wechat.interface';
|
||||
import type { IWechatProvider, WechatWxaCodeUnlimitedInput } from './wechat.interface';
|
||||
|
||||
@Injectable()
|
||||
export class WechatDisabledProvider implements IWechatProvider {
|
||||
@@ -54,4 +54,8 @@ export class WechatDisabledProvider implements IWechatProvider {
|
||||
parsePayNotification() {
|
||||
return this.disabled();
|
||||
}
|
||||
|
||||
getWxaCodeUnlimited(_input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||
return this.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,17 @@ export type WechatPayNotifyResult = {
|
||||
amountFen: number;
|
||||
};
|
||||
|
||||
export type WechatWxaCodeUnlimitedInput = {
|
||||
/** 最大 32 可见字符,扫码后小程序 onLaunch.options.scene */
|
||||
scene: string;
|
||||
/** 小程序页面路径,如 pages/home/index(不要前导 /) */
|
||||
page?: string;
|
||||
width?: number;
|
||||
checkPath?: boolean;
|
||||
envVersion?: 'release' | 'trial' | 'develop';
|
||||
isHyaline?: boolean;
|
||||
};
|
||||
|
||||
export interface IWechatProvider {
|
||||
isEnabled(): boolean;
|
||||
|
||||
@@ -81,4 +92,7 @@ export interface IWechatProvider {
|
||||
headers: Record<string, string | string[] | undefined>,
|
||||
rawBody: string,
|
||||
): Promise<WechatPayNotifyResult>;
|
||||
|
||||
/** 获取不限制的小程序码(PNG Buffer),须服务端调用 */
|
||||
getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { Injectable, NotImplementedException } from '@nestjs/common';
|
||||
import * as QRCode from 'qrcode';
|
||||
import type {
|
||||
IWechatProvider,
|
||||
WechatCodeSession,
|
||||
WechatOAuthSession,
|
||||
WechatWxaCodeUnlimitedInput,
|
||||
} from './wechat.interface';
|
||||
|
||||
/**
|
||||
@@ -81,4 +83,15 @@ export class WechatMockProvider implements IWechatProvider {
|
||||
parsePayNotification(): never {
|
||||
throw new NotImplementedException('FEATURE_DISABLED');
|
||||
}
|
||||
|
||||
/** Mock:用普通二维码 PNG 占位,内容含 scene,便于本地联调上传 OSS */
|
||||
async getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||
const scene = (input.scene ?? '').trim() || 'mock';
|
||||
return QRCode.toBuffer(`mock-wxa://promo?scene=${encodeURIComponent(scene)}`, {
|
||||
width: input.width ?? 430,
|
||||
margin: 1,
|
||||
type: 'png',
|
||||
color: { dark: '#1f1a17', light: '#ffffff' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,8 @@ export class WechatRouterProvider implements IWechatProvider {
|
||||
) {
|
||||
return this.resolve().parsePayNotification(headers, rawBody);
|
||||
}
|
||||
|
||||
getWxaCodeUnlimited(input: Parameters<IWechatProvider['getWxaCodeUnlimited']>[0]) {
|
||||
return this.resolve().getWxaCodeUnlimited(input);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user