feat: 导出增加用户名 + 用户LD板数 (快照级两列, 新增 group=users 用户级导出)

- 快照级 CSV/JSON 追加 用户名/用户LD板数 列 (DomainDetail 逐域聚合, 板键=rack+cluster+ld)
- 新增 group=users: 每快照x每用户一行, 含板号/Rack/域数/设计/T-Pod/运行时间
- 导出弹窗加导出粒度选项; README 同步
This commit is contained in:
Your Name
2026-09-01 16:37:31 +08:00
parent 5c42d802c0
commit 8ca1728ec5
3 changed files with 136 additions and 13 deletions
+109 -7
View File
@@ -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