修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:58 +08:00
parent 8bc5380ecb
commit fbd8fae44b
30 changed files with 829 additions and 138 deletions
+50 -17
View File
@@ -121,11 +121,37 @@ pub async fn update_idea(
field: String,
value: String,
) -> Result<bool, String> {
state
// BE-CMD-4:status 值合法性校验(防任意值进库)+ 拒绝经 update_field 直达 promoted
// (半立项:绕过 promote_idea 不建项目不写 promoted_to,须走立项流程)。
if field == "status" {
if IdeaStatus::from_db_str(value.trim()).is_none() {
return Err(format!(
"非法 status 值 {:?},合法值: draft/pending_review/approved/rejected/promoted/archived",
value
));
}
if value.trim() == "promoted" {
return Err(
"status 不能直接置为 promoted:立项须走 promote_idea(会创建项目并回写 promoted_to)"
.to_string(),
);
}
}
// BE-CMD-4(含 BE-CMD-23):related_ids/scores 是 JSON 字段,补合法性校验(防脏 JSON 落库)。
if field == "related_ids" || field == "scores" {
serde_json::from_str::<serde_json::Value>(&value)
.map_err(|e| format!("{field} 不是合法 JSON: {e}"))?;
}
// LW-6(BE-CMD-5):update_field_active 过滤软删(deleted_at IS NULL),回收站灵感不可改字段。
let updated = state
.ideas
.update_field(&id, &field, &value)
.update_field_active(&id, &field, &value)
.await
.map_err(err_str)
.map_err(err_str)?;
if !updated {
return Err(format!("灵感 ID {id} 不存在或已删除"));
}
Ok(true)
}
/// 删除灵感(软删 → 回收站,可恢复)。对标 delete_task(SET deleted_at=now)。
@@ -204,23 +230,21 @@ pub async fn promote_idea(
.await
.map_err(err_str)?;
// 回写灵感status=promoted + promoted_toupdate_full 单事务覆盖可变字段)
// 补偿删除:第二步失败时回滚第一步已建的 project,保证最终一致性(非原子,但防项目存留而
// 灵感状态未变的数据不一致)。Repository 方法各自持锁不支持跨 repo 共享事务对象,故选补偿
// 删除而非真事务(改动最小,工程投入产出比最高)
let updated = IdeaRecord {
status: IdeaStatus::Promoted,
promoted_to: Some(project_id.clone()),
updated_at: now,
..record
};
if let Err(e) = state.ideas.update_full(&updated).await {
// 回写失败:补偿软删已建项目(进回收站,可恢复),避免悬空项目(idea.promoted_to 仍空,可重试立项)
tracing::error!("灵感 {id} 回写失败,补偿删除已建项目 {project_id}: {e}");
// LW-8(BE-CMD-7):CAS 回写灵感(status=promoted + promoted_to,WHERE id AND promoted_to IS NULL)。
// 双击/并发两次 promote 都读到 promoted_to=None → 各自建项目;本方法原子「立项认领」,
// 仅首个 affected=1 成功,第二个 affected=0 → 判定「已立项」并补偿软删刚建项目(回滚)。
// 替代原 update_full(无条件覆盖):并发下两个项目都保留、灵感只指向一个,留悬空项目
if !state
.ideas
.claim_promotion(&id, &project_id)
.await
.map_err(err_str)?
{
tracing::warn!("灵感 {id} 已被并发立项,回滚本次新建项目 {project_id}");
if let Err(del_err) = state.projects.soft_delete(&project_id).await {
tracing::error!("补偿软删项目 {project_id} 也失败(需人工清理): {del_err}");
}
return Err(format!("灵感立项回写失败(已回滚项目创建): {}", e));
return Err(format!("灵感 {id} 已立项(并发双击),本次立项已回滚"));
}
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):idea_promoted 事件。best-effort 不阻断。
@@ -362,6 +386,15 @@ async fn evaluate_one(
engine: &df_ideas::adversarial::AdversarialEngine,
) -> Result<IdeaRecord, String> {
let id = record.id.clone();
// LW-7(BE-CMD-6):终态灵感不可再评估(防无条件覆盖 pending_review 打回终态)。
// promoted(已立项)/archived(已归档)是终态,评估会把 status 覆盖回 pending_review,
// 破坏「已立项/已归档不可回退」语义。软删灵感由 evaluate_idea/batch 的存在性检查已过滤。
if matches!(record.status, IdeaStatus::Promoted | IdeaStatus::Archived) {
return Err(format!(
"灵感 {id} 已是终态({}),不可再评估",
record.status.as_str()
));
}
let idea = record_to_idea(&record);
// 多维评分(0-10IPC 层 *10 缩放为 0-100