gongchunyi
4 天以前 be82fb8e6a3ce42786c42dc7c48c7baf67d1b49b
feat: 保存通知公告数据时进行APP消息推送
已添加7个文件
已修改5个文件
467 ■■■■■ 文件已修改
src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/GetuiConfig.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/SysUserClient.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysMenuMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/SysUserClientService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-new.yml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysMenuMapper.xml 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.GetuiConfig;
import com.ruoyi.project.system.domain.SysUserClient;
import com.ruoyi.project.system.service.SysUserClientService;
import com.ruoyi.common.utils.SecurityUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * ç”¨æˆ·å®‰å“设备管理控制层
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
@Api(tags = "用户设备绑定")
@RestController
@RequestMapping("/system/client")
public class SysUserClientController extends BaseController {
    @Autowired
    private SysUserClientService sysUserClientService;
    /**
     * æ·»åŠ /更新用户cid
     */
    @PostMapping("/addOrUpdateClientId")
    @ApiOperation("添加/更新用户cid")
    public AjaxResult addOrUpdateClientId(@RequestBody SysUserClient sysUserClient) {
        Long userId = SecurityUtils.getUserId();
        sysUserClient.setUserId(userId);
        boolean result = sysUserClientService.addOrUpdateClientId(sysUserClient);
        return result ? success() : error("设备绑定失败");
    }
}
src/main/java/com/ruoyi/project/system/domain/GetuiConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package com.ruoyi.project.system.domain;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * <p>
 * ä¸ªæŽ¨ (Unipush v2) æ¶ˆæ¯æŽ¨é€é…ç½®ç±»
 * </p>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
@Data
@Component
@ConfigurationProperties(prefix = "ruoyi.getui")
public class GetuiConfig {
    /**
     * AppID
     */
    private String appId;
    /**
     * AppKey
     */
    private String appKey;
    /**
     * MasterSecret
     */
    private String masterSecret;
    /**
     * ä¸ªæŽ¨ RESTful API åŸŸååœ°å€
     */
    private String domain;
    /**
     * ç¦»çº¿æŽ¨é€ Intent ç›®æ ‡ç»„件名
     * æ ¼å¼: åŒ…名/入口Activity名
     */
    private String intentComponent;
}
src/main/java/com/ruoyi/project/system/domain/SysUserClient.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package com.ruoyi.project.system.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
 * <br>
 * ç”¨æˆ·å®‰å“设备关联对象 sys_user_client
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user_client")
public class SysUserClient implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ç”¨æˆ·ID
     */
    @TableId(type = IdType.INPUT)
    private Long userId;
    /**
     * ä¸ªæŽ¨è®¾å¤‡æ ‡è¯† (CID)
     */
    private String cid;
    /**
     * æœ€åŽæ´»è·ƒæ—¶é—´
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
}
src/main/java/com/ruoyi/project/system/mapper/SysMenuMapper.java
@@ -122,4 +122,12 @@
     * @return ç»“æžœ
     */
    public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
    /**
     * æ ¹æ®è·¯ç”±åœ°å€æŸ¥è¯¢èœå•
     *
     * @param lastSegment è·¯ç”±åœ°å€
     * @return èœå•
     */
    SysMenu selectMenuByPath(String lastSegment);
}
src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.project.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.project.system.domain.SysUserClient;
/**
 * <br>
 * ç”¨æˆ·å®‰å“设备关联mapper
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
public interface SysUserClientMapper extends BaseMapper<SysUserClient> {
}
src/main/java/com/ruoyi/project/system/service/SysUserClientService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.project.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.project.system.domain.SysUserClient;
/**
 * <br>
 * ç”¨æˆ·å®‰å“设备关联接口
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
public interface SysUserClientService extends IService<SysUserClient> {
    boolean addOrUpdateClientId(SysUserClient sysUserClient);
}
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java
@@ -53,6 +53,9 @@
    @Lazy
    private ISysNoticeService sysNoticeService;
    @Autowired
    private UnipushService unipushService;
    /**
     * æŸ¥è¯¢å…¬å‘Šä¿¡æ¯
     *
@@ -146,6 +149,11 @@
        Long tenantId = SecurityUtils.getLoginUser().getTenantId();
        List<SysNotice> sysNotices = consigneeId.stream().map(it -> convertSysNotice(title, message, it,tenantId, jumpPath, userId)).collect(Collectors.toList());
        sysNoticeService.saveBatch(sysNotices);
        try {
            unipushService.sendClientMessage(sysNotices);
        } catch (Exception e) {
            log.error("APP推送通知失败,原因: {}", e);
        }
    }
    @Override
@@ -197,6 +205,11 @@
                .collect(Collectors.toList());
        sysNoticeService.saveBatch(collect);
        try {
            unipushService.sendClientMessage(collect);
        } catch (Exception e) {
            log.error("APP推送通知失败,原因: {}", e);
        }
    }
src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.project.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysUserClient;
import com.ruoyi.project.system.mapper.SysUserClientMapper;
import com.ruoyi.project.system.service.SysUserClientService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
 * ç”¨æˆ·å®‰å“设备关联接口实现类
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/2/9
 */
@Service
public class SysUserClientServiceImpl extends ServiceImpl<SysUserClientMapper, SysUserClient> implements SysUserClientService {
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addOrUpdateClientId(SysUserClient sysUserClient) {
        if (sysUserClient == null || sysUserClient.getUserId() == null || StringUtils.isEmpty(sysUserClient.getCid())) {
            return false;
        }
        String cid = sysUserClient.getCid();
        Long userId = sysUserClient.getUserId();
        remove(new LambdaQueryWrapper<SysUserClient>().eq(SysUserClient::getCid, cid).ne(SysUserClient::getUserId, userId));
        SysUserClient userClient = new SysUserClient();
        userClient.setUserId(userId);
        userClient.setCid(cid);
        userClient.setUpdateTime(new Date());
        return saveOrUpdate(userClient);
    }
}
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
package com.ruoyi.project.system.service.impl;
import com.getui.push.v2.sdk.ApiHelper;
import com.getui.push.v2.sdk.GtApiConfiguration;
import com.getui.push.v2.sdk.api.PushApi;
import com.getui.push.v2.sdk.common.ApiResult;
import com.getui.push.v2.sdk.dto.req.Audience;
import com.getui.push.v2.sdk.dto.req.message.PushChannel;
import com.getui.push.v2.sdk.dto.req.message.PushDTO;
import com.getui.push.v2.sdk.dto.req.message.PushMessage;
import com.getui.push.v2.sdk.dto.req.message.android.AndroidDTO;
import com.getui.push.v2.sdk.dto.req.message.android.ThirdNotification;
import com.getui.push.v2.sdk.dto.req.message.android.Ups;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.GetuiConfig;
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.domain.SysNotice;
import com.ruoyi.project.system.domain.SysUserClient;
import com.ruoyi.project.system.mapper.SysMenuMapper;
import com.ruoyi.project.system.service.SysUserClientService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
/**
 * APP消息推送服务
 *
 * @author deslrey
 * @version 1.4
 * @since 2026/2/9
 */
@Slf4j
@Component
public class UnipushService {
    @Autowired
    private SysMenuMapper sysMenuMapper;
    @Autowired
    private GetuiConfig getuiConfig;
    @Autowired
    private SysUserClientService userClientService;
    private PushApi pushApi;
    private static final String DEFAULT_APP_PAGE = "pages/index";
    @PostConstruct
    public void init() {
        GtApiConfiguration config = new GtApiConfiguration();
        config.setAppId(getuiConfig.getAppId());
        config.setAppKey(getuiConfig.getAppKey());
        config.setMasterSecret(getuiConfig.getMasterSecret());
        config.setDomain(getuiConfig.getDomain());
        ApiHelper apiHelper = ApiHelper.build(config);
        this.pushApi = apiHelper.creatApi(PushApi.class);
    }
    /**
     * æ‰¹é‡å‘送通知公告到移动端
     */
    @Async
    public void sendClientMessage(List<SysNotice> sysNoticeList) {
        if (sysNoticeList == null || sysNoticeList.isEmpty()) {
            return;
        }
        for (SysNotice sysNotice : sysNoticeList) {
            SysUserClient client = userClientService.getById(sysNotice.getConsigneeId());
            if (client == null || StringUtils.isEmpty(client.getCid())) {
                log.warn("用户 {} æœªç»‘定移动端 CID,跳过推送", sysNotice.getConsigneeId());
                continue;
            }
            // è½¬æ¢è·¯å¾„
            String appPath = convertWebPathToAppPath(sysNotice.getJumpPath());
            // æŽ¨é€
            sendRoutingPush(
                    client.getCid(),
                    sysNotice.getNoticeTitle(),
                    sysNotice.getRemark() != null ? sysNotice.getRemark() : sysNotice.getNoticeContent(),
                    appPath
            );
        }
    }
    /**
     * å°† Web ç«¯åˆ†å±‚全路径转换为 App ç«¯ç»„件路由
     */
    private String convertWebPathToAppPath(String webPath) {
        if (StringUtils.isEmpty(webPath)) {
            return DEFAULT_APP_PAGE;
        }
        String pathOnly = webPath;
        String queryString = "";
        if (webPath.contains("?")) {
            int index = webPath.indexOf("?");
            pathOnly = webPath.substring(0, index);
            queryString = webPath.substring(index);
        }
        String[] pathSegments = pathOnly.split("/");
        String lastSegment = "";
        for (int i = pathSegments.length - 1; i >= 0; i--) {
            if (StringUtils.isNotEmpty(pathSegments[i])) {
                lastSegment = pathSegments[i];
                break;
            }
        }
        if (StringUtils.isEmpty(lastSegment)) {
            return DEFAULT_APP_PAGE;
        }
        SysMenu menu = sysMenuMapper.selectMenuByPath(lastSegment);
        if (menu != null && StringUtils.isNotEmpty(menu.getAppComponent())) {
            String appComponent = menu.getAppComponent();
            if (appComponent.startsWith("/")) {
                appComponent = appComponent.substring(1);
            }
            return appComponent + queryString;
        }
        return DEFAULT_APP_PAGE;
    }
    /**
     * å‘送单人路由推送
     */
    private void sendRoutingPush(String cid, String title, String content, String targetPath) {
        log.info("准备推送消息: CID={}, Title={}, TargetPath={}", cid, title, targetPath);
        PushDTO<Audience> pushDTO = new PushDTO<>();
        pushDTO.setRequestId("REQ_" + System.currentTimeMillis());
        // åœ¨çº¿é€ä¼ å†…容
        PushMessage pushMessage = new PushMessage();
        String transmissionContent = String.format(
                "{\"title\":\"%s\",\"content\":\"%s\",\"payload\":\"%s\"}",
                title, content, targetPath
        );
        pushMessage.setTransmission(transmissionContent);
        pushDTO.setPushMessage(pushMessage);
        // æŽ¥æ”¶äºº
        Audience audience = new Audience();
        audience.addCid(cid);
        pushDTO.setAudience(audience);
        // ç¦»çº¿æŽ¨é€é€šé“
        pushDTO.setPushChannel(getPushChannel(title, content, targetPath));
        try {
            ApiResult<Map<String, Map<String, String>>> result = pushApi.pushToSingleByCid(pushDTO);
            if (result.isSuccess()) {
                log.info("Unipush æŽ¨é€æˆåŠŸ: CID={}", cid);
            } else {
                log.error("Unipush æŽ¨é€å¤±è´¥: CID={}, Code={}, Msg={}", cid, result.getCode(), result.getMsg());
            }
        } catch (Exception e) {
            log.error("Unipush æŽ¨é€å¼‚常: ", e);
        }
    }
    @NotNull
    private PushChannel getPushChannel(String title, String content, String targetPath) {
        PushChannel pushChannel = new PushChannel();
        AndroidDTO androidDTO = new AndroidDTO();
        Ups ups = new Ups();
        ThirdNotification thirdNotification = new ThirdNotification();
        thirdNotification.setTitle(title);
        thirdNotification.setBody(content);
        thirdNotification.setClickType("intent");
        String intent = "intent:#Intent;launchFlags=0x04000000;"
                + "component=" + getuiConfig.getIntentComponent() + ";"
                + "S.UP-OL-P9=true;"
                + "S.path=" + targetPath + ";"
                + "S.payload=" + targetPath + ";"
                + "end";
        thirdNotification.setIntent(intent);
        ups.setNotification(thirdNotification);
        androidDTO.setUps(ups);
        pushChannel.setAndroid(androidDTO);
        return pushChannel;
    }
}
src/main/resources/application-dev.yml
@@ -15,6 +15,16 @@
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: DEV
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
src/main/resources/application-new.yml
@@ -15,6 +15,16 @@
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
src/main/resources/mapper/system/SysMenuMapper.xml
@@ -85,7 +85,7 @@
        where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0
        order by m.parent_id, m.order_num
    </select>
    <select id="selectMenuListByRoleId" resultType="Long">
        select m.menu_id
        from sys_menu m
@@ -133,7 +133,14 @@
        <include refid="selectMenuVo"/>
        where menu_name=#{menuName} and parent_id = #{parentId} limit 1
    </select>
    <select id="selectMenuByPath" resultType="com.ruoyi.project.system.domain.SysMenu" parameterType="java.lang.String">
        SELECT menu_id, menu_name, parent_id, path, app_component, status
        FROM sys_menu
        WHERE path = #{path}
          AND status = '0' LIMIT 1
    </select>
    <update id="updateMenu" parameterType="com.ruoyi.project.system.domain.SysMenu">
        update sys_menu
        <set>