feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署

This commit is contained in:
fmq
2026-07-28 10:31:57 +08:00
commit 4b4238d702
71 changed files with 163402 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
use super::AppState;
use axum::{
extract::{Multipart, State},
response::IntoResponse,
Json,
};
use common::models::{GridPointParams, ModelSummary, TaskReport, TaskStatus};
use serde_json::json;
use std::path::Path;
use tokio::fs;
use tracing::{info, warn};
use axum::http::StatusCode;
pub async fn claim_task(State(state): State<AppState>) -> impl IntoResponse {
match state.queue.pop_task().await {
Ok(Some(task)) => {
if let Err(e) = state.db.mark_grid_point_running(&task.point_name).await {
warn!("领用任务 {} 后同步变更为 running 状态遇到异常: {}. 后置 stale 定时自取检索引索将介入修复维护", task.task_id, e);
}
(StatusCode::OK, Json(json!({"status": "ok", "task": task}))).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!({"status": "empty", "task": null}))).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("领用任务失败: {}", e)})),
)
.into_response(),
}
}
pub async fn report_task(
State(state): State<AppState>,
mut multipart: Multipart,
) -> impl IntoResponse {
let mut report_json: Option<TaskReport> = None;
let mut seed_file_data: Option<Vec<u8>> = None;
while let Ok(Some(field)) = multipart.next_field().await {
let field_name = field.name().unwrap_or("").to_string();
if field_name == "report" {
if let Ok(bytes) = field.bytes().await {
if let Ok(report) = serde_json::from_slice::<TaskReport>(&bytes) {
report_json = Some(report);
}
}
} else if field_name == "seed_file" {
if let Ok(bytes) = field.bytes().await {
seed_file_data = Some(bytes.to_vec());
}
}
}
let report = match report_json {
Some(r) => r,
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "请求中缺少 report 字段"})),
)
.into_response();
}
};
let name = report.point_name.clone();
if name.is_empty() || name.starts_with('.') || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '+' || c == '@') {
warn!("拒绝可能包含路径穿越或特殊非常规编码号攻击的网格点名称请求: {}", name);
return (
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "非法的网格点名称参数"})),
)
.into_response();
}
let params = match extract_params(&report) {
Some(p) => p,
None => {
warn!("网格点 {} 汇报数据解析失败: 无法解析 params 或 summary_json", name);
return (
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "无法解析 params 或 summary_json"})),
)
.into_response();
}
};
// Record in DB
if let Err(e) = state.db.record_task_report(&report).await {
warn!("记录网格点 {} 任务结果到数据库失败: {}", name, e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("记录数据库失败: {}", e)})),
)
.into_response();
}
// Clean up task from task_queue table to prevent queue DB bloat
if let Err(e) = state.queue.remove_task(&report.task_id.to_string()).await {
tracing::warn!("从任务队列中清理已上报任务记录 {} 失败: {}", report.task_id, e);
}
// 采用原子写入模式保持 conv.json 与核心二进制数据完整落地后才揭晓真实文件名
let model_dir = Path::new(&state.results_dir).join(&name);
if fs::create_dir_all(&model_dir).await.is_ok() {
let conv_tmp = model_dir.join(format!("conv.json.{}.tmp", uuid::Uuid::new_v4().simple()));
let conv_path = model_dir.join("conv.json");
if fs::write(&conv_tmp, &report.summary_json).await.is_ok() {
let _ = fs::rename(&conv_tmp, &conv_path).await;
}
// Save seed file .7 using atomic temporary writing strategy
if report.converged && !report.atmosphere_has_nan {
if let Some(bytes) = seed_file_data {
let seed_tmp = model_dir.join(format!("{}.7.{}.tmp", name, uuid::Uuid::new_v4().simple()));
let seed_path = model_dir.join(format!("{}.7", name));
if fs::write(&seed_tmp, bytes).await.is_ok() {
if fs::rename(&seed_tmp, &seed_path).await.is_ok() {
info!("成功保持原子写入落地并保存网格点 {} 的收敛种子文件: {}", name, seed_path.display());
let _ = state
.db
.insert_seed(&params, &seed_path.to_string_lossy())
.await;
}
}
}
}
}
if report.status == TaskStatus::Failed || report.status == TaskStatus::Timeout || report.atmosphere_has_nan {
// Task did not succeed -> check if seed_step fallback should be triggered
info!("网格点 {} 计算未成功完成,检查种子回退机制...", name);
if let Err(e) = state.scheduler.trigger_seed_step_fallback(&params).await {
warn!("网格点 {} 触发种子回退机制失败: {}", name, e);
}
}
(StatusCode::OK, Json(json!({"status": "ok", "message": "上报成功"}))).into_response()
}
fn extract_params(report: &TaskReport) -> Option<GridPointParams> {
if let Some(ref p) = report.params {
return Some(p.clone());
}
serde_json::from_str::<ModelSummary>(&report.summary_json)
.ok()
.map(|summary| summary.params)
}