package wecom import ( "bytes" "context" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/bytedance/sonic" "github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // ApprovalInfoQuery 描述按审批单提交时间查询审批单号的过滤条件。 type ApprovalInfoQuery struct { ApplicationID uint StartTime time.Time EndTime time.Time TemplateID string CreatorUserID string Cursor string Size int } // ApprovalInfoPage 是企业微信批量审批单号接口的一页结果。 type ApprovalInfoPage struct { SPNos []string NextCursor string } // ApprovalInfoClient 按提交时间窗批量获取企业微信审批单号。 type ApprovalInfoClient struct { tokens DirectoryTokenProvider integration TokenIntegrationLog httpClient *http.Client baseURL string now func() time.Time } // NewApprovalInfoClient 创建企业微信批量审批单号客户端。 func NewApprovalInfoClient(tokens DirectoryTokenProvider, integration TokenIntegrationLog, baseURL string, timeout time.Duration) *ApprovalInfoClient { if timeout <= 0 { timeout = constants.WeComDefaultHTTPTimeout } return &ApprovalInfoClient{ tokens: tokens, integration: integration, httpClient: &http.Client{Timeout: timeout}, baseURL: strings.TrimRight(baseURL, "/"), now: time.Now, } } // List 获取一页审批单号,并为每次真实外呼写入 Integration Log。 func (c *ApprovalInfoClient) List(ctx context.Context, input ApprovalInfoQuery) (ApprovalInfoPage, error) { if c == nil || c.tokens == nil || c.integration == nil || c.httpClient == nil { return ApprovalInfoPage{}, errors.New(errors.CodeServiceUnavailable, "企业微信批量审批单号客户端未配置") } if err := validateApprovalInfoQuery(input); err != nil { return ApprovalInfoPage{}, err } token, err := c.tokens.GetAccessToken(ctx, input.ApplicationID) if err != nil { return ApprovalInfoPage{}, err } request, err := c.newRequest(ctx, token, input) if err != nil { return ApprovalInfoPage{}, err } resourceID := strconv.FormatUint(uint64(input.ApplicationID), 10) attempt, err := c.integration.Start(ctx, integrationlog.Attempt{ Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound, Operation: constants.IntegrationOperationWeComApprovalInfo, ResourceType: constants.WeComApprovalInstanceResourceType, ResourceID: &resourceID, RequestSummary: map[string]any{ "application_id": input.ApplicationID, "starttime": input.StartTime.Unix(), "endtime": input.EndTime.Unix(), "template_id": input.TemplateID, "creator_userid": input.CreatorUserID, "size": input.Size, "cursor_present": strings.TrimSpace(input.Cursor) != "", }, }) if err != nil { return ApprovalInfoPage{}, err } startedAt := c.now() response, err := c.httpClient.Do(request) if err != nil { return ApprovalInfoPage{}, c.completeFailure(ctx, attempt.IntegrationID, 0, "request_failed", "企业微信批量审批单号请求失败", startedAt) } defer response.Body.Close() body, err := io.ReadAll(io.LimitReader(response.Body, constants.WeComMaxResponseBodyBytes+1)) if err != nil || int64(len(body)) > constants.WeComMaxResponseBodyBytes { return ApprovalInfoPage{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信批量审批单号响应无效", startedAt) } var result struct { ErrCode int64 `json:"errcode"` ErrMsg string `json:"errmsg"` SPNoList []string `json:"sp_no_list"` NewNextCursor string `json:"new_next_cursor"` } if err := sonic.Unmarshal(body, &result); err != nil { return ApprovalInfoPage{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信批量审批单号响应无效", startedAt) } if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices || result.ErrCode != 0 { return ApprovalInfoPage{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, strconv.FormatInt(result.ErrCode, 10), result.ErrMsg, startedAt) } spNos := normalizeSPNos(result.SPNoList) _, err = c.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{ Result: constants.IntegrationResultSuccess, HTTPStatus: response.StatusCode, ProviderCode: strconv.FormatInt(result.ErrCode, 10), ProviderMessage: result.ErrMsg, ResponseSummary: map[string]any{ "sp_no_count": len(spNos), "next_cursor_present": strings.TrimSpace(result.NewNextCursor) != "", }, DurationMS: c.now().Sub(startedAt).Milliseconds(), }) return ApprovalInfoPage{SPNos: spNos, NextCursor: strings.TrimSpace(result.NewNextCursor)}, err } func validateApprovalInfoQuery(input ApprovalInfoQuery) error { if input.ApplicationID == 0 || input.StartTime.IsZero() || input.EndTime.IsZero() || !input.StartTime.Before(input.EndTime) { return errors.New(errors.CodeInvalidParam, "企业微信批量审批单号查询参数无效") } if input.EndTime.Sub(input.StartTime) > constants.WeComApprovalInfoMaxWindow { return errors.New(errors.CodeInvalidParam, "企业微信批量审批单号查询时间跨度不能超过 31 天") } if input.Size <= 0 || input.Size > constants.WeComApprovalInfoMaxPageSize { return errors.New(errors.CodeInvalidParam, "企业微信批量审批单号查询每页数量无效") } return nil } // newRequest 组装企业微信批量审批单号请求,筛选值仅来自本地稳定提交快照。 func (c *ApprovalInfoClient) newRequest(ctx context.Context, token string, input ApprovalInfoQuery) (*http.Request, error) { endpoint, err := url.Parse(c.baseURL + "/cgi-bin/oa/getapprovalinfo") if err != nil || endpoint.Scheme == "" || endpoint.Host == "" { return nil, errors.New(errors.CodeWeComCredentialInvalid, "企业微信 API 地址配置无效") } query := endpoint.Query() query.Set("access_token", token) endpoint.RawQuery = query.Encode() filters := make([]map[string]string, 0, 2) if value := strings.TrimSpace(input.TemplateID); value != "" { filters = append(filters, map[string]string{"key": "template_id", "value": value}) } if value := strings.TrimSpace(input.CreatorUserID); value != "" { filters = append(filters, map[string]string{"key": "creator", "value": value}) } payload := map[string]any{ "starttime": input.StartTime.Unix(), "endtime": input.EndTime.Unix(), "new_cursor": strings.TrimSpace(input.Cursor), "size": input.Size, "filters": filters, } body, err := sonic.Marshal(payload) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "编码企业微信批量审批单号请求失败") } request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(body)) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "创建企业微信批量审批单号请求失败") } request.Header.Set("Content-Type", "application/json") return request, nil } func (c *ApprovalInfoClient) completeFailure(ctx context.Context, integrationID string, status int, providerCode, message string, startedAt time.Time) error { if strings.TrimSpace(message) == "" { message = "企业微信批量审批单号接口返回失败" } _, err := c.integration.Complete(ctx, integrationID, integrationlog.Completion{ Result: constants.IntegrationResultFailed, HTTPStatus: status, ProviderCode: providerCode, ProviderMessage: message, ResponseSummary: map[string]any{"success": false}, DurationMS: c.now().Sub(startedAt).Milliseconds(), }) if err != nil { return err } return errors.New(errors.CodeServiceUnavailable, "获取企业微信审批单号失败") } func normalizeSPNos(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 }