Files
junhong_cmp_fiber/internal/infrastructure/wecom/approval_detail_client.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

135 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
IntegrationID string
}
// 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, resourceID *string, correlationID 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
}
resourceKey := strings.TrimSpace(spNo)
triggerSeries := "wecom-approval-detail:" + resourceKey
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComApprovalDetail, ResourceType: constants.WeComApprovalInstanceResourceType,
ResourceID: resourceID, ResourceKey: &resourceKey, ExternalID: &spNo,
TriggerSeries: &triggerSeries, CorrelationID: optionalIntegrationString(correlationID),
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, IntegrationID: attempt.IntegrationID}, 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, "获取企业微信审批详情失败")
}