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" ) // ApprovalDetail 是企微审批详情的权威状态和安全原始 JSON 快照。 type ApprovalDetail struct { SPNo string SPStatus int Snapshot []byte } // ApprovalDetailClient 获取企业微信审批申请详情。 type ApprovalDetailClient struct { tokens DirectoryTokenProvider integration TokenIntegrationLog httpClient *http.Client baseURL string now func() time.Time } // NewApprovalDetailClient 创建企业微信审批详情客户端。 func NewApprovalDetailClient(tokens DirectoryTokenProvider, integration TokenIntegrationLog, baseURL string, timeout time.Duration) *ApprovalDetailClient { if timeout <= 0 { timeout = constants.WeComDefaultHTTPTimeout } return &ApprovalDetailClient{tokens: tokens, integration: integration, httpClient: &http.Client{Timeout: timeout}, baseURL: strings.TrimRight(baseURL, "/"), now: time.Now} } // Get 获取审批详情并记录一次真实外呼。 func (c *ApprovalDetailClient) Get(ctx context.Context, applicationID uint, spNo string) (ApprovalDetail, error) { token, err := c.tokens.GetAccessToken(ctx, applicationID) if err != nil { return ApprovalDetail{}, err } request, err := c.newRequest(ctx, token, spNo) if err != nil { return ApprovalDetail{}, err } resourceID := strconv.FormatUint(uint64(applicationID), 10) attempt, err := c.integration.Start(ctx, integrationlog.Attempt{ Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound, Operation: constants.IntegrationOperationWeComApprovalDetail, ResourceType: constants.WeComApprovalInstanceResourceType, ResourceID: &resourceID, ExternalID: &spNo, RequestSummary: map[string]any{"application_id": applicationID, "sp_no": spNo}, }) if err != nil { return ApprovalDetail{}, err } startedAt := c.now() response, err := c.httpClient.Do(request) if err != nil { return ApprovalDetail{}, c.completeFailure(ctx, attempt.IntegrationID, 0, "request_failed", "企业微信审批详情请求失败", startedAt) } defer response.Body.Close() body, err := io.ReadAll(io.LimitReader(response.Body, constants.WeComDirectoryMaxResponseBodyBytes+1)) if err != nil || int64(len(body)) > constants.WeComDirectoryMaxResponseBodyBytes { return ApprovalDetail{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信审批详情响应无效", startedAt) } var payload map[string]any if err := sonic.Unmarshal(body, &payload); err != nil { return ApprovalDetail{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信审批详情响应无效", startedAt) } errCode := numberToInt64(payload["errcode"]) errMsg, _ := payload["errmsg"].(string) info, _ := payload["info"].(map[string]any) status := int(numberToInt64(info["sp_status"])) if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices || errCode != 0 || info == nil { return ApprovalDetail{}, c.completeFailure(ctx, attempt.IntegrationID, response.StatusCode, strconv.FormatInt(errCode, 10), errMsg, startedAt) } snapshot, err := sonic.Marshal(info) if err != nil { return ApprovalDetail{}, errors.Wrap(errors.CodeInternalError, err, "编码企业微信审批详情快照失败") } _, err = c.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{ Result: constants.IntegrationResultSuccess, HTTPStatus: response.StatusCode, ProviderCode: strconv.FormatInt(errCode, 10), ProviderMessage: errMsg, ResponseSummary: map[string]any{"sp_no": spNo, "sp_status": status}, DurationMS: c.now().Sub(startedAt).Milliseconds(), }) return ApprovalDetail{SPNo: spNo, SPStatus: status, Snapshot: snapshot}, err } // newRequest 组装按字符串审批单号查询权威详情的企微请求。 func (c *ApprovalDetailClient) newRequest(ctx context.Context, token, spNo string) (*http.Request, error) { endpoint, err := url.Parse(c.baseURL + "/cgi-bin/oa/getapprovaldetail") 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() body, err := sonic.Marshal(map[string]string{"sp_no": spNo}) 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 } // completeFailure 先终结 Integration Log,再向 Asynq 返回可重试的统一错误。 func (c *ApprovalDetailClient) 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, "获取企业微信审批详情失败") }