All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
131 lines
5.7 KiB
Go
131 lines
5.7 KiB
Go
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, "获取企业微信审批详情失败")
|
||
}
|