钉钉开发第三方H5微应用入门详细教程[ISV][免登流程][授权码][HTTP回调推送][识别用户身份][获取用户信息]
转载请注明原文地址:https://www.cnblogs.com/applerosa/p/11509512.html (by lnexin@aliyun.com 世间草木)
此教程注意点:
- 适用于第三方企业开发 H5微应用 形式,非企业内部开发, 非钉钉推荐的“小程序”方式;
- 消息推送模式为 HTTP回调 ,不使用钉钉收费的“RDS钉钉云推送数据源“模式;

开发前准备:
- 关于服务器,有公网服务器最好,没有的话需要 内网穿透工具;
- 调试的时候,由于钉钉的H5微应用调试只能“真机”调试,极其恶心,所以极其建议调试的时候使用 内网穿透工具;
- 关于域名什么的,有没有无所谓,随缘;
其他一些需要明白的:
- 需要自备一个钉钉企业(没有的可以自己创建一个),测试应用无所谓认证不认证,发布的时候相关限制请参阅说明文档;
- H5微应用前端网页获取当前使用的企业的corpId ,需要在 首页URL地址里面 使用 $CORPID$ 占位符 ,然后页面里解析 url 参数,可获得 corpId
- 首页地址后面可以更改,创建时无所谓,回调地址需要搭建好我们自己的服务器,然后填写的时候需要验证有效性,可参考 服务端-示例 里面的 cn.lnexin.dingtalk.controller.SuiteCallbackController::callback(args...)
在我们自身的服务器回调接口搭建好之前, 不能够填写回调地址;
在配置好回调地址前, 不能进行企业授权;
- 在回调里面激活了当前企业, 才算授权成功;
在未授权之前, 手机端,PC端 肯定实在应用里面看不到我们的应用的;
另外本教程重在说明钉钉微应用的免登流程,所以前端部分使用原生的, 最简单的 js, 仅供参考;
目录
一、创建H5微应用
二、搭建微应用服务端 (服务点git示例代码地址: https://gitee.com/lne/ding-server )
三、确认自己的服务端程序运行成功, 并且填写回调地址;
四、实现授权 > 激活流程,将微应用添加到企业客户端的应用列表中;
五、编写简单的微应用首页 (html网页) 进行测试;
六、从安卓端和PC段访问,确认登录流程没有问题;
一. 创建H5微应用
创建完成之后:
在客户端和PC端是看不到这个程序的, 如果想看到这个程序, 就需要 授权> 激活的流程; 而授权>激活 是依赖于我们的服务器的;
添加有效的回调地址是为了让钉钉可以给我们发消息;
而在我们服务器的回调地址程序里面做正确业务的处理, 才能完成授权的流程; 只有当授权完成>激活企业应用了之后, 在客户端 才能看到微应用;
没有有效的回调地址,不在自己服务器里面处理授权>激活流程, 那么你在客户端永远也看不到这个程序;
第一步:填写基础信息

第二步. 配置开发信息,配置完点击创建应用即可。

配置完成之后,信息如下:

在开发者后台添加完大概就这样了, 其他信息:如 回调URL(在服务端搭好之后填写), 首页地址等, 后续可以修改.
二. 搭建微应用服务端
服务端程序可参照 (服务端-示例)
1. 相关配置参数可参照上面 应用基础信息 那张图来一 一对应 .
2. 所有的关键信息 是存储在服务端的, 如我们的suiteKey/suiteSecret/suiteTicket/aesKey/token;
3. 所以和钉钉相关的数据交互都是在服务端,后台完成的, 除了获取 免登授权码;
4. 我们的前端和我们的服务端交互过程中, corpId 由前端获取, 传递给我们;
5. 服务端和钉钉交互所使用的accessToken , 可以每次都去钉钉重新获取, 但是更建议在有效期内, 后端获取一次, 然后存储在前端, 每次的数据交互将token 传递给后端;
6. 钉钉向我们服务器发送请求, 也就是钉钉应用里面的回调地址;
7. 钉钉的所有消息都是通过回调通知我们的, 而且消息的结构是一致的;
下面这里给出一些关键代码: (完整的项目代码可参照上面的示例地址)
1. 钉钉回调请求接收
package cn.lnexin.dingtalk.controller; import cn.lnexin.dingtalk.service.IDingAuthService;
import cn.lnexin.dingtalk.service.ISuiteCallbackService;
import cn.lnexin.dingtalk.utils.JsonTool;
import cn.lnexin.dingtalk.utils.Strings;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import java.util.LinkedHashMap;
import java.util.Map;
import static cn.lnexin.dingtalk.constant.CallbackConstant.*;
/**
* [钉钉] - 钉钉的回调接口, 包含开通,授权,启用,停用,下单等
*
* @author lnexin@foxmail.com
**/
public class SuiteCallbackController {
static Logger logger = LoggerFactory.getLogger(SuiteCallbackController.class);
/**
* 钉钉发过来的数据格式:
* <p>
* http://您服务端部署的IP:您的端口/callback?signature=111108bb8e6dbce3c9671d6fdb69d15066227608×tamp=1783610513&nonce=380320111
* 包含的json数据为:
* {
* "encrypt":"1ojQf0NSvw2WPvW7LijxS8UvISr8pdDP+rXpPbcLGOmIBNbWetRg7IP0vdhVgkVwSoZBJeQwY2zhROsJq/HJ+q6tp1qhl9L1+ccC9ZjKs1wV5bmA9NoAWQiZ+7MpzQVq+j74rJQljdVyBdI/dGOvsnBSCxCVW0ISWX0vn9lYTuuHSoaxwCGylH9xRhYHL9bRDskBc7bO0FseHQQasdfghjkl"
* }
*/
@Autowired
ISuiteCallbackService suiteCallbackService;
/**
* 钉钉服务器推送消息 的地址
*
* @param signature
* @param timestamp
* @param nonce
* @param encryptNode
* @return
*/
@PostMapping(value = "/callback")
public Map<String, String> tempAuthCodeCallback(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestBody JsonNode encryptNode) {
String encryptMsg = encryptNode.get("encrypt").textValue();
String plainText = suiteCallbackService.decryptText(signature, timestamp, nonce, encryptMsg);
JsonNode plainNode = JsonTool.getNode(plainText);
//进入回调事件分支选择
Map<String, String> resultMap = caseProcess(plainNode);
return resultMap;
}
/**
* 根据回调数据类型做不同的业务处理
*
* @param plainNode
* @return
*/
private Map<String, String> caseProcess(JsonNode plainNode) {
Map<String, String> resultMap = new LinkedHashMap<>();
String eventType = plainNode.get("EventType").textValue();
switch (eventType) {
case SUITE_TICKET_CALLBACK_URL_VALIDATE:
logger.info("[callback] 验证回调地址有效性质:{}", plainNode);
resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
break;
case TEMP_AUTH_CODE_ACTIVE:
logger.info("[callback] 企业开通授权:{}", plainNode);
Boolean active = suiteActive(plainNode);
resultMap = suiteCallbackService.encryptText(active ? CALLBACK_RETURN_SUCCESS : ACTIVE_RETURN_FAILURE);
break;
case SUITE_RELIEVE:
logger.info("[callback] 企业解除授权:{}", plainNode);
// 处理解除授权逻辑break;
case CHECK_UPDATE_SUITE_URL:
logger.info("[callback] 在开发者后台修改回调地址:" + plainNode);
resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
break;
case CHECK_CREATE_SUITE_URL:
logger.info("[callback] 检查钉钉向回调URL POST数据解密后是否成功:" + plainNode);
resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
break;
case CONTACT_CHANGE_AUTH:
logger.info("[callback] 通讯录授权范围变更事件:" + plainNode);
break;
case ORG_MICRO_APP_STOP:
logger.info("[callback] 停用应用:" + plainNode);
break;
case ORG_MICRO_APP_RESTORE:
logger.info("[callback] 启用应用:" + plainNode);
break;
case MARKET_BUY:
logger.info("[callback] 用户下单购买事件:" + plainNode);
// 处理其他企业下单购买我们应用的具体逻辑
break;
default:
logger.info("[callback] 未知事件: {} , 内容: {}", eventType, plainNode);
resultMap = suiteCallbackService.encryptText("事件类型未定义, 请联系应用提供方!" + eventType);
break;
}
return resultMap;
}
/**
* 激活应用授权
* tmp_auth_code
*/
private Boolean suiteActive(JsonNode activeNode) {
Boolean isActive = false;
String corpId = activeNode.get("AuthCorpId").textValue();
String tempAuthCode = activeNode.get("AuthCode").textValue();
String suiteToken = suiteCallbackService.getSuiteToken();
String permanentCode = suiteCallbackService.getPermanentCode(suiteToken, tempAuthCode);
if (!Strings.isNullOrEmpty(permanentCode)) {
isActive = suiteCallbackService.activateSuite(suiteToken, corpId, permanentCode);
} else {
logger.error("获取永久授权码出错");
}
return isActive;
}
工具实现:
package cn.lnexin.dingtalk.service.impl; import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiServiceActivateSuiteRequest;
import com.dingtalk.api.request.OapiServiceGetPermanentCodeRequest;
import com.dingtalk.api.request.OapiServiceGetSuiteTokenRequest;
import com.dingtalk.api.response.OapiServiceActivateSuiteResponse;
import com.dingtalk.api.response.OapiServiceGetPermanentCodeResponse;
import com.dingtalk.api.response.OapiServiceGetSuiteTokenResponse;
import com.taobao.api.ApiException;
import cn.lnexin.dingtalk.constant.DingProperties;
import cn.lnexin.dingtalk.encrypt.DingTalkEncryptException;
import cn.lnexin.dingtalk.encrypt.DingTalkEncryptor;
import cn.lnexin.dingtalk.encrypt.Utils;
import cn.lnexin.dingtalk.service.ISuiteCallbackService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import java.util.LinkedHashMap;
import java.util.Map; /**
* 主要完成钉钉回调相关的一些功能
* @author lnexin@foxmail.com
* @Description TODO
**/
@Service
public class SuiteCallbackServiceImpl implements ISuiteCallbackService {
Logger logger = LoggerFactory.getLogger(SuiteCallbackServiceImpl.class); @Autowired
DingProperties dingProperties; @Override
public String decryptText(String signature, String timestamp, String nonce, String encryptMsg) {
String plainText = "";
try {
DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(dingProperties.getSuiteToken(), dingProperties.getEncodingAESKey(), dingProperties.getSuiteKey());
plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, encryptMsg);
} catch (DingTalkEncryptException e) {
logger.error("钉钉消息体解密错误, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, e: {}", signature, timestamp, nonce, encryptMsg, e);
}
logger.debug("钉钉消息体解密, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, 解密结果: {}", signature, timestamp, nonce, encryptMsg, plainText);
return plainText;
} @Override
public Map<String, String> encryptText(String text) {
Map<String, String> resultMap = new LinkedHashMap<>();
try {
DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(dingProperties.getSuiteToken(), dingProperties.getEncodingAESKey(), dingProperties.getSuiteKey());
resultMap = dingTalkEncryptor.getEncryptedMap(text, System.currentTimeMillis(), Utils.getRandomStr(8));
} catch (DingTalkEncryptException e) {
logger.error("钉钉消息体加密,text: {}, e: {}", text, e);
}
logger.debug("钉钉消息体加密,text: {}, resultMap: {}", text, resultMap);
return resultMap;
} /**
* {
* "suite_access_token":"61W3mEpU66027wgNZ_MhGHNQDHnFATkDa9-2llqrMBjUwxRSNPbVsMmyD-yq8wZETSoE5NQgecigDrSHkPtIYA",
* "expires_in":7200
* }
*/
@Override
public String getSuiteToken() {
DingTalkClient client = new DefaultDingTalkClient(DingProperties.url_suite_token);
OapiServiceGetSuiteTokenRequest request = new OapiServiceGetSuiteTokenRequest();
request.setSuiteKey(dingProperties.getSuiteKey());
request.setSuiteSecret(dingProperties.getSuiteSecret());
request.setSuiteTicket(dingProperties.getSuiteTicket()); String accessToken = "";
try {
OapiServiceGetSuiteTokenResponse response = client.execute(request);
accessToken = response != null ? response.getSuiteAccessToken() : "";
} catch (ApiException e) {
logger.error("获取第三方应用凭证suite_access_token出错, code: {}, msg: {}", e.getErrCode(), e.getErrMsg());
}
logger.debug("获取第三方应用凭证suite_access_token, accessToken:{}", accessToken);
return accessToken;
} /**
* {
* "permanent_code": "xxxx",
* "auth_corp_info":
* {
* "corpid": "xxxx",
* "corp_name": "name"
* }
* }
*/
@Override
public String getPermanentCode(String suiteAccessToken, String tempCode) {
StringBuilder url = new StringBuilder();
url.append(DingProperties.url_permanent_code);
url.append("?suite_access_token=").append(suiteAccessToken);
DingTalkClient client = new DefaultDingTalkClient(url.toString());
OapiServiceGetPermanentCodeRequest req = new OapiServiceGetPermanentCodeRequest();
req.setTmpAuthCode(tempCode); String permanentCode = "";
try {
OapiServiceGetPermanentCodeResponse rsp = client.execute(req);
permanentCode = (rsp != null ? rsp.getPermanentCode() : "");
} catch (ApiException e) {
logger.error("获取永久授权码出错, tempCode: {}, code: {}, msg: {}", tempCode, e.getErrCode(), e.getErrMsg());
}
logger.debug("获取永久授权码, tempCode: {}, permanentCode: {}", tempCode, permanentCode);
return permanentCode;
} /**
* 激活企业授权的应用
* {
* "errcode":0,
* "errmsg":"ok"
* }
*/
@Override
public Boolean activateSuite(String suiteAccessToken, String corpId, String permanentCode) {
StringBuilder url = new StringBuilder();
url.append(DingProperties.url_activate_suite);
url.append("?suite_access_token=").append(suiteAccessToken);
DingTalkClient client = new DefaultDingTalkClient(url.toString()); OapiServiceActivateSuiteRequest req = new OapiServiceActivateSuiteRequest();
req.setSuiteKey(dingProperties.getSuiteKey());
req.setAuthCorpid(corpId);
req.setPermanentCode(permanentCode);
boolean isActive = false;
try {
OapiServiceActivateSuiteResponse rsp = client.execute(req);
isActive = rsp.getErrmsg().equals("ok");
} catch (ApiException e) {
logger.error("激活应用的企业授权出错, corpId: {}, permanentCode: {}, code: {}, msg: {}", corpId, permanentCode, e.getErrCode(), e.getErrMsg());
}
logger.debug("激活应用的企业授权, corpId: {}, permanentCode: {}, isActive: {}", corpId, permanentCode, isActive);
return isActive;
} }
SuiteCallbackServiceImpl.java
构建发布程序, 发布到自己的服务器上. 如果使用内网穿透工具, 请忽略;
三. 确认自己的服务端程序运行成功, 并且填写回调地址
根据上面的相关说明将服务端放置在自己的公网服务器也好,或者使用相关的 内网穿透工具 也好 (自行解决)
总之, 现在要有一个可以访问我们 服务端项目的 公网地址
确保你自己的服务器可以使用公网地址访问到,并且成功返回数据;
同时确保:
- 必须有回调地址借口用来接收钉钉发送的消息; (本文示例地址: /ding/callback )
- 必须有一个接收免登授权码和企业corpId 来返回用户信息的接口; (本文示例地址: /ding/login )
比如我自己的测试例子为:
// 这里是我自己的测试地址 http://你的公网地址/ding/config
{
"suiteId": "6707015",
"suiteKey": "suiteqflsxxxxxxxx",
"suiteSecret": "E7TH7H3hGtmhtoGDgq8adJhn0xxxxxxxxxxxBf-GQSTWl8NTs6_",
"suiteToken": "customtoken",
"encodingAESKey": "qwp51j1k8eiudktvnip2dwrkqxxxxxcci",
"suiteTicket": "customTestTicket",
"url_suite_token": "https://oapi.dingtalk.com/service/get_suite_token",
"url_permanent_code": "https://oapi.dingtalk.com/service/get_permanent_code",
"url_activate_suite": "https://oapi.dingtalk.com/service/activate_suite",
"url_get_auth_info": "https://oapi.dingtalk.com/service/get_auth_info",
"url_get_access_token": "https://oapi.dingtalk.com/service/get_corp_token",
"url_get_user_id": "https://oapi.dingtalk.com/user/getuserinfo",
"url_get_user_item": "https://oapi.dingtalk.com/user/get" }
四. 实现授权 > 激活流程,将微应用添加到企业客户端的应用列表中
现在,经过以上步骤, 我们已经准备好的东西有:
- 公网可以访问的服务端地址, 接收钉钉发给我们的消息(回调地址)如: http://ding.lnexin.cn/server/ding/callback,我们自己的登录地址,如: http://ding.lnexin.cn/server/ding/login
- 在钉钉开发者平台创建配置好的一个H5微应用;
- 确保服务端的参数和微应用的基础信息一致;

完成上述步骤,在客户端依旧是没有应用入口的,如:

下面需要在开发者平台进行授权

点击授权之后,会在我们服务器收到钉钉发给我们的消息,我们服务端在经过一系列处理之后,向钉钉发送激活企业的请求,如果激活成功,那么授权就成功了;
点击授权后服务器收到的消息:

如果激活成功,如下所示:

此时授权激活成功,在客户端就有了相关微应用入口。如:

至此,所有前置准备工作已经完成,下面主要是免登和页面jsapi 对接。
五. 编写简单的微应用首页 (html网页) 进行测试
经过前面的步骤,我们现在可以看到微应用,并且拥有了可访问的公网服务端接口地址。
现在需要准备一个前端的公网地址,如果是使用springboot 前后端一体的可以忽略。( 我这里是分离的,大家需要根据自己的情况而定,示例地址如: http://ding.lnexin.cn/ )
下面我们编写一个最简单前端html 网页:

html 前端示例代码如下:(git仓库)
<!DOCTYPE html>
<meta charset="UTF-8">
<html> <head>
<title>H5微应用开发教学</title>
<!-- 这个必须引入的啊,钉钉的前端js SDK, 使用框架的请自行参照开发文档 -->
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.7.13/dingtalk.open.js"></script>
<!-- 这个jquery 想不想引入自己决定,没什么影响 -->
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head> <body>
<hr>
<h1>H5微应用免登教学</h1>
<p>当前页面的url:</p>
<p id="url"></p>
<br>
<p>解析url,获取的corpID:</p>
<p id="corpId"></p>
<br>
<p>SDK初始化获取的code:</p>
<p id="code"></p>
<br>
<p>请求我们服务端,登录返回的结果:</p>
<p id="result"></p>
</body>
<script type="text/javascript">
$(function () {
//钉钉sdk 初始化
// dd.ready参数为回调函数,在环境准备就绪时触发,jsapi的调用需要保证在该回调函数触发后调用,否则无效。
dd.ready(function () {
//获取当前网页的url
//http://ding-web.lnexin.cn/?corpid=ding46a9582af5b7541b35c2fxxxxxxxxxx8f
var currentUrl = document.location.toString()
$("#url").append(currentUrl) // 解析url中包含的corpId
var corpId = currentUrl.split("corpid=")[1];
$("#corpId").append(corpId) //使用SDK 获取免登授权码
dd.runtime.permission.requestAuthCode({
corpId: corpId,
onSuccess: function (result) {
var code = result.code;
$("#code").append(code)
//请求我们服务端的登陆地址
$.get("http://ding.lnexin.cn/server/ding/login?code=" + code + "&corpId=" + corpId, function (response) {
// 我们服务器返回的信息
// 下面代码主要是将返回结果显示出来,可以根据自己的数据结构随便写
for (item in response) {
$("#result").append("<li>" + item + ":" + response[item] + "</li>")
}
if (response.user) {
for (item in response.user) {
$("#result").append("<li>\t[user 属性] " + item + " : " + response.user[item] + "</li>")
}
}
});
}
});
});
}) </script> </html>
index.html
六. 从安卓端和PC段访问, 确认流程没有问题;
差不多第三方企业开发的免登和授权流程已经完毕了,剩下的就是每个应用自己的业务逻辑处理了,这个个人自己解决吧。
钉钉开发第三方H5微应用入门详细教程[ISV][免登流程][授权码][HTTP回调推送][识别用户身份][获取用户信息]的更多相关文章
- 钉钉开发入门,微应用识别用户身份,获取用户免登授权码code,获取用户userid,获取用户详细信息
最近有个需求,在钉钉内,点击微应用,获取用户身份,根据获取到的用户身份去企业内部的用户中心做校验,校验通过,相关子系统直接登陆; 就是在获取这个用户身份的时候,网上的资料七零八落的,找的人烦躁的很,所 ...
- ThinkJS框架入门详细教程(二)新手入门项目
一.准备工作 参考前一篇:ThinkJS框架入门详细教程(一)开发环境 安装thinkJS命令 npm install -g think-cli 监测是否安装成功 thinkjs -v 二.创建项目 ...
- spring入门详细教程(五)
前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...
- Spring入门详细教程(三)
前言 本篇紧接着spring入门详细教程(二),建议阅读本篇前,先阅读第一篇和第二篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/p/101 ...
- Spring入门详细教程(四)
前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...
- Spring入门详细教程(二)
前言 本篇紧接着spring入门详细教程(一),建议阅读本篇前,先阅读第一篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/p/1016553 ...
- Hbuilder第三方插件开发demo--第三方授权分享支付,推送等
<template> <view class="content"> <button id="loginByWX" @click=& ...
- 数据挖掘入门系列教程(八)之使用神经网络(基于pybrain)识别数字手写集MNIST
目录 数据挖掘入门系列教程(八)之使用神经网络(基于pybrain)识别数字手写集MNIST 下载数据集 加载数据集 构建神经网络 反向传播(BP)算法 进行预测 F1验证 总结 参考 数据挖掘入门系 ...
- 数据挖掘入门系列教程(十二)之使用keras构建CNN网络识别CIFAR10
简介 在上一篇博客:数据挖掘入门系列教程(十一点五)之CNN网络介绍中,介绍了CNN的工作原理和工作流程,在这一篇博客,将具体的使用代码来说明如何使用keras构建一个CNN网络来对CIFAR-10数 ...
随机推荐
- 基于opencv -python--银行卡识别
import cv2 def sort_contours(cnts, method="left-to-right"): reverse = False i = 0 if metho ...
- SQL Prompt提示和SQL默认智能提示冲突解决
- TP5 查询mysql数据库时的find_in_set用法
$where['class_id'] = ['in', '$cid_all']; $where['id'] = ['in', $all_user_id];//或这样子 $where['title'] ...
- opensciencegrid - GridFTP 安装
最近配置一个GridFTP 用于测试其传输FTP性能, 在这里简单记录,备忘:使用本教程可以简单起一个GridFTP用于测试服务: 预配置环境: 测试系统:CentOS 7 1806 配置Yum仓库: ...
- Kali设置1920x1080分辨率
root@kali:~# xrandr --newmode -hsync +vsync root@kali:~# xrandr --addmode Virtual1 1920x1080 root@ka ...
- navicat使用教程-PJ
navicat使用教程-PJ Navicat Keygen Patch是一款专门针对Navicat系列软件出品的一款小巧实用,功能强大的破解辅助工具.它能够实现对Navicat全系列版本进行激活破解, ...
- 构建LVS负载均衡集群——NAT模式(最简单方式)
一.装备一台lvs调度器主机要求两个网卡一个为内部局域网ip,一个为公网ip #IP地址设置过程不再重复 [root@localhost ~]# ip a | grep eth0 #内网ip : et ...
- Codeforces Round #594 (Div. 1) A. Ivan the Fool and the Probability Theory 动态规划
A. Ivan the Fool and the Probability Theory Recently Ivan the Fool decided to become smarter and stu ...
- 通过组件实现相同group下字符串拼接
实现效果 组件处理流程如下: 1 使用Sorter组件对ColA进行排序 2 使用expression组件进行如下配置 3 使用aggregate组件进行如下配置 ColA ...
- idea配置pyspark
默认python已经配好,并已经导入idea,只剩下pyspark的安装 1.解压spark-2.1.0-bin-hadoop2.7放入磁盘目录 D:\spark-2.1.0-bin-hadoop2. ...