修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账
This commit is contained in:
@@ -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_to(update_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-10,IPC 层 *10 缩放为 0-100)
|
||||
|
||||
Reference in New Issue
Block a user