Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 35s
- 新增新房预售证表 house_presale 存储预售许可信息 - 为 house_community 表添加 avg_price 字段存储小区参考均价 - 增强服务器操作审计日志功能,新增管理员账号、IP归属地、错误信息、UA等字段 - 实现纯Go版IP归属地离线解析器,无外部依赖,支持二分查找 - 优化看房列表查询逻辑,修复Fields设置位置导致的SQL语法错误 - 集成招聘模块,添加独立数据库配置和Bark推送服务支持 - 重构日志查询接口,支持多维度筛选和综合分页列表展示 - 更新DAO实体结构同步数据库表结构调整
100 lines
3.3 KiB
Go
100 lines
3.3 KiB
Go
// Package iploc 提供离线 IP 归属地解析(省/市/运营商)。
|
||
// 底层数据使用 ip2region 的 xdb(v4) 数据库,通过 go:embed 直接打进二进制,
|
||
// 无需外部文件、无需联网,单次查询为内存二分查找,性能稳定。
|
||
package iploc
|
||
|
||
import (
|
||
_ "embed"
|
||
"encoding/binary"
|
||
"net"
|
||
"strings"
|
||
)
|
||
|
||
//go:embed ip2region.xdb
|
||
var xdbContent []byte
|
||
|
||
// xdb v4 文件结构常量(与官方 C binding 保持一致)。
|
||
const (
|
||
headerInfoLength = 256 // 头部信息长度
|
||
vectorIndexCols = 256 // 向量索引列数
|
||
vectorIndexSize = 8 // 每条向量索引 8 字节(起始 ptr + 结束 ptr)
|
||
v4IndexSize = 14 // IPv4 段索引块:4(IP 起) + 4(IP 止) + 2(数据长度) + 4(数据偏移)
|
||
)
|
||
|
||
// Locate 解析 IPv4 的归属地,返回形如 "广东 深圳 电信" 的可读字符串。
|
||
// 内部/无效/解析失败的 IP 返回空字符串(调用方据此决定展示策略)。
|
||
// 备注:当前仅支持 IPv4;IPv6 直接返回空,避免误判。
|
||
func Locate(ipStr string) string {
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
return ""
|
||
}
|
||
v4 := ip.To4()
|
||
if v4 == nil {
|
||
// 暂不支持 IPv6 归属地解析,返回空。
|
||
return ""
|
||
}
|
||
// v4 已是网络序 4 字节 [a,b,c,d],按大端整型比较即可与段索引块中的起止 IP 对齐。
|
||
ipUint := binary.BigEndian.Uint32(v4)
|
||
|
||
// 1) 由前两个字节定位向量索引,得到段索引区间 [sPtr, ePtr]。
|
||
il0 := int(v4[0])
|
||
il1 := int(v4[1])
|
||
idx := il0*vectorIndexCols*vectorIndexSize + il1*vectorIndexSize
|
||
vOff := headerInfoLength + idx
|
||
if vOff+8 > len(xdbContent) {
|
||
return ""
|
||
}
|
||
sPtr := binary.LittleEndian.Uint32(xdbContent[vOff : vOff+4])
|
||
ePtr := binary.LittleEndian.Uint32(xdbContent[vOff+4 : vOff+8])
|
||
if sPtr == 0 || ePtr == 0 {
|
||
// 该前缀段无数据,归属地未知。
|
||
return ""
|
||
}
|
||
|
||
// 2) 在段索引区间中二分查找命中 IP 的段(段索引块按起始 IP 有序)。
|
||
l, h := 0, int((ePtr-sPtr)/v4IndexSize)
|
||
dataPtr, dataLen := uint32(0), uint16(0)
|
||
for l <= h {
|
||
m := (l + h) >> 1
|
||
p := sPtr + uint32(m)*v4IndexSize
|
||
if int(p)+v4IndexSize > len(xdbContent) {
|
||
break
|
||
}
|
||
startIP := binary.BigEndian.Uint32(xdbContent[p : p+4])
|
||
endIP := binary.BigEndian.Uint32(xdbContent[p+4 : p+8])
|
||
switch {
|
||
case ipUint < startIP:
|
||
h = m - 1
|
||
case ipUint > endIP:
|
||
l = m + 1
|
||
default:
|
||
// 命中:数据长度在偏移 8(小端 uint16),数据偏移在偏移 10(小端 uint32)。
|
||
dataLen = binary.LittleEndian.Uint16(xdbContent[p+8 : p+10])
|
||
dataPtr = binary.LittleEndian.Uint32(xdbContent[p+10 : p+14])
|
||
l, h = 0, -1 // 退出循环
|
||
}
|
||
}
|
||
if dataLen == 0 {
|
||
return ""
|
||
}
|
||
if int(dataPtr)+int(dataLen) > len(xdbContent) {
|
||
return ""
|
||
}
|
||
return cleanRegion(string(xdbContent[dataPtr : dataPtr+uint32(dataLen)]))
|
||
}
|
||
|
||
// cleanRegion 将 "国家|区域|省份|城市|运营商" 中的 0/空 段剔除,拼接为可读归属地。
|
||
func cleanRegion(raw string) string {
|
||
parts := strings.Split(raw, "|")
|
||
out := make([]string, 0, len(parts))
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" || p == "0" {
|
||
continue
|
||
}
|
||
out = append(out, p)
|
||
}
|
||
return strings.Join(out, " ")
|
||
}
|