Files
dukang/server/dukang-api/src/modules/common/wechat-location.service.ts
T
2026-08-04 21:38:49 +08:00

157 lines
4.9 KiB
TypeScript

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;
}
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() };
}
}