feat: 导出增加用户名 + 用户LD板数 (快照级两列, 新增 group=users 用户级导出)
- 快照级 CSV/JSON 追加 用户名/用户LD板数 列 (DomainDetail 逐域聚合, 板键=rack+cluster+ld) - 新增 group=users: 每快照x每用户一行, 含板号/Rack/域数/设计/T-Pod/运行时间 - 导出弹窗加导出粒度选项; README 同步
This commit is contained in:
@@ -113,11 +113,13 @@ curl -X POST http://<服务器>:5100/api/collect \
|
||||
|
||||
### 导出历史数据
|
||||
|
||||
`/api/export` 端点支持 CSV (Excel 友好, 带 UTF-8 BOM) 和 JSON 两种格式,可按时间范围和来源过滤。
|
||||
`/api/export` 端点支持 CSV (Excel 友好, 带 UTF-8 BOM) 和 JSON 两种格式,可按时间范围和来源过滤,并支持两种导出粒度。
|
||||
|
||||
```
|
||||
GET /api/export?format=csv # 默认: CSV 全部
|
||||
GET /api/export?format=json # JSON 全部
|
||||
GET /api/export?format=csv # 默认: 快照级 CSV 全部
|
||||
GET /api/export?format=json # 快照级 JSON 全部
|
||||
GET /api/export?group=users # 用户级: 每条快照 × 每个用户一行
|
||||
GET /api/export?group=users&format=json # 用户级 JSON
|
||||
GET /api/export?days=7 # 最近 7 天
|
||||
GET /api/export?since=2026-07-01&until=2026-07-31 # 自定义范围
|
||||
GET /api/export?source=collect.py # 按来源模糊匹配
|
||||
@@ -126,14 +128,23 @@ GET /api/export?format=json&days=30&source=scmp03 # 组合
|
||||
|
||||
参数说明:
|
||||
- `format`: `csv` (默认) 或 `json`
|
||||
- `group`: `snapshots` (默认) 或 `users` — 见下方两种粒度
|
||||
- `days`: 限制最近 N 天 (0/缺省 = 全部)
|
||||
- `since` / `until`: `YYYY-MM-DD` 或 ISO 时间戳
|
||||
- `source`: 模糊匹配 `source` 字段 (如 `collect.py`、`scmp03`)
|
||||
- `limit`: 最多返回条数 (默认 10000, 上限 100000)
|
||||
|
||||
CSV 文件名: `palladium_snapshots_YYYYMMDD_HHMMSS.csv`,包含 19 列(时间本地+UTC、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率、作业数等)。
|
||||
**粒度一: 按快照 (`group=snapshots`, 默认)** — 每条快照一行,CSV 文件名 `palladium_snapshots_YYYYMMDD_HHMMSS.csv`,21 列(时间本地+UTC、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率、作业数等),末尾追加:
|
||||
- `用户名`: 该快照下占用 LD 板的所有用户 (逗号分隔,如 `pzuser102, pzuser103`)
|
||||
- `用户LD板数`: 与用户名顺序对应的各用户占用 LD 板数 (如 `6, 5`)
|
||||
|
||||
Web 界面入口:历史记录页右上角「📤 导出」按钮。
|
||||
JSON 格式每行额外附 `users` 数组 (含每用户板号/域数/设计名/运行时间明细)。
|
||||
|
||||
**粒度二: 按用户 (`group=users`)** — 每条快照 × 每个占用 LD 板的用户一行,CSV 文件名 `palladium_users_YYYYMMDD_HHMMSS.csv`,12 列: 快照 ID、时间(本地/UTC)、数据来源、**用户名**、**用户LD板数**、LD 板号、Rack、占用域数、设计名、T-Pod、运行时间。
|
||||
|
||||
> 用户名/LD 板数来自详细版 `test_server` 输出的逐域归属数据 (DomainDetail);短格式 (`test_server -short`) 快照无逐域归属,快照级导出用户列显示 `-`,用户级导出中不出现该快照。
|
||||
|
||||
Web 界面入口:历史记录页右上角「📤 导出」按钮,弹窗内可选导出粒度。
|
||||
|
||||
## 解析器支持的输出格式
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+11
-1
@@ -110,6 +110,7 @@ function closeExportModal() {
|
||||
}
|
||||
function doExport() {
|
||||
const fmt = document.getElementById('expFormat').value;
|
||||
const group = document.getElementById('expGroup').value;
|
||||
const range = document.getElementById('expRange').value;
|
||||
const since = document.getElementById('expSince').value;
|
||||
const until = document.getElementById('expUntil').value;
|
||||
@@ -117,6 +118,7 @@ function doExport() {
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('format', fmt);
|
||||
params.set('group', group);
|
||||
if (range !== 'all') params.set('days', range);
|
||||
if (range === 'custom') {
|
||||
if (since) params.set('since', since);
|
||||
@@ -144,6 +146,13 @@ function toggleRangeFields() {
|
||||
<button class="modal-close" onclick="closeExportModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>导出粒度</label>
|
||||
<select id="expGroup" class="form-control">
|
||||
<option value="snapshots" selected>按快照 — 每条快照一行 (含用户名 + 各用户 LD 板数)</option>
|
||||
<option value="users">按用户 — 每条快照 × 每个用户一行 (用户名 / LD 板数 / 板号)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>格式</label>
|
||||
<select id="expFormat" class="form-control">
|
||||
@@ -179,7 +188,8 @@ function toggleRangeFields() {
|
||||
</div>
|
||||
<p class="modal-hint">
|
||||
导出上限 10 万条; 超过会自动截断。<br>
|
||||
CSV 默认包含 19 列: 时间(本地+UTC)、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率(1 位小数)、作业数等。
|
||||
按快照: 21 列 — 时间(本地+UTC)、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率、作业数、用户名、各用户 LD 板数 (多用户逗号分隔, 顺序对应)。<br>
|
||||
按用户: 12 列 — 快照时间/来源 + 用户名、该用户 LD 板数、板号、Rack、占用域数、设计名、T-Pod、运行时间。用户名/LD 板数来自详细版 test_server 逐域归属数据。
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
Reference in New Issue
Block a user