Skip to content

「未成年打赏退款」怎么核实是不是本人操作?用登录 IP 属地串一条证据链(Python 实现) ​

上个月一个客服群里在传同一份截图:家长拿着孩子的户口本要求全额退款,金额四万八,理由是「孩子趁大人睡着刷的」。平台看了下单记录——连续四个晚上,每晚二十多笔,付款流畅、没有输错密码、没有换设备。客服的疑问很朴素:这到底是不是孩子干的?

这不是道德问题,是举证问题。法院判退款时看的也不是谁哭得更真,而是「能不能证明涉案时段的操作人不是账号实名本人」。而这类案子里,能被固定下来的客观数据不多:设备、支付通道、登录 IP 属地,几乎是全部。

一、纠纷卡在哪儿 ​

《未成年直播打赏可退还》这条司法解释出来之后,实操里冒出一堆边界情况:

  • 家长白天用账号,孩子晚上接着用同一台手机,设备和账号都「不像外人」;
  • 成年人自己打赏完了后悔,拿孩子的身份来申请退款;
  • 账号借给亲戚家孩子用,实名人和操作人确实不一致,但设备一直在同一个家。

这几类案子,靠「人脸识别」只能覆盖到实名环节,覆盖不到「谁在按付款键」。能覆盖到的东西,只剩网络侧留下的痕迹:每一笔充值、每一次登录,请求都带着一个出口 IP。把 IP 翻成归属地,就能回答一个很具体的问题——这几笔打赏发生的时候,网络出口和这个账号平时的出口,是不是一回事。

二、先划边界,别把线索当结论 ​

说清楚 IP 属地能干什么、不能干什么,不然规则一上线就误伤:

  • IP 属地是出口网络的属地,不是人在哪。同一户人家,宽带和手机流量可能落在同一个省,也可能一个是本地运营商、一个是跨省漫游。
  • 精度是城市级。接口返回的经纬度是城市中心点,拿它算「离家多少公里」是错的。
  • 三大运营商的家宽 IP 段是动态分配的,同一城市换段很正常,别一看 IP 变了就报警。
  • 单看属地证明不了任何人的身份。它的作用是产出「需要人工进一步核实」的线索,最终定性还得靠问询和监护人沟通记录。

一句话:IP 属地不负责判决,负责把「值得追问的样本」从海量订单里挑出来。

三、Python 实现:从登录日志到证据链 ​

思路分三层:先给每个账号建属地基线(争议时段之外的登录记录),再看争议时段落没落在基线附近,最后把偏离的样本按账号聚合成一段可读的举证描述。缓存和限速放在查询函数里,别让免费额度被打满。

python
# dashang_evidence.py —— 未成年打赏退款:用登录 IP 属地拼证据链
# 依赖:仅标准库(Python 3.8+)
import json, time, urllib.parse, urllib.request
from collections import defaultdict, Counter

API = "https://ip9.com.cn/get"      # 不传 ip 查当前请求方,?ip= 查指定 IP
_cache, _last = {}, 0.0

def geo(ip):
    """查 IP 归属地。带进程内缓存 + 约 55 次/分钟限速,免费版够用。"""
    global _last
    if ip in _cache:
        return _cache[ip]
    gap = 1.1 - (time.time() - _last)          # 留余量,避免撞 60 次/分钟
    if gap > 0:
        time.sleep(gap)
    _last = time.time()
    req = urllib.request.Request(
        API + "?ip=" + urllib.parse.quote(ip),
        headers={"User-Agent": "Mozilla/5.0 (ip9-evidence-demo)"},   # 默认 UA 会被 403
    )
    try:
        with urllib.request.urlopen(req, timeout=5) as r:
            body = json.loads(r.read().decode("utf-8"))
    except Exception as e:
        return {"_err": "网络异常: %s" % e}
    if body.get("ret") != 200:                 # 非法 IP 是 400,限速是 429
        return {"_err": "ret=%s" % body.get("ret")}
    _cache[ip] = body["data"]
    return body["data"]

def place(d):
    """把返回字段压成「省+市」这种可比对的地名;境外只到国家。"""
    if "_err" in d:
        return "查询失败"
    prov, city, country = d.get("prov") or "", d.get("city") or "", d.get("country") or ""
    if not prov:
        return country or "未知"
    return prov if not city or city == prov else "%s%s" % (prov, city)

# 行为日志:时间、账号、事件、出口 IP(生产环境从登录/支付网关按行取)
logs = [
    ("2026-09-20 20:11", "u_88213", "login", "111.18.31.7"),    # 陕西铜川
    ("2026-09-22 21:02", "u_88213", "login", "223.104.38.19"),  # 北京(手机流量)
    ("2026-09-28 20:35", "u_88213", "login", "111.18.31.7"),
    ("2026-10-03 23:02", "u_88213", "pay",   "45.192.78.22"),   # 香港机房
    ("2026-10-03 23:06", "u_88213", "pay",   "45.192.78.22"),
    ("2026-10-04 23:40", "u_88213", "pay",   "120.79.4.66"),    # 深圳云主机
]
DISPUTE_DAYS = ("2026-10-03", "2026-10-04")   # 申请退款指向的争议时段

baseline = defaultdict(Counter)   # 账号 -> {属地: 次数},只统计争议时段之外
dispute  = defaultdict(list)      # 账号 -> [(时间, 事件, IP, 属地)]

for ts, uid, ev, ip in logs:
    p = place(geo(ip))
    if ts[:10] in DISPUTE_DAYS:
        dispute[uid].append((ts, ev, ip, p))
    else:
        baseline[uid][p] += 1

for uid, rows in dispute.items():
    base = baseline.get(uid)
    known = {p for p, _ in base.most_common(3)} if base else set()   # 常见的三个属地当基线
    print("账号 %s —— 争议时段样本 %d 条" % (uid, len(rows)))
    print("  历史属地基线: %s" % ("、".join(sorted(known)) or "样本不足,无基线"))
    outside = []
    for ts, ev, ip, p in rows:
        if not known:
            flag = "无基线可比"
        elif p in known:
            flag = "与基线一致"
        else:
            flag = "偏离基线"
            outside.append(p)
        print("  [%s] %-5s %-16s %-10s %s" % (ts[11:], ev, ip, p, flag))
    print("  结论: 争议时段有 %d 条操作落在 %s,需向监护人核实这些网络出口\n"
          % (len(outside), "、".join(sorted(set(outside))) or "-"))

跑出来是什么样:这个账号平时稳定在「陕西铜川」和「北京」两个移动出口,10 月 3 日夜里两笔支付却来自香港的一个机房地址,10 月 4 日又换到深圳的云主机段。这种「基线一致 + 争议时段突然漂到机房网段」的组合,就要在工单里写清楚——不是直接拒赔,是提示客服去问「这几笔是不是在境外网络下操作的、当时谁在旁边」。

四、落地注意事项 ​

限速和缓存别省。 免费版是 60 次/分钟/IP,一套退款工单要拉几十上百条记录,不缓存重复 IP 很容易撞 429。进程内缓存只够单机用,多实例部署就换 Redis,键是 IP,过期时间一小时起步。

记得带 User-Agent。 实测 Python 标准库 urllib 默认的 UA(Python-urllib/3.x)会被接口返回 403,随手加一个 UA 就正常;curl 和 requests 默认 UA 不受影响,但自建 HTTP 客户端时容易踩到。

基线窗口要选对。 拿最近七天当基线,会被出差、回家、换手机这些正常波动污染;行业里普遍用 30 到 90 天。窗口内的记录还得剔除掉争议时段本身,否则自己把自己洗白了。

境外地址不必然是作弊。 加速器、跨境办公、境外手机卡都会让出口跑到境外。所以规则输出的是「样本」不是「结论」,处置建议一栏要留人工判断的空间。

涉及未成年人,数据使用得收着点。 只保留举证必需的时间、事件类型和归属地,别把完整访问日志存成明文堆在工单里;个案处理完按内部留存期限清理。IP 属地接口调用本身不涉及内容,但日志链路要考虑个保法下最小必要原则。

给客服交付的东西要能直接抄。 上面打印的那几行,抓进工单就够用了:账号、时段、属地、是否偏离基线。别让客服自己去翻译 IP。

总结 ​

这类退款纠纷的解法不是「查出一个真相」,而是把可核验的客观线索整理成一份能经得起追问的材料。IP 归属地查询在里面扮演的是最轻量的一环——不加密、不注册、一次 HTTP 请求把 IP 翻成地名,剩下的是把规则和窗口调对。

接口就一个:https://ip9.com.cn/get?ip=xxx(IPv4/IPv6 都支持,不传参数查当前请求方),返回里的 prov、city、country、isp 足够支撑上面这套基线比对。更多字段和 VIP 能力(区县、ip_type、ip_asn)以官网 https://www.ip9.com.cn 为准。