// Package approval 提供渠道无关的通用审批领域事实。 package approval import ( "strings" "time" "github.com/bytedance/sonic" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // Instance 是只保存渠道无关事实的通用审批实例。 type Instance struct { ID uint BusinessType string BusinessID uint SubmitterAccountID uint SubmitterSnapshot []byte Provider string ExternalRef string Status int RequestSnapshot []byte DecisionSnapshot []byte CorrelationID string Version int StatusChangedAt time.Time CreatedAt time.Time UpdatedAt time.Time } // NewInstanceParams 是创建通用审批实例所需的稳定业务事实。 type NewInstanceParams struct { BusinessType string BusinessID uint SubmitterAccountID uint SubmitterSnapshot []byte Provider string RequestSnapshot []byte CorrelationID string } // NewInstance 创建处于提交中的渠道无关审批实例。 func NewInstance(params NewInstanceParams, now time.Time) (*Instance, error) { businessType := strings.TrimSpace(params.BusinessType) provider := strings.TrimSpace(params.Provider) correlationID := strings.TrimSpace(params.CorrelationID) if businessType == "" || params.BusinessID == 0 || params.SubmitterAccountID == 0 || provider == "" || correlationID == "" { return nil, errors.New(errors.CodeInvalidParam, "通用审批实例的业务引用、提交人、渠道和关联 ID 不能为空") } if !isJSONObject(params.SubmitterSnapshot) || !isJSONObject(params.RequestSnapshot) { return nil, errors.New(errors.CodeInvalidParam, "通用审批实例快照必须是有效 JSON 对象") } if now.IsZero() { return nil, errors.New(errors.CodeInvalidParam, "通用审批实例创建时间不能为空") } now = now.UTC() return &Instance{ BusinessType: businessType, BusinessID: params.BusinessID, SubmitterAccountID: params.SubmitterAccountID, SubmitterSnapshot: cloneBytes(params.SubmitterSnapshot), Provider: provider, Status: constants.ApprovalStatusSubmitting, RequestSnapshot: cloneBytes(params.RequestSnapshot), CorrelationID: correlationID, Version: constants.ApprovalInitialVersion, StatusChangedAt: now, CreatedAt: now, UpdatedAt: now, }, nil } // StatusForDecision 将渠道 Adapter 输出的标准决策映射为通用审批终态。 func StatusForDecision(decision string) (int, error) { switch decision { case constants.ApprovalDecisionApproved: return constants.ApprovalStatusApproved, nil case constants.ApprovalDecisionRejected: return constants.ApprovalStatusRejected, nil case constants.ApprovalDecisionCancelled: return constants.ApprovalStatusCancelled, nil case constants.ApprovalDecisionDeleted: return constants.ApprovalStatusDeleted, nil case constants.ApprovalDecisionRevokedAfterApproved: return constants.ApprovalStatusRevokedAfterApproved, nil default: return 0, errors.New(errors.CodeInvalidParam, "审批渠道返回了不受支持的标准决策") } } // IsTerminalStatus 判断状态是否为可分发给业务消费者的标准终态。 func IsTerminalStatus(status int) bool { return status == constants.ApprovalStatusApproved || status == constants.ApprovalStatusRejected || status == constants.ApprovalStatusCancelled || status == constants.ApprovalStatusDeleted || status == constants.ApprovalStatusRevokedAfterApproved } // ApplyDecision 校验标准决策状态迁移并冻结首次到达该终态的决策快照。 func (i *Instance) ApplyDecision(decision string, snapshot []byte, now time.Time) (bool, error) { if i == nil || now.IsZero() || !isJSONObject(snapshot) { return false, errors.New(errors.CodeInvalidParam, "审批决策实例、快照和决策时间不能为空") } targetStatus, err := StatusForDecision(decision) if err != nil { return false, err } if i.Status == targetStatus { return false, nil } if targetStatus == constants.ApprovalStatusRevokedAfterApproved { if i.Status != constants.ApprovalStatusApproved { return false, errors.New(errors.CodeInvalidStatus, "只有已通过审批可以进入通过后撤销状态") } } else if IsTerminalStatus(i.Status) { return false, errors.New(errors.CodeInvalidStatus, "审批已进入其他标准终态") } now = now.UTC() i.Status = targetStatus i.DecisionSnapshot = cloneBytes(snapshot) i.StatusChangedAt = now i.UpdatedAt = now i.Version++ return true, nil } func isJSONObject(value []byte) bool { var object map[string]any return len(value) > 0 && sonic.Unmarshal(value, &object) == nil && object != nil } func cloneBytes(value []byte) []byte { cloned := make([]byte, len(value)) copy(cloned, value) return cloned }