Files

340 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""容量看板分析接口。
基于 4G/5G 结果表(含富集 + 高负荷判定 + 优化建议列)做聚合分析,供前端容量看板展示。
读经 `make_warehouse`(直连 MySQL 或 Metrix 平台),分析对象为“主仓库”里的结果表。
"""
from __future__ import annotations
import csv
from datetime import datetime
from typing import Any
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from app import state
from app.config import CACHE_DIR
from app.utils.files import remove_file_safely
from app.warehouse import make_warehouse
router = APIRouter(tags=["dashboard"])
# 每个制式(结果表)的关键列映射
RAT = {
"4g": {
"table": "4G_结果表",
"id": "CGI",
"name": "小区名称",
"ul": "上行PUSCH利用率",
"dl": "下行PDSCH利用率",
"ul_label": "上行PUSCH利用率",
"dl_label": "下行PDSCH利用率",
"users": "YY-RRC连接建立最大用户数",
},
"5g": {
"table": "5G_结果表",
"id": "NCGI",
"name": "CU小区配置名称",
"ul": "上行PRB平均利用率",
"dl": "下行PRB平均利用率",
"ul_label": "上行PRB利用率",
"dl_label": "下行PRB利用率",
"users": "RRC连接平均连接用户数",
},
}
FLOW = "日均流量(GB)"
PROBLEMS = ["高负荷", "利用率预警", "高流量预警"]
def _db():
return make_warehouse(state.current_config())
def _esc(value: str) -> str:
return str(value).replace("\\", "\\\\").replace("'", "''")
def _rows(db, sql: str) -> list[dict[str, Any]]:
ok, result = db.execute_sql(sql)
if not ok:
raise HTTPException(status_code=500, detail=str(result))
return result if isinstance(result, list) else []
def _num(value: Any, digits: int | None = None) -> float | int:
try:
num = float(value)
except (TypeError, ValueError):
return 0
if digits is None:
return int(num)
return round(num, digits)
def _rat(rat: str) -> dict[str, str]:
cfg = RAT.get((rat or "").lower())
if not cfg:
raise HTTPException(status_code=400, detail="rat 仅支持 4g / 5g")
return cfg
def _existing_tables(db) -> set[str]:
try:
return {str(t).lower() for t in db.get_tables()}
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=500, detail=f"读取数据表失败: {exc}") from exc
@router.get("/api/dashboard/status")
def dashboard_status():
tables = _existing_tables(_db())
has_4g = RAT["4g"]["table"].lower() in tables
has_5g = RAT["5g"]["table"].lower() in tables
return {"has_4g": has_4g, "has_5g": has_5g, "ready": has_4g and has_5g}
@router.get("/api/dashboard/overview")
def dashboard_overview(rat: str = Query("4g")):
cfg = _rat(rat)
db = _db()
if cfg["table"].lower() not in _existing_tables(db):
raise HTTPException(status_code=404, detail=f"结果表 {cfg['table']} 不存在,请先进行数据处理")
t = f"`{cfg['table']}`"
ul, dl, flow, users = f"`{cfg['ul']}`", f"`{cfg['dl']}`", f"`{FLOW}`", f"`{cfg['users']}`"
summary = _rows(db, (
f"SELECT COUNT(*) total,"
f" SUM(`高负荷问题`='高负荷') high_load,"
f" SUM(`高负荷问题`='利用率预警') util_warn,"
f" SUM(`高负荷问题`='高流量预警') flow_warn,"
f" SUM(`高负荷问题` IS NULL) normal,"
f" ROUND(AVG({ul})*100,1) avg_ul,"
f" ROUND(AVG({dl})*100,1) avg_dl,"
f" ROUND(MAX({dl})*100,1) max_dl,"
f" ROUND(AVG({flow}),2) avg_flow,"
f" ROUND(SUM({flow}),1) total_flow"
f" FROM {t}"
))
s = summary[0] if summary else {}
summary_out = {
"total": _num(s.get("total")),
"high_load": _num(s.get("high_load")),
"util_warn": _num(s.get("util_warn")),
"flow_warn": _num(s.get("flow_warn")),
"normal": _num(s.get("normal")),
"avg_ul": _num(s.get("avg_ul"), 1),
"avg_dl": _num(s.get("avg_dl"), 1),
"max_dl": _num(s.get("max_dl"), 1),
"avg_flow": _num(s.get("avg_flow"), 2),
"total_flow": _num(s.get("total_flow"), 1),
}
problem_pie = [
{"name": "高负荷", "value": summary_out["high_load"]},
{"name": "利用率预警", "value": summary_out["util_warn"]},
{"name": "高流量预警", "value": summary_out["flow_warn"]},
{"name": "正常", "value": summary_out["normal"]},
]
def group_by(col: str, limit: int = 0, skip_unknown: bool = False) -> list[dict[str, Any]]:
limit_sql = f" LIMIT {int(limit)}" if limit else ""
where_sql = f" WHERE `{col}` IS NOT NULL AND `{col}` <> ''" if skip_unknown else ""
rows = _rows(db, (
f"SELECT IFNULL(`{col}`,'未知') name, COUNT(*) total,"
f" SUM(`是否高负荷小区`='是') high,"
f" SUM(`高负荷问题` IS NOT NULL) flagged"
f" FROM {t}{where_sql} GROUP BY `{col}` ORDER BY flagged DESC, total DESC{limit_sql}"
))
return [
{"name": str(r.get("name")), "total": _num(r.get("total")),
"high": _num(r.get("high")), "flagged": _num(r.get("flagged"))}
for r in rows
]
def util_hist(col: str) -> list[dict[str, Any]]:
rows = _rows(db, (
f"SELECT LEAST(FLOOR({col}*10),9) b, COUNT(*) c FROM {t}"
f" WHERE {col} IS NOT NULL GROUP BY b ORDER BY b"
))
bucket_map = {int(_num(r.get("b"))): _num(r.get("c")) for r in rows}
return [{"bucket": f"{i*10}-{i*10+10}%", "value": bucket_map.get(i, 0)} for i in range(10)]
top_rows = _rows(db, (
f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') `system`,"
f" IFNULL(`带宽`,'') band, IFNULL(`站型`,'') station,"
f" ROUND({ul}*100,1) ul, ROUND({dl}*100,1) dl, ROUND({flow},2) flow, ROUND({users},0) users,"
f" IFNULL(`高负荷问题`,'') problem"
f" FROM {t} WHERE `高负荷问题`='高负荷' ORDER BY {dl} DESC, {flow} DESC LIMIT 10"
))
top_cells = [
{"id": str(r.get("id")), "name": str(r.get("name")), "system": str(r.get("system")),
"band": str(r.get("band")), "station": str(r.get("station")),
"ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1),
"flow": _num(r.get("flow"), 2), "users": _num(r.get("users")), "problem": str(r.get("problem"))}
for r in top_rows
]
return {
"rat": rat.lower(),
"labels": {"ul": cfg["ul_label"], "dl": cfg["dl_label"]},
"summary": summary_out,
"problem_pie": problem_pie,
"by_system": group_by("制式", skip_unknown=True),
"by_station": group_by("站型", skip_unknown=True),
"by_freq": group_by("频段", limit=12, skip_unknown=True),
"ul_hist": util_hist(ul),
"dl_hist": util_hist(dl),
"top_cells": top_cells,
}
@router.get("/api/dashboard/cells")
def dashboard_cells(
rat: str = Query("4g"),
problem: str = Query(""),
keyword: str = Query(""),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=200),
):
cfg = _rat(rat)
db = _db()
if cfg["table"].lower() not in _existing_tables(db):
raise HTTPException(status_code=404, detail=f"结果表 {cfg['table']} 不存在,请先进行数据处理")
t = f"`{cfg['table']}`"
ul, dl, flow, users = f"`{cfg['ul']}`", f"`{cfg['dl']}`", f"`{FLOW}`", f"`{cfg['users']}`"
wheres = ["`高负荷问题` IS NOT NULL"]
if problem in PROBLEMS:
wheres = [f"`高负荷问题`='{problem}'"]
if keyword.strip():
kw = _esc(keyword.strip())
wheres.append(f"(`{cfg['id']}` LIKE '%{kw}%' OR `{cfg['name']}` LIKE '%{kw}%')")
where_sql = " WHERE " + " AND ".join(wheres)
total = _num(_rows(db, f"SELECT COUNT(*) c FROM {t}{where_sql}")[0].get("c"))
offset = (page - 1) * page_size
rows = _rows(db, (
f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') `system`,"
f" IFNULL(`带宽`,'') band, IFNULL(`站型`,'') station, IFNULL(`频段`,'') freq,"
f" ROUND({ul}*100,1) ul, ROUND({dl}*100,1) dl, ROUND({flow},2) flow, ROUND({users},0) users,"
f" IFNULL(`高负荷问题`,'') problem, IFNULL(`是否高负荷小区`,'否') is_high"
f" FROM {t}{where_sql}"
f" ORDER BY FIELD(`高负荷问题`,'高负荷','高流量预警','利用率预警'), {dl} DESC"
f" LIMIT {int(page_size)} OFFSET {int(offset)}"
))
items = [
{"id": str(r.get("id")), "name": str(r.get("name")), "system": str(r.get("system")),
"band": str(r.get("band")), "station": str(r.get("station")), "freq": str(r.get("freq")),
"ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1), "flow": _num(r.get("flow"), 2),
"users": _num(r.get("users")), "problem": str(r.get("problem")), "is_high": str(r.get("is_high"))}
for r in rows
]
return {"items": items, "total": total, "page": page, "page_size": page_size}
@router.get("/api/dashboard/export")
def dashboard_export(rat: str = Query("4g"), problem: str = Query(""), keyword: str = Query("")):
cfg = _rat(rat)
db = _db()
if cfg["table"].lower() not in _existing_tables(db):
raise HTTPException(status_code=404, detail=f"结果表 {cfg['table']} 不存在,请先进行数据处理")
t = f"`{cfg['table']}`"
ul, dl, flow, users = f"`{cfg['ul']}`", f"`{cfg['dl']}`", f"`{FLOW}`", f"`{cfg['users']}`"
wheres = ["`高负荷问题` IS NOT NULL"]
if problem in PROBLEMS:
wheres = [f"`高负荷问题`='{problem}'"]
if keyword.strip():
kw = _esc(keyword.strip())
wheres.append(f"(`{cfg['id']}` LIKE '%{kw}%' OR `{cfg['name']}` LIKE '%{kw}%')")
where_sql = " WHERE " + " AND ".join(wheres)
rows = _rows(db, (
f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') sys,"
f" IFNULL(`带宽`,'') band, IFNULL(`站型`,'') station, IFNULL(`频段`,'') freq,"
f" ROUND({ul}*100,1) ul, ROUND({dl}*100,1) dl, ROUND({flow},2) flow, ROUND({users},0) users,"
f" IFNULL(`高负荷问题`,'') problem, IFNULL(`优化建议`,'') suggestion"
f" FROM {t}{where_sql}"
f" ORDER BY FIELD(`高负荷问题`,'高负荷','高流量预警','利用率预警'), {dl} DESC"
))
CACHE_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"问题小区清单_{rat.lower()}_{timestamp}.csv"
filepath = CACHE_DIR / filename
header = [cfg["id"], "小区名称", "制式", "带宽", "站型", "频段",
"上行利用率(%)", "下行利用率(%)", "日均流量(GB)", "用户数", "高负荷问题", "优化建议"]
try:
with filepath.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(header)
for r in rows:
writer.writerow([
r.get("id"), r.get("name"), r.get("sys"), r.get("band"), r.get("station"),
r.get("freq"), r.get("ul"), r.get("dl"), r.get("flow"), r.get("users"),
r.get("problem"), r.get("suggestion"),
])
except Exception:
remove_file_safely(filepath)
raise
return FileResponse(
path=str(filepath), filename=filename, media_type="text/csv",
background=BackgroundTask(remove_file_safely, filepath),
)
@router.get("/api/dashboard/cell")
def dashboard_cell(rat: str = Query("4g"), id: str = Query(...)):
cfg = _rat(rat)
db = _db()
if cfg["table"].lower() not in _existing_tables(db):
raise HTTPException(status_code=404, detail=f"结果表 {cfg['table']} 不存在")
t = f"`{cfg['table']}`"
cell_id = _esc(id)
rows = _rows(db, f"SELECT * FROM {t} WHERE `{cfg['id']}`='{cell_id}' LIMIT 1")
if not rows:
raise HTTPException(status_code=404, detail="未找到该小区")
row = rows[0]
# 同扇区同 PLMN 的兄弟小区(用于详情页的均衡上下文)
sector = str(row.get("扇区") or "")
siblings: list[dict[str, Any]] = []
if sector:
plmn = _esc("-".join(str(id).split("-")[:2]))
sector_e = _esc(sector)
dl = f"`{cfg['dl']}`"
sib_rows = _rows(db, (
f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`带宽`,'') band,"
f" IFNULL(`频段`,'') freq, ROUND(`{cfg['ul']}`*100,1) ul, ROUND({dl}*100,1) dl,"
f" ROUND(`{FLOW}`,2) flow, IFNULL(`高负荷问题`,'正常') problem"
f" FROM {t} WHERE `扇区`='{sector_e}' AND SUBSTRING_INDEX(`{cfg['id']}`,'-',2)='{plmn}'"
f" AND `{cfg['id']}`<>'{cell_id}' ORDER BY {dl} DESC LIMIT 30"
))
siblings = [
{"id": str(r.get("id")), "name": str(r.get("name")), "band": str(r.get("band")),
"freq": str(r.get("freq")), "ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1),
"flow": _num(r.get("flow"), 2), "problem": str(r.get("problem"))}
for r in sib_rows
]
# 原始行转为字符串友好的 dict(数值保留,None→空)
detail = {str(k): (v if v is not None else "") for k, v in row.items()}
return {
"rat": rat.lower(),
"id": str(row.get(cfg["id"]) or id),
"name": str(row.get(cfg["name"]) or ""),
"labels": {"ul": cfg["ul_label"], "dl": cfg["dl_label"]},
"id_field": cfg["id"],
"name_field": cfg["name"],
"ul_field": cfg["ul"],
"dl_field": cfg["dl"],
"users_field": cfg["users"],
"flow_field": FLOW,
"detail": detail,
"siblings": siblings,
}