|
|
@@ -0,0 +1,125 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.dto.NotificationMessage;
|
|
|
+import com.etotem.cfc.entity.NotificationTemplate;
|
|
|
+import com.etotem.cfc.mapper.NotificationTemplateMapper;
|
|
|
+import com.etotem.cfc.service.notification.NotificationChannel;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 通知分发服务(W3 五通道模型统一入口)
|
|
|
+ * <p>
|
|
|
+ * 流程:读模板(回退默认文案)→ 占位符替换({domainName}/{reward} 等)→
|
|
|
+ * 按模板 channels 多选分发到匹配的 {@link NotificationChannel}。
|
|
|
+ * <p>
|
|
|
+ * 通道:in_app(内置写库)/ sms / wechat / gzh / phone(外部接口预留)。
|
|
|
+ * 外部通道未接入网关时(enabled=false)仅记录日志,不实际发送。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class NotificationDispatchService {
|
|
|
+
|
|
|
+ /** 默认通道:模板不存在/无 channels 时至少走站内 */
|
|
|
+ private static final String DEFAULT_CHANNELS = "in_app";
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private NotificationTemplateMapper notificationTemplateMapper;
|
|
|
+
|
|
|
+ /** Spring 自动注入全部 NotificationChannel 实现(含 5 个通道) */
|
|
|
+ @Resource
|
|
|
+ private List<NotificationChannel> channels;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按模板类型发送通知(简化入口,无占位符变量)
|
|
|
+ */
|
|
|
+ public void sendByTemplate(String type, Long userId, String refType, Long refId,
|
|
|
+ String defaultTitle, String defaultContent) {
|
|
|
+ sendByTemplate(type, userId, null, refType, refId, defaultTitle, defaultContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按模板类型发送通知
|
|
|
+ *
|
|
|
+ * @param type 通知类型(对应 notification_templates.type)
|
|
|
+ * @param userId 接收用户ID
|
|
|
+ * @param vars 占位符变量({key} → value)
|
|
|
+ * @param refType 关联对象类型
|
|
|
+ * @param refId 关联对象ID
|
|
|
+ * @param defaultTitle 模板不存在/停用时的回退标题
|
|
|
+ * @param defaultContent 模板不存在/停用时的回退内容
|
|
|
+ */
|
|
|
+ public void sendByTemplate(String type, Long userId, Map<String, Object> vars,
|
|
|
+ String refType, Long refId,
|
|
|
+ String defaultTitle, String defaultContent) {
|
|
|
+ // 1. 读取启用模板(表不存在/无记录/停用 → null)
|
|
|
+ NotificationTemplate tpl = getEnabledTemplate(type);
|
|
|
+
|
|
|
+ // 2. 解析标题/内容(占位符替换),回退默认文案
|
|
|
+ String title = (tpl != null && notEmpty(tpl.getTitle())) ? tpl.getTitle() : defaultTitle;
|
|
|
+ String content = (tpl != null && notEmpty(tpl.getContent())) ? tpl.getContent() : defaultContent;
|
|
|
+ if (vars != null && !vars.isEmpty()) {
|
|
|
+ for (Map.Entry<String, Object> entry : vars.entrySet()) {
|
|
|
+ content = content.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue()));
|
|
|
+ title = title.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 目标通道(多选,逗号分隔;模板无配置时默认站内)
|
|
|
+ String channelsStr = (tpl != null && notEmpty(tpl.getChannels())) ? tpl.getChannels() : DEFAULT_CHANNELS;
|
|
|
+ if (!channelsStr.contains("in_app")) {
|
|
|
+ channelsStr = channelsStr + ",in_app";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 按通道分发
|
|
|
+ NotificationMessage message = new NotificationMessage(userId, type, title, content, refType, refId, channelsStr);
|
|
|
+ for (NotificationChannel channel : channels) {
|
|
|
+ if (!message.containsChannel(channel.getChannelType())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (channel.isEnabled()) {
|
|
|
+ channel.send(message);
|
|
|
+ } else {
|
|
|
+ log.info("通知通道未启用,跳过: channel={}, type={}, userId={}, title={}",
|
|
|
+ channel.getChannelType(), type, userId, title);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("通知通道发送失败 channel={}, type={}, userId={}, error={}",
|
|
|
+ channel.getChannelType(), type, userId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询启用中的模板,异常(如表不存在)返回 null */
|
|
|
+ private NotificationTemplate getEnabledTemplate(String type) {
|
|
|
+ try {
|
|
|
+ List<NotificationTemplate> list = notificationTemplateMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<NotificationTemplate>()
|
|
|
+ .eq(NotificationTemplate::getType, type)
|
|
|
+ .eq(NotificationTemplate::getStatus, 1)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ return (list != null && !list.isEmpty()) ? list.get(0) : null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("读取通知模板失败 type={}, error={}", type, e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean notEmpty(String s) {
|
|
|
+ return s != null && !s.isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 供外部直接调用者便捷构造变量
|
|
|
+ public static Map<String, Object> vars(String key, Object value) {
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put(key, value);
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+}
|