Browse Source

钉钉相关

zhaopeiqing 7 months ago
parent
commit
4e6bb0a21d
22 changed files with 1870 additions and 6 deletions
  1. 64 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/CallbackConstant.java
  2. 178 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/Constants.java
  3. 54 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/DingUrlConstant.java
  4. 27 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/SyncHttpConstant.java
  5. 30 0
      yudao-module-system/yudao-module-system-biz/pom.xml
  6. 49 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/DingThirdAuthController.java
  7. 14 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/AuthLoginRequest.java
  8. 14 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/BaseRequest.java
  9. 66 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/DingToken.java
  10. 38 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantDingSaveReqVO.java
  11. 36 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dingding/DingUserTenantRelateDO.java
  12. 25 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dingding/DingUserTenantRelateMapper.java
  13. 1 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java
  14. 19 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingAuthServiceInfo.java
  15. 259 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingAuthServiceInfoImpl.java
  16. 141 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingDeptService.java
  17. 431 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingThirdTokenService.java
  18. 24 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java
  19. 55 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java
  20. 67 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/dingding/DingAppConfig.java
  21. 265 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/dingding/RedisCache.java
  22. 13 1
      yudao-server/src/main/resources/application.yaml

+ 64 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/CallbackConstant.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.system.enums.dingding;
+
+/**
+ * 钉钉回调相关事件类型
+ *
+ * @author lnexin
+ */
+public class CallbackConstant {
+    /**
+     * 回调时成功的字符串
+     */
+    public static final String CALLBACK_RETURN_SUCCESS = "success";
+    /**
+     * 激活失败返回
+     */
+    public static final String ACTIVE_RETURN_FAILURE = "active_failure";
+
+    /**
+     * 验证回调地址是否有效
+     */
+    public static final String SUITE_TICKET_CALLBACK_URL_VALIDATE = "suite_ticket";
+    /**
+     * 在开发者后台修改套件时如果回调地址有变化会推送该事件
+     */
+    public static final String CHECK_UPDATE_SUITE_URL = "check_update_suite_url";
+    /**
+     * 钉钉向回调URL POST数据, 解密后是否成功
+     */
+    public static final String CHECK_CREATE_SUITE_URL = "check_create_suite_url";
+
+    /**
+     * 临时授权码,授权开通
+     */
+    public static final String TEMP_AUTH_CODE_ACTIVE = "tmp_auth_code";
+    /**
+     * 解除授权事件
+     */
+    public static final String SUITE_RELIEVE = "suite_relieve";
+
+    /**
+     * 停用应用
+     */
+    public static final String ORG_MICRO_APP_STOP = "org_micro_app_stop";
+    /**
+     * 启用应用
+     */
+    public static final String ORG_MICRO_APP_RESTORE = "org_micro_app_restore";
+
+    /**
+     * 授权方(即授权企业)在钉钉手机客户端-微应用管理中,修改了对应用的授权企业通讯录范围
+     * 授权变更信息并不包括企业用户具体做了什么修改,所以收到推送之后,ISV需要通过调用“获取通讯录权限接口”查询新的授权范围。。
+     */
+    public static final String CONTACT_CHANGE_AUTH = "change_auth";
+
+    /**
+     * 用户下单购买事件
+     */
+    public static final String MARKET_BUY = "market_buy";
+
+    public static final String SYNC_HTTP_PUSH_HIGH="SYNC_HTTP_PUSH_HIGH";
+    public static final String CHECK_URL="check_url";
+
+
+}

+ 178 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/Constants.java

@@ -0,0 +1,178 @@
+package cn.iocoder.yudao.module.system.enums.dingding;
+
+
+/**
+ * 通用常量信息
+ * 
+ * @author ruoyi
+ */
+public class Constants
+{
+    /**
+     * UTF-8 字符集
+     */
+    public static final String UTF8 = "UTF-8";
+
+    /**
+     * GBK 字符集
+     */
+    public static final String GBK = "GBK";
+
+    /**
+     * www主域
+     */
+    public static final String WWW = "www.";
+
+    /**
+     * http请求
+     */
+    public static final String HTTP = "http://";
+
+    /**
+     * https请求
+     */
+    public static final String HTTPS = "https://";
+
+    /**
+     * 通用成功标识
+     */
+    public static final String SUCCESS = "0";
+
+    /**
+     * 通用失败标识
+     */
+    public static final String FAIL = "1";
+
+    /**
+     * 登录成功
+     */
+    public static final String LOGIN_SUCCESS = "Success";
+
+    /**
+     * 注销
+     */
+    public static final String LOGOUT = "Logout";
+
+    /**
+     * 注册
+     */
+    public static final String REGISTER = "Register";
+
+    /**
+     * 登录失败
+     */
+    public static final String LOGIN_FAIL = "Error";
+ 
+    /**
+     * 验证码有效期(分钟)
+     */
+    public static final Integer CAPTCHA_EXPIRATION = 2;
+
+    /**
+     * 令牌
+     */
+    public static final String TOKEN = "token";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String TOKEN_PREFIX = "Bearer ";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String LOGIN_USER_KEY = "login_user_key";
+
+    /**
+     * 用户ID
+     */
+    public static final String JWT_USERID = "userid";
+
+    /**
+     * 用户头像
+     */
+    public static final String JWT_AVATAR = "avatar";
+
+    /**
+     * 创建时间
+     */
+    public static final String JWT_CREATED = "created";
+
+    /**
+     * 用户权限
+     */
+    public static final String JWT_AUTHORITIES = "authorities";
+
+    /**
+     * 资源映射路径 前缀
+     */
+    public static final String RESOURCE_PREFIX = "/profile";
+
+    /**
+     * RMI 远程方法调用
+     */
+    public static final String LOOKUP_RMI = "rmi:";
+
+    /**
+     * LDAP 远程方法调用
+     */
+    public static final String LOOKUP_LDAP = "ldap:";
+
+    /**
+     * LDAPS 远程方法调用
+     */
+    public static final String LOOKUP_LDAPS = "ldaps:";
+
+    /**
+     * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
+     */
+    public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" };
+
+    /**
+     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+     */
+    public static final String[] JOB_WHITELIST_STR = { "com.ruoyi" };
+
+    /**
+     * 定时任务违规的字符
+     */
+    public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
+            "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config" };
+
+    /**
+     *  钉钉redis缓存key
+     */
+    public static final String DAILY_DING_ACCESS_TOKEN = "DAILY_DING_ACCESS_TOKEN";
+
+    /**
+     *  钉钉redis缓存第三方应用token
+     */
+    public static final String DAILY_DING_CORP_ACCESS_TOKEN = "CORP_ACCESS_TOKEN";
+
+    /**
+     *  钉钉redis缓存第三方应用suiteTicket
+     */
+    public static final String DAILY_DING_SUITE_TICKET = "SUITE_TICKET";
+    public static final String DAILY_DING_CORP_ID = "CORP_ID";
+    public static final String DAILY_DING_AUTH = "DAILY_DING_AUTH:";
+
+
+    /**
+     * 钉钉redis缓存key
+     */
+    public static final String DAILY_DING_JS_TICKET = "DAILY_DING_JS_TICKET";
+
+    /**
+     * 钉钉 GrantType
+     */
+    public static final String AUTHORIZATION_CODE = "authorization_code";
+
+    /**
+     * 钉钉 GrantType
+     */
+    public static final String REFRESH_TOKEN = "refresh_token";
+
+
+
+
+}

+ 54 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/DingUrlConstant.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.system.enums.dingding;
+
+/**
+ * 钉钉开放接口网关常量
+ *
+ * @author Administrator
+ */
+public class DingUrlConstant {
+
+    private static final String HOST = "https://oapi.dingtalk.com";
+
+    /**
+     * 获取access_token url
+     */
+    public static final String URL_GET_TOKEN = HOST + "/gettoken";
+
+    /**
+     * 获取jsapi_ticket url
+     */
+    public static final String URL_GET_JSTICKET = HOST + "/get_jsapi_ticket";
+
+    /**
+     * 通过免登授权码获取用户信息 url
+     */
+    public static final String URL_GET_USER_INFO = HOST + "/user/getuserinfo";
+
+    /**
+     * 根据用户id获取用户详情 url
+     */
+    public static final String URL_USER_GET = HOST + "/user/get";
+
+    public static final String URL_USER_GET_V2 = HOST + "/topapi/v2/user/get";
+
+    /**
+     * 获取部门列表 url
+     */
+    public static final String URL_DEPARTMENT_LIST = HOST + "/department/list";
+
+    /**
+     * 获取部门用户 url
+     */
+    public static final String URL_USER_SIMPLELIST = HOST + "/user/simplelist";
+
+    /**
+     * 获取登录用户企业认证信息
+     */
+    public static final String URL_ORG_INFO = HOST + "/contact/organizations/authInfos";
+
+    /**
+     * 获取应用管理后台免登的用户信息
+     */
+    public static final String URL_SSO_USER_INFO = HOST + "/oauth2/ssoUserInfo";
+
+}

+ 27 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/dingding/SyncHttpConstant.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.system.enums.dingding;
+
+/**
+ * 项目中的常量定义类
+ */
+public class SyncHttpConstant {
+    /**
+     * 应用的SuiteKey,登录开发者后台,点击应用管理,进入应用详情可见
+     */
+    public static final String SUITE_KEY="suiteyjd6ikxpg8629ydr";
+
+    /**
+     * 应用的SuiteSecret,登录开发者后台,点击应用管理,进入应用详情可见
+     */
+    public static final String SUITE_SECRET="znq4BDYYX4vFLxtjfknEoId6j84LT2xpW7gkRTvpVZVoMfbfHXZvZX3cQ2cLQzON";
+
+    /**
+     * 回调URL签名用。应用的签名Token, 登录开发者后台,点击应用管理,进入应用详情可见
+     */
+    public static final String TOKEN = "foHrAyOAp4kIeNvi";
+
+    /**
+     * 回调URL加解密用。应用的"数据加密密钥",登录开发者后台,点击应用管理,进入应用详情可见
+     */
+    public static final String ENCODING_AES_KEY = "o13UDrSomxLeKRhwWtOgjjYpwLkzFS10HXz408Jt7OI";
+
+}

+ 30 - 0
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -152,6 +152,36 @@
             <groupId>com.xingyuv</groupId>
             <artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 -->
         </dependency>
+
+        <!--新版本-->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dingtalk</artifactId>
+            <version>2.0.14</version>
+        </dependency>
+        <!-- hanlp lib -->
+        <dependency>
+            <groupId>com.hanlp</groupId>
+            <artifactId>hanlp</artifactId>
+            <version>1.8.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.taobao.top</groupId>
+            <artifactId>lippi-oapi-encrpt</artifactId>
+            <version>dingtalk-SNAPSHOT</version>
+        </dependency>
+        <!-- 钉钉SDK -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
+            <version>2.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.dingtalk.open</groupId>
+            <artifactId>app-stream-client</artifactId>
+            <version>1.3.2</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 49 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/DingThirdAuthController.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.system.controller.admin.dingding;
+
+import cn.iocoder.yudao.module.system.service.dingding.DingAuthServiceInfo;
+import cn.iocoder.yudao.module.system.service.dingding.DingThirdTokenService;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * <p>DingLoginController 此类用于:钉钉企业内部应用免登(H5微应用)</p>
+ * <p>@remark:钉钉企业内部微应用DEMO, 实现了身份验证(免登)功能</p>
+ */
+@Tag(name = "管理后台 - 钉钉第三方企业应用免登")
+@Controller
+@RequestMapping(value = "/auth/ding")
+@Slf4j
+public class DingThirdAuthController {
+
+    @Resource
+    private DingThirdTokenService dingAuthTokenService;
+
+    @Resource
+    private DingAuthServiceInfo dingAuthServiceInfo;
+
+    @PostMapping(value = "/callback")
+    @ResponseBody
+    public Map<String, String> callback(@RequestParam String signature,
+                                        @RequestParam String timestamp,
+                                        @RequestParam String nonce,
+                                        @RequestBody JsonNode encryptNode) {
+        String encryptMsg = encryptNode.get("encrypt").textValue();
+        String plainText = dingAuthServiceInfo.decryptText(signature, timestamp, nonce, encryptMsg);
+        JSONObject callBackContent = JSON.parseObject(plainText);
+
+        //进入回调事件分支选择
+        Map<String, String> resultMap = dingAuthServiceInfo.caseProcess(callBackContent);
+        return resultMap;
+    }
+
+}

+ 14 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/AuthLoginRequest.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.system.controller.admin.dingding.vo;
+
+import lombok.Data;
+
+/**
+ * @author hanzy
+ * @description 第三方企业应用登录
+ * @date 2024050917:27
+ */
+@Data
+public class AuthLoginRequest  extends BaseRequest {
+    //用户企业id
+    private String corpId;
+}

+ 14 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/BaseRequest.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.system.controller.admin.dingding.vo;
+
+import lombok.Data;
+
+/**
+ * @author hanzy
+ * @description
+ * @date 2024051017:28
+ */
+@Data
+public class BaseRequest {
+    //用户登录code
+    private String code;
+}

+ 66 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dingding/vo/DingToken.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.system.controller.admin.dingding.vo;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+/**
+ * @author hanzy
+ * @description
+ * @date 2024042913:10
+ */
+
+public class DingToken extends AbstractAuthenticationToken {
+    public DingToken(Collection<? extends GrantedAuthority> authorities, String userId, String corpId) {
+        super(authorities);
+        this.userId = userId;
+        this.corpId = corpId;
+    }
+
+    public DingToken(String userId, String corpId) {
+        super(null);
+        this.userId = userId;
+        this.corpId = corpId;
+    }
+
+    public DingToken(String userId, String corpId, Object details) {
+        super(null);
+        this.userId = userId;
+        this.corpId = corpId;
+        super.setDetails(details);
+    }
+
+    private String userId;
+
+    private String corpId;
+
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getCorpId() {
+        return corpId;
+    }
+
+    public void setCorpId(String corpId) {
+        this.corpId = corpId;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.corpId;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.userId;
+    }
+
+
+}

+ 38 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantDingSaveReqVO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 租户创建/修改 Request VO")
+@Data
+public class TenantDingSaveReqVO {
+
+    @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
+    private String name;
+
+    @Schema(description = "联系手机", example = "15601691300")
+    private String contactMobile;
+
+    @Schema(description = "租户状态:默认传0", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @NotNull(message = "租户状态")
+    private Integer status;
+
+    @Schema(description = "绑定域名", example = "https://www.iocoder.cn")
+    private String website;
+
+    @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime expireTime;
+
+    @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Integer accountCount;
+
+    @Schema(description = "企业ID")
+    private String corpId;
+
+    @Schema(description = "管理员ID")
+    private String manageUserId;
+
+}

+ 36 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dingding/DingUserTenantRelateDO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.dingding;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 钉钉用户租户关系 DO
+ *
+ * @author zhaopq
+ */
+@TableName("ding_user_tenant_relate")
+@KeySequence("ding_user_tenant_relate_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DingUserTenantRelateDO {
+
+    /**
+     * 关系ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户ID
+     */
+    private String userId;
+    /**
+     * 租户ID
+     */
+    private Long tenantId;
+
+}

+ 25 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dingding/DingUserTenantRelateMapper.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.system.dal.mysql.dingding;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.controller.admin.user.vo.tenant.UserTenantRelatePageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dingding.DingUserTenantRelateDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 钉钉用户租户关系 Mapper
+ *
+ * @author zhaopq
+ */
+@Mapper
+public interface DingUserTenantRelateMapper extends BaseMapperX<DingUserTenantRelateDO> {
+
+    default PageResult<DingUserTenantRelateDO> selectPage(UserTenantRelatePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<DingUserTenantRelateDO>()
+                .eqIfPresent(DingUserTenantRelateDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(DingUserTenantRelateDO::getTenantId, reqVO.getTenantId())
+                .orderByDesc(DingUserTenantRelateDO::getId));
+    }
+
+}

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java

@@ -46,6 +46,7 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
         return selectList(TenantDO::getPackageId, packageId);
     }
 
+    @TenantIgnore
     default TenantDO selectByCorpId(String corpId){
         return selectOne(TenantDO::getCorpId, corpId);
     }

+ 19 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingAuthServiceInfo.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.system.service.dingding;
+
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.Map;
+
+/**
+ * <p>DingAuthService 此接口用于:</p>
+ * <p>@remark:</p>
+ */
+public interface DingAuthServiceInfo {
+
+    String decryptText(String signature, String timestamp, String nonce, String encryptMsg);
+
+    Map<String, String> caseProcess(JSONObject plainNode);
+
+    Map<String, String> encryptText(String text);
+
+}

+ 259 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingAuthServiceInfoImpl.java

@@ -0,0 +1,259 @@
+package cn.iocoder.yudao.module.system.service.dingding;
+
+import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantDingSaveReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantJoinReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO;
+import cn.iocoder.yudao.module.system.enums.dingding.Constants;
+import cn.iocoder.yudao.module.system.enums.dingding.SyncHttpConstant;
+import cn.iocoder.yudao.module.system.service.tenant.TenantService;
+import cn.iocoder.yudao.module.system.util.dingding.RedisCache;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.dingtalk.oapi.lib.aes.DingTalkEncryptException;
+import com.dingtalk.oapi.lib.aes.DingTalkEncryptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import static cn.iocoder.yudao.module.system.enums.dingding.CallbackConstant.*;
+
+
+/**
+ * <p>DingAuthServiceImpl 此类用于:</p>
+ * <p>@author:hujm</p>
+ * <p>@date2021052015:56</p>
+ * <p>@remark:</p>
+ */
+@Slf4j
+@Service
+public class DingAuthServiceInfoImpl implements DingAuthServiceInfo {
+
+    @Resource
+    private RedisCache redisCache;
+    @Resource
+    private TenantService tenantService;
+
+    @Override
+    public String decryptText(String signature, String timestamp, String nonce, String encryptMsg) {
+        String plainText = "";
+        try {
+            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(SyncHttpConstant.TOKEN, SyncHttpConstant.ENCODING_AES_KEY, SyncHttpConstant.SUITE_KEY);
+            plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, encryptMsg);
+        } catch (DingTalkEncryptException e) {
+            log.error("钉钉消息体解密错误, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, e: {}", signature, timestamp, nonce, encryptMsg, e);
+        }
+        log.debug("钉钉消息体解密, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, 解密结果: {}", signature, timestamp, nonce, encryptMsg, plainText);
+        return plainText;
+    }
+    @Override
+    public Map<String, String> caseProcess(JSONObject plainNode) {
+        Map<String, String> resultMap = new LinkedHashMap<>();
+        String eventType = plainNode.getString("EventType");
+        JSONArray bizData = plainNode.getJSONArray("bizData");
+        String corp_id = null;
+        JSONObject biz_data = null;
+        Integer bizType = 0;
+        for (Object bizDatum : bizData) {
+            JSONObject bizDataJson = (JSONObject) bizDatum;
+            log.info("[callback] 获取到授权bizDataJson:" + bizDataJson);
+            String biz_data_str = bizDataJson.getString("biz_data");
+            biz_data = JSONObject.parseObject(biz_data_str);
+            bizType = bizDataJson.getInteger("biz_type");
+            log.info("[callback] 获取到bizType:" + bizType);
+            corp_id = bizDataJson.getString("corp_id");
+        }
+
+        switch (eventType) {
+            case SUITE_TICKET_CALLBACK_URL_VALIDATE:
+                log.info("[callback] 验证回调地址有效性质:{}", plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case TEMP_AUTH_CODE_ACTIVE:
+                log.info("[callback] 企业开通授权:{}", plainNode);
+                log.info("[callback] 企业开通授权信息:", bizData);
+                if (bizType == 4 && biz_data != null && corp_id != null) {
+//                    updateCompanyInfo(biz_data,corp_id);
+                }
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case SUITE_RELIEVE:
+                log.info("[callback] 企业解除授权:{}", plainNode);
+                if (bizType == 4 && biz_data != null && corp_id != null) {
+//                    tEnterpriseCompanyService.updateTCompanyUserClose(corp_id);
+                }
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CHECK_UPDATE_SUITE_URL:
+                log.info("[callback] 在开发者后台修改回调地址:" + plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CHECK_CREATE_SUITE_URL:
+                log.info("[callback] 检查钉钉向回调URL POST数据解密后是否成功:" + plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CONTACT_CHANGE_AUTH:
+                log.info("[callback] 通讯录授权范围变更事件:" + plainNode);
+                break;
+            case ORG_MICRO_APP_STOP:
+                log.info("[callback] 停用应用:" + plainNode);
+                break;
+            case ORG_MICRO_APP_RESTORE:
+                log.info("[callback] 启用应用:" + plainNode);
+                break;
+            case MARKET_BUY:
+                log.info("[callback] 用户下单购买事件:" + plainNode);
+                break;
+            case SYNC_HTTP_PUSH_HIGH:
+                log.info("[callback] 套件票据事件:" + plainNode);
+                for (Object bizDatum : bizData) {
+                    JSONObject bizDataJson = (JSONObject) bizDatum;
+                    log.info("[callback] 获取到授权bizDataJson:" + bizDataJson);
+                    String biz_data_str = bizDataJson.getString("biz_data");
+                    biz_data = JSONObject.parseObject(biz_data_str);
+                    bizType = bizDataJson.getInteger("biz_type");
+                    log.info("[callback] 获取到bizType:" + bizType);
+                    corp_id = bizDataJson.getString("corp_id");
+                }
+                log.info("[callback] 获取到套件票据corp_id:{}", corp_id);
+                //套件票据
+                if (bizType == 2 && biz_data != null && corp_id != null) {
+                    String suiteTicket = biz_data.getString("suiteTicket");
+                    String redisKeyPrefix = Constants.DAILY_DING_AUTH + corp_id + ":";
+                    redisCache.setCacheObject(redisKeyPrefix + Constants.DAILY_DING_SUITE_TICKET, suiteTicket, 5, TimeUnit.HOURS);
+                }
+                //套件票据授权
+                if (bizType == 4 && biz_data != null && corp_id != null) {
+                    //syncAction字段的取值如下
+                    //org_suite_auth:表示企业授权第三方企业应用
+                    //org_suite_change:表示企业变更授权范围
+                    //org_suite_relieve:表示企业解除授权
+                    //企业授权信息
+                    String syncAction = biz_data.getString("syncAction");
+                    if ("org_suite_auth".equals(syncAction)) {
+                        log.info("[callback] 企业授权第三方企业应用:{}",corp_id);
+                        this.createTenant(biz_data, corp_id);
+                    } else if ("org_suite_change".equals(syncAction)) {
+                        log.info("[callback] 表示企业变更授权范围:{}",corp_id);
+                        this.updateTenant(biz_data, corp_id);
+                    } else if ("org_suite_relieve".equals(syncAction)) {
+                        log.info("[callback] 表示企业解除授权:{}",corp_id);
+                        this.deleteTenant(biz_data, corp_id);
+                    }
+                }
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            case CHECK_URL:
+                log.info("[callback] 检查钉钉向回调URL POST数据解密后是否成功:" + plainNode);
+                resultMap = encryptText(CALLBACK_RETURN_SUCCESS);
+                break;
+            default:
+                log.info("[callback] 未知事件: {} , 内容: {}", eventType, plainNode);
+                resultMap = encryptText("事件类型未定义, 请联系应用提供方!" + eventType);
+                break;
+        }
+        return resultMap;
+    }
+    @Override
+    public Map<String, String> encryptText(String text) {
+        Map<String, String> resultMap = new LinkedHashMap<>();
+        try {
+            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(SyncHttpConstant.TOKEN, SyncHttpConstant.ENCODING_AES_KEY, SyncHttpConstant.SUITE_KEY);
+            resultMap = dingTalkEncryptor.getEncryptedMap(text, System.currentTimeMillis(), getRandomStr(8));
+        } catch (DingTalkEncryptException e) {
+            log.error("钉钉消息体加密,text: {}, e: {}", text, e);
+        }
+        log.debug("钉钉消息体加密,text: {}, resultMap: {}", text, resultMap);
+        return resultMap;
+    }
+
+
+    public static String getRandomStr(int count) {
+        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+
+        for (int i = 0; i < count; ++i) {
+            int number = random.nextInt(base.length());
+            sb.append(base.charAt(number));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 创建租户
+     *
+     * @param biz_data
+     * @param corp_id
+     */
+    private void createTenant(JSONObject biz_data, String corp_id) {
+        String authCorpInfo_str = biz_data.getString("auth_corp_info");//授权方企业信息。
+        String auth_info_str = biz_data.getString("auth_info");//授权信息。
+        String auth_user_info_str = biz_data.getString("auth_user_info");//授权方管理员信息
+        JSONObject authCorpInfo = JSONObject.parseObject(authCorpInfo_str);
+        JSONObject authInfo = JSONObject.parseObject(auth_info_str);
+        JSONArray agentArray = authInfo.getJSONArray("agent");
+        JSONObject agent = JSONObject.parseObject(agentArray.get(0).toString());
+        JSONObject authUserInfo = JSONObject.parseObject(auth_user_info_str);
+//        String accessToken = dingThirdTokenService.getCorpAccessToken(corp_id);
+        String manager_userid = authUserInfo.getString("userId");
+        TenantDingSaveReqVO respVO = new TenantDingSaveReqVO();
+        respVO.setCorpId(authCorpInfo.getString("corpid"));
+        respVO.setName(authCorpInfo.getString("full_corp_name"));
+        respVO.setManageUserId(manager_userid);
+        tenantService.createDingTenant(respVO);
+    }
+
+    /**
+     * 更新租户
+     *
+     * @param biz_data
+     * @param corp_id
+     */
+    private void updateTenant(JSONObject biz_data, String corp_id) {
+        String authCorpInfo_str = biz_data.getString("auth_corp_info");//授权方企业信息。
+        String auth_info_str = biz_data.getString("auth_info");//授权信息。
+        String auth_user_info_str = biz_data.getString("auth_user_info");//授权方管理员信息
+        JSONObject authCorpInfo = JSONObject.parseObject(authCorpInfo_str);
+        JSONObject authInfo = JSONObject.parseObject(auth_info_str);
+        JSONArray agentArray = authInfo.getJSONArray("agent");
+        JSONObject agent = JSONObject.parseObject(agentArray.get(0).toString());
+        JSONObject authUserInfo = JSONObject.parseObject(auth_user_info_str);
+//        String accessToken = dingThirdTokenService.getCorpAccessToken(corp_id);
+        String manager_userid = authUserInfo.getString("userId");
+        TenantDingSaveReqVO respVO = new TenantDingSaveReqVO();
+        respVO.setCorpId(authCorpInfo.getString("corpid"));
+        respVO.setName(authCorpInfo.getString("full_corp_name"));
+        respVO.setManageUserId(manager_userid);
+        tenantService.updateDingTenant(respVO);
+    }
+
+    /**
+     * 删除租户
+     *
+     * @param biz_data
+     * @param corp_id
+     */
+    private void deleteTenant(JSONObject biz_data, String corp_id) {
+        String authCorpInfo_str = biz_data.getString("auth_corp_info");//授权方企业信息。
+        String auth_info_str = biz_data.getString("auth_info");//授权信息。
+        String auth_user_info_str = biz_data.getString("auth_user_info");//授权方管理员信息
+        JSONObject authCorpInfo = JSONObject.parseObject(authCorpInfo_str);
+        JSONObject authInfo = JSONObject.parseObject(auth_info_str);
+        JSONArray agentArray = authInfo.getJSONArray("agent");
+        JSONObject agent = JSONObject.parseObject(agentArray.get(0).toString());
+        JSONObject authUserInfo = JSONObject.parseObject(auth_user_info_str);
+//        String accessToken = dingThirdTokenService.getCorpAccessToken(corp_id);
+        String manager_userid = authUserInfo.getString("userId");
+        TenantDingSaveReqVO respVO = new TenantDingSaveReqVO();
+        respVO.setCorpId(authCorpInfo.getString("corpid"));
+        tenantService.deleteDingTenant(respVO);
+    }
+
+}

+ 141 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingDeptService.java

@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.system.service.dingding;
+
+import cn.hutool.core.collection.CollUtil;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.DingTalkClient;
+import com.dingtalk.api.request.OapiV2DepartmentGetRequest;
+import com.dingtalk.api.request.OapiV2DepartmentListsubRequest;
+import com.dingtalk.api.request.OapiV2DepartmentListsubidRequest;
+import com.dingtalk.api.response.OapiV2DepartmentGetResponse;
+import com.dingtalk.api.response.OapiV2DepartmentListsubResponse;
+import com.dingtalk.api.response.OapiV2DepartmentListsubidResponse;
+import com.taobao.api.ApiException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+
+/**
+ * @author hanzy
+ * @description
+ * @date 2024060413:42
+ */
+@Slf4j
+@Service
+public class DingDeptService {
+    /**
+     * @description: 获取部门列表
+     * @param: accessToken
+     * @return: com.dingtalk.api.response.OapiV2DepartmentListsubResponse
+     */
+    public OapiV2DepartmentListsubResponse listsub(String accessToken, Long deptId) {
+        OapiV2DepartmentListsubResponse rsp;
+        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/listsub");
+        OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest();
+        req.setDeptId(deptId);
+        req.setLanguage("zh_CN");
+        try {
+            rsp = client.execute(req, accessToken);
+        } catch (ApiException e) {
+            log.error("Failed to getUserName: " + e.getErrMsg());
+            return null;
+        }
+        return rsp;
+    }
+
+    /**
+     * @description: 获取子部门ID列表
+     * @param: accessToken
+     * @return: com.dingtalk.api.response.OapiV2DepartmentListsubidResponse
+     */
+    public OapiV2DepartmentListsubidResponse listsubid(String accessToken, Long deptId) {
+        OapiV2DepartmentListsubidResponse rsp;
+        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/listsubid");
+        OapiV2DepartmentListsubidRequest req = new OapiV2DepartmentListsubidRequest();
+        req.setDeptId(deptId);
+        try {
+            rsp = client.execute(req, accessToken);
+        } catch (ApiException e) {
+            log.error("Failed to getUserName: " + e.getErrMsg());
+            return null;
+        }
+        return rsp;
+    }
+
+    /**
+     * @description: 获取部门详情
+     * @param: accessToken
+     * @return: com.dingtalk.api.response.OapiV2DepartmentGetResponse
+     */
+    public OapiV2DepartmentGetResponse get(String accessToken, Long deptId) {
+        OapiV2DepartmentGetResponse rsp;
+        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/get");
+        OapiV2DepartmentGetRequest req = new OapiV2DepartmentGetRequest();
+        req.setDeptId(deptId);
+        req.setLanguage("zh_CN");
+        try {
+            rsp = client.execute(req, accessToken);
+            ;
+        } catch (ApiException e) {
+            log.error("Failed to getUserName: " + e.getErrMsg());
+            return null;
+        }
+        return rsp;
+    }
+
+    public List<Long> getSubDeptIdList(String accessToken, Long deptId) {
+        List<Long> result = new ArrayList<>();
+        OapiV2DepartmentListsubidResponse listsubid = listsubid(accessToken, deptId);
+        if (listsubid.getErrcode() == 0) {
+            result = listsubid.getResult().getDeptIdList();
+        }
+        return result;
+    }
+
+    public OapiV2DepartmentGetResponse.DeptGetResponse getDepInfo(String accessToken, Long deptId) {
+        OapiV2DepartmentGetResponse.DeptGetResponse result = new OapiV2DepartmentGetResponse.DeptGetResponse();
+        OapiV2DepartmentGetResponse oapiV2DepartmentGetResponse = get(accessToken, deptId);
+        if (oapiV2DepartmentGetResponse.getErrcode() == 0) {
+            result = oapiV2DepartmentGetResponse.getResult();
+        }
+        return result;
+    }
+
+    public void getChildDeptIdList(String accessToken, Long parentId, List<Long> children) {
+        List<Long> deptIdList = getSubDeptIdList(accessToken, parentId);
+        if (CollUtil.isEmpty(deptIdList)) {
+            return;
+        }
+        children.addAll(deptIdList);
+        for (Long deptId : deptIdList) {
+            getChildDeptIdList(accessToken, deptId, children);
+        }
+    }
+
+    public List<OapiV2DepartmentGetResponse.DeptGetResponse> deptGetResponseList(String accessToken, Long parentId) {
+        List<OapiV2DepartmentGetResponse.DeptGetResponse> deptGetResponseList = new ArrayList<>();
+        List<Long> children = new ArrayList<>();
+        getChildDeptIdList(accessToken, parentId, children);
+        Set<Long> depIdSet = convertSet(children);
+        for (Long depId : depIdSet) {
+            OapiV2DepartmentGetResponse.DeptGetResponse depInfo = getDepInfo(accessToken, depId);
+            if (depInfo != null) {
+                deptGetResponseList.add(depInfo);
+            }
+        }
+        return deptGetResponseList;
+    }
+
+    public static <T> Set<T> convertSet(Collection<T> from) {
+        return convertSet(from, v -> v);
+    }
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+}

+ 431 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dingding/DingThirdTokenService.java

@@ -0,0 +1,431 @@
+package cn.iocoder.yudao.module.system.service.dingding;
+
+import cn.iocoder.yudao.module.system.enums.dingding.Constants;
+import cn.iocoder.yudao.module.system.enums.dingding.DingUrlConstant;
+import cn.iocoder.yudao.module.system.util.dingding.DingAppConfig;
+import cn.iocoder.yudao.module.system.util.dingding.RedisCache;
+import com.aliyun.dingtalkcontact_1_0.models.GetUserHeaders;
+import com.aliyun.dingtalkcontact_1_0.models.GetUserResponse;
+import com.aliyun.dingtalkoauth2_1_0.Client;
+import com.aliyun.dingtalkoauth2_1_0.models.*;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.DingTalkClient;
+import com.dingtalk.api.request.*;
+import com.dingtalk.api.response.*;
+
+import com.taobao.api.ApiException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author hzy
+ * @description: 钉钉第三方应用授权
+ * @param: null
+ * @return:
+ * @date: 2024/5/9 14:29
+ */
+@Service
+@Slf4j
+public class DingThirdTokenService {
+    @Resource
+    private DingAppConfig dingAppConfig;
+    @Resource
+    private RedisCache redisCache;
+
+    public static Client createClient2_1_0() throws Exception {
+        Config config = new Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new Client(config);
+    }
+
+    public static com.aliyun.dingtalkcontact_1_0.Client createClient_1_0() throws Exception {
+        Config config = new Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkcontact_1_0.Client(config);
+    }
+
+    public GetUserTokenResponse getUserAccessToken(String code) {
+        GetUserTokenResponse userToken = null;
+        try {
+            Client client = this.createClient2_1_0();
+            GetUserTokenRequest request = new GetUserTokenRequest()
+                    .setClientSecret(dingAppConfig.getAppSecret())
+                    .setClientId(dingAppConfig.getAppKey())
+                    .setCode(code).setGrantType(Constants.AUTHORIZATION_CODE);
+            userToken = client.getUserToken(request);
+        } catch (ApiException e) {
+            log.error("getAccessToken failed", e);
+        } catch (Exception e) {
+            log.error("getAccessToken failed", e);
+        }
+        return userToken;
+    }
+
+    /**
+     * 在此方法中,为了避免频繁获取access_token,
+     * 在距离上一次获取access_token时间在两个小时之内的情况,
+     * 将直接从持久化存储中读取access_token
+     * <p>
+     * 因为access_token和jsapi_ticket的过期时间都是7200
+     * 所以在获取access_token的同时也去获取了jsapi_ticket
+     * 注:jsapi_ticket是在前端页面JSAPI做权限验证配置的时候需要使用的
+     * 具体信息请查看开发者文档--权限验证配置
+     *
+     * @return accessToken 或错误信息
+     */
+    public String getCorpAccessToken(String corpId) {
+        String redisKeyPrefix = Constants.DAILY_DING_AUTH + corpId + ":";
+        // 从持久化存储中读取
+        String corpAccessToken = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_CORP_ACCESS_TOKEN);
+        log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}", corpAccessToken);
+        if (corpAccessToken != null) {
+            return corpAccessToken;
+        }
+        try {
+            com.aliyun.dingtalkoauth2_1_0.Client client = this.createClient2_1_0();
+            String suiteTicket = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_SUITE_TICKET);
+            log.info("从Redis缓存中获取到的第三方企业{},suiteTicket = {}", corpId, suiteTicket);
+            com.aliyun.dingtalkoauth2_1_0.models.GetCorpAccessTokenRequest getCorpAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetCorpAccessTokenRequest()
+                    .setSuiteKey(dingAppConfig.getAppKey())
+                    .setSuiteSecret(dingAppConfig.getAppSecret())
+                    .setAuthCorpId(corpId)
+                    .setSuiteTicket(suiteTicket);
+            GetCorpAccessTokenResponse response = client.getCorpAccessToken(getCorpAccessTokenRequest);
+            corpAccessToken = response.getBody().getAccessToken();
+            Long expireIn = response.getBody().getExpireIn();
+            log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}",corpId, corpAccessToken);
+            redisCache.setCacheObject(Constants.DAILY_DING_CORP_ACCESS_TOKEN, corpAccessToken, expireIn.intValue(), TimeUnit.MINUTES);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+            return corpAccessToken;
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+            return corpAccessToken;
+        }
+        return corpAccessToken;
+    }
+
+
+    //服务商获取第三方应用授权企业的access_token
+    public String getThirdCorpAccessToken(String corpId) {
+        String redisKeyPrefix = Constants.DAILY_DING_AUTH + corpId + ":";
+        // 从持久化存储中读取
+        String corpAccessToken = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_CORP_ACCESS_TOKEN);
+        log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}", corpAccessToken);
+        if (corpAccessToken != null) {
+            return corpAccessToken;
+        }
+        try {
+            String suiteTicket = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_SUITE_TICKET);
+            log.info("从Redis缓存中获取到的第三方企业{},suiteTicket = {}", corpId, suiteTicket);
+            DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/service/get_corp_token");
+            OapiServiceGetCorpTokenRequest req = new OapiServiceGetCorpTokenRequest();
+            req.setAuthCorpid(corpId);
+            OapiServiceGetCorpTokenResponse execute = client.execute(req, dingAppConfig.getAppKey(), dingAppConfig.getAppSecret(), suiteTicket);
+            corpAccessToken = execute.getAccessToken();
+            Long expireIn = execute.getExpiresIn();
+            log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}", execute.getAccessToken());
+            redisCache.setCacheObject(Constants.DAILY_DING_CORP_ACCESS_TOKEN, corpAccessToken, expireIn.intValue(), TimeUnit.MINUTES);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+            return corpAccessToken;
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+            return corpAccessToken;
+        }
+        return corpAccessToken;
+    }
+
+    public OapiV2UserGetuserinfoResponse getUserUnfo(String code, String access_token) {
+        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo");
+        OapiV2UserGetuserinfoRequest req = new OapiV2UserGetuserinfoRequest();
+        req.setCode(code);
+        OapiV2UserGetuserinfoResponse rsp = null;
+        try {
+            rsp = client.execute(req, access_token);
+        } catch (ApiException e) {
+            throw new RuntimeException(e);
+        }
+        return rsp;
+    }
+
+    public OapiV2UserGetResponse getAuthUser(String accessToken, String userId) {
+        DingTalkClient client = new DefaultDingTalkClient(DingUrlConstant.URL_USER_GET_V2);
+        OapiV2UserGetResponse response;
+        OapiV2UserGetRequest request = new OapiV2UserGetRequest();
+        request.setUserid(userId);
+        request.setLanguage("en_US");
+        try {
+            response = client.execute(request, accessToken);
+        } catch (ApiException e) {
+            log.error("Failed to getUserName: " + e.getErrMsg());
+            return null;
+        }
+        return response;
+    }
+
+    //获取应用管理后台免登的用户信息
+    public Object getSsoUserInfo(String xAcsDingtalkAccessToken, String code) {
+        GetSsoUserInfoResponse ssoUserInfoWithOptions = null;
+        try {
+            com.aliyun.dingtalkoauth2_1_0.Client client = this.createClient2_1_0();
+            GetSsoUserInfoHeaders getSsoUserInfoHeaders = new GetSsoUserInfoHeaders();
+            getSsoUserInfoHeaders.xAcsDingtalkAccessToken = xAcsDingtalkAccessToken;
+            GetSsoUserInfoRequest getSsoUserInfoRequest = new GetSsoUserInfoRequest()
+                    .setCode(code);
+            ssoUserInfoWithOptions = client.getSsoUserInfoWithOptions(getSsoUserInfoRequest, getSsoUserInfoHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return ssoUserInfoWithOptions;
+    }
+
+
+    /**
+     * @description: 获取用户通讯录个人信息
+     * @param: accessToken
+     * @param: unionId
+     * @return: java.lang.Object
+     * nick 用户的钉钉昵称。
+     * avatarUrl头像URL。
+     * mobile用户的手机号。
+     * openId用户的openId。
+     * unionId用户的unionId。
+     * email用户的个人邮箱。
+     * stateCode手机号对应的国家号。
+     */
+    public GetUserResponse getAddressBookUserInfo(String accessToken, String unionId) {
+        GetUserResponse response = null;
+        try {
+            com.aliyun.dingtalkcontact_1_0.Client client = this.createClient_1_0();
+            GetUserHeaders getUserHeaders = new GetUserHeaders();
+            getUserHeaders.xAcsDingtalkAccessToken = accessToken;
+            response = client.getUserWithOptions(unionId, getUserHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return response;
+    }
+
+
+    /****************************************************企业应用授权***********************************************************/
+
+    //获取第三方企业应用的suite_access_token
+    public OapiServiceGetSuiteTokenResponse getSuiteToken(String corpId) {
+        OapiServiceGetSuiteTokenResponse rsp = null;
+        String redisKeyPrefix = Constants.DAILY_DING_AUTH + corpId + ":";
+        // 从持久化存储中读取
+        String corpAccessToken = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_CORP_ACCESS_TOKEN);
+        log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}", corpAccessToken);
+        try {
+            String suiteTicket = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_SUITE_TICKET);
+            log.info("从Redis缓存中获取到的第三方企业{},suiteTicket = {}", corpId, suiteTicket);
+            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/service/get_suite_token");
+            OapiServiceGetSuiteTokenRequest req = new OapiServiceGetSuiteTokenRequest();
+            req.setSuiteKey(dingAppConfig.getAppKey());
+            req.setSuiteSecret(dingAppConfig.getAppSecret());
+            req.setSuiteTicket(suiteTicket);
+            rsp = client.execute(req);
+            redisCache.setCacheObject(Constants.DAILY_DING_CORP_ACCESS_TOKEN, rsp.getSuiteAccessToken(), rsp.getExpiresIn().intValue(), TimeUnit.MINUTES);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        }
+        return rsp;
+    }
+
+    //获取企业授权信息
+    public Object getAuthInfoRequest(String corpId) {
+        OapiServiceGetAuthInfoResponse rsp = null;
+        String redisKeyPrefix = Constants.DAILY_DING_AUTH + corpId + ":";
+        // 从持久化存储中读取
+        String corpAccessToken = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_CORP_ACCESS_TOKEN);
+        log.info("从Redis缓存中获取到的第三方企业{},corpAccessToken = {}", corpAccessToken);
+        try {
+            String suiteTicket = redisCache.getCacheObject(redisKeyPrefix + Constants.DAILY_DING_SUITE_TICKET);
+            log.info("从Redis缓存中获取到的第三方企业{},suiteTicket = {}", corpId, suiteTicket);
+            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/service/get_auth_info");
+            OapiServiceGetAuthInfoRequest req = new OapiServiceGetAuthInfoRequest();
+            req.setSuiteKey(suiteTicket);
+            req.setAuthCorpid(corpId);
+            // 第三方企业应用的填写应用SuiteKey和SuiteSecret。
+            // 定制应用填写应用的CustomKey和CustomSecret。
+            rsp = client.execute(req, dingAppConfig.getAppKey(), dingAppConfig.getAppSecret(), suiteTicket);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        }
+        return rsp;
+    }
+
+    //获取应用未激活的企业列表
+    public OapiServiceGetUnactiveCorpResponse getUnactiveCorpResponse(Long appId, String suiteAccessToken) {
+        OapiServiceGetUnactiveCorpResponse rsp = null;
+        try {
+            DingTalkClient client = new DefaultDingTalkClient(
+                    "https://oapi.dingtalk.com/service/get_unactive_corp?suite_access_token=" + suiteAccessToken + "");
+            OapiServiceGetUnactiveCorpRequest req = new OapiServiceGetUnactiveCorpRequest();
+            req.setAppId(appId);
+            rsp = client.execute(req);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        }
+        return rsp;
+    }
+
+    //重新授权未激活应用的企业
+    public OapiServiceReauthCorpResponse getReauthCorpResponse(String appId, String suiteAccessToken, List<String> corpidList) {
+        OapiServiceReauthCorpResponse rsp = null;
+        try {
+            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/service/reauth_corp?suite_access_token=" + suiteAccessToken + "");
+            OapiServiceReauthCorpRequest req = new OapiServiceReauthCorpRequest();
+            req.setAppId(appId);
+            req.setCorpidList(corpidList);
+            rsp = client.execute(req);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        }
+        return rsp;
+    }
+
+    //获取企业开通应用后的授权信息
+    public GetAuthInfoResponse getAuthInfoResponse(String corpId, String xAcsDingtalkAccessToken) {
+        GetAuthInfoResponse rsp = null;
+        String redisKeyPrefix = Constants.DAILY_DING_AUTH + corpId + ":";
+        // 从持久化存储中读取
+        try {
+            GetAuthInfoHeaders getAuthInfoHeaders = new GetAuthInfoHeaders();
+            getAuthInfoHeaders.xAcsDingtalkAccessToken = xAcsDingtalkAccessToken;
+            GetAuthInfoRequest getAuthInfoRequest = new GetAuthInfoRequest()
+                    .setAuthCorpId(corpId);
+            // 第三方企业应用的填写应用SuiteKey和SuiteSecret。
+            // 定制应用填写应用的CustomKey和CustomSecret。
+            rsp = this.createClient2_1_0().getAuthInfoWithOptions(getAuthInfoRequest, getAuthInfoHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+                log.error("getAccessToken failed", err.message);
+            }
+        }
+        return rsp;
+    }
+
+    /***********************************************************************************/
+    //获取jsapiTicket
+    public CreateJsapiTicketResponse createJsapiTicketResponse(String xAcsDingtalkAccessToken) {
+        CreateJsapiTicketResponse rsp = null;
+        try {
+            com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders createJsapiTicketHeaders = new com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders();
+            createJsapiTicketHeaders.xAcsDingtalkAccessToken = xAcsDingtalkAccessToken;
+            rsp = this.createClient2_1_0().createJsapiTicketWithOptions(createJsapiTicketHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return rsp;
+    }
+    //查询个人授权记录
+    public GetPersonalAuthRuleResponse getPersonalAuthRule(String xAcsDingtalkAccessToken) {
+        GetPersonalAuthRuleResponse rsp = null;
+        try {
+            GetPersonalAuthRuleHeaders getPersonalAuthRuleHeaders = new GetPersonalAuthRuleHeaders();
+            getPersonalAuthRuleHeaders.xAcsDingtalkAccessToken = xAcsDingtalkAccessToken;
+            rsp = this.createClient2_1_0().getPersonalAuthRuleWithOptions(getPersonalAuthRuleHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return rsp;
+    }
+
+}

+ 24 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java

@@ -27,6 +27,14 @@ public interface TenantService {
      */
     TenantSimpleRespVO createTenant(@Valid TenantSaveReqVO createReqVO);
 
+    /**
+     * 创建租户(钉钉)
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    void createDingTenant(@Valid TenantDingSaveReqVO createReqVO);
+
     /**
      * 加入租户
      *
@@ -42,6 +50,20 @@ public interface TenantService {
      */
     void updateTenant(@Valid TenantSaveReqVO updateReqVO);
 
+    /**
+     * 更新租户(钉钉)
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDingTenant(@Valid TenantDingSaveReqVO updateReqVO);
+
+    /**
+     * 删除租户(钉钉)
+     *
+     * @param updateReqVO 删除信息
+     */
+    void deleteDingTenant(@Valid TenantDingSaveReqVO updateReqVO);
+
     /**
      * 切换租户
      *
@@ -155,4 +177,6 @@ public interface TenantService {
      */
     void validTenant(Long id);
 
+
+
 }

+ 55 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java

@@ -28,21 +28,20 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuTenantRelateSaveReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
-import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantJoinReqVO;
-import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
-import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO;
-import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
-import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.*;
 import cn.iocoder.yudao.module.system.controller.admin.user.vo.tenant.UserTenantRelateSaveReqVO;
 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
 import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert;
+import cn.iocoder.yudao.module.system.dal.dataobject.dingding.DingUserTenantRelateDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import cn.iocoder.yudao.module.system.dal.mysql.dingding.DingUserTenantRelateMapper;
 import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper;
+import cn.iocoder.yudao.module.system.enums.common.DeletedEnum;
 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
 import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
@@ -127,6 +126,8 @@ public class TenantServiceImpl implements TenantService {
     private BpmModelApi bpmModelApi;
     @Resource
     private DictTypeTenantService dictTypeTenantService;
+    @Resource
+    private DingUserTenantRelateMapper dingUserTenantRelateMapper;
 
     @Override
     public List<Long> getTenantIdList() {
@@ -639,6 +640,55 @@ public class TenantServiceImpl implements TenantService {
         handler.handle(menuIds);
     }
 
+    @Override
+    @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
+    public void createDingTenant(TenantDingSaveReqVO createReqVO) {
+        // 校验租户名称是否重复
+        validTenantNameDuplicate(createReqVO.getName(), null);
+        // 创建租户
+        TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class);
+        tenant.setExpireTime(LocalDateTime.now().plusYears(10));// 默认设置10年有效期
+        tenantMapper.insert(tenant);
+        // 创建管理员与租户关系
+        dingUserTenantRelateMapper.insert(new DingUserTenantRelateDO().setUserId(createReqVO.getManageUserId()).setTenantId(tenant.getId()));
+    }
+
+    @Override
+    @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
+    public void updateDingTenant(TenantDingSaveReqVO updateReqVO) {
+        // 根据租户邀请码查询租户信息
+        TenantDO tenant = tenantMapper.selectByCorpId(updateReqVO.getCorpId());
+        if (tenant == null || tenant.getId() == null) {
+            throw exception(TENANT_NOT_EXISTS);
+        }
+        // 更新租户
+        tenantMapper.updateById(tenant);
+        // 更新管理员与租户关系
+        DingUserTenantRelateDO relateDO = dingUserTenantRelateMapper.selectOne(DingUserTenantRelateDO::getTenantId, tenant.getId());
+        if (relateDO != null) {
+            relateDO.setUserId(updateReqVO.getManageUserId());
+            dingUserTenantRelateMapper.updateById(relateDO);
+        }
+    }
+
+    @Override
+    @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
+    public void deleteDingTenant(TenantDingSaveReqVO updateReqVO) {
+        // 根据租户邀请码查询租户信息
+        TenantDO tenant = tenantMapper.selectByCorpId(updateReqVO.getCorpId());
+        if (tenant == null || tenant.getId() == null) {
+            throw exception(TENANT_NOT_EXISTS);
+        }
+        tenant.setDeleted(DeletedEnum.TRUE.getDeleted());
+        // 删除租户
+        tenantMapper.updateById(tenant);
+        // 删除管理员与租户关系
+        DingUserTenantRelateDO relateDO = dingUserTenantRelateMapper.selectOne(DingUserTenantRelateDO::getTenantId, tenant.getId());
+        if (relateDO != null) {
+            dingUserTenantRelateMapper.deleteById(relateDO);
+        }
+    }
+
     private static boolean isSystemTenant(TenantDO tenant) {
         return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM);
     }

+ 67 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/dingding/DingAppConfig.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.system.util.dingding;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * <p>DingAppConfig 此类用于:应用凭证配置</p>
+ * <p>@remark:</p>
+ */
+@Configuration
+public class DingAppConfig {
+
+    @Value("${dingtalk.app_key}")
+    private String appKey;
+
+    @Value("${dingtalk.app_secret}")
+    private String appSecret;
+
+    @Value("${dingtalk.agent_id}")
+    private String agentId;
+
+    @Value("${dingtalk.corp_id}")
+    private String corpId;
+
+    @Value("${dingtalk.sso_secret}")
+    private String ssoSecret;
+
+    public String getAppKey() {
+        return appKey;
+    }
+
+    public void setAppKey(String appKey) {
+        this.appKey = appKey;
+    }
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+
+    public String getAgentId() {
+        return agentId;
+    }
+
+    public void setAgentId(String agentId) {
+        this.agentId = agentId;
+    }
+
+    public String getCorpId() {
+        return corpId;
+    }
+
+    public void setCorpId(String corpId) {
+        this.corpId = corpId;
+    }
+
+    public String getSsoSecret() {
+        return ssoSecret;
+    }
+
+    public void setSsoSecret(String ssoSecret) {
+        this.ssoSecret = ssoSecret;
+    }
+}

+ 265 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/dingding/RedisCache.java

@@ -0,0 +1,265 @@
+package cn.iocoder.yudao.module.system.util.dingding;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * spring redis 工具类
+ *
+ * @author ruoyi
+ **/
+@SuppressWarnings(value = { "unchecked", "rawtypes" })
+@Component
+public class RedisCache
+{
+    @Autowired
+    public RedisTemplate redisTemplate;
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     */
+    public <T> void setCacheObject(final String key, final T value)
+    {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     */
+    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
+    {
+        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout)
+    {
+        return expire(key, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @param unit 时间单位
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout, final TimeUnit unit)
+    {
+        return redisTemplate.expire(key, timeout, unit);
+    }
+
+    /**
+     * 获取有效时间
+     *
+     * @param key Redis键
+     * @return 有效时间
+     */
+    public long getExpire(final String key)
+    {
+        return redisTemplate.getExpire(key);
+    }
+
+    /**
+     * 判断 key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public Boolean hasKey(String key)
+    {
+        return redisTemplate.hasKey(key);
+    }
+
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key)
+    {
+        ValueOperations<String, T> operation = redisTemplate.opsForValue();
+        return operation.get(key);
+    }
+
+    /**
+     * 删除单个对象
+     *
+     * @param key
+     */
+    public boolean deleteObject(final String key)
+    {
+        return redisTemplate.delete(key);
+    }
+
+    /**
+     * 删除集合对象
+     *
+     * @param collection 多个对象
+     * @return
+     */
+    public boolean deleteObject(final Collection collection)
+    {
+        return redisTemplate.delete(collection) > 0;
+    }
+
+    /**
+     * 缓存List数据
+     *
+     * @param key 缓存的键值
+     * @param dataList 待缓存的List数据
+     * @return 缓存的对象
+     */
+    public <T> long setCacheList(final String key, final List<T> dataList)
+    {
+        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+        return count == null ? 0 : count;
+    }
+
+    /**
+     * 获得缓存的list对象
+     *
+     * @param key 缓存的键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> List<T> getCacheList(final String key)
+    {
+        return redisTemplate.opsForList().range(key, 0, -1);
+    }
+
+    /**
+     * 缓存Set
+     *
+     * @param key 缓存键值
+     * @param dataSet 缓存的数据
+     * @return 缓存数据的对象
+     */
+    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
+    {
+        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
+        Iterator<T> it = dataSet.iterator();
+        while (it.hasNext())
+        {
+            setOperation.add(it.next());
+        }
+        return setOperation;
+    }
+
+    /**
+     * 获得缓存的set
+     *
+     * @param key
+     * @return
+     */
+    public <T> Set<T> getCacheSet(final String key)
+    {
+        return redisTemplate.opsForSet().members(key);
+    }
+
+    /**
+     * 缓存Map
+     *
+     * @param key
+     * @param dataMap
+     */
+    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
+    {
+        if (dataMap != null) {
+            redisTemplate.opsForHash().putAll(key, dataMap);
+        }
+    }
+
+    /**
+     * 获得缓存的Map
+     *
+     * @param key
+     * @return
+     */
+    public <T> Map<String, T> getCacheMap(final String key)
+    {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * 往Hash中存入数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @param value 值
+     */
+    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
+    {
+        redisTemplate.opsForHash().put(key, hKey, value);
+    }
+
+    /**
+     * 获取Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return Hash中的对象
+     */
+    public <T> T getCacheMapValue(final String key, final String hKey)
+    {
+        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
+        return opsForHash.get(key, hKey);
+    }
+
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKeys Hash键集合
+     * @return Hash对象集合
+     */
+    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
+    {
+        return redisTemplate.opsForHash().multiGet(key, hKeys);
+    }
+
+    /**
+     * 删除Hash中的某条数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return 是否成功
+     */
+    public boolean deleteCacheMapValue(final String key, final String hKey)
+    {
+        return redisTemplate.opsForHash().delete(key, hKey) > 0;
+    }
+
+    /**
+     * 获得缓存的基本对象列表
+     *
+     * @param pattern 字符串前缀
+     * @return 对象列表
+     */
+    public Collection<String> keys(final String pattern)
+    {
+        return redisTemplate.keys(pattern);
+    }
+}

+ 13 - 1
yudao-server/src/main/resources/application.yaml

@@ -277,4 +277,16 @@ debug: false
 # 积木报表配置
 jeecg:
   jmreport:
-    saas-mode: tenant
+    saas-mode: tenant
+
+# DingDing配置
+dingtalk:
+  # 应用的唯一标识key。
+  app_key: suiteyjd6ikxpg8629ydr
+  # 应用的密钥。
+  app_secret: znq4BDYYX4vFLxtjfknEoId6j84LT2xpW7gkRTvpVZVoMfbfHXZvZX3cQ2cLQzON
+  # 应用的标识
+  agent_id: 2955933829
+  # 企业ID
+  corp_id: ding870ccf3c4d8fc1bc
+  sso_secret: 123