用户端和服务端增加地址功能(三个地址共存)
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
export type ClientGpsLocation = {
|
||||||
|
province?: string;
|
||||||
|
city?: string;
|
||||||
|
district?: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WxLocationResult = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
wx?: {
|
||||||
|
getLocation?: (options: {
|
||||||
|
type?: string;
|
||||||
|
success?: (res: WxLocationResult) => void;
|
||||||
|
fail?: () => void;
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 尝试获取客户端 GPS(微信优先,其次 H5 Geolocation),失败返回 null 不阻塞下单 */
|
||||||
|
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
||||||
|
if (typeof window !== 'undefined' && window.wx?.getLocation) {
|
||||||
|
const wxResult = await new Promise<WxLocationResult | null>((resolve) => {
|
||||||
|
window.wx!.getLocation!({
|
||||||
|
type: 'gcj02',
|
||||||
|
success: (res) => resolve(res),
|
||||||
|
fail: () => resolve(null),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (wxResult) {
|
||||||
|
return {
|
||||||
|
latitude: wxResult.latitude,
|
||||||
|
longitude: wxResult.longitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
resolve({
|
||||||
|
latitude: pos.coords.latitude,
|
||||||
|
longitude: pos.coords.longitude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => resolve(null),
|
||||||
|
{ enableHighAccuracy: false, timeout: 5000, maximumAge: 60_000 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import AppImage from '@dukang/shared-ui/AppImage';
|
|||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { buildProductDetailUrl } from '../lib/navigation';
|
import { buildProductDetailUrl } from '../lib/navigation';
|
||||||
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
||||||
import { getProductMainImage } from '../lib/product-images';
|
import { tryGetClientGpsLocation } from '../lib/client-location';
|
||||||
|
|
||||||
type Address = {
|
type Address = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -119,9 +119,15 @@ export default function OrderConfirmPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
|
const clientLocation = await tryGetClientGpsLocation();
|
||||||
const order = await request<{ id: string }>('USER_H5', '/trade/orders', {
|
const order = await request<{ id: string }>('USER_H5', '/trade/orders', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ productId, quantity, addressId }),
|
body: JSON.stringify({
|
||||||
|
productId,
|
||||||
|
quantity,
|
||||||
|
addressId,
|
||||||
|
...(clientLocation ? { clientLocation } : {}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set('orderId', order.id);
|
qs.set('orderId', order.id);
|
||||||
|
|||||||
Generated
+12
@@ -181,6 +181,9 @@ importers:
|
|||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.4.1
|
specifier: ^5.4.1
|
||||||
version: 5.11.1
|
version: 5.11.1
|
||||||
|
ip2region:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0(@types/node@20.19.43)
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -1620,6 +1623,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==}
|
resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==}
|
||||||
engines: {node: '>=12.22.0'}
|
engines: {node: '>=12.22.0'}
|
||||||
|
|
||||||
|
ip2region@2.3.0:
|
||||||
|
resolution: {integrity: sha512-zV5Xsadzrx9Ej6heoyhbXMsfGWWQ3C6bAIYStrHhw9kzLpGpVNlnAyRBxxPgxA1GNqr1Ti7oUxcWsMWNN3jZBg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '*'
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -4137,6 +4145,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
ip2region@2.3.0(@types/node@20.19.43):
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.43
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ MOCK_SMS_CODE=123456
|
|||||||
MOCK_PAY=true
|
MOCK_PAY=true
|
||||||
MOCK_DELIVERY_AUTO=true
|
MOCK_DELIVERY_AUTO=true
|
||||||
AUTO_APPROVE_STORE=true
|
AUTO_APPROVE_STORE=true
|
||||||
|
|
||||||
|
# 反向代理后提取真实客户端 IP(下单 IP 定位)
|
||||||
|
# TRUST_PROXY=true
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"ip2region": "^2.3.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -574,6 +574,16 @@ model Order {
|
|||||||
receiverProvince String @map("receiver_province") @db.VarChar(32)
|
receiverProvince String @map("receiver_province") @db.VarChar(32)
|
||||||
receiverCity String @map("receiver_city") @db.VarChar(32)
|
receiverCity String @map("receiver_city") @db.VarChar(32)
|
||||||
receiverDistrict String @map("receiver_district") @db.VarChar(32)
|
receiverDistrict String @map("receiver_district") @db.VarChar(32)
|
||||||
|
clientIp String? @map("client_ip") @db.VarChar(45)
|
||||||
|
ipProvince String? @map("ip_province") @db.VarChar(32)
|
||||||
|
ipCity String? @map("ip_city") @db.VarChar(32)
|
||||||
|
ipDistrict String? @map("ip_district") @db.VarChar(32)
|
||||||
|
gpsProvince String? @map("gps_province") @db.VarChar(32)
|
||||||
|
gpsCity String? @map("gps_city") @db.VarChar(32)
|
||||||
|
gpsDistrict String? @map("gps_district") @db.VarChar(32)
|
||||||
|
gpsLatitude Decimal? @map("gps_latitude") @db.Decimal(10, 7)
|
||||||
|
gpsLongitude Decimal? @map("gps_longitude") @db.Decimal(10, 7)
|
||||||
|
gpsAddress String? @map("gps_address") @db.VarChar(256)
|
||||||
productAmount Decimal @map("product_amount") @db.Decimal(10, 2)
|
productAmount Decimal @map("product_amount") @db.Decimal(10, 2)
|
||||||
freightAmount Decimal @default(0) @map("freight_amount") @db.Decimal(10, 2)
|
freightAmount Decimal @default(0) @map("freight_amount") @db.Decimal(10, 2)
|
||||||
freightPayType String? @map("freight_pay_type") @db.VarChar(8)
|
freightPayType String? @map("freight_pay_type") @db.VarChar(8)
|
||||||
@@ -607,6 +617,8 @@ model Order {
|
|||||||
@@index([cityId, createdAt])
|
@@index([cityId, createdAt])
|
||||||
@@index([receiverPhone])
|
@@index([receiverPhone])
|
||||||
@@index([originOrderId])
|
@@index([originOrderId])
|
||||||
|
@@index([ipCity])
|
||||||
|
@@index([gpsCity])
|
||||||
@@map("orders")
|
@@map("orders")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { PrismaModule } from './common/prisma/prisma.module';
|
import { PrismaModule } from './common/prisma/prisma.module';
|
||||||
|
import { GeoModule } from './common/geo/geo.module';
|
||||||
import { RedisModule } from './common/redis/redis.module';
|
import { RedisModule } from './common/redis/redis.module';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { IamModule } from './modules/iam/iam.module';
|
import { IamModule } from './modules/iam/iam.module';
|
||||||
@@ -23,6 +24,7 @@ import { JobsModule } from './jobs/jobs.module';
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
GeoModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
IamModule,
|
IamModule,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
app.setGlobalPrefix('api/v1');
|
app.setGlobalPrefix('api/v1');
|
||||||
|
app.set('trust proxy', true);
|
||||||
app.enableCors({ origin: true, credentials: true });
|
app.enableCors({ origin: true, credentials: true });
|
||||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, Put, Query, Req, UseGuards } from '@nestjs/common';
|
||||||
|
import type { Request } from 'express';
|
||||||
import { TradeService } from './trade.service';
|
import { TradeService } from './trade.service';
|
||||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
@@ -14,8 +15,8 @@ export class TradeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>) {
|
create(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>, @Req() req: Request) {
|
||||||
return this.tradeService.createOrder(user.actorId, body as never);
|
return this.tradeService.createOrder(user.actorId, body as never, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import { BenefitService } from '../benefit/benefit.service';
|
|||||||
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
|
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
|
||||||
import { IPayProvider } from '../../integrations/pay/pay.interface';
|
import { IPayProvider } from '../../integrations/pay/pay.interface';
|
||||||
import { IDeliveryProvider } from '../../integrations/delivery/delivery.interface';
|
import { IDeliveryProvider } from '../../integrations/delivery/delivery.interface';
|
||||||
|
import { IpGeoService } from '../../common/geo/ip-geo.service';
|
||||||
|
import { buildOrderClientLocationSnapshot } from '../../common/geo/client-location.util';
|
||||||
|
import { extractClientIp } from '../../common/geo/client-ip.util';
|
||||||
|
import type { Request } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TradeService {
|
export class TradeService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly benefitService: BenefitService,
|
private readonly benefitService: BenefitService,
|
||||||
|
private readonly ipGeoService: IpGeoService,
|
||||||
@Inject(PAY_PROVIDER) private readonly payProvider: IPayProvider,
|
@Inject(PAY_PROVIDER) private readonly payProvider: IPayProvider,
|
||||||
@Inject(DELIVERY_PROVIDER) private readonly deliveryProvider: IDeliveryProvider,
|
@Inject(DELIVERY_PROVIDER) private readonly deliveryProvider: IDeliveryProvider,
|
||||||
) {}
|
) {}
|
||||||
@@ -80,7 +85,9 @@ export class TradeService {
|
|||||||
productId: string;
|
productId: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
addressId: string;
|
addressId: string;
|
||||||
|
clientLocation?: unknown;
|
||||||
},
|
},
|
||||||
|
req: Request,
|
||||||
) {
|
) {
|
||||||
const preview = await this.preview(userId, body);
|
const preview = await this.preview(userId, body);
|
||||||
const address = await this.prisma.userAddress.findFirst({
|
const address = await this.prisma.userAddress.findFirst({
|
||||||
@@ -95,6 +102,12 @@ export class TradeService {
|
|||||||
const orderNo = generateOrderNo();
|
const orderNo = generateOrderNo();
|
||||||
const payExpireAt = new Date(Date.now() + 30 * 60 * 1000);
|
const payExpireAt = new Date(Date.now() + 30 * 60 * 1000);
|
||||||
|
|
||||||
|
const location = buildOrderClientLocationSnapshot(
|
||||||
|
req,
|
||||||
|
this.ipGeoService.resolve(extractClientIp(req)),
|
||||||
|
body.clientLocation,
|
||||||
|
);
|
||||||
|
|
||||||
const order = await this.prisma.order.create({
|
const order = await this.prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
orderNo,
|
orderNo,
|
||||||
@@ -108,6 +121,16 @@ export class TradeService {
|
|||||||
receiverProvince: address.province,
|
receiverProvince: address.province,
|
||||||
receiverCity: address.city,
|
receiverCity: address.city,
|
||||||
receiverDistrict: address.district,
|
receiverDistrict: address.district,
|
||||||
|
clientIp: location.clientIp,
|
||||||
|
ipProvince: location.ipProvince,
|
||||||
|
ipCity: location.ipCity,
|
||||||
|
ipDistrict: location.ipDistrict,
|
||||||
|
gpsProvince: location.gpsProvince,
|
||||||
|
gpsCity: location.gpsCity,
|
||||||
|
gpsDistrict: location.gpsDistrict,
|
||||||
|
gpsLatitude: location.gpsLatitude,
|
||||||
|
gpsLongitude: location.gpsLongitude,
|
||||||
|
gpsAddress: location.gpsAddress,
|
||||||
productAmount: preview.productAmount,
|
productAmount: preview.productAmount,
|
||||||
freightAmount: preview.freightAmount,
|
freightAmount: preview.freightAmount,
|
||||||
freightPayType: preview.freightPayType,
|
freightPayType: preview.freightPayType,
|
||||||
|
|||||||
Reference in New Issue
Block a user