Files
junhong_cmp_fiber/internal/query/approval/query.go
2026-07-24 16:07:18 +08:00

219 lines
7.6 KiB
Go

// 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)
}