package wecom import ( "context" "strings" "github.com/bytedance/sonic" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" approvalquery "github.com/break/junhong_cmp_fiber/internal/query/approval" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // ApprovalApproverProjection 是企微审批节点成员到系统账号的可选映射。 type ApprovalApproverProjection struct { WeComUserID string `json:"wecom_userid"` AccountID *uint `json:"account_id,omitempty"` AccountName string `json:"account_name"` } // ApprovalChannelProjection 是通用审批 Query 返回的企微本地只读扩展。 type ApprovalChannelProjection struct { SPNo string `json:"sp_no"` SPStatus int `json:"sp_status"` Approvers []ApprovalApproverProjection `json:"approvers"` } // ApprovalProjectionResolver 从已同步详情快照批量投影审批人,不调用企业微信接口。 type ApprovalProjectionResolver struct { db *gorm.DB } // NewApprovalProjectionResolver 创建企业微信审批读取投影 Resolver。 func NewApprovalProjectionResolver(db *gorm.DB) *ApprovalProjectionResolver { return &ApprovalProjectionResolver{db: db} } // Resolve 按当前页审批实例批量读取快照,并批量映射同企业下的系统账号。 func (r *ApprovalProjectionResolver) Resolve( ctx context.Context, _ approvalquery.Viewer, references []approvalquery.ExtensionReference, ) (map[uint]any, error) { result := make(map[uint]any) if len(references) == 0 { return result, nil } if r == nil || r.db == nil { return nil, errors.New(errors.CodeInternalError, "企业微信审批读取投影未配置") } instanceIDs := weComExtensionInstanceIDs(references) if len(instanceIDs) == 0 { return result, nil } var contexts []model.WeComApprovalContext if err := r.db.WithContext(ctx).Where("approval_instance_id IN ?", instanceIDs).Find(&contexts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询企业微信审批详情快照失败") } applicationIDs := make([]uint, 0, len(contexts)) for _, channelContext := range contexts { applicationIDs = append(applicationIDs, channelContext.ApplicationID) } corpByApplication, err := r.loadApplicationCorps(ctx, applicationIDs) if err != nil { return nil, err } userIDsByCorp := make(map[string][]string) approversByInstance := make(map[uint][]string, len(contexts)) for _, channelContext := range contexts { userIDs := approvalNodeUserIDs(channelContext.LatestDetailSnapshot) approversByInstance[channelContext.ApprovalInstanceID] = userIDs corpID := corpByApplication[channelContext.ApplicationID] userIDsByCorp[corpID] = append(userIDsByCorp[corpID], userIDs...) } accounts, err := r.loadBoundAccounts(ctx, userIDsByCorp) if err != nil { return nil, err } for _, channelContext := range contexts { corpID := corpByApplication[channelContext.ApplicationID] approvers := make([]ApprovalApproverProjection, 0, len(approversByInstance[channelContext.ApprovalInstanceID])) for _, userID := range approversByInstance[channelContext.ApprovalInstanceID] { item := ApprovalApproverProjection{WeComUserID: userID} if account, exists := accounts[weComAccountKey(corpID, userID)]; exists { accountID := account.ID item.AccountID = &accountID item.AccountName = account.Username } approvers = append(approvers, item) } result[channelContext.ApprovalInstanceID] = ApprovalChannelProjection{ SPNo: channelContext.SPNo, SPStatus: channelContext.LatestSPStatus, Approvers: approvers, } } return result, nil } func weComExtensionInstanceIDs(references []approvalquery.ExtensionReference) []uint { seen := make(map[uint]struct{}, len(references)) ids := make([]uint, 0, len(references)) for _, reference := range references { if reference.InstanceID == 0 || reference.Provider != constants.IntegrationProviderWeCom { continue } if _, exists := seen[reference.InstanceID]; exists { continue } seen[reference.InstanceID] = struct{}{} ids = append(ids, reference.InstanceID) } return ids } func (r *ApprovalProjectionResolver) loadApplicationCorps(ctx context.Context, applicationIDs []uint) (map[uint]string, error) { result := make(map[uint]string) if len(applicationIDs) == 0 { return result, nil } var applications []model.WeComApplication if err := r.db.WithContext(ctx).Select("id", "corp_id").Where("id IN ?", applicationIDs).Find(&applications).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询企业微信应用企业标识失败") } for _, application := range applications { result[application.ID] = application.CorpID } return result, nil } func (r *ApprovalProjectionResolver) loadBoundAccounts(ctx context.Context, userIDsByCorp map[string][]string) (map[string]model.Account, error) { result := make(map[string]model.Account) query := r.db.WithContext(ctx).Model(&model.Account{}).Where("1 = 0") hasCondition := false for corpID, userIDs := range userIDsByCorp { corpID = strings.TrimSpace(corpID) userIDs = uniqueStrings(userIDs) if corpID == "" || len(userIDs) == 0 { continue } query = query.Or("wecom_corp_id = ? AND wecom_userid IN ? AND status = ?", corpID, userIDs, constants.StatusEnabled) hasCondition = true } if !hasCondition { return result, nil } var accounts []model.Account if err := query.Select("id", "username", "wecom_corp_id", "wecom_userid").Find(&accounts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量映射企业微信审批人账号失败") } for _, account := range accounts { result[weComAccountKey(account.WeComCorpID, account.WeComUserID)] = account } return result, nil } func approvalNodeUserIDs(snapshot []byte) []string { var detail struct { SPRecords []struct { Details []struct { Approver struct { UserID string `json:"userid"` } `json:"approver"` } `json:"details"` } `json:"sp_record"` } if sonic.Unmarshal(snapshot, &detail) != nil { return []string{} } values := make([]string, 0) for _, record := range detail.SPRecords { for _, node := range record.Details { values = append(values, node.Approver.UserID) } } return uniqueStrings(values) } func uniqueStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) result := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, exists := seen[value]; exists { continue } seen[value] = struct{}{} result = append(result, value) } return result } func weComAccountKey(corpID, userID string) string { return strings.TrimSpace(corpID) + "\x00" + strings.TrimSpace(userID) } var _ approvalquery.ChannelExtensionResolver = (*ApprovalProjectionResolver)(nil)