【Java 实现】用友 BIP V5 版本与飞书集成单点登录(飞书免密登录到用友 ERP)

前言

文本基于用友 BIP V5 版本,其它版本展现的功能、接口未必于此一样。

代码是此前写的了,现在分享一下

前置配置

飞书配置

简答略过:

  1. 飞书开放平台 创建自建应用

默认你已经懂得创建企业自建应用并获取到appId、appSecret,后续 yml 配置中需要用到

用友配置

1、左上角四片花瓣点击 --> 搜索 "集成认证中心"

然后新建一个,选择飞书,然后得到一个 集成认证编码,后续用来填写到yml配置文件中

2、新建一个 API 访问令牌,继续四个花瓣搜索 API,选择API调用

然后赋予以下 API 权限


代码

yml 配置文件

yml 复制代码
# 用友ERP对接配置(dev 环境)
yonbip:
  # 用友ERP基础URL
  base-url: http://bipdev.xxx.com.cn
  # 用友ERP应用凭证(API 调用处创建的)
  app:
    key: 78272d5c82884987b89fdc319b8
    secret: 95c549744f9394368ba9533af72409
  # 用友ERP第三方集成认证中心编码
  integration-auth-code: s5j0j94r # 填写上面创建的
  # 用友ERP单点登录配置
  sso:
    # 飞书自建应用应用凭证(用于SSO)
    feishu-app-id: cli_a9b3e.......
    feishu-app-secret: RsU2DGK0V.....
    # 用友ERP SSO登录路径
    sso-suffix-path: /cas/thirdOauth2CodeLogin
    # 单点登录URL(可选)
    sso-url: ${yonbip.base-url}${yonbip.sso.sso-suffix-path}

Java 代码

配置类用于从 yml 读取配置

java 复制代码
@Data
@Configuration
@ConfigurationProperties(prefix = "yonbip.sso")
public class YonBipSsoConfig {

    /**
     * 飞书应用 AppId(用于获取飞书用户信息)
     */
    private String feishuAppId;

    /**
     * 飞书应用 Secret(用于获取飞书用户信息)
     */
    private String feishuAppSecret;

    /**
     * 用友 ERP SSO 登录路径后缀(如:/cas/thirdOauth2CodeLogin)
     */
    private String ssoSuffixPath;

    /**
     * 用友 ERP SSO 登录完整地址(可在 yml 中通过占位符拼接生成)
     */
    private String ssoUrl;
}

我用 Interface (自带 final static)存储常量

java 复制代码
public interface YonBipSsoConst {

    /**
     * 用友ERP获取第三方登录临时code的接口路径
     */
    String GET_THIRD_LOGIN_CODE_PATH = "/iuap-api-gateway/yonbip/yht/getThirdLoginCode";

    /**
     * 授权类型:授权码模式
     */
    String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";

    /**
     * 成功响应码
     */
    Integer SUCCESS_CODE = 200;

    /**
     * Session中存储飞书用户ID的key
     */
    String SESSION_FEISHU_USER_ID_KEY = "yonbip:sso:feishu:userId";

    /**
     * Session中存储state参数的key
     */
    String SESSION_STATE_KEY = "yonbip:sso:state";
}

Controller 接口

java 复制代码
@Slf4j
@RestController
@RequestMapping("/yonbip/sso")
@Tag(name = "用友ERP单点登录")
public class YonBipSsoController {

    @Resource
    private YonBipSsoService yonBipSsoService;

    /**
     * 飞书授权回调接口,处理单点登录到用友ERP
     * <p>
     * 使用说明:
     * 1. 在飞书开放平台配置此接口为OAuth回调地址
     * 2. 用户在飞书中打开应用后,飞书会携带code参数回调此接口
     * 3. 本接口获取飞书用户信息后,自动跳转到用友ERP系统
     * <p>
     * 参数说明:
     * - code: 飞书平台返回的授权码,5分钟有效,每个code只能使用一次
     * - state: 可选参数,Base64编码的目标页面URL,登录成功后跳转到该页面
     * 如果不传,则跳转到配置文件中的defaultLoginUrl
     *
     * @param code  飞书授权码
     * @param state Base64编码的目标URL(可选)
     */
    @NoNeedLogin
    @GetMapping("/pc-login")
    @Operation(summary = "飞书授权回调-单点登录到用友ERP")
    public void feiShuCallback(
            @RequestParam("code") String code,
            @RequestParam(value = "state", required = false) String state) {

        log.info("[用友SSO Controller]接收飞书回调请求 - code: {}, state: {}", code, state);

        // 调用Service处理单点登录逻辑
        yonBipSsoService.handleFeiShuSsoToYonBip(code, state);
    }

    /**
     * 测试接口:检查配置是否正确
     *
     * @return 配置信息摘要
     */
    @GetMapping("/config-check")
    @Operation(summary = "检查SSO配置")
    public String checkConfig() {
        return "用友ERP单点登录配置检查接口,请查看日志输出";
    }
}

HTTP声明接口(使用 Forest:官网):

请求飞书有关的:

java 复制代码
/**
 * 飞书SSO认证HTTP客户端
 * 使用Forest框架调用飞书开放平台API
 *
 * @author lyk
 * @date 2025-12-22
 */
public interface FeiShuSsoHttpClient {

    /**
     * 获取飞书tenant_access_token
     * 文档: https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal
     *
     * @param requestBody 请求体,包含app_id和app_secret
     * @return 飞书响应
     */
    @Post(url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
            contentType = "application/json")
    ForestResponse<String> getTenantAccessToken(@Body Map<String, String> requestBody);

    /**
     * 获取飞书user_access_token
     * 文档: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/oidc-access_token/create
     *
     * @param authorization Authorization header,格式: "Bearer {tenant_access_token}"
     * @param requestBody   请求体,包含grant_type和code
     * @return 飞书响应
     */
    @Post(url = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token",
            contentType = "application/json")
    ForestResponse<String> getUserAccessToken(
            @Header("Authorization") String authorization,
            @Body Map<String, String> requestBody);

    /**
     * 获取飞书用户信息
     * 文档: https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get
     *
     * @param authorization Authorization header,格式: "Bearer {user_access_token}"
     * @return 飞书响应
     */
    @Get(url = "https://open.feishu.cn/open-apis/authen/v1/user_info")
    ForestResponse<String> getUserInfo(@Header("Authorization") String authorization);
}

请求用友有关的:

java 复制代码
public interface YonBipSsoHttpClient {

    /**
     * 获取用友ERP第三方登录临时code
     * 调用用友接口获取一次性登录凭证
     *
     * @param accessToken 用友ERP的access_token
     * @param requestBody 请求体,包含thirdUcId和userId
     * @return 用友ERP响应
     */
    @Post(url = "${yonbipBaseUrl}" + "/iuap-api-gateway/yonbip/yht/getThirdLoginCode",
            contentType = "application/json")
    ForestResponse<String> getThirdLoginCode(
            @Query(YonBipConst.ACCESS_TOKEN_KEY) String accessToken,
            @Body Map<String, String> requestBody);
}

调用飞书相关接口的逻辑:

java 复制代码
@Slf4j
@Service
public class FeiShuSsoAuthService {

    @Resource
    private YonBipSsoConfig yonBipSsoConfig;

    @Resource
    private FeiShuSsoHttpClient feiShuSsoHttpClient;

    /**
     * 获取飞书tenant_access_token
     * 文档: https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal
     *
     * @return tenant_access_token
     */
    public String getTenantAccessToken() {
        try {
            log.info("[飞书SSO认证]开始获取tenant_access_token");

            Map<String, String> requestBody = new HashMap<>();
            requestBody.put("app_id", yonBipSsoConfig.getFeishuAppId());
            requestBody.put("app_secret", yonBipSsoConfig.getFeishuAppSecret());

            ForestResponse<String> response = feiShuSsoHttpClient.getTenantAccessToken(requestBody);

            if (!response.isSuccess() || StringUtils.isBlank(response.getResult())) {
                throw new BusinessException("[飞书SSO认证]获取tenant_access_token失败,HTTP状态码: " + response.getStatusCode());
            }

            JSONObject jsonResult = JSON.parseObject(response.getResult());
            if (jsonResult == null || jsonResult.getInteger("code") != 0) {
                String errMsg = jsonResult != null ? jsonResult.getString("msg") : "响应为空";
                throw new BusinessException("[飞书SSO认证]获取tenant_access_token失败: " + errMsg);
            }

            String tenantAccessToken = jsonResult.getString("tenant_access_token");
            if (StringUtils.isBlank(tenantAccessToken)) {
                throw new BusinessException("[飞书SSO认证]tenant_access_token为空");
            }

            log.info("[飞书SSO认证]成功获取tenant_access_token");
            return tenantAccessToken;

        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            log.error("[飞书SSO认证]获取tenant_access_token异常", ex);
            throw new BusinessException("[飞书SSO认证]获取tenant_access_token异常: " + ex.getMessage());
        }
    }

    /**
     * 使用飞书授权码获取user_access_token
     * 文档: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/oidc-access_token/create
     *
     * @param code              飞书授权码
     * @param tenantAccessToken tenant_access_token
     * @return user_access_token
     */
    public String getUserAccessToken(String code, String tenantAccessToken) {
        try {
            log.info("[飞书SSO认证]开始获取user_access_token, code={}", code);

            Map<String, String> requestBody = new HashMap<>();
            requestBody.put("grant_type", "authorization_code");
            requestBody.put("code", code);

            String authorization = "Bearer " + tenantAccessToken;
            ForestResponse<String> response = feiShuSsoHttpClient.getUserAccessToken(authorization, requestBody);
            String result = response.getResult();

            if (!response.isSuccess() || StringUtils.isBlank(response.getResult())) {
                throw new BusinessException("[飞书SSO认证]获取user_access_token失败,HTTP状态码: " + response.getStatusCode());
            }

            JSONObject jsonResult = JSON.parseObject(response.getResult());
            if (jsonResult == null || jsonResult.getInteger("code") != 0) {
                String errMsg = jsonResult != null ? jsonResult.getString("msg") : "响应为空";
                throw new BusinessException("[飞书SSO认证]获取user_access_token失败: " + errMsg);
            }

            JSONObject dataObj = jsonResult.getJSONObject("data");
            if (dataObj == null) {
                throw new BusinessException("[飞书SSO认证]user_access_token数据为空");
            }

            String userAccessToken = dataObj.getString("access_token");
            if (StringUtils.isBlank(userAccessToken)) {
                throw new BusinessException("[飞书SSO认证]user_access_token为空");
            }

            log.info("[飞书SSO认证]成功获取user_access_token");
            return userAccessToken;

        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            log.error("[飞书SSO认证]获取user_access_token异常", ex);
            throw new BusinessException("[飞书SSO认证]获取user_access_token异常: " + ex.getMessage());
        }
    }

    /**
     * 根据user_access_token获取飞书用户ID
     * 文档: https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get
     *
     * @param userAccessToken user_access_token
     * @return 飞书用户ID
     */
    public String getFeishuUserId(String userAccessToken) {
        try {
            log.info("[飞书SSO认证]开始获取飞书用户信息");

            String authorization = "Bearer " + userAccessToken;
            ForestResponse<String> response = feiShuSsoHttpClient.getUserInfo(authorization);

            String result = response.getResult();

            if (!response.isSuccess() || StringUtils.isBlank(response.getResult())) {
                throw new BusinessException("[飞书SSO认证]获取用户信息失败,HTTP状态码: " + response.getStatusCode());
            }

            JSONObject jsonResult = JSON.parseObject(response.getResult());
            if (jsonResult == null || jsonResult.getInteger("code") != 0) {
                String errMsg = jsonResult != null ? jsonResult.getString("msg") : "响应为空";
                throw new BusinessException("[飞书SSO认证]获取用户信息失败: " + errMsg);
            }

            JSONObject dataObj = jsonResult.getJSONObject("data");
            if (dataObj == null) {
                throw new BusinessException("[飞书SSO认证]用户信息数据为空");
            }

            String userId = dataObj.getString("user_id");
            if (StringUtils.isBlank(userId)) {
                throw new BusinessException("[飞书SSO认证]用户ID为空");
            }

            log.info("[飞书SSO认证]成功获取飞书用户ID: {}", userId);
            return userId;

        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            log.error("[飞书SSO认证]获取飞书用户信息异常", ex);
            throw new BusinessException("[飞书SSO认证]获取飞书用户信息异常: " + ex.getMessage());
        }
    }

    /**
     * 完整流程:通过飞书授权码获取飞书用户ID
     *
     * @param code 飞书授权码
     * @return 飞书用户ID
     */
    public String getFeishuUserIdByCode(String code) {
        // 1. 获取tenant_access_token
        String tenantAccessToken = getTenantAccessToken();

        // 2. 使用授权码获取user_access_token
        String userAccessToken = getUserAccessToken(code, tenantAccessToken);

        // 3. 使用user_access_token获取用户ID
        return getFeishuUserId(userAccessToken);
    }
}

处理 SSO 的Service:

java 复制代码
@Slf4j
@Service
public class YonBipSsoService {

    @Resource
    private YonBipConfig yonBipConfig;

    @Resource
    private FeiShuSsoAuthService feiShuSsoAuthService;

    @Resource
    private YonBipTokenService yonBipTokenService;

    @Resource
    private YonBipSsoHttpClient yonBipSsoHttpClient;

    /**
     * 处理飞书授权回调,并单点登录到用友ERP
     * <p>
     * 流程说明:
     * 1. 从飞书获取授权码code
     * 2. 使用code获取飞书用户信息(user_id)
     * 3. 获取用友ERP的access_token
     * 4. 调用用友接口获取第三方登录临时code
     * 5. 构建用友ERP登录URL并重定向
     *
     * @param code  飞书授权码
     * @param state Base64编码的目标页面URL(可选)
     */
    public void handleFeiShuSsoToYonBip(String code, String state) {
        log.info("[用友SSO]==================== 飞书授权后单点登录到用友ERP流程开始 ====================");
        log.info("[用友SSO]接收参数 - code: {}, state: {}", code, state);

        // 获取当前请求和响应对象
        HttpServletRequest request = ContextHolderUtils.getRequest();
        HttpServletResponse response = ContextHolderUtils.getResponse();

        // 参数校验
        if (StringUtils.isBlank(code)) {
            log.error("[用友SSO]飞书授权码code为空");
            writeErrorResponse(response, "飞书授权码code为空");
            return;
        }

        // 创建Session并保存state
        HttpSession session = request.getSession(true);
        if (StringUtils.isNotBlank(state)) {
            session.setAttribute(YonBipSsoConst.SESSION_STATE_KEY, state);
        }

        // 步骤1: 获取飞书用户ID
        String feishuUserId;
        try {
            feishuUserId = feiShuSsoAuthService.getFeishuUserIdByCode(code);
            log.info("[用友SSO]步骤1完成 - 成功获取飞书用户ID: {}", feishuUserId);
        } catch (Exception e) {
            log.error("[用友SSO]获取飞书用户ID失败", e);
            writeErrorResponse(response, "获取飞书用户信息失败: " + e.getMessage());
            return;
        }

        if (StringUtils.isBlank(feishuUserId)) {
            log.error("[用友SSO]飞书用户ID为空");
            writeErrorResponse(response, "获取飞书用户ID为空");
            return;
        }

        // 保存用户ID到Session
        session.setAttribute(YonBipSsoConst.SESSION_FEISHU_USER_ID_KEY, feishuUserId);

        // 步骤2: 获取用友ERP的access_token
        String yonBipAccessToken;
        try {
            yonBipAccessToken = yonBipTokenService.getAccessToken();
            log.info("[用友SSO]步骤2完成 - 成功获取用友ERP access_token");
        } catch (Exception e) {
            log.error("[用友SSO]获取用友ERP access_token失败", e);
            writeErrorResponse(response, "获取用友ERP access_token失败: " + e.getMessage());
            return;
        }

        if (StringUtils.isBlank(yonBipAccessToken)) {
            log.error("[用友SSO]用友ERP access_token为空");
            writeErrorResponse(response, "用友ERP access_token为空");
            return;
        }

        // 步骤3: 调用用友接口获取第三方登录临时code
        String yonBipLoginCode;
        try {
            yonBipLoginCode = getYonBipThirdLoginCode(yonBipAccessToken, feishuUserId);
            log.info("[用友SSO]步骤3完成 - 成功获取用友ERP一次性登录code");
        } catch (Exception e) {
            log.error("[用友SSO]获取用友ERP一次性登录code失败", e);
            writeErrorResponse(response, "获取用友ERP登录code失败: " + e.getMessage());
            return;
        }

        if (StringUtils.isBlank(yonBipLoginCode)) {
            log.error("[用友SSO]用友ERP登录code为空");
            writeErrorResponse(response, "用友ERP登录code为空");
            return;
        }

        // 步骤4: 构建用友ERP登录URL
        String yonBipLoginUrl;
        try {
            yonBipLoginUrl = buildYonBipLoginUrl(yonBipLoginCode, state);
            log.info("[用友SSO]步骤4完成 - 成功构建用友ERP登录URL: {}", yonBipLoginUrl);
        } catch (Exception e) {
            log.error("[用友SSO]构建用友ERP登录URL失败", e);
            writeErrorResponse(response, "构建用友ERP登录URL失败: " + e.getMessage());
            return;
        }

        // 步骤5: 重定向到用友ERP
        try {
            log.info("[用友SSO]步骤5 - 开始重定向到用友ERP");
            response.sendRedirect(yonBipLoginUrl);
            log.info("[用友SSO]==================== 飞书授权后单点登录到用友ERP流程结束 ====================");
        } catch (IOException e) {
            log.error("[用友SSO]重定向到用友ERP失败", e);
            writeErrorResponse(response, "重定向到用友ERP失败: " + e.getMessage());
        }
    }

    /**
     * 调用用友接口获取第三方登录临时code
     *
     * @param accessToken 用友ERP的access_token
     * @param userId      飞书用户ID
     * @return 用友ERP一次性登录code
     */
    private String getYonBipThirdLoginCode(String accessToken, String userId) {
        try {
            // 构建请求体
            Map<String, String> requestBody = new HashMap<>();
            requestBody.put("thirdUcId", yonBipConfig.getIntegrationAuthCode());
            requestBody.put("userId", userId);

            log.info("[用友SSO]调用用友接口获取登录code, 请求体: {}", JSON.toJSONString(requestBody));

            ForestResponse<String> response = yonBipSsoHttpClient.getThirdLoginCode(accessToken, requestBody);
            String result = response.getResult();

            if (!response.isSuccess() || StringUtils.isBlank(response.getResult())) {
                throw new BusinessException("[用友SSO]调用用友获取登录code接口失败,HTTP状态码: " + response.getStatusCode());
            }

            log.info("[用友SSO]用友接口返回: {}", response.getResult());

            JSONObject jsonResult = JSON.parseObject(response.getResult());
            if (jsonResult == null) {
                throw new BusinessException("[用友SSO]用友接口响应解析失败");
            }

            Integer code = jsonResult.getInteger("code");
            if (!YonBipSsoConst.SUCCESS_CODE.equals(code)) {
                String errMsg = jsonResult.getString("message");
                throw new BusinessException("[用友SSO]用友接口返回失败,code=" + code + ", message=" + errMsg);
            }

            JSONObject dataObj = jsonResult.getJSONObject("data");
            if (dataObj == null) {
                throw new BusinessException("[用友SSO]用友接口返回数据为空");
            }

            String loginCode = dataObj.getString("code");
            if (StringUtils.isBlank(loginCode)) {
                throw new BusinessException("[用友SSO]用友登录code为空");
            }

            return loginCode;

        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            log.error("[用友SSO]调用用友获取登录code接口异常", ex);
            throw new BusinessException("[用友SSO]调用用友获取登录code接口异常: " + ex.getMessage());
        }
    }

    /**
     * 构建用友ERP OAuth2登录URL
     *
     * @param yonBipLoginCode 用友一次性登录code
     * @param state           Base64编码的目标URL(可选)
     * @return 完整的用友ERP登录URL
     */
    private String buildYonBipLoginUrl(String yonBipLoginCode, String state) throws UnsupportedEncodingException {
        // 确定最终跳转的URL
        String targetUrl = yonBipConfig.getBaseUrl();
        log.info("[用友SSO]使用默认目标URL: {}", targetUrl);

        // 构建service参数(用友要求的回调地址)
        String security = URLEncoder.encode(targetUrl, StandardCharsets.UTF_8);
        String serviceUrl = yonBipConfig.getBaseUrl() + "/login?service=" + security;
        String serviceParam = URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8);

        // 构建完整的用友ERP OAuth2登录URL
        String loginUrl = yonBipConfig.getBaseUrl() + "/cas/thirdOauth2CodeLogin"
                + "?thirdUCId=" + yonBipConfig.getIntegrationAuthCode()
                + "&code=" + yonBipLoginCode
                + "&service=" + serviceParam;

        return loginUrl;
    }

    /**
     * 向响应中写入错误信息
     *
     * @param response HttpServletResponse
     * @param errorMsg 错误信息
     */
    private void writeErrorResponse(HttpServletResponse response, String errorMsg) {
        try {
            response.setContentType("text/html;charset=UTF-8");
            response.getWriter().write("<html><body><h3>登录失败</h3><p>" + errorMsg + "</p></body></html>");
            response.getWriter().flush();
        } catch (IOException e) {
            log.error("[用友SSO]写入错误响应失败", e);
        }
    }
}

飞书补充配置

1、开启网页应用

配置网页地址,最终用户在飞书工作台点击应用会调用到上述提到的 Controller 接口

网页链接配置示例:

text 复制代码
https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a9b3e69ab4395cd9&response_type=code&state=1&redirect_uri=http%3A%2F%2F142.221.188.217%3A11024%2Fapi%2Fyonbip%2Fsso%2Fpc-login&scope=contact:contact.base:readonly%20contact:user.employee:readonly%20im:message

注意:上述redirect_uri对应的就是你的 Controller 接口地址,我只是随便写了一个 IP 地址做示例

2、赋予权限

无所吊谓了,我给了下面这么多权限,其它地方也要用到这个应用

json 复制代码
{
  "scopes": {
    "tenant": [
      "aily:message:read",
      "aily:message:write",
      "app_engine:approval:read",
      "app_engine:approval:write",
      "application:application.app_message_stats.overview:readonly",
      "application:application.bot.operator_name:readonly",
      "application:bot.menu:readonly",
      "application:bot.menu:write",
      "approval:approval",
      "approval:approval.list:readonly",
      "approval:approval:readonly",
      "approval:definition",
      "approval:external_approval",
      "approval:external_instance",
      "approval:instance.comment",
      "cardkit:template:read",
      "contact:contact.base:readonly",
      "contact:department.base:readonly",
      "contact:department.organize:readonly",
      "contact:functional_role:readonly",
      "contact:group:readonly",
      "contact:job_family:readonly",
      "contact:job_level:readonly",
      "contact:job_title:readonly",
      "contact:unit:readonly",
      "contact:user.base:readonly",
      "contact:user.department:readonly",
      "contact:user.email:readonly",
      "contact:user.employee:readonly",
      "contact:user.employee_id:readonly",
      "contact:user.employee_number:read",
      "contact:user.gender:readonly",
      "contact:user.id:readonly",
      "contact:user.job_family:readonly",
      "contact:user.job_level:readonly",
      "contact:user.phone:readonly",
      "corehr:contract.period:read",
      "corehr:contract.period:write",
      "corehr:employee.all_bp:read",
      "corehr:employee.bp:read",
      "corehr:employee:read",
      "corehr:employee:write",
      "corehr:employment.archive_cpst_plan:read",
      "corehr:employment.assignment:read",
      "corehr:employment.assignment_pay_group:read",
      "corehr:employment.contract_type:read",
      "corehr:employment.custom_field:read",
      "corehr:employment.custom_org_field:read",
      "corehr:employment.custom_org_field:write",
      "corehr:employment.job:read",
      "corehr:employment.job_grade:read",
      "corehr:employment.job_grade:write",
      "corehr:employment.job_level:read",
      "corehr:employment.job_level:write",
      "corehr:employment.offboarding_reason:read",
      "corehr:employment.pay_group:read",
      "corehr:employment.position:read",
      "corehr:employment.position:write",
      "corehr:job.job_level:read",
      "corehr:job_data.compensation_type:read",
      "corehr:job_data.service_company:read",
      "corehr:job_data.work_shift:read",
      "corehr:person.address:read",
      "corehr:person.address:write",
      "corehr:person.bank_account:read",
      "corehr:person.bank_account:write",
      "corehr:person.born_country_region:read",
      "corehr:person.born_country_region:write",
      "corehr:person.custom_field:read",
      "corehr:person.custom_field:write",
      "corehr:person.date_entered_workforce:read",
      "corehr:person.date_entered_workforce:write",
      "corehr:person.date_of_birth:read",
      "corehr:person.date_of_birth:write",
      "corehr:person.dependent:read",
      "corehr:person.dependent:write",
      "corehr:person.education:read",
      "corehr:person.education:write",
      "corehr:person.email:read",
      "corehr:person.email:write",
      "corehr:person.emergency_contact:read",
      "corehr:person.emergency_contact:write",
      "corehr:person.entry_leave_time:read",
      "corehr:person.gender:read",
      "corehr:person.gender:write",
      "corehr:person.hukou:read",
      "corehr:person.hukou:write",
      "corehr:person.is_disabled:read",
      "corehr:person.is_disabled:write",
      "corehr:person.is_old_alone:read",
      "corehr:person.is_old_alone:write",
      "corehr:person.legal_name:read",
      "corehr:person.legal_name:write",
      "corehr:person.marital_status:read",
      "corehr:person.marital_status:write",
      "corehr:person.martyr_family:read",
      "corehr:person.martyr_family:write",
      "corehr:person.national_id:read",
      "corehr:person.national_id:write",
      "corehr:person.nationality:read",
      "corehr:person.nationality:write",
      "corehr:person.native_region:read",
      "corehr:person.native_region:write",
      "corehr:person.personal_profile:read",
      "corehr:person.personal_profile:write",
      "corehr:person.phone.search:read",
      "corehr:person.phone:read",
      "corehr:person.phone:write",
      "corehr:person.political_affiliation:read",
      "corehr:person.political_affiliation:write",
      "corehr:person.race:read",
      "corehr:person.religion:read",
      "corehr:person.religion:write",
      "corehr:person.resident_tax:read",
      "corehr:person.resident_tax:write",
      "corehr:person.resident_tax_custom_field:read",
      "corehr:person.resident_tax_custom_field:write",
      "corehr:person.work_experience:read",
      "corehr:person.work_experience:write",
      "directory:employee:search",
      "ehr:employee:readonly",
      "im:app_feed_card:write",
      "im:chat.access_event.bot_p2p_chat:read",
      "im:chat.members:bot_access",
      "im:message",
      "im:message.group_at_msg:readonly",
      "im:message.group_msg",
      "im:message.p2p_msg:readonly",
      "im:message.pins:read",
      "im:message.pins:write_only",
      "im:message.reactions:read",
      "im:message.reactions:write_only",
      "im:message.urgent",
      "im:message.urgent.status:write",
      "im:message.urgent:phone",
      "im:message.urgent:sms",
      "im:message:readonly",
      "im:message:recall",
      "im:message:send_as_bot",
      "im:message:send_multi_depts",
      "im:message:send_multi_users",
      "im:message:send_sys_msg",
      "im:message:update"
    ],
    "user": [
      "aily:message:read",
      "aily:message:write",
      "contact:contact.base:readonly",
      "contact:department.base:readonly",
      "contact:user.employee:readonly",
      "contact:user.employee_id:readonly",
      "contact:user.id:readonly",
      "directory:employee.base.base:read",
      "directory:employee.base.email:read",
      "directory:employee.base.is_resigned:read",
      "directory:employee.base.mobile:read",
      "directory:employee.base.name.name:read",
      "directory:employee:search",
      "im:chat.access_event.bot_p2p_chat:read",
      "im:message",
      "im:message.pins:read",
      "im:message.pins:write_only",
      "im:message.reactions:read",
      "im:message.reactions:write_only",
      "im:message.urgent.status:write",
      "im:message:readonly",
      "im:message:recall",
      "search:message"
    ]
  }
}

大概就这么个流程,可能中途描述的东西有所缺失,毕竟做了好几个月了,现在抽时间记录一下,方便有需要的伙伴参考。因为我们当时想找用友要代码,他们说是要收钱的,就自己写了一个。有遗漏的不完善的请大家自行琢磨一下,代码肯定是可以跑的

相关推荐
IT WorryFree2 小时前
OpenClaw接入企业飞书机器人风险与防护
机器人·飞书
铁手飞鹰2 小时前
eBUS SDK Python环境安装
开发语言·python
放下华子我只抽RuiKe52 小时前
智聊机器人进阶:从 API 调试到全功能交互界面的完美落地
开发语言·人工智能·python·机器学习·分类·机器人·交互
qq_411262422 小时前
AP模式中修改下wifi名称就无法连接了,分析一下
java·前端·spring
东离与糖宝2 小时前
Spring AI MCP Server正式落地,Java一键部署AI服务保姆级教程
java·人工智能
微露清风2 小时前
系统性学习Linux-第八讲-进程间通信
java·linux·学习
放下华子我只抽RuiKe52 小时前
构建企业级私有化 AI:从大模型原理到本地智聊机器人全栈部署指南
开发语言·人工智能·python·深度学习·机器学习·分类·机器人
Knight_AL2 小时前
Java 中 Date 与 LocalDate 的区别
java·开发语言·数据库
白狐_7982 小时前
硬核实战:从零构建飞书 × OpenClaw 自动化情报站(四)
运维·自动化·飞书