├── .gitignore ├── README.md ├── backend ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── dingtalk │ │ └── h5app │ │ └── quickstart │ │ ├── Application.java │ │ ├── config │ │ ├── AppConfig.java │ │ └── UrlConstant.java │ │ ├── controller │ │ ├── AuthController.java │ │ └── ContactController.java │ │ ├── domain │ │ ├── ConfigDTO.java │ │ ├── DepartmentDTO.java │ │ ├── ServiceResult.java │ │ └── UserDTO.java │ │ ├── exception │ │ └── DingtalkEncryptException.java │ │ ├── service │ │ └── TokenService.java │ │ └── util │ │ ├── FileUtil.java │ │ └── JsApiSignature.java │ └── resources │ └── application.properties ├── frontend ├── .gitignore ├── README.md ├── package.json ├── pom.xml ├── public │ ├── favicon.ico │ ├── images │ │ └── logo.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ │ ├── contacts.js │ │ ├── deplist.js │ │ ├── header.js │ │ └── user.js │ ├── config.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── serviceWorker.js │ └── setupTests.js └── yarn.lock ├── lib └── taobao-sdk-java-auto_1479188381469-20200201.jar └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | backend/src/main/resources/public 3 | backend/src/main/resources/public/* 4 | 5 | *.log 6 | *.iml 7 | .idea 8 | .idea/* 9 | .DS_Store 10 | 11 | application-dev.properties 12 | node_modules/ 13 | 14 | /Permanent_Data 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 启动步骤 2 | 1. 将工程clone到本地: 3 | `git clone https://github.com/open-dingtalk/h5app-corp-quickstart.git` 4 | 2. 使用IDE导入工程: 5 | 比如eclipse点击`File->import`(推荐使用maven导入) 6 | IDEA点击`File->New->Project from Existing Sources...`, 文件编码都是UTF-8 7 | 3. OA后台创建微应用,并把工程的首页地址/index.html 填到微应用**首页地址**中。 8 | [如何创建微应用?](https://ding-doc.dingtalk.com/doc#/bgb96b/aw3h75) 9 | 4. 打开子模块 backend 中 src/main/resources/application.properties 文件,填入企业微应用的 的APP_KEY,APP_SECRET, CORP_ID, AGENT_ID (参考文档:https://ding-doc.dingtalk.com/doc#/faquestions/dcubhu) 10 | ``` 11 | dingtalk.app_key=APP_KEY 12 | dingtalk.app_secret=APP_SECRET 13 | dingtalk.agent_id=AGENT_ID 14 | dingtalk.corp_id=CORP_ID 15 | ``` 16 | 5. 使用 mvn install 构建 frontend 中必要组建 17 | 5. 部署工程,使用 mvn -DskipTests=true spring-boot:run -pl backend 运行或者IDE中的maven插件运行 18 | 6. 如修改了frontent中代码需要重新执行 mvn install 更新frontend项目内容 19 | 20 | -------------------------------------------------------------------------------- /backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | h5app-corp-quickstart 7 | com.dingtalk 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | backend 13 | 14 | 15 | 16 | com.alibaba 17 | fastjson 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-test 27 | test 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-devtools 36 | true 37 | 38 | 39 | org.apache.httpcomponents 40 | httpclient 41 | 42 | 43 | org.apache.httpcomponents 44 | httpcore 45 | 46 | 47 | org.apache.httpcomponents 48 | httpmime 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 54 | 55 | org.apache.commons 56 | commons-collections4 57 | 58 | 59 | com.taobao.top 60 | top-api-sdk-java 61 | 1.0 62 | system 63 | ${project.basedir}/../lib/taobao-sdk-java-auto_1479188381469-20200201.jar 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-maven-plugin 72 | 73 | true 74 | 75 | 76 | 77 | maven-resources-plugin 78 | 79 | 80 | copy frontend content 81 | generate-resources 82 | 83 | copy-resources 84 | 85 | 86 | src/main/resources/public 87 | true 88 | 89 | 90 | ${project.basedir}/../frontend/build 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/Application.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | 7 | /** 8 | * 启动入口 9 | * 10 | * @author openapi@dingtalk 11 | * @date 2020/2/4 12 | */ 13 | @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 14 | public class Application { 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | /** 7 | * @author openapi@dingtalk 8 | * @date 2020/2/4 9 | */ 10 | @Configuration 11 | public class AppConfig { 12 | @Value("${dingtalk.app_key}") 13 | private String appKey; 14 | 15 | @Value("${dingtalk.app_secret}") 16 | private String appSecret; 17 | 18 | @Value("${dingtalk.agent_id}") 19 | private String agentId; 20 | 21 | @Value("${dingtalk.corp_id}") 22 | private String corpId; 23 | 24 | public String getAppKey() { 25 | return appKey; 26 | } 27 | 28 | public void setAppKey(String appKey) { 29 | this.appKey = appKey; 30 | } 31 | 32 | public String getAppSecret() { 33 | return appSecret; 34 | } 35 | 36 | public void setAppSecret(String appSecret) { 37 | this.appSecret = appSecret; 38 | } 39 | 40 | public String getAgentId() { 41 | return agentId; 42 | } 43 | 44 | public void setAgentId(String agentId) { 45 | this.agentId = agentId; 46 | } 47 | 48 | public String getCorpId() { 49 | return corpId; 50 | } 51 | 52 | public void setCorpId(String corpId) { 53 | this.corpId = corpId; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/config/UrlConstant.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.config; 2 | 3 | /** 4 | * top地址相关配置 5 | * 6 | * @author openapi@dingtalk 7 | * @date 2020/2/4 8 | */ 9 | public class UrlConstant { 10 | private static final String HOST = "https://oapi.dingtalk.com"; 11 | 12 | /** 13 | * 钉钉网关 gettoken 地址 14 | */ 15 | public static final String URL_GET_TOKEN = HOST + "/gettoken"; 16 | 17 | /** 18 | * 获取 jsapi_ticket 地址 19 | */ 20 | public static final String URL_GET_JSTICKET = HOST + "/get_jsapi_ticket"; 21 | 22 | /** 23 | * 获取用户在企业内 userId 的接口URL 24 | */ 25 | public static final String URL_GET_USER_INFO = HOST + "/user/getuserinfo"; 26 | 27 | /** 28 | * 获取用户姓名的接口URL 29 | */ 30 | public static final String URL_USER_GET = HOST + "/user/get"; 31 | 32 | /** 33 | * 获取部门列表接口URL 34 | */ 35 | public static final String URL_DEPARTMENT_LIST = HOST + "/department/list"; 36 | 37 | /** 38 | * 获取部门用户接口URL 39 | */ 40 | public static final String URL_USER_SIMPLELIST = HOST + "/user/simplelist"; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.controller; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import com.dingtalk.api.DefaultDingTalkClient; 6 | import com.dingtalk.api.DingTalkClient; 7 | import com.dingtalk.api.request.OapiUserGetRequest; 8 | import com.dingtalk.api.request.OapiUserGetuserinfoRequest; 9 | import com.dingtalk.api.response.OapiUserGetResponse; 10 | import com.dingtalk.api.response.OapiUserGetuserinfoResponse; 11 | import com.dingtalk.h5app.quickstart.config.AppConfig; 12 | import com.dingtalk.h5app.quickstart.domain.ConfigDTO; 13 | import com.dingtalk.h5app.quickstart.domain.ServiceResult; 14 | import com.dingtalk.h5app.quickstart.domain.UserDTO; 15 | import com.dingtalk.h5app.quickstart.exception.DingtalkEncryptException; 16 | import com.dingtalk.h5app.quickstart.service.TokenService; 17 | import com.dingtalk.h5app.quickstart.util.JsApiSignature; 18 | import com.taobao.api.ApiException; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.web.bind.annotation.CrossOrigin; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.PostMapping; 25 | import org.springframework.web.bind.annotation.RequestParam; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_GET_USER_INFO; 29 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_USER_GET; 30 | 31 | /** 32 | * 微应用QuickStart示例,免登功能 33 | * 34 | * @author openapi@dingtalk 35 | * @date 2020/2/4 36 | */ 37 | @RestController 38 | @CrossOrigin("*") // NOTE:此处仅为本地调试使用,为避免安全风险,生产环境请勿设置CORS为 '*' 39 | public class AuthController { 40 | private static final Logger log = LoggerFactory.getLogger(AuthController.class); 41 | 42 | private TokenService tokenService; 43 | private AppConfig appConfig; 44 | 45 | @Autowired 46 | public AuthController( 47 | TokenService tokenService, 48 | AppConfig appConfig 49 | ) { 50 | this.tokenService = tokenService; 51 | this.appConfig = appConfig; 52 | } 53 | 54 | /** 55 | * 欢迎页面,通过 /welcome 访问,判断后端服务是否启动 56 | * 57 | * @return 字符串 welcome 58 | */ 59 | @GetMapping("/welcome") 60 | public String welcome() { 61 | return "welcome"; 62 | } 63 | 64 | /** 65 | * 钉钉用户登录,显示当前登录用户的userId和名称 66 | * 67 | * @param authCode 免登临时authCode 68 | * @return 当前用户 69 | */ 70 | @PostMapping("/login") 71 | public ServiceResult login(@RequestParam String authCode) { 72 | String accessToken; 73 | 74 | // 获取accessToken 75 | ServiceResult accessTokenSr = tokenService.getAccessToken(); 76 | if (!accessTokenSr.isSuccess()) { 77 | return ServiceResult.failure(accessTokenSr.getCode(), accessTokenSr.getMessage()); 78 | } 79 | accessToken = accessTokenSr.getResult(); 80 | 81 | // 获取用户userId 82 | ServiceResult userIdSr = getUserInfo(accessToken, authCode); 83 | if (!userIdSr.isSuccess()) { 84 | return ServiceResult.failure(userIdSr.getCode(), userIdSr.getMessage()); 85 | } 86 | 87 | // 获取用户详情 88 | return getUser(accessToken, userIdSr.getResult()); 89 | } 90 | 91 | /** 92 | * 访问/user/getuserinfo接口获取用户userId 93 | * 94 | * @param accessToken access_token 95 | * @param authCode 临时授权码 96 | * @return 用户userId或错误信息 97 | */ 98 | private ServiceResult getUserInfo(String accessToken, String authCode) { 99 | DingTalkClient client = new DefaultDingTalkClient(URL_GET_USER_INFO); 100 | OapiUserGetuserinfoRequest request = new OapiUserGetuserinfoRequest(); 101 | request.setCode(authCode); 102 | request.setHttpMethod("GET"); 103 | 104 | OapiUserGetuserinfoResponse response; 105 | try { 106 | response = client.execute(request, accessToken); 107 | } catch (ApiException e) { 108 | log.error("Failed to {}", URL_GET_USER_INFO, e); 109 | return ServiceResult.failure(e.getErrCode(), "Failed to getUserInfo: " + e.getErrMsg()); 110 | } 111 | if (!response.isSuccess()) { 112 | return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); 113 | } 114 | 115 | return ServiceResult.success(response.getUserid()); 116 | } 117 | 118 | /** 119 | * 访问/user/get 获取用户名称 120 | * 121 | * @param accessToken access_token 122 | * @param userId 用户userId 123 | * @return 用户名称或错误信息 124 | */ 125 | private ServiceResult getUser(String accessToken, String userId) { 126 | DingTalkClient client = new DefaultDingTalkClient(URL_USER_GET); 127 | OapiUserGetRequest request = new OapiUserGetRequest(); 128 | request.setUserid(userId); 129 | request.setHttpMethod("GET"); 130 | 131 | OapiUserGetResponse response; 132 | try { 133 | response = client.execute(request, accessToken); 134 | } catch (ApiException e) { 135 | log.error("Failed to {}", URL_USER_GET, e); 136 | return ServiceResult.failure(e.getErrCode(), "Failed to getUserName: " + e.getErrMsg()); 137 | } 138 | 139 | UserDTO user = new UserDTO(); 140 | user.setName(response.getName()); 141 | user.setUserid(response.getUserid()); 142 | user.setAvatar(response.getAvatar()); 143 | 144 | return ServiceResult.success(user); 145 | } 146 | 147 | @PostMapping("/config") 148 | public ServiceResult config(@RequestParam String url) { 149 | ConfigDTO config = new ConfigDTO(); 150 | 151 | ServiceResult jsTicketSr = tokenService.getJsTicket(); 152 | if (!jsTicketSr.isSuccess()) { 153 | return ServiceResult.failure(jsTicketSr.getCode(), jsTicketSr.getMessage()); 154 | } 155 | 156 | config.setAgentId(appConfig.getAgentId()); 157 | config.setCorpId(appConfig.getCorpId()); 158 | config.setJsticket(jsTicketSr.getResult()); 159 | config.setNonceStr(JsApiSignature.genNonce()); 160 | config.setTimeStamp(System.currentTimeMillis() / 1000); 161 | String sign; 162 | try { 163 | sign = JsApiSignature.sign(url, config.getNonceStr(), config.getTimeStamp(), config.getJsticket()); 164 | } catch (DingtalkEncryptException e) { 165 | return ServiceResult.failure(e.getCode().toString(), e.getMessage()); 166 | } 167 | config.setSignature(sign); 168 | return ServiceResult.success(config); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/controller/ContactController.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.controller; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.dingtalk.api.DefaultDingTalkClient; 9 | import com.dingtalk.api.DingTalkClient; 10 | import com.dingtalk.api.request.OapiDepartmentListRequest; 11 | import com.dingtalk.api.request.OapiUserSimplelistRequest; 12 | import com.dingtalk.api.response.OapiDepartmentListResponse; 13 | import com.dingtalk.api.response.OapiDepartmentListResponse.Department; 14 | import com.dingtalk.api.response.OapiUserSimplelistResponse; 15 | import com.dingtalk.api.response.OapiUserSimplelistResponse.Userlist; 16 | import com.dingtalk.h5app.quickstart.config.AppConfig; 17 | import com.dingtalk.h5app.quickstart.domain.DepartmentDTO; 18 | import com.dingtalk.h5app.quickstart.domain.ServiceResult; 19 | import com.dingtalk.h5app.quickstart.domain.UserDTO; 20 | import com.dingtalk.h5app.quickstart.service.TokenService; 21 | import com.taobao.api.ApiException; 22 | import org.apache.commons.collections4.CollectionUtils; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.web.bind.annotation.CrossOrigin; 27 | import org.springframework.web.bind.annotation.GetMapping; 28 | import org.springframework.web.bind.annotation.RequestParam; 29 | import org.springframework.web.bind.annotation.RestController; 30 | 31 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_DEPARTMENT_LIST; 32 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_USER_SIMPLELIST; 33 | 34 | /** 35 | * 微应用QuickStart示例,访问联系人API 36 | * 37 | * @author openapi@dingtalk 38 | * @date 2020/2/4 39 | */ 40 | @RestController 41 | @CrossOrigin("*") // NOTE:此处仅为本地调试使用,为避免安全风险,生产环境请勿设置CORS为 '*' 42 | public class ContactController { 43 | private static final Logger log = LoggerFactory.getLogger(ContactController.class); 44 | 45 | private TokenService tokenService; 46 | private AppConfig appConfig; 47 | 48 | @Autowired 49 | public ContactController( 50 | TokenService tokenService, 51 | AppConfig appConfig 52 | ) { 53 | this.tokenService = tokenService; 54 | this.appConfig = appConfig; 55 | } 56 | 57 | @GetMapping("/department/list") 58 | public ServiceResult> listDepartment( 59 | @RequestParam(value = "id", required = false, defaultValue = "1") String id 60 | ) { 61 | String accessToken; 62 | // 获取accessToken 63 | ServiceResult accessTokenSr = tokenService.getAccessToken(); 64 | if (!accessTokenSr.isSuccess()) { 65 | return ServiceResult.failure(accessTokenSr.getCode(), accessTokenSr.getMessage()); 66 | } 67 | accessToken = accessTokenSr.getResult(); 68 | 69 | DingTalkClient client = new DefaultDingTalkClient(URL_DEPARTMENT_LIST); 70 | OapiDepartmentListRequest request = new OapiDepartmentListRequest(); 71 | request.setId(id); 72 | request.setHttpMethod("GET"); 73 | 74 | OapiDepartmentListResponse response; 75 | try { 76 | response = client.execute(request, accessToken); 77 | } catch (ApiException e) { 78 | log.error("Failed to {}", URL_DEPARTMENT_LIST, e); 79 | return ServiceResult.failure(e.getErrCode(), "Failed to listDepartment: " + e.getErrMsg()); 80 | } 81 | if (!response.isSuccess()) { 82 | return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); 83 | } 84 | 85 | if (CollectionUtils.isNotEmpty(response.getDepartment())) { 86 | List results = new ArrayList<>(response.getDepartment().size()); 87 | for (Department department : response.getDepartment()) { 88 | DepartmentDTO departmentDTO = new DepartmentDTO(); 89 | departmentDTO.setId(department.getId()); 90 | departmentDTO.setName(department.getName()); 91 | departmentDTO.setCreateDeptGroup(department.getCreateDeptGroup()); 92 | departmentDTO.setAutoAddUser(department.getAutoAddUser()); 93 | departmentDTO.setParentid(department.getParentid()); 94 | results.add(departmentDTO); 95 | } 96 | return ServiceResult.success(results); 97 | } 98 | return ServiceResult.success(Collections.emptyList()); 99 | } 100 | 101 | @GetMapping("/user/simplelist") 102 | public ServiceResult> listDepartmentUsers( 103 | @RequestParam("department_id") Long id, 104 | @RequestParam(name = "offset", required = false, defaultValue = "1") Long offset, 105 | @RequestParam(name = "size", required = false, defaultValue = "50") Long size, 106 | @RequestParam(name = "order", required = false, defaultValue = "entry_desc") String order 107 | ) { 108 | String accessToken; 109 | // 获取accessToken 110 | ServiceResult accessTokenSr = tokenService.getAccessToken(); 111 | if (!accessTokenSr.isSuccess()) { 112 | return ServiceResult.failure(accessTokenSr.getCode(), accessTokenSr.getMessage()); 113 | } 114 | accessToken = accessTokenSr.getResult(); 115 | 116 | DingTalkClient client = new DefaultDingTalkClient(URL_USER_SIMPLELIST); 117 | OapiUserSimplelistRequest request = new OapiUserSimplelistRequest(); 118 | request.setDepartmentId(id); 119 | request.setOffset(offset); 120 | request.setSize(size); 121 | request.setOrder(order); 122 | request.setHttpMethod("GET"); 123 | 124 | OapiUserSimplelistResponse response; 125 | try { 126 | response = client.execute(request, accessToken); 127 | } catch (ApiException e) { 128 | log.error("Failed to {}", URL_DEPARTMENT_LIST, e); 129 | return ServiceResult.failure(e.getErrCode(), "Failed to listDepartment: " + e.getErrMsg()); 130 | } 131 | if (!response.isSuccess()) { 132 | return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); 133 | } 134 | 135 | if (CollectionUtils.isNotEmpty(response.getUserlist())) { 136 | List results = new ArrayList<>(response.getUserlist().size()); 137 | for (Userlist userlist : response.getUserlist()) { 138 | UserDTO user = new UserDTO(); 139 | user.setUserid(userlist.getUserid()); 140 | user.setName(userlist.getName()); 141 | results.add(user); 142 | } 143 | return ServiceResult.success(results); 144 | } 145 | return ServiceResult.success(Collections.emptyList()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/domain/ConfigDTO.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | /** 8 | * JSAPI鉴权签名信息 9 | * 10 | * @author openapi@dingtalk 11 | * @date 2020/2/4 12 | */ 13 | public class ConfigDTO implements Serializable { 14 | private String jsticket; 15 | /** 16 | * 随机串,自己定义 17 | */ 18 | private String nonceStr; 19 | /** 20 | * 应用的标识 21 | */ 22 | private String agentId; 23 | /** 24 | * 时间戳 25 | */ 26 | private Long timeStamp; 27 | /** 28 | * 企业ID 29 | */ 30 | private String corpId; 31 | /** 32 | * 签名 33 | */ 34 | private String signature; 35 | /** 36 | * 选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持 37 | */ 38 | private Integer type; 39 | 40 | public ConfigDTO() { 41 | type = 0; 42 | } 43 | 44 | public String getJsticket() { 45 | return jsticket; 46 | } 47 | 48 | public void setJsticket(String jsticket) { 49 | this.jsticket = jsticket; 50 | } 51 | 52 | public String getNonceStr() { 53 | return nonceStr; 54 | } 55 | 56 | public void setNonceStr(String nonceStr) { 57 | this.nonceStr = nonceStr; 58 | } 59 | 60 | public String getAgentId() { 61 | return agentId; 62 | } 63 | 64 | public void setAgentId(String agentId) { 65 | this.agentId = agentId; 66 | } 67 | 68 | public Long getTimeStamp() { 69 | return timeStamp; 70 | } 71 | 72 | public void setTimeStamp(Long timeStamp) { 73 | this.timeStamp = timeStamp; 74 | } 75 | 76 | public String getCorpId() { 77 | return corpId; 78 | } 79 | 80 | public void setCorpId(String corpId) { 81 | this.corpId = corpId; 82 | } 83 | 84 | public String getSignature() { 85 | return signature; 86 | } 87 | 88 | public void setSignature(String signature) { 89 | this.signature = signature; 90 | } 91 | 92 | public Integer getType() { 93 | return type; 94 | } 95 | 96 | public void setType(Integer type) { 97 | this.type = type; 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return ToStringBuilder.reflectionToString(this); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/domain/DepartmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | /** 8 | * 部门查询返回结果 9 | * 10 | * @author openapi@dingtalk 11 | * @date 2020/2/6 12 | */ 13 | public class DepartmentDTO implements Serializable { 14 | /** 15 | * 部门id 16 | */ 17 | private Long id; 18 | /** 19 | * 部门名称 20 | */ 21 | private String name; 22 | /** 23 | * 父部门id,根部门为1 24 | */ 25 | private Long parentid; 26 | /** 27 | * 是否同步创建一个关联此部门的企业群,true表示是,false表示不是 28 | */ 29 | private Boolean createDeptGroup; 30 | /** 31 | * 当群已经创建后,是否有新人加入部门会自动加入该群, true表示是,false表示不是 32 | */ 33 | private Boolean autoAddUser; 34 | 35 | public Long getId() { 36 | return id; 37 | } 38 | 39 | public void setId(Long id) { 40 | this.id = id; 41 | } 42 | 43 | public String getName() { 44 | return name; 45 | } 46 | 47 | public void setName(String name) { 48 | this.name = name; 49 | } 50 | 51 | public Long getParentid() { 52 | return parentid; 53 | } 54 | 55 | public void setParentid(Long parentid) { 56 | this.parentid = parentid; 57 | } 58 | 59 | public Boolean getCreateDeptGroup() { 60 | return createDeptGroup; 61 | } 62 | 63 | public void setCreateDeptGroup(Boolean createDeptGroup) { 64 | this.createDeptGroup = createDeptGroup; 65 | } 66 | 67 | public Boolean getAutoAddUser() { 68 | return autoAddUser; 69 | } 70 | 71 | public void setAutoAddUser(Boolean autoAddUser) { 72 | this.autoAddUser = autoAddUser; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return ToStringBuilder.reflectionToString(this); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/domain/ServiceResult.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * service层返回对象列表封装 7 | * 8 | * @author openapi@dingtalk 9 | * @date 2020/2/4 10 | */ 11 | public class ServiceResult implements Serializable { 12 | 13 | private boolean success = false; 14 | 15 | private String code; 16 | 17 | private String message; 18 | 19 | private T result; 20 | 21 | private ServiceResult() { 22 | } 23 | 24 | public static ServiceResult success(T result) { 25 | ServiceResult item = new ServiceResult(); 26 | item.success = true; 27 | item.result = result; 28 | item.code = "0"; 29 | item.message = "success"; 30 | return item; 31 | } 32 | 33 | public static ServiceResult failure(String errorCode, String errorMessage) { 34 | ServiceResult item = new ServiceResult(); 35 | item.success = false; 36 | item.code = errorCode; 37 | item.message = errorMessage; 38 | return item; 39 | } 40 | 41 | public static ServiceResult failure(String errorCode) { 42 | ServiceResult item = new ServiceResult(); 43 | item.success = false; 44 | item.code = errorCode; 45 | item.message = "failure"; 46 | return item; 47 | } 48 | 49 | public boolean hasResult() { 50 | return result != null; 51 | } 52 | 53 | public boolean isSuccess() { 54 | return success; 55 | } 56 | 57 | public T getResult() { 58 | return result; 59 | } 60 | 61 | public String getCode() { 62 | return code; 63 | } 64 | 65 | public String getMessage() { 66 | return message; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/domain/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | /** 8 | * @author openapi@dingtalk 9 | * @date 2020/2/4 10 | */ 11 | public class UserDTO implements Serializable { 12 | /** 13 | * 用户userId 14 | */ 15 | private String userid; 16 | /** 17 | * 用户名称 18 | */ 19 | private String name; 20 | /** 21 | * 头像URL 22 | */ 23 | private String avatar; 24 | 25 | public String getUserid() { 26 | return userid; 27 | } 28 | 29 | public void setUserid(String userid) { 30 | this.userid = userid; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public String getAvatar() { 42 | return avatar; 43 | } 44 | 45 | public void setAvatar(String avatar) { 46 | this.avatar = avatar; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return ToStringBuilder.reflectionToString(this); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/exception/DingtalkEncryptException.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.exception; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * 钉钉开放平台加解密异常类 8 | * 9 | * @author openapi@dingtalk 10 | * @date 2020/2/4 11 | */ 12 | public class DingtalkEncryptException extends Exception { 13 | /** 14 | * 成功 15 | */ 16 | public static final int SUCCESS = 0; 17 | /** 18 | * 加密明文文本非法 19 | */ 20 | public final static int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001; 21 | /** 22 | * 加密时间戳参数非法 23 | */ 24 | public final static int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002; 25 | /** 26 | * 加密随机字符串参数非法 27 | */ 28 | public final static int ENCRYPTION_NONCE_ILLEGAL = 900003; 29 | /** 30 | * 不合法的aeskey 31 | */ 32 | public final static int AES_KEY_ILLEGAL = 900004; 33 | /** 34 | * 签名不匹配 35 | */ 36 | public final static int SIGNATURE_NOT_MATCH = 900005; 37 | /** 38 | * 计算签名错误 39 | */ 40 | public final static int COMPUTE_SIGNATURE_ERROR = 900006; 41 | /** 42 | * 计算加密文字错误 43 | */ 44 | public final static int COMPUTE_ENCRYPT_TEXT_ERROR = 900007; 45 | /** 46 | * 计算解密文字错误 47 | */ 48 | public final static int COMPUTE_DECRYPT_TEXT_ERROR = 900008; 49 | /** 50 | * 计算解密文字长度不匹配 51 | */ 52 | public final static int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009; 53 | /** 54 | * 计算解密文字corpid不匹配 55 | */ 56 | public final static int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010; 57 | 58 | private static Map msgMap = new HashMap<>(); 59 | 60 | static { 61 | msgMap.put(SUCCESS, "成功"); 62 | msgMap.put(ENCRYPTION_PLAINTEXT_ILLEGAL, "加密明文文本非法"); 63 | msgMap.put(ENCRYPTION_TIMESTAMP_ILLEGAL, "加密时间戳参数非法"); 64 | msgMap.put(ENCRYPTION_NONCE_ILLEGAL, "加密随机字符串参数非法"); 65 | msgMap.put(SIGNATURE_NOT_MATCH, "签名不匹配"); 66 | msgMap.put(COMPUTE_SIGNATURE_ERROR, "签名计算失败"); 67 | msgMap.put(AES_KEY_ILLEGAL, "不合法的aes key"); 68 | msgMap.put(COMPUTE_ENCRYPT_TEXT_ERROR, "计算加密文字错误"); 69 | msgMap.put(COMPUTE_DECRYPT_TEXT_ERROR, "计算解密文字错误"); 70 | msgMap.put(COMPUTE_DECRYPT_TEXT_LENGTH_ERROR, "计算解密文字长度不匹配"); 71 | msgMap.put(COMPUTE_DECRYPT_TEXT_CORPID_ERROR, "计算解密文字corpid或者suiteKey不匹配"); 72 | } 73 | 74 | private Integer code; 75 | 76 | public DingtalkEncryptException(Integer exceptionCode) { 77 | this(exceptionCode, null); 78 | } 79 | 80 | public DingtalkEncryptException(Integer exceptionCode, Throwable e) { 81 | super(msgMap.get(exceptionCode), e); 82 | this.code = exceptionCode; 83 | } 84 | 85 | public Integer getCode() { 86 | return code; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.service; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | 5 | import com.dingtalk.api.DefaultDingTalkClient; 6 | import com.dingtalk.api.request.OapiGetJsapiTicketRequest; 7 | import com.dingtalk.api.request.OapiGettokenRequest; 8 | import com.dingtalk.api.response.OapiGetJsapiTicketResponse; 9 | import com.dingtalk.api.response.OapiGettokenResponse; 10 | import com.dingtalk.h5app.quickstart.config.AppConfig; 11 | import com.dingtalk.h5app.quickstart.domain.ServiceResult; 12 | import com.dingtalk.h5app.quickstart.util.FileUtil; 13 | import com.taobao.api.ApiException; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | 19 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_GET_JSTICKET; 20 | import static com.dingtalk.h5app.quickstart.config.UrlConstant.URL_GET_TOKEN; 21 | 22 | /** 23 | * 获取access_token 和 jsTicket方法 24 | * 25 | * @author openapi@dingtalk 26 | * @date 2020/2/4 27 | */ 28 | @Service 29 | public class TokenService { 30 | private static final Logger log = LoggerFactory.getLogger(TokenService.class); 31 | /** 32 | * 缓存时间:一小时50分钟 33 | */ 34 | private static final long CACHE_TTL = 60 * 55 * 2 * 1000; 35 | 36 | private AppConfig appConfig; 37 | 38 | @Autowired 39 | public TokenService(AppConfig appConfig) { 40 | this.appConfig = appConfig; 41 | } 42 | 43 | /** 44 | * 在此方法中,为了避免频繁获取access_token, 45 | * 在距离上一次获取access_token时间在两个小时之内的情况, 46 | * 将直接从持久化存储中读取access_token 47 | * 48 | * 因为access_token和jsapi_ticket的过期时间都是7200秒 49 | * 所以在获取access_token的同时也去获取了jsapi_ticket 50 | * 注:jsapi_ticket是在前端页面JSAPI做权限验证配置的时候需要使用的 51 | * 具体信息请查看开发者文档--权限验证配置 52 | * 53 | * @return accessToken 或错误信息 54 | */ 55 | public ServiceResult getAccessToken() { 56 | // 从持久化存储中读取 57 | String accessToken = getFromCache("accessToken", "access_token"); 58 | if (accessToken != null) { 59 | return ServiceResult.success(accessToken); 60 | } 61 | 62 | DefaultDingTalkClient client = new DefaultDingTalkClient(URL_GET_TOKEN); 63 | OapiGettokenRequest request = new OapiGettokenRequest(); 64 | OapiGettokenResponse response; 65 | 66 | request.setAppkey(appConfig.getAppKey()); 67 | request.setAppsecret(appConfig.getAppSecret()); 68 | request.setHttpMethod("GET"); 69 | 70 | try { 71 | response = client.execute(request); 72 | } catch (ApiException e) { 73 | log.error("getAccessToken failed", e); 74 | return ServiceResult.failure(e.getErrCode(), e.getErrMsg()); 75 | } 76 | 77 | accessToken = response.getAccessToken(); 78 | putToCache("accessToken", "access_token", accessToken); 79 | return ServiceResult.success(accessToken); 80 | } 81 | 82 | /** 83 | * 获取JSTicket, 用于js的签名计算 84 | * 正常的情况下,jsapi_ticket的有效期为7200秒,所以开发者需要在某个地方设计一个定时器,定期去更新jsapi_ticket 85 | * 86 | * @return jsTicket或错误信息 87 | */ 88 | public ServiceResult getJsTicket() { 89 | // 从持久化存储中读取 90 | String ticket = getFromCache("jsticket", "ticket"); 91 | if (ticket != null) { 92 | return ServiceResult.success(ticket); 93 | } 94 | 95 | String accessToken; 96 | ServiceResult tokenSr = getAccessToken(); 97 | if (!tokenSr.isSuccess()) { 98 | return ServiceResult.failure(tokenSr.getCode(), tokenSr.getMessage()); 99 | } 100 | accessToken = tokenSr.getResult(); 101 | 102 | DefaultDingTalkClient client = new DefaultDingTalkClient(URL_GET_JSTICKET); 103 | OapiGetJsapiTicketRequest request = new OapiGetJsapiTicketRequest(); 104 | OapiGetJsapiTicketResponse response; 105 | 106 | request.setHttpMethod("GET"); 107 | 108 | try { 109 | response = client.execute(request, accessToken); 110 | } catch (ApiException e) { 111 | log.error("getAccessToken failed", e); 112 | return ServiceResult.failure(e.getErrCode(), e.getErrMsg()); 113 | } 114 | 115 | if (!response.isSuccess()) { 116 | return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); 117 | } 118 | ticket = response.getTicket(); 119 | putToCache("jsticket", "ticket", ticket); 120 | return ServiceResult.success(ticket); 121 | } 122 | 123 | /** 124 | * 模拟从持久化存储中获取token并检查是否已过期 125 | * 126 | * @param section 存储key 127 | * @param field token字段名 128 | * @return token值 或 null (过期或未查到) 129 | */ 130 | private String getFromCache(String section, String field) { 131 | JSONObject o = (JSONObject) FileUtil.getValue(section, appConfig.getAppKey()); 132 | if (o != null && System.currentTimeMillis() - o.getLong("begin_time") <= CACHE_TTL) { 133 | return o.getString(field); 134 | } 135 | return null; 136 | } 137 | 138 | private void putToCache(String section, String field, String value) { 139 | JSONObject fieldObj = new JSONObject(2); 140 | fieldObj.put(field, value); 141 | fieldObj.put("begin_time", System.currentTimeMillis()); 142 | JSONObject wrapperObj = new JSONObject(1); 143 | wrapperObj.put(appConfig.getAppKey(), fieldObj); 144 | 145 | FileUtil.write2File(wrapperObj, section); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.File; 6 | import java.io.FileReader; 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | import com.alibaba.fastjson.JSON; 14 | import com.alibaba.fastjson.JSONObject; 15 | 16 | import org.apache.http.util.TextUtils; 17 | 18 | /** 19 | * 使用磁盘文件模拟accessToken和jsTicket的数据持久化 20 | * 正式项目请是写入到Mysql等持久化存储 21 | * 22 | * @author openapi@dingtalk 23 | * @date 2020/2/4 24 | */ 25 | public class FileUtil { 26 | private static final String FILEPATH = "Permanent_Data"; 27 | 28 | /** 29 | * 将json写入文件 30 | * 31 | * @param json 需要写入的json对象 32 | * @param fileName 文件名称 33 | */ 34 | public synchronized static void write2File(Object json, String fileName) { 35 | BufferedWriter writer = null; 36 | File filePath = new File(FILEPATH); 37 | JSONObject eJson = null; 38 | 39 | if (!filePath.exists() && !filePath.isDirectory()) { 40 | filePath.mkdirs(); 41 | } 42 | 43 | File file = new File(FILEPATH + File.separator + fileName + ".xml"); 44 | System.out.println("path:" + file.getPath() + " abs path:" + file.getAbsolutePath()); 45 | if (!file.exists()) { 46 | try { 47 | file.createNewFile(); 48 | } catch (Exception e) { 49 | System.out.println("createNewFile,出现异常:"); 50 | e.printStackTrace(); 51 | } 52 | } else { 53 | eJson = read2JSON(fileName); 54 | } 55 | 56 | try { 57 | writer = new BufferedWriter(new FileWriter(file)); 58 | 59 | if (eJson == null) { 60 | writer.write(json.toString()); 61 | } else { 62 | Object[] array = ((JSONObject)json).keySet().toArray(); 63 | for (Object o : array) { 64 | eJson.put(o.toString(), ((JSONObject)json).get(o.toString())); 65 | } 66 | 67 | writer.write(eJson.toString()); 68 | } 69 | 70 | } catch (IOException e) { 71 | e.printStackTrace(); 72 | } finally { 73 | try { 74 | if (writer != null) { 75 | writer.close(); 76 | } 77 | } catch (IOException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | 82 | } 83 | 84 | /** 85 | * 读文件到json 86 | * 87 | * @param fileName 文件名称 88 | * @return 文件内容Json对象 89 | */ 90 | public static JSONObject read2JSON(String fileName) { 91 | File file = new File(FILEPATH + File.separator + fileName + ".xml"); 92 | if (!file.exists()) { 93 | return null; 94 | } 95 | 96 | BufferedReader reader = null; 97 | String laststr = ""; 98 | try { 99 | reader = new BufferedReader(new FileReader(file)); 100 | String tempString = null; 101 | while ((tempString = reader.readLine()) != null) { 102 | laststr += tempString; 103 | } 104 | reader.close(); 105 | } catch (IOException e) { 106 | e.printStackTrace(); 107 | } 108 | 109 | return (JSONObject)JSON.parse(laststr); 110 | } 111 | 112 | /** 113 | * 通过key值获取文件中的value 114 | * 115 | * @param fileName 文件名称 116 | * @param key key值 117 | * @return key对应的value 118 | */ 119 | public static Object getValue(String fileName, String key) { 120 | JSONObject eJSON = null; 121 | eJSON = read2JSON(fileName); 122 | if (null != eJSON && eJSON.containsKey(key)) { 123 | @SuppressWarnings("unchecked") 124 | Map values = JSON.parseObject(eJSON.toString(), Map.class); 125 | return values.get(key); 126 | } else { 127 | return null; 128 | } 129 | } 130 | 131 | public static HashMap toHashMap(JSONObject js) { 132 | if (js == null) { 133 | return null; 134 | } 135 | HashMap data = new HashMap(); 136 | // 将json字符串转换成jsonObject 137 | Set set = js.keySet(); 138 | // 遍历jsonObject数据,添加到Map对象 139 | for (String s : set) { 140 | String key = String.valueOf(s); 141 | Long keyLong = Long.valueOf(key); 142 | 143 | String value = js.getString(key); 144 | Long valueLong; 145 | if (TextUtils.isEmpty(value)) { 146 | valueLong = js.getLong(key); 147 | } else { 148 | valueLong = Long.valueOf(value); 149 | } 150 | data.put(keyLong, valueLong); 151 | } 152 | return data; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /backend/src/main/java/com/dingtalk/h5app/quickstart/util/JsApiSignature.java: -------------------------------------------------------------------------------- 1 | package com.dingtalk.h5app.quickstart.util; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.util.Formatter; 7 | 8 | import com.dingtalk.h5app.quickstart.exception.DingtalkEncryptException; 9 | 10 | /** 11 | * 钉钉jsapi签名工具类 12 | * 13 | * @author openapi@dingtalk 14 | * @date 2020/2/4 15 | */ 16 | public class JsApiSignature { 17 | public static String sign(String url, String nonce, Long timestamp, String ticket) 18 | throws DingtalkEncryptException { 19 | String plain = String.format( 20 | "jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", 21 | ticket, nonce, timestamp, url); 22 | try { 23 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); 24 | sha1.reset(); 25 | sha1.update(plain.getBytes(StandardCharsets.UTF_8)); 26 | return bytesToHex(sha1.digest()); 27 | } catch (NoSuchAlgorithmException e) { 28 | throw new DingtalkEncryptException(DingtalkEncryptException.COMPUTE_SIGNATURE_ERROR, e); 29 | } 30 | } 31 | 32 | /** 33 | * 模拟生成随机 nonce 字符串 34 | * @return 随机字符串 35 | */ 36 | public static String genNonce() { 37 | return bytesToHex(Long.toString(System.nanoTime()).getBytes(StandardCharsets.UTF_8)); 38 | } 39 | 40 | private static String bytesToHex(final byte[] hash) { 41 | Formatter formatter = new Formatter(); 42 | for (byte b : hash){ 43 | formatter.format("%02x", b); 44 | } 45 | String result = formatter.toString(); 46 | formatter.close(); 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | dingtalk.app_key=YOUR_API_KEY_HERE 2 | dingtalk.app_secret=YOUR_API_SECRET_HERE 3 | dingtalk.agent_id=YOUR_APP_AGENT_ID 4 | dingtalk.corp_id=YOUR_CORP_ID -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "antd": "^3.26.8", 10 | "dingtalk-jsapi": "^2.8.33", 11 | "react": "^16.12.0", 12 | "react-dom": "^16.12.0", 13 | "react-router-dom": "^5.1.2", 14 | "react-scripts": "3.3.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | h5app-corp-quickstart 7 | com.dingtalk 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | frontend 13 | 14 | 15 | ${project.basedir}/.. 16 | 1.6 17 | 18 | 19 | 20 | 21 | 22 | com.github.eirslett 23 | frontend-maven-plugin 24 | ${frontend-maven-plugin.version} 25 | 26 | target 27 | v12.15.0 28 | v1.17.3 29 | https://npm.taobao.org/mirrors/node/ 30 | https://npm.taobao.org/mirrors/yarn/ 31 | 32 | 33 | 34 | install node and yarn 35 | 36 | install-node-and-yarn 37 | 38 | generate-resources 39 | 40 | 41 | yarn install 42 | 43 | yarn 44 | 45 | 46 | install 47 | 48 | 49 | 50 | yarn build 51 | 52 | yarn 53 | 54 | compile 55 | 56 | build 57 | 58 | 59 | 60 | 61 | 62 | com.mycila 63 | license-maven-plugin 64 | 3.0 65 | 66 | 67 | none 68 | 69 | 70 | 71 | 72 | net.orfjackal.retrolambda 73 | retrolambda-maven-plugin 74 | 2.5.5 75 | 76 | 77 | none 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dingtalk/h5app-corp-quickstart/e916fc874831590ef57d6d2e5d762a43c8faef0d/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dingtalk/h5app-corp-quickstart/e916fc874831590ef57d6d2e5d762a43c8faef0d/frontend/public/images/logo.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 微应用Demo 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dingtalk/h5app-corp-quickstart/e916fc874831590ef57d6d2e5d762a43c8faef0d/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dingtalk/h5app-corp-quickstart/e916fc874831590ef57d6d2e5d762a43c8faef0d/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .App { 4 | text-align: center; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | @media (prefers-reduced-motion: no-preference) { 13 | .App-logo { 14 | animation: App-logo-spin infinite 20s linear; 15 | } 16 | } 17 | 18 | .App-header { 19 | background-color: #282c34; 20 | min-height: 100vh; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: calc(10px + 2vmin); 26 | color: white; 27 | } 28 | 29 | .App-link { 30 | color: #61dafb; 31 | } 32 | 33 | @keyframes App-logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | 42 | img.logo { 43 | width: 32px; 44 | } 45 | span.logo { 46 | color: #FFF; 47 | margin-left:2vmin; 48 | vertical-align: bottom; 49 | font-size: 16px; 50 | } 51 | .user { 52 | text-align: right; 53 | color: #FFF; 54 | } 55 | .user .user-detail { 56 | margin-left: 15px; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; 3 | import { Layout, Row, Col, Menu, Icon, Card } from 'antd'; 4 | import H5AppQSHeader from './components/header.js' 5 | import H5AppQSContacts from './components/contacts.js' 6 | import H5AppQSDeplist from './components/deplist.js' 7 | import * as dd from 'dingtalk-jsapi'; 8 | import config from './config.js' 9 | 10 | import './App.css'; 11 | 12 | const host = config.host 13 | 14 | const { Header, Content, Footer } = Layout; 15 | const PlatformDetail = function(props) { 16 | return ( 17 | 18 | 19 | 平台: 20 | {props.env.platform} 21 | 22 | { 23 | 'version' in props.env && props.env.version && 24 | 25 | 版本: 26 | {props.env.version} 27 | 28 | } 29 | 30 | 应用类型: 31 | {props.env.appType} 32 | 33 | 34 | 语言: 35 | {props.env.language} 36 | 37 | 38 | ) 39 | } 40 | 41 | const alert = function(msg) { 42 | dd.device.notification.alert({ 43 | message: msg, 44 | title: "提示",//可传空 45 | buttonName: "确定", 46 | onSuccess : function() { 47 | }, 48 | onFail : function(err) {} 49 | }).catch((err) => { 50 | window.alert(msg); 51 | }); 52 | } 53 | 54 | function DeptList(props) { 55 | let { deptId } = useParams(); 56 | return 57 | } 58 | 59 | class H5AppQS extends React.Component { 60 | constructor(props) { 61 | super(props); 62 | this.state = { 63 | error: null, 64 | isLoaded: false, 65 | env: dd.env, 66 | user: { 67 | name: "用户名", 68 | userid: "userid", 69 | avatar: null 70 | }, 71 | authCode: null, 72 | config: { 73 | corpId: null, 74 | agentId: null 75 | }, 76 | current: 'home' 77 | }; 78 | } 79 | 80 | componentDidMount() { 81 | // 设置导航栏标题 82 | dd.biz.navigation.setTitle({ 83 | title : '微应用Demo',//控制标题文本,空字符串表示显示默认文本 84 | onSuccess : function(result) {}, 85 | onFail : function(err) {} 86 | }).catch(err => {console.log(err + '')}); 87 | 88 | const that = this 89 | fetch(host + '/config', { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 93 | 'Access-Control-Allow-Origin': '*' 94 | }, 95 | mode: 'cors', 96 | body: 'url='+window.location.href.replace(window.location.hash, "") 97 | }) 98 | .then(res => res.json()) 99 | .then( 100 | (result) => { 101 | that.setState({ 102 | config: { 103 | ...result.result 104 | } 105 | }) 106 | 107 | dd.runtime.permission.requestAuthCode({ 108 | corpId: that.state.config.corpId, 109 | onSuccess: function (info) { 110 | that.setState({ 111 | authCode: info.code 112 | }) 113 | fetch(host + '/login', { 114 | method: 'POST', 115 | headers: { 116 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 117 | 'Access-Control-Allow-Origin': '*' 118 | }, 119 | mode: 'cors', 120 | body: 'authCode='+info.code, 121 | }) 122 | .then(res => res.json()) 123 | .then((result) => { 124 | that.setState({ 125 | user: { 126 | ...result.result 127 | } 128 | }) 129 | }); 130 | 131 | }, 132 | onFail: function (err) { 133 | alert('免登授权码获取失败: ' + JSON.stringify(err)); 134 | } 135 | }).catch(err => {console.log(err + '')}); 136 | } 137 | ) 138 | .catch((err) => { 139 | alert('获取JS鉴权信息失败: ' + JSON.stringify(err)); 140 | }); 141 | } 142 | 143 | handleClick = e => { 144 | this.setState({ 145 | current: e.key, 146 | }); 147 | } 148 | 149 | render() { 150 | return ( 151 |
152 | 153 |
154 | 155 | 156 | 157 | 158 | 159 | 首页 160 | 161 | 162 | 企业联系人 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
@opendingtalk
177 |
178 |
179 | ) 180 | } 181 | } 182 | 183 | function App(props) { 184 | return ( 185 | 186 |
187 | 188 |
189 |
190 | ); 191 | } 192 | 193 | export default App; 194 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/components/contacts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Link, useParams } from 'react-router-dom'; 3 | import { List, Icon, Spin } from 'antd'; 4 | import config from '../config.js' 5 | 6 | const host = config.host 7 | class H5AppQSContacts extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | 11 | this.state = { 12 | loading: false, 13 | contacts: [], 14 | } 15 | } 16 | 17 | componentDidMount() { 18 | this.setState({loading: true}) 19 | fetch(host + '/department/list') 20 | .then(res => res.json()) 21 | .then(result => { 22 | this.setState({ 23 | contacts: result.result, 24 | loading: false 25 | }) 26 | }) 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 |
33 |

部门列表

34 | ( 38 | 39 | 40 | {item.name} 41 | 42 | 43 | )} 44 | /> 45 |
46 |
47 | ) 48 | } 49 | } 50 | 51 | export default H5AppQSContacts 52 | -------------------------------------------------------------------------------- /frontend/src/components/deplist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, Spin, Avatar, Button } from 'antd'; 3 | import * as dd from 'dingtalk-jsapi'; 4 | import config from '../config.js' 5 | 6 | const host = config.host 7 | class H5AppQSDeplist extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | 11 | this.state = { 12 | loading: false, 13 | users: [ 14 | ], 15 | } 16 | 17 | this.openChat = this.openChat.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | this.setState({loading: true}); 22 | // 单页应用使用首页地址做jsAPI鉴权 23 | dd.config({ 24 | ...this.props.config, 25 | jsApiList: [ 26 | 'biz.chat.openSingleChat' 27 | ] 28 | }) 29 | dd.error(err => { 30 | alert('dd.config error: ' + JSON.stringify(err)); 31 | }); 32 | 33 | fetch(host + '/user/simplelist?department_id='+ this.props.deptId) 34 | .then(res => res.json()) 35 | .then(result => { 36 | this.setState({ 37 | users: result.result, 38 | loading: false 39 | }) 40 | }) 41 | } 42 | 43 | openChat(e, userid) { 44 | //alert(JSON.stringify(this.props.config)); 45 | dd.biz.chat.openSingleChat({ 46 | corpId: this.props.config.corpId, // 企业id,必须是用户所属的企业的corpid 47 | userId: userid, // 用户的工号 48 | onSuccess : function() {}, 49 | onFail : function(err) {alert("fail:" + JSON.stringify(err));} 50 | }).catch(err => alert("exp:" + JSON.stringify(err) + JSON.stringify(this.props.config))) 51 | } 52 | 53 | render() { 54 | return ( 55 |
56 | 57 |

用户列表

58 | ( 62 | 63 | {item.name} 64 |
74 | ) 75 | } 76 | } 77 | 78 | export default H5AppQSDeplist 79 | -------------------------------------------------------------------------------- /frontend/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'antd'; 3 | import H5AppQSUser from './user.js' 4 | 5 | class H5AppQSHeader extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 16 | 17 | dingding 18 | 钉钉微应用QuickStart 19 | 20 | 21 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default H5AppQSHeader 35 | -------------------------------------------------------------------------------- /frontend/src/components/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Avatar } from 'antd'; 3 | 4 | function H5AppQSUser(props) { 5 | return ( 6 |
7 | 8 | 9 | {props.user.name} ({props.user.userid}) 10 | 11 |
12 | ) 13 | } 14 | 15 | export default H5AppQSUser 16 | -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // yarn start 本地调试时由于默认为3000端口 3 | // 访问后端host请配置为本机IP的backend服务端口,如8080端口 4 | // 例如 host: 'http://192.168.1.2:8080', 5 | // 使用backend中构建结果访问时请将host指定为空 6 | host: '', 7 | } 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /lib/taobao-sdk-java-auto_1479188381469-20200201.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dingtalk/h5app-corp-quickstart/e916fc874831590ef57d6d2e5d762a43c8faef0d/lib/taobao-sdk-java-auto_1479188381469-20200201.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.dingtalk 8 | h5app-corp-quickstart 9 | pom 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | frontend 14 | backend 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-parent 20 | 2.0.6.RELEASE 21 | 22 | 23 | 24 | 25 | 26 | com.alibaba 27 | fastjson 28 | 1.2.62 29 | 30 | 31 | 32 | org.apache.httpcomponents 33 | httpclient 34 | 4.5.1 35 | 36 | 37 | org.apache.httpcomponents 38 | httpcore 39 | 4.4.4 40 | 41 | 42 | org.apache.httpcomponents 43 | httpmime 44 | 4.3.5 45 | 46 | 47 | org.apache.commons 48 | commons-collections4 49 | 4.2 50 | 51 | 52 | 53 | 54 | --------------------------------------------------------------------------------