|
|
@@ -48,6 +48,9 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
private final CommonPushVendorHttpClient commonPushVendorHttpClient;
|
|
|
private final CommonPushTaskUserService commonPushTaskUserService;
|
|
|
|
|
|
+ /**
|
|
|
+ * 查询当前可参与下发的厂商渠道(enable=1、凭证 JSON 合法、未超日配额)。
|
|
|
+ */
|
|
|
@Override
|
|
|
public List<CommonPushChannelConfig> listSendableChannels() {
|
|
|
List<CommonPushChannelConfig> configs = commonPushChannelConfigMapper.selectList(
|
|
|
@@ -78,6 +81,16 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据任务 targetType / targetConfig 解析推送目标设备列表。
|
|
|
+ * <p>格式转换 targetConfig JSON → 查询条件:</p>
|
|
|
+ * <pre>
|
|
|
+ * targetType=1: 查 life_user 全部 device_id 非空用户
|
|
|
+ * targetConfig={"userIds":[100,101]} → WHERE id IN (100,101) AND device_id 非空
|
|
|
+ * targetConfig={"deviceIds":["tokenA"]} → WHERE device_id IN ('tokenA')
|
|
|
+ * </pre>
|
|
|
+ * <p>输出 {@link CommonPushTargetDto} 列表,见 {@link #toTargetsFromUsers}。</p>
|
|
|
+ */
|
|
|
@Override
|
|
|
public List<CommonPushTargetDto> resolveTargets(CommonPushTask task) {
|
|
|
if (task == null || task.getTargetType() == null) {
|
|
|
@@ -112,12 +125,16 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
|
|
|
+ /** life_user 查询条件:仅包含已上报 device_id(厂商 Push Token)的用户。 */
|
|
|
private LambdaQueryWrapper<LifeUser> deviceIdNotBlankWrapper() {
|
|
|
return new LambdaQueryWrapper<LifeUser>()
|
|
|
.isNotNull(LifeUser::getDeviceId)
|
|
|
.ne(LifeUser::getDeviceId, "");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 推送任务下发入口:解析目标 → 分组 → 调用各厂商 API。
|
|
|
+ */
|
|
|
@Override
|
|
|
public CommonPushSendResultDto send(CommonPushTask task) {
|
|
|
List<CommonPushTargetDto> targets = resolveTargets(task);
|
|
|
@@ -162,6 +179,14 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return task;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 向目标设备列表执行多渠道推送(全量广播 + 单推/批量推)。
|
|
|
+ * <p>格式转换 channelMap:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 List<CommonPushChannelConfig>: [{channelCode:"huawei"}, {channelCode:"oppo"}]
|
|
|
+ * 转换后 Map: { "huawei" -> config, "oppo" -> config }
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
private CommonPushSendResultDto sendToTargets(CommonPushTask task, List<CommonPushTargetDto> targets) {
|
|
|
CommonPushSendResultDto result = new CommonPushSendResultDto();
|
|
|
List<CommonPushChannelConfig> channels = listSendableChannels();
|
|
|
@@ -190,20 +215,28 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
int failedCount = 0;
|
|
|
Map<String, String> vendorTaskIds = new LinkedHashMap<>();
|
|
|
Set<String> broadcastHandledChannels = new HashSet<>();
|
|
|
+ // targetType=1 全量:对 apns/huawei/honor/xiaomi/oppo/vivo 依次走厂商广播或批量 API,与后续单推互斥
|
|
|
if (fullBroadcast) {
|
|
|
for (String channelCode : FULL_BROADCAST_CHANNELS) {
|
|
|
if (!channelMap.containsKey(channelCode)) {
|
|
|
continue;
|
|
|
}
|
|
|
+ // 标记该渠道已由广播处理,后续 grouped 单推循环会 skip,避免重复下发
|
|
|
broadcastHandledChannels.add(channelCode);
|
|
|
CommonPushChannelConfig channelConfig = channelMap.get(channelCode);
|
|
|
+ // 本渠道在 groupTargetsByChannel 中分到的设备;无匹配时为空列表(部分厂商仍支持无 token 广播)
|
|
|
List<CommonPushTargetDto> channelTargets = grouped.getOrDefault(channelCode, Collections.emptyList());
|
|
|
+ // APNs 需从全量 targets 中筛 iOS deviceToken;其他厂商直接用 channelTargets
|
|
|
List<CommonPushTargetDto> broadcastTargets = "apns".equals(channelCode)
|
|
|
? ApnsPushClient.filterApnsTargets(channelTargets, targets)
|
|
|
: channelTargets;
|
|
|
+ // 调用对应厂商全量/广播接口(如 OPPO broadcast、华为按 token 批量)
|
|
|
CommonPushVendorSendResult sendResult = invokeFullBroadcast(channelCode, channelConfig, task, broadcastTargets);
|
|
|
+ // 成功时写入 vendorTaskIds,格式:huawei_task_id -> requestId
|
|
|
CommonPushVendorTaskIdUtil.putTaskId(vendorTaskIds, channelCode, sendResult);
|
|
|
+ // 累加 sentCount、更新 sentTargets,并更新渠道 today_usage
|
|
|
sentCount += applyFullBroadcastResult(sendResult.isSuccess(), channelCode, grouped, task, result, channelConfig);
|
|
|
+ // 广播失败时累加 failedCount(无设备时分母按 1 计)
|
|
|
failedCount += countFullBroadcastFailure(sendResult.isSuccess(), channelCode, grouped);
|
|
|
}
|
|
|
}
|
|
|
@@ -249,10 +282,15 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /** estimatedCount 为空或 ≤0 时广播成功计数按 1 计。 */
|
|
|
private int defaultCount(Integer count) {
|
|
|
return count == null || count <= 0 ? 1 : count;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 全量广播渠道推送成功后的计数与结果汇总。
|
|
|
+ * <p>无具体设备时 sentCount 取 task.estimatedCount(默认至少 1)。</p>
|
|
|
+ */
|
|
|
private int applyFullBroadcastResult(boolean ok, String channelCode,
|
|
|
Map<String, List<CommonPushTargetDto>> grouped,
|
|
|
CommonPushTask task, CommonPushSendResultDto result,
|
|
|
@@ -279,6 +317,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
+ /** 按渠道编码调用对应厂商全量/广播推送 API(apns/huawei/honor/xiaomi/oppo/vivo)。 */
|
|
|
private CommonPushVendorSendResult invokeFullBroadcast(String channelCode, CommonPushChannelConfig config, CommonPushTask task,
|
|
|
List<CommonPushTargetDto> channelTargets) {
|
|
|
try {
|
|
|
@@ -304,6 +343,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 更新渠道 today_usage,失败仅打日志不中断推送。 */
|
|
|
private void updateChannelUsageSafely(CommonPushChannelConfig config, String channelCode) {
|
|
|
try {
|
|
|
updateChannelUsage(config);
|
|
|
@@ -312,6 +352,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 全量广播失败时的失败条数(无设备时分母按 1 计)。 */
|
|
|
private int countFullBroadcastFailure(boolean ok, String channelCode,
|
|
|
Map<String, List<CommonPushTargetDto>> grouped) {
|
|
|
if (ok) {
|
|
|
@@ -321,6 +362,15 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return channelTargets.isEmpty() ? 1 : channelTargets.size();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 推送完成后写入 common_push_task_user 发送记录。
|
|
|
+ * <p>格式转换发送状态:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前: target 在 sentTargets 中 → status=2(已发送)
|
|
|
+ * target 仅在 failedTargets 中 → status=0(失败)
|
|
|
+ * broadcast 占位目标(无 deviceId)→ 单独一条 status=2 记录
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
@Override
|
|
|
public void saveTaskUserRecords(Long pushTaskId, CommonPushSendResultDto sendResult) {
|
|
|
if (pushTaskId == null || sendResult == null) {
|
|
|
@@ -360,6 +410,15 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 按 targetConfig.deviceIds 解析目标(去重、trim 后查 life_user)。
|
|
|
+ * <p>格式转换:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 JSONArray: [" HUAWEI_CN_IQAAA... ", "token2"]
|
|
|
+ * 转换后 List<String>: ["HUAWEI_CN_IQAAA...", "token2"]
|
|
|
+ * 再经 toTargetsFromUsers → List<CommonPushTargetDto>
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
private List<CommonPushTargetDto> buildTargetsByDeviceIds(JSONArray deviceIds) {
|
|
|
if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
|
@@ -380,6 +439,15 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return toTargetsFromUsers(users);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将 life_user 转为推送目标 DTO。
|
|
|
+ * <p>格式转换:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 LifeUser: { id: 100, deviceId: "HUAWEI_CN_IQAAA..." }
|
|
|
+ * 转换后 CommonPushTargetDto: { userId: 100L, deviceId: "HUAWEI_CN_IQAAA...", platform: "android" }
|
|
|
+ * </pre>
|
|
|
+ * <p>华为 token 前缀剥离在 {@link CommonPushVendorHttpClient} 下发时处理。</p>
|
|
|
+ */
|
|
|
private List<CommonPushTargetDto> toTargetsFromUsers(List<LifeUser> users) {
|
|
|
if (users == null || users.isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
|
@@ -392,12 +460,22 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
CommonPushTargetDto target = new CommonPushTargetDto();
|
|
|
target.setUserId(user.getId() != null ? user.getId().longValue() : null);
|
|
|
target.setDeviceId(user.getDeviceId());
|
|
|
+ // TODO 这个 platform 是根据 deviceId 自动判断的,这里先默认 android 了?怎么判断是 android 还是 ios?
|
|
|
target.setPlatform("android");
|
|
|
targets.add(target);
|
|
|
}
|
|
|
return targets;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 按厂商渠道对推送目标分组,供后续逐渠道调用 API。
|
|
|
+ * <p>格式转换 targetConfig → preferredChannel:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 String: "{\"vendorChannel\":\"huawei\"}"
|
|
|
+ * 转换后 String: "huawei"(小写;未配置则为 null,走 platform 自动选择)
|
|
|
+ * </pre>
|
|
|
+ * <p>输出示例:{ "huawei" -> [target1, target2], "apns" -> [target3] }</p>
|
|
|
+ */
|
|
|
private Map<String, List<CommonPushTargetDto>> groupTargetsByChannel(List<CommonPushTargetDto> targets,
|
|
|
Map<String, CommonPushChannelConfig> channelMap,
|
|
|
String targetConfigJson) {
|
|
|
@@ -419,6 +497,11 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return grouped;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 为单个目标解析厂商渠道编码。
|
|
|
+ * <p>优先级:targetConfig.vendorChannel → iOS 用 apns → Android 用 channelMap 中优先级最高渠道。</p>
|
|
|
+ * <p>Android 优先级:huawei(1) > honor(2) > xiaomi(3) > oppo(4) > vivo(5) > samsung(6)</p>
|
|
|
+ */
|
|
|
private String resolveChannelCode(CommonPushTargetDto target,
|
|
|
Map<String, CommonPushChannelConfig> channelMap,
|
|
|
String preferredChannel) {
|
|
|
@@ -436,6 +519,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return androidChannels.isEmpty() ? null : androidChannels.get(0);
|
|
|
}
|
|
|
|
|
|
+ /** Android 厂商渠道自动选择时的排序权重(越小越优先)。 */
|
|
|
private int channelPriority(String channelCode) {
|
|
|
switch (channelCode) {
|
|
|
case "huawei":
|
|
|
@@ -462,6 +546,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return StringUtils.lowerCase(platform.trim());
|
|
|
}
|
|
|
|
|
|
+ /** 判断渠道是否已达 daily_quota 上限(0 表示不限)。 */
|
|
|
private boolean isQuotaExceeded(CommonPushChannelConfig config) {
|
|
|
if (config.getDailyQuota() == null || config.getDailyQuota() <= 0) {
|
|
|
return false;
|
|
|
@@ -470,6 +555,7 @@ public class CommonPushSendServiceImpl implements CommonPushSendService {
|
|
|
return usage >= config.getDailyQuota();
|
|
|
}
|
|
|
|
|
|
+ /** 单推成功后 today_usage +1 并写回 common_push_channel_config。 */
|
|
|
private void updateChannelUsage(CommonPushChannelConfig config) {
|
|
|
int usage = config.getTodayUsage() == null ? 0 : config.getTodayUsage();
|
|
|
config.setTodayUsage(usage + 1);
|