Files
dukang/server/dukang-api/src/modules/common/wechat.controller.ts
T

147 lines
4.2 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);
}
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),
});
}
}