e4e9eb2169
Root cause is JSSDK entry-URL mismatch after SPA post-OAuth, not camera permission. Hard-navigate on iOS, keep OAuth query in sign URL, skip redundant bind OAuth, and prompt refresh/re-auth on failure. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import { BadRequestException, Body, Controller, Get, Inject, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||
import { IsIn, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
||
import { ClientApp } from '@dukang/shared-types';
|
||
import type { Request } from 'express';
|
||
import { WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||
import { wechatActorRefFromAuth } from '../../integrations/wechat/wechat-log.util';
|
||
import { WechatLocationService } from './wechat-location.service';
|
||
|
||
class PhoneNumberDto {
|
||
@IsString()
|
||
@IsNotEmpty()
|
||
code: string;
|
||
|
||
@IsString()
|
||
@IsIn(['mini', 'h5'])
|
||
@IsOptional()
|
||
platform?: 'mini' | 'h5';
|
||
}
|
||
|
||
class WechatLocationDto {
|
||
@IsNumber()
|
||
@IsOptional()
|
||
latitude?: number;
|
||
|
||
@IsNumber()
|
||
@IsOptional()
|
||
longitude?: number;
|
||
|
||
@IsString()
|
||
@IsIn(['jssdk', 'geolocation'])
|
||
sdk: 'jssdk' | 'geolocation';
|
||
|
||
@IsString()
|
||
@IsIn(['success', 'fail'])
|
||
status: 'success' | 'fail';
|
||
|
||
@IsString()
|
||
@IsOptional()
|
||
errMsg?: string;
|
||
}
|
||
|
||
class WechatChooseImageDto {
|
||
@IsString()
|
||
@IsIn(['success', 'fail'])
|
||
status: 'success' | 'fail';
|
||
|
||
@IsString()
|
||
@IsOptional()
|
||
errMsg?: string;
|
||
|
||
@IsString()
|
||
@IsOptional()
|
||
sourceType?: string;
|
||
|
||
@IsString()
|
||
@IsIn(['jssdk', 'choose', 'read', 'empty'])
|
||
@IsOptional()
|
||
stage?: 'jssdk' | 'choose' | 'read' | 'empty';
|
||
|
||
@IsString()
|
||
@IsOptional()
|
||
pageUrl?: string;
|
||
}
|
||
|
||
@Controller('common/wechat')
|
||
export class WechatController {
|
||
constructor(
|
||
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
|
||
private readonly locationService: WechatLocationService,
|
||
) {}
|
||
|
||
@Get('jssdk-config')
|
||
async jssdkConfig(@Query('url') url: string, @Req() req: Request) {
|
||
if (!url) throw new BadRequestException('url 参数必填');
|
||
const decoded = decodeURIComponent(url).split('#')[0];
|
||
const pageUrl = this.normalizeJssdkUrl(decoded);
|
||
const user = (req as Request & { user?: AuthUser }).user;
|
||
const actorRef =
|
||
user?.actorType === 'USER' ? { refType: 'USER', refId: user.actorId } : undefined;
|
||
return this.wechat.createJssdkConfig(pageUrl, actorRef);
|
||
}
|
||
|
||
/**
|
||
* 仅去 hash。勿剔除 code/state:
|
||
* iOS 微信用「document 入场 URL」验签,OAuth 回跳页的 query 必须原样参与签名。
|
||
*/
|
||
private normalizeJssdkUrl(rawUrl: string): string {
|
||
return rawUrl.split('#')[0];
|
||
}
|
||
|
||
@Get('oauth-url')
|
||
oauthUrl(
|
||
@Query('redirectUri') redirectUri: string,
|
||
@Query('state') state: string,
|
||
@Query('scope') scope?: string,
|
||
) {
|
||
if (!redirectUri || !state) throw new BadRequestException('redirectUri 与 state 必填');
|
||
return { url: this.wechat.buildOAuthUrl(redirectUri, state, scope) };
|
||
}
|
||
|
||
@Post('phone-number')
|
||
phoneNumber(@Body() dto: PhoneNumberDto) {
|
||
return this.wechat
|
||
.getPhoneNumberByCode(dto.code, dto.platform ?? 'mini')
|
||
.then((phone) => ({ phone }));
|
||
}
|
||
|
||
@Post('location')
|
||
@UseGuards(OptionalJwtAuthGuard)
|
||
reportLocation(@Req() req: Request, @Body() dto: WechatLocationDto) {
|
||
const user = (req as Request & { user?: AuthUser }).user;
|
||
const userId = user?.actorType === 'USER' ? user.actorId : undefined;
|
||
const clientApp = (req.headers['x-client-app'] as string) || ClientApp.USER_H5;
|
||
return this.locationService.reportLocation({
|
||
latitude: dto.latitude,
|
||
longitude: dto.longitude,
|
||
sdk: dto.sdk,
|
||
status: dto.status,
|
||
errMsg: dto.errMsg,
|
||
clientApp,
|
||
userId,
|
||
});
|
||
}
|
||
|
||
@Post('choose-image')
|
||
@UseGuards(OptionalJwtAuthGuard)
|
||
reportChooseImage(@Req() req: Request, @Body() dto: WechatChooseImageDto) {
|
||
const user = (req as Request & { user?: AuthUser }).user;
|
||
return this.locationService.reportChooseImage({
|
||
status: dto.status,
|
||
errMsg: dto.errMsg,
|
||
sourceType: dto.sourceType,
|
||
stage: dto.stage,
|
||
pageUrl: dto.pageUrl,
|
||
actorRef: wechatActorRefFromAuth(user?.actorType, user?.actorId),
|
||
});
|
||
}
|
||
}
|