@@ -28,6 +28,8 @@ use crate::video_profile_domain::DomainKind;
2828
2929/// 一周秒数(草案 due_at 换算:第 N 周 = created_at + N*7d)。
3030const WEEK_SECS : i64 = 7 * 86_400 ;
31+ /// 访谈答案文本上限(防御超大 payload;chips/填空兜底的答案均为短文本)。
32+ const INTENT_FIELD_MAX : usize = 200 ;
3133
3234/// 新建目标入参(前端访谈结果全量提交;serde camelCase 契约)。
3335#[ derive( Debug , Clone , Deserialize ) ]
@@ -273,13 +275,14 @@ pub(crate) fn create_goal_inner(db: &Db, input: &GoalCreateInput) -> Result<Goal
273275 // 访谈校验:tier 与 scenario 同缺=快速模式;缺一=必答拦截(第 1/3 问必答)
274276 let tier_explicit = input. tier . as_deref ( ) . map ( str:: trim) . filter ( |s| !s. is_empty ( ) ) ;
275277 let tier = tier_explicit. unwrap_or ( TIER_DEFAULT ) ;
276- let scenario = trimmed ( input. scenario . as_deref ( ) ) ;
278+ let scenario = bounded ( input. scenario . as_deref ( ) ) ;
277279 if tier_explicit. is_some ( ) && scenario. is_none ( ) {
278280 return Err ( "第 1 问「学会以后想用它做什么?」必答" . to_string ( ) ) ;
279281 }
280282 let domain = parse_domain ( input. domain_tag . as_deref ( ) ) ?;
281283 let now = unix_seconds ( ) ;
282- let criteria = derive_criteria ( tier, input. non_scope . as_deref ( ) ) ;
284+ let non_scope = bounded ( input. non_scope . as_deref ( ) ) ;
285+ let criteria = derive_criteria ( tier, non_scope. as_deref ( ) ) ;
283286 // 边界校验:group_ids 逐组存在;草案 due_weeks 有界防溢出
284287 for gid in & input. group_ids {
285288 if db. get_group ( * gid) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
@@ -338,7 +341,10 @@ pub(crate) fn list_goals_inner(db: &Db) -> Result<Vec<GoalCardView>, String> {
338341
339342pub ( crate ) fn get_goal_detail_inner ( db : & Db , id : i64 ) -> Result < GoalDetailView , String > {
340343 require_goal ( db, id) ?;
341- let goal = db. get_goal ( id) . map_err ( |e| e. to_string ( ) ) ?. unwrap ( ) ;
344+ let goal = db
345+ . get_goal ( id)
346+ . map_err ( |e| e. to_string ( ) ) ?
347+ . ok_or_else ( || format ! ( "目标不存在: {}" , id) ) ?;
342348 let milestones = db. list_milestones ( id) . map_err ( |e| e. to_string ( ) ) ?;
343349 let group_ids = db. list_goal_group_ids ( id) . map_err ( |e| e. to_string ( ) ) ?;
344350 let mut groups = Vec :: new ( ) ;
@@ -378,7 +384,10 @@ pub(crate) fn get_goal_detail_inner(db: &Db, id: i64) -> Result<GoalDetailView,
378384
379385pub ( crate ) fn get_goal_progress_inner ( db : & Db , id : i64 ) -> Result < GoalProgressView , String > {
380386 require_goal ( db, id) ?;
381- let goal = db. get_goal ( id) . map_err ( |e| e. to_string ( ) ) ?. unwrap ( ) ;
387+ let goal = db
388+ . get_goal ( id)
389+ . map_err ( |e| e. to_string ( ) ) ?
390+ . ok_or_else ( || format ! ( "目标不存在: {}" , id) ) ?;
382391 let signals = collect_signals ( db, id) ?;
383392 let progress = build_report ( & signals) ;
384393 let statement = progress_statement ( & progress) ;
@@ -400,14 +409,22 @@ pub(crate) fn update_goal_inner(
400409 horizon : Option < String > ,
401410) -> Result < bool , String > {
402411 require_goal ( db, id) ?;
403- let goal = db. get_goal ( id) . map_err ( |e| e. to_string ( ) ) ?. unwrap ( ) ;
412+ let goal = db
413+ . get_goal ( id)
414+ . map_err ( |e| e. to_string ( ) ) ?
415+ . ok_or_else ( || format ! ( "目标不存在: {}" , id) ) ?;
404416 let domain = parse_domain ( domain_tag. as_deref ( ) ) ?;
405417 let now = unix_seconds ( ) ;
418+ // horizon 未填(None/空白)= 不改变时限锚点——防「改名顺手抹掉无期限锚点」
419+ let horizon_end = match horizon. as_deref ( ) . map ( str:: trim) . filter ( |s| !s. is_empty ( ) ) {
420+ Some ( _) => horizon_end_secs ( horizon. as_deref ( ) , now) ,
421+ None => goal. horizon_end ,
422+ } ;
406423 db. update_goal_core (
407424 id,
408425 & normalize_title ( name. to_string ( ) , "未命名目标" ) ,
409426 domain. as_deref ( ) ,
410- horizon_end_secs ( horizon . as_deref ( ) , now ) ,
427+ horizon_end ,
411428 & goal. success_criteria_json ,
412429 & goal. intent_json ,
413430 )
@@ -416,22 +433,28 @@ pub(crate) fn update_goal_inner(
416433
417434pub ( crate ) fn update_goal_interview_inner ( db : & Db , id : i64 , input : & GoalCreateInput ) -> Result < bool , String > {
418435 require_goal ( db, id) ?;
419- let goal = db. get_goal ( id) . map_err ( |e| e. to_string ( ) ) ?. unwrap ( ) ;
420- // 配方重推(答案可回溯编辑——判据/意图整体重写;名字与绑组不变)
436+ let goal = db
437+ . get_goal ( id)
438+ . map_err ( |e| e. to_string ( ) ) ?
439+ . ok_or_else ( || format ! ( "目标不存在: {}" , id) ) ?;
440+ // 配方重推(答案可回溯编辑——判据/意图整体重写;绑组不变;名称随对话窗口
441+ // 一并生效——空名回退旧名,防「编辑态改名不落地」契约陷阱)
421442 let tier_explicit = input. tier . as_deref ( ) . map ( str:: trim) . filter ( |s| !s. is_empty ( ) ) ;
422443 let tier = tier_explicit. unwrap_or ( TIER_DEFAULT ) ;
423- let scenario = trimmed ( input. scenario . as_deref ( ) ) ;
444+ let scenario = bounded ( input. scenario . as_deref ( ) ) ;
424445 if tier_explicit. is_some ( ) && scenario. is_none ( ) {
425446 return Err ( "第 1 问「学会以后想用它做什么?」必答" . to_string ( ) ) ;
426447 }
427- let criteria = derive_criteria ( tier, input. non_scope . as_deref ( ) ) ;
448+ let non_scope = bounded ( input. non_scope . as_deref ( ) ) ;
449+ let criteria = derive_criteria ( tier, non_scope. as_deref ( ) ) ;
428450 let intent = build_intent ( input) ;
451+ let name = normalize_title ( input. name . clone ( ) , & goal. name ) ;
429452 let criteria_json = serde_json:: to_string ( & criteria) . map_err ( |e| e. to_string ( ) ) ?;
430453 let intent_json = serde_json:: to_string ( & intent) . map_err ( |e| e. to_string ( ) ) ?;
431454 let now = unix_seconds ( ) ;
432455 db. update_goal_core (
433456 id,
434- & goal . name ,
457+ & name,
435458 goal. domain_tag . as_deref ( ) ,
436459 horizon_end_secs ( input. horizon . as_deref ( ) , now) ,
437460 & criteria_json,
@@ -451,7 +474,10 @@ pub(crate) fn delete_goal_inner(db: &Db, id: i64) -> Result<bool, String> {
451474
452475pub ( crate ) fn update_goal_status_inner ( db : & Db , id : i64 , status : & str ) -> Result < bool , String > {
453476 require_goal ( db, id) ?;
454- let goal = db. get_goal ( id) . map_err ( |e| e. to_string ( ) ) ?. unwrap ( ) ;
477+ let goal = db
478+ . get_goal ( id)
479+ . map_err ( |e| e. to_string ( ) ) ?
480+ . ok_or_else ( || format ! ( "目标不存在: {}" , id) ) ?;
455481 // M1 只开放 active⇄paused(状态机守卫总入口;放弃/毕业随 M2 流程)
456482 if status != "active" && status != "paused" {
457483 return Err ( format ! ( "v0.18.0 M1 仅支持暂停/恢复(收到 {})" , status) ) ;
@@ -496,11 +522,11 @@ pub(crate) fn set_goal_milestone_status_inner(db: &Db, id: i64, status: &str) ->
496522 return Err ( format ! ( "不支持的里程碑状态: {}(支持: {})" , status, MILESTONE_STATUSES . join( "/" ) ) ) ;
497523 }
498524 // 取旧状态(幂等埋点判据:仅「未完成 → 完成」的转变记 goal_milestone_done)
499- let prev = db. get_milestone ( id ) . map_err ( |e| e . to_string ( ) ) ? ;
500- if prev . is_none ( ) {
501- return Err ( format ! ( "里程碑不存在: {}" , id ) ) ;
502- }
503- let was_done = prev. unwrap ( ) . status == MILESTONE_DONE ;
525+ let prev = db
526+ . get_milestone ( id )
527+ . map_err ( |e| e . to_string ( ) ) ?
528+ . ok_or_else ( || format ! ( "里程碑不存在: {}" , id ) ) ? ;
529+ let was_done = prev. status == MILESTONE_DONE ;
504530 let ok = db. set_milestone_status ( id, status) . map_err ( |e| e. to_string ( ) ) ?;
505531 if !ok {
506532 return Err ( format ! ( "里程碑不存在: {}" , id) ) ;
@@ -540,17 +566,22 @@ fn trimmed(s: Option<&str>) -> Option<String> {
540566 s. map ( str:: trim) . filter ( |v| !v. is_empty ( ) ) . map ( str:: to_string)
541567}
542568
543- /// 访谈答案 → GoalIntent(全部可选字段过 trimmed 归一)。
569+ /// 访谈答案:空白归一 + 长度截断(防超大 payload 入库;truncate 保留前 200 字)。
570+ fn bounded ( s : Option < & str > ) -> Option < String > {
571+ trimmed ( s) . map ( |v| v. chars ( ) . take ( INTENT_FIELD_MAX ) . collect ( ) )
572+ }
573+
574+ /// 访谈答案 → GoalIntent(全部可选字段过 bounded 归一)。
544575fn build_intent ( input : & GoalCreateInput ) -> GoalIntent {
545576 GoalIntent {
546- scenario : trimmed ( input. scenario . as_deref ( ) ) ,
547- level : trimmed ( input. level . as_deref ( ) ) ,
548- driver : trimmed ( input. driver . as_deref ( ) ) ,
549- criteria_statement : trimmed ( input. criteria_statement . as_deref ( ) ) ,
550- horizon : trimmed ( input. horizon . as_deref ( ) ) ,
551- non_scope : trimmed ( input. non_scope . as_deref ( ) ) ,
552- weekly_commitment : trimmed ( input. weekly_commitment . as_deref ( ) ) ,
553- obstacles : trimmed ( input. obstacles . as_deref ( ) ) ,
577+ scenario : bounded ( input. scenario . as_deref ( ) ) ,
578+ level : bounded ( input. level . as_deref ( ) ) ,
579+ driver : bounded ( input. driver . as_deref ( ) ) ,
580+ criteria_statement : bounded ( input. criteria_statement . as_deref ( ) ) ,
581+ horizon : bounded ( input. horizon . as_deref ( ) ) ,
582+ non_scope : bounded ( input. non_scope . as_deref ( ) ) ,
583+ weekly_commitment : bounded ( input. weekly_commitment . as_deref ( ) ) ,
584+ obstacles : bounded ( input. obstacles . as_deref ( ) ) ,
554585 }
555586}
556587
0 commit comments