微信小程序审核不通过修改
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-21 09:32:06 +08:00
parent 350a086a73
commit 935ab0d1d4
14 changed files with 466 additions and 242 deletions
@@ -16,6 +16,7 @@ type TokenCache = { accessToken: string; expiresAt: number };
type TicketCache = { ticket: string; expiresAt: number };
const ACCESS_TOKEN_KEY = 'wechat:access_token';
const MINI_ACCESS_TOKEN_KEY = 'wechat:mini_access_token';
const JSAPI_TICKET_KEY = 'wechat:jsapi_ticket';
@Injectable()
@@ -255,7 +256,7 @@ export class WechatApiProvider implements IWechatProvider {
if (platform === 'h5') {
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
}
const accessToken = await this.getAccessToken();
const accessToken = await this.getMiniAccessToken();
const apiUrl = `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${accessToken}`;
const data = await this.fetchJson<{
errcode?: number;
@@ -433,6 +434,37 @@ export class WechatApiProvider implements IWechatProvider {
return data.access_token;
}
/** 小程序 access_tokengetPhoneNumber 等 wxa 接口必须用小程序 AppID) */
private async getMiniAccessToken(): Promise<string> {
const appId = this.miniAppId;
const appSecret = this.miniAppSecret;
if (!appId || !appSecret) {
throw new InternalServerErrorException(
'小程序未配置:请设置 WX_MINI_APP_ID / WX_MINI_APP_SECRET',
);
}
const cached = await this.redis.getJson<TokenCache>(MINI_ACCESS_TOKEN_KEY);
if (cached && cached.expiresAt > Date.now()) return cached.accessToken;
const url = new URL('https://api.weixin.qq.com/cgi-bin/token');
url.searchParams.set('grant_type', 'client_credential');
url.searchParams.set('appid', appId);
url.searchParams.set('secret', appSecret);
const data = await this.fetchJson<{ access_token?: string; expires_in?: number; errcode?: number; errmsg?: string }>(
url.toString(),
);
if (!data.access_token) {
throw new InternalServerErrorException(data.errmsg || '获取小程序 access_token 失败');
}
const ttl = Math.max((data.expires_in ?? 7200) - 300, 60);
await this.redis.setJson(
MINI_ACCESS_TOKEN_KEY,
{ accessToken: data.access_token, expiresAt: Date.now() + ttl * 1000 },
ttl,
);
return data.access_token;
}
private async getJsapiTicket(): Promise<string> {
const cached = await this.redis.getJson<TicketCache>(JSAPI_TICKET_KEY);
if (cached && cached.expiresAt > Date.now()) return cached.ticket;
@@ -10,6 +10,7 @@ import {
CheckPartnerPhoneDto,
LoginSmsDto,
LoginWechatDto,
LoginWechatPhoneDto,
RefreshTokenDto,
SendSmsDto,
} from './dto/auth.dto';
@@ -74,6 +75,23 @@ export class UserAuthController {
return this.authService.loginUserWechat(dto.code, clientApp, platform, guestId);
}
/** 小程序手机号快捷登录(getPhoneNumber */
@Post('auth/login/wechat-phone')
@UseGuards(OptionalJwtAuthGuard)
wechatPhoneLogin(@Req() req: Request, @Body() dto: LoginWechatPhoneDto) {
const guest = (req as Request & { user?: AuthUser }).user;
const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined;
const clientApp = resolveUserClientApp(req);
const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5');
return this.authService.loginUserWechatPhone(
dto.phoneCode,
clientApp,
platform,
guestId,
dto.loginCode,
);
}
@Post('auth/wechat/bind-phone')
bindPhoneLegacy(@Req() req: Request, @Body() dto: BindWechatPhoneDto) {
return this.authService.bindWechatPhone(
@@ -751,19 +751,13 @@ export class AuthService {
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, this.partnerTokenPayload(account, primary));
}
async loginUser(phone: string, code: string, clientApp: ClientApp, guestId?: bigint) {
const normalizedPhone = this.assertMobilePhone(phone);
const existingUser = await this.prisma.user.findUnique({
where: { phone: normalizedPhone },
select: { id: true },
});
await this.verifySmsForUser(
normalizedPhone,
code,
SmsScene.USER_LOGIN,
clientApp,
guestId ?? existingUser?.id,
);
/** 手机号已验证后建号/登录并签发会话(短信登录与微信手机号快捷登录共用) */
private async issueUserSessionByVerifiedPhone(
normalizedPhone: string,
clientApp: ClientApp,
guestId: bigint | undefined,
method: 'sms' | 'wechat_phone',
) {
let user: UserRow | null = await this.prisma.user.findUnique({
where: { phone: normalizedPhone },
include: { avatar: true },
@@ -816,12 +810,12 @@ export class AuthService {
if (guestId && guestId !== user.id) {
user = await this.mergeUsers(guestId, user.id);
this.analyticsService.trackOneSafe(user.id, clientApp, {
eventName: 'sms_login',
extraJson: { method: 'sms', accountMerged: true },
eventName: method === 'sms' ? 'sms_login' : 'wechat_phone_login',
extraJson: { method, accountMerged: true },
});
this.analyticsService.trackOneSafe(user.id, clientApp, {
eventName: 'login_success',
extraJson: { method: 'sms', accountMerged: true },
extraJson: { method, accountMerged: true },
});
return this.buildSessionResponse(user, clientApp, user.deviceKey, { accountMerged: true });
} else {
@@ -832,17 +826,70 @@ export class AuthService {
if (!user) throw new BadRequestException('登录失败');
this.analyticsService.trackOneSafe(user.id, clientApp, {
eventName: 'sms_login',
extraJson: { method: 'sms' },
eventName: method === 'sms' ? 'sms_login' : 'wechat_phone_login',
extraJson: { method },
});
this.analyticsService.trackOneSafe(user.id, clientApp, {
eventName: 'login_success',
extraJson: { method: 'sms' },
extraJson: { method },
});
return this.buildSessionResponse(user, clientApp, user.deviceKey);
}
async loginUser(phone: string, code: string, clientApp: ClientApp, guestId?: bigint) {
const normalizedPhone = this.assertMobilePhone(phone);
const existingUser = await this.prisma.user.findUnique({
where: { phone: normalizedPhone },
select: { id: true },
});
await this.verifySmsForUser(
normalizedPhone,
code,
SmsScene.USER_LOGIN,
clientApp,
guestId ?? existingUser?.id,
);
return this.issueUserSessionByVerifiedPhone(normalizedPhone, clientApp, guestId, 'sms');
}
/** 小程序 getPhoneNumber:用微信返回的 phoneCode 登录/注册,可选 loginCode 绑定 openId */
async loginUserWechatPhone(
phoneCode: string,
clientApp: ClientApp,
platform: 'h5' | 'mini' = 'mini',
guestId?: bigint,
loginCode?: string,
) {
this.assertWechatEnabled();
if (platform !== 'mini') {
throw new BadRequestException('仅小程序支持手机号快捷登录');
}
const phone = await this.wechatProvider.getPhoneNumberByCode(phoneCode, platform);
const normalizedPhone = this.assertMobilePhone(phone);
const session = await this.issueUserSessionByVerifiedPhone(
normalizedPhone,
clientApp,
guestId,
'wechat_phone',
);
if (loginCode?.trim() && session.actorId) {
try {
await this.bindUserWechat(
BigInt(session.actorId),
{ code: loginCode.trim() },
clientApp,
'mini',
);
} catch {
/* 绑定 openId 失败不阻断已成功的手机号登录 */
}
}
return session;
}
async bindPhone(actorId: bigint, phone: string, code: string, clientApp: ClientApp) {
const normalizedPhone = this.assertMobilePhone(phone);
await this.verifySmsForUser(normalizedPhone, code, SmsScene.BIND_PHONE, clientApp, actorId);
@@ -55,6 +55,22 @@ export class LoginWechatDto {
platform?: 'h5' | 'mini';
}
/** 小程序 getPhoneNumber 返回的 phoneCode,可选附带 wx.login code 绑定 openId */
export class LoginWechatPhoneDto {
@IsString()
@IsNotEmpty()
phoneCode: string;
@IsString()
@IsOptional()
loginCode?: string;
@IsString()
@IsIn(['h5', 'mini'])
@IsOptional()
platform?: 'h5' | 'mini';
}
export class BindWechatPhoneDto {
@IsString()
@IsNotEmpty()