diff --git a/server/feishu-sync/internal/config/config.go b/server/feishu-sync/internal/config/config.go index 1224b8b8..af335cd4 100644 --- a/server/feishu-sync/internal/config/config.go +++ b/server/feishu-sync/internal/config/config.go @@ -14,5 +14,7 @@ type Config struct { ApiHost string EncryptKey string VerificationToken string + AppId string + AppSecret string } } diff --git a/server/feishu-sync/internal/handler/routes.go b/server/feishu-sync/internal/handler/routes.go index db0e1b4c..db0c1419 100644 --- a/server/feishu-sync/internal/handler/routes.go +++ b/server/feishu-sync/internal/handler/routes.go @@ -17,6 +17,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/api/feishu/webhook", Handler: WebhookHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/api/feishu/sync_feishu_departments", + Handler: SyncFeiShuGroupsHandler(serverCtx), + }, }, ) } diff --git a/server/feishu-sync/internal/handler/syncfeishugroupshandler.go b/server/feishu-sync/internal/handler/syncfeishugroupshandler.go new file mode 100644 index 00000000..4c97aee1 --- /dev/null +++ b/server/feishu-sync/internal/handler/syncfeishugroupshandler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "net/http" + "reflect" + + "fusenapi/utils/basic" + + "fusenapi/server/feishu-sync/internal/logic" + "fusenapi/server/feishu-sync/internal/svc" + "fusenapi/server/feishu-sync/internal/types" +) + +func SyncFeiShuGroupsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var req types.Request + userinfo, err := basic.RequestParse(w, r, svcCtx, &req) + if err != nil { + return + } + + // 创建一个业务逻辑层实例 + l := logic.NewSyncFeiShuGroupsLogic(r.Context(), svcCtx) + + rl := reflect.ValueOf(l) + basic.BeforeLogic(w, r, rl) + + resp := l.SyncFeiShuGroups(&req, userinfo) + + if !basic.AfterLogic(w, r, rl, resp) { + basic.NormalAfterLogic(w, r, resp) + } + } +} diff --git a/server/feishu-sync/internal/logic/syncfeishugroupslogic.go b/server/feishu-sync/internal/logic/syncfeishugroupslogic.go new file mode 100644 index 00000000..8ab2800d --- /dev/null +++ b/server/feishu-sync/internal/logic/syncfeishugroupslogic.go @@ -0,0 +1,57 @@ +package logic + +import ( + "fmt" + "fusenapi/utils/auth" + "fusenapi/utils/basic" + "fusenapi/utils/feishu" + "log" + + "context" + + "fusenapi/server/feishu-sync/internal/svc" + "fusenapi/server/feishu-sync/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SyncFeiShuGroupsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSyncFeiShuGroupsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SyncFeiShuGroupsLogic { + return &SyncFeiShuGroupsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 处理进入前逻辑w,r +// func (l *SyncFeiShuGroupsLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { +// } + +func (l *SyncFeiShuGroupsLogic) SyncFeiShuGroups(req *types.Request, userinfo *auth.UserInfo) (resp *basic.Response) { + feishuApi := &feishu.FeiShuApi{ + AppId: l.svcCtx.Config.FeiShu.AppId, + AppSecret: l.svcCtx.Config.FeiShu.AppSecret, + ApiHost: l.svcCtx.Config.FeiShu.ApiHost, + } + token, err := feishuApi.GetTenantAccessToken() + if err != nil { + log.Fatalln(err) + } + users, err := feishuApi.FindUsersByDepartment(token.TenantAccessToken) + if err != nil { + log.Fatalln(err) + } + fmt.Println(users) + return resp.SetStatus(basic.CodeOK) +} + +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 +// func (l *SyncFeiShuGroupsLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { +// // httpx.OkJsonCtx(r.Context(), w, resp) +// } diff --git a/server_api/feishu-sync.api b/server_api/feishu-sync.api index 31db4bea..4528b483 100644 --- a/server_api/feishu-sync.api +++ b/server_api/feishu-sync.api @@ -13,4 +13,7 @@ service feishu-sync { //飞书ticket webhook事件接口 @handler WebhookHandler post /api/feishu/webhook(request) returns (response); + //同步飞书分组(现在是get请求) + @handler SyncFeiShuGroupsHandler + get /api/feishu/sync_feishu_departments(request) returns (response); } \ No newline at end of file diff --git a/utils/feishu/api.go b/utils/feishu/api.go new file mode 100644 index 00000000..4154c77b --- /dev/null +++ b/utils/feishu/api.go @@ -0,0 +1,231 @@ +package feishu + +import ( + "bytes" + "encoding/json" + "fusenapi/utils/curl" + "time" +) + +type FeiShuApi struct { + AppId string + AppSecret string + ApiHost string +} + +// 获取tenant_access_token +type GetTenantAccessTokenRsp struct { + Code int `json:"code"` + Msg string `json:"msg"` + TenantAccessToken string `json:"tenant_access_token"` + Expire int `json:"expire"` +} + +func (f *FeiShuApi) GetTenantAccessToken() (resp GetTenantAccessTokenRsp, err error) { + url := f.ApiHost + "/open-apis/auth/v3/tenant_access_token/internal" + header := map[string]string{"Content-Type": "application/json; charset=utf-8"} + req := map[string]interface{}{ + "app_secret": f.AppSecret, + "app_id": f.AppId, + } + postData, _ := json.Marshal(req) + rsp, err := curl.ApiCall2(url, "POST", header, bytes.NewReader(postData), time.Second*15) + if err != nil { + return GetTenantAccessTokenRsp{}, err + } + if err = json.Unmarshal(rsp, &resp); err != nil { + return GetTenantAccessTokenRsp{}, err + } + return resp, nil +} + +// 获取部门列表 +type GetDepartmentsFeiShuRsp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + HasMore bool `json:"has_more"` + PageToken string `json:"page_token"` + Items []DeptItem `json:"items"` + } `json:"data"` +} +type DeptItem struct { + Name string `json:"name"` + I18NName struct { + ZhCn string `json:"zh_cn"` + JaJp string `json:"ja_jp"` + EnUs string `json:"en_us"` + } `json:"i18n_name"` + ParentDepartmentId string `json:"parent_department_id"` + DepartmentId string `json:"department_id"` + OpenDepartmentId string `json:"open_department_id"` + LeaderUserId string `json:"leader_user_id"` + ChatId string `json:"chat_id"` + Order string `json:"order"` + UnitIds []string `json:"unit_ids"` + MemberCount int `json:"member_count"` + Status struct { + IsDeleted bool `json:"is_deleted"` + } `json:"status"` +} + +func (f *FeiShuApi) GetDepartments(tenantAccessToken string) (resp []DeptItem, err error) { + url := f.ApiHost + "/open-apis/contact/v3/departments" + header := map[string]string{ + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Bearer " + tenantAccessToken, + } + req := map[string]interface{}{ + "fetch_child": true, + "page_token": "", + "page_size": 50, + "department_id": "0", + } + for { + postData, _ := json.Marshal(req) + //先不循环 + requestRsp, err := curl.ApiCall2(url, "GET", header, bytes.NewReader(postData), time.Second*15) + if err != nil { + return nil, err + } + var parseData GetDepartmentsFeiShuRsp + if err = json.Unmarshal(requestRsp, &parseData); err != nil { + return nil, err + } + resp = append(resp, parseData.Data.Items...) + if !parseData.Data.HasMore { + break + } + req["page_token"] = parseData.Data.PageToken + } + return resp, nil +} + +// 获取部门直属用户列表 +type FindUsersByDepartmentFeiShuRsp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + HasMore bool `json:"has_more"` + PageToken string `json:"page_token"` + Items []UserItems `json:"items"` + } `json:"data"` +} +type UserItems struct { + UnionId string `json:"union_id"` + UserId string `json:"user_id"` + OpenId string `json:"open_id"` + Name string `json:"name"` + EnName string `json:"en_name"` + Nickname string `json:"nickname"` + Email string `json:"email"` + Mobile string `json:"mobile"` + MobileVisible bool `json:"mobile_visible"` + Gender int `json:"gender"` + AvatarKey string `json:"avatar_key"` + Avatar struct { + Avatar72 string `json:"avatar_72"` + Avatar240 string `json:"avatar_240"` + Avatar640 string `json:"avatar_640"` + AvatarOrigin string `json:"avatar_origin"` + } `json:"avatar"` + Status struct { + IsFrozen bool `json:"is_frozen"` + IsResigned bool `json:"is_resigned"` + IsActivated bool `json:"is_activated"` + IsExited bool `json:"is_exited"` + IsUnjoin bool `json:"is_unjoin"` + } `json:"status"` + DepartmentIds []string `json:"department_ids"` + LeaderUserId string `json:"leader_user_id"` + City string `json:"city"` + Country string `json:"country"` + WorkStation string `json:"work_station"` + JoinTime int `json:"join_time"` + IsTenantManager bool `json:"is_tenant_manager"` + EmployeeNo string `json:"employee_no"` + EmployeeType int `json:"employee_type"` + Orders []struct { + DepartmentId string `json:"department_id"` + UserOrder int `json:"user_order"` + DepartmentOrder int `json:"department_order"` + IsPrimaryDept bool `json:"is_primary_dept"` + } `json:"orders"` + CustomAttrs []struct { + Type string `json:"type"` + Id string `json:"id"` + Value struct { + Text string `json:"text"` + Url string `json:"url"` + PcUrl string `json:"pc_url"` + OptionId string `json:"option_id"` + OptionValue string `json:"option_value"` + Name string `json:"name"` + PictureUrl string `json:"picture_url"` + GenericUser struct { + Id string `json:"id"` + Type int `json:"type"` + } `json:"generic_user"` + } `json:"value"` + } `json:"custom_attrs"` + EnterpriseEmail string `json:"enterprise_email"` + JobTitle string `json:"job_title"` + IsFrozen bool `json:"is_frozen"` + Geo string `json:"geo"` + JobLevelId string `json:"job_level_id"` + JobFamilyId string `json:"job_family_id"` + DepartmentPath []struct { + DepartmentId string `json:"department_id"` + DepartmentName struct { + Name string `json:"name"` + I18NName struct { + ZhCn string `json:"zh_cn"` + JaJp string `json:"ja_jp"` + EnUs string `json:"en_us"` + } `json:"i18n_name"` + } `json:"department_name"` + DepartmentPath struct { + DepartmentIds []string `json:"department_ids"` + DepartmentPathName struct { + Name string `json:"name"` + I18NName struct { + ZhCn string `json:"zh_cn"` + JaJp string `json:"ja_jp"` + EnUs string `json:"en_us"` + } `json:"i18n_name"` + } `json:"department_path_name"` + } `json:"department_path"` + } `json:"department_path"` + DottedLineLeaderUserIds []string `json:"dotted_line_leader_user_ids"` +} + +func (f *FeiShuApi) FindUsersByDepartment(tenantAccessToken string) (resp []UserItems, err error) { + url := f.ApiHost + "/open-apis/contact/v3/users/find_by_department" + header := map[string]string{ + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Bearer " + tenantAccessToken, + } + req := map[string]interface{}{ + "page_token": "", + "page_size": 50, + "department_id": "0", + } + for { + postData, _ := json.Marshal(req) + //先不循环 + requestRsp, err := curl.ApiCall2(url, "GET", header, bytes.NewReader(postData), time.Second*15) + if err != nil { + return nil, err + } + var parseData FindUsersByDepartmentFeiShuRsp + if err = json.Unmarshal(requestRsp, &parseData); err != nil { + return nil, err + } + resp = append(resp, parseData.Data.Items...) + if !parseData.Data.HasMore { + break + } + req["page_token"] = parseData.Data.PageToken + } + return resp, nil +}