Stories

Detail Return Return

釘釘企業內部應用同步部門用户 - Stories Detail

前言

有了上一篇釘釘企業內部應用SSO單點登錄實戰及踩坑過程之後,再去看釘釘的文檔和接口就顯得輕車熟路了。

明確需求:定時把釘釘的用户同步到企業自己開發的系統中,以便讓企業內的員工都能使用SSO單點登錄。

確定接口

借鑑上一篇的經驗,我們優先選擇V2版本的接口。

圖片.png

釘釘開放平台-用户信息

右側提供了返回值的實體,注意到包括userId、姓名、電話這三個關鍵字段存在,説明該接口滿足需求。

把鼠標放到token上面,可以看到該token來源於:

圖片.png

釘釘開放平台-token

這樣,兩個接口就確定了。

前置設置

參考上一篇文章,需要以下信息:

  • 我們需要記下cropId、ClientId、Client Secret
  • 後端會定時獲取access_token
  • 每次登錄會實時生成一次性code
  • 其他信息都不再需要了

開通以下權限(圖中的權限都需要):

圖片.png

特別注意獲取手機號權限,如無此權限,接口仍可以正常返回信息,但不包含手機號。

安全設置中添加ip白名單:

圖片.png

同步功能只需要後端,並且釘釘接口的部分並不算難,主要解決一些細節問題。

定時器

同步通常由定時器進行觸發,例如這樣:

@Component
public class DingdingUserSyncSchedule {

    @Scheduled(cron = "0 10 02 * * *")
    public void sync() {
        System.out.println("同步定時器被觸發");
    }
}

其中,@Component使得此類作為組件被IoC加載,@Scheduled使得此方法會定時執行。

@Scheduled括號中的內容是cron表達式:

{秒} {分} {時} {日} {月} {星期} {年份(可選)}

其中, * 表示通配符,即該位置無論是任何值都會執行。

所以測試時可以使用@Scheduled(cron = "0 * * * * *")表示每分鐘0秒觸發一次,生產環境可以使用@Scheduled(cron = "0 10 02 * * *")表示每天凌晨2:10觸發一次。

啓動後就可以看到打印日誌了:

圖片.png

釘釘接口遞歸

由於是分頁獲取,一次只能獲取一部分,所以要遞歸調用接口,拼接list,這樣系統內的方法就能一次返回所有用户。

    public List<OapiV2UserListResponse.ListUserResponse> getDingdingUserList(Long deptId, Long cursor) {
        try {
            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/list");
            OapiV2UserListRequest req = new OapiV2UserListRequest();
            req.setDeptId(deptId);
            req.setCursor(cursor);
            req.setSize(10L);
            req.setOrderField("modify_desc");
            req.setContainAccessLimit(false);
            req.setLanguage("zh_CN");
            OapiV2UserListResponse rsp = client.execute(req, getAccessToken());
            System.out.println(rsp.getResult().getHasMore());
            System.out.println(rsp.getResult().getList());
            if (!rsp.isSuccess()) {
                this.logger.error("Failed to get user list: {}", rsp.getErrmsg());
                return null;
            }
            List<OapiV2UserListResponse.ListUserResponse> list = rsp.getResult().getList();
            // 如果還有,就遞歸
            if (rsp.getResult().getHasMore()) {
                list.addAll(getDingdingUserList(deptId, rsp.getResult().getNextCursor()));
                return list;
            }
            // 沒有了就返回list
            return list;
        } catch (ApiException e) {
            e.printStackTrace();
        }
        return null;
    }

這樣一次拿到了所有用户。

此外,還有accesstoken的獲取,見上一篇文章,一模一樣。

此處貼上代碼:

    @Value("${app.dingDing.cropId}")
    private String corpId;

    @Value("${app.dingDing.ClientId}")
    private String clientId;

    @Value("${app.dingDing.ClientSecret}")
    private String clientSecret;

    // 緩存的 token 值
    private String cachedToken;
    // 過期時間戳(毫秒)
    private long expireAt = 0;

    /**
     * getAccessToken
     */
    public String getAccessToken() {
        // 判斷緩存是否還有效(預留200秒作為刷新緩衝)
        if (cachedToken != null && (expireAt - 200_000) > System.currentTimeMillis()) {
            this.logger.info("accessToken: {}", cachedToken);
            return cachedToken;
        }
        try {
            // 獲取client、構造請求
            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
            OapiGettokenRequest req = new OapiGettokenRequest();
            req.setAppkey(clientId);
            req.setAppsecret(clientSecret);
            req.setHttpMethod("GET");
            // 發送請求獲取access_token
            String accessToken = client.execute(req).getAccessToken();
            this.logger.info("accessToken: {}", accessToken);
            // 緩存token,設置有效期7200秒
            this.cachedToken = accessToken;
            this.expireAt = System.currentTimeMillis() + 7200_000;
        } catch (ApiException err) {
            this.logger.error("獲取accessToken失敗:{}", err.getErrMsg());
            err.printStackTrace();
        }
        return cachedToken;
    }

同步用户

這步就是把釘釘用户同步到系統中,這沒什麼難的,由於不同系統的表結構不同,僅供參考:

/**
     * 根據返回值更新用户信息
     * @param list
     */
    public void updateDingdingUserList(List<OapiV2UserListResponse.ListUserResponse> list) {
        for (OapiV2UserListResponse.ListUserResponse userItem : list) {
            this.logger.info("開始更新釘釘用户: {}{}", userItem.getUserid(), userItem.getName());
            // 查當前系統的釘釘用户
            DingdingUser dingdingUser = dingdingUserRepository.findByUserId(userItem.getUserid()).orElse(null);
            // 如果查不到,就新建
            if (dingdingUser == null) {
                this.logger.info("釘釘用户{}{}不存在,新建", userItem.getUserid(), userItem.getName());
                dingdingUser = new DingdingUser();
                dingdingUser.setUserId(userItem.getUserid());
                dingdingUser.setName(userItem.getName());
                dingdingUserRepository.save(dingdingUser);
            }
            // 查系統用户
            User user = userRepository.findByDingdingUser(dingdingUser).orElse(null);
            if (user == null) {
                this.logger.info("系統用户{}{}不存在,新建", userItem.getUserid(), userItem.getName());
                // 查不到,就新建
                user = new User();
                user.setDingdingUser(dingdingUser);
                user.setName(userItem.getName());
                user.setPhone(userItem.getMobile());
                user.setPassword(this.systemConfigService.getByDefaultPassword().getValue());
                userRepository.save(user);
            } else {
                this.logger.info("系統用户{}{}存在,id為{}", userItem.getUserid(), userItem.getName(), user.getId());
                // 查到了,分別判斷關聯的用户、姓名、電話,為空則更新,否則不動
                if (user.getDingdingUser() == null) {
                    user.setDingdingUser(dingdingUser);
                }
                if (user.getName() == null) {
                    user.setName(userItem.getName());
                }
                if (user.getPhone() == null) {
                    user.setPhone(userItem.getMobile());
                }
                userRepository.save(user);
            }
            this.logger.info("釘釘用户: {}{}同步完成", userItem.getUserid(), userItem.getName());
        }
    }

關鍵在於——打日誌,如果效果和預期不符時,方便調試。

在釘釘的Service寫一個同步入口方法

同步入口方法,後續如果需要邏輯變更時更靈活。

    @Override
    public void syncDingdingUser() {
        // 加一個開關,默認不啓用同步,避免頻繁請求釘釘超過API調用次數限制
        if (!Objects.equals(this.enableSync, "false")) {
            return;
        }
        Long deptId = 1L;
        Long cursor = 0L;
        // 調用用户列表接口
        List<OapiV2UserListResponse.ListUserResponse> listUserResponse = getDingdingUserList(deptId, cursor);
        // 調用系統內方法更新信息
        updateDingdingUserList(listUserResponse);
    }

定時器觸發時調用入口方法

@Component
public class DingdingUserSyncSchedule {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final DingTalkService dingTalkService;

    DingdingUserSyncSchedule(DingTalkService dingTalkService) {
        this.dingTalkService = dingTalkService;
    }

    @Scheduled(cron = "0 * * * * *")
    public void sync() {
        this.logger.info("DingdingUserSyncSchedule 開始執行釘釘用户同步");
        this.dingTalkService.syncDingdingUser();
        this.logger.info("DingdingUserSyncSchedule 釘釘用户同步結束");
    }
}

啓動,觀察日誌

大量同步日誌打出,且沒有報錯,説明成功了。

圖片.png

後記

當前系統的業務沒有細分部門,如果需要考慮部門的話,應該增加以下接口:

圖片.png

獲取部門列表

開通以下權限:

圖片.png

然後從根部門的id = 1逐級遞歸,就能獲取所有部門,遍歷部門來分別獲取用户,最後同步即可。

user avatar mannayang Avatar jianxiangjie3rkv9 Avatar xiaoniuhululu Avatar sofastack Avatar u_16502039 Avatar debuginn Avatar u_16769727 Avatar lenglingx Avatar u_11365552 Avatar jiangyi Avatar kohler21 Avatar ahahan Avatar
Favorites 108 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.