├── .gitignore ├── .idea ├── .gitignore ├── idebug.iml ├── modules.xml └── vcs.xml ├── README.md ├── cmd ├── feishu.go ├── main.go ├── output.go └── wx.go ├── config └── config.go ├── go.mod ├── go.sum ├── images ├── image-20230606231323064.png ├── image-20230606231931180.png ├── image-20230606232126727.png ├── image-20230606232756363.png └── image-20230606232943841.png ├── logger └── logger.go ├── main.go ├── makefile ├── plugin ├── feishu │ ├── department.go │ ├── feishu.go │ ├── reqparam.go │ └── user.go ├── reqparams.go └── wechat │ ├── department.go │ ├── reqparam.go │ ├── user.go │ └── wechat.go ├── prompt └── prompt.go └── utils ├── cache.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/idebug.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # idebug 2 | 3 | ## 简介 4 | 5 | 用于通过企业微信的 corpid 和 corpsecret 自动获取access_token以调试接口。 6 | 7 | **仅用于开发人员用作接口调试,请勿用作其他非法用途。** 8 | 9 | ## 使用方法 10 | 11 | 目前仅支持查询功能,暂不支持添加数据。从2022年下半年开始,企业微信对`access_token`的数据访问权限限制的比较严格。 12 | 13 | 所有命令 14 | 15 | ![image-20230606231323064](./images/image-20230606231323064.png) 16 | 17 | 首先设置`corpid`和`corpsecret`,如有需要可以设置代理,之后再执行`run`命令。 18 | 19 | ![image-20230606231931180](./images/image-20230606231931180.png) 20 | 21 | 如果只需要获取用户数据直接执行`user --dump`即可,文件会自动保存为excel和对应的部门树html文件。 22 | 23 | ![image-20230606232126727](./images/image-20230606232126727.png) 24 | 25 | ![image-20230606232756363](./images/image-20230606232756363.png) 26 | 27 | ![image-20230606232943841](./images/image-20230606232943841.png) 28 | 29 | 其他命令请自行查看使用方法。测试用到的`key`比较少,可能存在未知问题。 30 | 31 | ## TODO 32 | 33 | ??? 34 | 35 | -------------------------------------------------------------------------------- /cmd/feishu.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/spf13/cobra" 8 | "idebug/logger" 9 | fs "idebug/plugin/feishu" 10 | "idebug/utils" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const feishuUsage = mainUsage + `feishu Module: 17 | 关于--dt和--ut说明,飞书部门和用户一般都有两种类型id,用户为user_id和open_id,部门为department_id和open_department_id,下面统一简化为了id和openid,互不影响。注意--dt或者--ut要和提供的或者实际类型对应,如没有与之对应的或者参数则表示设置的是返回的部门id或者用户id的类型,例如dp --dt --ut 表示:根据did查看部门详情,如果提供的did实际类型为id,则--dt的值应该为id,那么此时的--ut则表示返回的用户类型,按需赋值即可。如果某个id类型获取不到数据请更换类型。 18 | set appid 设置appid 19 | set appsecret 设置appsecret 20 | run --dt --ut 获取tenant_access_token 21 | dp --dt --ut 根据查看部门详情 22 | dp ls --dt --ut [-r] 根据查看子部门列表,-r:递归获取(默认false) 23 | user --dt --ut 根据查看用户详情 24 | user ls --dt --ut 根据查看部门直属用户列表,暂不提供递归获取,可使用dump命令代替 25 | email update --uid --pass --ut 根据更新[企业邮箱]密码 26 | dump --dt --ut 根据递归导出部门用户,如果能确定授权范围为所有部门请手动赋值为0 27 | ` 28 | 29 | type feiShuCli struct { 30 | Root *cobra.Command 31 | info *cobra.Command 32 | set *cobra.Command 33 | appId *cobra.Command 34 | appSecret *cobra.Command 35 | run *cobra.Command 36 | dp *cobra.Command 37 | dpLs *cobra.Command 38 | user *cobra.Command 39 | userLs *cobra.Command 40 | email *cobra.Command 41 | emailPasswordUpdate *cobra.Command 42 | dump *cobra.Command 43 | } 44 | 45 | func NewFeiShuCli() *feiShuCli { 46 | cli := &feiShuCli{} 47 | cli.Root = cli.newRoot() 48 | cli.info = cli.newInfo() 49 | cli.set = cli.newSet() 50 | cli.appId = cli.newAppId() 51 | cli.appSecret = cli.newAppSecret() 52 | cli.run = cli.newRun() 53 | cli.dp = cli.newDp() 54 | cli.dpLs = cli.newDpLs() 55 | cli.user = cli.newUser() 56 | cli.userLs = cli.newUserLs() 57 | cli.email = cli.newEmail() 58 | cli.emailPasswordUpdate = cli.newEmailPasswordUpdate() 59 | cli.dump = cli.newDump() 60 | cli.init() 61 | return cli 62 | } 63 | 64 | func (cli *feiShuCli) init() { 65 | cli.dp.PersistentFlags().StringVar(&departmentIdType, "dt", "", "用户ID类型,可选值: id、openid") 66 | cli.dp.PersistentFlags().StringVar(&userIdType, "ut", "", "用户ID类型,可选值: id、openid") 67 | cli.dpLs.Flags().BoolVarP(&recurse, "re", "r", false, "是否递归获取,默认false") 68 | 69 | cli.user.PersistentFlags().StringVar(&departmentIdType, "dt", "", "用户IID类型,可选值: id、openid") 70 | cli.user.PersistentFlags().StringVar(&userIdType, "ut", "", "用户ID类型,可选值: id、openid") 71 | //TODO: cli.userLs.Flags().BoolVarP(&recurse, "re", "r", false, "是否递归获取,默认false") 72 | 73 | cli.emailPasswordUpdate.Flags().StringVar(&userIdType, "ut", "", "用户ID类型,可选值: id、openid") 74 | cli.emailPasswordUpdate.Flags().StringVar(&password, "pass", "", "企业邮箱新密码") 75 | cli.emailPasswordUpdate.Flags().StringVar(&userId, "uid", "", "用户ID") 76 | cli.emailPasswordUpdate.MarkFlagRequired("pass") 77 | cli.emailPasswordUpdate.MarkFlagRequired("uid") 78 | cli.emailPasswordUpdate.MarkFlagRequired("ut") 79 | 80 | cli.run.Flags().StringVar(&userIdType, "ut", "", "用户ID类型,可选值: id、openid") 81 | cli.run.Flags().StringVar(&departmentIdType, "dt", "", "部门ID类型,可选值: id、openid") 82 | 83 | cli.dump.Flags().StringVar(&userIdType, "ut", "", "用户ID类型,可选值: id、openid") 84 | cli.dump.Flags().StringVar(&departmentIdType, "dt", "", "部门ID类型,可选值: id、openid") 85 | cli.dump.MarkFlagRequired("uid") 86 | cli.dump.MarkFlagRequired("ut") 87 | 88 | cli.set.AddCommand(cli.appId, cli.appSecret, newProxy()) 89 | cli.dp.AddCommand(cli.dpLs) 90 | cli.user.AddCommand(cli.userLs) 91 | cli.email.AddCommand(cli.emailPasswordUpdate) 92 | cli.Root.AddCommand(cli.set, cli.run, cli.info, cli.dp, cli.user, cli.email, cli.dump) 93 | 94 | cli.setHelpV1(cli.Root, cli.info, cli.set, cli.appId, cli.appSecret, cli.run, cli.dp, cli.dpLs, cli.user, cli.userLs, cli.email, cli.emailPasswordUpdate, cli.dump) 95 | } 96 | 97 | func (cli *feiShuCli) newRoot() *cobra.Command { 98 | return &cobra.Command{ 99 | Use: "feishu", 100 | Short: `飞书模块`, 101 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 102 | if *CurrentModule != FeiShuModule { 103 | return logger.FormatError(fmt.Errorf("请先设置模块为feishu")) 104 | } 105 | return nil 106 | }, 107 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 108 | reset() 109 | }, 110 | } 111 | } 112 | 113 | func (cli *feiShuCli) newInfo() *cobra.Command { 114 | return &cobra.Command{ 115 | Use: "info", 116 | Short: `查看设置`, 117 | Run: func(cmd *cobra.Command, args []string) { 118 | cli.showClientConfig() 119 | }, 120 | } 121 | } 122 | 123 | func (cli *feiShuCli) newSet() *cobra.Command { 124 | return &cobra.Command{ 125 | Use: "set", 126 | Short: `设置参数`, 127 | } 128 | } 129 | 130 | func (cli *feiShuCli) newAppId() *cobra.Command { 131 | return &cobra.Command{ 132 | Use: "appid", 133 | Short: `设置app_id`, 134 | Run: func(cmd *cobra.Command, args []string) { 135 | if len(args) < 1 { 136 | logger.Warning("请提供一个值") 137 | return 138 | } 139 | FeiShuClient.SetAppId(args[0]) 140 | logger.Success("appid => " + args[0]) 141 | }, 142 | } 143 | } 144 | 145 | func (cli *feiShuCli) newAppSecret() *cobra.Command { 146 | return &cobra.Command{ 147 | Use: "appsecret", 148 | Short: `设置app_secret`, 149 | Run: func(cmd *cobra.Command, args []string) { 150 | if len(args) < 1 { 151 | logger.Warning("请提供一个值") 152 | return 153 | } 154 | FeiShuClient.SetAppSecret(args[0]) 155 | logger.Success("appsecret => " + args[0]) 156 | }, 157 | } 158 | 159 | } 160 | 161 | func (cli *feiShuCli) newRun() *cobra.Command { 162 | return &cobra.Command{ 163 | Use: "run", 164 | Short: `根据设置获取tenant_access_token`, 165 | PreRunE: func(cmd *cobra.Command, args []string) error { 166 | conf := FeiShuClient.GetAuthScopeFromCache() 167 | if conf.AppId == nil || *conf.AppId == "" { 168 | return fmt.Errorf("请先设置appid") 169 | 170 | } 171 | if conf.AppSecret == nil || *conf.AppSecret == "" { 172 | return fmt.Errorf("请先设置appsecret") 173 | } 174 | if err := cli.checkIdType(); err != nil { 175 | return err 176 | } 177 | return nil 178 | }, 179 | Run: func(cmd *cobra.Command, args []string) { 180 | logger.Info("获取信息中,请稍等...") 181 | req := fs.NewGetAuthScopeReqBuilder(FeiShuClient). 182 | UserIdType(userIdTypeMap[userIdType]). 183 | DepartmentIdType(departmentIdTypeMap[departmentIdType]). 184 | Build() 185 | _, err := FeiShuClient.GetNewAuthScope(req) 186 | if err != nil { 187 | if errors.Is(err, context.Canceled) { 188 | return 189 | } 190 | logger.Warning("获取tenant_access_token通信录授权范围失败") 191 | logger.Error(logger.FormatError(err)) 192 | return 193 | } 194 | if HttpCanceled { 195 | return 196 | } 197 | departmentIdTypeCache = departmentIdType 198 | userIdTypeCache = userIdType 199 | cli.showClientConfig() 200 | }, 201 | } 202 | } 203 | 204 | func (cli *feiShuCli) newDp() *cobra.Command { 205 | return &cobra.Command{ 206 | Use: "dp", 207 | Short: `部门操作`, 208 | PreRunE: func(cmd *cobra.Command, args []string) error { 209 | if !cli.hasTenantAccessToken() { 210 | return fmt.Errorf("请先执行run获取tenant_access_token") 211 | } 212 | if err := cli.checkIdType(); err != nil { 213 | return err 214 | } 215 | if len(args) == 0 { 216 | return fmt.Errorf("请提供一个参数作为部门ID或者提供一个子命令") 217 | } 218 | return nil 219 | }, 220 | Run: func(cmd *cobra.Command, args []string) { 221 | var userIdTypeValue = strings.ToUpper(userIdTypeMap[userIdTypeCache]) 222 | var deptIdTypeValue = strings.ToUpper(departmentIdTypeMap[departmentIdTypeCache]) 223 | var uidType = userIdTypeMap[userIdType] 224 | var didType = departmentIdTypeMap[departmentIdType] 225 | var ( 226 | status string 227 | name string 228 | nameEnUs string 229 | nameJaJp string 230 | did string 231 | oid string 232 | parentName string 233 | parentId string 234 | leaderUserId string 235 | leaderUserName string 236 | leaders []string 237 | memberCount int 238 | primaryMemberCount int 239 | hrbps []string 240 | ) 241 | var deptInfo fs.DepartmentEntry 242 | var err error 243 | req := fs.NewGetDepartmentReqBuilder(FeiShuClient). 244 | DepartmentId(args[0]). 245 | DepartmentIdType(didType). 246 | UserIdType(uidType). 247 | Build() 248 | deptInfo, err = FeiShuClient.Department.Get(req) 249 | if err != nil { 250 | if errors.Is(err, context.Canceled) { 251 | return 252 | } 253 | logger.Error(logger.FormatError(err)) 254 | return 255 | } 256 | if HttpCanceled { 257 | return 258 | } 259 | if deptInfo.Status.IsDeleted { 260 | status = "已删除" 261 | } else { 262 | status = "正常" 263 | } 264 | did = deptInfo.DepartmentID 265 | oid = deptInfo.OpenDepartmentID 266 | name = deptInfo.Name 267 | nameEnUs = deptInfo.I18NName.EnUs 268 | nameJaJp = deptInfo.I18NName.JaJp 269 | leaderUserId = deptInfo.LeaderUserID 270 | parentId = deptInfo.ParentDepartmentID 271 | 272 | //获取上级部门信息 273 | req = fs.NewGetDepartmentReqBuilder(FeiShuClient). 274 | DepartmentId(deptInfo.ParentDepartmentID). 275 | DepartmentIdType(departmentIdTypeMap["id"]). 276 | UserIdType(uidType). 277 | Build() 278 | parentDeptInfo, err := FeiShuClient.Department.Get(req) 279 | if err != nil { 280 | if errors.Is(err, context.Canceled) { 281 | return 282 | } 283 | } else { 284 | parentName = parentDeptInfo.Name 285 | } 286 | if HttpCanceled { 287 | return 288 | } 289 | if leaderUserId != "" { 290 | req := fs.NewGetUserReqBuilder(FeiShuClient). 291 | UserId(leaderUserId). 292 | UserIdType(uidType). 293 | Build() 294 | leaderUserInfo, err := FeiShuClient.User.Get(req) 295 | if err == nil { 296 | leaderUserName = leaderUserInfo.Name 297 | } 298 | if errors.Is(err, context.Canceled) { 299 | return 300 | } 301 | } 302 | if HttpCanceled { 303 | return 304 | } 305 | for _, leader := range deptInfo.Leaders { 306 | req := fs.NewGetUserReqBuilder(FeiShuClient). 307 | UserId(leader.LeaderID). 308 | UserIdType(uidType). 309 | Build() 310 | leaderInfo, err := FeiShuClient.User.Get(req) 311 | if err != nil { 312 | if errors.Is(err, context.Canceled) { 313 | return 314 | } 315 | leaders = append(leaders, fmt.Sprintf("%s: %s", userIdTypeValue, leader.LeaderID)) 316 | continue 317 | } else { 318 | leaders = append(leaders, fmt.Sprintf("%s(%s: %s)", leaderInfo.Name, userIdTypeValue, leader.LeaderID)) 319 | } 320 | if HttpCanceled { 321 | return 322 | } 323 | } 324 | memberCount = deptInfo.MemberCount 325 | primaryMemberCount = deptInfo.PrimaryMemberCount 326 | if deptInfo.DepartmentHrbps != nil { 327 | for _, hrbp := range deptInfo.DepartmentHrbps { 328 | hrbps = append(hrbps, *hrbp) 329 | } 330 | 331 | } 332 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 333 | fmt.Println(fmt.Sprintf("%-12s: %s", "状态", status)) 334 | fmt.Println(fmt.Sprintf("%-12s: %s", "名称", name)) 335 | if nameEnUs != "" { 336 | fmt.Println(fmt.Sprintf("%-11s: %s", "英文名称", nameEnUs)) 337 | } 338 | if nameJaJp != "" { 339 | fmt.Println(fmt.Sprintf("%-11s: %s", "日文名称", nameJaJp)) 340 | } 341 | fmt.Println(fmt.Sprintf("%-14s: %s", "ID", did)) 342 | fmt.Println(fmt.Sprintf("%-14s: %s", "OPEN_ID", oid)) 343 | fmt.Println(fmt.Sprintf("%-10s: %s", "上级部门", fmt.Sprintf("%s(%s: %s)", parentName, 344 | deptIdTypeValue, parentId))) 345 | if leaderUserId != "" { 346 | fmt.Println(fmt.Sprintf("%-10s: %s", "主管用户", fmt.Sprintf("%s(%s: %s)", leaderUserName, userIdTypeValue, leaderUserId))) 347 | } else { 348 | fmt.Println(fmt.Sprintf("%-10s: %s", "主管用户", "")) 349 | } 350 | if len(leaders) != 0 { 351 | fmt.Println(fmt.Sprintf("%-11s: %s", "负责人", strings.Join(leaders, "、"))) 352 | } else { 353 | fmt.Println(fmt.Sprintf("%-11s: %s", "负责人", "")) 354 | } 355 | fmt.Println(fmt.Sprintf("%-10s: %d", "用户个数", memberCount)) 356 | fmt.Println(fmt.Sprintf("%-8s: %d", "主属用户个数", primaryMemberCount)) 357 | if len(hrbps) > 0 { 358 | fmt.Println(fmt.Sprintf("%-12s: %s", "部门HRBP", strings.Join(hrbps, "、"))) 359 | } else { 360 | fmt.Println(fmt.Sprintf("%-12s: %s", "部门HRBP", "")) 361 | } 362 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 363 | }, 364 | } 365 | } 366 | 367 | func (cli *feiShuCli) newDpLs() *cobra.Command { 368 | return &cobra.Command{ 369 | Use: "ls", 370 | Short: `根据部门ID获取子部门列表`, 371 | PreRunE: func(cmd *cobra.Command, args []string) error { 372 | if len(args) == 0 { 373 | return fmt.Errorf("请提供一个参数作为部门ID") 374 | } 375 | return nil 376 | }, 377 | Run: func(cmd *cobra.Command, args []string) { 378 | var index = 0 379 | var depts []*fs.DepartmentEntry 380 | err := cli.recursePrintDept(depts, args[0], departmentIdTypeMap[departmentIdType], userIdTypeMap[userIdType], 0, &index) 381 | if err != nil { 382 | if errors.Is(err, context.Canceled) { 383 | return 384 | } 385 | logger.Error(logger.FormatError(err)) 386 | return 387 | } 388 | }, 389 | } 390 | } 391 | 392 | func (cli *feiShuCli) newUser() *cobra.Command { 393 | return &cobra.Command{ 394 | Use: "user", 395 | Short: `用户操作`, 396 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 397 | if !cli.hasTenantAccessToken() { 398 | return fmt.Errorf("请先执行run获取tenant_access_token") 399 | } 400 | if err := cli.checkIdType(); err != nil { 401 | return err 402 | } 403 | return nil 404 | }, 405 | PreRunE: func(cmd *cobra.Command, args []string) error { 406 | if len(args) == 0 { 407 | return fmt.Errorf("请提供一个参数作为用户ID或者提供一个子命令") 408 | } 409 | return nil 410 | }, 411 | Run: func(cmd *cobra.Command, args []string) { 412 | req := fs.NewGetUserReqBuilder(FeiShuClient).UserId(args[0]). 413 | UserIdType(userIdTypeMap[userIdType]). 414 | DepartmentIdType(departmentIdTypeMap[departmentIdType]).Build() 415 | userInfo, err := FeiShuClient.User.Get(req) 416 | if err != nil { 417 | if errors.Is(err, context.Canceled) { 418 | return 419 | } 420 | logger.Error(logger.FormatError(err)) 421 | return 422 | } 423 | if HttpCanceled { 424 | return 425 | } 426 | cli.showUserInfo(*userInfo, false) 427 | }} 428 | } 429 | 430 | func (cli *feiShuCli) newUserLs() *cobra.Command { 431 | return &cobra.Command{ 432 | Use: "ls", 433 | Short: `根据部门ID获取用户列表`, 434 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 435 | if !cli.hasTenantAccessToken() { 436 | return fmt.Errorf("请先执行run获取tenant_access_token") 437 | } 438 | if err := cli.checkIdType(); err != nil { 439 | return err 440 | } 441 | return nil 442 | }, 443 | PreRunE: func(cmd *cobra.Command, args []string) error { 444 | if len(args) == 0 { 445 | return fmt.Errorf("请提供一个参数作为部门ID") 446 | } 447 | return nil 448 | }, 449 | Run: func(cmd *cobra.Command, args []string) { 450 | req := fs.NewGetUsersByDepartmentIdReqBuilder(FeiShuClient). 451 | DepartmentId(args[0]). 452 | DepartmentIdType(departmentIdTypeMap[departmentIdType]). 453 | UserIdType(userIdTypeMap[userIdType]). 454 | PageSize(50). 455 | Build() 456 | userList, err := FeiShuClient.User.GetUsersByDepartmentId(req) 457 | if err != nil { 458 | if errors.Is(err, context.Canceled) { 459 | return 460 | } 461 | logger.Error(logger.FormatError(err)) 462 | return 463 | } 464 | if HttpCanceled { 465 | return 466 | } 467 | length := len(userList) 468 | if length == 0 { 469 | logger.Info("无可用数据") 470 | return 471 | } 472 | for _, userInfo := range userList { 473 | //if (verbose > i && verbose >= 0) || (verbose < 0) { 474 | // cli.showUserInfo(*userInfo, true) 475 | //} 476 | cli.showUserInfo(*userInfo, true) 477 | } 478 | }, 479 | } 480 | } 481 | 482 | func (cli *feiShuCli) newEmail() *cobra.Command { 483 | return &cobra.Command{ 484 | Use: "email", 485 | Short: `企业邮箱操作`, 486 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 487 | if !cli.hasTenantAccessToken() { 488 | return fmt.Errorf("请先执行run获取tenant_access_token") 489 | } 490 | var userIdTypeList []string 491 | for idType := range userIdTypeMap { 492 | userIdTypeList = append(userIdTypeList, idType) 493 | } 494 | if !utils.StringInList(userIdType, userIdTypeList) { 495 | return errors.New("--ut :未设置或者是错误的用户ID类型,可选值:" + strings.Join(userIdTypeList, "、")) 496 | } 497 | return nil 498 | }, 499 | Run: func(cmd *cobra.Command, args []string) { 500 | 501 | }, 502 | } 503 | } 504 | 505 | func (cli *feiShuCli) newEmailPasswordUpdate() *cobra.Command { 506 | return &cobra.Command{ 507 | Use: "update", 508 | Short: `更新密码`, 509 | Run: func(cmd *cobra.Command, args []string) { 510 | req := fs.NewUserEmailPasswordChangeReqBuilder(FeiShuClient). 511 | UserIdType(userIdTypeMap[userIdType]). 512 | PostData(userId, password). 513 | Build() 514 | err := FeiShuClient.User.EmailPasswordUpdate(req) 515 | if err != nil { 516 | if errors.Is(err, context.Canceled) { 517 | return 518 | } 519 | logger.Error(logger.FormatError(err)) 520 | return 521 | } 522 | logger.Success("修改成功") 523 | }, 524 | } 525 | } 526 | 527 | func (cli *feiShuCli) newDump() *cobra.Command { 528 | return &cobra.Command{ 529 | Use: "dump", 530 | Short: `根据部门ID导出用户,不提供部门ID则导出通讯录授权范围内所有部门用户`, 531 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 532 | if !cli.hasTenantAccessToken() { 533 | return fmt.Errorf("请先执行run获取tenant_access_token") 534 | } 535 | if err := cli.checkIdType(); err != nil { 536 | return err 537 | } 538 | return nil 539 | }, 540 | Run: func(cmd *cobra.Command, args []string) { 541 | var deptNodeList []*FeiShuDepartmentNode 542 | var uidType = userIdTypeMap[userIdType] 543 | var didType = departmentIdTypeMap[departmentIdType] 544 | conf := FeiShuClient.GetAuthScopeFromCache() 545 | //根据指定部门ID先获取子部门列表 546 | var isErrorOcurred bool 547 | var deptIds []string 548 | if len(args) == 0 { 549 | for deptId := range conf.DepartmentScope { 550 | deptIds = append(deptIds, deptId) 551 | } 552 | } else { 553 | deptIds = append(deptIds, args[0]) 554 | } 555 | for _, deptId := range deptIds { 556 | deptNode := &FeiShuDepartmentNode{ 557 | UnitIds: []*string{}, 558 | DepartmentHrbps: []*string{}, 559 | User: []*fs.UserEntry{}, 560 | Children: []*FeiShuDepartmentNode{}, 561 | } 562 | var deptInfo fs.DepartmentEntry 563 | var err error 564 | if HttpCanceled { 565 | return 566 | } 567 | for i := 0; i < retry; i++ { 568 | if i == 0 { 569 | logger.Info(fmt.Sprintf("正在获取部门[%s]的信息....", deptId)) 570 | } 571 | //获取部门信息 572 | req := fs.NewGetDepartmentReqBuilder(FeiShuClient). 573 | DepartmentId(deptId). 574 | DepartmentIdType(didType). 575 | UserIdType(uidType). 576 | Build() 577 | deptInfo, err = FeiShuClient.Department.Get(req) 578 | if err != nil { 579 | if errors.Is(err, context.Canceled) { 580 | return 581 | } 582 | if i == retry-1 { 583 | logger.Error(logger.FormatError(err)) 584 | logger.Info(fmt.Sprintf("部门[%s]信息获取失败,终止获取", deptId)) 585 | return 586 | } 587 | logger.Error(logger.FormatError(err)) 588 | logger.Info(fmt.Sprintf("部门[%s]信息获取失败,正在重试...", deptId)) 589 | time.Sleep(FeiShuDefaultInterval) 590 | continue 591 | } 592 | break 593 | } 594 | if HttpCanceled { 595 | return 596 | } 597 | deptNode.Name = deptInfo.Name 598 | deptNode.ZhCnName = deptInfo.I18NName.ZhCn 599 | deptNode.JaJpName = deptInfo.I18NName.JaJp 600 | deptNode.EnUsName = deptInfo.I18NName.EnUs 601 | deptNode.DepartmentID = deptInfo.DepartmentID 602 | deptNode.OpenDepartmentID = deptInfo.OpenDepartmentID 603 | deptNode.ParentDepartmentID = deptInfo.ParentDepartmentID 604 | //deptNode.ParentDepartmentName = deptInfo 605 | //deptNode.Status = deptInfo 606 | deptNode.LeaderUserID = deptInfo.LeaderUserID 607 | //deptNode.LeaderUserName = deptInfo 608 | //deptNode.ChatID = deptInfo 609 | 610 | if deptInfo.Status.IsDeleted { 611 | deptNode.Status = "已删除" 612 | } else { 613 | deptNode.Status = "正常" 614 | } 615 | logger.Info(fmt.Sprintf("正在获取部门[%s]主管用户信息...", deptId)) 616 | //获取部门主管领导信息 617 | if deptInfo.LeaderUserID != "" { 618 | for i := 0; i < retry; i++ { 619 | req := fs.NewGetUserReqBuilder(FeiShuClient). 620 | UserId(deptInfo.LeaderUserID). 621 | UserIdType(uidType). 622 | DepartmentIdType(didType). 623 | Build() 624 | userInfo, err := FeiShuClient.User.Get(req) 625 | if err != nil { 626 | if errors.Is(err, context.Canceled) { 627 | return 628 | } 629 | if i == retry-1 { 630 | logger.Error(logger.FormatError(err)) 631 | logger.Info(fmt.Sprintf("部门[%s]主管用户信息获取失败,将会继续执行...", deptId)) 632 | break 633 | } 634 | logger.Error(logger.FormatError(err)) 635 | logger.Info(fmt.Sprintf("部门[%s]主管用户信息获取失败,正在重试...", deptId)) 636 | time.Sleep(FeiShuDefaultInterval) 637 | continue 638 | } 639 | deptNode.LeaderUserName = userInfo.Name 640 | break 641 | } 642 | } 643 | if HttpCanceled { 644 | return 645 | } 646 | deptNode.ChatID = deptInfo.ChatID 647 | 648 | //获取部门用户 649 | logger.Info(fmt.Sprintf("正在获取部门[%s]直属用户列表...", deptId)) 650 | for i := 0; i < retry; i++ { 651 | req1 := fs.NewGetUsersByDepartmentIdReqBuilder(FeiShuClient). 652 | DepartmentId(deptId). 653 | DepartmentIdType(didType).UserIdType(uidType).Build() 654 | users, err := FeiShuClient.User.GetUsersByDepartmentId(req1) 655 | if err != nil { 656 | if errors.Is(err, context.Canceled) { 657 | return 658 | } 659 | if i == retry-1 { 660 | logger.Error(logger.FormatError(err)) 661 | logger.Info(fmt.Sprintf("部门[%s]直属用户列表获取失败,终止获取", deptId)) 662 | return 663 | } 664 | logger.Error(logger.FormatError(err)) 665 | logger.Info(fmt.Sprintf("部门[%s]直属用户列表获取失败,正在重试...", deptId)) 666 | time.Sleep(FeiShuDefaultInterval) 667 | continue 668 | } 669 | deptNodeList = append(deptNodeList, deptNode) 670 | deptNode.User = append(deptNode.User, users...) 671 | err = cli.fetchDepartment(deptNode, deptId, departmentIdTypeMap[departmentIdType], userIdTypeMap[userIdType]) 672 | if err != nil { 673 | if errors.Is(err, context.Canceled) { 674 | return 675 | } 676 | isErrorOcurred = true 677 | logger.Error(logger.FormatError(err)) 678 | } 679 | break 680 | } 681 | if isErrorOcurred { 682 | break 683 | } 684 | if HttpCanceled { 685 | return 686 | } 687 | } 688 | if HttpCanceled { 689 | return 690 | } 691 | if isErrorOcurred { 692 | logger.Warning("终止获取,会保存已获取数据") 693 | } 694 | logger.Info("正在保存至html文件...") 695 | msg, err := cli.saveDepartmentTreeWithUsersToHTML(deptNodeList, "feishu_dump.html") 696 | if err != nil { 697 | logger.Error(logger.FormatError(err)) 698 | logger.Info("保存文件失败") 699 | } else { 700 | logger.Success(msg) 701 | } 702 | logger.Info("正在保存至xlsx文件...") 703 | msg, err = cli.saveDepartmentWithUserToExcel(deptNodeList, "feishu_dump.xlsx") 704 | if err != nil { 705 | logger.Error(logger.FormatError(err)) 706 | logger.Info("保存文件失败") 707 | } else { 708 | logger.Success(msg) 709 | } 710 | }, 711 | } 712 | } 713 | 714 | func (cli *feiShuCli) checkIdType() error { 715 | var deptIdTypeList []string 716 | var userIdTypeList []string 717 | for idType := range departmentIdTypeMap { 718 | deptIdTypeList = append(deptIdTypeList, idType) 719 | } 720 | for idType := range userIdTypeMap { 721 | userIdTypeList = append(userIdTypeList, idType) 722 | } 723 | if !utils.StringInList(departmentIdType, deptIdTypeList) { 724 | return errors.New("--dt :未设置或者是错误的部门ID类型,可选值:" + strings.Join(deptIdTypeList, "、")) 725 | } 726 | if !utils.StringInList(userIdType, userIdTypeList) { 727 | return errors.New("--ut :未设置或者是错误的用户ID类型,可选值:" + strings.Join(userIdTypeList, "、")) 728 | } 729 | return nil 730 | } 731 | 732 | func (cli *feiShuCli) hasTenantAccessToken() bool { 733 | return FeiShuClient.GetTenantAccessTokenFromCache() != "" 734 | } 735 | 736 | func (cli *feiShuCli) showUserInfo(userInfo fs.UserEntry, inLine bool) { 737 | var ( 738 | name string 739 | uid string 740 | oid string 741 | gender string 742 | employeeNo string 743 | status []string 744 | phone string 745 | email string 746 | enterpriseEmail string 747 | isTenantManager string 748 | workLocation string 749 | depts []string 750 | ) 751 | name = userInfo.Name 752 | uid = userInfo.UserId 753 | oid = userInfo.OpenId 754 | if userInfo.Gender == 0 { 755 | gender = "保密" 756 | } else if userInfo.Gender == 1 { 757 | gender = "男" 758 | } else if userInfo.Gender == 2 { 759 | gender = "女" 760 | } 761 | employeeNo = userInfo.EmployeeNo 762 | email = userInfo.Email 763 | enterpriseEmail = userInfo.EnterpriseEmail 764 | if userInfo.Status.IsUnjoin { 765 | status = append(status, "未加入") 766 | } 767 | if userInfo.Status.IsActivated { 768 | status = append(status, "已激活") 769 | } 770 | if userInfo.Status.IsResigned { 771 | status = append(status, "已离职") 772 | } 773 | if userInfo.Status.IsFrozen { 774 | status = append(status, "已冻结") 775 | } 776 | if userInfo.Status.IsExited { 777 | status = append(status, "已退出") 778 | } 779 | phone = userInfo.Mobile 780 | if userInfo.IsTenantManager { 781 | isTenantManager = "是" 782 | } else if userInfo.IsTenantManager { 783 | isTenantManager = "否" 784 | } 785 | for _, deptId := range userInfo.DepartmentIds { 786 | req := fs.NewGetDepartmentReqBuilder(FeiShuClient).DepartmentId(deptId).DepartmentIdType(departmentIdTypeMap[departmentIdType]).Build() 787 | deptInfo, err := FeiShuClient.Department.Get(req) 788 | if err != nil { 789 | if errors.Is(err, context.Canceled) { 790 | return 791 | } 792 | time.Sleep(FeiShuDefaultInterval) 793 | continue 794 | } 795 | var info string 796 | if deptInfo.Name != "" { 797 | info = fmt.Sprintf("%s(%s: %s)", deptInfo.Name, 798 | strings.ToUpper(departmentIdTypeMap["id"]), deptInfo.DepartmentID) 799 | } else if deptInfo.Name == "" { 800 | info = fmt.Sprintf("%s: %s", 801 | strings.ToUpper(departmentIdTypeMap["id"]), deptInfo.DepartmentID) 802 | } 803 | depts = append(depts, info) 804 | time.Sleep(FeiShuDefaultInterval) 805 | } 806 | if userInfo.Country != "" { 807 | workLocation += userInfo.Country + " " 808 | } 809 | if userInfo.City != "" { 810 | workLocation += userInfo.City 811 | } 812 | if inLine { 813 | s := fmt.Sprintf(" -ID[%s] OPEN_ID[%s] 姓名[%s] 性别[%s] 工号[%s] 手机号码[%s] 邮箱[%s] 企业邮箱[%s] 状态[%s] 是否企业管理员[%s] 所属部门[%s] 工作地点[%s]", uid, oid, name, gender, employeeNo, phone, email, enterpriseEmail, strings.Join(status, "、"), isTenantManager, strings.Join(depts, "、"), workLocation) 814 | fmt.Println(s) 815 | return 816 | } 817 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 818 | fmt.Printf("%-14s: %s\n", "USER_ID", uid) 819 | fmt.Printf("%-14s: %s\n", "OPEN_ID", oid) 820 | fmt.Printf("%-12s: %s\n", "姓名", name) 821 | fmt.Printf("%-12s: %s\n", "性别", gender) 822 | fmt.Printf("%-12s: %s\n", "工号", employeeNo) 823 | fmt.Printf("%-10s: %s\n", "手机号码", phone) 824 | fmt.Printf("%-12s: %s\n", "邮箱", email) 825 | fmt.Printf("%-10s: %s\n", "企业邮箱", enterpriseEmail) 826 | fmt.Printf("%-12s: %s\n", "状态", strings.Join(status, "、")) 827 | fmt.Printf("%-9s: %s\n", "是否管理员", isTenantManager) 828 | fmt.Printf("%-10s: %s\n", "所属部门", strings.Join(depts, "、")) 829 | fmt.Printf("%-10s: %s\n", "工作地点", workLocation) 830 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 831 | } 832 | 833 | func (cli *feiShuCli) showClientConfig() { 834 | fsClientConfig := FeiShuClient.GetAuthScopeFromCache() 835 | fmt.Printf("%s\n", strings.Repeat("=", 40)) 836 | if Proxy == nil { 837 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", "")) 838 | } else { 839 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", *Proxy)) 840 | } 841 | if fsClientConfig.AppId == nil { 842 | fmt.Println(fmt.Sprintf("%-17s: %s", "app_id", "")) 843 | } else { 844 | fmt.Println(fmt.Sprintf("%-17s: %s", "app_id", *fsClientConfig.AppId)) 845 | } 846 | if fsClientConfig.AppSecret == nil { 847 | fmt.Println(fmt.Sprintf("%-17s: %s", "app_secret", "")) 848 | } else { 849 | fmt.Println(fmt.Sprintf("%-17s: %s", "app_secret", *fsClientConfig.AppSecret)) 850 | } 851 | if fsClientConfig.TenantAccessToken == nil { 852 | fmt.Println(fmt.Sprintf("%-17s: %s", "tenant_access_token", "")) 853 | } else { 854 | fmt.Println(fmt.Sprintf("%-17s: %s", "tenant_access_token", *fsClientConfig.TenantAccessToken)) 855 | } 856 | var deptScope []string 857 | for deptId, deptName := range fsClientConfig.DepartmentScope { 858 | var deptIdType = "ID" 859 | if departmentIdTypeCache == "open_department_id" { 860 | deptIdType = "OPEN_ID" 861 | } 862 | if deptName != "" { 863 | deptScope = append(deptScope, fmt.Sprintf("%s(%s:%s)", deptName, deptIdType, deptId)) 864 | } else { 865 | deptScope = append(deptScope, fmt.Sprintf("%s:%s", deptIdType, deptId)) 866 | } 867 | 868 | } 869 | fmt.Println("-----------获取通讯录授权范围-----------") 870 | logger.Notice("该接口用于获取应用被授权可访问的通讯录范围,包括可访问的部门列表、用户列表和用户组列表。授权范围为全员时," + 871 | "返回的部门列表为该企业所有的一级部门;否则返回的部门为管理员在设置授权范围时勾选的部门(不包含勾选部门的子部门)") 872 | fmt.Println(fmt.Sprintf("%-13s: %s", "部门范围", strings.Join(deptScope, "、"))) 873 | var groupScope []string 874 | for groupId, groupName := range fsClientConfig.GroupScope { 875 | if groupName != "" { 876 | groupScope = append(groupScope, fmt.Sprintf("%s(ID:%s)", groupName, groupId)) 877 | } else { 878 | groupScope = append(groupScope, fmt.Sprintf("ID:%s", groupId)) 879 | } 880 | } 881 | fmt.Println(fmt.Sprintf("%-12s: %s", "用户组范围", strings.Join(groupScope, "、"))) 882 | var userScope []string 883 | for id, userName := range fsClientConfig.UserScope { 884 | var userIdType = "ID" 885 | if userIdTypeCache == "open_id" { 886 | userIdType = "OPEN_ID" 887 | } 888 | if userName != "" { 889 | userScope = append(userScope, fmt.Sprintf("%s(%s:%s)", userName, userIdType, id)) 890 | } else { 891 | userScope = append(userScope, fmt.Sprintf("%s:%s", userIdType, id)) 892 | } 893 | 894 | } 895 | fmt.Println(fmt.Sprintf("%-13s: %s", "用户范围", strings.Join(userScope, "、"))) 896 | fmt.Printf("%s\n", strings.Repeat("=", 40)) 897 | } 898 | 899 | func (cli *feiShuCli) fetchDepartment(node *FeiShuDepartmentNode, deptId, deptIdType, userIdType string) error { 900 | if HttpCanceled { 901 | return nil 902 | } 903 | var deptChildren []*fs.DepartmentEntry 904 | var err error 905 | logger.Info(fmt.Sprintf("正在获取部门[%s]的子部门...", deptId)) 906 | for i := 0; i < retry; i++ { 907 | req := fs.NewGetDepartmentChildrenReqBuilder(FeiShuClient). 908 | DepartmentId(deptId). 909 | DepartmentIdType(deptIdType). 910 | UserIdType(userIdType). 911 | Fetch(false). 912 | PageSize(50). 913 | Build() 914 | deptChildren, err = FeiShuClient.Department.Children(req) 915 | if err != nil { 916 | if errors.Is(err, context.Canceled) { 917 | return nil 918 | } 919 | if i == retry-1 { 920 | logger.Error(logger.FormatError(err)) 921 | logger.Info(fmt.Sprintf("部门[%s]的子部门获取失败,终止获取", deptId)) 922 | return errors.New("") 923 | } 924 | logger.Error(logger.FormatError(err)) 925 | logger.Info(fmt.Sprintf("部门[%s]的子部门获取失败,正在重试...", deptId)) 926 | time.Sleep(FeiShuDefaultInterval) 927 | continue 928 | } 929 | break 930 | } 931 | if HttpCanceled { 932 | return nil 933 | } 934 | for _, child := range deptChildren { 935 | var id string 936 | if deptIdType == "department_id" { 937 | id = child.DepartmentID 938 | } else { 939 | id = child.OpenDepartmentID 940 | } 941 | req := fs.NewGetDepartmentReqBuilder(FeiShuClient). 942 | DepartmentId(id). 943 | DepartmentIdType(deptIdType). 944 | UserIdType(userIdType). 945 | Build() 946 | deptInfo, err := FeiShuClient.Department.Get(req) 947 | if err != nil { 948 | logger.Error(logger.FormatError(err)) 949 | return errors.New("") 950 | } 951 | if HttpCanceled { 952 | return nil 953 | } 954 | deptNode := &FeiShuDepartmentNode{ 955 | Name: deptInfo.Name, 956 | ZhCnName: deptInfo.I18NName.ZhCn, 957 | JaJpName: deptInfo.I18NName.JaJp, 958 | EnUsName: deptInfo.I18NName.EnUs, 959 | DepartmentID: deptInfo.DepartmentID, 960 | OpenDepartmentID: deptInfo.OpenDepartmentID, 961 | ParentDepartmentID: deptInfo.ParentDepartmentID, 962 | ParentDepartmentName: node.Name, 963 | //Status: "", 964 | LeaderUserID: deptInfo.LeaderUserID, 965 | //LeaderUserName: "", 966 | ChatID: deptInfo.ChatID, 967 | UnitIds: []*string{}, 968 | DepartmentHrbps: []*string{}, 969 | User: []*fs.UserEntry{}, 970 | Children: []*FeiShuDepartmentNode{}, 971 | } 972 | if deptInfo.Status.IsDeleted { 973 | deptNode.Status = "已删除" 974 | } else { 975 | deptNode.Status = "正常" 976 | } 977 | 978 | //获取部门主管领导信息 979 | logger.Info(fmt.Sprintf("正在获取部门[%s]主管用户信息...", id)) 980 | if deptInfo.LeaderUserID != "" { 981 | for i := 0; i < retry; i++ { 982 | req := fs.NewGetUserReqBuilder(FeiShuClient). 983 | UserId(deptInfo.LeaderUserID). 984 | UserIdType(userIdType). 985 | DepartmentIdType(deptIdType). 986 | Build() 987 | userInfo, err := FeiShuClient.User.Get(req) 988 | if err != nil { 989 | if errors.Is(err, context.Canceled) { 990 | return nil 991 | } 992 | if i == retry-1 { 993 | logger.Error(logger.FormatError(err)) 994 | logger.Info(fmt.Sprintf("部门[%s]主管用户信息获取失败,将会继续执行...", id)) 995 | break 996 | } 997 | logger.Error(logger.FormatError(err)) 998 | logger.Info(fmt.Sprintf("部门[%s]主管用户信息获取失败,正在重试...", id)) 999 | time.Sleep(FeiShuDefaultInterval) 1000 | continue 1001 | } 1002 | deptNode.LeaderUserName = userInfo.Name 1003 | break 1004 | } 1005 | } 1006 | if HttpCanceled { 1007 | return nil 1008 | } 1009 | //获取部门用户 1010 | logger.Info(fmt.Sprintf("正在获取部门[%s]直属用户列表...", id)) 1011 | for i := 0; i < retry; i++ { 1012 | req1 := fs.NewGetUsersByDepartmentIdReqBuilder(FeiShuClient). 1013 | DepartmentId(id). 1014 | DepartmentIdType(deptIdType).UserIdType(userIdType).Build() 1015 | users, err := FeiShuClient.User.GetUsersByDepartmentId(req1) 1016 | if err != nil { 1017 | if errors.Is(err, context.Canceled) { 1018 | return nil 1019 | } 1020 | if i == retry-1 { 1021 | logger.Error(logger.FormatError(err)) 1022 | logger.Info(fmt.Sprintf("部门[%s]直属用户列表获取失败,终止获取", id)) 1023 | return errors.New("") 1024 | } 1025 | logger.Error(logger.FormatError(err)) 1026 | logger.Info(fmt.Sprintf("部门[%s]直属用户列表获取失败,正在重试...", id)) 1027 | time.Sleep(FeiShuDefaultInterval) 1028 | continue 1029 | } 1030 | node.Children = append(node.Children, deptNode) 1031 | deptNode.User = append(deptNode.User, users...) 1032 | if HttpCanceled { 1033 | return nil 1034 | } 1035 | err = cli.fetchDepartment(deptNode, id, deptIdType, userIdType) 1036 | if err != nil { 1037 | if errors.Is(err, context.Canceled) { 1038 | return nil 1039 | } 1040 | return err 1041 | } 1042 | break 1043 | } 1044 | } 1045 | return nil 1046 | } 1047 | 1048 | func (cli *feiShuCli) setHelpV1(cmds ...*cobra.Command) { 1049 | for _, cmd := range cmds { 1050 | // 不自己打印会多一个空白行 1051 | cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 1052 | logger.Error(err) 1053 | return nil //返回error会自动打印用法 1054 | }) 1055 | 1056 | cmd.SilenceErrors = true // 禁止打印错误信息 1057 | cmd.DisableSuggestions = true // 禁用帮助信息 1058 | cmd.SilenceUsage = true // 禁用用法信息 1059 | cmd.Flags().SortFlags = false 1060 | 1061 | // 设置自定义的使用帮助函数 1062 | cmd.SetHelpFunc(func(cmd *cobra.Command, s []string) { 1063 | fmt.Print(feishuUsage) 1064 | }) 1065 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 1066 | fmt.Print(feishuUsage) 1067 | return nil 1068 | }) 1069 | } 1070 | } 1071 | 1072 | type FeiShuDepartmentNode struct { 1073 | Name string // 部门名称 1074 | ZhCnName string // 部门的中文名 1075 | JaJpName string // 部门的日文名 1076 | EnUsName string // 部门的英文名 1077 | DepartmentID string // 部门ID 1078 | OpenDepartmentID string // 部门open_department_id 1079 | ParentDepartmentID string // 上级部门ID 1080 | ParentDepartmentName string // 上级部门名称 1081 | Status string // 部门状态 1082 | LeaderUserID string // 主管领导ID 1083 | LeaderUserName string // 主管领导姓名 1084 | ChatID string // 部门群ID 1085 | UnitIds []*string 1086 | DepartmentHrbps []*string 1087 | User []*fs.UserEntry 1088 | Children []*FeiShuDepartmentNode 1089 | } 1090 | 1091 | type colItem struct { 1092 | Name string // 部门名称 1093 | ZhCnName string // 部门的中文名 1094 | JaJpName string // 部门的日文名 1095 | EnUsName string // 部门的英文名 1096 | DepartmentID string // 部门ID 1097 | OpenDepartmentID string // 部门open_department_id 1098 | ParentDepartmentID string // 上级部门ID 1099 | ParentDepartmentName string // 上级部门名称 1100 | Status string // 部门状态 1101 | LeaderUserID string // 主管领导ID 1102 | LeaderUserName string // 主管领导姓名 1103 | DepartmentHrbps []*string 1104 | User fs.UserEntry 1105 | } 1106 | 1107 | func (cli *feiShuCli) fetchColItem(tree []*FeiShuDepartmentNode) []*colItem { 1108 | var items []*colItem 1109 | for _, node := range tree { 1110 | for _, user := range node.User { 1111 | var item = &colItem{ 1112 | Name: node.Name, 1113 | ZhCnName: node.ZhCnName, 1114 | JaJpName: node.JaJpName, 1115 | EnUsName: node.EnUsName, 1116 | DepartmentID: node.DepartmentID, 1117 | OpenDepartmentID: node.OpenDepartmentID, 1118 | ParentDepartmentID: node.ParentDepartmentID, 1119 | ParentDepartmentName: node.ParentDepartmentName, 1120 | Status: node.Status, 1121 | LeaderUserID: node.LeaderUserID, 1122 | LeaderUserName: node.LeaderUserName, 1123 | DepartmentHrbps: node.DepartmentHrbps, 1124 | User: *user, 1125 | } 1126 | items = append(items, item) 1127 | } 1128 | items = append(items, cli.fetchColItem(node.Children)...) 1129 | } 1130 | return items 1131 | } 1132 | 1133 | // saveDepartmentWithUserToExcel 生成包含所属部门ID的用户信息的XLSX文档,保存文件为空则自动生成 1134 | func (cli *feiShuCli) saveDepartmentWithUserToExcel(tree []*FeiShuDepartmentNode, filename string) (string, error) { 1135 | index := strings.LastIndex(filename, ".xlsx") 1136 | if index == -1 { 1137 | filename = filename + ".xlsx" 1138 | } 1139 | tmp := filename 1140 | // 判断文件是否存在 1141 | if utils.IsFileExists(filename) { 1142 | newFilename := generateNewFilename(filename) 1143 | filename = newFilename 1144 | } 1145 | var items []*colItem 1146 | items = append(items, cli.fetchColItem(tree)...) 1147 | // 设置表头 1148 | headers := []any{"id", "部门名称", "部门中文名称", "部门日文名称", "部门英文名称", "部门ID", "部门OPEN_ID", "上级部门ID", "上级部门名称", "部门状态", "部门主管ID", "部门主管姓名", "Hrbps", "用户ID", "用户OPEN_ID", 1149 | "姓名", "英文姓名", "昵称", "性别", "电话号码", "邮箱", "企业邮箱", "用户状态", "用户所属部门", "工作地点", "加入时间", "工号", "是否企业管理员", "职称"} 1150 | 1151 | var data [][]any 1152 | // 写入内容 1153 | for i, item := range items { 1154 | var gender string 1155 | user := item.User 1156 | if user.Gender == 0 { 1157 | gender = "保密" 1158 | } else if user.Gender == 1 { 1159 | gender = "男" 1160 | } else if user.Gender == 2 { 1161 | gender = "女" 1162 | } 1163 | var hrbps []string 1164 | for _, hrbp := range item.DepartmentHrbps { 1165 | hrbps = append(hrbps, *hrbp) 1166 | } 1167 | var userStat []string 1168 | if user.Status.IsUnjoin { 1169 | userStat = append(userStat, "未加入") 1170 | } 1171 | if user.Status.IsResigned { 1172 | userStat = append(userStat, "已离职") 1173 | } 1174 | if user.Status.IsActivated { 1175 | userStat = append(userStat, "已激活") 1176 | } 1177 | if user.Status.IsExited { 1178 | userStat = append(userStat, "已退出") 1179 | } 1180 | if user.Status.IsFrozen { 1181 | userStat = append(userStat, "已冻结") 1182 | } 1183 | var workLocation string 1184 | if user.Country != "" { 1185 | workLocation += user.Country 1186 | } 1187 | if user.City != "" { 1188 | workLocation += user.City 1189 | } 1190 | var isAdmin string 1191 | if user.IsTenantManager { 1192 | isAdmin = "是" 1193 | } 1194 | if !user.IsTenantManager { 1195 | isAdmin = "否" 1196 | } 1197 | timestamp := int64(user.JoinTime) // 已知的时间戳(以秒为单位) 1198 | joinTime := time.Unix(timestamp, 0) // 使用time.Unix()函数将时间戳转换为本地时间 1199 | var ( 1200 | deptName = item.Name // 部门名称 1201 | zhCnName = item.ZhCnName // 部门的中文名 1202 | jaJpName = item.JaJpName // 部门的日文名 1203 | enUsName = item.EnUsName // 部门的英文名 1204 | departmentID = item.DepartmentID // 部门ID 1205 | openDepartmentID = item.OpenDepartmentID // 部门open_department_id 1206 | parentDepartmentID = item.ParentDepartmentID // 上级部门ID 1207 | parentDepartmentName = item.ParentDepartmentName // 上级部门名称 1208 | status = item.Status // 部门状态 1209 | leaderUserID = item.LeaderUserID // 主管领导ID 1210 | leaderUserName = item.LeaderUserName // 主管领导姓名 1211 | departmentHrbps = strings.Join(hrbps, "、") 1212 | uid = user.UserId 1213 | userOpenId = user.OpenId 1214 | username = user.Name 1215 | usernameEn = user.EnName 1216 | userNikname = user.Nickname 1217 | userGender = gender 1218 | userPhone = user.Mobile 1219 | userEmail = user.Email 1220 | userEnterpriseEmail = user.EnterpriseEmail 1221 | userStatus = strings.Join(userStat, "、") 1222 | userDepts = strings.Join(user.DepartmentIds, "、") 1223 | userWorkLocation = workLocation 1224 | userJoinTime = joinTime.String() 1225 | userEmployeeNo = user.EmployeeNo 1226 | userIsAdmin = isAdmin 1227 | userJobTitle = user.JobTitle 1228 | ) 1229 | d := []any{ 1230 | i + 1, 1231 | deptName, 1232 | zhCnName, 1233 | jaJpName, 1234 | enUsName, 1235 | departmentID, 1236 | openDepartmentID, 1237 | parentDepartmentID, 1238 | parentDepartmentName, 1239 | status, 1240 | leaderUserID, 1241 | leaderUserName, 1242 | departmentHrbps, 1243 | uid, 1244 | userOpenId, 1245 | username, 1246 | usernameEn, 1247 | userNikname, 1248 | userGender, 1249 | userPhone, 1250 | userEmail, 1251 | userEnterpriseEmail, 1252 | userStatus, 1253 | userDepts, 1254 | userWorkLocation, 1255 | userJoinTime, 1256 | userEmployeeNo, 1257 | userIsAdmin, 1258 | userJobTitle, 1259 | } 1260 | data = append(data, d) 1261 | } 1262 | 1263 | // 保存文件 1264 | err := saveToExcel(headers, data, filename) 1265 | if err != nil { 1266 | return "", errors.New("保存 Excel 文件失败: " + err.Error()) 1267 | } 1268 | if tmp != filename { 1269 | return fmt.Sprintf("%s 已存在,已另存为 %s", tmp, filename), nil 1270 | } 1271 | return fmt.Sprintf("文件已保存至 %s", filename), nil 1272 | } 1273 | 1274 | func (cli *feiShuCli) recursePrintDept(depts []*fs.DepartmentEntry, did, didType, uidType string, level int, index *int) error { 1275 | var deptChildren []*fs.DepartmentEntry 1276 | var err error 1277 | for i := 0; i < retry; i++ { 1278 | req := fs.NewGetDepartmentChildrenReqBuilder(FeiShuClient). 1279 | DepartmentId(did). 1280 | DepartmentIdType(didType). 1281 | UserIdType(uidType). 1282 | PageSize(50). 1283 | Build() 1284 | deptChildren, err = FeiShuClient.Department.Children(req) 1285 | if err != nil { 1286 | if errors.Is(err, context.Canceled) { 1287 | return nil 1288 | } 1289 | if i == retry-1 { 1290 | return err 1291 | } 1292 | time.Sleep(FeiShuDefaultInterval) 1293 | continue 1294 | } 1295 | break 1296 | } 1297 | if HttpCanceled { 1298 | return nil 1299 | } 1300 | var length = len(deptChildren) 1301 | if length == 0 && level == 0 { 1302 | logger.Info("无可用数据") 1303 | return nil 1304 | } 1305 | for i := 0; i < length; i++ { 1306 | var ( 1307 | status string 1308 | name string 1309 | did string 1310 | oid string 1311 | leaderUserId string 1312 | memberCount int 1313 | primaryMemberCount int 1314 | ) 1315 | if deptChildren[i].Status.IsDeleted { 1316 | status = "已删除" 1317 | } else { 1318 | status = "正常" 1319 | } 1320 | name = deptChildren[i].Name 1321 | did = deptChildren[i].DepartmentID 1322 | oid = deptChildren[i].OpenDepartmentID 1323 | memberCount = deptChildren[i].MemberCount 1324 | leaderUserId = deptChildren[i].LeaderUserID 1325 | primaryMemberCount = deptChildren[i].PrimaryMemberCount 1326 | s := fmt.Sprintf("%s -名称[%s] 状态[%s] 部门ID[%s] OPEN_ID[%s] 主管用户ID[%s] 用户个数[%d] 直属用户个数[%d]", 1327 | strings.Repeat(" ", 3*level), name, status, did, oid, leaderUserId, memberCount, primaryMemberCount) 1328 | fmt.Println(s) 1329 | depts = append(depts, deptChildren[i]) 1330 | if recurse { 1331 | var id string 1332 | if didType == "department_id" { 1333 | id = did 1334 | } else { 1335 | id = oid 1336 | } 1337 | err := cli.recursePrintDept(depts, id, didType, uidType, level+1, index) 1338 | if err != nil { 1339 | return err 1340 | } 1341 | } 1342 | } 1343 | return nil 1344 | } 1345 | 1346 | // generateDepartmentTreeWithUsersHTML 生成包含部门信息和用户信息的部门树HTML代码 1347 | func (cli *feiShuCli) generateDepartmentTreeWithUsersHTML(nodes []*FeiShuDepartmentNode, level int) string { 1348 | html := "" 1349 | for _, dept := range nodes { 1350 | html += fmt.Sprintf("
", level) 1351 | 1352 | s := fmt.Sprintf("ID:%s  OPEN_ID:%s  名称:%s", dept.DepartmentID, dept.OpenDepartmentID, dept.Name) 1353 | if dept.EnUsName != "" { 1354 | s += fmt.Sprintf("  英文名称:%s", dept.EnUsName) 1355 | } 1356 | if dept.JaJpName != "" { 1357 | s += fmt.Sprintf("  日文名称:%s", dept.JaJpName) 1358 | } 1359 | if dept.LeaderUserName != "" { 1360 | s += fmt.Sprintf("  领导:%s(ID:%s)\n", dept.LeaderUserName, dept.LeaderUserID) 1361 | } else if dept.LeaderUserID != "" { 1362 | s += fmt.Sprintf("  领导:ID:%s\n", dept.LeaderUserID) 1363 | } 1364 | if len(dept.DepartmentHrbps) > 0 { 1365 | var tmp []string 1366 | for _, hrbp := range dept.DepartmentHrbps { 1367 | tmp = append(tmp, *hrbp) 1368 | } 1369 | s += fmt.Sprintf("  Hrbp:%s\n", strings.Join(tmp, "、")) 1370 | } 1371 | 1372 | // 添加折叠/展开按钮 1373 | if len(dept.Children) > 0 { 1374 | html += fmt.Sprintf("-") 1375 | } else { 1376 | html += "" 1377 | } 1378 | html += fmt.Sprintf("%s", s) 1379 | 1380 | // 添加部门名称和用户列表的父级容器 1381 | html += fmt.Sprintf("
") 1382 | a := "" 1383 | for i := 0; i < len(dept.User); i++ { 1384 | user := dept.User[i] 1385 | var gender string 1386 | if user.Gender == 0 { 1387 | gender = "保密" 1388 | } else if user.Gender == 1 { 1389 | gender = "男" 1390 | } else if user.Gender == 2 { 1391 | gender = "女" 1392 | } 1393 | var hrbps []string 1394 | for _, hrbp := range dept.DepartmentHrbps { 1395 | hrbps = append(hrbps, *hrbp) 1396 | } 1397 | var userStat []string 1398 | if user.Status.IsUnjoin { 1399 | userStat = append(userStat, "未加入") 1400 | } 1401 | if user.Status.IsResigned { 1402 | userStat = append(userStat, "已离职") 1403 | } 1404 | if user.Status.IsActivated { 1405 | userStat = append(userStat, "已激活") 1406 | } 1407 | if user.Status.IsExited { 1408 | userStat = append(userStat, "已退出") 1409 | } 1410 | if user.Status.IsFrozen { 1411 | userStat = append(userStat, "已冻结") 1412 | } 1413 | var workLocation string 1414 | if user.Country != "" { 1415 | workLocation += user.Country 1416 | } 1417 | if user.City != "" { 1418 | workLocation += user.City 1419 | } 1420 | var isAdmin string 1421 | if user.IsTenantManager { 1422 | isAdmin = "是" 1423 | } 1424 | if !user.IsTenantManager { 1425 | isAdmin = "否" 1426 | } 1427 | m := fmt.Sprintf("ID:%s  OPEN_ID:%s  姓名:%s  性别:%s  电话号码:%s  邮箱:%s  企业邮箱:%s  状态:%s  工号:%s  是否企业管理员:%s", user.UserId, user.OpenId, user.Name, gender, user.Mobile, user.Email, user.EnterpriseEmail, userStat, user.EmployeeNo, isAdmin) 1428 | a += fmt.Sprintf("
  • %s
  • ", m) 1429 | } 1430 | html += fmt.Sprintf("
      %s
    ", a) 1431 | 1432 | // 递归生成子部门树 1433 | if len(dept.Children) > 0 { 1434 | html += cli.generateDepartmentTreeWithUsersHTML(dept.Children, level+1) 1435 | } 1436 | 1437 | html += "
    " 1438 | } 1439 | return html 1440 | } 1441 | 1442 | // saveDepartmentTreeWithUsersToHTML 生成包含部门信息和用户信息的部门树并将其输出为HTML文档 1443 | func (cli *feiShuCli) saveDepartmentTreeWithUsersToHTML(nodes []*FeiShuDepartmentNode, filename string) (string, error) { 1444 | index := strings.LastIndex(filename, ".html") 1445 | if index == -1 { 1446 | filename = filename + ".html" 1447 | } 1448 | tmp := filename 1449 | // 判断文件是否存在 1450 | if utils.IsFileExists(filename) { 1451 | newFilename := generateNewFilename(filename) 1452 | filename = newFilename 1453 | } 1454 | file, err := os.Create(filename) 1455 | if err != nil { 1456 | return "", errors.New("创建HTML文件失败: " + err.Error()) 1457 | } 1458 | defer file.Close() 1459 | departmentTreeHTML := cli.generateDepartmentTreeWithUsersHTML(nodes, 0) 1460 | htmlDocument := cli.generateTreeHTMLDocument(departmentTreeHTML) 1461 | _, err = file.WriteString(htmlDocument) 1462 | if err != nil { 1463 | return "", errors.New("无法写入HTML内容到文件: " + err.Error()) 1464 | } 1465 | if filename == tmp { 1466 | return fmt.Sprintf("文件已保存至 %s", filename), nil 1467 | } 1468 | return fmt.Sprintf("%s 已存在,已另存为 %s", tmp, filename), nil 1469 | } 1470 | 1471 | // generateTreeHTMLDocument 生成添加折叠功能的完整的HTML文档 1472 | func (cli *feiShuCli) generateTreeHTMLDocument(content string) string { 1473 | html := ` 1474 | 1475 | 1476 | 1477 | 1496 | 1532 | 1533 | 1534 |
    1535 |
    1536 | 1537 | 1538 |
    1539 | %s 1540 |
    1541 | 1542 | 1543 | ` 1544 | return fmt.Sprintf(html, content) 1545 | } 1546 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/fasnow/ghttp" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/pflag" 9 | "idebug/logger" 10 | fs "idebug/plugin/feishu" 11 | "idebug/plugin/wechat" 12 | "os" 13 | "os/exec" 14 | "runtime" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | type Module string 20 | 21 | const ( 22 | WxModule Module = "wechat" 23 | FeiShuModule Module = "feishu" 24 | NoModule Module = "" 25 | ) 26 | 27 | var ( 28 | Context context.Context 29 | Cancel context.CancelFunc 30 | HttpCanceled bool 31 | CurrentModule *Module 32 | WxClient *wechat.Client 33 | FeiShuClient *fs.Client 34 | FeiShuDefaultInterval = 100 * time.Millisecond 35 | departmentIdTypeMap = map[string]string{"id": "department_id", "openid": "open_department_id"} //飞书部门ID类型 36 | userIdTypeMap = map[string]string{"id": "user_id", "openid": "open_id"} //飞书用户ID类型 37 | retry = 3 38 | ) 39 | 40 | var ( 41 | Proxy *string 42 | userId string 43 | departmentId string 44 | userIdType string //飞书用户ID类型 45 | departmentIdType string //飞书部门ID类型缓存 46 | userIdTypeCache string //飞书用户ID类型 47 | departmentIdTypeCache string //飞书部门ID类型缓存 48 | password string //飞书企业邮箱密码更改 49 | recurse bool //递归获取 50 | verbose int //打印过程的数量 51 | ) 52 | 53 | const mainUsage = `Global Commands: 54 | clear,cls 清屏 55 | exit 退出 56 | info 查看当前设置 57 | use 切换模块,可选值:wechat、feishu 58 | update 检测更新 59 | -h,--help,help 查看帮助 60 | set proxy 设置代理,支持socks5,http 61 | ` 62 | 63 | type mainCli struct { 64 | Root *cobra.Command 65 | set *cobra.Command 66 | proxy *cobra.Command 67 | clear *cobra.Command 68 | update *cobra.Command 69 | info *cobra.Command 70 | use *cobra.Command 71 | exit *cobra.Command 72 | } 73 | 74 | func NewMainCli() *mainCli { 75 | cli := &mainCli{} 76 | cli.Root = cli.newRoot() 77 | cli.set = cli.newSet() 78 | cli.proxy = newProxy() 79 | cli.clear = cli.newClear() 80 | cli.update = cli.newUpdate() 81 | cli.info = cli.newInfo() 82 | cli.use = cli.newUse() 83 | cli.exit = cli.newExit() 84 | cli.init() 85 | return cli 86 | } 87 | 88 | func (cli *mainCli) init() { 89 | cli.set.AddCommand(cli.proxy) 90 | cli.Root.AddCommand(cli.set, cli.clear, cli.update, cli.info, cli.use, cli.exit) 91 | //cli.setHelpV1(cli.Root, "") 92 | //cli.setHelpV1(cli.proxy, "") 93 | //cli.setHelpV1(cli.set, "") 94 | // 95 | //cli.setHelpV1(cli.clear, "") 96 | //cli.setHelpV1(cli.update, "") 97 | //cli.setHelpV1(cli.info, "") 98 | //cli.setHelpV1(cli.use, "") 99 | //cli.setHelpV1(cli.exit, "") 100 | 101 | cli.setHelpV2(cli.Root, cli.proxy, cli.set, cli.clear, cli.update, cli.info, cli.use, cli.exit) 102 | } 103 | 104 | func (cli *mainCli) newExit() *cobra.Command { 105 | return &cobra.Command{ 106 | Use: "exit", 107 | Short: `退出模块或者程序`, 108 | Run: func(cmd *cobra.Command, args []string) { 109 | //if *CurrentModule != NoModule { 110 | // *CurrentModule = NoModule 111 | //} else { 112 | // if runtime.GOOS != "windows" { 113 | // cmd := exec.Command("reset") 114 | // err := cmd.Run() 115 | // if err != nil { 116 | // logger.Error(logger.FormatError(err)) 117 | // } 118 | // } 119 | // os.Exit(0) 120 | //} 121 | if runtime.GOOS != "windows" { 122 | cmd := exec.Command("reset") 123 | err := cmd.Run() 124 | if err != nil { 125 | logger.Error(logger.FormatError(err)) 126 | } 127 | } 128 | os.Exit(0) 129 | }, 130 | } 131 | } 132 | 133 | func (cli *mainCli) newRoot() *cobra.Command { 134 | return &cobra.Command{ 135 | Use: "idebug", 136 | Run: func(cmd *cobra.Command, args []string) { 137 | }, 138 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 139 | reset() 140 | }, 141 | } 142 | } 143 | 144 | func (cli *mainCli) newSet() *cobra.Command { 145 | return &cobra.Command{ 146 | Use: "set", 147 | Short: `设置参数`, 148 | Run: func(cmd *cobra.Command, args []string) { 149 | cmd.Usage() 150 | }, 151 | } 152 | } 153 | 154 | func (cli *mainCli) newClear() *cobra.Command { 155 | return &cobra.Command{ 156 | Use: `clear`, 157 | Short: `清屏`, 158 | Aliases: []string{"cls"}, 159 | Run: func(cmd *cobra.Command, args []string) { 160 | cmd1 := exec.Command("clear") // for Linux/MacOS 161 | if _, err := os.Stat("c:\\windows\\system32\\cmd.exe"); err == nil { 162 | cmd1 = exec.Command("cmd", "/c", "cls") // for Windows 163 | } 164 | cmd1.Stdout = os.Stdout 165 | err := cmd1.Run() 166 | if err != nil { 167 | logger.Error(logger.FormatError(err)) 168 | } 169 | }, 170 | } 171 | } 172 | 173 | func (cli *mainCli) newUpdate() *cobra.Command { 174 | return &cobra.Command{ 175 | Use: `update`, 176 | Short: `检查更新`, 177 | Run: func(cmd *cobra.Command, args []string) { 178 | version, releaseUrl, publishTime, content := CheckUpdate() 179 | if version != "" { 180 | s := fmt.Sprintf("最新版本: %s %s", version, publishTime) 181 | s += "\n 下载地址: " + releaseUrl 182 | s += "\n 更新内容: " + content 183 | logger.Notice(s) 184 | return 185 | } 186 | logger.Success("当前已是最新版本") 187 | }, 188 | } 189 | } 190 | 191 | func (cli *mainCli) newInfo() *cobra.Command { 192 | return &cobra.Command{ 193 | Use: `info`, 194 | Short: `查看环境变量`, 195 | Run: func(cmd *cobra.Command, args []string) { 196 | if Proxy == nil { 197 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", "")) 198 | } else { 199 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", *Proxy)) 200 | } 201 | }, 202 | } 203 | } 204 | 205 | func (cli *mainCli) newUse() *cobra.Command { 206 | return &cobra.Command{ 207 | Use: `use`, 208 | Short: `选择模块,可用模块:wechat、feishu `, 209 | Run: func(cmd *cobra.Command, args []string) { 210 | if len(args) == 0 { 211 | logger.Warning("请选择一个有效模块: wechat、feishu") 212 | return 213 | } 214 | switch Module(args[0]) { 215 | case FeiShuModule: 216 | *CurrentModule = FeiShuModule 217 | if FeiShuClient == nil { 218 | FeiShuClient = fs.NewClient() 219 | } 220 | break 221 | case WxModule: 222 | *CurrentModule = WxModule 223 | if WxClient == nil { 224 | WxClient = wechat.NewWxClient() 225 | } 226 | break 227 | default: 228 | logger.Error(fmt.Errorf("未知模块:" + args[0])) 229 | } 230 | }, 231 | } 232 | } 233 | 234 | func newProxy() *cobra.Command { 235 | return &cobra.Command{ 236 | Use: `proxy`, 237 | Short: `设置代理`, 238 | Run: func(cmd *cobra.Command, args []string) { 239 | setProxy(args) 240 | }, 241 | } 242 | } 243 | 244 | func commandMsiSet(cmd *cobra.Command) { 245 | //// 打印自定义用法 246 | //cmd.SetUsageFunc(func(cmd *cobra.Command) error { 247 | // if name == "root" { 248 | // ShowAllUsage() 249 | // } else { 250 | // ShowUsageByName(name) 251 | // } 252 | // return nil 253 | //}) 254 | 255 | } 256 | 257 | func (cli *mainCli) setHelpV1(cmd *cobra.Command, extraUsage string) { 258 | // 不自己打印会多一个空白行 259 | cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 260 | //返回error会自动打印用法 261 | logger.Error(err) 262 | return nil 263 | }) 264 | 265 | // 禁止打印错误信息 266 | cmd.SilenceErrors = true 267 | // 268 | // 禁用帮助信息 269 | cmd.DisableSuggestions = true 270 | 271 | // 禁用用法信息 272 | cmd.SilenceUsage = true 273 | 274 | cmd.Flags().SortFlags = false 275 | 276 | var cmdCount int 277 | var cmdOutput string 278 | // 遍历子命令,生成命令列表 279 | for _, child := range cmd.Commands() { 280 | if child.IsAvailableCommand() || child.Name() == "help" { 281 | cmdCount++ 282 | if cmdCount == 1 { 283 | cmdOutput = "Available Commands:\n" 284 | } 285 | cmdOutput += fmt.Sprintf(" %-15s %s\n", child.Name(), child.Short) 286 | } 287 | } 288 | flags := cmd.Flags() 289 | var flagCount int 290 | var flagOutput string 291 | flags.VisitAll(func(flag *pflag.Flag) { 292 | flagCount++ 293 | if flagCount == 1 { 294 | flagOutput += "Flags:\n" 295 | } 296 | if flag.Shorthand != "" { 297 | flagOutput += fmt.Sprintf(" %-15s %s\n", fmt.Sprintf("-%s,--%s", flag.Shorthand, flag.Name), flag.Usage) 298 | } else { 299 | flagOutput += fmt.Sprintf(" %-15s %s\n", flag.Name, flag.Usage) 300 | } 301 | }) 302 | var commandPath string 303 | paths := strings.Split(cmd.CommandPath(), " ") 304 | if len(paths) > 1 { 305 | commandPath = strings.Join(paths[1:], " ") 306 | } 307 | var usage = "Usage:\n" 308 | if extraUsage != "" { 309 | usage += fmt.Sprintf(" %s %s\n", commandPath, extraUsage) 310 | } 311 | if flagCount > 0 { 312 | if commandPath != "" { 313 | usage += fmt.Sprintf(" %s [flags]\n", commandPath) 314 | } else { 315 | usage += " [flags]\n" 316 | } 317 | 318 | } 319 | if cmdCount > 0 { 320 | if commandPath != "" { 321 | usage += fmt.Sprintf(" %s [command]\n", commandPath) 322 | } else { 323 | usage += " [command]\n" 324 | } 325 | } 326 | if cmdCount > 0 { 327 | usage += cmdOutput 328 | } 329 | if flagCount > 0 { 330 | usage += flagOutput 331 | } 332 | 333 | //设置自定义的使用帮助函数 334 | cmd.SetHelpFunc(func(cmd *cobra.Command, s []string) { 335 | fmt.Print(usage) 336 | }) 337 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 338 | fmt.Print(usage) 339 | return nil 340 | }) 341 | } 342 | 343 | func (cli *mainCli) setHelpV2(cmds ...*cobra.Command) { 344 | for _, cmd := range cmds { 345 | // 不自己打印会多一个空白行 346 | cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 347 | logger.Error(err) 348 | return nil //返回error会自动打印用法 349 | }) 350 | 351 | cmd.SilenceErrors = true // 禁止打印错误信息 352 | cmd.DisableSuggestions = true // 禁用帮助信息 353 | cmd.SilenceUsage = true // 禁用用法信息 354 | cmd.Flags().SortFlags = false 355 | 356 | // 设置自定义的使用帮助函数 357 | cmd.SetHelpFunc(func(cmd *cobra.Command, s []string) { 358 | fmt.Print(mainUsage) 359 | }) 360 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 361 | fmt.Print(mainUsage) 362 | return nil 363 | }) 364 | } 365 | } 366 | 367 | func setProxy(args []string) { 368 | if len(args) == 0 { 369 | *Proxy = "" 370 | } else { 371 | *Proxy = args[0] 372 | } 373 | ghttp.SetGlobalProxy(*Proxy) 374 | logger.Success("proxy => " + *Proxy) 375 | } 376 | 377 | func reset() { 378 | userId = "" 379 | departmentId = "" 380 | userIdType = "" 381 | departmentIdType = "" 382 | password = "" 383 | recurse = false 384 | verbose = -1 385 | HttpCanceled = false 386 | } 387 | 388 | func SetContext() { 389 | ctx, cancel := context.WithCancel(context.Background()) 390 | Context = ctx 391 | Cancel = cancel 392 | switch *CurrentModule { 393 | case WxModule: 394 | WxClient.SetContext(&ctx) 395 | WxClient.StopWhenContextCanceled(true) 396 | case FeiShuModule: 397 | FeiShuClient.SetContext(&ctx) 398 | FeiShuClient.StopWhenContextCanceled(true) 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /cmd/output.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/buger/jsonparser" 6 | "github.com/coreos/go-semver/semver" 7 | "github.com/fasnow/ghttp" 8 | "github.com/xuri/excelize/v2" 9 | "idebug/config" 10 | "idebug/logger" 11 | "net/http" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // generateNewFilename 生成带时间戳的新文件名 18 | func generateNewFilename(filename string) string { 19 | var ext string 20 | t := time.Now() 21 | loc, _ := time.LoadLocation("Asia/Shanghai") // 设置时区为中国 22 | chinaTime := t.In(loc) // 转换为中国时间 23 | formattedTime := chinaTime.Format("2006_01_02_15_04_05") // 格式化时间 24 | index := strings.LastIndex(filename, ".") 25 | if index != -1 { 26 | ext = filename[index+1:] 27 | } 28 | baseName := filename[:index] // 去除扩展名 29 | newFilename := fmt.Sprintf("%s_%s.%s", baseName, formattedTime, ext) 30 | return newFilename 31 | } 32 | 33 | // 保存数据至excel,header长度要和数据列数匹配 34 | func saveToExcel(header []any, data [][]any, filename string) error { 35 | file := excelize.NewFile() 36 | data = append([][]any{header}, data...) 37 | // 添加数据 38 | for i := 0; i < len(data); i++ { 39 | row := data[i] 40 | startCell, err := excelize.JoinCellName("A", i+1) 41 | if err != nil { 42 | return err 43 | } 44 | if i == 0 { 45 | // 首行大写 46 | for j := 0; j < len(row); j++ { 47 | if value, ok := row[j].(string); ok { 48 | row[j] = strings.ToUpper(value) 49 | } 50 | } 51 | if err = file.SetSheetRow("Sheet1", startCell, &row); err != nil { 52 | return err 53 | } 54 | continue 55 | } 56 | if err = file.SetSheetRow("Sheet1", startCell, &row); err != nil { 57 | return err 58 | } 59 | } 60 | 61 | // 表头颜色填充 62 | headerStyle, err := file.NewStyle(&excelize.Style{ 63 | Fill: excelize.Fill{Type: "pattern", Color: []string{"#d0cece"}, Pattern: 1, Shading: 1}, 64 | Alignment: &excelize.Alignment{ 65 | Horizontal: "center", 66 | }, 67 | }) 68 | if err != nil { 69 | 70 | } 71 | err = file.SetCellStyle("Sheet1", "A1", columnNumberToName(len(data[0]))+"1", headerStyle) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // 添加边框 77 | dataStyle, err := file.NewStyle(&excelize.Style{ 78 | Fill: excelize.Fill{Type: "pattern"}, 79 | Alignment: &excelize.Alignment{ 80 | Horizontal: "left", 81 | }, 82 | Border: []excelize.Border{ 83 | {Type: "right", Color: "#000000", Style: 1}, 84 | {Type: "left", Color: "#000000", Style: 1}, 85 | {Type: "top", Color: "#000000", Style: 1}, 86 | {Type: "bottom", Color: "#000000", Style: 1}, 87 | }, 88 | }) 89 | if err != nil { 90 | return err 91 | } 92 | err = file.SetCellStyle("Sheet1", "A1", columnNumberToName(len(data[0]))+strconv.Itoa(len(data)), dataStyle) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if err := file.SaveAs(filename); err != nil { 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func columnNumberToName(n int) string { 104 | name := "" 105 | for n > 0 { 106 | n-- 107 | name = string(byte(n%26)+'A') + name 108 | n /= 26 109 | } 110 | return name 111 | } 112 | 113 | func Banner() { 114 | s := 115 | ` ____ ____ __________ __ ________ 116 | / _/ / __ \/ ____/ __ )/ / / / ____/ 117 | / / / / / / __/ / __ / / / / / __ 118 | _/ / / /_/ / /___/ /_/ / /_/ / /_/ / 119 | /___/ /_____/_____/_____/\____/\____/ 120 | @github.com/fasnow version 121 | 企业微信、企业飞书接口调用工具` 122 | s = strings.Replace(s, "version", config.Version, 1) 123 | fmt.Println(s) 124 | logger.Warning("仅用于开发人员用作接口调试,请勿用作其他非法用途") 125 | } 126 | 127 | // CheckUpdate latestVersion, releaseUrl, publishTime, content 128 | func CheckUpdate() (string, string, string, string) { 129 | var httpClient = &ghttp.Client{} 130 | request, err := http.NewRequest("GET", "https://api.github.com/repos/fasnow/idebug/releases/latest", nil) 131 | //request.Header.Set("User-Agent", "1") 132 | if err != nil { 133 | return "", "", "", "" 134 | } 135 | response, err := httpClient.Do(request, ghttp.Options{Timeout: 3 * time.Second}) 136 | if err != nil { 137 | return "", "", "", "" 138 | } 139 | if response.StatusCode != 200 { 140 | return "", "", "", "" 141 | } 142 | body, err := ghttp.GetResponseBody(response.Body) 143 | if err != nil { 144 | return "", "", "", "" 145 | } 146 | latestVersion, err := jsonparser.GetString(body, "tag_name") 147 | if err != nil { 148 | return "", "", "", "" 149 | } 150 | currentVersion, err := semver.NewVersion(config.Version[1:]) 151 | if err != nil { 152 | fmt.Println(err) 153 | } 154 | 155 | publishTime, err := jsonparser.GetString(body, "published_at") 156 | 157 | v2, err := semver.NewVersion(latestVersion[1:]) 158 | if err != nil { 159 | fmt.Println(err) 160 | } 161 | // 比较版本 162 | if v2.Compare(*currentVersion) < 1 { 163 | return "", "", "", "" 164 | } 165 | releaseUrl, err := jsonparser.GetString(body, "html_url") 166 | if err != nil { 167 | return "", "", "", "" 168 | } 169 | content, err := jsonparser.GetString(body, "body") 170 | if err != nil { 171 | return "", "", "", "" 172 | } 173 | return latestVersion, releaseUrl, publishTime, content 174 | } 175 | -------------------------------------------------------------------------------- /cmd/wx.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/spf13/cobra" 8 | "idebug/logger" 9 | "idebug/plugin/wechat" 10 | "idebug/utils" 11 | "os" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | const wechatUsage = mainUsage + `wechat Module: 17 | set corpid 设置corpid 18 | set corpsecret 设置corpsecret 19 | set token 设置access_token,与set corpid和set corpsecret互斥 20 | run 获取access_token 21 | dp 根据查看部门详情 22 | dp ls 根据递归获取子部门id,不提供则递归获取默认部门 23 | dp tree 根据递归获取子部门信息,稍微详细一些,不提供则递归获取默认部门 24 | user 根据查看用户详情 25 | user ls [-r] 根据查看部门用户列表,-r:递归获取(默认false) 26 | dump 根据递归导出部门用户,不提供则递归获取默认部门 27 | ` 28 | 29 | // set domain 设置接口域名,默认值为官方接口【https://qyapi.weixin.qq.com】,自建企业微信使用该方法设置 30 | type wechatCli struct { 31 | Root *cobra.Command 32 | info *cobra.Command 33 | run *cobra.Command 34 | set *cobra.Command 35 | corpId *cobra.Command 36 | corpSecret *cobra.Command 37 | token *cobra.Command 38 | domain *cobra.Command 39 | dp *cobra.Command 40 | dpLs *cobra.Command 41 | dpTree *cobra.Command 42 | user *cobra.Command 43 | userLs *cobra.Command 44 | dump *cobra.Command 45 | } 46 | 47 | func NewWechatCli() *wechatCli { 48 | cli := &wechatCli{} 49 | cli.Root = cli.newRoot() 50 | cli.info = cli.newInfo() 51 | cli.run = cli.newRun() 52 | cli.set = cli.newSet() 53 | cli.corpId = cli.newCorpId() 54 | cli.corpSecret = cli.newCorpSecret() 55 | cli.domain = cli.newBaseDomain() 56 | cli.dp = cli.newDp() 57 | cli.dpLs = cli.newDpLs() 58 | cli.dpTree = cli.newDpTree() 59 | cli.user = cli.newUser() 60 | cli.userLs = cli.newUserLs() 61 | cli.dump = cli.newDump() 62 | cli.init() 63 | return cli 64 | } 65 | 66 | func (cli *wechatCli) init() { 67 | //cli.dpLs.Flags().IntVarP(&verbose, "verbose", "v", -1, "控制台输出的条数,默认全部输出") 68 | //cli.dpTree.Flags().IntVarP(&verbose, "verbose", "v", -1, "控制台输出的条数,默认全部输出") 69 | 70 | //cli.userLs.Flags().IntVarP(&verbose, "verbose", "v", -1, "控制台输出的条数,默认全部输出") 71 | cli.userLs.Flags().BoolVarP(&recurse, "re", "r", false, "是否递归获取,默认false") 72 | 73 | cli.set.AddCommand(cli.corpId) 74 | cli.set.AddCommand(cli.corpSecret) 75 | cli.set.AddCommand(cli.domain) 76 | cli.set.AddCommand(newProxy()) 77 | cli.dp.AddCommand(cli.dpLs, cli.dpTree) 78 | cli.user.AddCommand(cli.userLs) 79 | cli.Root.AddCommand(cli.set, cli.run, cli.info, cli.dp, cli.user, cli.dump) 80 | 81 | cli.setHelpV1(cli.Root, cli.info, cli.run, cli.set, cli.corpId, cli.corpSecret, cli.dp, cli.dpLs, cli.dpTree, cli.user, cli.userLs, cli.dump) 82 | } 83 | 84 | func (cli *wechatCli) newRoot() *cobra.Command { 85 | return &cobra.Command{ 86 | Use: "wechat", 87 | Short: `微信模块`, 88 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 89 | if *CurrentModule != WxModule { 90 | return logger.FormatError(fmt.Errorf("请先设置模块为wechat")) 91 | } 92 | return nil 93 | }, 94 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 95 | reset() 96 | }, 97 | } 98 | } 99 | 100 | func (cli *wechatCli) newRun() *cobra.Command { 101 | return &cobra.Command{ 102 | Use: `run`, 103 | Short: `根据设置获取access_token`, 104 | PreRunE: func(cmd *cobra.Command, args []string) error { 105 | conf := WxClient.GetConfig() 106 | if conf.AccessToken != nil && *conf.AccessToken != "" { 107 | return nil 108 | } else { 109 | if conf.CorpId == nil || *conf.CorpId == "" { 110 | return fmt.Errorf("请先设置corpid") 111 | } 112 | if conf.CorpSecret == nil || *conf.CorpSecret == "" { 113 | return fmt.Errorf("请先设置corpsecret") 114 | } 115 | return nil 116 | } 117 | }, 118 | Run: func(cmd *cobra.Command, args []string) { 119 | conf := WxClient.GetConfig() 120 | if conf.AccessToken != nil && *conf.AccessToken != "" { 121 | cli.showClientConfig() 122 | return 123 | } 124 | _, err := WxClient.GetAccessToken() 125 | if err != nil { 126 | if errors.Is(err, context.Canceled) { 127 | return 128 | } 129 | logger.Error(logger.FormatError(err)) 130 | return 131 | } 132 | if HttpCanceled { 133 | return 134 | } 135 | cli.showClientConfig() 136 | }, 137 | } 138 | } 139 | 140 | func (cli *wechatCli) newSet() *cobra.Command { 141 | return &cobra.Command{ 142 | Use: "set", 143 | Short: `设置参数`, 144 | } 145 | } 146 | 147 | func (cli *wechatCli) newBaseDomain() *cobra.Command { 148 | return &cobra.Command{ 149 | Use: "domain", 150 | Short: `设置接口域名,默认值为官方接口【https://qyapi.weixin.qq.com】,自建企业微信使用该方法设置`, 151 | Run: func(cmd *cobra.Command, args []string) { 152 | if len(args) < 1 { 153 | logger.Warning("请提供一个值") 154 | return 155 | } 156 | if strings.HasSuffix(args[0], "/") { 157 | args[0] = args[0][0 : len(args[0])-1] 158 | } 159 | wechat.SetBaseDomain(args[0]) 160 | logger.Success("domain => " + args[0]) 161 | }, 162 | } 163 | } 164 | 165 | func (cli *wechatCli) newCorpId() *cobra.Command { 166 | return &cobra.Command{ 167 | Use: "corpid", 168 | Short: `设置corpid`, 169 | Run: func(cmd *cobra.Command, args []string) { 170 | if len(args) < 1 { 171 | logger.Warning("请提供一个值") 172 | return 173 | } 174 | WxClient.SetCorpId(args[0]) 175 | logger.Success("corpid => " + args[0]) 176 | }, 177 | } 178 | } 179 | 180 | func (cli *wechatCli) newCorpSecret() *cobra.Command { 181 | return &cobra.Command{ 182 | Use: "corpsecret", 183 | Short: `设置corpsecret`, 184 | RunE: func(cmd *cobra.Command, args []string) error { 185 | if len(args) < 1 { 186 | logger.Warning("请提供一个值") 187 | return nil 188 | } 189 | WxClient.SetCorpSecret(args[0]) 190 | logger.Success("corpsecret => " + args[0]) 191 | return nil 192 | }, 193 | } 194 | } 195 | 196 | func (cli *wechatCli) newToken() *cobra.Command { 197 | return &cobra.Command{ 198 | Use: "token", 199 | Short: `设置access_token,与set corpid和set corpsecret互斥`, 200 | Run: func(cmd *cobra.Command, args []string) { 201 | if len(args) < 1 { 202 | logger.Warning("请提供一个值") 203 | return 204 | } 205 | WxClient.SetAccessToken(args[0]) 206 | logger.Success("access_token => " + args[0]) 207 | }, 208 | } 209 | } 210 | 211 | func (cli *wechatCli) newDp() *cobra.Command { 212 | return &cobra.Command{ 213 | Use: "dp", 214 | Short: `部门操作`, 215 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 216 | if !cli.hasAccessToken() { 217 | return fmt.Errorf("请先执行run获取access_token") 218 | } 219 | return nil 220 | }, 221 | PreRunE: func(cmd *cobra.Command, args []string) error { 222 | if len(args) < 1 { 223 | return fmt.Errorf("请提供一个参数作为部门ID或者提供一个子命令") 224 | } 225 | return nil 226 | }, 227 | Run: func(cmd *cobra.Command, args []string) { 228 | req := wechat.NewGetDepartmentReqBuilder(WxClient).DepartmentId(args[0]).Build() 229 | deptInfo, err := WxClient.Department.Get(req) 230 | if err != nil { 231 | if errors.Is(err, context.Canceled) { 232 | return 233 | } 234 | logger.Error(logger.FormatError(err)) 235 | return 236 | } 237 | if HttpCanceled { 238 | return 239 | } 240 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 241 | fmt.Printf("%-10s: %d\n", "部门ID", deptInfo.ID) 242 | fmt.Printf("%-8s: %d\n", "上级部门ID", deptInfo.ParentId) 243 | fmt.Printf("%-6s: %s\n", "部门中文名称", deptInfo.Name) 244 | fmt.Printf("%-6s: %s\n", "部门英文名称", deptInfo.NameEn) 245 | fmt.Printf("%-8s: %s\n", "部门领导", strings.Join(deptInfo.DepartmentLeader, "、")) 246 | fmt.Printf("%-12s: %d\n", "Order", deptInfo.Order) 247 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 248 | }, 249 | } 250 | } 251 | 252 | func (cli *wechatCli) newDpLs() *cobra.Command { 253 | return &cobra.Command{ 254 | Use: "ls", 255 | Short: `根据部门ID获取子部门ID`, 256 | Run: func(cmd *cobra.Command, args []string) { 257 | var departmentList []*WxDepartmentNode 258 | var depts []*wechat.DepartmentEntrySimplified 259 | for i := 0; i < retry; i++ { 260 | var err error 261 | if len(args) == 0 { 262 | req := wechat.NewGetDepartmentIdListReqBuilder(WxClient).Build() 263 | depts, err = WxClient.Department.GetIdList(req) 264 | 265 | } else { 266 | //递归获取指定部门所有ID 267 | req := wechat.NewGetDepartmentIdListReqBuilder(WxClient).DepartmentId(args[0]).Build() 268 | depts, err = WxClient.Department.GetIdList(req) 269 | } 270 | if err != nil { 271 | if errors.Is(err, context.Canceled) { 272 | return 273 | } 274 | if i == retry-1 { 275 | logger.Error(logger.FormatError(err)) 276 | return 277 | } 278 | continue 279 | } 280 | break 281 | } 282 | if HttpCanceled { 283 | return 284 | } 285 | if len(depts) == 0 { 286 | logger.Warning("无可用部门信息") 287 | return 288 | } 289 | for _, dept := range depts { 290 | departmentList = append(departmentList, &WxDepartmentNode{ 291 | DepartmentEntry: wechat.DepartmentEntry{ 292 | DepartmentEntrySimplified: *dept}, 293 | }) 294 | } 295 | tree := cli.buildDepartmentIdsTree(departmentList) 296 | index := 0 297 | cli.printDepartmentIdsTree(tree, 0, &index) 298 | logger.Info("正在保存至HTML文件...") 299 | msg, err := cli.departmentIdsTreeToHTML(tree, "wechat_dept_ids.html") 300 | if err != nil { 301 | logger.Error(logger.FormatError(err)) 302 | return 303 | } 304 | logger.Success(msg) 305 | }, 306 | } 307 | } 308 | 309 | func (cli *wechatCli) newDpTree() *cobra.Command { 310 | return &cobra.Command{ 311 | Use: "tree", 312 | Short: `根据部门ID获取子部门ID,获取的信息稍微详细点`, 313 | Run: func(cmd *cobra.Command, args []string) { 314 | var departmentList []*WxDepartmentNode 315 | var err error 316 | var departments []*wechat.DepartmentEntry 317 | for i := 0; i < retry; i++ { 318 | if len(args) == 0 { 319 | req := wechat.NewGetDepartmentListReqBuilder(WxClient).Build() 320 | departments, err = WxClient.Department.GetList(req) 321 | } else { 322 | req := wechat.NewGetDepartmentListReqBuilder(WxClient).DepartmentId(args[0]).Build() 323 | departments, err = WxClient.Department.GetList(req) 324 | } 325 | if err != nil { 326 | if errors.Is(err, context.Canceled) { 327 | return 328 | } 329 | if i == retry-1 { 330 | logger.Error(logger.FormatError(err)) 331 | return 332 | } 333 | continue 334 | } 335 | break 336 | } 337 | if HttpCanceled { 338 | return 339 | } 340 | if len(departments) == 0 { 341 | logger.Warning("无可用部门信息") 342 | return 343 | } 344 | for _, v := range departments { 345 | d := wechat.DepartmentEntry{ 346 | DepartmentEntrySimplified: wechat.DepartmentEntrySimplified{ 347 | ID: v.ID, 348 | ParentId: v.ParentId, 349 | Order: v.Order, 350 | }, 351 | Name: v.Name, 352 | NameEn: v.NameEn, 353 | DepartmentLeader: v.DepartmentLeader, 354 | } 355 | departmentList = append(departmentList, &WxDepartmentNode{DepartmentEntry: d}) 356 | } 357 | tree := cli.buildDepartmentIdsTree(departmentList) 358 | var index int 359 | index = 0 360 | cli.printDepartmentTree(tree, 0, &index) 361 | logger.Info("正在保存至HTML文件...") 362 | msg, err := cli.saveDepartmentTreeToHTML(tree, "wechat_dept.html") 363 | if err != nil { 364 | logger.Error(logger.FormatError(err)) 365 | return 366 | } 367 | logger.Success(msg) 368 | }} 369 | } 370 | 371 | func (cli *wechatCli) newUser() *cobra.Command { 372 | return &cobra.Command{ 373 | Use: "user", 374 | Short: `用户操作`, 375 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 376 | if !cli.hasAccessToken() { 377 | return fmt.Errorf("请先执行run获取access_token") 378 | } 379 | return nil 380 | }, 381 | PreRunE: func(cmd *cobra.Command, args []string) error { 382 | if len(args) < 1 { 383 | return fmt.Errorf("请提供一个参数作为用户ID或者提供一个子命令") 384 | } 385 | return nil 386 | }, 387 | Run: func(cmd *cobra.Command, args []string) { 388 | req := wechat.NewGetUserReqBuilder(WxClient).UserId(args[0]).Build() 389 | userInfo, err := WxClient.User.Get(req) 390 | if err != nil { 391 | if errors.Is(err, context.Canceled) { 392 | return 393 | } 394 | logger.Error(logger.FormatError(err)) 395 | return 396 | } 397 | if HttpCanceled { 398 | return 399 | } 400 | cli.showUserInfo(userInfo, false) 401 | return 402 | }, 403 | } 404 | } 405 | 406 | func (cli *wechatCli) newUserLs() *cobra.Command { 407 | return &cobra.Command{ 408 | Use: "ls", 409 | Short: `根据部门ID获取用户`, 410 | PreRunE: func(cmd *cobra.Command, args []string) error { 411 | if len(args) == 0 { 412 | return fmt.Errorf("请提供一个参数作为部门ID") 413 | } 414 | return nil 415 | }, 416 | Run: func(cmd *cobra.Command, args []string) { 417 | req := wechat.NewGetUsersByDepartmentIdReqBuilder(WxClient).DepartmentId(args[0]).Fetch(recurse).Build() 418 | userList, err := WxClient.User.GetUsersByDepartmentId(req) 419 | if err != nil { 420 | if errors.Is(err, context.Canceled) { 421 | return 422 | } 423 | logger.Error(logger.FormatError(err)) 424 | return 425 | } 426 | if HttpCanceled { 427 | return 428 | } 429 | if len(userList) == 0 { 430 | logger.Warning("无可用用户信息") 431 | return 432 | } 433 | for i := 0; i < len(userList); i++ { 434 | //if i >= verbose && verbose > -1 { 435 | // logger.Info("更多数据请查看文件") 436 | // break 437 | //} 438 | cli.showUserInfo(userList[i], true) 439 | } 440 | logger.Info("正在保存至XLSX文件...") 441 | msg, err := cli.saveUserToExcel(userList, "wechat_users.xlsx") 442 | if err != nil { 443 | logger.Error(logger.FormatError(err)) 444 | return 445 | } 446 | logger.Success(msg) 447 | }, 448 | } 449 | } 450 | 451 | func (cli *wechatCli) newDump() *cobra.Command { 452 | return &cobra.Command{ 453 | Use: "dump", 454 | Short: `根据部门ID导出用户,不提供部门ID则导出通讯录授权范围内所有部门用户`, 455 | PreRunE: func(cmd *cobra.Command, args []string) error { 456 | if !cli.hasAccessToken() { 457 | return fmt.Errorf("请先执行run获取access_token") 458 | 459 | } 460 | return nil 461 | }, 462 | Run: func(cmd *cobra.Command, args []string) { 463 | var departmentTreeResource []*wechat.DepartmentEntry 464 | var deptList []*wechat.DepartmentEntry 465 | var err error 466 | logger.Info("正在获取部门树...") 467 | for i := 0; i < retry; i++ { 468 | if len(args) == 0 { 469 | req := wechat.NewGetDepartmentListReqBuilder(WxClient).Build() 470 | deptList, err = WxClient.Department.GetList(req) 471 | } else { 472 | req := wechat.NewGetDepartmentListReqBuilder(WxClient).DepartmentId(args[0]).Build() 473 | deptList, err = WxClient.Department.GetList(req) 474 | } 475 | if err != nil { 476 | if errors.Is(err, context.Canceled) { 477 | return 478 | } 479 | if i == retry-1 { 480 | logger.Error(logger.FormatError(err)) 481 | return 482 | } 483 | continue 484 | } 485 | break 486 | } 487 | if HttpCanceled { 488 | return 489 | } 490 | if len(deptList) == 0 { 491 | logger.Warning("无可用部门信息") 492 | return 493 | } 494 | for _, v := range deptList { 495 | d := wechat.DepartmentEntry{ 496 | DepartmentEntrySimplified: wechat.DepartmentEntrySimplified{ 497 | ID: v.ID, 498 | ParentId: v.ParentId, 499 | Order: v.Order, 500 | }, 501 | Name: v.Name, 502 | NameEn: v.NameEn, 503 | DepartmentLeader: v.DepartmentLeader, 504 | } 505 | departmentTreeResource = append(departmentTreeResource, &d) 506 | } 507 | departmentTree := cli.buildDepartmentTree(departmentTreeResource) 508 | logger.Info("正在获取用户...") 509 | var userList []*wechat.UserEntry 510 | for i := 0; i < retry; i++ { 511 | req := wechat.NewGetUsersByDepartmentIdReqBuilder(WxClient).DepartmentId(strconv.Itoa(departmentTreeResource[0].ID)).Fetch(true).Build() 512 | userList, err = WxClient.User.GetUsersByDepartmentId(req) 513 | if err != nil { 514 | if errors.Is(err, context.Canceled) { 515 | return 516 | } 517 | if i == retry-1 { 518 | logger.Error(logger.FormatError(err)) 519 | return 520 | } 521 | continue 522 | } 523 | break 524 | } 525 | if HttpCanceled { 526 | return 527 | } 528 | 529 | // 将用户插入到部门树中 530 | for _, wxUser := range userList { 531 | departmentIDs := wxUser.Department 532 | for _, departmentID := range departmentIDs { 533 | cli.insertUserToDepartmentTree(wxUser, departmentID, departmentTree) 534 | } 535 | } 536 | 537 | //保存文件 538 | logger.Info("正在保存至html文件...") 539 | msg, err := cli.saveDepartmentTreeWithUsersToHTML(departmentTree, "wechat_dump.html") 540 | if err != nil { 541 | logger.Error(logger.FormatError(err)) 542 | logger.Info("保存文件失败") 543 | } else { 544 | logger.Success(msg) 545 | } 546 | 547 | logger.Info("正在保存至xlsx文件...") 548 | msg, err = cli.saveUserTreeToExcel(departmentTree, "wechat_dump.xlsx") 549 | if err != nil { 550 | logger.Error(logger.FormatError(err)) 551 | logger.Info("保存文件失败") 552 | } else { 553 | logger.Success(msg) 554 | } 555 | }, 556 | } 557 | } 558 | 559 | func (cli *wechatCli) newInfo() *cobra.Command { 560 | return &cobra.Command{ 561 | Use: `info`, 562 | Short: `查看当前设置`, 563 | Run: func(cmd *cobra.Command, args []string) { 564 | cli.showClientConfig() 565 | }, 566 | } 567 | } 568 | 569 | func (cli *wechatCli) hasAccessToken() bool { 570 | return WxClient.GetAccessTokenFromCache() != "" 571 | } 572 | 573 | func (cli *wechatCli) showUserInfo(userInfo *wechat.UserEntry, inLine bool) { 574 | var depts []string 575 | for _, deptId := range userInfo.Department { 576 | for i := 0; i < retry; i++ { 577 | req := wechat.NewGetDepartmentReqBuilder(WxClient).DepartmentId(strconv.Itoa(deptId)).Build() 578 | deptInfo, err := WxClient.Department.Get(req) 579 | if err != nil { 580 | logger.Error(logger.FormatError(err)) 581 | continue 582 | } 583 | depts = append(depts, fmt.Sprintf("%s (ID:%d)", deptInfo.Name, deptId)) 584 | break 585 | } 586 | } 587 | if inLine { 588 | s := fmt.Sprintf(" -ID[%s] 姓名[%s] 所属部门ID[%s] 职位[%s] 手机[%s] 邮箱[%s] 微信二维码[%s]", userInfo.UserId, userInfo.Name, strings.Join(depts, "、"), userInfo.Position, userInfo.Mobile, userInfo.Email, userInfo.QrCode) 589 | fmt.Println(s) 590 | return 591 | } 592 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 593 | fmt.Printf("%-10s: %s\n", "ID", userInfo.UserId) 594 | fmt.Printf("%-8s: %s\n", "姓名", userInfo.Name) 595 | fmt.Printf("%-6s: %s\n", "所属部门", strings.Join(depts, "、")) 596 | fmt.Printf("%-8s: %s\n", "职位", userInfo.Position) 597 | fmt.Printf("%-8s: %s\n", "手机", userInfo.Mobile) 598 | fmt.Printf("%-8s: %s\n", "邮箱", userInfo.Email) 599 | fmt.Printf("%-5s: %s\n", "微信二维码", userInfo.QrCode) 600 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 601 | } 602 | 603 | func (cli *wechatCli) showClientConfig() { 604 | wxClientConfig := WxClient.GetConfig() 605 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 606 | if Proxy == nil { 607 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", "")) 608 | } else { 609 | fmt.Println(fmt.Sprintf("%-17s: %s", "proxy", *Proxy)) 610 | } 611 | fmt.Println(fmt.Sprintf("%-17s: %s", "domain", wechat.GetBaseDomain())) 612 | if wxClientConfig.CorpId == nil { 613 | fmt.Println(fmt.Sprintf("%-17s: %s", "corpid", "")) 614 | } else { 615 | fmt.Println(fmt.Sprintf("%-17s: %s", "corpid", *wxClientConfig.CorpId)) 616 | } 617 | if wxClientConfig.CorpSecret == nil { 618 | fmt.Println(fmt.Sprintf("%-17s: %s", "corpsecret", "")) 619 | } else { 620 | fmt.Println(fmt.Sprintf("%-17s: %s", "corpsecret", *wxClientConfig.CorpSecret)) 621 | } 622 | if wxClientConfig.AccessToken == nil { 623 | fmt.Println(fmt.Sprintf("%-17s: %s", "access_token", "")) 624 | } else { 625 | fmt.Println(fmt.Sprintf("%-17s: %s", "access_token", *wxClientConfig.AccessToken)) 626 | } 627 | fmt.Printf("%s\n", strings.Repeat("=", 20)) 628 | } 629 | 630 | func (cli *wechatCli) setHelpV1(cmds ...*cobra.Command) { 631 | for _, cmd := range cmds { 632 | // 不自己打印会多一个空白行 633 | cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 634 | logger.Error(err) 635 | return nil //返回error会自动打印用法 636 | }) 637 | 638 | cmd.SilenceErrors = true // 禁止打印错误信息 639 | cmd.DisableSuggestions = true // 禁用帮助信息 640 | cmd.SilenceUsage = true // 禁用用法信息 641 | cmd.Flags().SortFlags = false 642 | 643 | // 设置自定义的使用帮助函数 644 | cmd.SetHelpFunc(func(cmd *cobra.Command, s []string) { 645 | fmt.Print(wechatUsage) 646 | }) 647 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 648 | fmt.Print(wechatUsage) 649 | return nil 650 | }) 651 | } 652 | } 653 | 654 | type WxDepartmentNode struct { 655 | wechat.DepartmentEntry 656 | User []*wechat.UserEntry 657 | Children []*WxDepartmentNode 658 | } 659 | 660 | func (cli *wechatCli) insertUserToDepartmentTree(wxUser *wechat.UserEntry, departmentID int, departments []*WxDepartmentNode) { 661 | for _, department := range departments { 662 | if department.ID == departmentID { 663 | // 创建一个新的用户对象并复制数据 664 | newUser := *wxUser 665 | department.User = append(department.User, &newUser) 666 | return 667 | } 668 | 669 | cli.insertUserToDepartmentTree(wxUser, departmentID, department.Children) 670 | } 671 | } 672 | 673 | func (cli *wechatCli) buildDepartmentIdsTree(nodes []*WxDepartmentNode) []*WxDepartmentNode { 674 | var rootDepartments []*WxDepartmentNode 675 | departmentMap := make(map[int]*WxDepartmentNode) 676 | // 创建部门映射表 677 | for _, dept := range nodes { 678 | departmentMap[dept.ID] = dept 679 | } 680 | 681 | // 构建部门树 682 | for _, dept := range nodes { 683 | departmentNode := departmentMap[dept.ID] 684 | parentDept, ok := departmentMap[dept.ParentId] 685 | if ok { 686 | parentDept.Children = append(parentDept.Children, departmentNode) 687 | } else { 688 | rootDepartments = append(rootDepartments, departmentNode) 689 | } 690 | } 691 | return rootDepartments 692 | } 693 | 694 | func (cli *wechatCli) buildDepartmentTree(depts []*wechat.DepartmentEntry) []*WxDepartmentNode { 695 | var rootDepartments []*WxDepartmentNode 696 | departmentMap := make(map[int]*WxDepartmentNode) 697 | // 创建部门映射表 698 | for _, dept := range depts { 699 | departmentMap[dept.ID] = &WxDepartmentNode{ 700 | DepartmentEntry: *dept, 701 | User: []*wechat.UserEntry{}, 702 | Children: []*WxDepartmentNode{}, 703 | } 704 | } 705 | 706 | // 构建部门树 707 | for _, dept := range depts { 708 | departmentNode := departmentMap[dept.ID] 709 | parentDept, ok := departmentMap[dept.ParentId] 710 | if ok { 711 | parentDept.Children = append(parentDept.Children, departmentNode) 712 | } else { 713 | rootDepartments = append(rootDepartments, departmentNode) 714 | } 715 | } 716 | return rootDepartments 717 | } 718 | 719 | // printDepartmentTree 打印部门树 720 | func (cli *wechatCli) printDepartmentTree(nodes []*WxDepartmentNode, level int, index *int) { 721 | for _, dept := range nodes { 722 | //if *index >= verbose && verbose > -1 { 723 | // break 724 | //} 725 | var nameEn string 726 | var leaderId string 727 | if dept.NameEn != "" { 728 | nameEn = fmt.Sprintf(" 英文名称[%s]", dept.NameEn) 729 | } 730 | if len(dept.DepartmentLeader) != 0 { 731 | leaderId = fmt.Sprintf(" 领导ID[%s]", strings.Join(dept.DepartmentLeader, "、")) 732 | } 733 | fmt.Printf("%s -ID[%d] 名称[%s]%s%s\n", strings.Repeat(" ", 3*level), dept.ID, dept.Name, nameEn, leaderId) 734 | *index++ 735 | cli.printDepartmentTree(dept.Children, level+1, index) 736 | } 737 | } 738 | 739 | // printDepartmentIdsTree 打印部门ID树 740 | func (cli *wechatCli) printDepartmentIdsTree(nodes []*WxDepartmentNode, level int, index *int) { 741 | for _, dept := range nodes { 742 | if *index >= verbose && verbose > -1 { 743 | break 744 | } 745 | fmt.Printf("%s -ID:%d\n", strings.Repeat(" ", 3*level), dept.ID) 746 | *index++ 747 | cli.printDepartmentIdsTree(dept.Children, level+1, index) 748 | } 749 | } 750 | 751 | // generateDepartmentTreeHTML 生成包含部门信息的部门树HTML代码 752 | func (cli *wechatCli) generateDepartmentTreeHTML(nodes []*WxDepartmentNode, level int) string { 753 | html := "" 754 | for _, dept := range nodes { 755 | html += fmt.Sprintf("
    ", level) 756 | 757 | // 添加折叠/展开按钮 758 | if len(dept.Children) > 0 { 759 | html += fmt.Sprintf("-") 760 | } else { 761 | html += "" 762 | } 763 | 764 | s := fmt.Sprintf("ID:%d  %s", dept.ID, dept.Name) 765 | if dept.NameEn != "" { 766 | s += fmt.Sprintf("  英文名称:%s", dept.NameEn) 767 | } 768 | if len(dept.DepartmentLeader) != 0 { 769 | s += fmt.Sprintf("  领导ID:%s\n", strings.Join(dept.DepartmentLeader, "、")) 770 | } 771 | 772 | // 添加部门名称 773 | html += fmt.Sprintf("%s", s) 774 | 775 | // 递归生成子部门树 776 | if len(dept.Children) > 0 { 777 | html += cli.generateDepartmentTreeHTML(dept.Children, level+1) 778 | } 779 | 780 | html += "
    " 781 | } 782 | return html 783 | } 784 | 785 | // saveDepartmentTreeToHTML 生成含部门信息的部门树并将其输出为HTML文档 786 | func (cli *wechatCli) saveDepartmentTreeToHTML(nodes []*WxDepartmentNode, filename string) (string, error) { 787 | index := strings.LastIndex(filename, ".html") 788 | if index == -1 { 789 | filename = filename + ".html" 790 | } 791 | tmp := filename 792 | // 判断文件是否存在 793 | if utils.IsFileExists(filename) { 794 | newFilename := generateNewFilename(filename) 795 | filename = newFilename 796 | } 797 | file, err := os.Create(filename) 798 | if err != nil { 799 | return "", errors.New("创建HTML文件失败: " + err.Error()) 800 | } 801 | defer file.Close() 802 | departmentTreeHTML := cli.generateDepartmentTreeHTML(nodes, 0) 803 | htmlDocument := cli.generateTreeHTMLDocument(departmentTreeHTML) 804 | _, err = file.WriteString(htmlDocument) 805 | if err != nil { 806 | return "", errors.New("无法写入HTML内容到文件: " + err.Error()) 807 | } 808 | if filename == tmp { 809 | return fmt.Sprintf("文件已保存至 %s", filename), nil 810 | } 811 | return fmt.Sprintf("文件 %s 已存在,已保存至 %s", tmp, filename), nil 812 | } 813 | 814 | // generateDepartmentIdsTreeHTML 生成只包含ID的部门树HTML代码 815 | func (cli *wechatCli) generateDepartmentIdsTreeHTML(nodes []*WxDepartmentNode, level int) string { 816 | html := "" 817 | for _, dept := range nodes { 818 | html += fmt.Sprintf("
    ", level) 819 | 820 | // 添加折叠/展开按钮 821 | if len(dept.Children) > 0 { 822 | html += fmt.Sprintf("-") 823 | } else { 824 | html += "" 825 | } 826 | 827 | s := fmt.Sprintf("ID:%d", dept.ID) 828 | 829 | // 添加部门ID 830 | html += fmt.Sprintf("%s", s) 831 | 832 | // 递归生成子部门树 833 | if len(dept.Children) > 0 { 834 | html += cli.generateDepartmentTreeHTML(dept.Children, level+1) 835 | } 836 | 837 | html += "
    " 838 | } 839 | return html 840 | } 841 | 842 | // departmentIdsTreeToHTML 生成只包含ID的部门树并将其输出为HTML文档 843 | func (cli *wechatCli) departmentIdsTreeToHTML(nodes []*WxDepartmentNode, filename string) (string, error) { 844 | index := strings.LastIndex(filename, ".html") 845 | if index == -1 { 846 | filename = filename + ".html" 847 | } 848 | tmp := filename 849 | // 判断文件是否存在 850 | if utils.IsFileExists(filename) { 851 | newFilename := generateNewFilename(filename) 852 | filename = newFilename 853 | } 854 | file, err := os.Create(filename) 855 | if err != nil { 856 | return "", errors.New("创建HTML文件失败: " + err.Error()) 857 | } 858 | defer file.Close() 859 | departmentTreeHTML := cli.generateDepartmentIdsTreeHTML(nodes, 0) 860 | htmlDocument := cli.generateTreeHTMLDocument(departmentTreeHTML) 861 | _, err = file.WriteString(htmlDocument) 862 | if err != nil { 863 | return "", errors.New("无法写入HTML内容到文件: " + err.Error()) 864 | } 865 | if filename == tmp { 866 | return fmt.Sprintf("文件已保存至 %s", filename), nil 867 | } 868 | return fmt.Sprintf("文件 %s 已存在,已保存至 %s", tmp, filename), nil 869 | } 870 | 871 | // generateDepartmentTreeWithUsersHTML 生成包含部门信息和用户信息的部门树HTML代码 872 | func (cli *wechatCli) generateDepartmentTreeWithUsersHTML(nodes []*WxDepartmentNode, level int) string { 873 | html := "" 874 | for _, dept := range nodes { 875 | html += fmt.Sprintf("
    ", level) 876 | 877 | s := fmt.Sprintf("ID:%d  %s", dept.ID, dept.Name) 878 | if dept.NameEn != "" { 879 | s += fmt.Sprintf("  英文名称:%s", dept.NameEn) 880 | } 881 | if len(dept.DepartmentLeader) != 0 { 882 | s += fmt.Sprintf("  领导ID:%s\n", strings.Join(dept.DepartmentLeader, "、")) 883 | } 884 | 885 | // 添加折叠/展开按钮 886 | if len(dept.Children) > 0 { 887 | html += fmt.Sprintf("-") 888 | } else { 889 | html += "" 890 | } 891 | html += fmt.Sprintf("%s", s) 892 | 893 | // 添加部门名称和用户列表的父级容器 894 | html += fmt.Sprintf("
    ") 895 | a := "" 896 | for i := 0; i < len(dept.User); i++ { 897 | user := dept.User[i] 898 | m := fmt.Sprintf("%s  %s", user.UserId, user.Name) 899 | if user.Position != "" { 900 | m += fmt.Sprintf("  %s", user.Position) 901 | } 902 | m += fmt.Sprintf("  %s  %s  %s", user.Mobile, user.Email, user.QrCode) 903 | a += fmt.Sprintf("
  • %s
  • ", m) 904 | } 905 | html += fmt.Sprintf("
      %s
    ", a) 906 | 907 | // 递归生成子部门树 908 | if len(dept.Children) > 0 { 909 | html += cli.generateDepartmentTreeWithUsersHTML(dept.Children, level+1) 910 | } 911 | 912 | html += "
    " 913 | } 914 | return html 915 | } 916 | 917 | // saveDepartmentTreeWithUsersToHTML 生成包含部门信息和用户信息的部门树并将其输出为HTML文档 918 | func (cli *wechatCli) saveDepartmentTreeWithUsersToHTML(nodes []*WxDepartmentNode, filename string) (string, error) { 919 | index := strings.LastIndex(filename, ".html") 920 | if index == -1 { 921 | filename = filename + ".html" 922 | } 923 | tmp := filename 924 | // 判断文件是否存在 925 | if utils.IsFileExists(filename) { 926 | newFilename := generateNewFilename(filename) 927 | filename = newFilename 928 | } 929 | file, err := os.Create(filename) 930 | if err != nil { 931 | return "", errors.New("创建HTML文件失败: " + err.Error()) 932 | } 933 | defer file.Close() 934 | departmentTreeHTML := cli.generateDepartmentTreeWithUsersHTML(nodes, 0) 935 | htmlDocument := cli.generateTreeHTMLDocument(departmentTreeHTML) 936 | _, err = file.WriteString(htmlDocument) 937 | if err != nil { 938 | return "", errors.New("无法写入HTML内容到文件: " + err.Error()) 939 | } 940 | if filename == tmp { 941 | return fmt.Sprintf("文件已保存至 %s", filename), nil 942 | } 943 | return fmt.Sprintf("%s 已存在,已另存为 %s", tmp, filename), nil 944 | } 945 | 946 | // generateTreeHTMLDocument 生成添加折叠功能的完整的HTML文档 947 | func (cli *wechatCli) generateTreeHTMLDocument(content string) string { 948 | html := ` 949 | 950 | 951 | 952 | 971 | 1007 | 1008 | 1009 |
    1010 |
    1011 | 1012 | 1013 |
    1014 | %s 1015 |
    1016 | 1017 | 1018 | ` 1019 | return fmt.Sprintf(html, content) 1020 | } 1021 | 1022 | // saveUserTreeToExcel 生成包含部门名称的用户信息的XLSX文档 1023 | func (cli *wechatCli) saveUserTreeToExcel(nodes []*WxDepartmentNode, filename string) (string, error) { 1024 | if !strings.HasSuffix(filename, ".xlsx") { 1025 | filename = filename + ".xlsx" 1026 | } 1027 | tmp := filename 1028 | // 判断文件是否存在 1029 | if utils.IsFileExists(filename) { 1030 | newFilename := generateNewFilename(filename) 1031 | filename = newFilename 1032 | } 1033 | 1034 | // 设置表头 1035 | headers := []any{"id", "部门名称", "部门英文名称", "部门ID", "部门领导", "上级部门ID", "用户ID", "姓名", "性别", "电话号码", "邮箱", "职位", "微信二维码"} 1036 | 1037 | var data [][]any 1038 | var index = 0 1039 | for _, d := range nodes { 1040 | for _, user := range d.User { 1041 | index++ 1042 | gender := "" 1043 | if user.Gender == "1" { 1044 | gender = "男" 1045 | } else if user.Gender == "0" { 1046 | gender = "女" 1047 | } 1048 | s := []any{index, d.Name, d.NameEn, d.ID, strings.Join(d.DepartmentLeader, "、"), d.ParentId, user.UserId, user.Name, gender, user.Mobile, user.Email, user.Position, user.QrCode} 1049 | data = append(data, s) 1050 | } 1051 | cli.fetchColItem(&index, &data, d.Children) 1052 | } 1053 | 1054 | // 保存文件 1055 | err := saveToExcel(headers, data, filename) 1056 | if err != nil { 1057 | return "", errors.New("保存 Excel 文件失败: " + err.Error()) 1058 | } 1059 | if tmp != filename { 1060 | return fmt.Sprintf("%s 已存在,已另存为 %s", tmp, filename), nil 1061 | } 1062 | return fmt.Sprintf("文件已保存至 %s", filename), nil 1063 | } 1064 | 1065 | // saveUserToExcel 生成包含所属部门ID的用户信息的XLSX文档 1066 | func (cli *wechatCli) saveUserToExcel(users []*wechat.UserEntry, filename string) (string, error) { 1067 | index := strings.LastIndex(filename, ".xlsx") 1068 | if index == -1 { 1069 | filename = filename + ".xlsx" 1070 | } 1071 | tmp := filename 1072 | // 判断文件是否存在 1073 | if utils.IsFileExists(filename) { 1074 | newFilename := generateNewFilename(filename) 1075 | filename = newFilename 1076 | } 1077 | 1078 | // 设置表头 1079 | headers := []any{"id", "所属部门ID", "用户ID", "姓名", "性别", "电话号码", "邮箱", "职位", "微信二维码"} 1080 | 1081 | var data [][]any 1082 | // 写入内容 1083 | for i, user := range users { 1084 | var s []string 1085 | for _, deptId := range user.Department { 1086 | s = append(s, strconv.Itoa(deptId)) 1087 | } 1088 | var gender string 1089 | if user.Gender == "1" { 1090 | gender = "男" 1091 | } else if user.Gender == "0" { 1092 | gender = "女" 1093 | } 1094 | d := []any{i, strings.Join(s, " "), user.UserId, user.Name, gender, user.Mobile, user.Email, user.Position, user.QrCode} 1095 | data = append(data, d) 1096 | } 1097 | // 保存文件 1098 | err := saveToExcel(headers, data, filename) 1099 | if err != nil { 1100 | return "", errors.New("保存 Excel 文件失败: " + err.Error()) 1101 | } 1102 | if tmp != filename { 1103 | return fmt.Sprintf("%s 已存在,已另存为 %s", tmp, filename), nil 1104 | } 1105 | return fmt.Sprintf("文件已保存至 %s", filename), nil 1106 | } 1107 | 1108 | func (cli *wechatCli) fetchColItem(index *int, data *[][]any, dept []*WxDepartmentNode) { 1109 | for _, d := range dept { 1110 | for _, user := range d.User { 1111 | *index++ 1112 | gender := "" 1113 | if user.Gender == "1" { 1114 | gender = "男" 1115 | } else if user.Gender == "0" { 1116 | gender = "女" 1117 | } 1118 | s := []any{*index, d.Name, d.NameEn, d.ID, strings.Join(d.DepartmentLeader, "、"), d.ParentId, user.UserId, user.Name, gender, user.Mobile, user.Email, user.Position, user.QrCode} 1119 | *data = append(*data, s) 1120 | } 1121 | cli.fetchColItem(index, data, d.Children) 1122 | } 1123 | } 1124 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const Version = "v1.0.4" 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module idebug 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/buger/jsonparser v1.1.1 7 | github.com/chzyer/readline v1.5.1 8 | github.com/coreos/go-semver v0.3.1 9 | github.com/fasnow/ghttp v0.0.9 10 | github.com/fasnow/go-prompt v0.0.2 11 | github.com/fasnow/readline v0.0.0-2023070501 12 | github.com/larksuite/oapi-sdk-go/v3 v3.0.23 13 | github.com/manifoldco/promptui v0.9.0 14 | github.com/mattn/go-colorable v0.1.13 15 | github.com/nsf/termbox-go v1.1.1 16 | github.com/spf13/cobra v1.7.0 17 | github.com/spf13/pflag v1.0.5 18 | github.com/xuri/excelize/v2 v2.7.1 19 | ) 20 | 21 | require ( 22 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/mattn/go-runewidth v0.0.9 // indirect 25 | github.com/mattn/go-tty v0.0.3 // indirect 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 27 | github.com/pkg/term v1.2.0-beta.2 // indirect 28 | github.com/richardlehane/mscfb v1.0.4 // indirect 29 | github.com/richardlehane/msoleps v1.0.3 // indirect 30 | github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect 31 | github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect 32 | golang.org/x/crypto v0.8.0 // indirect 33 | golang.org/x/net v0.9.0 // indirect 34 | golang.org/x/sys v0.10.0 // indirect 35 | golang.org/x/text v0.9.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 2 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 5 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 7 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 8 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 11 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 12 | github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= 13 | github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/fasnow/ghttp v0.0.9 h1:LsFVwRFeep4VGq/V23OSTvBCbuNnADYu+WvKN3M+JKI= 19 | github.com/fasnow/ghttp v0.0.9/go.mod h1:FhjXn9lR5qcoG3XsbdgBjBqqQ8KFOn4CasqS6GD4GfE= 20 | github.com/fasnow/go-prompt v0.0.2 h1:DWc0x92g/39gjEqiSow1KP7faWlVJ45rfjrZj24Fdt4= 21 | github.com/fasnow/go-prompt v0.0.2/go.mod h1:NHz7oCaGdmmtHxNGn0xf/HyX52tAS0nKksFf2gRMAs4= 22 | github.com/fasnow/readline v0.0.0-2023070501 h1:ihC0BLzInOjx7I0A88I1s2bWaZAqRHPsTcOj7oMElnk= 23 | github.com/fasnow/readline v0.0.0-2023070501/go.mod h1:WENUG0hqY/pslQ03uWJBBeRMVZbLqwNEXQ1GoBXD0u4= 24 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 25 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 26 | github.com/larksuite/oapi-sdk-go/v3 v3.0.23 h1:mn4pD4JXgzYbpc9TT0grGHPcYppYEmisKMHeTwULu5Q= 27 | github.com/larksuite/oapi-sdk-go/v3 v3.0.23/go.mod h1:FKi8vBgtkBt/xNRQUwdWvoDmsPh7/wP75Sn5IBIBQLk= 28 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 29 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 30 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 31 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 34 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 35 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 36 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 37 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 38 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 40 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 41 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 42 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= 43 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= 44 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 45 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 46 | github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= 47 | github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= 48 | github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= 49 | github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= 53 | github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= 54 | github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 55 | github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= 56 | github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 57 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 59 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 60 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 61 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 64 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 65 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 66 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 67 | github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c= 68 | github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 69 | github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI= 70 | github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY= 71 | github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M= 72 | github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 73 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 76 | golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= 77 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 78 | golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= 79 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= 80 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 81 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 82 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 83 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 84 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 85 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 86 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 87 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 88 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 90 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 91 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 93 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 110 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 112 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 113 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 114 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 117 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 118 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 119 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 120 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 121 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 122 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 123 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 124 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 128 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 129 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 130 | -------------------------------------------------------------------------------- /images/image-20230606231323064.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasnow/idebug/1006b54f39dbb3af9c398363bbeec7b5fa8a4e75/images/image-20230606231323064.png -------------------------------------------------------------------------------- /images/image-20230606231931180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasnow/idebug/1006b54f39dbb3af9c398363bbeec7b5fa8a4e75/images/image-20230606231931180.png -------------------------------------------------------------------------------- /images/image-20230606232126727.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasnow/idebug/1006b54f39dbb3af9c398363bbeec7b5fa8a4e75/images/image-20230606232126727.png -------------------------------------------------------------------------------- /images/image-20230606232756363.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasnow/idebug/1006b54f39dbb3af9c398363bbeec7b5fa8a4e75/images/image-20230606232756363.png -------------------------------------------------------------------------------- /images/image-20230606232943841.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasnow/idebug/1006b54f39dbb3af9c398363bbeec7b5fa8a4e75/images/image-20230606232943841.png -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mattn/go-colorable" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | ) 10 | 11 | var Writer = colorable.NewColorableStdout() 12 | 13 | const ( 14 | reset = "\033[0m" 15 | red = "\033[31m" 16 | green = "\033[32m" 17 | blue = "\033[34m" 18 | ) 19 | 20 | func Error(err error) { 21 | fmt.Fprintln(Writer, red+"[!] "+reset+err.Error()) 22 | } 23 | 24 | func Success(str string) { 25 | fmt.Fprintln(Writer, green+"[+] "+reset+str) 26 | } 27 | 28 | func Info(str string) { 29 | fmt.Fprintln(Writer, "[+] "+str) 30 | } 31 | func Warning(str string) { 32 | fmt.Fprintln(Writer, red+"[!] "+reset+str) 33 | } 34 | 35 | func Println(any string) { 36 | fmt.Fprintln(Writer, Writer, any) 37 | } 38 | 39 | func Notice(str string) { 40 | fmt.Fprintln(Writer, green+"[+] "+str+reset) 41 | } 42 | 43 | func ModuleSelectedV1(module string) { 44 | if module != "" { 45 | fmt.Fprint(Writer, fmt.Sprintf("idebug %s >", red+module+reset)) 46 | return 47 | } 48 | fmt.Fprint(Writer, "idebug >") 49 | } 50 | 51 | func ModuleSelectedV2(module string) string { 52 | if module != "" { 53 | return fmt.Sprintf("idebug %s >", red+module+reset) 54 | } 55 | return "idebug >" 56 | } 57 | 58 | func ModuleSelectedV3(module string) string { 59 | if module != "" { 60 | return fmt.Sprintf("idebug %s >", module) 61 | } 62 | return "idebug >" 63 | } 64 | 65 | func FormatError(err error) error { 66 | pc, _, line, ok := runtime.Caller(1) 67 | if !ok { 68 | return err 69 | } 70 | dir, e := os.Getwd() 71 | if e != nil { 72 | return err 73 | } 74 | dir = filepath.ToSlash(dir) 75 | funcName := runtime.FuncForPC(pc).Name() 76 | return fmt.Errorf("%s %d line: %s", funcName, line, err) 77 | } 78 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "idebug/cmd" 6 | "idebug/logger" 7 | "idebug/prompt" 8 | ) 9 | 10 | func main() { 11 | client := prompt.Client{} 12 | cmd.Banner() 13 | version, releaseUrl, publishTime, content := cmd.CheckUpdate() 14 | if version != "" { 15 | s := fmt.Sprintf("最新版本: %s %s", version, publishTime) 16 | s += "\n 下载地址: " + releaseUrl 17 | s += "\n 更新内容: " + content 18 | logger.Notice(s) 19 | } 20 | client.Run() 21 | } 22 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # 定义要构建的目标名称 2 | BINARY_NAME=idebug 3 | 4 | # 定义要构建的目标平台和架构 5 | PLATFORMS=darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 windows/amd64 windows/386 6 | 7 | # 定义 Go 编译器和标志 8 | GO=go 9 | BUILD_FLAGS=-ldflags="-s -w " -tags "!test" 10 | 11 | # 定义 UPX 压缩命令 12 | UPX=upx 13 | UPX_FLAGS=-9 14 | 15 | # 构建所有平台的目标 16 | all: clean build compress 17 | 18 | # 清理构建输出 19 | clean: 20 | rm -rf ./bin 21 | 22 | # 构建目标 23 | build: 24 | mkdir -p ./bin 25 | $(foreach platform, $(PLATFORMS), \ 26 | $(eval GOOS=$(word 1, $(subst /, ,$(platform)))) \ 27 | $(eval GOARCH=$(word 2, $(subst /, ,$(platform)))) \ 28 | $(eval BINARY_EXT=$(if $(filter $(GOOS),windows),.exe,)) \ 29 | env GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build $(BUILD_FLAGS) -o ./bin/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(BINARY_EXT) .; \ 30 | ) 31 | 32 | 33 | # 压缩二进制文件 34 | compress: 35 | $(foreach file, $(wildcard ./bin/*), \ 36 | $(UPX) $(UPX_FLAGS) $(file); \ 37 | ) 38 | 39 | .PHONY: all clean build compress 40 | -------------------------------------------------------------------------------- /plugin/feishu/department.go: -------------------------------------------------------------------------------- 1 | package feishu 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/fasnow/ghttp" 8 | "idebug/plugin" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type DepartmentI18nName struct { 16 | ZhCn string `json:"zh_cn,omitempty"` // 部门的中文名 17 | JaJp string `json:"ja_jp,omitempty"` // 部门的日文名 18 | EnUs string `json:"en_us,omitempty"` // 部门的英文名 19 | } 20 | 21 | type DepartmentStatus struct { 22 | IsDeleted bool `json:"is_deleted,omitempty"` // 是否被删除 23 | } 24 | 25 | type DepartmentLeader struct { 26 | LeaderType int `json:"leaderType,omitempty"` // 负责人类型 27 | LeaderID string `json:"leaderID,omitempty"` // 负责人ID 28 | } 29 | 30 | type DepartmentEntry struct { 31 | DepartmentID string `json:"department_id"` // 本部门的自定义部门ID;;注意:除需要满足正则规则外,同时不能以`od-`开头 32 | I18NName DepartmentI18nName `json:"i18n_name"` // 国际化的部门名称 33 | MemberCount int `json:"member_count"` // 部门下用户的个数 34 | Name string `json:"name"` // 部门名称 35 | OpenDepartmentID string `json:"open_department_id"` // 部门的open_id,类型与通过请求的查询参数传入的department_id_type相同 36 | Order string `json:"order"` // 部门的排序,即部门在其同级部门的展示顺序 37 | ParentDepartmentID string `json:"parent_department_id"` // 父部门的ID;;* 在根部门下创建新部门,该参数值为 “0” 38 | PrimaryMemberCount int `json:"primary_member_count"` // 部门下主属用户的个数 39 | Status DepartmentStatus `json:"status"` // 部门状态 40 | 41 | LeaderUserID string `json:"leader_user_id"` // 部门主管用户ID 42 | ChatID string `json:"chat_id"` // 部门群ID 43 | UnitIds []*string `json:"unit_ids"` // 部门单位自定义ID列表,当前只支持一个 44 | Leaders []*DepartmentLeader `json:"leaders"` // 部门负责人 45 | GroupChatEmployeeTypes []*int `json:"group_chat_employee_types"` // 部门群雇员类型限制。[]空列表时,表示为无任何雇员类型。类型字段可包含以下值,支持多个类型值;若有多个,用英文','分隔:;1、正式员工;2、实习生;3、外包;4、劳务;5、顾问;6、其他自定义类型字段,可通过下方接口获取到该租户的自定义员工类型的名称,参见[获取人员类型](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/employee_type_enum/list)。 46 | DepartmentHrbps []*string `json:"department_hrbps"` // 部门HRBP 47 | } 48 | 49 | type GetDepartmentReq struct { 50 | req *Req 51 | } 52 | 53 | type GetDepartmentReqBuilder struct { 54 | req *Req 55 | } 56 | 57 | func NewGetDepartmentReqBuilder(client *Client) *GetDepartmentReqBuilder { 58 | builder := &GetDepartmentReqBuilder{} 59 | builder.req = &Req{ 60 | PathParams: &plugin.PathParams{}, 61 | QueryParams: &plugin.QueryParams{}, 62 | Client: client, 63 | } 64 | return builder 65 | 66 | } 67 | 68 | func (builder *GetDepartmentReqBuilder) DepartmentId(id string) *GetDepartmentReqBuilder { 69 | builder.req.PathParams.Set(":department_id", id) 70 | return builder 71 | } 72 | 73 | func (builder *GetDepartmentReqBuilder) DepartmentIdType(t string) *GetDepartmentReqBuilder { 74 | builder.req.QueryParams.Set("department_id_type", t) 75 | return builder 76 | } 77 | 78 | func (builder *GetDepartmentReqBuilder) UserIdType(t string) *GetDepartmentReqBuilder { 79 | builder.req.QueryParams.Set("user_id_type", t) 80 | return builder 81 | } 82 | 83 | func (builder *GetDepartmentReqBuilder) Build() *GetDepartmentReq { 84 | req := &GetDepartmentReq{} 85 | req.req = builder.req 86 | return req 87 | } 88 | 89 | func (dept *department) Get(req *GetDepartmentReq) (DepartmentEntry, error) { 90 | deptEntry := DepartmentEntry{} 91 | id := strings.TrimSpace(req.req.PathParams.Get(":department_id")) 92 | if id == "" { 93 | return deptEntry, errors.New("部门ID不能为空") 94 | } 95 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", strings.Replace(getDepartmentUrl, ":department_id", id, 1), req.req.QueryParams.Encode()), nil) 96 | if err != nil { 97 | return deptEntry, err 98 | } 99 | if err != nil { 100 | return deptEntry, err 101 | } 102 | token, err := req.req.Client.autoGetTenantAccessToken() 103 | if err != nil { 104 | return deptEntry, err 105 | } 106 | request.Header.Set("Authorization", "Bearer "+token) 107 | response, err := req.req.Client.http.Do(request) 108 | if err != nil { 109 | return deptEntry, err 110 | } 111 | body, err := ghttp.GetResponseBody(response.Body) 112 | if err != nil { 113 | return deptEntry, err 114 | } 115 | var tmp struct { 116 | Code int `json:"code"` 117 | Msg string `json:"msg"` 118 | Data struct { 119 | DepartmentEntry `json:"department"` 120 | } `json:"data"` 121 | } 122 | err = json.Unmarshal(body, &tmp) 123 | if err != nil { 124 | return deptEntry, err 125 | } 126 | if tmp.Code != 0 { 127 | return deptEntry, fmt.Errorf("from server - " + tmp.Msg) 128 | } 129 | return tmp.Data.DepartmentEntry, nil 130 | } 131 | 132 | type GetBatchDepartmentReq struct { 133 | req *Req 134 | } 135 | 136 | type GetBatchDepartmentReqBuilder struct { 137 | req *Req 138 | } 139 | 140 | func NewGetBatchDepartmentReqBuilder(client *Client) *GetBatchDepartmentReqBuilder { 141 | builder := &GetBatchDepartmentReqBuilder{} 142 | builder.req = &Req{ 143 | PathParams: &plugin.PathParams{}, 144 | QueryParams: &plugin.QueryParams{}, 145 | Client: client, 146 | } 147 | return builder 148 | } 149 | 150 | func (builder *GetBatchDepartmentReqBuilder) DepartmentIds(ids []string) *GetBatchDepartmentReqBuilder { 151 | if len(ids) <= 0 { 152 | builder.req.QueryParams.Set("department_ids", "") 153 | return builder 154 | } 155 | builder.req.QueryParams.Set("department_ids", ids[0]) 156 | for _, id := range ids[1:] { 157 | builder.req.QueryParams.Add("department_ids", id) 158 | } 159 | return builder 160 | } 161 | 162 | func (builder *GetBatchDepartmentReqBuilder) DepartmentIdType(t string) *GetBatchDepartmentReqBuilder { 163 | builder.req.QueryParams.Set("department_id_type", t) 164 | return builder 165 | } 166 | 167 | func (builder *GetBatchDepartmentReqBuilder) UserIdType(t string) *GetBatchDepartmentReqBuilder { 168 | builder.req.QueryParams.Set("user_id_type", t) 169 | return builder 170 | } 171 | 172 | func (builder *GetBatchDepartmentReqBuilder) Build() *GetBatchDepartmentReq { 173 | req := &GetBatchDepartmentReq{} 174 | req.req = builder.req 175 | return req 176 | } 177 | 178 | func (dept *department) Bath(req *GetBatchDepartmentReq) ([]DepartmentEntry, error) { 179 | request, err := http.NewRequest("GET", getBatchDepartmentUrl+"?"+req.req.QueryParams.Encode(), nil) 180 | if err != nil { 181 | return nil, err 182 | } 183 | token, err := req.req.Client.autoGetTenantAccessToken() 184 | if err != nil { 185 | return nil, err 186 | } 187 | request.Header.Set("Authorization", "Bearer "+token) 188 | if err != nil { 189 | return nil, err 190 | } 191 | response, err := req.req.Client.http.Do(request) 192 | if err != nil { 193 | return nil, err 194 | } 195 | body, err := ghttp.GetResponseBody(response.Body) 196 | if err != nil { 197 | return nil, err 198 | } 199 | var tmp struct { 200 | Code int `json:"code"` 201 | Msg string `json:"msg"` 202 | Data struct { 203 | Items []DepartmentEntry `json:"items"` 204 | } `json:"data"` 205 | } 206 | err = json.Unmarshal(body, &tmp) 207 | if err != nil { 208 | return nil, err 209 | } 210 | if tmp.Code != 0 { 211 | return nil, fmt.Errorf("from server - " + tmp.Msg) 212 | } 213 | return tmp.Data.Items, nil 214 | } 215 | 216 | type GetDepartmentChildrenReq struct { 217 | req *Req 218 | } 219 | 220 | type GetDepartmentChildrenReqBuilder struct { 221 | req *Req 222 | } 223 | 224 | func NewGetDepartmentChildrenReqBuilder(client *Client) *GetDepartmentChildrenReqBuilder { 225 | builder := &GetDepartmentChildrenReqBuilder{} 226 | builder.req = &Req{ 227 | PathParams: &plugin.PathParams{}, 228 | QueryParams: &plugin.QueryParams{}, 229 | Client: client, 230 | } 231 | return builder 232 | } 233 | 234 | func (builder *GetDepartmentChildrenReqBuilder) DepartmentId(id string) *GetDepartmentChildrenReqBuilder { 235 | builder.req.PathParams.Set(":department_id", id) 236 | return builder 237 | } 238 | 239 | func (builder *GetDepartmentChildrenReqBuilder) DepartmentIdType(t string) *GetDepartmentChildrenReqBuilder { 240 | builder.req.QueryParams.Set("department_id_type", t) 241 | return builder 242 | } 243 | 244 | func (builder *GetDepartmentChildrenReqBuilder) UserIdType(t string) *GetDepartmentChildrenReqBuilder { 245 | builder.req.QueryParams.Set("user_id_type", t) 246 | return builder 247 | } 248 | 249 | func (builder *GetDepartmentChildrenReqBuilder) Fetch(fetch bool) *GetDepartmentChildrenReqBuilder { 250 | if fetch { 251 | builder.req.QueryParams.Set("fetch_children", "true") 252 | } else { 253 | builder.req.QueryParams.Set("fetch_children", "false") 254 | } 255 | return builder 256 | } 257 | 258 | func (builder *GetDepartmentChildrenReqBuilder) PageSize(size int) *GetDepartmentChildrenReqBuilder { 259 | builder.req.QueryParams.Set("page_size", strconv.Itoa(size)) 260 | return builder 261 | } 262 | 263 | func (builder *GetDepartmentChildrenReqBuilder) Build() *GetDepartmentChildrenReq { 264 | req := &GetDepartmentChildrenReq{} 265 | req.req = builder.req 266 | return req 267 | } 268 | 269 | func (dept *department) moreDepartment(f *Client, req *GetDepartmentChildrenReq, pageToken string) ([]*DepartmentEntry, error) { 270 | id := strings.TrimSpace(req.req.PathParams.Get(":department_id")) 271 | if id == "" { 272 | return nil, errors.New("部门ID不能为空") 273 | } 274 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s&page_token=%s", strings.Replace(getDepartmentChildrenUrl, ":department_id", id, 1), req.req.QueryParams.Encode(), pageToken), nil) 275 | if err != nil { 276 | return nil, err 277 | } 278 | pageToken, err = req.req.Client.autoGetTenantAccessToken() 279 | if err != nil { 280 | return nil, err 281 | } 282 | request.Header.Set("Authorization", "Bearer "+pageToken) 283 | if err != nil { 284 | return nil, err 285 | } 286 | response, err := f.http.Do(request) 287 | if err != nil { 288 | return nil, err 289 | } 290 | body, err := ghttp.GetResponseBody(response.Body) 291 | if err != nil { 292 | return nil, err 293 | } 294 | var tmp struct { 295 | Code int `json:"code"` 296 | Msg string `json:"msg"` 297 | Data struct { 298 | HasMore bool `json:"has_more"` 299 | PageToken string `json:"page_token"` 300 | Items []*DepartmentEntry `json:"items"` 301 | } `json:"data"` 302 | } 303 | err = json.Unmarshal(body, &tmp) 304 | if err != nil { 305 | return nil, err 306 | } 307 | if tmp.Code != 0 { 308 | return nil, fmt.Errorf("from server - " + tmp.Msg) 309 | } 310 | var deptItems []*DepartmentEntry 311 | deptItems = append(deptItems, tmp.Data.Items...) 312 | if !tmp.Data.HasMore { 313 | return deptItems, nil 314 | } 315 | time.Sleep(defaultInterval) 316 | moreDepartments, err := dept.moreDepartment(req.req.Client, req, tmp.Data.PageToken) 317 | if err != nil { 318 | return nil, err 319 | } 320 | return append(deptItems, moreDepartments...), nil 321 | } 322 | 323 | func (dept *department) Children(req *GetDepartmentChildrenReq) ([]*DepartmentEntry, error) { 324 | id := strings.TrimSpace(req.req.PathParams.Get(":department_id")) 325 | if id == "" { 326 | return nil, errors.New("部门ID不能为空") 327 | } 328 | request, err := http.NewRequest("GET", strings.Replace(getDepartmentChildrenUrl, ":department_id", id, 1)+"?"+req.req.QueryParams.Encode(), nil) 329 | if err != nil { 330 | return nil, err 331 | } 332 | token, err := req.req.Client.autoGetTenantAccessToken() 333 | if err != nil { 334 | return nil, err 335 | } 336 | request.Header.Set("Authorization", "Bearer "+token) 337 | if err != nil { 338 | return nil, err 339 | } 340 | response, err := req.req.Client.http.Do(request) 341 | if err != nil { 342 | return nil, err 343 | } 344 | body, err := ghttp.GetResponseBody(response.Body) 345 | if err != nil { 346 | return nil, err 347 | } 348 | var tmp struct { 349 | Code int `json:"code"` 350 | Msg string `json:"msg"` 351 | Data struct { 352 | HasMore bool `json:"has_more"` 353 | PageToken string `json:"page_token"` 354 | Items []*DepartmentEntry `json:"items"` 355 | } `json:"data"` 356 | } 357 | err = json.Unmarshal(body, &tmp) 358 | if err != nil { 359 | return nil, err 360 | } 361 | if tmp.Code != 0 { 362 | return nil, fmt.Errorf("from server - " + tmp.Msg) 363 | } 364 | var deptItems []*DepartmentEntry 365 | deptItems = append(deptItems, tmp.Data.Items...) 366 | if !tmp.Data.HasMore { 367 | return deptItems, nil 368 | } 369 | time.Sleep(defaultInterval) 370 | moreDepartments, err := dept.moreDepartment(req.req.Client, req, tmp.Data.PageToken) 371 | if err != nil { 372 | return nil, err 373 | } 374 | return append(deptItems, moreDepartments...), nil 375 | } 376 | -------------------------------------------------------------------------------- /plugin/feishu/feishu.go: -------------------------------------------------------------------------------- 1 | package feishu 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/fasnow/ghttp" 9 | "idebug/plugin" 10 | "idebug/utils" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | const ( 16 | getTenantAccessTokenUrl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" // 应用将代表租户(企业或团队)执行对应的操作,例如获取一个通讯录用户的信息。API 所能操作的数据资源范围受限于应用的身份所能操作的资源范围。由于商店应用会为多家企业提供服务,所以需要先获取对应企业的授权访问凭证 tenant_access_token,并使用该访问凭证来调用 API 访问企业的数据或者资源 17 | getAppAccessTokenUrl = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal" 18 | getUserAccessToken = "" // 应用以用户的身份进行相关的操作,访问的数据范围、可以执行的操作将会受到该用户的权限影响。 19 | getAuthScopeUrl = "https://open.feishu.cn/open-apis/contact/v3/scopes" 20 | getDepartmentUrl = "https://open.feishu.cn/open-apis/contact/v3/departments/:department_id" 21 | getBatchDepartmentUrl = "https://open.feishu.cn/open-apis/contact/v3/departments/batch" 22 | getDepartmentChildrenUrl = "https://open.feishu.cn/open-apis/contact/v3/departments/:department_id/children" 23 | getUserUrl = "https://open.feishu.cn/open-apis/contact/v3/users/:user_id" 24 | getUsersIdUrl = "https://open.feishu.cn/open-apis/contact/v3/users/find_by_department" 25 | userEmailPasswordChangeUrl = "https://open.feishu.cn/open-apis/admin/v1/password/reset" 26 | ) 27 | 28 | var defaultInterval = 200 * time.Millisecond 29 | 30 | type config struct { 31 | AppId *string 32 | AppSecret *string 33 | TenantAccessToken *string 34 | DepartmentScope map[string]string 35 | GroupScope map[string]string 36 | UserScope map[string]string 37 | } 38 | 39 | type department struct { 40 | client *Client 41 | } 42 | 43 | type user struct { 44 | client *Client 45 | } 46 | 47 | type Client struct { 48 | config *config 49 | Department *department 50 | User *user 51 | cache *utils.Cache // 保存access_token 52 | http *ghttp.Client 53 | } 54 | 55 | func NewClient() *Client { 56 | f := &Client{ 57 | config: &config{}, 58 | cache: utils.NewCache(3 * time.Second), 59 | http: &ghttp.Client{}, 60 | User: &user{}, 61 | Department: &department{}, 62 | } 63 | f.User.client = f 64 | f.Department.client = f 65 | return f 66 | } 67 | 68 | func (client *Client) SetContext(ctx *context.Context) { 69 | client.http.Context = ctx 70 | } 71 | 72 | func (client *Client) StopWhenContextCanceled(enable bool) { 73 | client.http.StopWhenContextCanceled = enable 74 | } 75 | 76 | func (client *Client) Set(appId, appSecret string) { 77 | conf := &config{ 78 | AppId: &appId, 79 | AppSecret: &appSecret, 80 | } 81 | client.config = conf 82 | client.cache = utils.NewCache(3 * time.Second) 83 | } 84 | 85 | func (client *Client) SetAppId(appId string) { 86 | conf := &config{AppSecret: client.config.AppSecret, AppId: &appId} 87 | client.config = conf 88 | client.cache = utils.NewCache(3 * time.Second) 89 | } 90 | 91 | func (client *Client) SetAppSecret(appSecret string) { 92 | conf := &config{AppSecret: &appSecret, AppId: client.config.AppId} 93 | client.config = conf 94 | client.cache = utils.NewCache(3 * time.Second) 95 | } 96 | 97 | // SetTenantAccessTokenFromServer 设置新的tenant_access_token 98 | func (client *Client) SetTenantAccessTokenFromServer() error { 99 | _, err := client.getNewTenantAccessToken() 100 | if err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | 106 | // GetNewAuthScope 获取新的tenant_access_token和新的权限范围,并设置两者的缓存,返回新的config 107 | func (client *Client) GetNewAuthScope(req *GetAuthScopeReq) (*config, error) { 108 | client.config.DepartmentScope = map[string]string{} 109 | client.config.UserScope = map[string]string{} 110 | departmentScope, _, userScope, err := client.getNewAuthScope(req) 111 | if err != nil { 112 | return nil, err 113 | } 114 | didType := req.req.QueryParams.Get("department_id_type") 115 | uidType := req.req.QueryParams.Get("user_id_type") 116 | for _, deptId := range departmentScope { 117 | //if client.http.Context!=nil && client.http.Context{ 118 | // 119 | //} 120 | req1 := NewGetDepartmentReqBuilder(client). 121 | DepartmentIdType(didType). 122 | DepartmentId(*deptId). 123 | Build() 124 | dpetInfo, err := client.Department.Get(req1) 125 | if err != nil { 126 | client.config.DepartmentScope[*deptId] = "" 127 | time.Sleep(defaultInterval) 128 | continue 129 | } 130 | client.config.DepartmentScope[*deptId] = dpetInfo.Name 131 | time.Sleep(defaultInterval) 132 | } 133 | for _, userId := range userScope { 134 | req1 := NewGetUserReqBuilder(client). 135 | UserId(*userId). 136 | UserIdType(uidType). 137 | Build() 138 | userInfo, err := client.User.Get(req1) 139 | if err != nil { 140 | client.config.UserScope[*userId] = "" 141 | time.Sleep(defaultInterval) 142 | continue 143 | } 144 | client.config.UserScope[*userId] = userInfo.Name 145 | time.Sleep(defaultInterval) 146 | } 147 | 148 | return client.config, nil 149 | } 150 | 151 | // getNewAuthScope dpetIds,gids,uids,error:获取新的tenant_access_token并设置两缓存,返回新的权限范围, 152 | func (client *Client) getNewAuthScope(req *GetAuthScopeReq) ([]*string, []*string, []*string, error) { 153 | req.req.QueryParams.Set("page_size", "100") 154 | request, err := http.NewRequest("GET", getAuthScopeUrl+"?"+req.req.QueryParams.Encode(), nil) 155 | if err != nil { 156 | return nil, nil, nil, err 157 | } 158 | token, err := client.getNewTenantAccessToken() 159 | if err != nil { 160 | return nil, nil, nil, err 161 | } 162 | request.Header.Set("Authorization", "Bearer "+token) 163 | if err != nil { 164 | return nil, nil, nil, err 165 | } 166 | response, err := client.http.Do(request) 167 | if err != nil { 168 | return nil, nil, nil, err 169 | } 170 | body, err := ghttp.GetResponseBody(response.Body) 171 | if err != nil { 172 | return nil, nil, nil, err 173 | } 174 | var tmp struct { 175 | Code int `json:"code"` 176 | Data struct { 177 | DepartmentIds []*string `json:"department_ids"` 178 | GroupIds []*string `json:"group_ids"` 179 | HasMore bool `json:"has_more"` 180 | PageToken string `json:"page_token"` 181 | UserIds []*string `json:"user_ids"` 182 | } `json:"data"` 183 | Msg string `json:"msg"` 184 | } 185 | err = json.Unmarshal(body, &tmp) 186 | if err != nil { 187 | return nil, nil, nil, err 188 | } 189 | if tmp.Code != 0 { 190 | return nil, nil, nil, fmt.Errorf("from server - " + tmp.Msg) 191 | } 192 | var ( 193 | departmentIds []*string 194 | groupIds []*string 195 | userIds []*string 196 | ) 197 | departmentIds = append(departmentIds, tmp.Data.DepartmentIds...) 198 | groupIds = append(groupIds, tmp.Data.GroupIds...) 199 | userIds = append(userIds, tmp.Data.UserIds...) 200 | if !tmp.Data.HasMore { 201 | return departmentIds, groupIds, userIds, nil 202 | } 203 | time.Sleep(defaultInterval) 204 | deptIds, groupIds, userIds, err := client.getAuthScopeMore(tmp.Data.PageToken, req) 205 | if err != nil { 206 | return nil, nil, nil, err 207 | } 208 | departmentIds = append(departmentIds, deptIds...) 209 | groupIds = append(groupIds, groupIds...) 210 | userIds = append(userIds, userIds...) 211 | return departmentIds, groupIds, userIds, nil 212 | } 213 | 214 | // GetAuthScopeFromCache 从缓存中取出token的权限范围,不涉及tenant_access_token更新 215 | func (client *Client) GetAuthScopeFromCache() *config { 216 | return client.config 217 | } 218 | 219 | // GetTenantAccessTokenFromCache 从缓存中取出token,不涉及tenant_access_token更新 220 | func (client *Client) GetTenantAccessTokenFromCache() string { 221 | value, ok := client.cache.Get("tenantAccessToken") 222 | if ok { 223 | if token, ok := value.(string); ok { 224 | return token 225 | } 226 | } 227 | return "" 228 | } 229 | 230 | func (client *Client) getAccessTokenByUrl(url string) (string, int, error) { 231 | postData := map[string]string{ 232 | "app_id": *client.config.AppId, 233 | "app_secret": *client.config.AppSecret, 234 | } 235 | request, err := http.NewRequest("POST", url, utils.ConvertToReader(postData)) 236 | if err != nil { 237 | return "", 0, err 238 | } 239 | request.Header.Set("Content-Type", "application/json; charset=utf-8") 240 | if err != nil { 241 | return "", 0, err 242 | } 243 | response, err := client.http.Do(request) 244 | if err != nil { 245 | return "", 0, err 246 | } 247 | body, err := ghttp.GetResponseBody(response.Body) 248 | if err != nil { 249 | return "", 0, err 250 | } 251 | var tmp struct { 252 | Code int `json:"code"` 253 | Expire int `json:"expire"` 254 | Msg string `json:"msg"` 255 | TenantAccessToken string `json:"tenant_access_token"` 256 | } 257 | err = json.Unmarshal(body, &tmp) 258 | if err != nil { 259 | return "", 0, err 260 | } 261 | if tmp.Code != 0 { 262 | return "", 0, fmt.Errorf("from server - " + tmp.Msg) 263 | } 264 | return tmp.TenantAccessToken, tmp.Expire, nil 265 | } 266 | 267 | func (client *Client) getAuthScopeMore(pageToken string, req *GetAuthScopeReq) ([]*string, []*string, []*string, error) { 268 | req.req.QueryParams.Set("page_token", pageToken) 269 | request, err := http.NewRequest("GET", getAuthScopeUrl+"?"+req.req.QueryParams.Encode(), nil) 270 | if err != nil { 271 | return nil, nil, nil, err 272 | } 273 | token, err := client.autoGetTenantAccessToken() 274 | if err != nil { 275 | return nil, nil, nil, err 276 | } 277 | request.Header.Set("Authorization", "Bearer "+token) 278 | if err != nil { 279 | return nil, nil, nil, err 280 | } 281 | response, err := client.http.Do(request) 282 | if err != nil { 283 | return nil, nil, nil, err 284 | } 285 | body, err := ghttp.GetResponseBody(response.Body) 286 | if err != nil { 287 | return nil, nil, nil, err 288 | } 289 | var tmp struct { 290 | Code int `json:"code"` 291 | Data struct { 292 | DepartmentIds []*string `json:"department_ids"` 293 | GroupIds []*string `json:"group_ids"` 294 | HasMore bool `json:"has_more"` 295 | PageToken string `json:"page_token"` 296 | UserIds []*string `json:"user_ids"` 297 | } `json:"data"` 298 | Msg string `json:"msg"` 299 | } 300 | err = json.Unmarshal(body, &tmp) 301 | if err != nil { 302 | return nil, nil, nil, err 303 | } 304 | if tmp.Code != 0 { 305 | return nil, nil, nil, fmt.Errorf("from server - " + tmp.Msg) 306 | } 307 | var ( 308 | departmentIds []*string 309 | groupIds []*string 310 | userIds []*string 311 | ) 312 | departmentIds = append(departmentIds, tmp.Data.GroupIds...) 313 | groupIds = append(groupIds, tmp.Data.GroupIds...) 314 | userIds = append(userIds, tmp.Data.UserIds...) 315 | if !tmp.Data.HasMore { 316 | return departmentIds, groupIds, userIds, nil 317 | } 318 | time.Sleep(defaultInterval) 319 | return client.getAuthScopeMore(tmp.Data.PageToken, req) 320 | } 321 | 322 | // 从缓存中取出tenant_access_token,没有缓存的话则添加新的 323 | func (client *Client) autoGetTenantAccessToken() (string, error) { 324 | value, ok := client.cache.Get("tenantAccessToken") 325 | if !ok { 326 | return client.getNewTenantAccessToken() 327 | } else { 328 | if token, ok := value.(string); ok { 329 | return token, nil 330 | } 331 | } 332 | return "", errors.New("获取tenant_access_token时出错") 333 | } 334 | 335 | // 获取新的tenant_access_token并设置缓存 336 | func (client *Client) getNewTenantAccessToken() (string, error) { 337 | token, expire, err := client.getAccessTokenByUrl(getTenantAccessTokenUrl) 338 | if err != nil { 339 | return "", err 340 | } 341 | client.cache.Set("tenantAccessToken", token, time.Duration(expire)*time.Second) 342 | client.config.TenantAccessToken = &token 343 | return token, nil 344 | } 345 | 346 | type GetAuthScopeReqBuilder struct { 347 | req *Req 348 | } 349 | 350 | type GetAuthScopeReq struct { 351 | req *Req 352 | } 353 | 354 | func NewGetAuthScopeReqBuilder(client *Client) *GetAuthScopeReqBuilder { 355 | builder := &GetAuthScopeReqBuilder{} 356 | builder.req = &Req{ 357 | PathParams: &plugin.PathParams{}, 358 | QueryParams: &plugin.QueryParams{}, 359 | Client: client, 360 | } 361 | return builder 362 | } 363 | 364 | func (builder *GetAuthScopeReqBuilder) UserIdType(t string) *GetAuthScopeReqBuilder { 365 | builder.req.QueryParams.Set("user_id_type", t) 366 | return builder 367 | } 368 | 369 | func (builder *GetAuthScopeReqBuilder) DepartmentIdType(t string) *GetAuthScopeReqBuilder { 370 | builder.req.QueryParams.Set("department_id_type", t) 371 | return builder 372 | } 373 | 374 | func (builder *GetAuthScopeReqBuilder) Build() *GetAuthScopeReq { 375 | req := &GetAuthScopeReq{} 376 | req.req = builder.req 377 | return req 378 | } 379 | -------------------------------------------------------------------------------- /plugin/feishu/reqparam.go: -------------------------------------------------------------------------------- 1 | package feishu 2 | 3 | import ( 4 | "idebug/plugin" 5 | ) 6 | 7 | type Req struct { 8 | Client *Client 9 | HttpMethod string 10 | ApiPath string 11 | Body interface{} 12 | QueryParams *plugin.QueryParams 13 | PathParams *plugin.PathParams 14 | } 15 | -------------------------------------------------------------------------------- /plugin/feishu/user.go: -------------------------------------------------------------------------------- 1 | package feishu 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/fasnow/ghttp" 8 | "idebug/plugin" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type UserEntry struct { 16 | UserId string `json:"user_id"` // 用户的user_id,租户内用户的唯一标识,不同ID的说明参见 [用户相关的 ID 概念](https://open.feishu.cn/document/home/user-identity-introduction/introduction) 17 | OpenId string `json:"open_id"` // 用户的open_id,应用内用户的唯一标识,不同ID的说明参见 [用户相关的 ID 概念](https://open.feishu.cn/document/home/user-identity-introduction/introduction) 18 | Name string `json:"name"` // 用户名 19 | EnName string `json:"en_name"` // 英文名 20 | Nickname string `json:"nickname"` // 别名 21 | Email string `json:"email"` // 邮箱;;注意:;1. 非中国大陆手机号成员必须同时添加邮箱;2. 邮箱不可重复 22 | Mobile string `json:"mobile"` // 手机号,在本企业内不可重复;未认证企业仅支持添加中国大陆手机号,通过飞书认证的企业允许添加海外手机号,注意国际电话区号前缀中必须包含加号 + 23 | MobileVisible bool `json:"mobile_visible"` // 手机号码可见性,true 为可见,false 为不可见,目前默认为 true。不可见时,组织员工将无法查看该员工的手机号码 24 | Gender int `json:"gender"` // 性别 25 | Status struct { 26 | IsFrozen bool `json:"is_frozen"` // 是否暂停 27 | IsResigned bool `json:"is_resigned"` // 是否离职 28 | IsActivated bool `json:"is_activated"` // 是否激活 29 | IsExited bool `json:"is_exited"` // 是否主动退出,主动退出一段时间后用户会自动转为已离职 30 | IsUnjoin bool `json:"is_unjoin"` // 是否未加入,需要用户自主确认才能加入团队 31 | } `json:"status"` // 用户状态,枚举类型,包括is_frozen、is_resigned、is_activated、is_exited 。;;用户状态转移参见:[用户状态图](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/user/field-overview#4302b5a1) 32 | DepartmentIds []string `json:"department_ids"` // 用户所属部门的ID列表,一个用户可属于多个部门。;;ID值的类型与查询参数中的department_id_type 对应。;;不同 ID 的说明与department_id的获取方式参见 [部门ID说明](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/department/field-overview#23857fe0) 33 | LeaderUserId string `json:"leader_user_id"` // 用户的直接主管的用户ID,ID值与查询参数中的user_id_type 对应。;;不同 ID 的说明参见 [用户相关的 ID 概念](https://open.feishu.cn/document/home/user-identity-introduction/introduction);;获取方式参见[如何获取user_id](https://open.feishu.cn/document/home/user-identity-introduction/how-to-get) 34 | City string `json:"city"` // 工作城市 35 | Country string `json:"country"` // 国家或地区Code缩写,具体写入格式请参考 [国家/地区码表](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/user/country-code-description) 36 | WorkStation string `json:"work_station"` // 工位 37 | JoinTime int `json:"join_time"` // 入职时间,时间戳格式,表示从1970年1月1日开始所经过的秒数 38 | IsTenantManager bool `json:"is_tenant_manager"` // 是否是租户超级管理员 39 | EmployeeNo string `json:"employee_no"` // 工号 40 | EmployeeType int `json:"employee_type"` // 员工类型,可选值有:;- `1`:正式员工;- `2`:实习生;- `3`:外包;- `4`:劳务;- `5`:顾问 ;同时可读取到自定义员工类型的 int 值,可通过下方接口获取到该租户的自定义员工类型的名称,参见[获取人员类型](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/employee_type_enum/list) 41 | 42 | EnterpriseEmail string `json:"enterprise_email"` // 企业邮箱,请先确保已在管理后台启用飞书邮箱服务;;创建用户时,企业邮箱的使用方式参见[用户接口相关问题](https://open.feishu.cn/document/ugTN1YjL4UTN24CO1UjN/uQzN1YjL0cTN24CN3UjN#77061525) 43 | 44 | JobTitle string `json:"job_title"` // 职务 45 | IsFrozen bool `json:"is_frozen"` // 是否暂停用户 46 | } 47 | 48 | type GetUsersByDepartmentIdReqBuilder struct { 49 | req Req 50 | } 51 | 52 | type GetUsersByDepartmentIdReq struct { 53 | req Req 54 | } 55 | 56 | func NewGetUsersByDepartmentIdReqBuilder(f *Client) *GetUsersByDepartmentIdReqBuilder { 57 | builder := &GetUsersByDepartmentIdReqBuilder{} 58 | builder.req = Req{ 59 | PathParams: &plugin.PathParams{}, 60 | QueryParams: &plugin.QueryParams{}, 61 | Client: f, 62 | } 63 | return builder 64 | } 65 | 66 | func (builder *GetUsersByDepartmentIdReqBuilder) DepartmentId(id string) *GetUsersByDepartmentIdReqBuilder { 67 | builder.req.QueryParams.Set("department_id", id) 68 | return builder 69 | } 70 | 71 | func (builder *GetUsersByDepartmentIdReqBuilder) DepartmentIdType(t string) *GetUsersByDepartmentIdReqBuilder { 72 | builder.req.QueryParams.Set("department_id_type", t) 73 | return builder 74 | } 75 | 76 | func (builder *GetUsersByDepartmentIdReqBuilder) UserIdType(t string) *GetUsersByDepartmentIdReqBuilder { 77 | builder.req.QueryParams.Set("user_id_type", t) 78 | return builder 79 | } 80 | 81 | func (builder *GetUsersByDepartmentIdReqBuilder) PageSize(size int) *GetUsersByDepartmentIdReqBuilder { 82 | builder.req.QueryParams.Set("page_size", strconv.Itoa(size)) 83 | return builder 84 | } 85 | 86 | func (builder *GetUsersByDepartmentIdReqBuilder) Build() *GetUsersByDepartmentIdReq { 87 | req := &GetUsersByDepartmentIdReq{} 88 | req.req = builder.req 89 | return req 90 | } 91 | 92 | func (u *user) GetUsersByDepartmentId(req *GetUsersByDepartmentIdReq) ([]*UserEntry, error) { 93 | id := strings.TrimSpace(req.req.QueryParams.Get("department_id")) 94 | if id == "" { 95 | return nil, errors.New("部门ID不能为空") 96 | } 97 | request, err := http.NewRequest("GET", getUsersIdUrl+"?"+req.req.QueryParams.Encode(), nil) 98 | if err != nil { 99 | return nil, err 100 | } 101 | token, err := req.req.Client.autoGetTenantAccessToken() 102 | if err != nil { 103 | return nil, err 104 | } 105 | request.Header.Set("Authorization", "Bearer "+token) 106 | if err != nil { 107 | return nil, err 108 | } 109 | response, err := req.req.Client.http.Do(request) 110 | if err != nil { 111 | return nil, err 112 | } 113 | body, err := ghttp.GetResponseBody(response.Body) 114 | if err != nil { 115 | return nil, err 116 | } 117 | var tmp struct { 118 | Code int `json:"code"` 119 | Msg string `json:"msg"` 120 | Data struct { 121 | HasMore bool `json:"has_more"` 122 | PageToken string `json:"page_token"` 123 | Items []*UserEntry `json:"items"` 124 | } `json:"data"` 125 | } 126 | err = json.Unmarshal(body, &tmp) 127 | if err != nil { 128 | return nil, err 129 | } 130 | if tmp.Code != 0 { 131 | return nil, fmt.Errorf("from server - " + tmp.Msg) 132 | } 133 | var users []*UserEntry 134 | users = append(users, tmp.Data.Items...) 135 | if !tmp.Data.HasMore { 136 | return users, nil 137 | } 138 | time.Sleep(defaultInterval) 139 | user, err := u.moreUser(req.req.Client, req, tmp.Data.PageToken) 140 | if err != nil { 141 | return nil, err 142 | } 143 | users = append(users, user...) 144 | return users, nil 145 | } 146 | 147 | func (u *user) moreUser(f *Client, req *GetUsersByDepartmentIdReq, pageToken string) ([]*UserEntry, error) { 148 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s&page_token=%s", getUsersIdUrl, req.req.QueryParams.Encode(), pageToken), nil) 149 | if err != nil { 150 | return nil, err 151 | } 152 | pageToken, err = req.req.Client.autoGetTenantAccessToken() 153 | if err != nil { 154 | return nil, err 155 | } 156 | request.Header.Set("Authorization", "Bearer "+pageToken) 157 | if err != nil { 158 | return nil, err 159 | } 160 | response, err := f.http.Do(request) 161 | if err != nil { 162 | return nil, err 163 | } 164 | body, err := ghttp.GetResponseBody(response.Body) 165 | if err != nil { 166 | return nil, err 167 | } 168 | var tmp struct { 169 | Code int `json:"code"` 170 | Msg string `json:"msg"` 171 | Data struct { 172 | HasMore bool `json:"has_more"` 173 | PageToken string `json:"page_token"` 174 | Items []*UserEntry `json:"items"` 175 | } `json:"data"` 176 | } 177 | err = json.Unmarshal(body, &tmp) 178 | if err != nil { 179 | return nil, err 180 | } 181 | if tmp.Code != 0 { 182 | return nil, fmt.Errorf("from server - " + tmp.Msg) 183 | } 184 | var users []*UserEntry 185 | users = append(users, tmp.Data.Items...) 186 | if !tmp.Data.HasMore { 187 | return users, nil 188 | } 189 | time.Sleep(defaultInterval) 190 | moreUsers, err := u.moreUser(f, req, tmp.Data.PageToken) 191 | if err != nil { 192 | return nil, err 193 | } 194 | return append(users, moreUsers...), nil 195 | } 196 | 197 | type GetUserReqBuilder struct { 198 | req Req 199 | } 200 | 201 | type GetUserReq struct { 202 | req Req 203 | } 204 | 205 | func NewGetUserReqBuilder(f *Client) *GetUserReqBuilder { 206 | builder := &GetUserReqBuilder{} 207 | builder.req = Req{ 208 | PathParams: &plugin.PathParams{}, 209 | QueryParams: &plugin.QueryParams{}, 210 | Client: f, 211 | } 212 | return builder 213 | } 214 | func (builder *GetUserReqBuilder) UserId(id string) *GetUserReqBuilder { 215 | builder.req.PathParams.Set(":user_id", id) 216 | return builder 217 | } 218 | 219 | func (builder *GetUserReqBuilder) UserIdType(t string) *GetUserReqBuilder { 220 | builder.req.QueryParams.Set("user_id_type", t) 221 | return builder 222 | } 223 | 224 | func (builder *GetUserReqBuilder) DepartmentIdType(t string) *GetUserReqBuilder { 225 | builder.req.QueryParams.Set("department_id_type", t) 226 | return builder 227 | } 228 | 229 | func (builder *GetUserReqBuilder) Build() *GetUserReq { 230 | req := &GetUserReq{} 231 | req.req = builder.req 232 | return req 233 | } 234 | 235 | func (u *user) Get(req *GetUserReq) (*UserEntry, error) { 236 | userEntry := &UserEntry{} 237 | id := strings.TrimSpace(req.req.PathParams.Get(":user_id")) 238 | if id == "" { 239 | return nil, errors.New("用户ID不能为空") 240 | } 241 | request, err := http.NewRequest("GET", strings.Replace(fmt.Sprintf("%s?%s", getUserUrl, req.req.QueryParams.Encode()), ":user_id", id, 1), nil) 242 | if err != nil { 243 | return nil, err 244 | } 245 | token, err := req.req.Client.autoGetTenantAccessToken() 246 | if err != nil { 247 | return nil, err 248 | } 249 | request.Header.Set("Authorization", "Bearer "+token) 250 | if err != nil { 251 | return nil, err 252 | } 253 | response, err := req.req.Client.http.Do(request) 254 | if err != nil { 255 | return nil, err 256 | } 257 | body, err := ghttp.GetResponseBody(response.Body) 258 | if err != nil { 259 | return nil, err 260 | } 261 | var tmp struct { 262 | Code int `json:"code"` 263 | Msg string `json:"msg"` 264 | Data struct { 265 | User UserEntry `json:"user"` 266 | } `json:"data"` 267 | } 268 | err = json.Unmarshal(body, &tmp) 269 | if err != nil { 270 | return nil, err 271 | } 272 | if tmp.Code != 0 { 273 | return nil, fmt.Errorf("from server - " + tmp.Msg) 274 | } 275 | userEntry = &tmp.Data.User 276 | return userEntry, nil 277 | } 278 | 279 | type UserEmailPasswordUpdateReqBuilder struct { 280 | req Req 281 | } 282 | 283 | type UserEmailPasswordChangeReq struct { 284 | req Req 285 | } 286 | 287 | func NewUserEmailPasswordChangeReqBuilder(f *Client) *UserEmailPasswordUpdateReqBuilder { 288 | builder := &UserEmailPasswordUpdateReqBuilder{} 289 | builder.req = Req{ 290 | PathParams: &plugin.PathParams{}, 291 | QueryParams: &plugin.QueryParams{}, 292 | Client: f, 293 | } 294 | return builder 295 | } 296 | 297 | func (builder *UserEmailPasswordUpdateReqBuilder) PostData(userId, password string) *UserEmailPasswordUpdateReqBuilder { 298 | var tmp struct { 299 | Password struct { 300 | EntEmailPassword string `json:"ent_email_password"` 301 | } `json:"password"` 302 | UserID string `json:"user_id"` 303 | } 304 | tmp.Password.EntEmailPassword = password 305 | tmp.UserID = userId 306 | postData, _ := json.Marshal(tmp) 307 | builder.req.Body = string(postData) 308 | return builder 309 | } 310 | 311 | func (builder *UserEmailPasswordUpdateReqBuilder) UserIdType(t string) *UserEmailPasswordUpdateReqBuilder { 312 | builder.req.QueryParams.Set("user_id_type", t) 313 | return builder 314 | } 315 | 316 | func (builder *UserEmailPasswordUpdateReqBuilder) Build() *UserEmailPasswordChangeReq { 317 | req := &UserEmailPasswordChangeReq{} 318 | req.req = builder.req 319 | return req 320 | } 321 | 322 | func (u *user) EmailPasswordUpdate(req *UserEmailPasswordChangeReq) error { 323 | value := "" 324 | if v, ok := req.req.Body.(string); ok { 325 | value = v 326 | } 327 | postData := strings.NewReader(value) 328 | request, err := http.NewRequest("POST", fmt.Sprintf("%s?%s", userEmailPasswordChangeUrl, req.req.QueryParams.Encode()), postData) 329 | if err != nil { 330 | return err 331 | } 332 | request.Header.Add("Content-Type", "application/json") 333 | if err != nil { 334 | return err 335 | } 336 | token, err := req.req.Client.autoGetTenantAccessToken() 337 | if err != nil { 338 | return err 339 | } 340 | request.Header.Set("Authorization", "Bearer "+token) 341 | if err != nil { 342 | return err 343 | } 344 | response, err := req.req.Client.http.Do(request) 345 | if err != nil { 346 | return err 347 | } 348 | body, err := ghttp.GetResponseBody(response.Body) 349 | if err != nil { 350 | return err 351 | } 352 | var tmp struct { 353 | Code int `json:"code"` 354 | Msg string `json:"msg"` 355 | Data struct { 356 | } `json:"data"` 357 | } 358 | err = json.Unmarshal(body, &tmp) 359 | if err != nil { 360 | return err 361 | } 362 | if tmp.Code != 0 { 363 | return fmt.Errorf("from server - " + tmp.Msg) 364 | } 365 | return nil 366 | } 367 | -------------------------------------------------------------------------------- /plugin/reqparams.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "net/url" 4 | 5 | type PathParams map[string]string 6 | 7 | func (u PathParams) Get(key string) string { 8 | vs := u[key] 9 | if len(vs) == 0 { 10 | return "" 11 | } 12 | return vs 13 | } 14 | func (u PathParams) Set(key, value string) { 15 | u[key] = value 16 | } 17 | 18 | type QueryParams map[string][]string 19 | 20 | func (u QueryParams) Get(key string) string { 21 | vs := u[key] 22 | if len(vs) == 0 { 23 | return "" 24 | } 25 | return vs[0] 26 | } 27 | func (u QueryParams) Set(key, value string) { 28 | u[key] = []string{value} 29 | } 30 | 31 | func (u QueryParams) Encode() string { 32 | return url.Values(u).Encode() 33 | } 34 | 35 | func (u QueryParams) Add(key, value string) { 36 | u[key] = append(u[key], value) 37 | } 38 | -------------------------------------------------------------------------------- /plugin/wechat/department.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/fasnow/ghttp" 8 | "idebug/plugin" 9 | "net/http" 10 | "net/url" 11 | ) 12 | 13 | type DepartmentEntrySimplified struct { 14 | ID int `json:"id"` 15 | ParentId int `json:"parentid"` 16 | Order int `json:"order"` 17 | } 18 | 19 | type DepartmentEntry struct { 20 | DepartmentEntrySimplified 21 | Name string `json:"name"` 22 | NameEn string `json:"name_en"` 23 | DepartmentLeader []string `json:"department_leader"` 24 | } 25 | 26 | type GetDepartmentReq struct { 27 | req *Req 28 | } 29 | 30 | type GetDepartmentReqBuilder struct { 31 | req *Req 32 | } 33 | 34 | func NewGetDepartmentReqBuilder(client *Client) *GetDepartmentReqBuilder { 35 | builder := &GetDepartmentReqBuilder{} 36 | builder.req = &Req{ 37 | PathParams: &plugin.PathParams{}, 38 | QueryParams: &plugin.QueryParams{}, 39 | Client: client, 40 | } 41 | return builder 42 | 43 | } 44 | 45 | func (builder *GetDepartmentReqBuilder) DepartmentId(id string) *GetDepartmentReqBuilder { 46 | builder.req.QueryParams.Set("id", id) 47 | return builder 48 | } 49 | 50 | func (builder *GetDepartmentReqBuilder) Build() *GetDepartmentReq { 51 | req := &GetDepartmentReq{} 52 | req.req = builder.req 53 | return req 54 | } 55 | 56 | func (d *department) Get(req *GetDepartmentReq) (*DepartmentEntry, error) { 57 | token, err := req.req.Client.getAccessTokenFromCache() 58 | if err != nil { 59 | return nil, err 60 | } 61 | id := req.req.QueryParams.Get("id") 62 | if id == "" { 63 | return nil, errors.New("部门ID不能为空") 64 | } 65 | params := url.Values{} 66 | params.Add("access_token", token) 67 | params.Add("id", id) 68 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getDepartmentUrl, params.Encode()), nil) 69 | if err != nil { 70 | return nil, err 71 | } 72 | response, err := d.client.http.Do(request) 73 | if err != nil { 74 | return nil, err 75 | } 76 | if response.StatusCode != 200 { 77 | return nil, errors.New(response.Status) 78 | } 79 | body, err := ghttp.GetResponseBody(response.Body) 80 | if err != nil { 81 | return nil, err 82 | } 83 | var tmp struct { 84 | ErrCode int `json:"errcode"` 85 | ErrMsg string `json:"errmsg"` 86 | Department DepartmentEntry `json:"department"` 87 | } 88 | err = json.Unmarshal(body, &tmp) 89 | if err != nil { 90 | return nil, err 91 | } 92 | if tmp.ErrCode != 0 { 93 | return nil, fmt.Errorf("from server - " + tmp.ErrMsg) 94 | } 95 | return &tmp.Department, nil 96 | } 97 | 98 | type GetDepartmentListReq struct { 99 | req *Req 100 | } 101 | 102 | type GetDepartmentListReqBuilder struct { 103 | req *Req 104 | } 105 | 106 | func NewGetDepartmentListReqBuilder(client *Client) *GetDepartmentListReqBuilder { 107 | builder := &GetDepartmentListReqBuilder{} 108 | builder.req = &Req{ 109 | PathParams: &plugin.PathParams{}, 110 | QueryParams: &plugin.QueryParams{}, 111 | Client: client, 112 | } 113 | return builder 114 | 115 | } 116 | 117 | func (builder *GetDepartmentListReqBuilder) DepartmentId(id string) *GetDepartmentListReqBuilder { 118 | builder.req.QueryParams.Set("id", id) 119 | return builder 120 | } 121 | 122 | func (builder *GetDepartmentListReqBuilder) Build() *GetDepartmentListReq { 123 | req := &GetDepartmentListReq{} 124 | req.req = builder.req 125 | return req 126 | } 127 | 128 | // GetList 递归获取 129 | func (d *department) GetList(req *GetDepartmentListReq) ([]*DepartmentEntry, error) { 130 | token, err := req.req.Client.getAccessTokenFromCache() 131 | if err != nil { 132 | return nil, err 133 | } 134 | params := url.Values{} 135 | params.Add("access_token", token) 136 | id := req.req.QueryParams.Get("id") 137 | if id != "" { 138 | params.Add("id", id) 139 | } 140 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getDepartmentListUrl, params.Encode()), nil) 141 | if err != nil { 142 | return nil, err 143 | } 144 | response, err := d.client.http.Do(request) 145 | if err != nil { 146 | return nil, err 147 | } 148 | if response.StatusCode != 200 { 149 | return nil, errors.New(response.Status) 150 | } 151 | body, err := ghttp.GetResponseBody(response.Body) 152 | if err != nil { 153 | return nil, err 154 | } 155 | var tmp struct { 156 | ErrCode int `json:"errcode"` 157 | ErrMsg string `json:"errmsg"` 158 | Department []*DepartmentEntry `json:"department"` 159 | } 160 | err = json.Unmarshal(body, &tmp) 161 | if err != nil { 162 | return nil, err 163 | } 164 | if tmp.ErrCode != 0 { 165 | return nil, fmt.Errorf("from server - " + tmp.ErrMsg) 166 | } 167 | return tmp.Department, nil 168 | } 169 | 170 | type GetDepartmentIdListReq struct { 171 | req *Req 172 | } 173 | 174 | type GetDepartmentIdListReqBuilder struct { 175 | req *Req 176 | } 177 | 178 | func NewGetDepartmentIdListReqBuilder(client *Client) *GetDepartmentIdListReqBuilder { 179 | builder := &GetDepartmentIdListReqBuilder{} 180 | builder.req = &Req{ 181 | PathParams: &plugin.PathParams{}, 182 | QueryParams: &plugin.QueryParams{}, 183 | Client: client, 184 | } 185 | return builder 186 | 187 | } 188 | 189 | func (builder *GetDepartmentIdListReqBuilder) DepartmentId(id string) *GetDepartmentIdListReqBuilder { 190 | builder.req.QueryParams.Set("id", id) 191 | return builder 192 | } 193 | 194 | func (builder *GetDepartmentIdListReqBuilder) Build() *GetDepartmentIdListReq { 195 | req := &GetDepartmentIdListReq{} 196 | req.req = builder.req 197 | return req 198 | } 199 | 200 | // GetIdList 递归获取 201 | func (d *department) GetIdList(req *GetDepartmentIdListReq) ([]*DepartmentEntrySimplified, error) { 202 | token, err := req.req.Client.getAccessTokenFromCache() 203 | if err != nil { 204 | return nil, err 205 | } 206 | params := url.Values{} 207 | params.Add("access_token", token) 208 | id := req.req.QueryParams.Get("id") 209 | if id != "" { 210 | params.Add("id", id) 211 | } 212 | var res struct { 213 | ErrCode int `json:"errcode"` 214 | ErrMsg string `json:"errmsg"` 215 | Department []*DepartmentEntrySimplified `json:"department_id"` 216 | } 217 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getDepartmentIdListUrl, params.Encode()), nil) 218 | if err != nil { 219 | return nil, err 220 | } 221 | response, err := d.client.http.Do(request) 222 | if err != nil { 223 | return nil, err 224 | } 225 | if response.StatusCode != 200 { 226 | return nil, errors.New(response.Status) 227 | } 228 | body, err := ghttp.GetResponseBody(response.Body) 229 | if err != nil { 230 | return nil, err 231 | } 232 | err = json.Unmarshal(body, &res) 233 | if err != nil { 234 | return nil, err 235 | } 236 | if res.ErrCode != 0 { 237 | return res.Department, fmt.Errorf("from server - " + res.ErrMsg) 238 | } 239 | return res.Department, nil 240 | } 241 | -------------------------------------------------------------------------------- /plugin/wechat/reqparam.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "idebug/plugin" 5 | ) 6 | 7 | type Req struct { 8 | Client *Client 9 | HttpMethod string 10 | ApiPath string 11 | Body interface{} 12 | QueryParams *plugin.QueryParams 13 | PathParams *plugin.PathParams 14 | } 15 | -------------------------------------------------------------------------------- /plugin/wechat/user.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/fasnow/ghttp" 8 | "idebug/plugin" 9 | "net/http" 10 | "net/url" 11 | ) 12 | 13 | type UserEntrySimplified struct { 14 | Name string `json:"name"` 15 | UserId string `json:"userid"` 16 | Department []int `json:"department"` 17 | OpenUserid string `json:"open_userid"` 18 | } 19 | 20 | type UserEntry struct { 21 | UserEntrySimplified 22 | Order []int `json:"order"` 23 | Position string `json:"position"` 24 | Mobile string `json:"mobile"` 25 | Gender string `json:"gender"` 26 | Email string `json:"email"` 27 | BizMail string `json:"biz_mail"` 28 | IsLeaderInDept []int `json:"is_leader_in_dept"` 29 | DirectLeader []string `json:"direct_leader"` 30 | Avatar string `json:"avatar"` 31 | ThumbAvatar string `json:"thumb_avatar"` 32 | Telephone string `json:"telephone"` 33 | Alias string `json:"alias"` 34 | Status int `json:"status"` 35 | Address string `json:"address"` 36 | EnglishName string `json:"english_name"` 37 | MainDepartment int `json:"main_department"` 38 | Extattr struct { 39 | Attrs []struct { 40 | Type int `json:"type"` 41 | Name string `json:"name"` 42 | Text struct { 43 | Value string `json:"value"` 44 | } `json:"text,omitempty"` 45 | Web struct { 46 | URL string `json:"url"` 47 | Title string `json:"title"` 48 | } `json:"web,omitempty"` 49 | } `json:"attrs"` 50 | } `json:"extattr"` 51 | QrCode string `json:"qr_code"` 52 | ExternalPosition string `json:"external_position"` 53 | ExternalProfile struct { 54 | ExternalCorpName string `json:"external_corp_name"` 55 | WechatChannels struct { 56 | Nickname string `json:"nickname"` 57 | Status int `json:"status"` 58 | } `json:"wechat_channels"` 59 | ExternalAttr []struct { 60 | Type int `json:"type"` 61 | Name string `json:"name"` 62 | Text struct { 63 | Value string `json:"value"` 64 | } `json:"text,omitempty"` 65 | Web struct { 66 | URL string `json:"url"` 67 | Title string `json:"title"` 68 | } `json:"web,omitempty"` 69 | Miniprogram struct { 70 | Appid string `json:"appid"` 71 | Pagepath string `json:"pagepath"` 72 | Title string `json:"title"` 73 | } `json:"miniprogram,omitempty"` 74 | } `json:"external_attr"` 75 | } `json:"external_profile"` 76 | } 77 | 78 | type GetUserReq struct { 79 | req *Req 80 | } 81 | 82 | type GetUserReqBuilder struct { 83 | req *Req 84 | } 85 | 86 | func NewGetUserReqBuilder(client *Client) *GetUserReqBuilder { 87 | builder := &GetUserReqBuilder{} 88 | builder.req = &Req{ 89 | Client: client, 90 | QueryParams: &plugin.QueryParams{}, 91 | PathParams: &plugin.PathParams{}, 92 | } 93 | return builder 94 | } 95 | 96 | func (builder *GetUserReqBuilder) UserId(id string) *GetUserReqBuilder { 97 | builder.req.QueryParams.Set("userid", id) 98 | return builder 99 | } 100 | 101 | func (builder *GetUserReqBuilder) Build() *GetUserReq { 102 | req := &GetUserReq{} 103 | req.req = builder.req 104 | return req 105 | } 106 | 107 | func (u *user) Get(req *GetUserReq) (*UserEntry, error) { 108 | token, err := req.req.Client.getAccessTokenFromCache() 109 | if err != nil { 110 | return nil, err 111 | } 112 | id := req.req.QueryParams.Get("userid") 113 | if id == "" { 114 | return nil, errors.New("用户ID不能为空") 115 | } 116 | params := url.Values{} 117 | params.Add("access_token", token) 118 | params.Add("userid", id) 119 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getUserUrl, params.Encode()), nil) 120 | if err != nil { 121 | return nil, err 122 | } 123 | request.Header.Add("User-Agent", "") 124 | response, err := u.client.http.Do(request) 125 | if err != nil { 126 | return nil, err 127 | } 128 | if response.StatusCode != 200 { 129 | return nil, errors.New(response.Status) 130 | } 131 | body, err := ghttp.GetResponseBody(response.Body) 132 | if err != nil { 133 | return nil, err 134 | } 135 | var res struct { 136 | ErrCode int `json:"errcode"` 137 | ErrMsg string `json:"errmsg"` 138 | *UserEntry 139 | } 140 | err = json.Unmarshal(body, &res) 141 | if err != nil { 142 | return nil, err 143 | } 144 | if res.ErrCode != 0 { 145 | return res.UserEntry, fmt.Errorf("from server - " + res.ErrMsg) 146 | } 147 | return res.UserEntry, nil 148 | } 149 | 150 | type GetUsersByDepartmentIdReq struct { 151 | req *Req 152 | } 153 | 154 | type GetUsersByDepartmentIdReqBuilder struct { 155 | req *Req 156 | } 157 | 158 | func NewGetUsersByDepartmentIdReqBuilder(client *Client) *GetUsersByDepartmentIdReqBuilder { 159 | builder := &GetUsersByDepartmentIdReqBuilder{} 160 | builder.req = &Req{ 161 | Client: client, 162 | QueryParams: &plugin.QueryParams{}, 163 | PathParams: &plugin.PathParams{}, 164 | } 165 | return builder 166 | } 167 | 168 | func (builder *GetUsersByDepartmentIdReqBuilder) DepartmentId(id string) *GetUsersByDepartmentIdReqBuilder { 169 | builder.req.QueryParams.Set("department_id", id) 170 | return builder 171 | } 172 | 173 | func (builder *GetUsersByDepartmentIdReqBuilder) Fetch(fetch bool) *GetUsersByDepartmentIdReqBuilder { 174 | if fetch { 175 | builder.req.QueryParams.Set("fetch_child", "1") 176 | } 177 | return builder 178 | } 179 | 180 | func (builder *GetUsersByDepartmentIdReqBuilder) Build() *GetUsersByDepartmentIdReq { 181 | req := &GetUsersByDepartmentIdReq{} 182 | req.req = builder.req 183 | return req 184 | } 185 | 186 | func (u *user) GetUsersByDepartmentId(req *GetUsersByDepartmentIdReq) ([]*UserEntry, error) { 187 | token, err := req.req.Client.getAccessTokenFromCache() 188 | if err != nil { 189 | return nil, err 190 | } 191 | id := req.req.QueryParams.Get("department_id") 192 | if id == "" { 193 | return nil, errors.New("部门ID不能为空") 194 | } 195 | params := url.Values{} 196 | params.Add("access_token", token) 197 | params.Add("department_id", id) 198 | fetch := req.req.QueryParams.Get("fetch_child") 199 | if fetch == "1" { 200 | params.Add("fetch_child", fetch) 201 | } 202 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getDepartmentUserUrl, params.Encode()), nil) 203 | if err != nil { 204 | return nil, err 205 | } 206 | response, err := u.client.http.Do(request) 207 | if err != nil { 208 | return nil, err 209 | } 210 | if response.StatusCode != 200 { 211 | return nil, errors.New(response.Status) 212 | } 213 | body, err := ghttp.GetResponseBody(response.Body) 214 | if err != nil { 215 | return nil, err 216 | } 217 | var res struct { 218 | ErrCode int `json:"errcode"` 219 | ErrMsg string `json:"errmsg"` 220 | UserList []*UserEntry `json:"userlist"` 221 | } 222 | err = json.Unmarshal(body, &res) 223 | if err != nil { 224 | return nil, err 225 | } 226 | if res.ErrCode != 0 { 227 | return res.UserList, fmt.Errorf("from server - " + res.ErrMsg) 228 | } 229 | return res.UserList, nil 230 | } 231 | 232 | type GetUsersSimplifiedByDepartmentIdReq struct { 233 | req *Req 234 | } 235 | 236 | type GetUsersSimplifiedByDepartmentIdReqBuilder struct { 237 | req *Req 238 | } 239 | 240 | func NewGetUsersSimplifiedByDepartmentIdReqBuilder(client *Client) *GetUsersSimplifiedByDepartmentIdReqBuilder { 241 | builder := &GetUsersSimplifiedByDepartmentIdReqBuilder{} 242 | builder.req = &Req{ 243 | Client: client, 244 | QueryParams: &plugin.QueryParams{}, 245 | PathParams: &plugin.PathParams{}, 246 | } 247 | return builder 248 | } 249 | 250 | func (builder *GetUsersSimplifiedByDepartmentIdReqBuilder) DepartmentId(id string) *GetUsersSimplifiedByDepartmentIdReqBuilder { 251 | builder.req.QueryParams.Set("department_id", id) 252 | return builder 253 | } 254 | 255 | func (builder *GetUsersSimplifiedByDepartmentIdReqBuilder) Fetch(fetch bool) *GetUsersSimplifiedByDepartmentIdReqBuilder { 256 | if fetch { 257 | builder.req.QueryParams.Set("fetch_child", "1") 258 | } 259 | return builder 260 | } 261 | 262 | func (builder *GetUsersSimplifiedByDepartmentIdReqBuilder) Build() *GetUsersSimplifiedByDepartmentIdReq { 263 | req := &GetUsersSimplifiedByDepartmentIdReq{} 264 | req.req = builder.req 265 | return req 266 | } 267 | 268 | func (u *user) GetUsersSimplifiedByDepartmentId(req *GetUsersSimplifiedByDepartmentIdReq) ([]*UserEntrySimplified, error) { 269 | token, err := req.req.Client.getAccessTokenFromCache() 270 | if err != nil { 271 | return nil, err 272 | } 273 | id := req.req.QueryParams.Get("department_id") 274 | if id == "" { 275 | return nil, errors.New("部门ID不能为空") 276 | } 277 | params := url.Values{} 278 | params.Add("access_token", token) 279 | params.Add("department_id", id) 280 | fetch := req.req.QueryParams.Get("fetch_child") 281 | if fetch == "1" { 282 | params.Add("fetch_child", fetch) 283 | } 284 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getDepartmentSimpleUserUrl, params.Encode()), nil) 285 | if err != nil { 286 | return nil, err 287 | } 288 | response, err := u.client.http.Do(request) 289 | if err != nil { 290 | return nil, err 291 | } 292 | if response.StatusCode != 200 { 293 | return nil, errors.New(response.Status) 294 | } 295 | body, err := ghttp.GetResponseBody(response.Body) 296 | if err != nil { 297 | return nil, err 298 | } 299 | var res struct { 300 | ErrCode int `json:"errcode"` 301 | ErrMsg string `json:"errmsg"` 302 | UserList []*UserEntrySimplified `json:"userlist"` 303 | } 304 | err = json.Unmarshal(body, &res) 305 | if err != nil { 306 | return nil, err 307 | } 308 | if res.ErrCode != 0 { 309 | return res.UserList, fmt.Errorf("from server - " + res.ErrMsg) 310 | } 311 | return res.UserList, nil 312 | } 313 | -------------------------------------------------------------------------------- /plugin/wechat/wechat.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/fasnow/ghttp" 9 | "idebug/utils" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | ) 14 | 15 | type apiConfig struct { 16 | // 获取access_token ?corpid=&corpsecret= 17 | getAccessTokenUrl string 18 | 19 | // 获取access_token权限分配 ?access_token= 20 | getAccessTokenDetailUrl string 21 | 22 | // 递归获取部门详情 ?access_token=ACCESS_TOKEN&id= 23 | getDepartmentListUrl string 24 | 25 | // 根据部门ID递归获取所有部门ID ?access_token=ACCESS_TOKEN&id=ID 26 | getDepartmentIdListUrl string 27 | 28 | // 获取单个部门详情 ?access_token=ACCESS_TOKEN&id=ID 29 | getDepartmentUrl string 30 | 31 | // 获取成员ID列表 获取企业成员的open_userid与对应的部门ID列表 ?access_token=ACCESS_TOKEN 32 | getUserIdListUrl string 33 | 34 | // 获取部门成员 ?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID 35 | getDepartmentSimpleUserUrl string 36 | 37 | // 获取部门成员详情 ?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID 38 | getDepartmentUserUrl string 39 | 40 | // 创建成员 ?access_token=ACCESS_TOKEN 41 | createUserUrl string 42 | 43 | // 读取成员 ?access_token=ACCESS_TOKEN&userid=USERID 44 | getUserUrl string 45 | 46 | // 更新成员 ?access_token=ACCESS_TOKEN 47 | updateSpecifiedUserUrl string 48 | 49 | // 删除成员 ?access_token=ACCESS_TOKEN&userid=USERID 50 | deleteSpecifiedUserUrl string 51 | 52 | // 手机号获取userid ?access_token=ACCESS_TOKEN 53 | getUserIDByPhone string 54 | 55 | // 邮箱获取userid ?access_token=ACCESS_TOKEN 56 | getUserIDByEmail string 57 | 58 | // 获取企业微信接口IP段 ?access_token=ACCESS_TOKEN 59 | getAPIDomainCIDRUrl string 60 | } 61 | 62 | var ( 63 | baseUrl = "https://qyapi.weixin.qq.com" 64 | api = initApi(baseUrl) 65 | ) 66 | 67 | func initApi(baseDomain string) apiConfig { 68 | return apiConfig{ 69 | getAccessTokenUrl: baseDomain + "/cgi-bin/gettoken", 70 | getAccessTokenDetailUrl: "https://open.work.weixin.qq.com/devtool/getInfoByAccessToken", 71 | getDepartmentListUrl: baseDomain + "/cgi-bin/department/list", 72 | getDepartmentIdListUrl: baseDomain + "/cgi-bin/department/simplelist", 73 | getDepartmentUrl: baseDomain + "/cgi-bin/department/get", 74 | getUserIdListUrl: baseDomain + "/cgi-bin/user/list_id", 75 | getDepartmentSimpleUserUrl: baseDomain + "/cgi-bin/user/simplelist", 76 | getDepartmentUserUrl: baseDomain + "/cgi-bin/user/list", 77 | createUserUrl: baseDomain + "/cgi-bin/user/create", 78 | getUserUrl: baseDomain + "/cgi-bin/user/get", 79 | updateSpecifiedUserUrl: baseDomain + "/cgi-bin/user/update", 80 | deleteSpecifiedUserUrl: baseDomain + "/cgi-bin/user/delete", 81 | getUserIDByPhone: baseDomain + "/cgi-bin/user/getuserid", 82 | getUserIDByEmail: baseDomain + "/cgi-bin/user/get_userid_by_email", 83 | getAPIDomainCIDRUrl: baseDomain + "/cgi-bin/get_api_domain_ip", 84 | } 85 | } 86 | 87 | var roleGroup = map[int]string{ 88 | -1: "未知管理组", 89 | 1: "应用", 90 | 4: "第三方服务商", 91 | 8: "通讯录管理助手", 92 | 64: "分级管理组", 93 | } 94 | 95 | func SetBaseDomain(domain string) { 96 | baseUrl = domain 97 | api = initApi(baseUrl) 98 | } 99 | 100 | func GetBaseDomain() string { 101 | return baseUrl 102 | } 103 | 104 | type AccessTokenAuthItem struct { 105 | AuthApps []struct { 106 | AppName string `json:"appname"` 107 | AppOpenid int `json:"appopenid"` 108 | ReliableDomain string `json:"reliabledomain"` 109 | } `json:"authapps"` 110 | AuthUsers []struct { 111 | AcctId string `json:"acctid"` 112 | } `json:"authusers"` 113 | AuthTags []struct { 114 | TagName string `json:"tagname"` 115 | TagOpenid int `json:"tagopenid"` 116 | } `json:"authtags"` 117 | AuthParties []struct { 118 | PartyName string `json:"partyname"` 119 | PartyOpenid string `json:"partyopenid"` 120 | } `json:"authparties"` 121 | } 122 | 123 | type AccessTokenAuthScope struct { 124 | RoleGroup string `json:"rolegroup"` // 应用类型 125 | RoleName string `json:"rolename"` // 应用名称 126 | item *AccessTokenAuthItem 127 | } 128 | type config struct { 129 | CorpId *string 130 | CorpSecret *string 131 | AccessToken *string 132 | ExpireIn *int 133 | //authScope *AccessTokenAuthScope 134 | } 135 | 136 | type department struct { 137 | client *Client 138 | } 139 | 140 | type user struct { 141 | client *Client 142 | } 143 | 144 | type Client struct { 145 | config *config 146 | Department *department 147 | User *user 148 | cache *utils.Cache // 保存access_token 149 | http *ghttp.Client 150 | } 151 | 152 | func NewWxClient() *Client { 153 | client := &Client{ 154 | config: &config{}, 155 | cache: utils.NewCache(3 * time.Second), 156 | http: &ghttp.Client{}, 157 | User: &user{}, 158 | Department: &department{}, 159 | } 160 | client.User.client = client 161 | client.Department.client = client 162 | return client 163 | } 164 | 165 | func (client *Client) SetContext(ctx *context.Context) { 166 | client.http.Context = ctx 167 | } 168 | 169 | func (client *Client) StopWhenContextCanceled(enable bool) { 170 | client.http.StopWhenContextCanceled = enable 171 | } 172 | 173 | func (client *Client) Set(corpId, corpSecret string) { 174 | conf := &config{ 175 | CorpId: &corpId, 176 | CorpSecret: &corpSecret, 177 | } 178 | client.config = conf 179 | client.cache = utils.NewCache(3 * time.Second) 180 | } 181 | 182 | func (client *Client) SetCorpId(corpId string) { 183 | conf := &config{ 184 | CorpId: &corpId, 185 | CorpSecret: client.config.CorpSecret, 186 | } 187 | client.config = conf 188 | client.cache = utils.NewCache(3 * time.Second) 189 | } 190 | 191 | func (client *Client) SetAccessToken(token string) { 192 | client.config = &config{AccessToken: &token} 193 | } 194 | 195 | func (client *Client) SetCorpSecret(corpSecret string) { 196 | conf := &config{ 197 | CorpId: client.config.CorpId, 198 | CorpSecret: &corpSecret, 199 | } 200 | client.config = conf 201 | client.cache = utils.NewCache(3 * time.Second) 202 | } 203 | 204 | func (client *Client) GetAccessToken() (string, error) { 205 | token, err := client.getAccessTokenFromCache() 206 | if err != nil { 207 | return "", err 208 | } 209 | return token, nil 210 | } 211 | 212 | func (client *Client) GetAccessTokenFromCache() string { 213 | if client.config.AccessToken == nil || *client.config.AccessToken == "" { 214 | return "" 215 | } 216 | return *client.config.AccessToken 217 | } 218 | 219 | func (client *Client) GetAccessTokenFromServer() (string, error) { 220 | token, expire, err := client.getAccessToken() 221 | if err != nil { 222 | return "", err 223 | } 224 | client.cache.Set("accessToken", token, time.Duration(expire)*time.Second) 225 | client.config.AccessToken = &token 226 | client.config.ExpireIn = &expire 227 | return token, nil 228 | } 229 | 230 | func (client *Client) getAccessTokenFromCache() (string, error) { 231 | value, ok := client.cache.Get("accessToken") 232 | if !ok { 233 | token, expire, err := client.getAccessToken() 234 | if err != nil { 235 | return "", err 236 | } 237 | client.cache.Set("accessToken", token, time.Duration(expire)*time.Second) 238 | client.config.AccessToken = &token 239 | client.config.ExpireIn = &expire 240 | return token, nil 241 | } else { 242 | if token, ok := value.(string); ok { 243 | return token, nil 244 | } 245 | } 246 | return "", errors.New("获取access_token时出错") 247 | } 248 | 249 | func (client *Client) getAccessToken() (string, int, error) { 250 | params := url.Values{} 251 | params.Add("corpid", *client.config.CorpId) 252 | params.Add("corpsecret", *client.config.CorpSecret) 253 | request, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", api.getAccessTokenUrl, params.Encode()), nil) 254 | if err != nil { 255 | return "", 0, err 256 | } 257 | request.Header.Set("User-Agent", "") 258 | response, err := client.http.Do(request) 259 | if response.StatusCode != 200 { 260 | return "", 0, errors.New(response.Status) 261 | } 262 | body, err := ghttp.GetResponseBody(response.Body) 263 | if err != nil { 264 | return "", 0, err 265 | } 266 | var res struct { 267 | ErrCode *int `json:"errcode"` 268 | ErrMsg *string `json:"errmsg"` 269 | AccessToken *string `json:"access_token"` 270 | ExpiresIn *int `json:"expires_in"` 271 | } 272 | err = json.Unmarshal(body, &res) 273 | if err != nil { 274 | return "", 0, errors.New("获取 access_token 时出错") 275 | } 276 | if *res.ErrCode != 0 { 277 | return "", 0, fmt.Errorf("from server - " + *res.ErrMsg) 278 | } 279 | return *res.AccessToken, *res.ExpiresIn, nil 280 | } 281 | 282 | func (client *Client) GetConfig() *config { 283 | return client.config 284 | } 285 | -------------------------------------------------------------------------------- /prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fasnow/readline" 6 | "github.com/spf13/cobra" 7 | "idebug/cmd" 8 | "idebug/logger" 9 | "idebug/utils" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | ) 15 | 16 | var globalCmd = []string{"clear", "cls", "use", "update", "exit"} 17 | 18 | type Client struct { 19 | module *cmd.Module 20 | proxy *string 21 | //ppt *prompt.Prompt 22 | } 23 | 24 | func (client *Client) Run() { 25 | client.proxy = new(string) 26 | client.module = (*cmd.Module)(new(string)) 27 | *client.proxy = "" 28 | cmd.Proxy = client.proxy 29 | *client.module = cmd.NoModule 30 | cmd.CurrentModule = client.module 31 | line, err := readline.NewEx(&readline.Config{}) 32 | if err != nil { 33 | logger.Error(logger.FormatError(err)) 34 | return 35 | } 36 | line.Config.Stdout = logger.Writer 37 | line.HistoryEnable() 38 | go client.ctrlCListener() 39 | for { 40 | line.SetPrompt(logger.ModuleSelectedV2(string(*client.module))) 41 | input, err := line.Readline() 42 | if err != nil { 43 | if err.Error() != "Interrupt" { 44 | logger.Error(logger.FormatError(err)) 45 | } 46 | continue 47 | } 48 | client.executor(input) 49 | } 50 | } 51 | 52 | func (client *Client) executor(in string) { 53 | cmd.SetContext() 54 | go func() { 55 | client.exec(in) 56 | cmd.Cancel() 57 | }() 58 | for { 59 | select { 60 | case <-cmd.Context.Done(): 61 | return 62 | } 63 | } 64 | } 65 | 66 | func (client *Client) exec(in string) { 67 | var moduleMap = map[cmd.Module]*cobra.Command{ 68 | cmd.WxModule: cmd.NewWechatCli().Root, 69 | cmd.FeiShuModule: cmd.NewFeiShuCli().Root, 70 | cmd.NoModule: cmd.NewMainCli().Root, 71 | } 72 | in = strings.TrimSpace(in) 73 | args := strings.Fields(in) 74 | if len(args) == 0 { 75 | return 76 | } 77 | var cmdFunc *cobra.Command 78 | var ok bool 79 | if utils.StringInList(args[0], globalCmd) { 80 | cmdFunc = moduleMap[cmd.NoModule] 81 | } else { 82 | if cmdFunc, ok = moduleMap[*client.module]; !ok { 83 | logger.Error(logger.FormatError(fmt.Errorf("模块错误"))) 84 | return 85 | } 86 | } 87 | cmdFunc.SetArgs(args) 88 | if err := cmdFunc.Execute(); err != nil { 89 | logger.Error(err) 90 | } 91 | return 92 | } 93 | 94 | func (client *Client) ctrlCListener() { 95 | interrupt := make(chan os.Signal, 1) 96 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 97 | go func() { 98 | for { 99 | <-interrupt // 接收ctrl+c中断信号 100 | cmd.Cancel() // 取消http上下文 101 | if !cmd.HttpCanceled { 102 | cmd.HttpCanceled = true 103 | } 104 | } 105 | }() 106 | select {} 107 | } 108 | -------------------------------------------------------------------------------- /utils/cache.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Cache struct { 9 | data map[string]interface{} 10 | expire map[string]time.Time 11 | mutex sync.RWMutex 12 | interval time.Duration 13 | } 14 | 15 | func NewCache(interval time.Duration) *Cache { 16 | c := &Cache{ 17 | data: make(map[string]interface{}), 18 | expire: make(map[string]time.Time), 19 | interval: interval, 20 | } 21 | go c.startCleanup() 22 | return c 23 | } 24 | 25 | func (c *Cache) Set(key string, value interface{}, expire time.Duration) { 26 | c.mutex.Lock() 27 | defer c.mutex.Unlock() 28 | 29 | c.data[key] = value 30 | c.expire[key] = time.Now().Add(expire) 31 | } 32 | 33 | func (c *Cache) Get(key string) (interface{}, bool) { 34 | c.mutex.RLock() 35 | defer c.mutex.RUnlock() 36 | 37 | value, ok := c.data[key] 38 | return value, ok 39 | } 40 | 41 | func (c *Cache) startCleanup() { 42 | ticker := time.NewTicker(c.interval) 43 | for range ticker.C { 44 | c.mutex.Lock() 45 | for key, expireTime := range c.expire { 46 | if time.Now().After(expireTime) { 47 | delete(c.data, key) 48 | delete(c.expire, key) 49 | } 50 | } 51 | c.mutex.Unlock() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // IsFileExists 判断文件是否存在 12 | func IsFileExists(filename string) bool { 13 | _, err := os.Stat(filename) 14 | return err == nil 15 | } 16 | 17 | func ConvertToReader(data any) io.Reader { 18 | marshal, err := json.Marshal(data) 19 | if err != nil { 20 | return nil 21 | } 22 | return strings.NewReader(string(marshal)) 23 | } 24 | 25 | func ConvertString2Reader(data string) io.Reader { 26 | return strings.NewReader(data) 27 | } 28 | 29 | func StringInList(target string, strList []string) bool { 30 | sort.Strings(strList) 31 | index := sort.SearchStrings(strList, target) 32 | //index的取值:[0,len(str_array)] 33 | if index < len(strList) && strList[index] == target { //需要注意此处的判断,先判断 &&左侧的条件,如果不满足则结束此处判断,不会再进行右侧的判断 34 | return true 35 | } 36 | return false 37 | } 38 | --------------------------------------------------------------------------------