Appearance
电商多仓就近发货怎么做?Java 按 IP 归属地自动选仓
做电商的都知道这个矛盾:仓库建得越多,物流越快,但成本也越高。几个仓放哪、订单走哪个仓,直接决定「今天下单明天到」还是「路上飘三天」。手动分仓不现实——几千单里买家遍布全国,靠人工判断早爆仓了。自动选仓的核心输入就一个:买家在哪个城市。城市从哪来?下单选填的收货地址当然准,但没填对、或者想提前预估运费时效的时候,买家 IP 归属地就是最省事的数据源。这篇用 Java 写一套完整的「IP 归属地 → 就近仓库」选仓逻辑。
一、多仓路由为什么难
多仓发货的常规做法是「按省分区」:华东仓管华东的订单。但问题在于:
- 买家在边界城市:比如廊坊的订单,离华北仓和华东仓都很近,按行政区域切容易切错;
- 仓库在挪:新仓上线、旧仓搬迁,省区映射表要跟着改,维护成本高;
- 缺兜底:收货地址异常、解析失败的单,总得有个默认仓接着。
用 IP 归属地 + 经纬度距离来算,能把这些问题一起解决:不用维护省区表,拿仓库和买家的经纬度直接算直线距离,谁近发谁。IP 归属地的经纬度是城市中心点,对选仓来说精度完全够用——你不需要精确到楼,需要的是「这个买家离哪个仓最近」。
二、方案设计
- 数据准备:仓库表(仓库名 + 经纬度),维护在配置里或数据库;
- 拿买家 IP:服务端从请求里取真实 IP(注意取
X-Forwarded-For不能直接信,要校验格式); - 查归属地:调
https://ip9.com.cn/get?ip=xxx拿省份、城市、经纬度; - 算距离:Haversine 公式算买家城市中心和每个仓库的球面距离;
- 选仓:距离最近的仓库为发货仓;归属地查询失败就回退默认仓。
加上缓存:同城市买家很多,按「城市」缓存归属地(而不是按 IP 缓存),命中率更高,免费接口 60 次/分钟/IP 的额度完全够日常订单量。
三、Java 实现
java
// WarehouseRouter.java —— 按买家 IP 归属地选择最近仓库
// 运行:java WarehouseRouter.java(Java 11+,单文件,无外部依赖)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class WarehouseRouter {
// 仓库列表:名称 + 经度 + 纬度(城市中心级即可)
static final List<Warehouse> WAREHOUSES = List.of(
new Warehouse("华北仓(北京)", 116.40, 39.90),
new Warehouse("华东仓(上海)", 121.47, 31.23),
new Warehouse("华南仓(广州)", 113.26, 23.13),
new Warehouse("西南仓(成都)", 104.07, 30.67),
new Warehouse("华中仓(武汉)", 114.31, 30.59)
);
static final HttpClient HTTP = HttpClient.newHttpClient();
// 城市 -> 归属地 缓存(选仓场景按城市缓存命中率最高)
static final Map<String, GeoInfo> CITY_CACHE = new ConcurrentHashMap<>();
static class GeoInfo { String prov, city; double lng, lat; }
static class Warehouse { String name; double lng, lat;
Warehouse(String n, double lng, double lat) { this.name = n; this.lng = lng; this.lat = lat; } }
/** 调 ip9.com.cn/get 查询 IP 归属地,免费版 60 次/分钟/IP */
static GeoInfo geoByIP(String ip) throws Exception {
String url = "https://ip9.com.cn/get?ip=" + ip;
HttpRequest req = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
// 返回 {"ret":200,"data":{...,"prov":"江苏","city":"南京","lng":"118.77","lat":"32.04",...}}
String body = resp.body();
GeoInfo g = new GeoInfo();
g.prov = strOf(body, "prov");
g.city = strOf(body, "city");
g.lng = Double.parseDouble(strOf(body, "lng")); // 城市中心经度
g.lat = Double.parseDouble(strOf(body, "lat")); // 城市中心纬度
return g;
}
/** 从 JSON 字符串里抠字段值(样例数据,生产建议用 Jackson/Gson) */
static String strOf(String json, String key) {
int i = json.indexOf("\"" + key + "\"");
if (i < 0) return "";
i = json.indexOf('"', i + key.length() + 2); // 跳到值起始引号
int j = json.indexOf('"', i + 1);
return json.substring(i + 1, j);
}
/** Haversine 球面距离,返回公里数 */
static double distanceKm(double lat1, double lng1, double lat2, double lng2) {
double R = 6371.0;
double dLat = Math.toRadians(lat2 - lat1);
double dLng = Math.toRadians(lng2 - lng1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
return 2 * R * Math.asin(Math.sqrt(a));
}
/** 核心:给定买家 IP,返回最近仓库 */
static Warehouse nearestWarehouse(String buyerIP) throws Exception {
GeoInfo g = CITY_CACHE.get(buyerIP); // 简化:演示用 IP 做 key,生产按城市
if (g == null) {
g = geoByIP(buyerIP);
CITY_CACHE.put(buyerIP, g);
}
System.out.println("买家归属地: " + g.prov + g.city
+ " (" + g.lat + "," + g.lng + ")");
Warehouse best = null;
double bestDist = Double.MAX_VALUE;
for (Warehouse w : WAREHOUSES) {
double d = distanceKm(g.lat, g.lng, w.lat, w.lng);
System.out.printf(" -> %s 距离 %.0f km%n", w.name, d);
if (d < bestDist) { bestDist = d; best = w; }
}
return best;
}
public static void main(String[] args) throws Exception {
// 示例:查两个不同地区的买家
for (String ip : new String[]{"114.114.114.114", "183.14.28.11"}) {
Warehouse w = nearestWarehouse(ip);
System.out.println("选仓结果: " + w.name + "\n");
}
}
}跑起来的思路:程序会把买家的归属地和每个仓库的距离都打出来。江苏南京的 IP 距离上海仓最近,选华东仓;广东的 IP 选华南仓。归属地查询那段做了字段解析,生产环境建议直接换 Jackson,字段更多更稳(免费版还返回城市编码、邮编、大区、运营商等)。
四、落地注意事项
- IP 取法要小心:服务器前面挂了 CDN/负载均衡,
X-Forwarded-For由客户端可控——先取最后一个可信代理追加的 IP,别直接把整段 XFF 当买家 IP,否则会被伪造。 - 经纬度精度认知:IP 归属地给的是城市中心坐标,不是买家精确位置。选仓要的是「城市级」判断,城市中心距离误差几十公里不影响结论;但如果你拿它去算「小时级送达范围」就会翻车,那是另一套数据。
- 缓存策略:同一城市买家共享归属地,按
城市编码或省+市缓存,而不是按 IP 缓存,免费额度能用得非常省。缓存失效时间设 24 小时,IP 归属地不会频繁变。 - 兜底逻辑:查询失败、解析异常、海外 IP(无对应国内仓)都要有默认仓;海外订单单独走国际仓线,别硬塞国内仓。
- 拆单场景:一单多件、部分仓缺货时,允许「就近仓为主 + 次近仓补发」,跨仓拆单前先算下运费和时效哪个划算。
总结
多仓选仓的本质是「拿买家位置和仓库位置做距离排序」,IP 归属地接口提供了这个位置——城市级精度、免费、不用买家授权,最适合做下单前的预估和默认路由。接口就是 https://ip9.com.cn/get?ip=xxx,返回国家、省、市、经纬度、运营商等字段,免费版 60 次/分钟/IP、无需注册;要更细的区县级或其他字段可看官网 https://www.ip9.com.cn 的 VIP 说明。代码量不大,核心就是「查归属地 → 算距离 → 排序选仓」三步,接进订单服务后,运费预估和时效承诺都能自动算。