<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const api = "http://127.0.0.1:8000";
let chartIns = null;
let chartRenderLock = false;
async function addIp(){
let ip = document.getElementById("ipAddr").value;
let town = document.getElementById("town").value;
let village = document.getElementById("village").value;
let remark = document.getElementById("ipRemark").value;
let res = await fetch(api+"/ip/add",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({ip,town,village,remark})
})
let ret = await res.json();
if (!res.ok) {
alert(ret.detail);
return;
}
alert(ret.msg);
document.getElementById("ipAddr").value = "";
document.getElementById("town").value = "";
document.getElementById("village").value = "";
document.getElementById("ipRemark").value = "";
}
async function importIp(){
let file = document.getElementById("importFile").files[0];
if(!file){
alert("请选择csv文件");
return;
}
let fd = new FormData();
fd.append("file", file);
let res = await fetch(api+"/ip/import",{
method:"POST",
body:fd
})
let ret = await res.json();
alert(ret.msg);
}
async function pingAll(){
if(chartRenderLock) return;
chartRenderLock = true;
try{
let res = await fetch(api+"/ping/all");
let data = await res.json();
let list = data.data;
const total = list.length;
let online = 0;
let offline = 0;
list.forEach(item=>{
if(item.status === "online") online++;
else offline++;
})
let rate = total === 0 ? "0.00" : (online / total * 100).toFixed(2);
from fastapi import FastAPI, Query, Body, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import pythonping
import sqlite3
import time
import csv
import re
import sys
import os
import threading
from io import StringIO
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor
def get_resource_path():
if hasattr(sys, '_MEIPASS'):
return os.path.dirname(sys.executable)
return os.path.abspath(".")
def init_db():
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS monitor_ip
(id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT, town TEXT, village TEXT, remark TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS ping_log
(id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT, delay REAL, loss REAL, status TEXT, create_time TEXT)''')
conn.commit()
conn.close()
init_db()
class IpItem(BaseModel):
ip: str
town: str
village: str
remark: str
def ping_target(ip: str, town: str, village: str, remark: str, count=2, timeout=1200):
try:
res = pythonping.ping(ip, count=count, timeout=timeout/1000)
avg_delay = res.rtt_avg_ms
loss = round(res.packet_loss, 2)
status = "online" if loss < 100 else "offline"
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
c.execute("INSERT INTO ping_log(ip,delay,loss,status,create_time) VALUES (?,?,?,?,?)",
(ip, avg_delay, loss, status, now))
conn.commit()
conn.close()
return {
"ip": ip,
"town": town,
"village": village,
"name": remark,
"avg_delay": avg_delay,
"packet_loss": loss,
"status": status,
"time": now
}
except Exception as e:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
c.execute("INSERT INTO ping_log(ip,delay,loss,status,create_time) VALUES (?,?,?,?,?)",
(ip, 9999, 100, "error", now))
conn.commit()
conn.close()
return {"ip": ip, "town": town, "village": village, "name": remark, "status": "error", "msg": str(e)}
@app.post("/ip/add")
def add_ip(item: IpItem):
ip = item.ip.strip()
if not is_valid_ip(ip):
raise HTTPException(status_code=400, detail="仅支持纯IPv4地址,不支持域名/DNS/网关域名")
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
c.execute("INSERT INTO monitor_ip(ip,town,village,remark) VALUES (?,?,?,?)",
(ip, item.town.strip(), item.village.strip(), item.remark.strip()))
conn.commit()
conn.close()
return {"code":200, "msg":"IP添加成功"}
@app.get("/ip/list")
def get_ip():
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
data = conn.execute("SELECT * FROM monitor_ip").fetchall()
conn.close()
lst = [{"id":d[0],"ip":d[1],"town":d[2],"village":d[3],"remark":d[4]} for d in data]
return {"code":200, "data":lst}
@app.post("/ip/import")
async def import_ip(file: UploadFile = File(...)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(StringIO(text))
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
succ_count = 0
skip_count = 0
for row in reader:
ip = row.get("ip", "").strip()
town = row.get("town", "").strip()
village = row.get("village", "").strip()
remark = row.get("remark", "").strip()
if is_valid_ip(ip):
c.execute("INSERT INTO monitor_ip(ip,town,village,remark) VALUES (?,?,?,?)", (ip, town, village, remark))
succ_count += 1
else:
skip_count += 1
conn.commit()
conn.close()
return {"code":200, "msg":f"成功导入{succ_count}条合法IP,跳过{skip_count}条域名/非法地址"}
@app.get("/export/all_ip")
def export_all_ip():
output = StringIO()
writer = csv.writer(output)
writer.writerow(["id","ip","乡镇","村落","设备名称"])
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
rows = conn.execute("SELECT * FROM monitor_ip").fetchall()
for r in rows:
writer.writerow(r)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition":"attachment; filename=全部IP列表.csv"}
)
@app.get("/export/online_ip")
def export_online_ip():
output = StringIO()
writer = csv.writer(output)
writer.writerow(["ip","乡镇","村落","设备名称","延迟ms","丢包率%","状态","检测时间"])
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
sql = '''
SELECT DISTINCT l.ip, m.town, m.village, m.remark, l.delay, l.loss, l.status, l.create_time
FROM ping_log l
LEFT JOIN monitor_ip m ON l.ip=m.ip
WHERE l.status = 'online'
GROUP BY l.ip
ORDER BY l.delay ASC
'''
rows = conn.execute(sql).fetchall()
for r in rows:
writer.writerow(r)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition":"attachment; filename=在线IP清单.csv"}
)
@app.get("/export/offline_ip")
def export_offline_ip():
output = StringIO()
writer = csv.writer(output)
writer.writerow(["ip","乡镇","村落","设备名称","延迟ms","丢包率%","状态","检测时间"])
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
sql = '''
SELECT DISTINCT l.ip, m.town, m.village, m.remark, l.delay, l.loss, l.status, l.create_time
FROM ping_log l
LEFT JOIN monitor_ip m ON l.ip=m.ip
WHERE l.status IN ('offline','error')
GROUP BY l.ip
ORDER BY l.create_time DESC
'''
rows = conn.execute(sql).fetchall()
for r in rows:
writer.writerow(r)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition":"attachment; filename=离线IP清单.csv"}
)
@app.get("/export/loss_ip")
def export_loss_ip():
output = StringIO()
writer = csv.writer(output)
writer.writerow(["ip","乡镇","村落","设备名称","延迟ms","丢包率%","状态","检测时间"])
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
sql = '''
SELECT DISTINCT l.ip, m.town, m.village, m.remark, l.delay, l.loss, l.status, l.create_time
FROM ping_log l
LEFT JOIN monitor_ip m ON l.ip=m.ip
WHERE l.loss > 0
GROUP BY l.ip
ORDER BY l.loss DESC
'''
rows = conn.execute(sql).fetchall()
for r in rows:
writer.writerow(r)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition":"attachment; filename=存在丢包IP清单.csv"}
)
@app.get("/ping/single")
def ping_single(ip:str=Query(...)):
ip = ip.strip()
if not is_valid_ip(ip):
raise HTTPException(status_code=400, detail="仅支持纯IPv4,禁止域名/DNS")
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
row = conn.execute("SELECT town,village,remark FROM monitor_ip WHERE ip=?", (ip,)).fetchone()
conn.close()
town = row[0] if row else ""
village = row[1] if row else ""
name = row[2] if row else "未知设备"
return ping_target(ip, town, village, name)
@app.get("/ping/all")
def ping_all():
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
rows = conn.execute("SELECT ip, town, village, remark FROM monitor_ip").fetchall()
conn.close()
result = []
for ip, town, village, remark in rows:
result.append(ping_target(ip, town, village, remark))
time.sleep(0.08)
return {"code":200, "data":result}
def base_60s_task():
if not task_running_lock.acquire(blocking=False):
return
try:
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
all_rows = conn.execute("SELECT ip, town, village, remark FROM monitor_ip").fetchall()
conn.close()
for ip, town, village, remark in all_rows:
ping_target(ip, town, village, remark)
time.sleep(0.08)
except Exception:
pass
finally:
task_running_lock.release()
def offline_retry_10min_task():
if not offline_task_lock.acquire(blocking=False):
return
try:
db_path = os.path.join(get_resource_path(), "ping_db.db")
conn = sqlite3.connect(db_path, check_same_thread=False)
sql = '''
SELECT DISTINCT m.ip, m.town, m.village, m.remark
FROM monitor_ip m
INNER JOIN (
SELECT ip, MAX(create_time) as max_time FROM ping_log GROUP BY ip
) last_log ON m.ip = last_log.ip
INNER JOIN ping_log l ON l.ip = last_log.ip AND l.create_time = last_log.max_time
WHERE l.status IN ('offline','error')
'''
offline_rows = conn.execute(sql).fetchall()
conn.close()
for ip, town, village, remark in offline_rows:
ping_target(ip, town, village, remark)
time.sleep(0.08)
except Exception:
pass
finally:
offline_task_lock.release()