This commit is contained in:
@@ -43,7 +43,7 @@ func NewDirectoryClient(tokens DirectoryTokenProvider, integration TokenIntegrat
|
||||
}
|
||||
}
|
||||
|
||||
// ListVisibleMembers 拉取根部门及其子部门中当前应用可见的成员。
|
||||
// ListVisibleMembers 按应用当前可见部门拉取成员。
|
||||
func (c *DirectoryClient) ListVisibleMembers(ctx context.Context, applicationID uint) ([]wecomapp.DirectoryMember, error) {
|
||||
if c == nil || c.tokens == nil || c.integration == nil || c.httpClient == nil || applicationID == 0 {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信通讯录服务未配置")
|
||||
@@ -52,7 +52,64 @@ func (c *DirectoryClient) ListVisibleMembers(ctx context.Context, applicationID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := c.newListRequest(ctx, token)
|
||||
departments, err := c.listVisibleDepartments(ctx, applicationID, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
departmentIDs := visibleDepartmentRoots(departments)
|
||||
remoteMembers := make([]directoryMember, 0)
|
||||
for _, departmentID := range departmentIDs {
|
||||
members, err := c.listDepartmentMembers(ctx, applicationID, token, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteMembers = append(remoteMembers, members...)
|
||||
}
|
||||
return normalizeRemoteMembers(remoteMembers), nil
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) listVisibleDepartments(ctx context.Context, applicationID uint, token string) ([]directoryDepartment, error) {
|
||||
request, err := c.newDepartmentListRequest(ctx, token)
|
||||
if err != nil {
|
||||
return nil, 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.IntegrationOperationWeComVisibleDepartments, ResourceType: constants.WeComApplicationResourceType,
|
||||
ResourceID: &resourceID, RequestSummary: map[string]any{"application_id": applicationID},
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startedAt := c.now()
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, 0, "request_failed", "企业微信通讯录请求失败", startedAt)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var result departmentResponse
|
||||
if err := c.readResponse(response, &result); err != nil {
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信通讯录响应无效", startedAt)
|
||||
}
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices || result.ErrCode != 0 {
|
||||
providerCode := strconv.FormatInt(result.ErrCode, 10)
|
||||
if result.ErrCode == 0 {
|
||||
providerCode = strconv.Itoa(response.StatusCode)
|
||||
}
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, response.StatusCode, providerCode, result.ErrMsg, startedAt)
|
||||
}
|
||||
if err := c.completeSuccess(ctx, attempt.IntegrationID, response.StatusCode, result.ErrCode, result.ErrMsg,
|
||||
map[string]any{"errcode": result.ErrCode, "department_count": len(result.Departments)}, startedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Departments, nil
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) listDepartmentMembers(ctx context.Context, applicationID uint, token string, departmentID int64) ([]directoryMember, error) {
|
||||
request, err := c.newMemberListRequest(ctx, token, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,7 +119,7 @@ func (c *DirectoryClient) ListVisibleMembers(ctx context.Context, applicationID
|
||||
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: constants.IntegrationOperationWeComVisibleMembers, ResourceType: constants.WeComApplicationResourceType,
|
||||
ResourceID: &resourceID, RequestSummary: map[string]any{
|
||||
"application_id": applicationID, "department_id": constants.WeComRootDepartmentID, "fetch_child": true,
|
||||
"application_id": applicationID, "department_id": departmentID, "fetch_child": true,
|
||||
}, RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -74,8 +131,8 @@ func (c *DirectoryClient) ListVisibleMembers(ctx context.Context, applicationID
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, 0, "request_failed", "企业微信通讯录请求失败", startedAt)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
result, err := c.readResponse(response)
|
||||
if err != nil {
|
||||
var result directoryResponse
|
||||
if err := c.readResponse(response, &result); err != nil {
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, response.StatusCode, "invalid_response", "企业微信通讯录响应无效", startedAt)
|
||||
}
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices || result.ErrCode != 0 {
|
||||
@@ -85,21 +142,36 @@ func (c *DirectoryClient) ListVisibleMembers(ctx context.Context, applicationID
|
||||
}
|
||||
return nil, c.completeFailed(ctx, attempt.IntegrationID, response.StatusCode, providerCode, result.ErrMsg, startedAt)
|
||||
}
|
||||
members := normalizeRemoteMembers(result.UserList)
|
||||
if err := c.completeSuccess(ctx, attempt.IntegrationID, response.StatusCode, result, len(members), startedAt); err != nil {
|
||||
if err := c.completeSuccess(ctx, attempt.IntegrationID, response.StatusCode, result.ErrCode, result.ErrMsg,
|
||||
map[string]any{"errcode": result.ErrCode, "member_count": len(result.UserList)}, startedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return members, nil
|
||||
return result.UserList, nil
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) newListRequest(ctx context.Context, token string) (*http.Request, error) {
|
||||
func (c *DirectoryClient) newDepartmentListRequest(ctx context.Context, token string) (*http.Request, error) {
|
||||
endpoint, err := url.Parse(c.baseURL + "/cgi-bin/department/list")
|
||||
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()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建企业微信部门请求失败")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) newMemberListRequest(ctx context.Context, token string, departmentID int64) (*http.Request, error) {
|
||||
endpoint, err := url.Parse(c.baseURL + "/cgi-bin/user/simplelist")
|
||||
if err != nil || endpoint.Scheme == "" || endpoint.Host == "" {
|
||||
return nil, errors.New(errors.CodeWeComCredentialInvalid, "企业微信 API 地址配置无效")
|
||||
}
|
||||
query := endpoint.Query()
|
||||
query.Set("access_token", token)
|
||||
query.Set("department_id", strconv.FormatInt(constants.WeComRootDepartmentID, 10))
|
||||
query.Set("department_id", strconv.FormatInt(departmentID, 10))
|
||||
query.Set("fetch_child", "1")
|
||||
endpoint.RawQuery = query.Encode()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
@@ -115,25 +187,54 @@ type directoryResponse struct {
|
||||
UserList []directoryMember `json:"userlist"`
|
||||
}
|
||||
|
||||
type departmentResponse struct {
|
||||
ErrCode int64 `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
Departments []directoryDepartment `json:"department"`
|
||||
}
|
||||
|
||||
type directoryDepartment struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentID int64 `json:"parentid"`
|
||||
}
|
||||
|
||||
type directoryMember struct {
|
||||
UserID string `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
Department []int64 `json:"department"`
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) readResponse(response *http.Response) (directoryResponse, error) {
|
||||
var result directoryResponse
|
||||
func (c *DirectoryClient) readResponse(response *http.Response, result any) error {
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, constants.WeComDirectoryMaxResponseBodyBytes+1))
|
||||
if err != nil {
|
||||
return result, errors.Wrap(errors.CodeServiceUnavailable, err, "读取企业微信通讯录响应失败")
|
||||
return errors.Wrap(errors.CodeServiceUnavailable, err, "读取企业微信通讯录响应失败")
|
||||
}
|
||||
if int64(len(body)) > constants.WeComDirectoryMaxResponseBodyBytes {
|
||||
return result, errors.New(errors.CodeServiceUnavailable, "企业微信通讯录响应过大")
|
||||
return errors.New(errors.CodeServiceUnavailable, "企业微信通讯录响应过大")
|
||||
}
|
||||
if err := sonic.Unmarshal(body, &result); err != nil {
|
||||
return result, errors.Wrap(errors.CodeServiceUnavailable, err, "解析企业微信通讯录响应失败")
|
||||
if err := sonic.Unmarshal(body, result); err != nil {
|
||||
return errors.Wrap(errors.CodeServiceUnavailable, err, "解析企业微信通讯录响应失败")
|
||||
}
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func visibleDepartmentRoots(departments []directoryDepartment) []int64 {
|
||||
visible := make(map[int64]struct{}, len(departments))
|
||||
for _, department := range departments {
|
||||
if department.ID > 0 {
|
||||
visible[department.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
result := make([]int64, 0, len(departments))
|
||||
for _, department := range departments {
|
||||
if department.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, parentVisible := visible[department.ParentID]; department.ParentID <= 0 || department.ParentID == department.ID || !parentVisible {
|
||||
result = append(result, department.ID)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeRemoteMembers(source []directoryMember) []wecomapp.DirectoryMember {
|
||||
@@ -157,12 +258,11 @@ func normalizeRemoteMembers(source []directoryMember) []wecomapp.DirectoryMember
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *DirectoryClient) completeSuccess(ctx context.Context, integrationID string, status int, result directoryResponse, memberCount int, startedAt time.Time) error {
|
||||
func (c *DirectoryClient) completeSuccess(ctx context.Context, integrationID string, status int, providerCode int64, providerMessage string, responseSummary map[string]any, startedAt time.Time) error {
|
||||
_, err := c.integration.Complete(ctx, integrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, HTTPStatus: status,
|
||||
ProviderCode: strconv.FormatInt(result.ErrCode, 10), ProviderMessage: result.ErrMsg,
|
||||
ResponseSummary: map[string]any{"errcode": result.ErrCode, "member_count": memberCount},
|
||||
DurationMS: c.now().Sub(startedAt).Milliseconds(),
|
||||
ProviderCode: strconv.FormatInt(providerCode, 10), ProviderMessage: providerMessage,
|
||||
ResponseSummary: responseSummary, DurationMS: c.now().Sub(startedAt).Milliseconds(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user