172 lines
5.5 KiB
Go
172 lines
5.5 KiB
Go
package docnotify
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"chat-agent-system/internal/ctxmgr"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/notification"
|
|
)
|
|
|
|
// LifecycleAction identifies a governance transition for routing.
|
|
type LifecycleAction string
|
|
|
|
const (
|
|
LifecycleSubmitReview LifecycleAction = "submit_review"
|
|
LifecycleReviewApprove LifecycleAction = "review_approve"
|
|
LifecycleReviewReject LifecycleAction = "review_reject"
|
|
LifecycleApproverApprove LifecycleAction = "approver_approve"
|
|
LifecycleApproverReject LifecycleAction = "approver_reject"
|
|
LifecycleReopen LifecycleAction = "reopen"
|
|
)
|
|
|
|
// RecipientForDocEvent resolves the duty-holder inbox recipient for a document event.
|
|
func RecipientForDocEvent(doc *docgov.DocumentRecord, eventType string) int64 {
|
|
if doc == nil {
|
|
return 0
|
|
}
|
|
switch eventType {
|
|
case notification.EventDocReviewRequested:
|
|
if doc.ReviewerID > 0 {
|
|
return doc.ReviewerID
|
|
}
|
|
case notification.EventDocApprovalRequested:
|
|
if doc.ApproverID > 0 {
|
|
return doc.ApproverID
|
|
}
|
|
case notification.EventDocRejected, notification.EventDocReopened, notification.EventDocPendingSupplements:
|
|
if doc.AuthorID > 0 {
|
|
return doc.AuthorID
|
|
}
|
|
case notification.EventDocApproved:
|
|
if doc.AuthorID > 0 {
|
|
return doc.AuthorID
|
|
}
|
|
}
|
|
if doc.AuthorID > 0 {
|
|
return doc.AuthorID
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// EmitLifecycleParams carries optional chat side channels.
|
|
type EmitLifecycleParams struct {
|
|
Notify notificationEmitter
|
|
Digest digestUpserter
|
|
Chat *ctxmgr.ContextManager
|
|
Action LifecycleAction
|
|
Doc *docgov.DocumentRecord
|
|
ActorID int64
|
|
ActorName string
|
|
Detail string
|
|
}
|
|
|
|
type digestUpserter interface {
|
|
UpsertDocumentDigest(ctx context.Context, p notification.UpsertDigestParams) error
|
|
}
|
|
|
|
// EmitLifecycle notifies the duty holder for a governance transition.
|
|
func EmitLifecycle(ctx context.Context, p EmitLifecycleParams) {
|
|
if p.Doc == nil || p.Notify == nil || !p.Notify.Available() {
|
|
return
|
|
}
|
|
eventType, notifType, title, body, severity := lifecycleCopy(p.Action, p.Doc, p.ActorName, p.Detail)
|
|
if eventType == "" {
|
|
return
|
|
}
|
|
recipient := RecipientForDocEvent(p.Doc, eventType)
|
|
if recipient <= 0 {
|
|
return
|
|
}
|
|
payload := notification.DocumentEventPayload{
|
|
ProjectID: p.Doc.ProjectID, DocID: p.Doc.DocID, DocTitle: p.Doc.Title,
|
|
Summary: truncateLifecycle(p.Detail, 80),
|
|
}
|
|
dedupe := fmt.Sprintf("doc_lifecycle:%s:%s:%d:%s", p.Action, p.Doc.DocID, recipient, p.Doc.CurrentState)
|
|
_, _ = p.Notify.EmitDocumentEvent(ctx, eventType, notifType, title, body, dedupe, recipient, payload)
|
|
|
|
if p.Digest != nil && severity == notification.SeverityActionRequired {
|
|
kind := digestKindForEvent(eventType)
|
|
if kind != "" {
|
|
_ = p.Digest.UpsertDocumentDigest(ctx, notification.UpsertDigestParams{
|
|
UserID: recipient, ProjectID: p.Doc.ProjectID, DigestKind: kind,
|
|
DocID: p.Doc.DocID, DocTitle: p.Doc.Title, Severity: severity,
|
|
ActorID: p.ActorID, ActorName: p.ActorName, Summary: truncateLifecycle(p.Detail, 60),
|
|
})
|
|
}
|
|
}
|
|
|
|
if p.Chat != nil {
|
|
_ = PostProjectNotice(ctx, p.Chat, p.Doc.ProjectID, recipient, body, map[string]any{
|
|
"doc_id": p.Doc.DocID, "action": string(p.Action), "event": eventType,
|
|
})
|
|
}
|
|
}
|
|
|
|
func digestKindForEvent(eventType string) string {
|
|
switch eventType {
|
|
case notification.EventDocReviewRequested:
|
|
return notification.DigestKindReviewRequested
|
|
case notification.EventDocApprovalRequested:
|
|
return notification.DigestKindApprovalRequested
|
|
case notification.EventDocPendingSupplements:
|
|
return notification.DigestKindPendingSupplements
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func lifecycleCopy(action LifecycleAction, doc *docgov.DocumentRecord, actorName, detail string) (eventType, notifType, title, body, severity string) {
|
|
titleDoc := strings.TrimSpace(doc.Title)
|
|
if titleDoc == "" {
|
|
titleDoc = doc.DocID
|
|
}
|
|
actor := strings.TrimSpace(actorName)
|
|
if actor == "" {
|
|
actor = "成员"
|
|
}
|
|
reason := strings.TrimSpace(detail)
|
|
switch action {
|
|
case LifecycleSubmitReview:
|
|
return notification.EventDocReviewRequested, notification.TypeDocReviewRequested,
|
|
fmt.Sprintf("《%s》待您审核", titleDoc),
|
|
fmt.Sprintf("%s 已送审,请审核本文档。", actor),
|
|
notification.SeverityActionRequired
|
|
case LifecycleReviewApprove:
|
|
return notification.EventDocApprovalRequested, notification.TypeDocApprovalRequested,
|
|
fmt.Sprintf("《%s》待您批准", titleDoc),
|
|
fmt.Sprintf("%s 审核已通过,请批准本文档。", actor),
|
|
notification.SeverityActionRequired
|
|
case LifecycleReviewReject, LifecycleApproverReject:
|
|
msg := fmt.Sprintf("%s 驳回了《%s》。", actor, titleDoc)
|
|
if reason != "" {
|
|
msg += "原因:" + reason
|
|
}
|
|
return notification.EventDocRejected, notification.TypeDocRejected,
|
|
fmt.Sprintf("《%s》已被驳回", titleDoc), msg,
|
|
notification.SeverityActionRequired
|
|
case LifecycleApproverApprove:
|
|
return notification.EventDocApproved, notification.TypeDocApproved,
|
|
fmt.Sprintf("《%s》已批准归档", titleDoc),
|
|
fmt.Sprintf("%s 已终审批准,《%s》已归档锁定。", actor, titleDoc),
|
|
notification.SeverityInfo
|
|
case LifecycleReopen:
|
|
return notification.EventDocReopened, notification.TypeDocReopened,
|
|
fmt.Sprintf("《%s》已重开修订", titleDoc),
|
|
fmt.Sprintf("%s 批准重开修订,《%s》已回编制期。", actor, titleDoc),
|
|
notification.SeverityActionRequired
|
|
default:
|
|
return "", "", "", "", ""
|
|
}
|
|
}
|
|
|
|
func truncateLifecycle(s string, n int) string {
|
|
r := []rune(strings.TrimSpace(s))
|
|
if len(r) <= n {
|
|
return string(r)
|
|
}
|
|
return string(r[:n]) + "…"
|
|
}
|