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 { 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; } async reportChooseImage(input: { status: 'success' | 'fail'; errMsg?: string; sourceType?: string; stage?: string; pageUrl?: string; actorRef?: WechatActorRef; }) { const logId = await logWechatAuth(this.prisma, { scene: 'CHOOSE_IMAGE', requestUrl: input.pageUrl?.split('#')[0]?.slice(0, 512), requestBody: { status: input.status, ...(input.sourceType ? { sourceType: input.sourceType } : {}), ...(input.stage ? { stage: input.stage } : {}), ...(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: input.actorRef, }); return { ok: true, logId: logId.toString() }; } }