用户端和服务端增加地址功能(三个地址共存)

This commit is contained in:
2026-07-01 00:52:44 +08:00
parent eb85e0926f
commit d3dd3a0ad1
15 changed files with 282 additions and 6 deletions
@@ -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 };
}
}
}