转载请注明原文地址: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&timestamp=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

构建发布程序, 发布到自己的服务器上. 如果使用内网穿透工具, 请忽略;

三. 确认自己的服务端程序运行成功, 并且填写回调地址

  根据上面的相关说明将服务端放置在自己的公网服务器也好,或者使用相关的 内网穿透工具 也好  (自行解决)

  总之, 现在要有一个可以访问我们 服务端项目的 公网地址 

  确保你自己的服务器可以使用公网地址访问到,并且成功返回数据;

  同时确保:

  1. 必须有回调地址借口用来接收钉钉发送的消息;                                    (本文示例地址:  /ding/callback )
  2. 必须有一个接收免登授权码和企业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" }

  

四. 实现授权 > 激活流程,将微应用添加到企业客户端的应用列表中

  现在,经过以上步骤, 我们已经准备好的东西有:

  1. 公网可以访问的服务端地址, 接收钉钉发给我们的消息(回调地址)如: http://ding.lnexin.cn/server/ding/callback,我们自己的登录地址,如: http://ding.lnexin.cn/server/ding/login
  2. 在钉钉开发者平台创建配置好的一个H5微应用;
  3. 确保服务端的参数和微应用的基础信息一致;

         

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

      

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

  

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

  点击授权后服务器收到的消息:   

  

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

  

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

   

  至此,所有前置准备工作已经完成,下面主要是免登和页面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回调推送][识别用户身份][获取用户信息]的更多相关文章

  1. 钉钉开发入门,微应用识别用户身份,获取用户免登授权码code,获取用户userid,获取用户详细信息

    最近有个需求,在钉钉内,点击微应用,获取用户身份,根据获取到的用户身份去企业内部的用户中心做校验,校验通过,相关子系统直接登陆; 就是在获取这个用户身份的时候,网上的资料七零八落的,找的人烦躁的很,所 ...

  2. ThinkJS框架入门详细教程(二)新手入门项目

    一.准备工作 参考前一篇:ThinkJS框架入门详细教程(一)开发环境 安装thinkJS命令 npm install -g think-cli 监测是否安装成功 thinkjs -v 二.创建项目 ...

  3. spring入门详细教程(五)

    前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...

  4. Spring入门详细教程(三)

    前言 本篇紧接着spring入门详细教程(二),建议阅读本篇前,先阅读第一篇和第二篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/p/101 ...

  5. Spring入门详细教程(四)

    前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...

  6. Spring入门详细教程(二)

    前言 本篇紧接着spring入门详细教程(一),建议阅读本篇前,先阅读第一篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/p/1016553 ...

  7. Hbuilder第三方插件开发demo--第三方授权分享支付,推送等

    <template> <view class="content"> <button id="loginByWX" @click=& ...

  8. 数据挖掘入门系列教程(八)之使用神经网络(基于pybrain)识别数字手写集MNIST

    目录 数据挖掘入门系列教程(八)之使用神经网络(基于pybrain)识别数字手写集MNIST 下载数据集 加载数据集 构建神经网络 反向传播(BP)算法 进行预测 F1验证 总结 参考 数据挖掘入门系 ...

  9. 数据挖掘入门系列教程(十二)之使用keras构建CNN网络识别CIFAR10

    简介 在上一篇博客:数据挖掘入门系列教程(十一点五)之CNN网络介绍中,介绍了CNN的工作原理和工作流程,在这一篇博客,将具体的使用代码来说明如何使用keras构建一个CNN网络来对CIFAR-10数 ...

随机推荐

  1. skip-broken to work around the problem rpm -Va --nofiles --nodigest

    清除yum缓存 yum clean all 重新安装,见结尾[root@localhost ~]# yum install libstdc++.so.6Loaded plugins: fastestm ...

  2. [Go] 使用protobuf进行序列化和反序列化

    先定义消息类型 orders.proto syntax = "proto2"; package message; message Orders { required int32 o ...

  3. STL 中 list 的使用

    list 容器实现了双向链表的数据结构,数据元素是通过链表指针串连成逻辑意义上的线性表,这样,对链表的任一位置的元素进行插入.删除和查找都是极快速的.由于list对象的节点并不要求在一段连续的内存中, ...

  4. June 03rd, 2019. Week 23rd, Monday

    There is no shame in hard work. 努力从来不丢人. Stop complaining about the current work arrangements, just ...

  5. 【CodeForces】CodeForcesRound594 Div1 解题报告

    点此进入比赛 \(A\):Ivan the Fool and the Probability Theory(点此看题面) 大致题意: 给一个\(n\times m\)的矩阵\(01\)染色,使得不存在 ...

  6. 隐马尔可夫模型(HMM)及Viterbi算法

    HMM简介 对于算法爱好者来说,隐马尔可夫模型的大名那是如雷贯耳.那么,这个模型到底长什么样?具体的原理又是什么呢?有什么具体的应用场景呢?本文将会解答这些疑惑. 本文将通过具体形象的例子来引入该模型 ...

  7. 有了faker,再也不用为了构造测试数据而烦恼啦!

    在软件需求.开发.测试过程中,有时候需要使用一些测试数据,针对这种情况,我们一般要么使用已有的系统数据,要么需要手动制造一些数据.在手动制造数据的过程中,可能需要花费大量精力和工作量,此时可以借助Py ...

  8. Java Serializable:明明就一个空的接口嘛

    对于 Java 的序列化,我一直停留在最浅显的认知上——把那个要序列化的类实现 Serializbale 接口就可以了.我不愿意做更深入的研究,因为会用就行了嘛. 但随着时间的推移,见到 Serial ...

  9. PHP echo一个对象报语法错误,为什么?

    为什么直接echo一个对象就会报语法错误,而如果这个对象实现了__toString方法后就可以直接输出呢? 原因是echo本来可以打印一个对象,而且也实现了这个接口,但是PHP对其做了个限制,只有实现 ...

  10. 黄聪:不使用 webpack,vuejs 异步加载模板

    webpack 打包不会玩,整了这么个小玩具 一段 vue 绑定代码,关键点在 gmallComponent 1.异步加载外部 vue 文件(非 .vue) 2.按一定规则拆分 template.sc ...