Files
weichen 143ee934b8 feat(collab): free-lane chat, claim-on-view, lease ownership, notify inbox
Open member free-lane for chat initiation; auto-claim on author view;
harden lease persist/ownership with readable DOC_BUSY; add notification
inbox and supplement-impact HTTP decision APIs wired to composition root.
2026-09-24 11:49:43 +00:00

113 lines
3.2 KiB
Go

package projectinstance
const (
DutyRoleAuthor = "author"
DutyRoleReviewer = "reviewer"
DutyRoleApprover = "approver"
AccessCodeNotActivated = "WORKFLOW_NOT_ACTIVATED"
GovernanceBlockedHint = "当前项目尚未激活工作流,文档任务不会启动。请先在设置中配置并激活至少一份纳入人工审批的文档。"
)
// requiredDutyRole maps a document action to the duty that may perform it.
// run/edit are member-level (empty = no duty binding); submit/review/approve stay duty-gated.
func requiredDutyRole(action string) string {
switch action {
case DocAccessActionSubmit:
return DutyRoleAuthor
case DocAccessActionReview:
return DutyRoleReviewer
case DocAccessActionApprove:
return DutyRoleApprover
default:
return ""
}
}
func userHoldsDuty(d DocDuty, userID int64, role string) bool {
if userID <= 0 || role == "" {
return false
}
switch role {
case DutyRoleAuthor:
return d.AuthorUserID == userID
case DutyRoleReviewer:
return d.ReviewerUserID == userID
case DutyRoleApprover:
return d.ApproverUserID == userID
default:
return false
}
}
// classifyAccess applies the locked duty-action matrix. govLive=false is pre-activation.
// Callers must already have enforced project membership; run/edit are then member-level
// under DutyModeFull/DutyModeNone. submit/review/approve remain duty-bound.
// DutyModePartial still denies all non-view actions to avoid half-configured mis-grants.
func classifyAccess(dt DocTaskPlan, action string, userID int64, isCreator bool, govLive bool) DocAccessDecision {
allow := DocAccessDecision{Allowed: true}
if action == DocAccessActionView {
return allow
}
if !govLive {
if action == DocAccessActionRun {
return DocAccessDecision{
Allowed: true,
Code: AccessCodeNotActivated,
Message: GovernanceBlockedHint,
}
}
return DocAccessDecision{
Allowed: false,
Code: AccessCodeNotActivated,
Message: GovernanceBlockedHint,
}
}
if !dt.Enabled {
return DocAccessDecision{Allowed: false, Code: AccessCodeDocTaskDisabled, Message: "该文档已被创建者禁用"}
}
mode, effective, _ := ResolveDocTaskDuty(dt)
switch mode {
case DutyModeNone:
switch action {
case DocAccessActionRun, DocAccessActionEdit:
// deny_only_submit: any project member (membership checked upstream) may run/edit.
_ = isCreator
_ = userID
return allow
case DocAccessActionReview, DocAccessActionApprove, DocAccessActionSubmit:
return DocAccessDecision{
Allowed: false,
Code: AccessCodeDutyNotManaged,
Message: "该文档未纳入人工审批,不能审核、批准或提交审批",
}
}
case DutyModePartial:
return DocAccessDecision{
Allowed: false,
Code: AccessCodeForbiddenDuty,
Message: "文档职责未完整指派,不能执行该动作",
}
case DutyModeFull:
role := requiredDutyRole(action)
if role == "" {
// run/edit: member-level
return allow
}
if userHoldsDuty(effective, userID, role) {
return allow
}
return DocAccessDecision{
Allowed: false,
Code: AccessCodeForbiddenDuty,
Message: "当前用户不是该文档的对应职责人",
}
}
return DocAccessDecision{
Allowed: false,
Code: AccessCodeForbiddenDuty,
Message: "当前用户不是该文档职责人",
}
}