diff --git a/JSD-8449配置使用文档.docx b/JSD-8449配置使用文档.docx
new file mode 100644
index 0000000..7f75097
Binary files /dev/null and b/JSD-8449配置使用文档.docx differ
diff --git a/README.md b/README.md
index 0796de2..5abf539 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
# open-JSD-8449
-JSD-8449 钉钉单点+推送(特殊)开源任务材料
\ No newline at end of file
+JSD-8449 钉钉单点+推送(特殊)开源任务材料\
+免责说明:该源码为第三方爱好者提供,不保证源码和方案的可靠性,也不提供任何形式的源码教学指导和协助!\
+仅作为开发者学习参考使用!禁止用于任何商业用途!\
+为保护开发者隐私,开发者信息已隐去!若原开发者希望公开自己的信息,可联系hugh处理。
\ No newline at end of file
diff --git a/单点/plugin.xml b/单点/plugin.xml
new file mode 100644
index 0000000..ef8ec4e
--- /dev/null
+++ b/单点/plugin.xml
@@ -0,0 +1,24 @@
+
+
+ com.fr.plugin.yuyuantm.oauth2
+
+ yes
+ 1.5
+ 10.0
+ 2018-07-31
+ fr.open
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CommonUtils.java b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CommonUtils.java
new file mode 100644
index 0000000..822a870
--- /dev/null
+++ b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CommonUtils.java
@@ -0,0 +1,132 @@
+package com.fr.plugin.yuyuantm.oauth2;
+
+import com.fr.data.NetworkHelper;
+import com.fr.decision.authority.data.User;
+import com.fr.decision.mobile.terminal.TerminalHandler;
+import com.fr.decision.webservice.utils.DecisionServiceConstants;
+import com.fr.decision.webservice.utils.DecisionStatusService;
+import com.fr.decision.webservice.v10.login.LoginService;
+import com.fr.decision.webservice.v10.login.TokenResource;
+import com.fr.decision.webservice.v10.user.UserService;
+import com.fr.log.FineLoggerFactory;
+import com.fr.stable.StringUtils;
+import com.fr.stable.web.Device;
+import com.fr.web.utils.WebUtils;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author fr.open
+ * @since 2021/8/24
+ */
+public class CommonUtils {
+
+ public static String getProperty(Properties props, String key, String defaultValue, boolean allowBlank) {
+ String value = props.getProperty(key);
+ if (StringUtils.isNotBlank(value)) {
+ return value;
+ } else {
+ if (allowBlank) {
+ FineLoggerFactory.getLogger().warn("Property[" + key + "] value is blank.");
+ return defaultValue;
+ } else {
+ throw new IllegalArgumentException("Property[" + key + "] cann't be blank.");
+ }
+ }
+ }
+
+ public static String getProperty(Properties props, String key, boolean allowBlank) {
+ return getProperty(props, key, null, allowBlank);
+ }
+
+ public static String getProperty(Properties props, String key) {
+ return getProperty(props, key, null, true);
+ }
+
+ public static boolean isLogin(HttpServletRequest request) {
+ String oldToken = TokenResource.COOKIE.getToken(request);
+ return oldToken != null && checkTokenValid(request, (String) oldToken);
+ }
+
+ private static boolean checkTokenValid(HttpServletRequest req, String token) {
+ try {
+ Device device = NetworkHelper.getDevice(req);
+ LoginService.getInstance().loginStatusValid(token, TerminalHandler.getTerminal(req, device));
+ return true;
+ } catch (Exception ignore) {
+ }
+ return false;
+ }
+
+ /**
+ * 跳转到过滤器链中的下一个过滤器
+ *
+ * @param request
+ * @param response
+ * @param chain
+ */
+ public static void next(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
+ try {
+ chain.doFilter(request, response);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void login(String username, HttpServletRequest request, HttpServletResponse response) {
+ try {
+ User user = UserService.getInstance().getUserByUserName(username);
+ if (user == null) {
+ throw new RuntimeException("系统未授权, 当前用户是\"" + username + "\"");
+ }
+ String token = LoginService.getInstance().login(request, response, username);
+ request.setAttribute(DecisionServiceConstants.FINE_AUTH_TOKEN_NAME, token);
+ } catch (Exception e) {
+ FineLoggerFactory.getLogger().error("sso >> Failed to login with[" + username + "]", e);
+ throw new RuntimeException("用户\"" + username +"\"登录失败");
+ }
+ }
+
+ public static boolean isMobileDevice(HttpServletRequest request) {
+ if (WebUtils.getDevice(request).isMobile()) {
+ FineLoggerFactory.getLogger().info("current request is is mobile request ,url is {}", request.getRequestURI());
+ return true;
+ }
+ String requestHeader = request.getHeader("user-agent");
+ String[] deviceArray = new String[]{"android", "iphone", "ipad", "ios", "windows phone", "wechat"};
+ if (requestHeader == null) {
+ return false;
+ }
+ requestHeader = requestHeader.toLowerCase();
+ for (int i = 0; i < deviceArray.length; i++) {
+ if (requestHeader.toLowerCase().contains(deviceArray[i])) {
+ FineLoggerFactory.getLogger().info("current request:{} is mobile request!", request.getRequestURI());
+ return true;
+ }
+ }
+ String op = WebUtils.getHTTPRequestParameter(request, "op");
+ return StringUtils.isNotBlank(op) && StringUtils.equals("h5",op);
+ }
+
+ public static void cacheParams(String key, Map values) {
+ try {
+ DecisionStatusService.originUrlStatusService().put(key, values);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String getCachedParam(String key, String name) {
+ try {
+ Map values = DecisionStatusService.originUrlStatusService().get(key);
+ return values.get(name);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CustomLogInOutEventProvider.java b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CustomLogInOutEventProvider.java
new file mode 100644
index 0000000..0c128ad
--- /dev/null
+++ b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/CustomLogInOutEventProvider.java
@@ -0,0 +1,31 @@
+package com.fr.plugin.yuyuantm.oauth2;
+
+import com.fr.decision.fun.impl.AbstractLogInOutEventProvider;
+import com.fr.decision.webservice.login.LogInOutResultInfo;
+import com.fr.general.PropertiesUtils;
+import com.fr.stable.StringUtils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Properties;
+
+import static com.fr.plugin.yuyuantm.oauth2.CommonUtils.getProperty;
+
+/**
+ * @Author fr.open
+ * @since 2021/8/24
+ **/
+public class CustomLogInOutEventProvider extends AbstractLogInOutEventProvider {
+
+ @Override
+ public String logoutAction(LogInOutResultInfo result) {
+ Properties props = PropertiesUtils.getProperties("yuyuantm");
+ String syscode = getProperty(props, "api.syscode", false);
+ String logoutURL = getProperty(props, "api.logout", true);
+ String logoutRedirectURL = getProperty(props, "api.logout-redirect", "http://www.baidu.com", true);
+ if (StringUtils.isBlank(logoutURL)) {
+ return null;
+ }
+ return String.format("%s?yuyuan_syscode=%s&service=%s", logoutURL, syscode, logoutRedirectURL);
+ }
+}
diff --git a/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/DesECBUtil.java b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/DesECBUtil.java
new file mode 100644
index 0000000..36a0134
--- /dev/null
+++ b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/DesECBUtil.java
@@ -0,0 +1,66 @@
+package com.fr.plugin.yuyuantm.oauth2;
+
+import com.fr.third.org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Key;
+
+/**
+ * @Author fr.open
+ * @Date 2021/8/24
+ * @Description
+ **/
+public class DesECBUtil {
+ /**
+ * 加密数据
+ *
+ * @param encryptString
+ * @param encryptKey
+ * @return
+ * @throws Exception
+ */
+ public static String encryptDES(String encryptString, String encryptKey) throws Exception {
+ Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(getKey(encryptKey), "DES"));
+ byte[] encryptedData = cipher.doFinal(encryptString.getBytes("UTF-8"));
+ return Base64.encodeBase64String(encryptedData);
+ }
+
+ /**
+ * key 不足8位补位
+ *
+ * @param
+ */
+ public static byte[] getKey(String keyRule) {
+ Key key = null;
+ byte[] keyByte = keyRule.getBytes();
+ // 创建一个空的八位数组,默认情况下为0
+ byte[] byteTemp = new byte[8];
+ // 将用户指定的规则转换成八位数组
+ for (int i = 0; i < byteTemp.length && i < keyByte.length; i++) {
+ byteTemp[i] = keyByte[i];
+ }
+ key = new SecretKeySpec(byteTemp, "DES");
+ return key.getEncoded();
+ }
+
+ /***
+ * 解密数据
+ * @param decryptString
+ * @param decryptKey
+ * @return
+ * @throws Exception
+ */
+
+ public static String decryptDES(String decryptString, String decryptKey) throws Exception {
+ byte[] sourceBytes = Base64.decodeBase64(decryptString);
+ Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(getKey(decryptKey), "DES"));
+ byte[] decoded = cipher.doFinal(sourceBytes);
+ return new String(decoded, "UTF-8");
+
+ }
+
+}
+
diff --git a/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/SsoFilter.java b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/SsoFilter.java
new file mode 100644
index 0000000..add6f4e
--- /dev/null
+++ b/单点/src/main/java/com/fr/plugin/yuyuantm/oauth2/SsoFilter.java
@@ -0,0 +1,192 @@
+package com.fr.plugin.yuyuantm.oauth2;
+
+import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider;
+import com.fr.decision.webservice.utils.WebServiceUtils;
+import com.fr.general.PropertiesUtils;
+import com.fr.general.http.HttpToolbox;
+import com.fr.json.JSONObject;
+import com.fr.locale.InterProviderFactory;
+import com.fr.log.FineLoggerFactory;
+import com.fr.plugin.transform.FunctionRecorder;
+import com.fr.stable.StringUtils;
+import com.fr.web.utils.WebUtils;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static com.fr.plugin.yuyuantm.oauth2.CommonUtils.*;
+
+/**
+ * @author fr.open
+ * @since 2021/8/24
+ */
+@FunctionRecorder
+public class SsoFilter extends AbstractGlobalRequestFilterProvider {
+
+ private static String[] NOT_FILTER = {
+ "/decision/file",
+ "/decision/resources",
+ "/system",
+ "/materials.min.js.map",
+ "/remote",
+ "/login",
+ "/url/mobile",
+ "/login/config",
+ "/help/css",
+ };
+
+ private static final String E_KEY = "xxxx";
+
+ private String syscode;
+
+ private String apiClientId;
+
+ private String apiClientSecret;
+
+ private String apiAuthorize;
+
+ private String apiGetToken;
+
+ private String apiGetUser;
+
+ private void initParams() {
+ Properties props = PropertiesUtils.getProperties("yuyuantm");
+ this.syscode = getProperty(props, "api.syscode", false);
+ this.apiClientId = getProperty(props, "api.client_id", false);
+ this.apiAuthorize = getProperty(props, "api.authorize", false);
+ this.apiGetToken = getProperty(props, "api.get-token", false);
+ this.apiGetUser = getProperty(props, "api.get-user", false);
+ this.apiClientSecret = getProperty(props, "api.client_secret", false);
+ }
+
+
+ @Override
+ public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
+ if (isAccept(request)) {
+ next(request, response, chain);
+ return;
+ }
+ try {
+ String resultSign = request.getParameter("result_sign");
+ if (StringUtils.isNotBlank(resultSign)) {
+ String username = DesECBUtil.decryptDES(resultSign, E_KEY);
+ login(username, request, response);
+ next(request, response, chain);
+ return;
+ }
+ initParams();
+ String code = request.getParameter("code");
+ if (StringUtils.isBlank(code)) {
+ jumpAuthorize(request, response);
+ return;
+ }
+ login(getUsername(getToken(code)), request, response);
+ String state = request.getParameter("state");
+ if (StringUtils.isNotBlank(state)) {
+ String accessURL = getCachedParam(state, "accessURL");
+ if (StringUtils.isNotBlank(accessURL)) {
+ response.sendRedirect(accessURL);
+ return;
+ }
+ }
+ next(request, response, chain);
+ } catch (Exception e) {
+ FineLoggerFactory.getLogger().error("oauth2 >>> 单点登陆处理失败", e);
+ setError(response, e.getMessage());
+ }
+ }
+
+ private String getUsername(String token) throws IOException {
+ String address = String.format("%s?yuyuan_syscode=%s", apiGetUser, syscode);
+ HashMap params = new HashMap<>();
+ params.put("access_token", token);
+ String res = HttpToolbox.post(address, params);
+ JSONObject body = new JSONObject(res);
+ if (body.has("account")) {
+ return body.getString("account");
+ }
+ throw new RuntimeException("获取用户信息失败,Cause by: " + res);
+ }
+
+ private String getToken(String code) throws IOException {
+ String address = String.format("%s?yuyuan_syscode=%s", apiGetToken, syscode);
+ HashMap params = new HashMap<>();
+ params.put("client_id", apiClientId);
+ params.put("client_secret", apiClientSecret);
+ params.put("code", code);
+ params.put("grant_type", "authorization_code");
+ String res = HttpToolbox.post(address, params);
+ JSONObject body = new JSONObject(res);
+ if (body.has("access_token")) {
+ return body.getString("access_token");
+ }
+ throw new RuntimeException("获取access_token失败,Cause by: " + res);
+ }
+
+ private void jumpAuthorize(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String state = UUID.randomUUID().toString();
+ String accessURL = request.getRequestURI();
+ if (StringUtils.isNotBlank(request.getQueryString())) {
+ accessURL += "?" + request.getQueryString();
+ }
+ Map params = new HashMap<>();
+ params.put("accessURL", accessURL);
+ cacheParams(state, params);
+ String address = String.format("%s?yuyuan_syscode=%s&response_type=code&client_id=%s&state=%s&redirect=%s", apiAuthorize, syscode, apiClientId, state, request.getRequestURI());
+ FineLoggerFactory.getLogger().info("oauth2 >>> 请求中不包含code值,条转到登陆页面 >>> \"{}\"", address);
+ response.sendRedirect(address);
+ }
+
+ @Override
+ public String filterName() {
+ return "sso";
+ }
+
+ @Override
+ public String[] urlPatterns() {
+ return new String[]{"/*"};
+ }
+
+ private boolean isAccept(HttpServletRequest request) {
+ if (request.getRequestURI().endsWith("/view/form") || request.getRequestURI().endsWith("/view/report")) {
+ String viewlet = WebUtils.getHTTPRequestParameter(request, "viewlet");
+ if (StringUtils.isBlank(viewlet)) {
+ return true;
+ }
+ }
+ String url = request.getRequestURL().toString();
+ if (Stream.of(NOT_FILTER).anyMatch(url::contains)) {
+ return true;
+ }
+ String t = request.getParameter("ref_t");
+ if (StringUtils.equals("design", t)) {
+ return true;
+ }
+ return isLogin(request);
+ }
+
+ private void setError(HttpServletResponse res, String reason) {
+ try {
+ PrintWriter printWriter = WebUtils.createPrintWriter(res);
+ Map map = new HashMap<>();
+ map.put("result", InterProviderFactory.getProvider().getLocText("Fine-Engine_Error_Page_Result"));
+ map.put("reason", reason);
+ map.put("solution", InterProviderFactory.getProvider().getLocText("Fine-Engine_Please_Contact_Platform_Admin"));
+ String page = WebServiceUtils.parseWebPageResourceSafe("com/fr/web/controller/decision/entrance/resources/unavailable.html", map);
+ printWriter.write(page);
+ printWriter.flush();
+ printWriter.close();
+ } catch (Exception e) {
+ FineLoggerFactory.getLogger().error(e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/单点/src/main/resources/yuyuantm.properties b/单点/src/main/resources/yuyuantm.properties
new file mode 100644
index 0000000..7cac4ab
--- /dev/null
+++ b/单点/src/main/resources/yuyuantm.properties
@@ -0,0 +1,14 @@
+# \u63A5\u53E3\u6240\u9700\u7684 yuyuan_syscode \u53C2\u6570\u503C
+api.syscode=xxxxx
+api.client_id=xxxxx
+api.client_secret=xxxx
+# \u767B\u9646\u9875\u9762\u83B7\u53D6code\u5730\u5740
+api.authorize=https://xxxx/oauth2/authorize
+# \u83B7\u53D6access_token\u63A5\u53E3
+api.get-token=https://xxxx/oauth2/token
+# \u83B7\u53D6\u7528\u6237\u4FE1\u606F\u63A5\u53E3
+api.get-user=https://xxxx/oauth2/resource
+# \u6CE8\u9500\u767B\u9646\u63A5\u53E3\uFF0C\u4E0D\u9700\u8981\u53EF\u4E3A\u7A7A
+api.logout=https://xxxx/oauth2/logout
+# \u6CE8\u9500\u767B\u9646\u540E\u6761\u8F6C\u5730\u5740
+api.logout-redirect=https://www.baidu.com
\ No newline at end of file
diff --git a/推送/plugin.xml b/推送/plugin.xml
new file mode 100644
index 0000000..81be93d
--- /dev/null
+++ b/推送/plugin.xml
@@ -0,0 +1,27 @@
+
+ com.fr.plugin.yuyuan.dingding
+
+ yes
+ 1.2
+ 10.0
+ 2018-07-31
+ fr.open
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingDIngDBAccess.java b/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingDIngDBAccess.java
new file mode 100644
index 0000000..81c33e6
--- /dev/null
+++ b/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingDIngDBAccess.java
@@ -0,0 +1,44 @@
+package com.fr.plugin.yuyuan.dingding;
+
+import com.fr.decision.plugin.db.AbstractDecisionDBAccessProvider;
+import com.fr.plugin.yuyuan.dingding.dao.OutputDingTalkDAO;
+import com.fr.plugin.yuyuan.dingding.entity.OutputDingTalkEntity;
+import com.fr.stable.db.accessor.DBAccessor;
+import com.fr.stable.db.dao.BaseDAO;
+import com.fr.stable.db.dao.DAOProvider;
+
+/**
+ * @Author fr.open
+ * @Date 2021/08/22
+ * @Description
+ **/
+public class DingDIngDBAccess extends AbstractDecisionDBAccessProvider {
+
+ private static DBAccessor dbAccessor;
+
+ public DBAccessor getDbAccessor() {
+ return dbAccessor;
+ }
+
+ @Override
+ public DAOProvider[] registerDAO() {
+ return new DAOProvider[]{
+ new DAOProvider() {
+ @Override
+ public Class getEntityClass() {
+ return OutputDingTalkEntity.class;
+ }
+
+ @Override
+ public Class extends BaseDAO> getDAOClass() {
+ return OutputDingTalkDAO.class;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onDBAvailable(DBAccessor dbAccessor) {
+ DingDIngDBAccess.dbAccessor = dbAccessor;
+ }
+}
diff --git a/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingTalkFormulaProvider.java b/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingTalkFormulaProvider.java
new file mode 100644
index 0000000..36f6b2c
--- /dev/null
+++ b/推送/src/main/java/com/fr/plugin/yuyuan/dingding/DingTalkFormulaProvider.java
@@ -0,0 +1,27 @@
+package com.fr.plugin.yuyuan.dingding;
+
+import com.fr.main.workbook.ResultWorkBook;
+import com.fr.plugin.yuyuan.dingding.bean.OutputDingTalk;
+import com.fr.schedule.base.provider.impl.AbstractOutputFormulaProvider;
+import com.fr.schedule.extension.report.util.ScheduleParameterUtils;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Author fr.open
+ * @Date 2021/08/22
+ * @Description
+ **/
+public class DingTalkFormulaProvider extends AbstractOutputFormulaProvider {
+ @Override
+ public void dealWithFormulaParam(OutputDingTalk action, ResultWorkBook result, List