wuhb 4 месяцев назад
Родитель
Сommit
097cccedf1

+ 2 - 1
.vscode/settings.json

@@ -1,3 +1,4 @@
 {
-    "java.configuration.updateBuildConfiguration": "interactive"
+    "java.configuration.updateBuildConfiguration": "interactive",
+    "java.compile.nullAnalysis.mode": "disabled"
 }

+ 27 - 0
README.md

@@ -1 +1,28 @@
 宇光同行工作汇报
+
+## 个推推送服务集成说明
+
+### 配置说明
+在`application-dev.yml`或相应环境配置文件中添加个推配置:
+
+```yaml
+getui:
+  enabled: true  # 是否启用个推服务
+  app-id: xxxxxx  # 个推AppId
+  app-key: xxxxxx  # 个推AppKey
+  app-secret: xxxxxx  # 个推AppSecret
+  master-secret: xxxxxx  # 个推MasterSecret
+```
+
+### 使用方法
+```java
+// 推送消息给单个用户
+GetuiTemplate.pushMessageToSingle("用户的CID", "标题", "内容");
+
+// 推送消息给单个用户(带透传内容)
+GetuiTemplate.pushMessageToSingle("用户的CID", "标题", "内容", "{\"type\":\"order\",\"id\":123}");
+
+// 批量推送消息给多个用户
+String[] cids = {"用户1的CID", "用户2的CID", "用户3的CID"};
+GetuiTemplate.pushMessageToBatch(cids, "标题", "内容");
+```

+ 49 - 0
ygtx-admin/src/main/java/com/ygtx/web/controller/common/PushController.java

@@ -0,0 +1,49 @@
+package com.ygtx.web.controller.common;
+
+import com.ygtx.common.config.RuoYiConfig;
+import com.ygtx.common.core.domain.AjaxResult;
+import com.ygtx.common.utils.SecurityUtils;
+import com.ygtx.common.utils.StringUtils;
+import com.ygtx.common.utils.file.FileUploadUtils;
+import com.ygtx.common.utils.file.FileUtils;
+import com.ygtx.framework.config.ServerConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 推送注册处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/push")
+public class PushController
+{
+    private static final Logger log = LoggerFactory.getLogger(PushController.class);
+
+    /**
+     * 推送注册
+     */
+    @GetMapping("/register/{cid}")
+    public AjaxResult register(@PathVariable String cid) throws Exception
+    {
+        try
+        {
+            System.out.println(SecurityUtils.getUserId() + "-注册成功-" +cid);
+            return AjaxResult.success();
+        }
+        catch (Exception e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+}

+ 18 - 0
ygtx-admin/src/main/java/com/ygtx/web/controller/system/SysIndexController.java

@@ -1,11 +1,16 @@
 package com.ygtx.web.controller.system;
 
+import com.ygtx.common.core.domain.AjaxResult;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import com.ygtx.common.config.RuoYiConfig;
 import com.ygtx.common.utils.StringUtils;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * 首页
  *
@@ -26,4 +31,17 @@ public class SysIndexController
     {
         return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
     }
+
+    /**
+     * 获取设备统计数据
+     */
+    @GetMapping("/getAppInfo")
+    public AjaxResult getAppInfo()
+    {
+        Map<String, String> appInfoMap = new HashMap<>();
+        appInfoMap.put("name", ruoyiConfig.getName());
+        appInfoMap.put("version", ruoyiConfig.getVersion());
+        appInfoMap.put("copyrightYear", ruoyiConfig.getCopyrightYear());
+        return AjaxResult.success(appInfoMap);
+    }
 }

+ 9 - 0
ygtx-admin/src/main/resources/application.yml

@@ -110,3 +110,12 @@ xss:
   excludes: /system/notice
   # 匹配链接
   urlPatterns: /system/*,/monitor/*,/tool/*,/worklog/*
+
+# 个推
+getui:
+  domain: https://restapi.getui.com/v2/
+  enabled: false
+  app-id: pSzmat7Cnh7gP5vqHtWcu1
+  app-key: Ok6Z5MoIqZ91H2xrpWp0C5
+  app-secret: c42Gq0cAxL64rSdpStYpv7
+  master-secret: 【消息推送】OvrKSuwbDF8tvdZLrXc1U3

+ 6 - 0
ygtx-common/pom.xml

@@ -131,6 +131,12 @@
             <version>5.8.0.M3</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.getui.push</groupId>
+            <artifactId>restful-sdk</artifactId>
+            <version>1.0.7.0</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 80 - 0
ygtx-common/src/main/java/com/ygtx/common/utils/getui/GetuiConfig.java

@@ -0,0 +1,80 @@
+package com.ygtx.common.utils.getui;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 个推配置类
+ *
+ * @author ruoyi
+ */
+@Component
+@ConfigurationProperties(prefix = "getui")
+public class GetuiConfig {
+
+    /** 个推AppId */
+    private String appId;
+
+    /** 个推AppKey */
+    private String appKey;
+
+    /** 个推AppSecret */
+    private String appSecret;
+
+    /** 个推MasterSecret */
+    private String masterSecret;
+
+    /** 个推PushUrl */
+    private String pushUrl = "https://restapi.getui.com/v2/";
+
+    /** 是否启用个推服务 */
+    private boolean enabled = false;
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getAppKey() {
+        return appKey;
+    }
+
+    public void setAppKey(String appKey) {
+        this.appKey = appKey;
+    }
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+
+    public String getMasterSecret() {
+        return masterSecret;
+    }
+
+    public void setMasterSecret(String masterSecret) {
+        this.masterSecret = masterSecret;
+    }
+
+    public String getPushUrl() {
+        return pushUrl;
+    }
+
+    public void setPushUrl(String pushUrl) {
+        this.pushUrl = pushUrl;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}

+ 298 - 0
ygtx-common/src/main/java/com/ygtx/common/utils/getui/GetuiTemplate.java

@@ -0,0 +1,298 @@
+package com.ygtx.common.utils.getui;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson2.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+
+/**
+ * 个推推送服务工具类
+ *
+ * @author ruoyi
+ */
+@Component
+public class GetuiTemplate {
+
+    private static final Logger log = LoggerFactory.getLogger(GetuiTemplate.class);
+
+    @Autowired
+    private GetuiConfig getuiConfig;
+
+    private static GetuiTemplate getuiTemplate;
+
+    private static final String AUTH_URL = "/auth";
+    private static final String PUSH_SINGLE_URL = "/push/single/cid";
+    private static final String PUSH_BATCH_URL = "/push/list/cid";
+
+    @PostConstruct
+    public void init() {
+        getuiTemplate = this;
+        getuiTemplate.getuiConfig = this.getuiConfig;
+    }
+
+    /**
+     * 获取鉴权Token
+     *
+     * @return token
+     */
+    private String getAuthToken() {
+        if (!getuiConfig.isEnabled()) {
+            throw new RuntimeException("个推服务未启用");
+        }
+
+        try {
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("appkey", getuiConfig.getAppKey());
+            paramMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
+            paramMap.put("sign", generateSign(paramMap));
+
+            String url = getuiConfig.getPushUrl() + getuiConfig.getAppId() + AUTH_URL;
+
+            HttpResponse response = HttpRequest.post(url)
+                    .header("content-type", "application/json")
+                    .body(JSONUtil.toJsonStr(paramMap))
+                    .execute();
+
+            if (response.getStatus() == 200) {
+                JSONObject result = JSONObject.parseObject(response.body());
+                if ("ok".equals(result.getString("code"))) {
+                    return result.getJSONObject("data").getString("token");
+                } else {
+                    log.error("获取个推鉴权Token失败: {}", result.getString("msg"));
+                    return null;
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取个推鉴权Token异常", e);
+        }
+        return null;
+    }
+
+    /**
+     * 生成签名
+     *
+     * @param paramMap 参数map
+     * @return 签名
+     */
+    private String generateSign(Map<String, Object> paramMap) {
+        // 根据个推官方文档实现签名生成逻辑
+        String appKey = paramMap.get("appkey").toString();
+        String timestamp = paramMap.get("timestamp").toString();
+        String masterSecret = getuiConfig.getMasterSecret();
+
+        // 个推签名算法:SHA256(masterSecret + timestamp + appKey)
+        String signStr = masterSecret + timestamp + appKey;
+        return cn.hutool.crypto.SecureUtil.sha256(signStr);
+    }
+
+    /**
+     * 推送消息给单个用户
+     *
+     * @param cid 用户cid
+     * @param title 消息标题
+     * @param content 消息内容
+     * @param payload 透传内容
+     * @return 推送结果
+     */
+    public boolean pushToSingle(String cid, String title, String content, String payload) {
+        if (!getuiConfig.isEnabled()) {
+            log.warn("个推服务未启用");
+            return false;
+        }
+
+        if (StrUtil.isBlank(cid)) {
+            log.warn("用户cid不能为空");
+            return false;
+        }
+
+        String token = getAuthToken();
+        if (StrUtil.isBlank(token)) {
+            log.error("获取个推鉴权Token失败");
+            return false;
+        }
+
+        try {
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("request_id", String.valueOf(System.currentTimeMillis()));
+
+            Map<String, Object> pushMessage = new HashMap<>();
+            pushMessage.put("notification", createNotification(title, content));
+            if (StrUtil.isNotBlank(payload)) {
+                pushMessage.put("transmission", createTransmission(payload));
+            }
+
+            paramMap.put("settings", createSettings());
+            paramMap.put("push_message", pushMessage);
+            paramMap.put("audience", createAudience(cid));
+
+            String url = getuiConfig.getPushUrl() + getuiConfig.getAppId() + PUSH_SINGLE_URL;
+
+            HttpResponse response = HttpRequest.post(url)
+                    .header("content-type", "application/json")
+                    .header("token", token)
+                    .body(JSONUtil.toJsonStr(paramMap))
+                    .execute();
+
+            if (response.getStatus() == 200) {
+                JSONObject result = JSONObject.parseObject(response.body());
+                if ("ok".equals(result.getString("code"))) {
+                    log.info("个推消息推送成功: cid={}", cid);
+                    return true;
+                } else {
+                    log.error("个推消息推送失败: cid={}, 错误信息={}", cid, result.getString("msg"));
+                    return false;
+                }
+            }
+        } catch (Exception e) {
+            log.error("个推消息推送异常: cid={}", cid, e);
+        }
+        return false;
+    }
+
+    /**
+     * 批量推送消息给多个用户
+     *
+     * @param cids 用户cid列表
+     * @param title 消息标题
+     * @param content 消息内容
+     * @param payload 透传内容
+     * @return 推送结果
+     */
+    public boolean pushToBatch(String[] cids, String title, String content, String payload) {
+        if (!getuiConfig.isEnabled()) {
+            log.warn("个推服务未启用");
+            return false;
+        }
+
+        if (cids == null || cids.length == 0) {
+            log.warn("用户cid列表不能为空");
+            return false;
+        }
+
+        String token = getAuthToken();
+        if (StrUtil.isBlank(token)) {
+            log.error("获取个推鉴权Token失败");
+            return false;
+        }
+
+        try {
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("request_id", String.valueOf(System.currentTimeMillis()));
+
+            Map<String, Object> pushMessage = new HashMap<>();
+            pushMessage.put("notification", createNotification(title, content));
+            if (StrUtil.isNotBlank(payload)) {
+                pushMessage.put("transmission", createTransmission(payload));
+            }
+
+            paramMap.put("settings", createSettings());
+            paramMap.put("push_message", pushMessage);
+            paramMap.put("audience", createAudience(cids));
+
+            String url = getuiConfig.getPushUrl() + getuiConfig.getAppId() + PUSH_BATCH_URL;
+
+            HttpResponse response = HttpRequest.post(url)
+                    .header("content-type", "application/json")
+                    .header("token", token)
+                    .body(JSONUtil.toJsonStr(paramMap))
+                    .execute();
+
+            if (response.getStatus() == 200) {
+                JSONObject result = JSONObject.parseObject(response.body());
+                if ("ok".equals(result.getString("code"))) {
+                    log.info("个推批量消息推送成功: cids数量={}", cids.length);
+                    return true;
+                } else {
+                    log.error("个推批量消息推送失败: cids数量={}, 错误信息={}", cids.length, result.getString("msg"));
+                    return false;
+                }
+            }
+        } catch (Exception e) {
+            log.error("个推批量消息推送异常: cids数量={}", cids.length, e);
+        }
+        return false;
+    }
+
+    /**
+     * 创建通知消息
+     */
+    private Map<String, Object> createNotification(String title, String content) {
+        Map<String, Object> notification = new HashMap<>();
+        notification.put("title", StrUtil.blankToDefault(title, "消息通知"));
+        notification.put("body", StrUtil.blankToDefault(content, "您有一条新消息"));
+        notification.put("click_type", "intent");
+        return notification;
+    }
+
+    /**
+     * 创建透传消息
+     */
+    private Map<String, Object> createTransmission(String payload) {
+        Map<String, Object> transmission = new HashMap<>();
+        transmission.put("transmission_content", payload);
+        return transmission;
+    }
+
+    /**
+     * 创建推送设置
+     */
+    private Map<String, Object> createSettings() {
+        Map<String, Object> settings = new HashMap<>();
+        settings.put("ttl", 3600000); // 消息离线时间1小时
+        return settings;
+    }
+
+    /**
+     * 创建推送目标(单个用户)
+     */
+    private Map<String, Object> createAudience(String cid) {
+        Map<String, Object> audience = new HashMap<>();
+        audience.put("cid", new String[]{cid});
+        return audience;
+    }
+
+    /**
+     * 创建推送目标(多个用户)
+     */
+    private Map<String, Object> createAudience(String[] cids) {
+        Map<String, Object> audience = new HashMap<>();
+        audience.put("cid", cids);
+        return audience;
+    }
+
+    /**
+     * 静态方法推送消息给单个用户
+     */
+    public static boolean pushMessageToSingle(String cid, String title, String content) {
+        return getuiTemplate.pushToSingle(cid, title, content, null);
+    }
+
+    /**
+     * 静态方法推送消息给单个用户(带透传内容)
+     */
+    public static boolean pushMessageToSingle(String cid, String title, String content, String payload) {
+        return getuiTemplate.pushToSingle(cid, title, content, payload);
+    }
+
+    /**
+     * 静态方法批量推送消息给多个用户
+     */
+    public static boolean pushMessageToBatch(String[] cids, String title, String content) {
+        return getuiTemplate.pushToBatch(cids, title, content, null);
+    }
+
+    /**
+     * 静态方法批量推送消息给多个用户(带透传内容)
+     */
+    public static boolean pushMessageToBatch(String[] cids, String title, String content, String payload) {
+        return getuiTemplate.pushToBatch(cids, title, content, payload);
+    }
+}

+ 30 - 0
ygtx-common/src/main/java/com/ygtx/common/utils/getui/GetuiTest.java

@@ -0,0 +1,30 @@
+package com.ygtx.common.utils.getui;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * 个推服务使用示例
+ *
+ * @author ruoyi
+ */
+@Component
+public class GetuiTest {
+
+    /**
+     * 推送消息示例
+     */
+    public void testPushMessage() {
+        // 推送消息给单个用户
+        GetuiTemplate.pushMessageToSingle("用户的CID", "测试标题", "测试内容");
+
+        // 推送消息给单个用户(带透传内容)
+        GetuiTemplate.pushMessageToSingle("用户的CID", "测试标题", "测试内容", "{\"type\":\"order\",\"id\":123}");
+
+        // 批量推送消息给多个用户
+        String[] cids = {"用户1的CID", "用户2的CID", "用户3的CID"};
+        GetuiTemplate.pushMessageToBatch(cids, "测试标题", "测试内容");
+
+        // 批量推送消息给多个用户(带透传内容)
+        GetuiTemplate.pushMessageToBatch(cids, "测试标题", "测试内容", "{\"type\":\"notice\",\"id\":456}");
+    }
+}

+ 8 - 1
ygtx-ui/src/api/system/dashboard.js

@@ -22,4 +22,11 @@ export function getChartData() {
     url: '/getChartData',
     method: 'get'
   })
-}
+}
+
+export function getAppInfo() {
+    return request({
+        url: '/getAppInfo',
+        method: 'get'
+    })
+}

+ 20 - 1
ygtx-ui/src/layout/components/Navbar.vue

@@ -34,7 +34,10 @@
             </router-link>
             <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
                 <span>布局设置</span>
-              </el-dropdown-item>
+            </el-dropdown-item>
+            <el-dropdown-item @click="showViersion">
+              <span>版本信息</span>
+            </el-dropdown-item>
             <el-dropdown-item divided command="logout">
               <span>退出登录</span>
             </el-dropdown-item>
@@ -58,11 +61,14 @@ import RuoYiDoc from '@/components/RuoYi/Doc'
 import useAppStore from '@/store/modules/app'
 import useUserStore from '@/store/modules/user'
 import useSettingsStore from '@/store/modules/settings'
+import {getAppInfo} from "@/api/system/dashboard";
 
 const appStore = useAppStore()
 const userStore = useUserStore()
 const settingsStore = useSettingsStore()
 
+const { proxy } = getCurrentInstance()
+
 function toggleSideBar() {
   appStore.toggleSideBar()
 }
@@ -100,6 +106,19 @@ function setLayout() {
 function toggleTheme() {
   settingsStore.toggleTheme()
 }
+
+function showViersion() {
+  getAppInfo().then(response => {
+    const appInfo = response.data;
+    proxy.$alert(`版本名称:${appInfo.name}<br/>当前版本:V${appInfo.version}<br/>版权:${appInfo.copyrightYear}`, '版本信息', {
+      confirmButtonText: '确定',
+      dangerouslyUseHTMLString: true,
+      type: 'info'
+    }).then(() => {
+    }).catch(() => {});
+  });
+}
+
 </script>
 
 <style lang='scss' scoped>