@@ -17,9 +17,7 @@ use crate::db::{unix_seconds, Db};
1717use crate :: goal_interview:: { derive_criteria, horizon_end_secs} ;
1818use crate :: goal_progress:: GoalSignals ;
1919use crate :: goal_schema:: {
20- Goal , GoalIntent , GoalMilestone , NewGoal , NewMilestone , SuccessCriteria , CRITERIA_GROUP_SETTLED ,
21- CRITERIA_MANUAL , MILESTONE_DONE , MILESTONE_IN_PROGRESS , MILESTONE_PENDING , MILESTONE_SKIPPED ,
22- TIER_DEFAULT ,
20+ Goal , GoalIntent , NewGoal , NewMilestone , SuccessCriteria , CRITERIA_MANUAL , TIER_DEFAULT ,
2321} ;
2422use crate :: video_profile_domain:: DomainKind ;
2523
@@ -28,6 +26,11 @@ use crate::video_profile_domain::DomainKind;
2826#[ path = "commands_goals_views.rs" ]
2927pub ( crate ) mod views;
3028
29+ /// 里程碑与目标↔组绑定写入域(7 条命令 + 4 个 inner + 2 个白名单常量;6 emit)。
30+ /// `pub(crate)`:同 `views`(注册路径三段式)且测试文件按 `commands_goals::milestones::x` 导入。
31+ #[ path = "commands_goals_milestones.rs" ]
32+ pub ( crate ) mod milestones;
33+
3134/// 一周秒数(草案 due_at 换算:第 N 周 = created_at + N*7d)。
3235const WEEK_SECS : i64 = 7 * 86_400 ;
3336/// 访谈答案文本上限(防御超大 payload;chips/填空兜底的答案均为短文本)。
@@ -140,105 +143,8 @@ pub fn update_goal_status(state: State<'_, AppState>, id: i64, status: String) -
140143 Ok ( ok)
141144}
142145
143- /// 里程碑草案建议(宣言页预填;前端薄——单一事实源在 goal_interview.rs)。
144- #[ tauri:: command]
145- pub fn suggest_goal_milestones (
146- _state : State < ' _ , AppState > ,
147- level : Option < String > ,
148- weekly_commitment : Option < String > ,
149- ) -> Vec < crate :: goal_schema:: MilestoneDraft > {
150- crate :: goal_interview:: suggest_milestones ( level. as_deref ( ) , weekly_commitment. as_deref ( ) )
151- }
152-
153- /// 里程碑增删改与状态流转(status → done 记 goal_milestone_done)。
154- #[ tauri:: command]
155- pub fn add_goal_milestone (
156- state : State < ' _ , AppState > ,
157- goal_id : i64 ,
158- title : String ,
159- due_at : Option < i64 > ,
160- criteria_type : Option < String > ,
161- ref_group_id : Option < i64 > ,
162- ) -> Result < GoalMilestone , String > {
163- let m = add_goal_milestone_inner ( & state. db , goal_id, & title, due_at, criteria_type, ref_group_id) ?;
164- // REQ-278:里程碑新增 → 广播 goals 域
165- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
166- Ok ( m)
167- }
168-
169- #[ tauri:: command]
170- pub fn update_goal_milestone (
171- state : State < ' _ , AppState > ,
172- id : i64 ,
173- title : String ,
174- due_at : Option < i64 > ,
175- ) -> Result < bool , String > {
176- if id <= 0 {
177- return Err ( "无效的里程碑 id" . to_string ( ) ) ;
178- }
179- let title = normalize_title ( title, "未命名里程碑" ) ;
180- let ok = state. db . update_milestone ( id, & title, due_at) . map_err ( |e| e. to_string ( ) ) ?;
181- // REQ-278:里程碑更新 → 广播 goals 域
182- if ok {
183- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
184- }
185- Ok ( ok)
186- }
187-
188- #[ tauri:: command]
189- pub fn delete_goal_milestone ( state : State < ' _ , AppState > , id : i64 ) -> Result < bool , String > {
190- if id <= 0 {
191- return Err ( "无效的里程碑 id" . to_string ( ) ) ;
192- }
193- let ok = state. db . delete_milestone ( id) . map_err ( |e| e. to_string ( ) ) ?;
194- // REQ-278:里程碑删除 → 广播 goals 域
195- if ok {
196- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
197- }
198- Ok ( ok)
199- }
200-
201- #[ tauri:: command]
202- pub fn set_goal_milestone_status ( state : State < ' _ , AppState > , id : i64 , status : String ) -> Result < bool , String > {
203- let ok = set_goal_milestone_status_inner ( & state. db , id, & status) ?;
204- // REQ-278:里程碑状态流转 → 广播 goals 域
205- if ok {
206- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
207- }
208- Ok ( ok)
209- }
210-
211- /// 绑定/解绑组(N:M——一组可服务多目标;组仍是唯一容器)。
212- #[ tauri:: command]
213- pub fn bind_goal_group ( state : State < ' _ , AppState > , goal_id : i64 , group_id : i64 ) -> Result < bool , String > {
214- let ok = bind_goal_group_inner ( & state. db , goal_id, group_id) ?;
215- // REQ-278:绑组(N:M)→ 广播 goals 域(组侧服务标随目标页刷新)
216- if ok {
217- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
218- }
219- Ok ( ok)
220- }
221-
222- #[ tauri:: command]
223- pub fn unbind_goal_group ( state : State < ' _ , AppState > , goal_id : i64 , group_id : i64 ) -> Result < bool , String > {
224- let ok = unbind_goal_group_inner ( & state. db , goal_id, group_id) ?;
225- // REQ-278:解绑组 → 广播 goals 域
226- if ok {
227- crate :: notify:: emit_changed ( & state. app , crate :: notify:: DataDomain :: Goals ) ;
228- }
229- Ok ( ok)
230- }
231-
232146// ─────────────────────────── inner(供测试与复用) ───────────────────────────
233147
234- /// 里程碑状态白名单(TEXT 无 CHECK 惯例——命令层白名单先例)。
235- const MILESTONE_STATUSES : [ & str ; 4 ] =
236- [ MILESTONE_PENDING , MILESTONE_IN_PROGRESS , MILESTONE_DONE , MILESTONE_SKIPPED ] ;
237-
238- /// 里程碑判据类型白名单(self_test 仅登记占位契约——CRITERIA_SELF_TEST 常量
239- /// 供 M3 真实化后启用;本版不写入)。
240- const CRITERIA_TYPES : [ & str ; 2 ] = [ CRITERIA_MANUAL , CRITERIA_GROUP_SETTLED ] ;
241-
242148/// 校验目标存在(id 合法 + 行存在)。
243149pub ( crate ) fn require_goal ( db : & Db , id : i64 ) -> Result < ( ) , String > {
244150 if id <= 0 {
@@ -389,69 +295,6 @@ pub(crate) fn update_goal_status_inner(db: &Db, id: i64, status: &str) -> Result
389295 db. set_goal_status ( id, status) . map_err ( |e| e. to_string ( ) )
390296}
391297
392- pub ( crate ) fn add_goal_milestone_inner (
393- db : & Db ,
394- goal_id : i64 ,
395- title : & str ,
396- due_at : Option < i64 > ,
397- criteria_type : Option < String > ,
398- ref_group_id : Option < i64 > ,
399- ) -> Result < GoalMilestone , String > {
400- require_goal ( db, goal_id) ?;
401- let criteria_type = criteria_type. unwrap_or_else ( || CRITERIA_MANUAL . to_string ( ) ) ;
402- if !CRITERIA_TYPES . contains ( & criteria_type. as_str ( ) ) {
403- return Err ( format ! ( "不支持的里程碑判据类型: {}(支持: {})" , criteria_type, CRITERIA_TYPES . join( "/" ) ) ) ;
404- }
405- if criteria_type == CRITERIA_GROUP_SETTLED {
406- let gid = ref_group_id. ok_or_else ( || "group_settled 型里程碑必须绑定组" . to_string ( ) ) ?;
407- if db. get_group ( gid) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
408- return Err ( format ! ( "笔记组不存在: {}" , gid) ) ;
409- }
410- }
411- db. add_milestone ( goal_id, & NewMilestone {
412- title : normalize_title ( title. to_string ( ) , "未命名里程碑" ) ,
413- due_at,
414- order_idx : 0 ,
415- criteria_type,
416- ref_group_id,
417- } )
418- . map_err ( |e| e. to_string ( ) )
419- }
420-
421- pub ( crate ) fn set_goal_milestone_status_inner ( db : & Db , id : i64 , status : & str ) -> Result < bool , String > {
422- if !MILESTONE_STATUSES . contains ( & status) {
423- return Err ( format ! ( "不支持的里程碑状态: {}(支持: {})" , status, MILESTONE_STATUSES . join( "/" ) ) ) ;
424- }
425- // 取旧状态(幂等埋点判据:仅「未完成 → 完成」的转变记 goal_milestone_done)
426- let prev = db
427- . get_milestone ( id)
428- . map_err ( |e| e. to_string ( ) ) ?
429- . ok_or_else ( || format ! ( "里程碑不存在: {}" , id) ) ?;
430- let was_done = prev. status == MILESTONE_DONE ;
431- let ok = db. set_milestone_status ( id, status) . map_err ( |e| e. to_string ( ) ) ?;
432- if !ok {
433- return Err ( format ! ( "里程碑不存在: {}" , id) ) ;
434- }
435- if !was_done && status == MILESTONE_DONE {
436- let payload = serde_json:: json!( { "milestoneId" : id } ) . to_string ( ) ;
437- let _ = db. add_metric_event ( "goal_milestone_done" , & payload) ;
438- }
439- Ok ( true )
440- }
441-
442- pub ( crate ) fn bind_goal_group_inner ( db : & Db , goal_id : i64 , group_id : i64 ) -> Result < bool , String > {
443- require_goal ( db, goal_id) ?;
444- if db. get_group ( group_id) . map_err ( |e| e. to_string ( ) ) ?. is_none ( ) {
445- return Err ( format ! ( "笔记组不存在: {}" , group_id) ) ;
446- }
447- db. bind_group ( goal_id, group_id) . map_err ( |e| e. to_string ( ) )
448- }
449-
450- pub ( crate ) fn unbind_goal_group_inner ( db : & Db , goal_id : i64 , group_id : i64 ) -> Result < bool , String > {
451- require_goal ( db, goal_id) ?;
452- db. unbind_group ( goal_id, group_id) . map_err ( |e| e. to_string ( ) )
453- }
454-
455298/// 领域标签校验(与 commands_groups 同口径:kebab-case 白名单;空 → None)。
456299fn parse_domain ( domain_tag : Option < & str > ) -> Result < Option < String > , String > {
457300 match domain_tag {
0 commit comments