Initial commit

This commit is contained in:
2026-04-19 23:51:17 +08:00
parent 222cb1cca5
commit f2b7d7e2aa
5 changed files with 170 additions and 0 deletions

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM python:3.11-slim
WORKDIR /app
# 1. 安装系统编译依赖qqwry-py3 需要编译)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libffi-dev \
libssl-dev \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# 2. 复制并安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. 复制源码 & 纯真库
COPY qqwry.dat app.py ./
# 4. 默认端口 5000可 docker run -e PORT=8080 覆盖
ENV PORT=5000
EXPOSE ${PORT}
# 5. 直接 python 启动
CMD ["python", "app.py"]

134
app.py Normal file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
RESTful API + 响应式 WebUI手机优先
自动把用户公网 IP 填到输入框
依赖flask>=3.0.0 qqwry-py3>=1.2.1
"""
import os
from flask import Flask, jsonify, request
app = Flask(__name__)
# 1. 加载纯真库
from qqwry import QQwry
q = QQwry()
dat_path = os.path.join(os.path.dirname(__file__), "qqwry.dat")
q.load_file(dat_path)
# 2. API查询 IP
@app.route("/ip/<ip>", methods=["GET"])
def query_ip(ip: str):
country, area = q.lookup(ip)
return jsonify(ip=ip, country=country or None, area=area or None)
# 3. API健康检查
@app.route("/health", methods=["GET"])
def health():
return "ok"
# 4. 响应式 WebUI手机优先+ 自动获取客户端公网 IP
UI_HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>纯真 IP 查询</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f2f4f7}
.container{max-width:600px;margin:0 auto;padding:16px}
.card{background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08);padding:24px}
h1{font-size:22px;margin:0 0 20px;text-align:center;color:#303133}
.input-group{position:relative;margin-bottom:16px}
input{width:100%;padding:12px 44px 12px 12px;font-size:16px;border:1px solid #dcdfe6;border-radius:8px;outline:none;transition:border .2s}
input:focus{border-color:#409eff}
.btn-clear{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;font-size:14px;color:#909399;cursor:pointer;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}
.btn-clear:hover{color:#409eff;background:#ecf5ff}
button{width:100%;padding:12px 0;font-size:17px;color:#fff;background:#409eff;border:none;border-radius:8px;cursor:pointer;transition:background .2s}
button:hover{background:#66b1ff}
.result{margin-top:20px;line-height:1.7;font-size:15px;color:#606266}
.error{color:#f56c6c}
footer{text-align:center;margin-top:16px;font-size:12px;color:#909399}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>纯真 IP 查询</h1>
<div class="input-group">
<input id="ipIn" placeholder="正在获取本机 IP…" autocomplete="off">
<button class="btn-clear" onclick="ipIn.value=''" title="清空">✕</button>
</div>
<button onclick="query()">查 询</button>
<div id="res" class="result"></div>
</div>
<footer>数据提供:纯真 IP 库</footer>
</div>
<script>
async function query(){
const ip = ipIn.value.trim();
if(!ip){show('请输入 IP 地址','error'); return}
show('查询中…');
try{
const r = await fetch(`/ip/${ip}`);
const j = await r.json();
if(!r.ok) throw new Error(j.message||'网络异常');
show(`
<b>IP</b>${j.ip}<br>
<b>国家/地区:</b>${j.country||'-'}<br>
<b>详细:</b>${j.area||'-'}`, 'success');
}catch(e){
show(e.message, 'error');
}
}
function show(msg, type=''){
res.innerHTML = type==='error' ? `<span class="error">${msg}</span>` : msg;
}
// 自动获取当前设备公网 IP 并填入输入框
(async function fillMyIP(){
const services = [
'https://1.1.1.1/cdn-cgi/trace',
'https://checkip.amazonaws.com',
'https://ifconfig.me/ip',
'https://api.ipify.org',
'https://icanhazip.com',
'https://ipinfo.io/ip'
];
const timeout = ms => new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms));
for (const svc of services) {
try {
const r = await Promise.race([fetch(svc), timeout(1500)]);
if (!r.ok) throw new Error('bad response');
const t = await r.text();
const ip = svc.includes('cdn-cgi/trace') ? (t.match(/^ip=(.+)$/m)?.[1] || '') : t.trim();
if (ip && /^[\d.]+$/.test(ip) || /^[\da-f:]+$/i.test(ip)) {
ipIn.value = ip;
ipIn.placeholder = '请输入 IP 地址';
return;
}
}catch{}
}
ipIn.placeholder = '请输入 IP 地址';
})();
// 回车快捷查询
ipIn.addEventListener('keyup', e => e.key==='Enter' && query());
</script>
</body>
</html>"""
@app.route("/ui")
def ui():
return UI_HTML
@app.route("/")
def index():
return """<script>location="/ui"</script>"""
# 5. 启动
if __name__ == "__main__":
app.run(host="0.0.0.0",
port=int(os.getenv("PORT", 5000)),
debug=False)

9
docker-compose.yml Normal file
View File

@@ -0,0 +1,9 @@
services:
app:
build: .
container_name: czip-api
restart: unless-stopped
ports:
- "5000:5000"
environment:
- PORT=5000

BIN
qqwry.dat Normal file

Binary file not shown.

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
flask==3.0.0
qqwry-py3==1.2.1