Skip to content

直播账号「人在深圳、开播在东北」怎么查?用 IP 属地识别异地开播和代播(Go 实现) ​

一个主播的粉丝群里吵起来了:她主页写着「深圳宝妈,每天晚八点播」,声音口音却是东北的,评论区还贴了「行程码」。运营去后台翻推流日志,发现这个账号最近七天用了三个不同省份的出口 IP。账号是谁在播、在哪播,推流网关的日志里其实写得清清楚楚,只是没人把 IP 翻译成地名而已。

一、直播平台为什么盯开播属地 ​

三类需求叠在一起:

  • 平台合规与账号安全:账号租借、代播、工作室批量开号(一个房间几十个「新人主播」),这些行为在属地维度上会留下痕迹——多个账号共用同一个出口 IP,或者同一个账号的登录地和开播地长期不一致。
  • 公会管理:公会签了二十个主播,实际开播的可能只有三个场地,属地数据能看出「谁在替谁播」。
  • 主播属地展示:主流平台的主页和直播间都会展示属地,规则基本是按出口 IP 推断,多数到省级。用户看得到,就意味着平台必须能解释它怎么来的。

二、IP 属地能证明什么,不能证明什么 ​

先把边界立住,不然规则一上线就误伤:

  • 它是出口网络的属地,不是人站在哪。家里宽带、公司专线、手机流量三种出口,属地可能三个答案;挂了代理或加速器,属地直接跟着节点走。
  • 精度是城市级,接口返回的经纬度是城市中心点,拿来做「精确到街道」的判断是错的。
  • 动态 IP 会在同一城市换段,同省范围的小幅变化不算异常,别一看到 IP 变了就告警。
  • 单看属地证明不了作弊。出差的、回老家开播的、用小机房做推流中转的,都会落在「异常」区间里。属地只出线索,人工核验和后续问询才是处置依据。

三、Go 实现:一次跑出两条线索 ​

下面这个单文件程序吃的是「开播事件」(时间、账号、IP、事件类型,生产环境从推流网关日志按行取),输出属地明细,顺带跑两条规则:同一账号属地跳变、多个账号共用同一出口 IP。

go
// live_ip_audit.go —— 直播开播属地审计:账号租借 / 异地开播 / 多号共出口
// 用法: go run live_ip_audit.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

const apiBase = "https://ip9.com.cn/get"

// 示例:开播事件(时间, 账号, 出口IP, 事件类型)。生产环境从推流网关日志里按行取。
var sample = []string{
	"2026-10-06 20:01,live_1001,111.18.31.7,start_push",
	"2026-10-06 20:03,live_1001,120.79.4.66,login",
	"2026-10-05 21:10,live_1001,117.136.52.18,start_push",
	"2026-10-06 20:05,live_1002,120.79.4.66,start_push",
	"2026-10-06 20:07,live_1003,120.79.4.66,start_push",
	"2026-10-06 20:09,live_1004,120.79.4.66,start_push",
	"2026-10-06 21:00,live_2001,203.0.113.9,start_push",
}

type rec struct {
	Time, Acct, IP, Event string
	Region                string // "省 市" 或 "国家"
}

type geoResp struct {
	Ret  int `json:"ret"`
	Data struct {
		Country string `json:"country"`
		Prov    string `json:"prov"`
		City    string `json:"city"`
		ISP     string `json:"isp"`
		IPType  string `json:"ip_type"` // VIP 版字段:ISP/BUS/IDC
	} `json:"data"`
}

var (
	mu    sync.Mutex
	cache = map[string]geoResp{}
	last  = time.Now().Add(-time.Second) // 限速游标:免费版 60 次/分钟
)

func region(ip string) geoResp {
	mu.Lock()
	if g, ok := cache[ip]; ok {
		mu.Unlock()
		return g
	}
	// 免费版 60 次/分钟 → 每次至少间隔 1.05 秒,宁慢勿触发 429
	if wait := 1050*time.Millisecond - time.Since(last); wait > 0 {
		time.Sleep(wait)
	}
	last = time.Now()
	mu.Unlock()

	var g geoResp
	client := &http.Client{Timeout: 3 * time.Second}
	resp, err := client.Get(apiBase + "?ip=" + url.QueryEscape(ip))
	if err != nil {
		return g
	}
	defer resp.Body.Close()
	if err := json.NewDecoder(resp.Body).Decode(&g); err != nil || g.Ret != 200 {
		return geoResp{}
	}
	mu.Lock()
	cache[ip] = g
	mu.Unlock()
	return g
}

func label(g geoResp) string {
	switch {
	case g.Ret != 200:
		return "未知"
	case g.Data.City != "":
		return g.Data.Prov + " " + g.Data.City
	case g.Data.Prov != "":
		return g.Data.Prov
	default:
		return g.Data.Country
	}
}

func main() {
	var rows []rec
	for _, line := range sample {
		p := strings.Split(line, ",")
		if len(p) != 4 {
			continue
		}
		g := region(p[2])
		rows = append(rows, rec{p[0], p[1], p[2], p[3], label(g)})
	}

	fmt.Println("== 开播属地明细 ==")
	for _, r := range rows {
		fmt.Printf("%s  %-9s %-15s %-12s %s\n", r.Time, r.Acct, r.IP, r.Event, r.Region)
	}

	// 线索一:同一账号开播属地跨省跳变(含登录地 ≠ 开播地)
	fmt.Println("\n== 线索一:账号属地跳变 ==")
	seen := map[string]map[string]bool{}
	for _, r := range rows {
		if seen[r.Acct] == nil {
			seen[r.Acct] = map[string]bool{}
		}
		seen[r.Acct][r.Region] = true
	}
	for acct, set := range seen {
		if len(set) > 1 {
			list := make([]string, 0, len(set))
			for k := range set {
				list = append(list, k)
			}
			fmt.Printf("  %-9s 出现 %d 个属地: %s\n", acct, len(set), strings.Join(list, " / "))
		}
	}

	// 线索二:多个账号共用同一出口 IP(代播 / 租号 / 工作室)
	fmt.Println("\n== 线索二:出口 IP 聚合 ==")
	byIP := map[string][]string{}
	for _, r := range rows {
		if r.Event == "start_push" {
			byIP[r.IP] = append(byIP[r.IP], r.Acct)
		}
	}
	for ip, accts := range byIP {
		if len(accts) >= 3 {
			fmt.Printf("  %-15s %d 个账号同 IP 开播: %s\n", ip, len(accts), strings.Join(accts, ","))
		}
	}
}

go run live_ip_audit.go 的真实输出(示例 IP 是真实可查的):

== 开播属地明细 ==
2026-10-06 20:01  live_1001 111.18.31.7     start_push   陕西 铜川
2026-10-06 20:03  live_1001 120.79.4.66     login        广东 深圳
2026-10-05 21:10  live_1001 117.136.52.18   start_push   湖北 武汉
2026-10-06 20:05  live_1002 120.79.4.66     start_push   广东 深圳
2026-10-06 20:07  live_1003 120.79.4.66     start_push   广东 深圳
2026-10-06 20:09  live_1004 120.79.4.66     start_push   广东 深圳
2026-10-06 21:00  live_2001 203.0.113.9     start_push   保留

== 线索一:账号属地跳变 ==
  live_1001 出现 3 个属地: 湖北 武汉 / 陕西 铜川 / 广东 深圳

== 线索二:出口 IP 聚合 ==
  120.79.4.66     3 个账号同 IP 开播: live_1002,live_1003,live_1004

代码里三个细节值得抄:

  • last 这个限速游标是整个程序唯一的节流阀,免费版 60 次/分钟,串行 1.05 秒一次最稳;真上生产要审计十万条日志,就该上 VIP 版(18 万次/分钟)或者本地缓存落库,只查没见过的 IP。
  • cache 加 sync.Mutex:IP 重复率很高——一个 wifi 下十个账号、一个机房几百个主播,缓存能省掉大部分请求。
  • 超时 3 秒、解析失败一律返回空结构体,label() 里统一降级成「未知」,审计程序最忌讳因为接口抖一下就把 200 个主播标成异常。

还想再准一层,可以看 ip_type:出口是 IDC(机房)的账号要格外留意,正常主播用家庭宽带(ISP),批量养号的工作室往往走机房或代理池。这个字段是 VIP 版提供的,免费版没有。

四、阈值怎么定,误伤怎么兜 ​

「几个属地算异常」没有标准答案,但有几个坑必须绕开:

  • 同省变化不算。家庭宽带动态 IP 每天换段很正常,只要还是「广东 广州 / 广东 佛山」,别当跳变处理。建议规则写成「同一账号 7 天内出现 ≥3 个不同省份的开播属地」再进人工池。
  • 共享出口要豁免。公司、园区、校园网、直播基地都是一个出口几百号人,同一 IP 上挂十几个账号不稀奇——所以出口 IP 聚合的结果要跟「该 IP 是否出现在白名单场地」比对,再定风险等级。
  • 别忘了解释成本。真出差的主播被降权,客服电话会打爆。规则命中只做「标记 + 排队人工核验」,不直接断流、不直接扣分。
  • 日志本身要规范:开播事件记一次、登录事件记一次,都带 IP 和端口字段;日志里的 IP 属于个人信息,按合规要求设留存期限、做脱敏,只保留聚合指标。

总结 ​

推流网关的每行日志里都躺着一个 IP,把它翻译成地名,账号租借、代播、工作室批量开播这些靠人工盯不过来的问题就变成了跑批任务。几百行的 Go 程序一晚上就能把一周的数据过一遍,先出线索清单,再人工核。查询用 https://ip9.com.cn/get?ip=<ip> 就行,免费版不用注册、支持 IPv6,主要是限速 60 次/分钟,批量跑的时候限速和缓存别省;要区分家庭宽带和机房出口(ip_type),或者要区县粒度,再看官网 https://www.ip9.com.cn 的 VIP 说明。