219 lines
7.6 KiB
Go
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)
|
|
}
|