// Package approval 提供渠道无关审批实例的读取模型和权限投影。 package approval import ( "context" "strings" "time" "github.com/bytedance/sonic" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // Viewer 是业务权限 Adapter 判定当前读取主体所需的最小身份。 type Viewer struct { AccountID uint UserType int } // BusinessReference 是通用审批实例指向的稳定业务引用。 type BusinessReference struct { InstanceID uint BusinessType string BusinessID uint } // BusinessProjection 是业务权限 Adapter 返回的安全摘要和处理状态。 type BusinessProjection struct { Allowed bool Summary string ProcessingStatus int ProcessingStatusName string ProcessingSummary string } // BusinessProjectionResolver 批量复核业务资源当前权限并投影处理摘要。 type BusinessProjectionResolver interface { Resolve(ctx context.Context, viewer Viewer, references []BusinessReference) (map[uint]BusinessProjection, error) } // ExtensionReference 是渠道 Adapter 读取本地扩展快照所需的最小引用。 type ExtensionReference struct { InstanceID uint Provider string ExternalRef string } // ChannelExtensionResolver 可为平台主体批量补充渠道专属本地快照。 // 实现不得在 Query 请求中实时调用外部审批平台。 type ChannelExtensionResolver interface { Resolve(ctx context.Context, viewer Viewer, references []ExtensionReference) (map[uint]any, error) } // Projection 是通用审批 Query 对业务列表和详情输出的稳定读取模型。 type Projection struct { ID uint `json:"id"` BusinessType string `json:"business_type"` BusinessID uint `json:"business_id"` BusinessSummary string `json:"business_summary"` SubmitterAccountID uint `json:"submitter_account_id"` SubmitterName string `json:"submitter_name"` Provider string `json:"provider"` Status int `json:"status"` StatusName string `json:"status_name"` StatusChangedAt time.Time `json:"status_changed_at"` ProcessingStatus int `json:"processing_status"` ProcessingStatusName string `json:"processing_status_name"` ProcessingSummary string `json:"processing_summary"` ChannelExtension any `json:"channel_extension,omitempty"` } // Query 批量读取通用审批事实,并通过业务 Adapter 复核当前权限。 type Query struct { db *gorm.DB businessResolver BusinessProjectionResolver extensionResolver ChannelExtensionResolver } // NewQuery 创建通用审批 Query。 func NewQuery(db *gorm.DB, businessResolver BusinessProjectionResolver, extensionResolver ChannelExtensionResolver) *Query { return &Query{db: db, businessResolver: businessResolver, extensionResolver: extensionResolver} } // GetByID 读取单条通用审批投影;资源不存在、引用失效或无权限统一返回禁止访问。 func (q *Query) GetByID(ctx context.Context, instanceID uint) (*Projection, error) { items, err := q.BatchByIDs(ctx, []uint{instanceID}) if err != nil { return nil, err } if len(items) != 1 { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return &items[0], nil } // BatchByIDs 按调用方当前页实例 ID 批量返回有权读取的审批投影,并保持输入顺序。 func (q *Query) BatchByIDs(ctx context.Context, instanceIDs []uint) ([]Projection, error) { ids, err := normalizeInstanceIDs(instanceIDs) if err != nil { return nil, err } if len(ids) == 0 { return []Projection{}, nil } if q == nil || q.db == nil || q.businessResolver == nil { return nil, errors.New(errors.CodeInternalError, "通用审批 Query 未完整配置") } var records []model.ApprovalInstance if err := q.db.WithContext(ctx).Where("id IN ?", ids).Find(&records).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询通用审批实例失败") } recordByID := make(map[uint]model.ApprovalInstance, len(records)) references := make([]BusinessReference, 0, len(records)) for _, record := range records { recordByID[record.ID] = record references = append(references, BusinessReference{ InstanceID: record.ID, BusinessType: record.BusinessType, BusinessID: record.BusinessID, }) } viewer := Viewer{AccountID: middleware.GetUserIDFromContext(ctx), UserType: middleware.GetUserTypeFromContext(ctx)} if viewer.AccountID == 0 || viewer.UserType == 0 { return nil, errors.New(errors.CodeForbidden) } businessByID, err := q.businessResolver.Resolve(ctx, viewer, references) if err != nil { return nil, err } extensionByID, err := q.resolveExtensions(ctx, viewer, records, businessByID) if err != nil { return nil, err } items := make([]Projection, 0, len(records)) for _, id := range ids { record, exists := recordByID[id] business, allowed := businessByID[id] if !exists || !allowed || !business.Allowed { continue } items = append(items, project(record, business, extensionByID[id])) } return items, nil } func (q *Query) resolveExtensions( ctx context.Context, viewer Viewer, records []model.ApprovalInstance, businessByID map[uint]BusinessProjection, ) (map[uint]any, error) { if q.extensionResolver == nil || viewer.UserType == constants.UserTypeAgent || viewer.UserType == constants.UserTypeEnterprise { return map[uint]any{}, nil } if viewer.UserType != constants.UserTypeSuperAdmin && viewer.UserType != constants.UserTypePlatform { return map[uint]any{}, nil } references := make([]ExtensionReference, 0, len(records)) for _, record := range records { business, exists := businessByID[record.ID] if !exists || !business.Allowed { continue } references = append(references, ExtensionReference{ InstanceID: record.ID, Provider: record.Provider, ExternalRef: record.ExternalRef, }) } if len(references) == 0 { return map[uint]any{}, nil } return q.extensionResolver.Resolve(ctx, viewer, references) } func normalizeInstanceIDs(instanceIDs []uint) ([]uint, error) { if len(instanceIDs) > constants.ApprovalQueryMaxBatchSize { return nil, errors.New(errors.CodeInvalidParam, "单次最多查询 100 条审批摘要") } seen := make(map[uint]struct{}, len(instanceIDs)) ids := make([]uint, 0, len(instanceIDs)) for _, id := range instanceIDs { if id == 0 { return nil, errors.New(errors.CodeInvalidParam) } if _, exists := seen[id]; exists { continue } seen[id] = struct{}{} ids = append(ids, id) } return ids, nil } func project(record model.ApprovalInstance, business BusinessProjection, extension any) Projection { return Projection{ ID: record.ID, BusinessType: record.BusinessType, BusinessID: record.BusinessID, BusinessSummary: business.Summary, SubmitterAccountID: record.SubmitterAccountID, SubmitterName: submitterName(record.SubmitterSnapshot), Provider: record.Provider, Status: record.Status, StatusName: constants.GetApprovalStatusName(record.Status), StatusChangedAt: record.StatusChangedAt, ProcessingStatus: business.ProcessingStatus, ProcessingStatusName: business.ProcessingStatusName, ProcessingSummary: business.ProcessingSummary, ChannelExtension: extension, } } func submitterName(snapshot []byte) string { var value struct { AccountName string `json:"account_name"` } if sonic.Unmarshal(snapshot, &value) != nil || strings.TrimSpace(value.AccountName) == "" { return constants.ApprovalUnknownSubmitterName } return strings.TrimSpace(value.AccountName) }