package docgov import ( "fmt" "strings" "time" ) // StateMachine 文档编制/审核/批准生命周期状态机与权限守卫 type StateMachine struct{} func NewStateMachine() *StateMachine { return &StateMachine{} } // CanEditDocumentContent 权限守卫 1:校验当前操作人是否有权修改文档正文 // 规则: // 1. 文档处于锁定状态 (reviewing, approving, approved) 时严禁直接修改 // 2. 只有编制负责人 (Author) 有权修改文档正文;审核人和批准人严禁直接修改 func (sm *StateMachine) CanEditDocumentContent(doc *DocumentRecord, actorID int64, actorRole UserRole) error { if doc == nil { return ErrDocumentNotFound } // 1. 检查锁定状态 if doc.IsLocked { return fmt.Errorf("%w: 当前文档处于【%s】状态并已锁定", ErrDocumentLocked, doc.CurrentState) } // 2. 状态合法性 if doc.CurrentState != StateDrafting { return fmt.Errorf("%w: 只有在编制草稿状态 (drafting) 下才允许修改内容,当前为【%s】", ErrInvalidStateTransition, doc.CurrentState) } // 3. 角色与身份权限校验 switch actorRole { case RoleReviewer: return ErrReviewerCannotEdit case RoleApprover: return ErrApproverCannotEdit case RoleAuthor: if doc.AuthorID > 0 && actorID > 0 && doc.AuthorID != actorID { return fmt.Errorf("%w: 文档编制负责人为用户ID %d,非操作人 %d", ErrOnlyAuthorCanEdit, doc.AuthorID, actorID) } return nil case RoleSystem: // 系统或智能体代为编辑:在编制中且未锁定阶段放行 (认领检查在外部业务层联动) return nil default: return ErrUnauthorizedActor } } // TransitionSubmitForReview 编制完成送审: drafting -> reviewing // 只有编制负责人可以发起送审 func (sm *StateMachine) TransitionSubmitForReview(doc *DocumentRecord, actorID int64, actorRole UserRole, actorName string) (*AuditLog, error) { if doc == nil { return nil, ErrDocumentNotFound } if doc.CurrentState != StateDrafting { return nil, fmt.Errorf("%w: 无法从【%s】阶段提交审核,必须为【drafting】", ErrInvalidStateTransition, doc.CurrentState) } if actorRole != RoleAuthor && actorRole != RoleSystem { return nil, fmt.Errorf("%w: 只有编制人或系统小队有权发起送审", ErrUnauthorizedActor) } if actorRole == RoleAuthor && doc.AuthorID > 0 && actorID > 0 && doc.AuthorID != actorID { return nil, fmt.Errorf("%w: 当前送审操作人 %d 不是该文档指定的编制人 %d", ErrUnauthorizedActor, actorID, doc.AuthorID) } fromState := doc.CurrentState toState := StateReviewing doc.CurrentState = toState doc.IsLocked = true // 审核阶段进入锁定状态 now := time.Now() doc.UpdatedAt = now return &AuditLog{ DocID: doc.DocID, Action: "submit_review", ActorID: actorID, ActorName: actorName, ActorRole: actorRole, FromState: fromState, ToState: toState, Details: map[string]any{"timestamp": now}, CreatedAt: now, }, nil } // TransitionReviewApprove 审核通过: reviewing -> approving // 只有审核人有权执行 func (sm *StateMachine) TransitionReviewApprove(doc *DocumentRecord, actorID int64, actorRole UserRole, actorName string, comment string) (*AuditLog, error) { if doc == nil { return nil, ErrDocumentNotFound } if doc.CurrentState != StateReviewing { return nil, fmt.Errorf("%w: 无法在【%s】状态下执行审核通过,当前应为【reviewing】", ErrInvalidStateTransition, doc.CurrentState) } if actorRole != RoleReviewer && actorRole != RoleSystem { return nil, fmt.Errorf("%w: 只有审核人有权执行审核通过", ErrUnauthorizedActor) } if actorRole == RoleReviewer && doc.ReviewerID > 0 && actorID > 0 && doc.ReviewerID != actorID { return nil, fmt.Errorf("%w: 当前审核人 %d 与文档指定审核人 %d 不匹配", ErrUnauthorizedActor, actorID, doc.ReviewerID) } fromState := doc.CurrentState toState := StateApproving doc.CurrentState = toState doc.IsLocked = true // 批准阶段保持锁定 now := time.Now() doc.UpdatedAt = now return &AuditLog{ DocID: doc.DocID, Action: "review_approve", ActorID: actorID, ActorName: actorName, ActorRole: actorRole, FromState: fromState, ToState: toState, Details: map[string]any{"comment": comment, "timestamp": now}, CreatedAt: now, }, nil } // TransitionReviewReject 审核驳回: reviewing -> drafting // 审核人驳回时必须填写驳回原因,退回编制草稿并解锁,记录驳回上下文 func (sm *StateMachine) TransitionReviewReject(doc *DocumentRecord, actorID int64, actorRole UserRole, actorName string, rejectReason string) (*AuditLog, error) { if doc == nil { return nil, ErrDocumentNotFound } if doc.CurrentState != StateReviewing { return nil, fmt.Errorf("%w: 无法在【%s】状态下执行审核驳回,当前应为【reviewing】", ErrInvalidStateTransition, doc.CurrentState) } if actorRole != RoleReviewer && actorRole != RoleSystem { return nil, fmt.Errorf("%w: 只有审核人有权执行审核驳回", ErrUnauthorizedActor) } if actorRole == RoleReviewer && doc.ReviewerID > 0 && actorID > 0 && doc.ReviewerID != actorID { return nil, fmt.Errorf("%w: 当前审核人 %d 与文档指定审核人 %d 不匹配", ErrUnauthorizedActor, actorID, doc.ReviewerID) } trimmedReason := strings.TrimSpace(rejectReason) if trimmedReason == "" { return nil, ErrRejectReasonRequired } fromState := doc.CurrentState toState := StateDrafting doc.CurrentState = toState doc.IsLocked = false // 退回编制草稿阶段,解锁文档供编制人修订 now := time.Now() doc.LastRejectedReason = trimmedReason doc.LastRejectedByRole = string(actorRole) doc.LastRejectedByName = actorName doc.LastRejectedAt = &now doc.UpdatedAt = now return &AuditLog{ DocID: doc.DocID, Action: "review_reject", ActorID: actorID, ActorName: actorName, ActorRole: actorRole, FromState: fromState, ToState: toState, Details: map[string]any{ "reject_reason": trimmedReason, "timestamp": now, }, CreatedAt: now, }, nil } // TransitionApproverApprove 批准通过: approving -> approved (终态归档) func (sm *StateMachine) TransitionApproverApprove(doc *DocumentRecord, actorID int64, actorRole UserRole, actorName string, comment string) (*AuditLog, error) { if doc == nil { return nil, ErrDocumentNotFound } if doc.CurrentState != StateApproving { return nil, fmt.Errorf("%w: 无法在【%s】状态下执行批准通过,当前应为【approving】", ErrInvalidStateTransition, doc.CurrentState) } if actorRole != RoleApprover && actorRole != RoleSystem { return nil, fmt.Errorf("%w: 只有批准人有权执行最终批准", ErrUnauthorizedActor) } if actorRole == RoleApprover && doc.ApproverID > 0 && actorID > 0 && doc.ApproverID != actorID { return nil, fmt.Errorf("%w: 当前批准人 %d 与文档指定批准人 %d 不匹配", ErrUnauthorizedActor, actorID, doc.ApproverID) } fromState := doc.CurrentState toState := StateApproved doc.CurrentState = toState doc.IsLocked = true // 终态受控归档锁定 now := time.Now() doc.ApprovedAt = &now doc.UpdatedAt = now return &AuditLog{ DocID: doc.DocID, Action: "approver_approve", ActorID: actorID, ActorName: actorName, ActorRole: actorRole, FromState: fromState, ToState: toState, Details: map[string]any{ "comment": comment, "timestamp": now, }, CreatedAt: now, }, nil } // TransitionApproverReject 批准驳回: approving -> drafting 或 reviewing // 驳回时必须填写驳回原因,退回目标根据配置和参数决定 func (sm *StateMachine) TransitionApproverReject(doc *DocumentRecord, actorID int64, actorRole UserRole, actorName string, rejectReason string, rollbackTo DocumentState) (*AuditLog, error) { if doc == nil { return nil, ErrDocumentNotFound } if doc.CurrentState != StateApproving { return nil, fmt.Errorf("%w: 无法在【%s】状态下执行批准驳回,当前应为【approving】", ErrInvalidStateTransition, doc.CurrentState) } if actorRole != RoleApprover && actorRole != RoleSystem { return nil, fmt.Errorf("%w: 只有批准人有权执行批准驳回", ErrUnauthorizedActor) } if actorRole == RoleApprover && doc.ApproverID > 0 && actorID > 0 && doc.ApproverID != actorID { return nil, fmt.Errorf("%w: 当前批准人 %d 与文档指定批准人 %d 不匹配", ErrUnauthorizedActor, actorID, doc.ApproverID) } trimmedReason := strings.TrimSpace(rejectReason) if trimmedReason == "" { return nil, ErrRejectReasonRequired } // 确定退回目标阶段 targetState := rollbackTo if targetState == "" { targetState = doc.Config.RejectRollbackTo } if targetState != StateDrafting && targetState != StateReviewing { targetState = StateDrafting } if targetState == StateReviewing && !doc.Config.AllowApproverToReviewer { targetState = StateDrafting } fromState := doc.CurrentState toState := targetState doc.CurrentState = toState if toState == StateDrafting { doc.IsLocked = false // 退回编制阶段解锁 } else { doc.IsLocked = true // 退回审核阶段保持锁定 } now := time.Now() doc.LastRejectedReason = trimmedReason doc.LastRejectedByRole = string(actorRole) doc.LastRejectedByName = actorName doc.LastRejectedAt = &now doc.UpdatedAt = now return &AuditLog{ DocID: doc.DocID, Action: "approver_reject", ActorID: actorID, ActorName: actorName, ActorRole: actorRole, FromState: fromState, ToState: toState, Details: map[string]any{ "reject_reason": trimmedReason, "rollback_to": toState, "timestamp": now, }, CreatedAt: now, }, nil }