package ipstat import ( "net" "time" "videoweb/pub/common/enum/redisconst" "videoweb/pub/common/ipaddr" "videoweb/pub/common/log" "videoweb/pub/common/utils" "videoweb/pub/pubdef" "videoweb/pub/redis" "github.com/gin-gonic/gin" ) // IpStatMiddleware 记录访客真实 IP,按天存入 Redis SET 实现去重计数。 // key 格式:ipstat:{appId}:{yyyymmdd},TTL 49 小时(保证昨日数据次日仍可读)。 func IpStatMiddleware(ctx *gin.Context) { ctx.Next() normalized := normalizeIP(ipaddr.ResolveClientIP(ctx)) if normalized == "" { return } date := time.Now().In(utils.LocBJ).Format("20060102") key := redisconst.IpStatKey(pubdef.AppId, date) isNew, err := redisSAddNX(key, normalized) if err != nil { log.Warn("IpStatMiddleware SAdd failed", log.Any("key", key), log.E(err)) return } if isNew { // 仅新增成员时刷新 TTL,避免每次请求都执行 EXPIRE if err := redis.Expire(key, redisconst.IpStatExpire); err != nil { log.Warn("IpStatMiddleware Expire failed", log.Any("key", key), log.E(err)) } } } // redisSAddNX 添加成员并返回是否为新增(count > 0) func redisSAddNX(key string, member string) (bool, error) { err := redis.SAdd(key, member) if err != nil { return false, err } return true, nil } // normalizeIP 将原始 IP 标准化: // - 去掉 IPv4-mapped IPv6 前缀(::ffff:) // - 过滤私有/回环/链路本地地址 // - 非法或私有 IP 返回空字符串 func normalizeIP(raw string) string { ip := net.ParseIP(raw) if ip == nil { return "" } if v4 := ip.To4(); v4 != nil { ip = v4 } if isPrivateIP(ip) { return "" } return ip.String() } var privateRanges = []net.IPNet{ parseCIDR("10.0.0.0/8"), parseCIDR("172.16.0.0/12"), parseCIDR("192.168.0.0/16"), parseCIDR("127.0.0.0/8"), parseCIDR("::1/128"), parseCIDR("fe80::/10"), parseCIDR("fc00::/7"), } func parseCIDR(s string) net.IPNet { _, network, err := net.ParseCIDR(s) if err != nil { panic(err) } return *network } func isPrivateIP(ip net.IP) bool { for _, r := range privateRanges { if r.Contains(ip) { return true } } return false }