定位获取城市功能,需要微信地图的key?
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { IntegrationsModule } from '../../integrations/integrations.module';
|
||||
import { ResourceService } from './resource.service';
|
||||
import { EventService } from './event.service';
|
||||
@@ -11,9 +12,10 @@ import { TicketController } from './ticket.controller';
|
||||
import { ThirdPartyLogController } from './third-party-log.controller';
|
||||
import { WechatController } from './wechat.controller';
|
||||
import { ClientConfigController } from './client-config.controller';
|
||||
import { WechatLocationService } from './wechat-location.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, IntegrationsModule],
|
||||
imports: [IamModule, IntegrationsModule, forwardRef(() => AnalyticsModule)],
|
||||
controllers: [
|
||||
ResourceController,
|
||||
EventController,
|
||||
@@ -22,7 +24,7 @@ import { ClientConfigController } from './client-config.controller';
|
||||
WechatController,
|
||||
ClientConfigController,
|
||||
],
|
||||
providers: [ResourceService, EventService, TicketService, ThirdPartyLogService],
|
||||
providers: [ResourceService, EventService, TicketService, ThirdPartyLogService, WechatLocationService],
|
||||
exports: [ResourceService, EventService, TicketService],
|
||||
})
|
||||
export class CommonModule {}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { TencentLbsProvider } from '../../integrations/map/tencent-lbs.provider';
|
||||
import { logWechatAuth, type WechatActorRef } from '../../integrations/wechat/wechat-log.util';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
|
||||
export type ReportWechatLocationInput = {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
sdk: 'jssdk' | 'geolocation';
|
||||
status: 'success' | 'fail';
|
||||
errMsg?: string;
|
||||
clientApp?: string;
|
||||
userId?: bigint;
|
||||
};
|
||||
|
||||
export type ReportWechatLocationResult = {
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
cityCode?: string;
|
||||
cityName?: string;
|
||||
openCity: boolean;
|
||||
thirdPartyLogIds: {
|
||||
location?: string;
|
||||
geocode?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function normalizeCityName(name: string) {
|
||||
return name.replace(/市$/, '').trim();
|
||||
}
|
||||
|
||||
function matchOpenCity(
|
||||
cities: Array<{ code: string; name: string; province: string }>,
|
||||
province: string,
|
||||
city: string,
|
||||
) {
|
||||
const cityNorm = normalizeCityName(city);
|
||||
return cities.find((c) => {
|
||||
const nameNorm = normalizeCityName(c.name);
|
||||
if (nameNorm !== cityNorm && c.name !== city && c.name !== `${cityNorm}市`) return false;
|
||||
if (c.province && province && c.province !== province) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WechatLocationService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly tencentLbs: TencentLbsProvider,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
async reportLocation(input: ReportWechatLocationInput): Promise<ReportWechatLocationResult> {
|
||||
const actorRef: WechatActorRef | undefined = input.userId
|
||||
? { refType: 'USER', refId: input.userId }
|
||||
: undefined;
|
||||
const clientApp = (input.clientApp as ClientApp) || ClientApp.USER_H5;
|
||||
const thirdPartyLogIds: ReportWechatLocationResult['thirdPartyLogIds'] = {};
|
||||
|
||||
const locationLogId = await logWechatAuth(this.prisma, {
|
||||
scene: 'GET_LOCATION',
|
||||
requestBody: {
|
||||
sdk: input.sdk,
|
||||
status: input.status,
|
||||
...(input.latitude != null && input.longitude != null
|
||||
? {
|
||||
latitude: Number(input.latitude.toFixed(3)),
|
||||
longitude: Number(input.longitude.toFixed(3)),
|
||||
}
|
||||
: {}),
|
||||
...(input.errMsg ? { errMsg: input.errMsg.slice(0, 200) } : {}),
|
||||
},
|
||||
responseBody: { reported: true },
|
||||
status: input.status === 'success' ? 'SUCCESS' : 'FAILED',
|
||||
errorMessage: input.status === 'fail' ? input.errMsg?.slice(0, 512) : undefined,
|
||||
actorRef,
|
||||
});
|
||||
thirdPartyLogIds.location = locationLogId.toString();
|
||||
|
||||
if (input.status !== 'success' || input.latitude == null || input.longitude == null) {
|
||||
return { openCity: false, thirdPartyLogIds };
|
||||
}
|
||||
|
||||
const geo = await this.tencentLbs.reverseGeocode(input.latitude, input.longitude, actorRef);
|
||||
if (geo) {
|
||||
thirdPartyLogIds.geocode = geo.logId.toString();
|
||||
}
|
||||
if (!geo) {
|
||||
return { openCity: false, thirdPartyLogIds };
|
||||
}
|
||||
|
||||
const openCities = await this.prisma.commonCity.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
select: { code: true, name: true, province: true },
|
||||
});
|
||||
const matched = matchOpenCity(openCities, geo.province, geo.city);
|
||||
|
||||
const result: ReportWechatLocationResult = {
|
||||
province: geo.province,
|
||||
city: geo.city,
|
||||
district: geo.district,
|
||||
cityCode: matched?.code,
|
||||
cityName: matched?.name ?? `${geo.city}市`,
|
||||
openCity: !!matched,
|
||||
thirdPartyLogIds,
|
||||
};
|
||||
|
||||
if (input.userId) {
|
||||
const mapLogId = geo.logId;
|
||||
this.analyticsService.trackOneSafe(input.userId, clientApp, {
|
||||
eventName: 'wechat_location',
|
||||
refType: 'THIRD_PARTY_LOG',
|
||||
refId: mapLogId,
|
||||
extraJson: {
|
||||
sdk: input.sdk,
|
||||
province: geo.province,
|
||||
city: geo.city,
|
||||
district: geo.district,
|
||||
openCity: !!matched,
|
||||
cityCode: matched?.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { BadRequestException, Body, Controller, Get, Inject, Post, Query } from '@nestjs/common';
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
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 { WechatLocationService } from './wechat-location.service';
|
||||
|
||||
class PhoneNumberDto {
|
||||
@IsString()
|
||||
@@ -14,15 +19,43 @@ class PhoneNumberDto {
|
||||
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;
|
||||
}
|
||||
|
||||
@Controller('common/wechat')
|
||||
export class WechatController {
|
||||
constructor(@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider) {}
|
||||
constructor(
|
||||
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
|
||||
private readonly locationService: WechatLocationService,
|
||||
) {}
|
||||
|
||||
@Get('jssdk-config')
|
||||
async jssdkConfig(@Query('url') url: string) {
|
||||
async jssdkConfig(@Query('url') url: string, @Req() req: Request) {
|
||||
if (!url) throw new BadRequestException('url 参数必填');
|
||||
const pageUrl = decodeURIComponent(url).split('#')[0];
|
||||
return this.wechat.createJssdkConfig(pageUrl);
|
||||
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);
|
||||
}
|
||||
|
||||
@Get('oauth-url')
|
||||
@@ -41,4 +74,21 @@ export class WechatController {
|
||||
.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user