|
@@ -1,5 +1,9 @@
|
|
|
package cn.iocoder.yudao.module.system.service.appnotify;
|
|
package cn.iocoder.yudao.module.system.service.appnotify;
|
|
|
|
|
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
|
|
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.service.notify.NotifyTemplateService;
|
|
|
import com.aliyun.auth.credentials.utils.ParameterHelper;
|
|
import com.aliyun.auth.credentials.utils.ParameterHelper;
|
|
|
import com.aliyuncs.DefaultAcsClient;
|
|
import com.aliyuncs.DefaultAcsClient;
|
|
|
import com.aliyuncs.IAcsClient;
|
|
import com.aliyuncs.IAcsClient;
|
|
@@ -8,226 +12,363 @@ import com.aliyuncs.exceptions.ServerException;
|
|
|
import com.aliyuncs.profile.DefaultProfile;
|
|
import com.aliyuncs.profile.DefaultProfile;
|
|
|
import com.aliyuncs.push.model.v20160801.PushRequest;
|
|
import com.aliyuncs.push.model.v20160801.PushRequest;
|
|
|
import com.aliyuncs.push.model.v20160801.PushResponse;
|
|
import com.aliyuncs.push.model.v20160801.PushResponse;
|
|
|
|
|
+import com.google.common.annotations.VisibleForTesting;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
|
|
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
-import java.util.UUID;
|
|
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+
|
|
|
|
|
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
|
|
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND;
|
|
|
|
|
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_SEND_TEMPLATE_PARAM_MISS;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * app消息发送 Service 实现类(修正Push类找不到的版本)
|
|
|
|
|
|
|
+ * App消息推送服务实现类(阿里云推送)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 功能说明:
|
|
|
|
|
+ * 1. 基于阿里云移动推送(Push)服务实现App消息推送能力
|
|
|
|
|
+ * 2. 支持两种推送模式:单用户(按设备ID)推送、全量用户推送
|
|
|
|
|
+ * 3. 适配Android和iOS双端推送配置,包含离线存储、推送时效等核心配置
|
|
|
|
|
+ * 4. 已修复阿里云SDK中Push类找不到的兼容性问题
|
|
|
*
|
|
*
|
|
|
* @author xrcoder
|
|
* @author xrcoder
|
|
|
|
|
+ * @version 1.0
|
|
|
|
|
+ * @date 2026/01/07
|
|
|
*/
|
|
*/
|
|
|
@Service
|
|
@Service
|
|
|
@Validated
|
|
@Validated
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
public class AppNotifySendServiceImpl implements AppNotifySendService {
|
|
public class AppNotifySendServiceImpl implements AppNotifySendService {
|
|
|
|
|
|
|
|
- // ========== 配置项(放到application.yml中) ==========
|
|
|
|
|
- /** 阿里云Push的AppKey */
|
|
|
|
|
- @Value("${aliyun.push.app-key:}")
|
|
|
|
|
|
|
+ // ========== 阿里云推送配置项(从application.yml读取) ==========
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 阿里云Push应用唯一标识(AppKey)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 配置路径:aliyun.push.app-key
|
|
|
|
|
+ * 获取方式:阿里云控制台 -> 移动推送 -> 应用管理 -> 应用详情
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${aliyun.push.app-key:}") // 从配置文件读取,默认空字符串
|
|
|
private Long appKey;
|
|
private Long appKey;
|
|
|
|
|
|
|
|
- /** 阿里云AccessKey ID */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 阿里云AccessKey ID(账号身份标识)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 配置路径:aliyun.access-key-id
|
|
|
|
|
+ * 获取方式:阿里云控制台 -> RAM访问控制 -> 访问密钥
|
|
|
|
|
+ */
|
|
|
@Value("${aliyun.access-key-id:}")
|
|
@Value("${aliyun.access-key-id:}")
|
|
|
private String accessKeyId;
|
|
private String accessKeyId;
|
|
|
|
|
|
|
|
- /** 阿里云AccessKey Secret */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 阿里云AccessKey Secret(账号身份密钥)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 配置路径:aliyun.access-key-secret
|
|
|
|
|
+ * 注意事项:禁止明文硬编码,建议通过配置中心/环境变量注入
|
|
|
|
|
+ */
|
|
|
@Value("${aliyun.access-key-secret:}")
|
|
@Value("${aliyun.access-key-secret:}")
|
|
|
private String accessKeySecret;
|
|
private String accessKeySecret;
|
|
|
|
|
|
|
|
- /** 阿里云Push的地域ID(默认杭州) */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 阿里云Push服务地域ID
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 配置路径:aliyun.push.region-id
|
|
|
|
|
+ * 默认值:cn-hangzhou(杭州地域,阿里云默认)
|
|
|
|
|
+ * 可选值:cn-beijing(北京)、cn-shenzhen(深圳)等
|
|
|
|
|
+ */
|
|
|
@Value("${aliyun.push.region-id:cn-hangzhou}")
|
|
@Value("${aliyun.push.region-id:cn-hangzhou}")
|
|
|
private String regionId;
|
|
private String regionId;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据用户ID发送单条App通知(核心业务方法)
|
|
|
|
|
|
|
+ * 【核心业务方法】根据用户ID发送单条App通知(按设备ID精准推送)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param userId 接收通知的用户ID(业务系统内的用户唯一标识)
|
|
|
|
|
+ * @param templateCode 消息模板编码(用于解析推送标题和内容)
|
|
|
|
|
+ * @param templateParams 模板参数(填充模板变量,如订单号、通知内容等)
|
|
|
|
|
+ * @return 阿里云推送返回的消息ID(Long类型);推送失败返回null
|
|
|
|
|
+ * @throws ServerException 阿里云服务端异常(如权限不足、参数非法)
|
|
|
|
|
+ * @throws ClientException 客户端异常(如网络问题、配置错误)
|
|
|
*/
|
|
*/
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private NotifyTemplateService notifyTemplateService;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@Override
|
|
@Override
|
|
|
public Long sendSingleAppNotifyToAdmin(Long userId, String templateCode, Map<String, Object> templateParams) {
|
|
public Long sendSingleAppNotifyToAdmin(Long userId, String templateCode, Map<String, Object> templateParams) {
|
|
|
- // 1. 根据userId查询对应的设备ID(替换为你的实际查询逻辑)
|
|
|
|
|
|
|
+ // 1. 第一步:根据用户ID查询绑定的设备ID(需替换为业务系统实际查询逻辑)
|
|
|
|
|
+ // 说明:设备ID是App端集成阿里云推送SDK后上报的唯一设备标识
|
|
|
String deviceId = getDeviceIdByUserId(userId);
|
|
String deviceId = getDeviceIdByUserId(userId);
|
|
|
if (deviceId.isEmpty()) {
|
|
if (deviceId.isEmpty()) {
|
|
|
- log.warn("发送单用户App通知失败:用户{}未绑定设备ID", userId);
|
|
|
|
|
|
|
+ log.warn("发送单用户App通知失败:用户{}未绑定任何设备ID,无法推送", userId);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 解析模板标题和内容(替换为你的实际模板逻辑)
|
|
|
|
|
- String pushTitle = parseTemplateTitle(templateCode, templateParams);
|
|
|
|
|
- String pushBody = parseTemplateBody(templateCode, templateParams);
|
|
|
|
|
|
|
+ // 2. 第二步:通过notifyId获取站内信已经发送的消息,进行消息复制发送
|
|
|
|
|
+ String content = getMessageByNotify(userId, templateCode, templateParams);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 3. 创建阿里云客户端(修正后的方式,不会报找不到Push类)
|
|
|
|
|
|
|
+ // 3. 第三步:创建阿里云ACS客户端(修复Push类找不到的兼容方案)
|
|
|
IAcsClient client = createAcsClient();
|
|
IAcsClient client = createAcsClient();
|
|
|
|
|
|
|
|
- // 4. 构建单设备推送请求
|
|
|
|
|
- PushRequest pushRequest = buildSingleDevicePushRequest(deviceId, pushTitle, pushBody);
|
|
|
|
|
|
|
+ // 4. 第四步:构建单设备推送请求(封装Android/iOS双端配置)
|
|
|
|
|
+ PushRequest pushRequest = buildSingleDevicePushRequest(deviceId, "博士安全", content);
|
|
|
|
|
|
|
|
- // 5. 发送推送
|
|
|
|
|
|
|
+ // 5. 第五步:发送推送请求,调用阿里云Push接口
|
|
|
PushResponse response = client.getAcsResponse(pushRequest);
|
|
PushResponse response = client.getAcsResponse(pushRequest);
|
|
|
|
|
|
|
|
- // 6. 解析返回结果
|
|
|
|
|
- String requestId = response.getRequestId();
|
|
|
|
|
- String messageId = response.getMessageId();
|
|
|
|
|
|
|
+ // 6. 第六步:解析返回结果,记录日志并返回消息ID
|
|
|
|
|
+ String requestId = response.getRequestId(); // 阿里云请求唯一标识(用于排查问题)
|
|
|
|
|
+ String messageId = response.getMessageId(); // 推送消息唯一标识(用于查询推送状态)
|
|
|
log.info("单用户App推送成功:userId={}, requestId={}, messageId={}", userId, requestId, messageId);
|
|
log.info("单用户App推送成功:userId={}, requestId={}, messageId={}", userId, requestId, messageId);
|
|
|
|
|
|
|
|
- // 返回消息ID(转为Long,若messageId是字符串可自行调整)
|
|
|
|
|
|
|
+ // 注意:若messageId是字符串格式(如含字母),需调整返回类型为String
|
|
|
return Long.parseLong(messageId);
|
|
return Long.parseLong(messageId);
|
|
|
} catch (ServerException e) {
|
|
} catch (ServerException e) {
|
|
|
- log.error("单用户App推送失败(服务端错误):userId={}, templateCode={}", userId, templateCode, e);
|
|
|
|
|
|
|
+ // 服务端异常:阿里云接口返回错误(如AppKey无效、额度不足)
|
|
|
|
|
+ log.error("单用户App推送失败(阿里云服务端错误):userId={}, templateCode={}", userId, templateCode, e);
|
|
|
return null;
|
|
return null;
|
|
|
} catch (ClientException e) {
|
|
} catch (ClientException e) {
|
|
|
|
|
+ // 客户端异常:本地配置/网络/参数错误(如AccessKey错误、地域ID无效)
|
|
|
log.error("单用户App推送失败(客户端错误):userId={}, templateCode={}", userId, templateCode, e);
|
|
log.error("单用户App推送失败(客户端错误):userId={}, templateCode={}", userId, templateCode, e);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 通用推送方法(推送给全部用户)
|
|
|
|
|
|
|
+ * 通用推送方法:推送给全部用户(全量广播)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 推送结果:true-成功,false-失败
|
|
|
*/
|
|
*/
|
|
|
public Boolean pushNotify() {
|
|
public Boolean pushNotify() {
|
|
|
|
|
+ // 前置校验:AppKey未配置直接返回失败
|
|
|
if (appKey == null || appKey == 0) {
|
|
if (appKey == null || appKey == 0) {
|
|
|
- log.error("App推送失败:未配置阿里云Push的appKey");
|
|
|
|
|
|
|
+ log.error("App全量推送失败:未配置阿里云Push的appKey,请检查application.yml配置");
|
|
|
return Boolean.FALSE;
|
|
return Boolean.FALSE;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 创建客户端
|
|
|
|
|
|
|
+ // 1. 创建阿里云ACS客户端
|
|
|
IAcsClient client = createAcsClient();
|
|
IAcsClient client = createAcsClient();
|
|
|
- // 构建全量推送请求
|
|
|
|
|
|
|
+ // 2. 构建全量推送请求(示例标题和内容,需根据业务调整)
|
|
|
PushRequest pushRequest = buildAllDevicePushRequest("系统通知", "这是一条全量推送的测试通知");
|
|
PushRequest pushRequest = buildAllDevicePushRequest("系统通知", "这是一条全量推送的测试通知");
|
|
|
- // 发送推送
|
|
|
|
|
|
|
+ // 3. 发送推送请求
|
|
|
PushResponse response = client.getAcsResponse(pushRequest);
|
|
PushResponse response = client.getAcsResponse(pushRequest);
|
|
|
|
|
|
|
|
log.info("全量App推送成功:requestId={}, messageId={}", response.getRequestId(), response.getMessageId());
|
|
log.info("全量App推送成功:requestId={}, messageId={}", response.getRequestId(), response.getMessageId());
|
|
|
return Boolean.TRUE;
|
|
return Boolean.TRUE;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
|
|
+ // 捕获所有异常,保证方法不抛出异常(根据业务需求可调整)
|
|
|
log.error("全量App推送失败", e);
|
|
log.error("全量App推送失败", e);
|
|
|
return Boolean.FALSE;
|
|
return Boolean.FALSE;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ========== 私有工具方法 ==========
|
|
|
|
|
|
|
+ // ========== 私有工具方法(内部调用,封装核心逻辑) ==========
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 创建阿里云ACS客户端(修正后的方式,解决Push类找不到的问题)
|
|
|
|
|
|
|
+ * 创建阿里云ACS客户端(修复Push类找不到的兼容方案)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 核心逻辑:使用阿里云官方推荐的DefaultAcsClient创建客户端,替代过时的PushClient
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 初始化完成的IAcsClient客户端实例
|
|
|
|
|
+ * @throws IllegalArgumentException 配置为空时抛出非法参数异常
|
|
|
*/
|
|
*/
|
|
|
private IAcsClient createAcsClient() {
|
|
private IAcsClient createAcsClient() {
|
|
|
- // 校验配置
|
|
|
|
|
|
|
+ // 前置校验:AccessKey配置不能为空
|
|
|
if (accessKeyId.isEmpty() || accessKeySecret.isEmpty()) {
|
|
if (accessKeyId.isEmpty() || accessKeySecret.isEmpty()) {
|
|
|
- throw new IllegalArgumentException("阿里云AccessKey配置为空,请检查配置项");
|
|
|
|
|
|
|
+ throw new IllegalArgumentException("阿里云AccessKey配置为空,请检查aliyun.access-key-id和aliyun.access-key-secret配置项");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 初始化阿里云配置
|
|
|
|
|
|
|
+ // 1. 初始化阿里云地域配置
|
|
|
DefaultProfile profile = DefaultProfile.getProfile(
|
|
DefaultProfile profile = DefaultProfile.getProfile(
|
|
|
- regionId, // 地域ID
|
|
|
|
|
|
|
+ regionId, // 地域ID(如cn-hangzhou)
|
|
|
accessKeyId, // AccessKey ID
|
|
accessKeyId, // AccessKey ID
|
|
|
accessKeySecret // AccessKey Secret
|
|
accessKeySecret // AccessKey Secret
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 创建客户端(这是阿里云官方推荐的稳定方式)
|
|
|
|
|
|
|
+ // 2. 创建并返回客户端实例(阿里云官方推荐的稳定方式)
|
|
|
return new DefaultAcsClient(profile);
|
|
return new DefaultAcsClient(profile);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 构建单设备推送请求
|
|
|
|
|
|
|
+ * 构建单设备推送请求(适配Android+iOS双端)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param deviceId 设备唯一标识(阿里云Push SDK上报的deviceId)
|
|
|
|
|
+ * @param title 推送标题(展示在App通知栏)
|
|
|
|
|
+ * @param body 推送内容(通知栏展示的具体消息)
|
|
|
|
|
+ * @return 封装完成的PushRequest请求对象
|
|
|
*/
|
|
*/
|
|
|
private PushRequest buildSingleDevicePushRequest(String deviceId, String title, String body) {
|
|
private PushRequest buildSingleDevicePushRequest(String deviceId, String title, String body) {
|
|
|
PushRequest request = new PushRequest();
|
|
PushRequest request = new PushRequest();
|
|
|
- // 基础配置
|
|
|
|
|
- request.setAppKey(appKey);
|
|
|
|
|
- request.setTarget("DEVICE"); // 按设备推送
|
|
|
|
|
- request.setTargetValue(deviceId); // 设备ID
|
|
|
|
|
- request.setDeviceType("ALL"); // 支持Android+iOS
|
|
|
|
|
- request.setPushType("NOTICE"); // 通知类型
|
|
|
|
|
- request.setTitle(title);
|
|
|
|
|
- request.setBody(body);
|
|
|
|
|
|
|
|
|
|
- // iOS配置
|
|
|
|
|
- request.setIOSBadge(1);
|
|
|
|
|
- request.setIOSApnsEnv("PRODUCT");
|
|
|
|
|
- request.setIOSRemind(true);
|
|
|
|
|
|
|
+ // ========== 基础推送配置 ==========
|
|
|
|
|
+ request.setAppKey(appKey); // 关联的阿里云Push应用ID
|
|
|
|
|
+ request.setTarget("DEVICE"); // 推送目标类型:DEVICE-按设备ID,ACCOUNT-按账号,TAG-按标签
|
|
|
|
|
+ request.setTargetValue(deviceId); // 目标值:对应DEVICE类型的设备ID
|
|
|
|
|
+ request.setDeviceType("ALL"); // 推送设备类型:ALL-全部,ANDROID-安卓,IOS-iOS
|
|
|
|
|
+ request.setPushType("NOTICE"); // 推送类型:NOTICE-通知,MESSAGE-透传消息
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 推送内容 ==========
|
|
|
|
|
+ request.setTitle(title); // 通知标题
|
|
|
|
|
+ request.setBody(body); // 通知内容
|
|
|
|
|
+
|
|
|
|
|
+ // ========== iOS端专属配置 ==========
|
|
|
|
|
+ request.setIOSBadge(1); // iOS角标数(未读消息数)
|
|
|
|
|
+ request.setIOSApnsEnv("PRODUCT"); // iOS推送环境:PRODUCT-生产环境,DEV-开发环境
|
|
|
|
|
+ request.setIOSRemind(true); // 是否开启iOS消息提醒(响铃/震动)
|
|
|
|
|
+ // iOS扩展参数(JSON格式):供App端解析自定义逻辑
|
|
|
request.setIOSExtParameters("{\"type\":\"single\"}");
|
|
request.setIOSExtParameters("{\"type\":\"single\"}");
|
|
|
|
|
|
|
|
- // Android配置
|
|
|
|
|
- request.setAndroidOpenType("ACTIVITY");
|
|
|
|
|
- request.setAndroidNotifyType("SOUND");
|
|
|
|
|
|
|
+ // ========== Android端专属配置 ==========
|
|
|
|
|
+ request.setAndroidOpenType("NONE"); // 点击通知打开方式:ACTIVITY-打开页面,URL-打开链接,NONE-无动作
|
|
|
|
|
+ request.setAndroidNotifyType("SOUND"); // Android通知类型:SOUND-声音,VIBRATE-震动,BOTH-声音+震动
|
|
|
|
|
+ request.setAndroidNotificationChannel("Normal");
|
|
|
|
|
+ // Android扩展参数(JSON格式):供App端解析自定义逻辑
|
|
|
request.setAndroidExtParameters("{\"type\":\"single\"}");
|
|
request.setAndroidExtParameters("{\"type\":\"single\"}");
|
|
|
|
|
|
|
|
- // 推送时间(立即推送)
|
|
|
|
|
|
|
+ // ========== 推送时效配置 ==========
|
|
|
|
|
+ // 推送时间:立即推送(ISO8601格式,阿里云要求的标准时间格式)
|
|
|
request.setPushTime(ParameterHelper.getISO8601Time(new Date()));
|
|
request.setPushTime(ParameterHelper.getISO8601Time(new Date()));
|
|
|
- // 离线存储(12小时过期)
|
|
|
|
|
- request.setStoreOffline(true);
|
|
|
|
|
|
|
+ request.setStoreOffline(true); // 是否开启离线存储(设备离线时保存,上线后推送)
|
|
|
|
|
+ // 离线消息过期时间:12小时后过期(当前时间+12小时)
|
|
|
request.setExpireTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)));
|
|
request.setExpireTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)));
|
|
|
|
|
|
|
|
return request;
|
|
return request;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 构建全量推送请求
|
|
|
|
|
|
|
+ * 构建全量推送请求(推送给所有绑定设备)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param title 推送标题
|
|
|
|
|
+ * @param body 推送内容
|
|
|
|
|
+ * @return 封装完成的PushRequest请求对象
|
|
|
*/
|
|
*/
|
|
|
private PushRequest buildAllDevicePushRequest(String title, String body) {
|
|
private PushRequest buildAllDevicePushRequest(String title, String body) {
|
|
|
PushRequest request = new PushRequest();
|
|
PushRequest request = new PushRequest();
|
|
|
- // 基础配置
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 基础推送配置 ==========
|
|
|
request.setAppKey(appKey);
|
|
request.setAppKey(appKey);
|
|
|
- request.setTarget("ALL");
|
|
|
|
|
- request.setTargetValue("all");
|
|
|
|
|
|
|
+ request.setTarget("ALL"); // 推送目标:ALL-全部用户
|
|
|
|
|
+ request.setTargetValue("all"); // 目标值:all表示全量
|
|
|
request.setDeviceType("ALL");
|
|
request.setDeviceType("ALL");
|
|
|
request.setPushType("NOTICE");
|
|
request.setPushType("NOTICE");
|
|
|
request.setTitle(title);
|
|
request.setTitle(title);
|
|
|
request.setBody(body);
|
|
request.setBody(body);
|
|
|
|
|
|
|
|
- // iOS配置
|
|
|
|
|
- request.setIOSBadge(5);
|
|
|
|
|
- request.setIOSMusic("default");
|
|
|
|
|
|
|
+ // ========== iOS端专属配置 ==========
|
|
|
|
|
+ request.setIOSBadge(5); // 角标数设为5(示例值)
|
|
|
|
|
+ request.setIOSMusic("default"); // iOS提示音:default-系统默认
|
|
|
request.setIOSApnsEnv("PRODUCT");
|
|
request.setIOSApnsEnv("PRODUCT");
|
|
|
|
|
+ request.setIOSMutableContent(true); // 允许内容修改
|
|
|
request.setIOSRemind(true);
|
|
request.setIOSRemind(true);
|
|
|
- request.setIOSRemindBody(body);
|
|
|
|
|
|
|
+ request.setIOSRemindBody(body); // iOS提醒文案(锁屏界面展示)
|
|
|
request.setIOSExtParameters("{\"k1\":\"ios\",\"k2\":\"v2\"}");
|
|
request.setIOSExtParameters("{\"k1\":\"ios\",\"k2\":\"v2\"}");
|
|
|
|
|
|
|
|
- // Android配置
|
|
|
|
|
|
|
+ // ========== Android端专属配置 ==========
|
|
|
request.setAndroidOpenType("ACTIVITY");
|
|
request.setAndroidOpenType("ACTIVITY");
|
|
|
- request.setAndroidNotifyType("SOUND");
|
|
|
|
|
- request.setAndroidOpenUrl("http://www.alibaba.com");
|
|
|
|
|
- request.setAndroidMusic("alicloud_notification_sound");
|
|
|
|
|
- request.setAndroidActivity("com.alibaba.push.PushActivity");
|
|
|
|
|
- request.setAndroidPopupActivity("com.alibaba.push.PopupActivity");
|
|
|
|
|
- request.setAndroidPopupTitle("系统通知");
|
|
|
|
|
- request.setAndroidPopupBody(body);
|
|
|
|
|
|
|
+ request.setAndroidNotifyType("VIBRATE");
|
|
|
|
|
+ // request.setAndroidOpenUrl("com.iscs.bozzys.ui.pages.home.PageHome"); // 点击通知打开的URL(示例)
|
|
|
|
|
+ request.setAndroidMusic("alicloud_notification_sound"); // Android提示音
|
|
|
|
|
+ // request.setAndroidActivity("com.iscs.bozzys.ui.pages.home.PageHome"); // 点击通知打开的Activity(示例)
|
|
|
|
|
+ request.setAndroidPopupActivity("com.iscs.bozzys.ui.pages.home.PageHome"); // 弹窗Activity(示例)
|
|
|
|
|
+ request.setAndroidPopupTitle("系统通知"); // 弹窗标题
|
|
|
|
|
+ request.setAndroidPopupBody(body); // 弹窗内容
|
|
|
request.setAndroidExtParameters("{\"k1\":\"android\",\"k2\":\"v2\"}");
|
|
request.setAndroidExtParameters("{\"k1\":\"android\",\"k2\":\"v2\"}");
|
|
|
|
|
|
|
|
- // 推送时间(1小时后)
|
|
|
|
|
- request.setPushTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 3600 * 1000)));
|
|
|
|
|
- // 离线存储
|
|
|
|
|
- request.setStoreOffline(true);
|
|
|
|
|
- request.setExpireTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)));
|
|
|
|
|
|
|
+ // 推送控制
|
|
|
|
|
+ // final Date pushDate = new Date(System.currentTimeMillis() + 3600 * 1000); //用于定时发送。不设置缺省是立即发送。时间格式按照 ISO8601 标准表示,并需要使用 UTC 时间,格式为`YYYY-MM-DDThh:mm:ssZ`。
|
|
|
|
|
+ // final String pushTime = ParameterHelper.getISO8601Time(pushDate);
|
|
|
|
|
+ // request.setPushTime(pushTime); // 延后推送。可选,如果不设置表示立即推送
|
|
|
|
|
+ request.setStoreOffline(true); // 离线消息是否保存,若保存, 在推送时候,用户即使不在线,下一次上线则会收到,安卓中若为 false 则只走阿里云自有在线通道
|
|
|
|
|
+ final String expireTime = ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)); // 12 小时后消息失效, 不会再发送
|
|
|
|
|
+ request.setExpireTime(expireTime);
|
|
|
|
|
+ // request.setSendChannels("accs,huawei,honor,vivo,xiaomi,oppo"); // 指定下发的推送通道,若不填可从任何可行的通道下发
|
|
|
|
|
+ // request.setAndroidNotificationNotifyId(1234567); // 设置通知覆盖参数,避免重试等场景用户显示多条相同的通知
|
|
|
|
|
+ // request.setAndroidTargetUserType(0); // 代表本次推送如果推送到华为、荣耀、vivo 通道,是一个正式通知,但本代码示例中这个值被厂商通道特有参数所覆盖
|
|
|
|
|
+ // request.setAndroidHuaweiTargetUserType(1); // 代表本次推送如果推送到华为通道,是一个测试性质的通知
|
|
|
|
|
+ // request.setAndroidHonorTargetUserType(1); // 代表本次推送如果推送到荣耀通道,是一个测试性质的通知
|
|
|
|
|
+ // request.setAndroidVivoPushMode(1); // 代表本次推送如果推送到华为通道,是一个测试性质的通知,请在推送前把 vivo 设备 regId 加入 vivo 推送平台的测试设备列表中
|
|
|
|
|
+ // request.setAndroidHuaweiReceiptId("ABCDEFG"); // 华为厂商通道
|
|
|
|
|
+ // 厂商通道通知分类
|
|
|
|
|
+ request.setAndroidNotificationChannel("Normal"); // OPPO 通道私信通道 channel_id,以及通用安卓 channel_id
|
|
|
|
|
+ // request.setAndroidNotificationXiaomiChannel("user_define"); //小米通道通知类型 channel_id
|
|
|
|
|
+ // request.setAndroidNotificationHonorChannel("NORMAL"); // 荣耀通道消息分类参数,对应荣耀通道 importance 参数
|
|
|
|
|
+ // request.setAndroidMessageHuaweiCategory("ACCOUNT"); // 华为通道的通知分类参数,服务与通讯类通知需向华为通道申请权限
|
|
|
|
|
+ // request.setAndroidMessageVivoCategory("ACCOUNT"); // vivo 通道的通知分类参数,系统消息需向 vivo 通道申请权限
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 推送时效配置 ==========
|
|
|
|
|
+ // 推送时间:1小时后推送(示例)
|
|
|
|
|
+ // request.setPushTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 3600 * 1000)));
|
|
|
|
|
+ request.setStoreOffline(true); // 开启离线存储
|
|
|
|
|
+ // request.setExpireTime(ParameterHelper.getISO8601Time(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))); // 12小时过期
|
|
|
|
|
|
|
|
return request;
|
|
return request;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据用户ID获取设备ID(替换为你的实际逻辑)
|
|
|
|
|
|
|
+ * 根据用户ID获取绑定的设备ID(示例方法)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 注意:需替换为业务系统实际逻辑,例如:
|
|
|
|
|
+ * 1. 从数据库查询用户-设备绑定关系表
|
|
|
|
|
+ * 2. 从缓存中获取用户最新的设备ID
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param userId 业务系统用户ID
|
|
|
|
|
+ * @return 设备ID(示例返回随机生成的模拟值)
|
|
|
*/
|
|
*/
|
|
|
private String getDeviceIdByUserId(Long userId) {
|
|
private String getDeviceIdByUserId(Long userId) {
|
|
|
- // 示例:模拟返回设备ID
|
|
|
|
|
- return "DEVICE" + UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
|
|
+ // 示例:模拟返回设备ID(实际项目中需替换为真实查询逻辑)
|
|
|
|
|
+ return "f92564ccc1a84aebaf58a796091a66a2";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析模板标题(替换为你的实际逻辑)
|
|
|
|
|
|
|
+ * 通过站内信获取相同发送信息
|
|
|
|
|
+ * @return
|
|
|
*/
|
|
*/
|
|
|
- private String parseTemplateTitle(String templateCode, Map<String, Object> params) {
|
|
|
|
|
- return "【系统通知】" + params.get("titleSuffix");
|
|
|
|
|
|
|
+ private String getMessageByNotify(Long userId, String templateCode, Map<String, Object> templateParams) {
|
|
|
|
|
+ // 校验模版
|
|
|
|
|
+ NotifyTemplateDO template = validateNotifyTemplate(templateCode);
|
|
|
|
|
+ if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
|
|
|
|
|
+ log.info("[sendSingleNotify][模版({})已经关闭,无法给用户({})发送]", templateCode, userId);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验参数
|
|
|
|
|
+ validateTemplateParams(template, templateParams);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送站内信
|
|
|
|
|
+ return StrUtil.format(template.getContent(), templateParams);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @VisibleForTesting
|
|
|
|
|
+ public NotifyTemplateDO validateNotifyTemplate(String templateCode) {
|
|
|
|
|
+ // 获得站内信模板。考虑到效率,从缓存中获取
|
|
|
|
|
+ NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode);
|
|
|
|
|
+ // 站内信模板不存在
|
|
|
|
|
+ if (template == null) {
|
|
|
|
|
+ throw exception(NOTICE_NOT_FOUND);
|
|
|
|
|
+ }
|
|
|
|
|
+ return template;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析模板内容(替换为你的实际逻辑)
|
|
|
|
|
|
|
+ * 校验站内信模版参数是否确实
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param template 邮箱模板
|
|
|
|
|
+ * @param templateParams 参数列表
|
|
|
*/
|
|
*/
|
|
|
- private String parseTemplateBody(String templateCode, Map<String, Object> params) {
|
|
|
|
|
- return "您有一条新的通知:" + params.get("content");
|
|
|
|
|
|
|
+ @VisibleForTesting
|
|
|
|
|
+ public void validateTemplateParams(NotifyTemplateDO template, Map<String, Object> templateParams) {
|
|
|
|
|
+ template.getParams().forEach(key -> {
|
|
|
|
|
+ Object value = templateParams.get(key);
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ throw exception(NOTIFY_SEND_TEMPLATE_PARAM_MISS, key);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
}
|
|
}
|