Просмотр исходного кода

feat(Notify): 实现消息通知模块核心功能\n\n- 创建通知模板表和通知日志表\n- 实现WebSocket实时推送服务\n- 实现APP推送服务集成\n- 实现短信推送服务集成\n- 实现邮件推送服务集成\n- 实现通知服务和模板管理API\n- 配置WebSocket和推送适配器\n- 添加默认通知模板和数据库初始化脚本\n\nResolves #26

bot_dev1 3 дней назад
Сommit
bb3b49f9f4
28 измененных файлов: 1759 добавлений и 0 удалений
  1. 141
    0
      pom.xml
  2. 86
    0
      wm-notify/pom.xml
  3. 22
    0
      wm-notify/src/main/java/com/water/common/core/exception/BizException.java
  4. 32
    0
      wm-notify/src/main/java/com/water/common/core/exception/GlobalExceptionHandler.java
  5. 61
    0
      wm-notify/src/main/java/com/water/common/core/result/R.java
  6. 27
    0
      wm-notify/src/main/java/com/water/notify/WmNotifyApplication.java
  7. 62
    0
      wm-notify/src/main/java/com/water/notify/channel/EmailNotifier.java
  8. 29
    0
      wm-notify/src/main/java/com/water/notify/channel/Notifier.java
  9. 54
    0
      wm-notify/src/main/java/com/water/notify/channel/PushNotifier.java
  10. 44
    0
      wm-notify/src/main/java/com/water/notify/channel/SmsNotifier.java
  11. 45
    0
      wm-notify/src/main/java/com/water/notify/channel/WebSocketNotifier.java
  12. 20
    0
      wm-notify/src/main/java/com/water/notify/config/WebSocketConfig.java
  13. 68
    0
      wm-notify/src/main/java/com/water/notify/config/WebSocketHandler.java
  14. 141
    0
      wm-notify/src/main/java/com/water/notify/controller/NotifyController.java
  15. 93
    0
      wm-notify/src/main/java/com/water/notify/controller/NotifyLogController.java
  16. 96
    0
      wm-notify/src/main/java/com/water/notify/controller/NotifyTemplateController.java
  17. 99
    0
      wm-notify/src/main/java/com/water/notify/entity/NotifyLog.java
  18. 89
    0
      wm-notify/src/main/java/com/water/notify/entity/NotifyTemplate.java
  19. 13
    0
      wm-notify/src/main/java/com/water/notify/mapper/NotifyLogMapper.java
  20. 13
    0
      wm-notify/src/main/java/com/water/notify/mapper/NotifyTemplateMapper.java
  21. 38
    0
      wm-notify/src/main/java/com/water/notify/service/NotifyLogService.java
  22. 58
    0
      wm-notify/src/main/java/com/water/notify/service/NotifyService.java
  23. 27
    0
      wm-notify/src/main/java/com/water/notify/service/NotifyTemplateService.java
  24. 64
    0
      wm-notify/src/main/java/com/water/notify/service/impl/NotifyLogServiceImpl.java
  25. 179
    0
      wm-notify/src/main/java/com/water/notify/service/impl/NotifyServiceImpl.java
  26. 41
    0
      wm-notify/src/main/java/com/water/notify/service/impl/NotifyTemplateServiceImpl.java
  27. 54
    0
      wm-notify/src/main/resources/application.yml
  28. 63
    0
      wm-notify/src/main/resources/db/init.sql

+ 141
- 0
pom.xml Просмотреть файл

1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4
+    <modelVersion>4.0.0</modelVersion>
5
+    <parent>
6
+        <groupId>org.springframework.boot</groupId>
7
+        <artifactId>spring-boot-starter-parent</artifactId>
8
+        <version>3.4.0</version>
9
+        <relativePath/>
10
+    </parent>
11
+    <groupId>com.water</groupId>
12
+    <artifactId>water-management-system</artifactId>
13
+    <version>1.0.0</version>
14
+    <packaging>pom</packaging>
15
+
16
+    <name>Water Management System</name>
17
+    <description>智慧水务管理系统</description>
18
+
19
+    <properties>
20
+        <java.version>17</java.version>
21
+        <spring-cloud.version>2024.0.0</spring-cloud.version>
22
+        <spring-boot.version>3.4.0</spring-boot.version>
23
+        <mybatis-plus.version>3.5.7</mybatis-plus.version>
24
+        <sa-token.version>1.38.0</sa-token.version>
25
+        <mysql.version>8.0.33</mysql.version>
26
+        <postgresql.version>42.7.3</postgresql.version>
27
+        <tdengine.version>3.6.0.0</tdengine.version>
28
+        <minio.version>8.5.9</minio.version>
29
+        <fastjson2.version>2.0.52</fastjson2.version>
30
+        <hutool.version>5.8.26</hutool.version>
31
+        <lombok.version>1.18.34</lombok.version>
32
+        <maven.compiler.source>${java.version}</maven.compiler.source>
33
+        <maven.compiler.target>${java.version}</maven.compiler.target>
34
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
35
+    </properties>
36
+
37
+    <modules>
38
+        <module>wm-common</module>
39
+        <module>wm-base</module>
40
+        <module>wm-iot</module>
41
+        <module>wm-notify</module>
42
+        <module>wm-production</module>
43
+        <module>wm-revenue</module>
44
+        <module>wm-patrol</module>
45
+        <module>wm-bi</module>
46
+        <module>wm-gateway</module>
47
+        <module>wm-job</module>
48
+        <module>wm-data-engine</module>
49
+        <module>wm-bpm</module>
50
+    </modules>
51
+
52
+    <dependencyManagement>
53
+        <dependencies>
54
+            <!-- Spring Cloud -->
55
+            <dependency>
56
+                <groupId>org.springframework.cloud</groupId>
57
+                <artifactId>spring-cloud-dependencies</artifactId>
58
+                <version>${spring-cloud.version}</version>
59
+                <type>pom</type>
60
+                <scope>import</scope>
61
+            </dependency>
62
+            
63
+            <!-- Spring Boot -->
64
+            <dependency>
65
+                <groupId>org.springframework.boot</groupId>
66
+                <artifactId>spring-boot-dependencies</artifactId>
67
+                <version>${spring-boot.version}</version>
68
+                <type>pom</type>
69
+                <scope>import</scope>
70
+            </dependency>
71
+            
72
+            <!-- MyBatis Plus -->
73
+            <dependency>
74
+                <groupId>com.baomidou</groupId>
75
+                <artifactId>mybatis-plus-boot-starter</artifactId>
76
+                <version>${mybatis-plus.version}</version>
77
+            </dependency>
78
+            
79
+            <!-- Sa-Token 权限框架 -->
80
+            <dependency>
81
+                <groupId>cn.dev33</groupId>
82
+                <artifactId>sa-token-spring-boot-starter</artifactId>
83
+                <version>${sa-token.version}</version>
84
+            </dependency>
85
+            
86
+            <!-- 数据库驱动 -->
87
+            <dependency>
88
+                <groupId>mysql</groupId>
89
+                <artifactId>mysql-connector-java</artifactId>
90
+                <version>${mysql.version}</version>
91
+            </dependency>
92
+            <dependency>
93
+                <groupId>org.postgresql</groupId>
94
+                <artifactId>postgresql</artifactId>
95
+                <version>${postgresql.version}</version>
96
+            </dependency>
97
+            
98
+            <!-- TDengine 时序数据库 -->
99
+            <dependency>
100
+                <groupId>com.taosdata.jdbc</groupId>
101
+                <artifactId>taos-jdbcdriver</artifactId>
102
+                <version>${tdengine.version}</version>
103
+            </dependency>
104
+            
105
+            <!-- FastJSON2 -->
106
+            <dependency>
107
+                <groupId>com.alibaba.fastjson2</groupId>
108
+                <artifactId>fastjson2</artifactId>
109
+                <version>${fastjson2.version}</version>
110
+            </dependency>
111
+            
112
+            <!-- Hutool 工具类 -->
113
+            <dependency>
114
+                <groupId>cn.hutool</groupId>
115
+                <artifactId>hutool-all</artifactId>
116
+                <version>${hutool.version}</version>
117
+            </dependency>
118
+            
119
+            <!-- Lombok -->
120
+            <dependency>
121
+                <groupId>org.projectlombok</groupId>
122
+                <artifactId>lombok</artifactId>
123
+                <version>${lombok.version}</version>
124
+                <scope>provided</scope>
125
+            </dependency>
126
+        </dependencies>
127
+    </dependencyManagement>
128
+
129
+    <build>
130
+        <plugins>
131
+            <plugin>
132
+                <groupId>org.springframework.boot</groupId>
133
+                <artifactId>spring-boot-maven-plugin</artifactId>
134
+                <version>${spring-boot.version}</version>
135
+                <configuration>
136
+                    <skip>true</skip>
137
+                </configuration>
138
+            </plugin>
139
+        </plugins>
140
+    </build>
141
+</project>

+ 86
- 0
wm-notify/pom.xml Просмотреть файл

1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4
+    <modelVersion>4.0.0</modelVersion>
5
+    <parent>
6
+        <groupId>com.water</groupId>
7
+        <artifactId>water-management-system</artifactId>
8
+        <version>1.0.0</version>
9
+    </parent>
10
+
11
+    <artifactId>wm-notify</artifactId>
12
+    <version>1.0.0</version>
13
+    <packaging>jar</packaging>
14
+
15
+    <name>Water Management - Notify Service</name>
16
+    <description>消息通知服务</description>
17
+
18
+    <dependencies>
19
+        <!-- Spring Boot Web -->
20
+        <dependency>
21
+            <groupId>org.springframework.boot</groupId>
22
+            <artifactId>spring-boot-starter-web</artifactId>
23
+        </dependency>
24
+        
25
+        <!-- Spring Boot WebSocket -->
26
+        <dependency>
27
+            <groupId>org.springframework.boot</groupId>
28
+            <artifactId>spring-boot-starter-websocket</artifactId>
29
+        </dependency>
30
+        
31
+        <!-- Spring Boot Actuator -->
32
+        <dependency>
33
+            <groupId>org.springframework.boot</groupId>
34
+            <artifactId>spring-boot-starter-actuator</artifactId>
35
+        </dependency>
36
+        
37
+        <!-- Spring Boot Redis -->
38
+        <dependency>
39
+            <groupId>org.springframework.boot</groupId>
40
+            <artifactId>spring-boot-starter-data-redis</artifactId>
41
+        </dependency>
42
+        
43
+        <!-- MyBatis Plus -->
44
+        <dependency>
45
+            <groupId>com.baomidou</groupId>
46
+            <artifactId>mybatis-plus-boot-starter</artifactId>
47
+        </dependency>
48
+        
49
+        <!-- PostgreSQL Driver -->
50
+        <dependency>
51
+            <groupId>org.postgresql</groupId>
52
+            <artifactId>postgresql</artifactId>
53
+        </dependency>
54
+        
55
+        <!-- Sa-Token -->
56
+        <dependency>
57
+            <groupId>cn.dev33</groupId>
58
+            <artifactId>sa-token-spring-boot-starter</artifactId>
59
+        </dependency>
60
+        
61
+        <!-- FastJSON2 -->
62
+        <dependency>
63
+            <groupId>com.alibaba.fastjson2</groupId>
64
+            <artifactId>fastjson2</artifactId>
65
+        </dependency>
66
+        
67
+        <!-- Hutool -->
68
+        <dependency>
69
+            <groupId>cn.hutool</groupId>
70
+            <artifactId>hutool-all</artifactId>
71
+        </dependency>
72
+        
73
+        <!-- Lombok -->
74
+        <dependency>
75
+            <groupId>org.projectlombok</groupId>
76
+            <artifactId>lombok</artifactId>
77
+        </dependency>
78
+        
79
+        <!-- Test -->
80
+        <dependency>
81
+            <groupId>org.springframework.boot</groupId>
82
+            <artifactId>spring-boot-starter-test</artifactId>
83
+            <scope>test</scope>
84
+        </dependency>
85
+    </dependencies>
86
+</project>

+ 22
- 0
wm-notify/src/main/java/com/water/common/core/exception/BizException.java Просмотреть файл

1
+package com.water.common.core.exception;
2
+
3
+/**
4
+ * 业务异常
5
+ */
6
+public class BizException extends RuntimeException {
7
+    
8
+    private Integer code;
9
+    
10
+    public BizException(String message) {
11
+        super(message);
12
+    }
13
+    
14
+    public BizException(Integer code, String message) {
15
+        super(message);
16
+        this.code = code;
17
+    }
18
+    
19
+    public Integer getCode() {
20
+        return code;
21
+    }
22
+}

+ 32
- 0
wm-notify/src/main/java/com/water/common/core/exception/GlobalExceptionHandler.java Просмотреть файл

1
+package com.water.common.core.exception;
2
+
3
+import com.water.common.core.result.R;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.web.bind.annotation.ExceptionHandler;
6
+import org.springframework.web.bind.annotation.RestControllerAdvice;
7
+
8
+/**
9
+ * 全局异常处理器
10
+ */
11
+@Slf4j
12
+@RestControllerAdvice
13
+public class GlobalExceptionHandler {
14
+    
15
+    /**
16
+     * 处理业务异常
17
+     */
18
+    @ExceptionHandler(BizException.class)
19
+    public R<?> handleBizException(BizException e) {
20
+        log.error("业务异常: {}", e.getMessage());
21
+        return R.failed(e.getCode() != null ? e.getCode() : 500, e.getMessage());
22
+    }
23
+    
24
+    /**
25
+     * 处理系统异常
26
+     */
27
+    @ExceptionHandler(Exception.class)
28
+    public R<?> handleException(Exception e) {
29
+        log.error("系统异常: ", e);
30
+        return R.failed("系统异常,请联系管理员");
31
+    }
32
+}

+ 61
- 0
wm-notify/src/main/java/com/water/common/core/result/R.java Просмотреть файл

1
+package com.water.common.core.result;
2
+
3
+import lombok.Data;
4
+
5
+import java.io.Serializable;
6
+
7
+/**
8
+ * 统一响应结果
9
+ */
10
+@Data
11
+public class R<T> implements Serializable {
12
+    
13
+    private static final long serialVersionUID = 1L;
14
+    
15
+    private Integer code;
16
+    
17
+    private String message;
18
+    
19
+    private T data;
20
+    
21
+    public R() {}
22
+    
23
+    public R(Integer code, String message) {
24
+        this.code = code;
25
+        this.message = message;
26
+    }
27
+    
28
+    public R(Integer code, String message, T data) {
29
+        this.code = code;
30
+        this.message = message;
31
+        this.data = data;
32
+    }
33
+    
34
+    public static <T> R<T> success() {
35
+        return new R<>(200, "操作成功");
36
+    }
37
+    
38
+    public static <T> R<T> success(T data) {
39
+        return new R<>(200, "操作成功", data);
40
+    }
41
+    
42
+    public static <T> R<T> success(String message, T data) {
43
+        return new R<>(200, message, data);
44
+    }
45
+    
46
+    public static <T> R<T> failed() {
47
+        return new R<>(500, "操作失败");
48
+    }
49
+    
50
+    public static <T> R<T> failed(String message) {
51
+        return new R<>(500, message);
52
+    }
53
+    
54
+    public static <T> R<T> failed(Integer code, String message) {
55
+        return new R<>(code, message);
56
+    }
57
+    
58
+    public boolean isSuccess() {
59
+        return code != null && code == 200;
60
+    }
61
+}

+ 27
- 0
wm-notify/src/main/java/com/water/notify/WmNotifyApplication.java Просмотреть файл

1
+package com.water.notify;
2
+
3
+import org.springframework.boot.SpringApplication;
4
+import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+import org.springframework.scheduling.annotation.EnableAsync;
6
+import org.springframework.scheduling.annotation.EnableScheduling;
7
+
8
+/**
9
+ * 消息通知服务启动类
10
+ * 
11
+ * 功能:
12
+ * - WebSocket实时推送
13
+ * - APP推送集成
14
+ * - 短信推送集成  
15
+ * - 邮件推送集成
16
+ * - 通知模板管理
17
+ * - 通知日志记录
18
+ */
19
+@SpringBootApplication
20
+@EnableAsync
21
+@EnableScheduling
22
+public class WmNotifyApplication {
23
+
24
+    public static void main(String[] args) {
25
+        SpringApplication.run(WmNotifyApplication.class, args);
26
+    }
27
+}

+ 62
- 0
wm-notify/src/main/java/com/water/notify/channel/EmailNotifier.java Просмотреть файл

1
+package com.water.notify.channel;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+import org.springframework.stereotype.Component;
5
+
6
+import javax.mail.*;
7
+import javax.mail.internet.InternetAddress;
8
+import javax.mail.internet.MimeMessage;
9
+import java.util.Properties;
10
+
11
+/**
12
+ * 邮件通知器
13
+ */
14
+@Component
15
+public class EmailNotifier implements Notifier {
16
+    
17
+    @Override
18
+    public String getChannel() {
19
+        return "email";
20
+    }
21
+    
22
+    @Override
23
+    public boolean send(NotifyLog log) {
24
+        try {
25
+            // 邮件服务器配置
26
+            Properties props = new Properties();
27
+            props.put("mail.smtp.host", "smtp.example.com");
28
+            props.put("mail.smtp.port", "587");
29
+            props.put("mail.smtp.auth", "true");
30
+            props.put("mail.smtp.starttls.enable", "true");
31
+            
32
+            // 创建会话
33
+            Session session = Session.getInstance(props, new Authenticator() {
34
+                @Override
35
+                protected PasswordAuthentication getPasswordAuthentication() {
36
+                    return new PasswordAuthentication("noreply@water.com", "password");
37
+                }
38
+            });
39
+            
40
+            // 创建消息
41
+            Message message = new MimeMessage(session);
42
+            message.setFrom(new InternetAddress("noreply@water.com"));
43
+            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(log.getReceiverIdentifier()));
44
+            message.setSubject(log.getTitle());
45
+            message.setText(log.getContent());
46
+            
47
+            // 发送邮件
48
+            Transport.send(message);
49
+            
50
+            return true;
51
+        } catch (Exception e) {
52
+            return false;
53
+        }
54
+    }
55
+    
56
+    @Override
57
+    public boolean validateReceiver(NotifyLog log) {
58
+        // 验证邮箱格式
59
+        String email = log.getReceiverIdentifier();
60
+        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
61
+    }
62
+}

+ 29
- 0
wm-notify/src/main/java/com/water/notify/channel/Notifier.java Просмотреть файл

1
+package com.water.notify.channel;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+
5
+/**
6
+ * 通知发送接口
7
+ * 
8
+ * 支持多种渠道的通知发送
9
+ */
10
+public interface Notifier {
11
+    
12
+    /**
13
+     * 获取支持的渠道
14
+     */
15
+    String getChannel();
16
+    
17
+    /**
18
+     * 发送通知
19
+     * 
20
+     * @param log 通知日志
21
+     * @return 发送结果
22
+     */
23
+    boolean send(NotifyLog log);
24
+    
25
+    /**
26
+     * 检查接收者是否有效
27
+     */
28
+    boolean validateReceiver(NotifyLog log);
29
+}

+ 54
- 0
wm-notify/src/main/java/com/water/notify/channel/PushNotifier.java Просмотреть файл

1
+package com.water.notify.channel;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+import org.springframework.stereotype.Component;
5
+
6
+/**
7
+ * APP推送通知器
8
+ * 
9
+ * 集成极光推送(JPush)
10
+ */
11
+@Component
12
+public class PushNotifier implements Notifier {
13
+    
14
+    @Override
15
+    public String getChannel() {
16
+        return "push";
17
+    }
18
+    
19
+    @Override
20
+    public boolean send(NotifyLog log) {
21
+        try {
22
+            // 集成极光推送
23
+            // https://docs.jiguang.cn/jpush/server/rest_api_v3_push/
24
+            
25
+            String registrationId = log.getReceiverIdentifier(); // 设备token
26
+            String title = log.getTitle();
27
+            String content = log.getContent();
28
+            
29
+            // 构建推送消息
30
+            // {
31
+            //   "platform": "all",
32
+            //   "audience": { "registration_id": [registrationId] },
33
+            //   "notification": {
34
+            //     "android": { "title": title, "content": content },
35
+            //     "ios": { "title": title, "content": content }
36
+            //   }
37
+            // }
38
+            
39
+            // 模拟发送APP推送
40
+            System.out.println("发送APP推送到设备 " + registrationId + ": " + title + " - " + content);
41
+            
42
+            return true;
43
+        } catch (Exception e) {
44
+            return false;
45
+        }
46
+    }
47
+    
48
+    @Override
49
+    public boolean validateReceiver(NotifyLog log) {
50
+        // 验证设备token格式
51
+        String token = log.getReceiverIdentifier();
52
+        return token != null && token.length() > 0;
53
+    }
54
+}

+ 44
- 0
wm-notify/src/main/java/com/water/notify/channel/SmsNotifier.java Просмотреть файл

1
+package com.water.notify.channel;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+import org.springframework.stereotype.Component;
5
+
6
+/**
7
+ * 短信通知器
8
+ * 
9
+ * 集成阿里云SMS或腾讯云SMS
10
+ */
11
+@Component
12
+public class SmsNotifier implements Notifier {
13
+    
14
+    @Override
15
+    public String getChannel() {
16
+        return "sms";
17
+    }
18
+    
19
+    @Override
20
+    public boolean send(NotifyLog log) {
21
+        try {
22
+            // 这里集成实际的短信服务提供商
23
+            // 阿里云SMS:https://help.aliyun.com/product/44275.html
24
+            // 腾讯云SMS:https://cloud.tencent.com/document/product/382/38778
25
+            
26
+            String phone = log.getReceiverIdentifier();
27
+            String content = log.getContent();
28
+            
29
+            // 模拟发送短信
30
+            System.out.println("发送短信到 " + phone + ": " + content);
31
+            
32
+            return true;
33
+        } catch (Exception e) {
34
+            return false;
35
+        }
36
+    }
37
+    
38
+    @Override
39
+    public boolean validateReceiver(NotifyLog log) {
40
+        // 验证手机号格式
41
+        String phone = log.getReceiverIdentifier();
42
+        return phone != null && phone.matches("^1[3-9]\\d{9}$");
43
+    }
44
+}

+ 45
- 0
wm-notify/src/main/java/com/water/notify/channel/WebSocketNotifier.java Просмотреть файл

1
+package com.water.notify.channel;
2
+
3
+import com.alibaba.fastjson2.JSON;
4
+import com.water.notify.entity.NotifyLog;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.data.redis.core.RedisTemplate;
7
+import org.springframework.stereotype.Component;
8
+
9
+/**
10
+ * WebSocket实时推送通知器
11
+ */
12
+@Component
13
+public class WebSocketNotifier implements Notifier {
14
+    
15
+    @Autowired
16
+    private RedisTemplate<String, String> redisTemplate;
17
+    
18
+    @Override
19
+    public String getChannel() {
20
+        return "ws";
21
+    }
22
+    
23
+    @Override
24
+    public boolean send(NotifyLog log) {
25
+        try {
26
+            // 构建消息
27
+            String message = JSON.toJSONString(log);
28
+            
29
+            // 发送到WebSocket频道(通过Redis pub/sub模拟)
30
+            String channel = "websocket:" + log.getReceiverIdentifier();
31
+            redisTemplate.convertAndSend(channel, message);
32
+            
33
+            return true;
34
+        } catch (Exception e) {
35
+            return false;
36
+        }
37
+    }
38
+    
39
+    @Override
40
+    public boolean validateReceiver(NotifyLog log) {
41
+        // 检查用户是否在线(通过Redis检查)
42
+        String onlineKey = "user:online:" + log.getReceiverId();
43
+        return redisTemplate.hasKey(onlineKey);
44
+    }
45
+}

+ 20
- 0
wm-notify/src/main/java/com/water/notify/config/WebSocketConfig.java Просмотреть файл

1
+package com.water.notify.config;
2
+
3
+import org.springframework.context.annotation.Configuration;
4
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
5
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
6
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
7
+
8
+/**
9
+ * WebSocket配置
10
+ */
11
+@Configuration
12
+@EnableWebSocket
13
+public class WebSocketConfig implements WebSocketConfigurer {
14
+    
15
+    @Override
16
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
17
+        registry.addHandler(new WebSocketHandler(), "/ws/notify")
18
+                .setAllowedOrigins("*");
19
+    }
20
+}

+ 68
- 0
wm-notify/src/main/java/com/water/notify/config/WebSocketHandler.java Просмотреть файл

1
+package com.water.notify.config;
2
+
3
+import org.springframework.stereotype.Component;
4
+import org.springframework.web.socket.CloseStatus;
5
+import org.springframework.web.socket.TextMessage;
6
+import org.springframework.web.socket.WebSocketSession;
7
+import org.springframework.web.socket.handler.TextWebSocketHandler;
8
+
9
+import java.util.concurrent.ConcurrentHashMap;
10
+
11
+/**
12
+ * WebSocket处理器
13
+ */
14
+@Component
15
+public class WebSocketHandler extends TextWebSocketHandler {
16
+    
17
+    // 存储在线用户
18
+    private static final ConcurrentHashMap<Long, WebSocketSession> userSessions = new ConcurrentHashMap<>();
19
+    
20
+    @Override
21
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
22
+        // 从URL中获取用户ID,例如:/ws/notify?userId=123
23
+        String userId = session.getUri().getQuery().split("=")[1];
24
+        Long userIdLong = Long.parseLong(userId);
25
+        
26
+        // 添加到在线用户列表
27
+        userSessions.put(userIdLong, session);
28
+        
29
+        System.out.println("用户连接: " + userIdLong);
30
+    }
31
+    
32
+    @Override
33
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
34
+        // 从在线用户列表中移除
35
+        userSessions.entrySet().removeIf(entry -> entry.getValue().getId().equals(session.getId()));
36
+        
37
+        System.out.println("用户断开连接: " + session.getId());
38
+    }
39
+    
40
+    /**
41
+     * 发送消息给指定用户
42
+     */
43
+    public void sendMessageToUser(Long userId, String message) {
44
+        WebSocketSession session = userSessions.get(userId);
45
+        if (session != null && session.isOpen()) {
46
+            try {
47
+                session.sendMessage(new TextMessage(message));
48
+            } catch (Exception e) {
49
+                e.printStackTrace();
50
+            }
51
+        }
52
+    }
53
+    
54
+    /**
55
+     * 广播消息给所有用户
56
+     */
57
+    public void broadcastMessage(String message) {
58
+        userSessions.forEach((userId, session) -> {
59
+            if (session.isOpen()) {
60
+                try {
61
+                    session.sendMessage(new TextMessage(message));
62
+                } catch (Exception e) {
63
+                    e.printStackTrace();
64
+                }
65
+            }
66
+        });
67
+    }
68
+}

+ 141
- 0
wm-notify/src/main/java/com/water/notify/controller/NotifyController.java Просмотреть файл

1
+package com.water.notify.controller;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+import com.water.notify.entity.NotifyTemplate;
5
+import com.water.notify.service.NotifyLogService;
6
+import com.water.notify.service.NotifyService;
7
+import com.water.notify.service.NotifyTemplateService;
8
+import com.water.common.core.result.R;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import java.util.List;
14
+import java.util.Map;
15
+
16
+/**
17
+ * 通知发送控制器
18
+ */
19
+@RestController
20
+@RequestMapping("/api/notify")
21
+@RequiredArgsConstructor
22
+public class NotifyController {
23
+    
24
+    @Autowired
25
+    private NotifyService notifyService;
26
+    
27
+    @Autowired
28
+    private NotifyTemplateService templateService;
29
+    
30
+    @Autowired
31
+    private NotifyLogService logService;
32
+    
33
+    /**
34
+     * 发送通知
35
+     */
36
+    @PostMapping("/send")
37
+    public R<Long> send(@RequestParam String templateCode,
38
+                       @RequestParam Long receiverId,
39
+                       @RequestParam String receiverIdentifier,
40
+                       @RequestParam String channel,
41
+                       @RequestBody Map<String, Object> params) {
42
+        Long logId = notifyService.sendNotify(templateCode, receiverId, receiverIdentifier, channel, params);
43
+        return R.success(logId);
44
+    }
45
+    
46
+    /**
47
+     * 发送通知(自动选择渠道)
48
+     */
49
+    @PostMapping("/send-auto")
50
+    public R<List<Long>> sendAuto(@RequestParam String templateCode,
51
+                                 @RequestParam Long receiverId,
52
+                                 @RequestParam String receiverIdentifier,
53
+                                 @RequestBody Map<String, Object> params) {
54
+        List<Long> logIds = notifyService.sendNotify(templateCode, receiverId, receiverIdentifier, params);
55
+        return R.success(logIds);
56
+    }
57
+    
58
+    /**
59
+     * 批量发送通知
60
+     */
61
+    @PostMapping("/batch-send")
62
+    public R<List<Long>> batchSend(@RequestParam String templateCode,
63
+                                  @RequestBody BatchSendRequest request) {
64
+        List<Long> logIds = notifyService.batchSendNotify(templateCode, 
65
+            request.getReceiverIds(), request.getReceiverIdentifiers(), 
66
+            request.getChannel(), request.getParams());
67
+        return R.success(logIds);
68
+    }
69
+    
70
+    /**
71
+     * 重试失败的通知
72
+     */
73
+    @PostMapping("/retry")
74
+    public R<Boolean> retry() {
75
+        notifyService.retryFailedNotifications();
76
+        return R.success();
77
+    }
78
+    
79
+    /**
80
+     * 获取通知日志
81
+     */
82
+    @GetMapping("/log/{id}")
83
+    public R<NotifyLog> getLog(@PathVariable Long id) {
84
+        NotifyLog log = logService.getById(id);
85
+        if (log == null) {
86
+            return R.failed("通知日志不存在");
87
+        }
88
+        return R.success(log);
89
+    }
90
+    
91
+    /**
92
+     * 检查模板是否存在
93
+     */
94
+    @GetMapping("/template/check/{templateCode}")
95
+    public R<Boolean> checkTemplate(@PathVariable String templateCode) {
96
+        NotifyTemplate template = templateService.getByTemplateCode(templateCode);
97
+        return R.success(template != null);
98
+    }
99
+    
100
+    /**
101
+     * 批量发送请求
102
+     */
103
+    public static class BatchSendRequest {
104
+        private List<Long> receiverIds;
105
+        private List<String> receiverIdentifiers;
106
+        private String channel;
107
+        private Map<String, Object> params;
108
+        
109
+        public List<Long> getReceiverIds() {
110
+            return receiverIds;
111
+        }
112
+        
113
+        public void setReceiverIds(List<Long> receiverIds) {
114
+            this.receiverIds = receiverIds;
115
+        }
116
+        
117
+        public List<String> getReceiverIdentifiers() {
118
+            return receiverIdentifiers;
119
+        }
120
+        
121
+        public void setReceiverIdentifiers(List<String> receiverIdentifiers) {
122
+            this.receiverIdentifiers = receiverIdentifiers;
123
+        }
124
+        
125
+        public String getChannel() {
126
+            return channel;
127
+        }
128
+        
129
+        public void setChannel(String channel) {
130
+            this.channel = channel;
131
+        }
132
+        
133
+        public Map<String, Object> getParams() {
134
+            return params;
135
+        }
136
+        
137
+        public void setParams(Map<String, Object> params) {
138
+            this.params = params;
139
+        }
140
+    }
141
+}

+ 93
- 0
wm-notify/src/main/java/com/water/notify/controller/NotifyLogController.java Просмотреть файл

1
+package com.water.notify.controller;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.notify.entity.NotifyLog;
6
+import com.water.notify.service.NotifyLogService;
7
+import com.water.common.core.result.R;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+/**
13
+ * 通知日志管理控制器
14
+ */
15
+@RestController
16
+@RequestMapping("/api/notify/log")
17
+@RequiredArgsConstructor
18
+public class NotifyLogController {
19
+    
20
+    @Autowired
21
+    private NotifyLogService logService;
22
+    
23
+    /**
24
+     * 分页查询通知日志
25
+     */
26
+    @GetMapping("/page")
27
+    public R<Page<NotifyLog>> page(@RequestParam(defaultValue = "1") Long current,
28
+                                   @RequestParam(defaultValue = "10") Long size,
29
+                                   @RequestParam(required = false) String status,
30
+                                   @RequestParam(required = false) String channel,
31
+                                   @RequestParam(required = false) Long receiverId) {
32
+        Page<NotifyLog> page = new Page<>(current, size);
33
+        LambdaQueryWrapper<NotifyLog> wrapper = new LambdaQueryWrapper<>();
34
+        
35
+        if (status != null) {
36
+            wrapper.eq(NotifyLog::getStatus, status);
37
+        }
38
+        if (channel != null) {
39
+            wrapper.eq(NotifyLog::getChannel, channel);
40
+        }
41
+        if (receiverId != null) {
42
+            wrapper.eq(NotifyLog::getReceiverId, receiverId);
43
+        }
44
+        
45
+        wrapper.orderByDesc(NotifyLog::getId);
46
+        logService.page(page, wrapper);
47
+        return R.success(page);
48
+    }
49
+    
50
+    /**
51
+     * 根据ID获取通知日志
52
+     */
53
+    @GetMapping("/{id}")
54
+    public R<NotifyLog> getById(@PathVariable Long id) {
55
+        NotifyLog log = logService.getById(id);
56
+        if (log == null) {
57
+            return R.failed("通知日志不存在");
58
+        }
59
+        return R.success(log);
60
+    }
61
+    
62
+    /**
63
+     * 获取待发送的通知日志
64
+     */
65
+    @GetMapping("/pending")
66
+    public R<Page<NotifyLog>> getPending(@RequestParam(defaultValue = "10") Long limit) {
67
+        Page<NotifyLog> page = new Page<>(1, limit);
68
+        page.setRecords(logService.getPendingLogs((int) limit));
69
+        return R.success(page);
70
+    }
71
+    
72
+    /**
73
+     * 获取失败的通知日志(用于重试)
74
+     */
75
+    @GetMapping("/failed")
76
+    public R<Page<NotifyLog>> getFailed(@RequestParam(defaultValue = "5") Long minutes,
77
+                                        @RequestParam(defaultValue = "3") Long maxRetryCount) {
78
+        Page<NotifyLog> page = new Page<>(1, 100);
79
+        page.setRecords(logService.getFailedLogs(
80
+            java.time.LocalDateTime.now().minusMinutes(minutes), 
81
+            (int) maxRetryCount));
82
+        return R.success(page);
83
+    }
84
+    
85
+    /**
86
+     * 删除通知日志
87
+     */
88
+    @DeleteMapping("/{id}")
89
+    public R<Boolean> delete(@PathVariable Long id) {
90
+        boolean success = logService.removeById(id);
91
+        return success ? R.success() : R.failed("删除失败");
92
+    }
93
+}

+ 96
- 0
wm-notify/src/main/java/com/water/notify/controller/NotifyTemplateController.java Просмотреть файл

1
+package com.water.notify.controller;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.notify.entity.NotifyTemplate;
6
+import com.water.notify.service.NotifyTemplateService;
7
+import com.water.common.core.result.R;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.util.List;
13
+
14
+/**
15
+ * 通知模板管理控制器
16
+ */
17
+@RestController
18
+@RequestMapping("/api/notify/template")
19
+@RequiredArgsConstructor
20
+public class NotifyTemplateController {
21
+    
22
+    @Autowired
23
+    private NotifyTemplateService templateService;
24
+    
25
+    /**
26
+     * 分页查询模板
27
+     */
28
+    @GetMapping("/page")
29
+    public R<Page<NotifyTemplate>> page(@RequestParam(defaultValue = "1") Long current,
30
+                                       @RequestParam(defaultValue = "10") Long size) {
31
+        Page<NotifyTemplate> page = new Page<>(current, size);
32
+        LambdaQueryWrapper<NotifyTemplate> wrapper = new LambdaQueryWrapper<>();
33
+        wrapper.orderByDesc(NotifyTemplate::getId);
34
+        templateService.page(page, wrapper);
35
+        return R.success(page);
36
+    }
37
+    
38
+    /**
39
+     * 获取模板详情
40
+     */
41
+    @GetMapping("/{id}")
42
+    public R<NotifyTemplate> getById(@PathVariable Long id) {
43
+        NotifyTemplate template = templateService.getById(id);
44
+        if (template == null) {
45
+            return R.failed("模板不存在");
46
+        }
47
+        return R.success(template);
48
+    }
49
+    
50
+    /**
51
+     * 创建模板
52
+     */
53
+    @PostMapping
54
+    public R<Boolean> create(@RequestBody NotifyTemplate template) {
55
+        template.setStatus(1); // 默认启用
56
+        boolean success = templateService.save(template);
57
+        return success ? R.success() : R.failed("创建失败");
58
+    }
59
+    
60
+    /**
61
+     * 更新模板
62
+     */
63
+    @PutMapping("/{id}")
64
+    public R<Boolean> update(@PathVariable Long id, @RequestBody NotifyTemplate template) {
65
+        template.setId(id);
66
+        boolean success = templateService.updateById(template);
67
+        return success ? R.success() : R.failed("更新失败");
68
+    }
69
+    
70
+    /**
71
+     * 删除模板
72
+     */
73
+    @DeleteMapping("/{id}")
74
+    public R<Boolean> delete(@PathVariable Long id) {
75
+        boolean success = templateService.removeById(id);
76
+        return success ? R.success() : R.failed("删除失败");
77
+    }
78
+    
79
+    /**
80
+     * 获取启用模板列表
81
+     */
82
+    @GetMapping("/enabled")
83
+    public R<List<NotifyTemplate>> getEnabledTemplates() {
84
+        List<NotifyTemplate> templates = templateService.getEnabledTemplates();
85
+        return R.success(templates);
86
+    }
87
+    
88
+    /**
89
+     * 根据渠道获取启用模板
90
+     */
91
+    @GetMapping("/enabled/{channel}")
92
+    public R<List<NotifyTemplate>> getEnabledTemplatesByChannel(@PathVariable String channel) {
93
+        List<NotifyTemplate> templates = templateService.getEnabledTemplatesByChannel(channel);
94
+        return R.success(templates);
95
+    }
96
+}

+ 99
- 0
wm-notify/src/main/java/com/water/notify/entity/NotifyLog.java Просмотреть файл

1
+package com.water.notify.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
6
+import lombok.Data;
7
+import lombok.experimental.Accessors;
8
+
9
+import java.time.LocalDateTime;
10
+
11
+/**
12
+ * 通知日志表
13
+ * 
14
+ * 记录每次通知发送的状态和结果
15
+ * 对应设计规格:§1.6 消息通知模块
16
+ */
17
+@Data
18
+@Accessors(chain = true)
19
+@TableName("notify_log")
20
+public class NotifyLog {
21
+    
22
+    @TableId(type = IdType.AUTO)
23
+    private Long id;
24
+    
25
+    /**
26
+     * 关联的模板ID
27
+     */
28
+    private Long templateId;
29
+    
30
+    /**
31
+     * 模板代码
32
+     */
33
+    private String templateCode;
34
+    
35
+    /**
36
+     * 接收者ID(用户ID、设备ID等)
37
+     */
38
+    private Long receiverId;
39
+    
40
+    /**
41
+     * 接收者标识(手机号、邮箱、设备token等)
42
+     */
43
+    private String receiverIdentifier;
44
+    
45
+    /**
46
+     * 发送的渠道
47
+     */
48
+    private String channel;
49
+    
50
+    /**
51
+     * 发送状态:pending/success/failed/timeout
52
+     */
53
+    private String status;
54
+    
55
+    /**
56
+     * 标题
57
+     */
58
+    private String title;
59
+    
60
+    /**
61
+     * 内容
62
+     */
63
+    private String content;
64
+    
65
+    /**
66
+     * 错误信息
67
+     */
68
+    private String errorMsg;
69
+    
70
+    /**
71
+     * 重试次数
72
+     */
73
+    private Integer retryCount;
74
+    
75
+    /**
76
+     * 最大重试次数
77
+     */
78
+    private Integer maxRetryCount;
79
+    
80
+    /**
81
+     * 首次发送时间
82
+     */
83
+    private LocalDateTime firstSendAt;
84
+    
85
+    /**
86
+     * 发送成功时间
87
+     */
88
+    private LocalDateTime successAt;
89
+    
90
+    /**
91
+     * 发送失败时间
92
+     */
93
+    private LocalDateTime failedAt;
94
+    
95
+    /**
96
+     * 通知配置JSON
97
+     */
98
+    private String config;
99
+}

+ 89
- 0
wm-notify/src/main/java/com/water/notify/entity/NotifyTemplate.java Просмотреть файл

1
+package com.water.notify.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
6
+import lombok.Data;
7
+import lombok.experimental.Accessors;
8
+
9
+import java.time.LocalDateTime;
10
+
11
+/**
12
+ * 通知模板表
13
+ * 
14
+ * 对应设计规格:§1.6 消息通知模块
15
+ */
16
+@Data
17
+@Accessors(chain = true)
18
+@TableName("notify_template")
19
+public class NotifyTemplate {
20
+    
21
+    @TableId(type = IdType.AUTO)
22
+    private Long id;
23
+    
24
+    /**
25
+     * 模板代码,唯一标识
26
+     */
27
+    private String templateCode;
28
+    
29
+    /**
30
+     * 模板名称
31
+     */
32
+    private String templateName;
33
+    
34
+    /**
35
+     * 支持的渠道,逗号分隔:ws,sms,push,email
36
+     */
37
+    private String channels;
38
+    
39
+    /**
40
+     * 标题模板
41
+     */
42
+    private String titleTpl;
43
+    
44
+    /**
45
+     * 内容模板(支持变量替换)
46
+     */
47
+    private String contentTpl;
48
+    
49
+    /**
50
+     * 模板状态:1-启用 0-停用
51
+     */
52
+    private Integer status;
53
+    
54
+    /**
55
+     * 创建时间
56
+     */
57
+    private LocalDateTime createdAt;
58
+    
59
+    /**
60
+     * 更新时间
61
+     */
62
+    private LocalDateTime updatedAt;
63
+    
64
+    /**
65
+     * 渠道枚举
66
+     */
67
+    public enum Channel {
68
+        WEBSOCKET("ws", "WebSocket实时推送"),
69
+        SMS("sms", "短信推送"),
70
+        PUSH("push", "APP推送"),
71
+        EMAIL("email", "邮件推送");
72
+        
73
+        private final String code;
74
+        private final String description;
75
+        
76
+        Channel(String code, String description) {
77
+            this.code = code;
78
+            this.description = description;
79
+        }
80
+        
81
+        public String getCode() {
82
+            return code;
83
+        }
84
+        
85
+        public String getDescription() {
86
+            return description;
87
+        }
88
+    }
89
+}

+ 13
- 0
wm-notify/src/main/java/com/water/notify/mapper/NotifyLogMapper.java Просмотреть файл

1
+package com.water.notify.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.notify.entity.NotifyLog;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * 通知日志Mapper
9
+ */
10
+@Mapper
11
+public interface NotifyLogMapper extends BaseMapper<NotifyLog> {
12
+    
13
+}

+ 13
- 0
wm-notify/src/main/java/com/water/notify/mapper/NotifyTemplateMapper.java Просмотреть файл

1
+package com.water.notify.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.notify.entity.NotifyTemplate;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * 通知模板Mapper
9
+ */
10
+@Mapper
11
+public interface NotifyTemplateMapper extends BaseMapper<NotifyTemplate> {
12
+    
13
+}

+ 38
- 0
wm-notify/src/main/java/com/water/notify/service/NotifyLogService.java Просмотреть файл

1
+package com.water.notify.service;
2
+
3
+import com.baomidou.mybatisplus.extension.service.IService;
4
+import com.water.notify.entity.NotifyLog;
5
+
6
+import java.time.LocalDateTime;
7
+import java.util.List;
8
+
9
+/**
10
+ * 通知日志服务
11
+ */
12
+public interface NotifyLogService extends IService<NotifyLog> {
13
+    
14
+    /**
15
+     * 记录通知发送日志
16
+     */
17
+    void recordLog(NotifyLog log);
18
+    
19
+    /**
20
+     * 更新日志状态为成功
21
+     */
22
+    void updateSuccess(Long logId, String response);
23
+    
24
+    /**
25
+     * 更新日志状态为失败
26
+     */
27
+    void updateFailed(Long logId, String errorMsg);
28
+    
29
+    /**
30
+     * 获取失败的通知日志(用于重试)
31
+     */
32
+    List<NotifyLog> getFailedLogs(LocalDateTime beforeTime, int maxRetryCount);
33
+    
34
+    /**
35
+     * 获取待发送的通知日志
36
+     */
37
+    List<NotifyLog> getPendingLogs(int limit);
38
+}

+ 58
- 0
wm-notify/src/main/java/com/water/notify/service/NotifyService.java Просмотреть файл

1
+package com.water.notify.service;
2
+
3
+import com.water.notify.entity.NotifyLog;
4
+import com.water.notify.entity.NotifyTemplate;
5
+
6
+import java.util.List;
7
+
8
+/**
9
+ * 通知发送服务
10
+ * 
11
+ * 统一的消息发送入口,支持多种渠道
12
+ */
13
+public interface NotifyService {
14
+    
15
+    /**
16
+     * 发送通知
17
+     * 
18
+     * @param templateCode 模板代码
19
+     * @param receiverId 接收者ID
20
+     * @param receiverIdentifier 接收者标识
21
+     * @param channel 发送渠道
22
+     * @param params 模板参数
23
+     * @return 通知日志ID
24
+     */
25
+    Long sendNotify(String templateCode, Long receiverId, String receiverIdentifier, 
26
+                   String channel, java.util.Map<String, Object> params);
27
+    
28
+    /**
29
+     * 发送通知(自动选择渠道)
30
+     * 
31
+     * @param templateCode 模板代码
32
+     * @param receiverId 接收者ID
33
+     * @param receiverIdentifier 接收者标识
34
+     * @param params 模板参数
35
+     * @return 通知日志ID列表
36
+     */
37
+    List<Long> sendNotify(String templateCode, Long receiverId, String receiverIdentifier,
38
+                         java.util.Map<String, Object> params);
39
+    
40
+    /**
41
+     * 批量发送通知
42
+     * 
43
+     * @param templateCode 模板代码
44
+     * @param receiverIds 接收者ID列表
45
+     * @param receiverIdentifiers 接收者标识列表
46
+     * @param channel 发送渠道
47
+     * @param params 模板参数
48
+     * @return 通知日志ID列表
49
+     */
50
+    List<Long> batchSendNotify(String templateCode, List<Long> receiverIds, 
51
+                              List<String> receiverIdentifiers, String channel,
52
+                              java.util.Map<String, Object> params);
53
+    
54
+    /**
55
+     * 重新发送失败的通知
56
+     */
57
+    void retryFailedNotifications();
58
+}

+ 27
- 0
wm-notify/src/main/java/com/water/notify/service/NotifyTemplateService.java Просмотреть файл

1
+package com.water.notify.service;
2
+
3
+import com.baomidou.mybatisplus.extension.service.IService;
4
+import com.water.notify.entity.NotifyTemplate;
5
+
6
+import java.util.List;
7
+
8
+/**
9
+ * 通知模板服务
10
+ */
11
+public interface NotifyTemplateService extends IService<NotifyTemplate> {
12
+    
13
+    /**
14
+     * 根据模板代码获取模板
15
+     */
16
+    NotifyTemplate getByTemplateCode(String templateCode);
17
+    
18
+    /**
19
+     * 获取启用的模板列表
20
+     */
21
+    List<NotifyTemplate> getEnabledTemplates();
22
+    
23
+    /**
24
+     * 根据渠道获取启用的模板
25
+     */
26
+    List<NotifyTemplate> getEnabledTemplatesByChannel(String channel);
27
+}

+ 64
- 0
wm-notify/src/main/java/com/water/notify/service/impl/NotifyLogServiceImpl.java Просмотреть файл

1
+package com.water.notify.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
5
+import com.water.notify.entity.NotifyLog;
6
+import com.water.notify.mapper.NotifyLogMapper;
7
+import com.water.notify.service.NotifyLogService;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.time.LocalDateTime;
11
+import java.util.List;
12
+
13
+/**
14
+ * 通知日志服务实现
15
+ */
16
+@Service
17
+public class NotifyLogServiceImpl extends ServiceImpl<NotifyLogMapper, NotifyLog> 
18
+    implements NotifyLogService {
19
+    
20
+    @Override
21
+    public void recordLog(NotifyLog log) {
22
+        log.setFirstSendAt(LocalDateTime.now());
23
+        log.setStatus("pending");
24
+        log.setRetryCount(0);
25
+        this.save(log);
26
+    }
27
+    
28
+    @Override
29
+    public void updateSuccess(Long logId, String response) {
30
+        NotifyLog log = new NotifyLog();
31
+        log.setId(logId);
32
+        log.setStatus("success");
33
+        log.setSuccessAt(LocalDateTime.now());
34
+        this.updateById(log);
35
+    }
36
+    
37
+    @Override
38
+    public void updateFailed(Long logId, String errorMsg) {
39
+        NotifyLog log = new NotifyLog();
40
+        log.setId(logId);
41
+        log.setStatus("failed");
42
+        log.setErrorMsg(errorMsg);
43
+        log.setFailedAt(LocalDateTime.now());
44
+        this.updateById(log);
45
+    }
46
+    
47
+    @Override
48
+    public List<NotifyLog> getFailedLogs(LocalDateTime beforeTime, int maxRetryCount) {
49
+        LambdaQueryWrapper<NotifyLog> wrapper = new LambdaQueryWrapper<>();
50
+        wrapper.eq(NotifyLog::getStatus, "failed")
51
+               .lt(NotifyLog::getFailedAt, beforeTime)
52
+               .lt(NotifyLog::getRetryCount, maxRetryCount);
53
+        return this.list(wrapper);
54
+    }
55
+    
56
+    @Override
57
+    public List<NotifyLog> getPendingLogs(int limit) {
58
+        LambdaQueryWrapper<NotifyLog> wrapper = new LambdaQueryWrapper<>();
59
+        wrapper.eq(NotifyLog::getStatus, "pending")
60
+               .orderByAsc(NotifyLog::getFirstSendAt)
61
+               .last("LIMIT " + limit);
62
+        return this.list(wrapper);
63
+    }
64
+}

+ 179
- 0
wm-notify/src/main/java/com/water/notify/service/impl/NotifyServiceImpl.java Просмотреть файл

1
+package com.water.notify.service.impl;
2
+
3
+import com.alibaba.fastjson2.JSON;
4
+import com.alibaba.fastjson2.JSONWriter;
5
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
6
+import com.water.notify.channel.Notifier;
7
+import com.water.notify.entity.NotifyLog;
8
+import com.water.notify.entity.NotifyTemplate;
9
+import com.water.notify.service.NotifyLogService;
10
+import com.water.notify.service.NotifyService;
11
+import com.water.notify.service.NotifyTemplateService;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.scheduling.annotation.Async;
14
+import org.springframework.stereotype.Service;
15
+
16
+import java.time.LocalDateTime;
17
+import java.util.ArrayList;
18
+import java.util.List;
19
+import java.util.Map;
20
+import java.util.stream.Collectors;
21
+
22
+/**
23
+ * 通知服务实现
24
+ */
25
+@Service
26
+public class NotifyServiceImpl implements NotifyService {
27
+    
28
+    @Autowired
29
+    private NotifyTemplateService templateService;
30
+    
31
+    @Autowired
32
+    private NotifyLogService logService;
33
+    
34
+    @Autowired
35
+    private List<Notifier> notifiers;
36
+    
37
+    @Override
38
+    public Long sendNotify(String templateCode, Long receiverId, String receiverIdentifier, 
39
+                          String channel, Map<String, Object> params) {
40
+        // 获取模板
41
+        NotifyTemplate template = templateService.getByTemplateCode(templateCode);
42
+        if (template == null) {
43
+            throw new RuntimeException("模板不存在: " + templateCode);
44
+        }
45
+        
46
+        // 检查渠道是否支持
47
+        if (!template.getChannels().contains(channel)) {
48
+            throw new RuntimeException("模板不支持该渠道: " + channel);
49
+        }
50
+        
51
+        // 处理模板变量
52
+        String title = processTemplate(template.getTitleTpl(), params);
53
+        String content = processTemplate(template.getContentTpl(), params);
54
+        
55
+        // 创建通知日志
56
+        NotifyLog log = new NotifyLog();
57
+        log.setTemplateId(template.getId());
58
+        log.setTemplateCode(templateCode);
59
+        log.setReceiverId(receiverId);
60
+        log.setReceiverIdentifier(receiverIdentifier);
61
+        log.setChannel(channel);
62
+        log.setTitle(title);
63
+        log.setContent(content);
64
+        log.setConfig(JSON.toJSONString(params, JSONWriter.Feature.WriteMapNullValue));
65
+        
66
+        // 记录日志
67
+        logService.recordLog(log);
68
+        
69
+        // 异步发送
70
+        sendAsync(log);
71
+        
72
+        return log.getId();
73
+    }
74
+    
75
+    @Override
76
+    public List<Long> sendNotify(String templateCode, Long receiverId, String receiverIdentifier,
77
+                                Map<String, Object> params) {
78
+        // 获取模板
79
+        NotifyTemplate template = templateService.getByTemplateCode(templateCode);
80
+        if (template == null) {
81
+            throw new RuntimeException("模板不存在: " + templateCode);
82
+        }
83
+        
84
+        List<Long> logIds = new ArrayList<>();
85
+        String[] channels = template.getChannels().split(",");
86
+        
87
+        // 为每个支持的渠道发送通知
88
+        for (String channel : channels) {
89
+            try {
90
+                Long logId = sendNotify(templateCode, receiverId, receiverIdentifier, channel.trim(), params);
91
+                logIds.add(logId);
92
+            } catch (Exception e) {
93
+                // 记录失败,继续尝试其他渠道
94
+                System.err.println("发送失败 - " + channel + ": " + e.getMessage());
95
+            }
96
+        }
97
+        
98
+        return logIds;
99
+    }
100
+    
101
+    @Override
102
+    public List<Long> batchSendNotify(String templateCode, List<Long> receiverIds, 
103
+                                     List<String> receiverIdentifiers, String channel,
104
+                                     Map<String, Object> params) {
105
+        List<Long> logIds = new ArrayList<>();
106
+        
107
+        for (int i = 0; i < receiverIds.size(); i++) {
108
+            Long receiverId = receiverIds.get(i);
109
+            String receiverIdentifier = receiverIdentifiers.get(i);
110
+            
111
+            try {
112
+                Long logId = sendNotify(templateCode, receiverId, receiverIdentifier, channel, params);
113
+                logIds.add(logId);
114
+            } catch (Exception e) {
115
+                System.err.println("批量发送失败 - 用户" + receiverId + ": " + e.getMessage());
116
+            }
117
+        }
118
+        
119
+        return logIds;
120
+    }
121
+    
122
+    @Async
123
+    private void sendAsync(NotifyLog log) {
124
+        try {
125
+            // 获取对应的notifier
126
+            Notifier notifier = notifiers.stream()
127
+                .filter(n -> n.getChannel().equals(log.getChannel()))
128
+                .findFirst()
129
+                .orElseThrow(() -> new RuntimeException("不支持的渠道: " + log.getChannel()));
130
+            
131
+            // 验证接收者
132
+            if (!notifier.validateReceiver(log)) {
133
+                logService.updateFailed(log.getId(), "接收者无效");
134
+                return;
135
+            }
136
+            
137
+            // 发送通知
138
+            boolean success = notifier.send(log);
139
+            if (success) {
140
+                logService.updateSuccess(log.getId(), "发送成功");
141
+            } else {
142
+                logService.updateFailed(log.getId(), "发送失败");
143
+            }
144
+        } catch (Exception e) {
145
+            logService.updateFailed(log.getId(), e.getMessage());
146
+        }
147
+    }
148
+    
149
+    private String processTemplate(String template, Map<String, Object> params) {
150
+        if (template == null || params == null) {
151
+            return template;
152
+        }
153
+        
154
+        // 简单的变量替换 ${variable} -> value
155
+        String result = template;
156
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
157
+            String key = "${" + entry.getKey() + "}";
158
+            String value = entry.getValue() != null ? entry.getValue().toString() : "";
159
+            result = result.replace(key, value);
160
+        }
161
+        
162
+        return result;
163
+    }
164
+    
165
+    @Override
166
+    public void retryFailedNotifications() {
167
+        // 获取5分钟内失败且重试次数小于3的日志
168
+        LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);
169
+        List<NotifyLog> failedLogs = logService.getFailedLogs(fiveMinutesAgo, 3);
170
+        
171
+        for (NotifyLog log : failedLogs) {
172
+            // 增加重试次数
173
+            log.setRetryCount(log.getRetryCount() + 1);
174
+            
175
+            // 重新发送
176
+            sendAsync(log);
177
+        }
178
+    }
179
+}

+ 41
- 0
wm-notify/src/main/java/com/water/notify/service/impl/NotifyTemplateServiceImpl.java Просмотреть файл

1
+package com.water.notify.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
5
+import com.water.notify.entity.NotifyTemplate;
6
+import com.water.notify.mapper.NotifyTemplateMapper;
7
+import com.water.notify.service.NotifyTemplateService;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.util.List;
11
+
12
+/**
13
+ * 通知模板服务实现
14
+ */
15
+@Service
16
+public class NotifyTemplateServiceImpl extends ServiceImpl<NotifyTemplateMapper, NotifyTemplate> 
17
+    implements NotifyTemplateService {
18
+    
19
+    @Override
20
+    public NotifyTemplate getByTemplateCode(String templateCode) {
21
+        LambdaQueryWrapper<NotifyTemplate> wrapper = new LambdaQueryWrapper<>();
22
+        wrapper.eq(NotifyTemplate::getTemplateCode, templateCode)
23
+               .eq(NotifyTemplate::getStatus, 1);
24
+        return this.getOne(wrapper);
25
+    }
26
+    
27
+    @Override
28
+    public List<NotifyTemplate> getEnabledTemplates() {
29
+        LambdaQueryWrapper<NotifyTemplate> wrapper = new LambdaQueryWrapper<>();
30
+        wrapper.eq(NotifyTemplate::getStatus, 1);
31
+        return this.list(wrapper);
32
+    }
33
+    
34
+    @Override
35
+    public List<NotifyTemplate> getEnabledTemplatesByChannel(String channel) {
36
+        LambdaQueryWrapper<NotifyTemplate> wrapper = new LambdaQueryWrapper<>();
37
+        wrapper.eq(NotifyTemplate::getStatus, 1)
38
+               .like(NotifyTemplate::getChannels, channel);
39
+        return this.list(wrapper);
40
+    }
41
+}

+ 54
- 0
wm-notify/src/main/resources/application.yml Просмотреть файл

1
+server:
2
+  port: 8089
3
+  
4
+spring:
5
+  application:
6
+    name: wm-notify
7
+  
8
+  # 数据库配置
9
+  datasource:
10
+    url: jdbc:postgresql://localhost:5432/water_management
11
+    username: water
12
+    password: water123
13
+    driver-class-name: org.postgresql.Driver
14
+    
15
+  # Redis配置
16
+  redis:
17
+    host: localhost
18
+    port: 6379
19
+    database: 0
20
+    timeout: 3000ms
21
+    lettuce:
22
+      pool:
23
+        max-active: 8
24
+        max-idle: 8
25
+        min-idle: 0
26
+        
27
+  # Jackson配置
28
+  jackson:
29
+    date-format: yyyy-MM-dd HH:mm:ss
30
+    time-zone: Asia/Shanghai
31
+
32
+# MyBatis Plus配置
33
+mybatis-plus:
34
+  mapper-locations: classpath*:/mapper/**/*.xml
35
+  type-aliases-package: com.water.notify.entity
36
+  configuration:
37
+    map-underscore-to-camel-case: true
38
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
39
+
40
+# 日志配置
41
+logging:
42
+  level:
43
+    com.water.notify: debug
44
+    root: info
45
+
46
+# Actuator配置
47
+management:
48
+  endpoints:
49
+    web:
50
+      exposure:
51
+        include: health,info,metrics
52
+  endpoint:
53
+    health:
54
+      show-details: always

+ 63
- 0
wm-notify/src/main/resources/db/init.sql Просмотреть файл

1
+-- 消息通知模块数据库初始化脚本
2
+-- 对应设计规格:§1.6 消息通知模块
3
+
4
+-- 创建通知模板表
5
+CREATE TABLE IF NOT EXISTS notify_template (
6
+    id                  BIGSERIAL PRIMARY KEY,
7
+    template_code       VARCHAR(50) UNIQUE NOT NULL,
8
+    template_name       VARCHAR(100),
9
+    channels            VARCHAR(50) DEFAULT 'ws',
10
+    title_tpl           VARCHAR(200),
11
+    content_tpl         TEXT NOT NULL,
12
+    status              SMALLINT DEFAULT 1,
13
+    created_at          TIMESTAMPTZ DEFAULT NOW(),
14
+    updated_at          TIMESTAMPTZ DEFAULT NOW()
15
+);
16
+
17
+-- 创建通知日志表
18
+CREATE TABLE IF NOT EXISTS notify_log (
19
+    id                  BIGSERIAL PRIMARY KEY,
20
+    template_id         BIGINT,
21
+    template_code       VARCHAR(50),
22
+    receiver_id         BIGINT,
23
+    receiver_identifier VARCHAR(255),
24
+    channel             VARCHAR(20) NOT NULL,
25
+    status              VARCHAR(20) DEFAULT 'pending',
26
+    title               VARCHAR(500),
27
+    content             TEXT,
28
+    error_msg           TEXT,
29
+    retry_count         INT DEFAULT 0,
30
+    max_retry_count     INT DEFAULT 3,
31
+    first_send_at       TIMESTAMPTZ,
32
+    success_at          TIMESTAMPTZ,
33
+    failed_at           TIMESTAMPTZ,
34
+    config              TEXT,
35
+    created_at          TIMESTAMPTZ DEFAULT NOW(),
36
+    updated_at          TIMESTAMPTZ DEFAULT NOW()
37
+);
38
+
39
+-- 创建索引
40
+CREATE INDEX IF NOT EXISTS idx_notify_template_code ON notify_template(template_code);
41
+CREATE INDEX IF NOT EXISTS idx_notify_template_status ON notify_template(status);
42
+CREATE INDEX IF NOT EXISTS idx_notify_log_receiver ON notify_log(receiver_id);
43
+CREATE INDEX IF NOT EXISTS idx_notify_log_channel ON notify_log(channel);
44
+CREATE INDEX IF NOT EXISTS idx_notify_log_status ON notify_log(status);
45
+CREATE INDEX IF NOT EXISTS idx_notify_log_created_at ON notify_log(created_at);
46
+
47
+-- 插入默认通知模板
48
+INSERT INTO notify_template (template_code, template_name, channels, title_tpl, content_tpl, status) VALUES
49
+('device_alert', '设备报警通知', 'ws,push,sms', '设备报警: ${device_sn}', 
50
+ '设备 ${device_sn} 发生 ${alert_level} 报警\n设备类型: ${device_type}\n报警内容: ${message}\n发生时间: ${alert_time}', 1),
51
+('maintenance_reminder', '维护提醒', 'ws,email', '设备维护提醒: ${device_sn}',
52
+ '设备 ${device_sn} 将在 ${maintenance_date} 进行维护,请提前做好准备。\n维护类型: ${maintenance_type}\n负责人: ${maintenance_person}', 1),
53
+('system_notice', '系统通知', 'ws,push,email', '系统通知', '${content}', 1),
54
+('bill_notification', '账单通知', 'ws,push,email', '账单通知: ${customer_name}',
55
+ '尊敬的${customer_name},您${bill_period}的账单已生成,金额${total_fee}元,请及时缴费。', 1),
56
+('inspection_reminder', '巡检提醒', 'ws,push', '巡检提醒: ${inspector_name}',
57
+ '${inspector_name},您今日有${task_count}个巡检任务,请及时完成。\n开始时间: ${plan_time}', 1)
58
+ON CONFLICT (template_code) DO NOTHING;
59
+
60
+-- 插入测试数据
61
+INSERT INTO notify_template (template_code, template_name, channels, title_tpl, content_tpl, status) VALUES
62
+('test_template', '测试模板', 'ws,sms,push,email', '测试标题', '这是测试内容:${parameter}', 1)
63
+ON CONFLICT (template_code) DO NOTHING;