七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
240
internal/infrastructure/wecom/template_client.go
Normal file
240
internal/infrastructure/wecom/template_client.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
|
||||
wecomapp "github.com/break/junhong_cmp_fiber/internal/application/wecom"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// TemplateClient 读取企微后台已有审批模板的控件结构。
|
||||
type TemplateClient struct {
|
||||
tokens DirectoryTokenProvider
|
||||
integration TokenIntegrationLog
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewTemplateClient 创建企业微信审批模板详情客户端。
|
||||
func NewTemplateClient(tokens DirectoryTokenProvider, integration TokenIntegrationLog, baseURL string, timeout time.Duration) *TemplateClient {
|
||||
if timeout <= 0 {
|
||||
timeout = constants.WeComDefaultHTTPTimeout
|
||||
}
|
||||
return &TemplateClient{
|
||||
tokens: tokens, integration: integration, httpClient: &http.Client{Timeout: timeout},
|
||||
baseURL: strings.TrimRight(baseURL, "/"), now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplateDetail 获取模板详情并只投影控件结构,不保存审批节点和审批人规则。
|
||||
func (c *TemplateClient) GetTemplateDetail(ctx context.Context, applicationID uint, templateID string) (wecomapp.TemplateDefinition, error) {
|
||||
if c == nil || c.tokens == nil || c.integration == nil || c.httpClient == nil {
|
||||
return wecomapp.TemplateDefinition{}, errors.New(errors.CodeServiceUnavailable, "企业微信模板服务未配置")
|
||||
}
|
||||
token, err := c.tokens.GetAccessToken(ctx, applicationID)
|
||||
if err != nil {
|
||||
return wecomapp.TemplateDefinition{}, err
|
||||
}
|
||||
request, err := c.newRequest(ctx, token, templateID)
|
||||
if err != nil {
|
||||
return wecomapp.TemplateDefinition{}, err
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(applicationID), 10)
|
||||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||||
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: constants.IntegrationOperationWeComTemplateDetail, ResourceType: constants.WeComApprovalSceneResourceType,
|
||||
ResourceID: &resourceID, RequestSummary: map[string]any{
|
||||
"application_id": applicationID, "template_id": templateID,
|
||||
}, RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
return wecomapp.TemplateDefinition{}, err
|
||||
}
|
||||
startedAt := c.now()
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return wecomapp.TemplateDefinition{}, c.completeTemplateFailure(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 wecomapp.TemplateDefinition{}, c.completeTemplateFailure(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信模板详情响应无效", startedAt)
|
||||
}
|
||||
definition, errCode, errMsg, err := parseTemplateDefinition(templateID, body)
|
||||
if err != nil || response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices || errCode != 0 {
|
||||
providerCode := strconv.FormatInt(errCode, 10)
|
||||
if errCode == 0 {
|
||||
providerCode = strconv.Itoa(response.StatusCode)
|
||||
}
|
||||
return wecomapp.TemplateDefinition{}, c.completeTemplateFailure(ctx, attempt.IntegrationID, response.StatusCode, providerCode, errMsg, startedAt)
|
||||
}
|
||||
_, 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{"errcode": errCode, "control_count": len(definition.Controls)},
|
||||
DurationMS: c.now().Sub(startedAt).Milliseconds(),
|
||||
})
|
||||
if err != nil {
|
||||
return wecomapp.TemplateDefinition{}, err
|
||||
}
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
func (c *TemplateClient) newRequest(ctx context.Context, token, templateID string) (*http.Request, error) {
|
||||
endpoint, err := url.Parse(c.baseURL + "/cgi-bin/oa/gettemplatedetail")
|
||||
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{"template_id": templateID})
|
||||
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 parseTemplateDefinition(templateID string, body []byte) (wecomapp.TemplateDefinition, int64, string, error) {
|
||||
var payload map[string]any
|
||||
if err := sonic.Unmarshal(body, &payload); err != nil {
|
||||
return wecomapp.TemplateDefinition{}, 0, "", err
|
||||
}
|
||||
errCode := numberToInt64(payload["errcode"])
|
||||
errMsg, _ := payload["errmsg"].(string)
|
||||
if errCode != 0 {
|
||||
return wecomapp.TemplateDefinition{}, errCode, errMsg, nil
|
||||
}
|
||||
content, ok := payload["template_content"].(map[string]any)
|
||||
if !ok {
|
||||
return wecomapp.TemplateDefinition{}, errCode, errMsg, errors.New(errors.CodeServiceUnavailable, "企业微信模板详情缺少模板内容")
|
||||
}
|
||||
controls := make([]wecomapp.TemplateControl, 0)
|
||||
seen := make(map[string]struct{})
|
||||
collectTemplateControls(content["controls"], &controls, seen)
|
||||
if len(controls) == 0 {
|
||||
return wecomapp.TemplateDefinition{}, errCode, errMsg, errors.New(errors.CodeInvalidParam, "企业微信模板没有可映射控件")
|
||||
}
|
||||
name := localizedText(content["template_name"])
|
||||
if name == "" {
|
||||
name = localizedText(content["template_names"])
|
||||
}
|
||||
return wecomapp.TemplateDefinition{
|
||||
TemplateID: templateID, Name: name, Controls: controls,
|
||||
}, errCode, errMsg, nil
|
||||
}
|
||||
|
||||
func collectTemplateControls(value any, result *[]wecomapp.TemplateControl, seen map[string]struct{}) {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
collectTemplateControls(item, result, seen)
|
||||
}
|
||||
case map[string]any:
|
||||
if property, ok := typed["property"].(map[string]any); ok {
|
||||
id, _ := property["id"].(string)
|
||||
controlType, _ := property["control"].(string)
|
||||
if id != "" && controlType != "" {
|
||||
if _, exists := seen[id]; !exists {
|
||||
seen[id] = struct{}{}
|
||||
*result = append(*result, wecomapp.TemplateControl{
|
||||
ID: id, Type: controlType, Title: localizedText(property["title"]),
|
||||
Required: numberToInt64(property["require"]) == 1, OptionKeys: collectOptionKeys(typed),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range typed {
|
||||
collectTemplateControls(item, result, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectOptionKeys(value any) []string {
|
||||
keys := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
var walk func(any)
|
||||
walk = func(current any) {
|
||||
switch typed := current.(type) {
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
walk(item)
|
||||
}
|
||||
case map[string]any:
|
||||
if key, ok := typed["key"].(string); ok && key != "" {
|
||||
if _, exists := seen[key]; !exists {
|
||||
seen[key] = struct{}{}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
for _, item := range typed {
|
||||
walk(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(value)
|
||||
return keys
|
||||
}
|
||||
|
||||
func localizedText(value any) string {
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
if items, ok := value.([]any); ok {
|
||||
for _, item := range items {
|
||||
if translated, ok := item.(map[string]any); ok {
|
||||
if text, ok := translated["text"].(string); ok && text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func numberToInt64(value any) int64 {
|
||||
switch number := value.(type) {
|
||||
case float64:
|
||||
return int64(number)
|
||||
case int64:
|
||||
return number
|
||||
case int:
|
||||
return int64(number)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TemplateClient) completeTemplateFailure(ctx context.Context, integrationID string, status int, providerCode, providerMessage string, startedAt time.Time) error {
|
||||
if providerMessage == "" {
|
||||
providerMessage = "企业微信模板详情接口返回失败"
|
||||
}
|
||||
_, err := c.integration.Complete(ctx, integrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, HTTPStatus: status, ProviderCode: providerCode,
|
||||
ProviderMessage: providerMessage, ResponseSummary: map[string]any{"success": false},
|
||||
DurationMS: c.now().Sub(startedAt).Milliseconds(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(errors.CodeInvalidParam, "企业微信审批模板无效或不可访问")
|
||||
}
|
||||
Reference in New Issue
Block a user