用户端和服务端增加地址功能(三个地址共存)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import type { Request } from 'express';
|
||||
|
||||
function normalizeIp(raw?: string | null): string | null {
|
||||
if (!raw) return null;
|
||||
const ip = raw.replace(/^::ffff:/, '').trim();
|
||||
if (!ip || ip === '::1') return null;
|
||||
return ip;
|
||||
}
|
||||
|
||||
/** 从反向代理 / Socket 提取客户端 IP */
|
||||
export function extractClientIp(req: Request): string | null {
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
if (typeof forwarded === 'string' && forwarded.length > 0) {
|
||||
return normalizeIp(forwarded.split(',')[0]);
|
||||
}
|
||||
if (Array.isArray(forwarded) && forwarded.length > 0) {
|
||||
return normalizeIp(forwarded[0]?.split(',')[0]);
|
||||
}
|
||||
const realIp = req.headers['x-real-ip'];
|
||||
if (typeof realIp === 'string' && realIp.length > 0) {
|
||||
return normalizeIp(realIp);
|
||||
}
|
||||
return normalizeIp(req.ip ?? req.socket?.remoteAddress ?? null);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ClientGpsLocation, OrderClientLocationSnapshot } from './geo.types';
|
||||
import type { IpRegion } from './geo.types';
|
||||
import { extractClientIp } from './client-ip.util';
|
||||
import type { Request } from 'express';
|
||||
|
||||
function parseGpsLocation(raw: unknown): ClientGpsLocation | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const input = raw as Record<string, unknown>;
|
||||
const latitude = Number(input.latitude);
|
||||
const longitude = Number(input.longitude);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
|
||||
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) return null;
|
||||
|
||||
return {
|
||||
province: input.province != null ? String(input.province).trim() || null : null,
|
||||
city: input.city != null ? String(input.city).trim() || null : null,
|
||||
district: input.district != null ? String(input.district).trim() || null : null,
|
||||
latitude,
|
||||
longitude,
|
||||
address: input.address != null ? String(input.address).trim() || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOrderClientLocationSnapshot(
|
||||
req: Request,
|
||||
ipGeo: IpRegion,
|
||||
clientLocationRaw?: unknown,
|
||||
): OrderClientLocationSnapshot {
|
||||
const clientIp = extractClientIp(req);
|
||||
const gps = parseGpsLocation(clientLocationRaw);
|
||||
|
||||
return {
|
||||
clientIp,
|
||||
ipProvince: ipGeo.province,
|
||||
ipCity: ipGeo.city,
|
||||
ipDistrict: ipGeo.district,
|
||||
gpsProvince: gps?.province ?? null,
|
||||
gpsCity: gps?.city ?? null,
|
||||
gpsDistrict: gps?.district ?? null,
|
||||
gpsLatitude: gps?.latitude ?? null,
|
||||
gpsLongitude: gps?.longitude ?? null,
|
||||
gpsAddress: gps?.address ?? null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { IpGeoService } from './ip-geo.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [IpGeoService],
|
||||
exports: [IpGeoService],
|
||||
})
|
||||
export class GeoModule {}
|
||||
@@ -0,0 +1,27 @@
|
||||
export type IpRegion = {
|
||||
province: string | null;
|
||||
city: string | null;
|
||||
district: string | null;
|
||||
};
|
||||
|
||||
export type ClientGpsLocation = {
|
||||
province?: string | null;
|
||||
city?: string | null;
|
||||
district?: string | null;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address?: string | null;
|
||||
};
|
||||
|
||||
export type OrderClientLocationSnapshot = {
|
||||
clientIp: string | null;
|
||||
ipProvince: string | null;
|
||||
ipCity: string | null;
|
||||
ipDistrict: string | null;
|
||||
gpsProvince: string | null;
|
||||
gpsCity: string | null;
|
||||
gpsDistrict: string | null;
|
||||
gpsLatitude: number | null;
|
||||
gpsLongitude: number | null;
|
||||
gpsAddress: string | null;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import IP2Region from 'ip2region';
|
||||
import type { IpRegion } from './geo.types';
|
||||
|
||||
function cleanRegionName(value?: string | null): string | null {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed === '0' || trimmed === '内网IP') return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IpGeoService implements OnModuleInit {
|
||||
private readonly logger = new Logger(IpGeoService.name);
|
||||
private query: IP2Region | null = null;
|
||||
|
||||
onModuleInit() {
|
||||
try {
|
||||
this.query = new IP2Region();
|
||||
} catch (error) {
|
||||
this.logger.warn(`ip2region 初始化失败: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(ip: string | null): IpRegion {
|
||||
if (!ip || !this.query) {
|
||||
return { province: null, city: null, district: null };
|
||||
}
|
||||
if (ip === '127.0.0.1' || ip.startsWith('192.168.') || ip.startsWith('10.')) {
|
||||
return { province: null, city: null, district: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.query.search(ip) as {
|
||||
province?: string;
|
||||
city?: string;
|
||||
region?: string;
|
||||
};
|
||||
return {
|
||||
province: cleanRegionName(result?.province),
|
||||
city: cleanRegionName(result?.city),
|
||||
district: cleanRegionName(result?.region),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.debug(`IP 解析失败 ${ip}: ${error instanceof Error ? error.message : error}`);
|
||||
return { province: null, city: null, district: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user