feat(projectinstance): apply duty mode to readiness access and snapshots
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ const (
|
|||||||
AccessCodeDocTaskDisabled = "DOC_TASK_DISABLED"
|
AccessCodeDocTaskDisabled = "DOC_TASK_DISABLED"
|
||||||
AccessCodeNotCurrentStage = "DOC_TASK_NOT_IN_ACTIVE_STAGE"
|
AccessCodeNotCurrentStage = "DOC_TASK_NOT_IN_ACTIVE_STAGE"
|
||||||
AccessCodeForbiddenDuty = "DOC_DUTY_FORBIDDEN"
|
AccessCodeForbiddenDuty = "DOC_DUTY_FORBIDDEN"
|
||||||
|
AccessCodeDutyNotManaged = "DOC_DUTY_NOT_MANAGED"
|
||||||
AccessCodeInstanceClosed = "WORKFLOW_INSTANCE_CLOSED"
|
AccessCodeInstanceClosed = "WORKFLOW_INSTANCE_CLOSED"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,6 +113,26 @@ func (s *Service) CheckDocTaskAccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if classifyDocDuty(planTask.Duties) == DutyModeNone {
|
||||||
|
switch action {
|
||||||
|
case DocAccessActionRun, DocAccessActionEdit:
|
||||||
|
if isCreator {
|
||||||
|
return allow
|
||||||
|
}
|
||||||
|
return DocAccessDecision{
|
||||||
|
Allowed: false,
|
||||||
|
Code: AccessCodeForbiddenDuty,
|
||||||
|
Message: "未纳入人工审批的文档仅创建者可编辑或执行",
|
||||||
|
}
|
||||||
|
case DocAccessActionReview, DocAccessActionApprove, DocAccessActionSubmit:
|
||||||
|
return DocAccessDecision{
|
||||||
|
Allowed: false,
|
||||||
|
Code: AccessCodeDutyNotManaged,
|
||||||
|
Message: "该文档未纳入人工审批,不能审核、批准或提交审批",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isCreator {
|
if isCreator {
|
||||||
return allow
|
return allow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ const (
|
|||||||
issueSeparationOfDutyViolation = "SEPARATION_OF_DUTY_VIOLATION"
|
issueSeparationOfDutyViolation = "SEPARATION_OF_DUTY_VIOLATION"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ClassifyDocDuty 供运行时/门禁复用,禁止平行再写一套三职判定。
|
||||||
|
func ClassifyDocDuty(d DocDuty) DutyMode {
|
||||||
|
return classifyDocDuty(d)
|
||||||
|
}
|
||||||
|
|
||||||
func classifyDocDuty(d DocDuty) DutyMode {
|
func classifyDocDuty(d DocDuty) DutyMode {
|
||||||
ids := []int64{d.AuthorUserID, d.ReviewerUserID, d.ApproverUserID}
|
ids := []int64{d.AuthorUserID, d.ReviewerUserID, d.ApproverUserID}
|
||||||
assigned := 0
|
assigned := 0
|
||||||
|
|||||||
@@ -25,18 +25,20 @@ func evaluateStageReadiness(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
td, hasDef := taskDefMap[dt.TaskKey]
|
td, hasDef := taskDefMap[dt.TaskKey]
|
||||||
required := !hasDef || td.BlocksWhenEnabled()
|
approvalManaged := classifyDocDuty(dt.Duties) != DutyModeNone
|
||||||
|
required := approvalManaged && (!hasDef || td.BlocksWhenEnabled())
|
||||||
state := docStates[dt.TaskKey]
|
state := docStates[dt.TaskKey]
|
||||||
if state == "" {
|
if state == "" {
|
||||||
state = "missing"
|
state = "missing"
|
||||||
}
|
}
|
||||||
approved := state == "approved"
|
approved := state == "approved"
|
||||||
item := DocTaskReadiness{
|
item := DocTaskReadiness{
|
||||||
TaskKey: dt.TaskKey,
|
TaskKey: dt.TaskKey,
|
||||||
Required: required,
|
Required: required,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
DocState: state,
|
ApprovalManaged: approvalManaged,
|
||||||
Approved: approved,
|
DocState: state,
|
||||||
|
Approved: approved,
|
||||||
}
|
}
|
||||||
readiness = append(readiness, item)
|
readiness = append(readiness, item)
|
||||||
if required && !approved {
|
if required && !approved {
|
||||||
|
|||||||
@@ -59,12 +59,13 @@ func (s *Service) ResolveStageAccess(ctx context.Context, projectID int64, userI
|
|||||||
}
|
}
|
||||||
status := stageStatus[stPlan.StageKey]
|
status := stageStatus[stPlan.StageKey]
|
||||||
hasDuty := stageDutyForUser(stPlan, userID)
|
hasDuty := stageDutyForUser(stPlan, userID)
|
||||||
|
hasUnmanaged := stageHasUnmanagedDoc(stPlan)
|
||||||
allow := false
|
allow := false
|
||||||
switch status {
|
switch status {
|
||||||
case StageActive:
|
case StageActive:
|
||||||
allow = isCreator || hasDuty
|
allow = isCreator || hasDuty || hasUnmanaged
|
||||||
case StageCompleted:
|
case StageCompleted:
|
||||||
allow = hasDuty || isCreator
|
allow = hasDuty || isCreator || hasUnmanaged
|
||||||
default:
|
default:
|
||||||
allow = false
|
allow = false
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ func (s *Service) ResolveStageAccess(ctx context.Context, projectID int64, userI
|
|||||||
if !dt.Enabled {
|
if !dt.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isCreator || docTaskDutyForUser(dt, userID) {
|
if isCreator || docTaskDutyForUser(dt, userID) || classifyDocDuty(dt.Duties) == DutyModeNone {
|
||||||
out.AllowedTaskKeys[dt.TaskKey] = true
|
out.AllowedTaskKeys[dt.TaskKey] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,11 +95,23 @@ func stageDutyForUser(st StagePlan, userID int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func docTaskDutyForUser(dt DocTaskPlan, userID int64) bool {
|
func docTaskDutyForUser(dt DocTaskPlan, userID int64) bool {
|
||||||
|
if classifyDocDuty(dt.Duties) != DutyModeFull {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return dt.Duties.AuthorUserID == userID ||
|
return dt.Duties.AuthorUserID == userID ||
|
||||||
dt.Duties.ReviewerUserID == userID ||
|
dt.Duties.ReviewerUserID == userID ||
|
||||||
dt.Duties.ApproverUserID == userID
|
dt.Duties.ApproverUserID == userID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stageHasUnmanagedDoc(st StagePlan) bool {
|
||||||
|
for _, dt := range st.DocTasks {
|
||||||
|
if dt.Enabled && classifyDocDuty(dt.Duties) == DutyModeNone {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// TaskKeyAllowed 判断 task_key 是否在允许范围内。
|
// TaskKeyAllowed 判断 task_key 是否在允许范围内。
|
||||||
func (a *StageAccess) TaskKeyAllowed(taskKey string) bool {
|
func (a *StageAccess) TaskKeyAllowed(taskKey string) bool {
|
||||||
if a == nil || !a.Restricted {
|
if a == nil || !a.Restricted {
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ func (s *pgStore) replaceInstanceChildren(ctx context.Context, tx pgx.Tx, inst *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !dt.Enabled {
|
if !dt.Enabled || classifyDocDuty(dt.Duties) == DutyModeNone {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for role, uid := range map[string]int64{
|
for role, uid := range map[string]int64{
|
||||||
@@ -960,9 +960,9 @@ func (m *memStore) TerminateInstance(_ context.Context, instanceID int64, projec
|
|||||||
return nil, ErrInstanceNotFound
|
return nil, ErrInstanceNotFound
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
inst.Status = "terminated"
|
inst.Status = InstanceTerminated
|
||||||
copyInst := *inst
|
copyInst := *inst
|
||||||
delete(m.live, projectID)
|
m.live[projectID] = ©Inst
|
||||||
pm := m.ensureMeta(projectID)
|
pm := m.ensureMeta(projectID)
|
||||||
pm.lifecycle = LifecyclePlanning
|
pm.lifecycle = LifecyclePlanning
|
||||||
pm.activeKey = ""
|
pm.activeKey = ""
|
||||||
|
|||||||
@@ -110,15 +110,24 @@ type ActivationPreview struct {
|
|||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreviewDocRef 预览中列出的例外文档。
|
||||||
|
type PreviewDocRef struct {
|
||||||
|
TaskKey string `json:"task_key"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
// PreviewSummary 激活影响摘要。
|
// PreviewSummary 激活影响摘要。
|
||||||
type PreviewSummary struct {
|
type PreviewSummary struct {
|
||||||
WorkflowName string `json:"workflow_name"`
|
WorkflowName string `json:"workflow_name"`
|
||||||
WorkflowVersion int `json:"workflow_version"`
|
WorkflowVersion int `json:"workflow_version"`
|
||||||
EnabledStages int `json:"enabled_stages"`
|
EnabledStages int `json:"enabled_stages"`
|
||||||
EnabledDocTasks int `json:"enabled_doc_tasks"`
|
EnabledDocTasks int `json:"enabled_doc_tasks"`
|
||||||
AssignedDuties int `json:"assigned_duties"`
|
GovernedDocTasks int `json:"governed_doc_tasks"`
|
||||||
FirstStageKey string `json:"first_stage_key,omitempty"`
|
UnmanagedDocTasks int `json:"unmanaged_doc_tasks"`
|
||||||
FirstStageName string `json:"first_stage_name,omitempty"`
|
AssignedDuties int `json:"assigned_duties"`
|
||||||
|
UnmanagedDocuments []PreviewDocRef `json:"unmanaged_documents,omitempty"`
|
||||||
|
FirstStageKey string `json:"first_stage_key,omitempty"`
|
||||||
|
FirstStageName string `json:"first_stage_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivateInput 二次确认激活。
|
// ActivateInput 二次确认激活。
|
||||||
@@ -199,11 +208,12 @@ type GateIssue struct {
|
|||||||
|
|
||||||
// DocTaskReadiness 文档任务就绪状态。
|
// DocTaskReadiness 文档任务就绪状态。
|
||||||
type DocTaskReadiness struct {
|
type DocTaskReadiness struct {
|
||||||
TaskKey string `json:"task_key"`
|
TaskKey string `json:"task_key"`
|
||||||
Required bool `json:"required"`
|
Required bool `json:"required"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
DocState string `json:"doc_state"`
|
ApprovalManaged bool `json:"approval_managed"`
|
||||||
Approved bool `json:"approved"`
|
DocState string `json:"doc_state"`
|
||||||
|
Approved bool `json:"approved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StageReadiness 阶段推进就绪评估。
|
// StageReadiness 阶段推进就绪评估。
|
||||||
|
|||||||
@@ -237,14 +237,24 @@ func previewSummary(plan WorkflowPlan, version *workflowdef.Version) PreviewSumm
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sum.EnabledDocTasks++
|
sum.EnabledDocTasks++
|
||||||
if dt.Duties.AuthorUserID > 0 {
|
switch classifyDocDuty(dt.Duties) {
|
||||||
sum.AssignedDuties++
|
case DutyModeFull:
|
||||||
}
|
sum.GovernedDocTasks++
|
||||||
if dt.Duties.ReviewerUserID > 0 {
|
if dt.Duties.AuthorUserID > 0 {
|
||||||
sum.AssignedDuties++
|
sum.AssignedDuties++
|
||||||
}
|
}
|
||||||
if dt.Duties.ApproverUserID > 0 {
|
if dt.Duties.ReviewerUserID > 0 {
|
||||||
sum.AssignedDuties++
|
sum.AssignedDuties++
|
||||||
|
}
|
||||||
|
if dt.Duties.ApproverUserID > 0 {
|
||||||
|
sum.AssignedDuties++
|
||||||
|
}
|
||||||
|
case DutyModeNone:
|
||||||
|
sum.UnmanagedDocTasks++
|
||||||
|
sum.UnmanagedDocuments = append(sum.UnmanagedDocuments, PreviewDocRef{
|
||||||
|
TaskKey: dt.TaskKey,
|
||||||
|
Title: dutyTaskLabel(dt),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,7 +538,15 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>工作流:{preview.summary.workflow_name} v{preview.summary.workflow_version}</div>
|
<div>工作流:{preview.summary.workflow_name} v{preview.summary.workflow_version}</div>
|
||||||
<div>启用阶段:{preview.summary.enabled_stages} · 文档任务:{preview.summary.enabled_doc_tasks}</div>
|
<div>启用阶段:{preview.summary.enabled_stages} · 文档任务:{preview.summary.enabled_doc_tasks}</div>
|
||||||
|
<div>
|
||||||
|
纳入人工审批:{preview.summary.governed_doc_tasks ?? preview.summary.enabled_doc_tasks} · 不纳入:{preview.summary.unmanaged_doc_tasks ?? 0}
|
||||||
|
</div>
|
||||||
<div>职责指派:{preview.summary.assigned_duties} 项</div>
|
<div>职责指派:{preview.summary.assigned_duties} 项</div>
|
||||||
|
{(preview.summary.unmanaged_documents || []).length > 0 && (
|
||||||
|
<div>
|
||||||
|
例外文档:{(preview.summary.unmanaged_documents || []).map((doc) => doc.title || doc.task_key).join('、')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{preview.summary.first_stage_name && (
|
{preview.summary.first_stage_name && (
|
||||||
<div>首阶段:{preview.summary.first_stage_name}</div>
|
<div>首阶段:{preview.summary.first_stage_name}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ export interface ActivationPreview {
|
|||||||
workflow_version: number;
|
workflow_version: number;
|
||||||
enabled_stages: number;
|
enabled_stages: number;
|
||||||
enabled_doc_tasks: number;
|
enabled_doc_tasks: number;
|
||||||
|
governed_doc_tasks?: number;
|
||||||
|
unmanaged_doc_tasks?: number;
|
||||||
assigned_duties: number;
|
assigned_duties: number;
|
||||||
|
unmanaged_documents?: Array<{ task_key: string; title: string }>;
|
||||||
first_stage_key?: string;
|
first_stage_key?: string;
|
||||||
first_stage_name?: string;
|
first_stage_name?: string;
|
||||||
};
|
};
|
||||||
@@ -124,6 +127,7 @@ export interface StageReadiness {
|
|||||||
task_key: string;
|
task_key: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
approval_managed?: boolean;
|
||||||
doc_state: string;
|
doc_state: string;
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
Reference in New Issue
Block a user