修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账
This commit is contained in:
@@ -43,10 +43,17 @@ pub struct ProjectQuery {
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
/// order_by 白名单(独立于 update_field 白名单,对齐 ideas 的 validate_idea_order_by 模式)。
|
||||
///
|
||||
/// BE-CMD-3:projects update_field 白名单已剔除 id/created_at(主键与创建时间不可经通用
|
||||
/// update_field 改写),但 `created_at` 作为**排序字段**仍合法——故排序白名单单独定义,
|
||||
/// 不依赖 update_field 白名单(否则 order_by=created_at 会被误拒,破坏 list_by_query 默认排序)。
|
||||
const PROJECT_ORDER_BY_ALLOWED: &[&str] = &["created_at", "updated_at", "name", "status"];
|
||||
|
||||
/// 解析 order_by 入参为 "col DIR" SQL 片段(列名走白名单校验防注入)。
|
||||
///
|
||||
/// 接受 "col" / "col asc" / "col desc"(DIR 大小写不敏感)。col 走 `validate_column_name`
|
||||
/// 校验(列名不可参数化,必须拼字符串,白名单是唯一防注入手段,对齐 impl_repo! 宏)。
|
||||
/// 接受 "col" / "col asc" / "col desc"(DIR 大小写不敏感)。col 走 `PROJECT_ORDER_BY_ALLOWED`
|
||||
/// 白名单校验(列名不可参数化,必须拼字符串,白名单是唯一防注入手段,对齐 impl_repo! 宏)。
|
||||
/// 非法列名返回 Err;合法但无 DIR 默认 DESC(与 list_active 一致)。
|
||||
fn build_order_clause(order_by: Option<&str>) -> Result<String> {
|
||||
let Some(raw) = order_by else {
|
||||
@@ -60,8 +67,13 @@ fn build_order_clause(order_by: Option<&str>) -> Result<String> {
|
||||
let parts: Vec<&str> = raw.split_whitespace().collect();
|
||||
let col = parts[0];
|
||||
let dir = parts.get(1).map(|s| s.to_ascii_uppercase());
|
||||
// 白名单校验列名(防 SQL 注入:列名拼字符串前必须校验)。
|
||||
validate_column_name(col, "projects")?;
|
||||
// 排序白名单校验列名(防 SQL 注入:列名拼字符串前必须校验;独立于 update_field 白名单)。
|
||||
if !PROJECT_ORDER_BY_ALLOWED.contains(&col) {
|
||||
return Err(df_types::error::Error::Storage(format!(
|
||||
"非法 order_by 字段名: {col},合法值: {:?}",
|
||||
PROJECT_ORDER_BY_ALLOWED
|
||||
)));
|
||||
}
|
||||
match dir.as_deref() {
|
||||
None | Some("DESC") => Ok(format!("{col} DESC")),
|
||||
Some("ASC") => Ok(format!("{col} ASC")),
|
||||
@@ -449,6 +461,32 @@ impl ProjectRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 更新单字段,**仅作用于未软删记录**(`WHERE id AND deleted_at IS NULL`)。
|
||||
///
|
||||
/// LW-6(BE-CMD-5):通用 [`update_field`] 不过滤软删(回收站项目仍可改字段),
|
||||
/// 本方法收口软删防护,供命令层 `update_project` 使用——软删项目(回收站)返回 `false`,
|
||||
/// 调用方据此报「已删除」。字段名走同款 [`validate_column_name`] 白名单防注入。
|
||||
pub async fn update_field_active(&self, id: &str, field: &str, value: &str) -> Result<bool> {
|
||||
validate_column_name(field, "projects")?;
|
||||
let conn = self.conn.clone();
|
||||
let sql = format!(
|
||||
"UPDATE projects SET {} = ?1, updated_at = ?2 WHERE id = ?3 AND deleted_at IS NULL",
|
||||
field
|
||||
);
|
||||
let id = id.to_owned();
|
||||
let value = value.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(&sql, params![value, now, id])
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 彻底删除:事务级联删全部关联子表→projects(不可恢复)
|
||||
///
|
||||
/// SQLite 已开 PRAGMA foreign_keys=ON 但表无 ON DELETE CASCADE,ALTER 改不了 FK 约束,
|
||||
|
||||
Reference in New Issue
Block a user