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); } private normalizeJssdkUrl(rawUrl: string): string { try { const parsed = new URL(rawUrl); parsed.hash = ''; parsed.searchParams.delete('code'); parsed.searchParams.delete('state'); const query = parsed.searchParams.toString(); return `${parsed.origin}${parsed.pathname}${query ? `?${query}` : ''}`; } catch { return rawUrl; } } @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), }); } }