feat: 导出增加用户名 + 用户LD板数 (快照级两列, 新增 group=users 用户级导出)
- 快照级 CSV/JSON 追加 用户名/用户LD板数 列 (DomainDetail 逐域聚合, 板键=rack+cluster+ld) - 新增 group=users: 每快照x每用户一行, 含板号/Rack/域数/设计/T-Pod/运行时间 - 导出弹窗加导出粒度选项; README 同步
This commit is contained in:
@@ -1116,8 +1116,80 @@ EXPORT_FIELDS = [
|
||||
("disabled_domains", "已禁用域"),
|
||||
("not_exist_domains", "不存在域"),
|
||||
("active_jobs", "活跃作业"),
|
||||
("user_names", "用户名"),
|
||||
("user_ld_counts", "用户LD板数"),
|
||||
]
|
||||
|
||||
# 用户级导出 (group=users): 每条快照 × 每个占用 LD 板的用户一行
|
||||
EXPORT_USER_FIELDS = [
|
||||
("id", "快照 ID"),
|
||||
("timestamp_local", "时间(本地)"),
|
||||
("timestamp_utc", "时间(UTC)"),
|
||||
("source", "数据来源"),
|
||||
("owner", "用户名"),
|
||||
("ld_count", "用户LD板数"),
|
||||
("boards", "LD板号"),
|
||||
("racks", "Rack"),
|
||||
("domain_count", "占用域数"),
|
||||
("designs", "设计名"),
|
||||
("pods", "T-Pod"),
|
||||
("elap_times", "运行时间"),
|
||||
]
|
||||
|
||||
|
||||
def _collect_user_board_stats(snapshot_ids):
|
||||
"""
|
||||
按快照 × 用户聚合 LD 板使用情况 (基于 DomainDetail 逐域归属)。
|
||||
|
||||
返回 {snapshot_id: [user_stat, ...]}, user_stat 含:
|
||||
owner / ld_count / boards / racks / domain_count / designs / pods / elap_times
|
||||
列表按用户名排序; 短格式 (无逐域归属) 快照不会出现在结果里。
|
||||
LD 板唯一键 = (rack, cluster, ld_index) 三元组。
|
||||
"""
|
||||
result = {}
|
||||
if not snapshot_ids:
|
||||
return result
|
||||
q = (
|
||||
DomainDetail.query
|
||||
.filter(DomainDetail.snapshot_id.in_(snapshot_ids))
|
||||
.filter(~DomainDetail.owner.in_(("NONE", "", "—")))
|
||||
)
|
||||
for d in q.yield_per(2000):
|
||||
users = result.setdefault(d.snapshot_id, {})
|
||||
u = users.setdefault(d.owner, {
|
||||
"boards": set(), "racks": set(), "domain_count": 0,
|
||||
"designs": set(), "pods": set(), "elap_times": set(),
|
||||
})
|
||||
u["boards"].add((d.rack, d.cluster, d.ld_index))
|
||||
if d.rack is not None:
|
||||
u["racks"].add(d.rack)
|
||||
u["domain_count"] += 1
|
||||
if d.design and d.design not in ("--", "NONE"):
|
||||
u["designs"].add(d.design)
|
||||
# 过滤 "--" / "-- --" 等纯占位 T-Pod
|
||||
if d.t_pod and d.t_pod.strip("- ").strip():
|
||||
u["pods"].add(d.t_pod)
|
||||
if d.elap_time and d.elap_time not in ("--",):
|
||||
u["elap_times"].add(d.elap_time)
|
||||
|
||||
out = {}
|
||||
for sid, users in result.items():
|
||||
items = []
|
||||
for owner in sorted(users):
|
||||
u = users[owner]
|
||||
items.append({
|
||||
"owner": owner,
|
||||
"ld_count": len(u["boards"]),
|
||||
"boards": ", ".join(f"LD{ld}" for _, _, ld in sorted(u["boards"])),
|
||||
"racks": ", ".join(str(r) for r in sorted(u["racks"])),
|
||||
"domain_count": u["domain_count"],
|
||||
"designs": ", ".join(sorted(u["designs"])) or "-",
|
||||
"pods": ", ".join(sorted(u["pods"])) or "-",
|
||||
"elap_times": ", ".join(sorted(u["elap_times"])) or "-",
|
||||
})
|
||||
out[sid] = items
|
||||
return out
|
||||
|
||||
|
||||
def _build_export_query():
|
||||
"""
|
||||
@@ -1205,46 +1277,76 @@ def api_export():
|
||||
|
||||
Query:
|
||||
- format: csv | json (默认 csv)
|
||||
- group: snapshots (默认, 每条快照一行, 追加用户名+各用户 LD 板数两列)
|
||||
users (每条快照 × 每个占用 LD 板的用户一行: 用户名/LD板数/板号等)
|
||||
- days: 限制最近 N 天
|
||||
- since: 起始时间 (YYYY-MM-DD 或 ISO)
|
||||
- until: 截止时间
|
||||
- source: 模糊匹配 source 字段
|
||||
- limit: 最多 N 条 (默认 10000, 上限 100000)
|
||||
|
||||
CSV 文件名: palladium_snapshots_YYYYMMDD_HHMMSS.csv
|
||||
JSON 文件名: palladium_snapshots_YYYYMMDD_HHMMSS.json
|
||||
CSV 文件名:
|
||||
快照级: palladium_snapshots_YYYYMMDD_HHMMSS.csv
|
||||
用户级: palladium_users_YYYYMMDD_HHMMSS.csv
|
||||
"""
|
||||
group = request.args.get("group", "snapshots").strip().lower()
|
||||
if group not in ("snapshots", "users"):
|
||||
group = "snapshots"
|
||||
|
||||
q = _build_export_query()
|
||||
rows = [_snapshot_to_row(s) for s in q.all()]
|
||||
snaps = q.all()
|
||||
|
||||
# 按快照 × 用户聚合 LD 板使用 (详细版输出才有 DomainDetail; 短格式快照无用户数据)
|
||||
user_stats = _collect_user_board_stats([s.id for s in snaps])
|
||||
|
||||
rows = []
|
||||
for s in snaps:
|
||||
users = user_stats.get(s.id, [])
|
||||
row = _snapshot_to_row(s)
|
||||
row["user_names"] = ", ".join(u["owner"] for u in users) if users else "-"
|
||||
row["user_ld_counts"] = ", ".join(str(u["ld_count"]) for u in users) if users else "-"
|
||||
if group == "users":
|
||||
# 用户级: 每个用户一行, 带快照公共字段 + 该用户的 LD 板明细
|
||||
for u in users:
|
||||
r = dict(row)
|
||||
r.update(u)
|
||||
rows.append(r)
|
||||
else:
|
||||
# 快照级: JSON 附 users 明细数组 (CSV 只写 user_names / user_ld_counts 两列)
|
||||
row["users"] = users
|
||||
rows.append(row)
|
||||
|
||||
fmt = request.args.get("format", "csv").lower()
|
||||
ts_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
base_name = "palladium_users" if group == "users" else "palladium_snapshots"
|
||||
|
||||
if fmt == "json":
|
||||
body = {
|
||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||
"group": group,
|
||||
"count": len(rows),
|
||||
"rows": rows,
|
||||
}
|
||||
resp = jsonify(body)
|
||||
resp.headers["Content-Disposition"] = (
|
||||
f'attachment; filename="palladium_snapshots_{ts_str}.json"'
|
||||
f'attachment; filename="{base_name}_{ts_str}.json"'
|
||||
)
|
||||
return resp
|
||||
|
||||
# ── CSV (默认) ──
|
||||
fields = EXPORT_USER_FIELDS if group == "users" else EXPORT_FIELDS
|
||||
buf = io.StringIO()
|
||||
# 写 BOM 让 Excel 直接识别 UTF-8 (否则中文乱码)
|
||||
buf.write("\ufeff")
|
||||
writer = csv.writer(buf)
|
||||
writer.writerow([label for _, label in EXPORT_FIELDS])
|
||||
writer.writerow([label for _, label in fields])
|
||||
for r in rows:
|
||||
writer.writerow([r.get(key, "") for key, _ in EXPORT_FIELDS])
|
||||
writer.writerow([r.get(key, "") for key, _ in fields])
|
||||
csv_data = buf.getvalue()
|
||||
|
||||
resp = Response(csv_data, mimetype="text/csv")
|
||||
resp.headers["Content-Disposition"] = (
|
||||
f'attachment; filename="palladium_snapshots_{ts_str}.csv"'
|
||||
f'attachment; filename="{base_name}_{ts_str}.csv"'
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
Reference in New Issue
Block a user