service.xpcool.com/internal/library/iploc/iploc.go
夏犀麟 ea50261cb8
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 35s
feat(house): 扩充看房数据并增强服务器日志功能
- 新增新房预售证表 house_presale 存储预售许可信息
- 为 house_community 表添加 avg_price 字段存储小区参考均价
- 增强服务器操作审计日志功能,新增管理员账号、IP归属地、错误信息、UA等字段
- 实现纯Go版IP归属地离线解析器,无外部依赖,支持二分查找
- 优化看房列表查询逻辑,修复Fields设置位置导致的SQL语法错误
- 集成招聘模块,添加独立数据库配置和Bark推送服务支持
- 重构日志查询接口,支持多维度筛选和综合分页列表展示
- 更新DAO实体结构同步数据库表结构调整
2026-08-27 01:47:32 +08:00

100 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 返回空字符串(调用方据此决定展示策略)。
// 备注:当前仅支持 IPv4IPv6 直接返回空,避免误判。
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, " ")
}