fix(saas): P1 审计修复 — 连接池断路器 + Worker重试 + XSS防护 + 状态机SQL解析器

P1 修复内容:
- F7: health handler 连接池容量检查 (80%阈值返回503 degraded)
- F9: SSE spawned task 并发限制 (Semaphore 16 permits)
- F10: Key Pool 单次 JOIN 查询优化 (消除 N+1)
- F12: CORS panic → 配置错误
- F14: 连接池使用率计算修正 (ratio = used*100/total)
- F15: SQL 迁移解析器替换为状态机 (支持 $$, DO $body$, 存储过程)
- Worker 重试机制: 失败任务通过 mpsc channel 重新入队
- DOMPurify XSS 防护 (PipelineResultPreview)
- Admin V2: ErrorBoundary + SWR全局配置 + 请求优化
This commit is contained in:
iven
2026-03-30 14:21:39 +08:00
parent bc8c77e7fe
commit ba2c6a6105
38 changed files with 490 additions and 236 deletions

View File

@@ -90,7 +90,7 @@ async fn run_migration_files(pool: &PgPool, dir: &std::path::Path) -> SaasResult
let filename = path.file_name().unwrap_or_default().to_string_lossy();
tracing::info!("Running migration: {}", filename);
let content = std::fs::read_to_string(path)?;
for stmt in content.split(';') {
for stmt in split_sql_statements(&content) {
let trimmed = stmt.trim();
if !trimmed.is_empty() && !trimmed.starts_with("--") {
sqlx::query(trimmed).execute(pool).await?;
@@ -100,6 +100,150 @@ async fn run_migration_files(pool: &PgPool, dir: &std::path::Path) -> SaasResult
Ok(())
}
/// 按语句分割 SQL 文件内容,正确处理:
/// - 单引号字符串 `'...'`
/// - 双引号标识符 `"..."`
/// - 美元符号引用字符串 `$$...$$` 和 `$tag$...$tag$`
/// - `--` 单行注释
/// - `/* ... */` 块注释
/// - `E'...'` 转义字符串
fn split_sql_statements(sql: &str) -> Vec<String> {
let mut statements = Vec::new();
let mut current = String::new();
let mut chars = sql.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\'' => {
// 单引号字符串
current.push(ch);
loop {
match chars.next() {
Some('\'') => {
current.push('\'');
// 检查是否为转义引号 ''
if chars.peek() == Some(&'\'') {
current.push(chars.next().unwrap());
} else {
break;
}
}
Some(c) => current.push(c),
None => break,
}
}
}
'"' => {
// 双引号标识符
current.push(ch);
loop {
match chars.next() {
Some('"') => {
current.push('"');
break;
}
Some(c) => current.push(c),
None => break,
}
}
}
'-' if chars.peek() == Some(&'-') => {
// 单行注释: 跳过直到行尾
chars.next(); // consume second '-'
while let Some(&c) = chars.peek() {
if c == '\n' {
chars.next();
current.push(c);
break;
}
chars.next();
}
}
'/' if chars.peek() == Some(&'*') => {
// 块注释: 跳过直到 */
chars.next(); // consume '*'
current.push_str("/*");
let mut prev = ' ';
loop {
match chars.next() {
Some('/') if prev == '*' => {
current.push('/');
break;
}
Some(c) => {
current.push(c);
prev = c;
}
None => break,
}
}
}
'$' => {
// 美元符号引用: $$ 或 $tag$ ... $tag$
current.push(ch);
// 读取 tag (字母数字和下划线)
let mut tag = String::new();
while let Some(&c) = chars.peek() {
if c == '$' || c.is_alphanumeric() || c == '_' {
if c == '$' {
chars.next();
current.push(c);
break;
}
chars.next();
tag.push(c);
current.push(c);
} else {
break;
}
}
// 如果 tag 为空,就是 $$ 格式
let end_marker = if tag.is_empty() {
"$$".to_string()
} else {
format!("${}$", tag)
};
// 读取直到遇到 end_marker
let mut buf = String::new();
loop {
match chars.next() {
Some(c) => {
current.push(c);
buf.push(c);
if buf.len() > end_marker.len() {
buf.remove(0);
}
if buf == end_marker {
break;
}
}
None => break,
}
}
}
';' => {
// 语句结束
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
statements.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
// 最后一条语句 (可能不以分号结尾)
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
statements.push(trimmed);
}
statements
}
/// Seed 角色数据
async fn seed_roles(pool: &PgPool) -> SaasResult<()> {
let now = chrono::Utc::now().to_rfc3339();