package shop.alien.store.service.impl;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import shop.alien.entity.store.CommonPushChannelConfig;
import shop.alien.entity.store.CommonPushTask;
import shop.alien.entity.store.CommonPushTaskUser;
import shop.alien.entity.store.LifeUser;
import shop.alien.mapper.CommonPushChannelConfigMapper;
import shop.alien.mapper.LifeUserMapper;
import shop.alien.store.dto.CommonPushSendResultDto;
import shop.alien.store.dto.CommonPushTargetDto;
import shop.alien.store.dto.CommonPushVendorSendResult;
import shop.alien.store.service.CommonPushSendService;
import shop.alien.store.service.CommonPushTaskUserService;
import shop.alien.store.service.channel.ApnsPushClient;
import shop.alien.store.service.channel.CommonPushVendorHttpClient;
import shop.alien.store.util.CommonPushVendorTaskIdUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class CommonPushSendServiceImpl implements CommonPushSendService {

    /** targetType=1 时走厂商全量广播 API 的渠道（按顺序执行，互不影响） */
    private static final List<String> FULL_BROADCAST_CHANNELS =
            Arrays.asList("apns", "huawei", "honor", "xiaomi", "oppo", "vivo");

    private final CommonPushChannelConfigMapper commonPushChannelConfigMapper;
    private final LifeUserMapper lifeUserMapper;
    private final CommonPushVendorHttpClient commonPushVendorHttpClient;
    private final CommonPushTaskUserService commonPushTaskUserService;

    /**
     * 查询当前可参与下发的厂商渠道（enable=1、凭证 JSON 合法、未超日配额）。
     */
    @Override
    public List<CommonPushChannelConfig> listSendableChannels() {
        List<CommonPushChannelConfig> configs = commonPushChannelConfigMapper.selectList(
                new LambdaQueryWrapper<CommonPushChannelConfig>()
                        .eq(CommonPushChannelConfig::getEnable, 1)
                        .orderByAsc(CommonPushChannelConfig::getPriorityLevel));
        if (configs == null || configs.isEmpty()) {
            return Collections.emptyList();
        }
        List<CommonPushChannelConfig> result = new ArrayList<>();
        for (CommonPushChannelConfig config : configs) {
            if (StringUtils.isBlank(config.getCredentialJson())) {
                continue;
            }
            try {
                JSONObject.parseObject(config.getCredentialJson());
            } catch (Exception e) {
                log.warn("渠道凭证非法, channelCode={}", config.getChannelCode());
                continue;
            }
            if (isQuotaExceeded(config)) {
                log.warn("渠道配额已满, channelCode={}, todayUsage={}, dailyQuota={}",
                        config.getChannelCode(), config.getTodayUsage(), config.getDailyQuota());
                continue;
            }
            result.add(config);
        }
        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) {
            return Collections.emptyList();
        }
        if (task.getTargetType() == 1) {
            return toTargetsFromUsers(lifeUserMapper.selectList(deviceIdNotBlankWrapper()));
        }
        if (StringUtils.isBlank(task.getTargetConfig())) {
            return Collections.emptyList();
        }
        JSONObject targetConfig = JSONObject.parseObject(task.getTargetConfig());
        if (targetConfig == null) {
            return Collections.emptyList();
        }
        if (targetConfig.containsKey("deviceIds")) {
            return buildTargetsByDeviceIds(targetConfig.getJSONArray("deviceIds"));
        }
        if (targetConfig.containsKey("userIds")) {
            JSONArray userIds = targetConfig.getJSONArray("userIds");
            if (userIds == null || userIds.isEmpty()) {
                return Collections.emptyList();
            }
            List<Integer> ids = userIds.toJavaList(Integer.class);
            if (ids.isEmpty()) {
                return Collections.emptyList();
            }
            List<LifeUser> users = lifeUserMapper.selectList(
                    deviceIdNotBlankWrapper().in(LifeUser::getId, ids));
            return toTargetsFromUsers(users);
        }
        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);
        return sendToTargets(task, targets);
    }

    @Override
    public CommonPushSendResultDto sendTest(String deviceId, String title, String content,
                                            String vendorChannel, String jumpUrl, String platform) {
        CommonPushSendResultDto result = new CommonPushSendResultDto();
        if (StringUtils.isBlank(deviceId)) {
            result.setSuccess(false);
            result.setMessage("deviceId不能为空");
            return result;
        }
        String normalizedDeviceId = deviceId.trim();
        CommonPushTask task = buildTestTask(title, content, jumpUrl, vendorChannel);
        CommonPushTargetDto target = new CommonPushTargetDto();
        target.setDeviceId(normalizedDeviceId);
        target.setPlatform(normalizePlatform(platform));
        LifeUser user = lifeUserMapper.selectOne(
                new LambdaQueryWrapper<LifeUser>()
                        .eq(LifeUser::getDeviceId, normalizedDeviceId)
                        .last("LIMIT 1"));
        if (user != null && user.getId() != null) {
            target.setUserId(user.getId().longValue());
        }
        return sendToTargets(task, Collections.singletonList(target));
    }

    private CommonPushTask buildTestTask(String title, String content, String jumpUrl, String vendorChannel) {
        CommonPushTask task = new CommonPushTask();
        task.setTitle(StringUtils.defaultIfBlank(title, "测试推送"));
        task.setContent(StringUtils.defaultIfBlank(content, "这是一条测试推送消息"));
        task.setJumpUrl(jumpUrl);
        task.setTaskNo("TEST" + System.currentTimeMillis());
        if (StringUtils.isNotBlank(vendorChannel)) {
            JSONObject cfg = new JSONObject();
            cfg.put("vendorChannel", vendorChannel.trim());
            task.setTargetConfig(cfg.toJSONString());
        }
        return task;
    }

    /**
     * 向目标设备列表执行多渠道推送（全量广播 + 单推/批量推）。
     * <p>格式转换 channelMap：</p>
     * <pre>
     * 转换前 List&lt;CommonPushChannelConfig&gt;: [{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();
        if (channels.isEmpty()) {
            result.setSuccess(false);
            result.setMessage("无可用推送渠道，请检查渠道配置是否启用且凭证完整");
            return result;
        }
        Map<String, CommonPushChannelConfig> channelMap = channels.stream()
                .collect(Collectors.toMap(c -> StringUtils.lowerCase(c.getChannelCode()), c -> c, (a, b) -> a));

        boolean fullBroadcast = Integer.valueOf(1).equals(task.getTargetType());
        boolean anyFullBroadcast = fullBroadcast && FULL_BROADCAST_CHANNELS.stream().anyMatch(channelMap::containsKey);
        if ((targets == null || targets.isEmpty()) && !anyFullBroadcast) {
            result.setSuccess(false);
            result.setMessage("未找到可推送的目标设备");
            return result;
        }
        if (targets == null) {
            targets = Collections.emptyList();
        }

        Map<String, List<CommonPushTargetDto>> grouped = groupTargetsByChannel(targets, channelMap, task.getTargetConfig());

        int sentCount = 0;
        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);
            }
        }

        for (Map.Entry<String, List<CommonPushTargetDto>> entry : grouped.entrySet()) {
            if (broadcastHandledChannels.contains(entry.getKey())) {
                continue;
            }
            CommonPushChannelConfig channelConfig = channelMap.get(entry.getKey());
            if (channelConfig == null) {
                failedCount += entry.getValue().size();
                result.getFailedTargets().addAll(entry.getValue());
                continue;
            }
            for (CommonPushTargetDto target : entry.getValue()) {
                CommonPushVendorSendResult sendResult;
                try {
                    sendResult = commonPushVendorHttpClient.send(channelConfig, task, target);
                } catch (Exception e) {
                    log.error("渠道单推异常, channelCode={}, deviceId={}", entry.getKey(), target.getDeviceId(), e);
                    sendResult = CommonPushVendorSendResult.fail();
                }
                if (sendResult.isSuccess()) {
                    sentCount++;
                    result.getSentTargets().add(target);
                    updateChannelUsage(channelConfig);
                    CommonPushVendorTaskIdUtil.mergeTaskId(vendorTaskIds, entry.getKey(), sendResult);
                } else {
                    failedCount++;
                    result.getFailedTargets().add(target);
                }
            }
        }
        result.setVendorTaskIds(vendorTaskIds);
        result.setSentCount(sentCount);
        result.setFailedCount(failedCount);
        result.setSuccess(sentCount > 0);
        if (sentCount > 0 && failedCount > 0) {
            result.setMessage("推送部分完成，成功 " + sentCount + "，失败 " + failedCount);
        } else {
            result.setMessage(sentCount > 0 ? "推送完成" : "推送失败，请检查渠道凭证与设备 token");
        }
        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,
                                         CommonPushChannelConfig channelConfig) {
        List<CommonPushTargetDto> channelTargets = grouped.getOrDefault(channelCode, Collections.emptyList());
        if (ok) {
            int broadcastCount = channelTargets.isEmpty()
                    ? defaultCount(task.getEstimatedCount())
                    : channelTargets.size();
            if (channelTargets.isEmpty()) {
                CommonPushTargetDto broadcastTarget = new CommonPushTargetDto();
                broadcastTarget.setPlatform("android");
                broadcastTarget.setBroadcast(true);
                result.getSentTargets().add(broadcastTarget);
            } else {
                result.getSentTargets().addAll(channelTargets);
            }
            updateChannelUsageSafely(channelConfig, channelCode);
            return broadcastCount;
        }
        if (!channelTargets.isEmpty()) {
            result.getFailedTargets().addAll(channelTargets);
        }
        return 0;
    }

    /** 按渠道编码调用对应厂商全量/广播推送 API（apns/huawei/honor/xiaomi/oppo/vivo）。 */
    private CommonPushVendorSendResult invokeFullBroadcast(String channelCode, CommonPushChannelConfig config, CommonPushTask task,
                                          List<CommonPushTargetDto> channelTargets) {
        try {
            switch (channelCode) {
                case "apns":
                    return commonPushVendorHttpClient.sendApnsBroadcast(config, task, channelTargets);
                case "huawei":
                    return commonPushVendorHttpClient.sendHuaweiBroadcast(config, task, channelTargets);
                case "honor":
                    return commonPushVendorHttpClient.sendHonorBroadcast(config, task, channelTargets);
                case "xiaomi":
                    return commonPushVendorHttpClient.sendXiaomiBroadcast(config, task);
                case "oppo":
                    return commonPushVendorHttpClient.sendOppoBroadcast(config, task);
                case "vivo":
                    return commonPushVendorHttpClient.sendVivoBroadcast(config, task);
                default:
                    return CommonPushVendorSendResult.fail();
            }
        } catch (Exception e) {
            log.error("全量广播推送异常, channelCode={}, taskNo={}", channelCode, task.getTaskNo(), e);
            return CommonPushVendorSendResult.fail();
        }
    }

    /** 更新渠道 today_usage，失败仅打日志不中断推送。 */
    private void updateChannelUsageSafely(CommonPushChannelConfig config, String channelCode) {
        try {
            updateChannelUsage(config);
        } catch (Exception e) {
            log.warn("更新渠道用量失败, channelCode={}", channelCode, e);
        }
    }

    /** 全量广播失败时的失败条数（无设备时分母按 1 计）。 */
    private int countFullBroadcastFailure(boolean ok, String channelCode,
                                          Map<String, List<CommonPushTargetDto>> grouped) {
        if (ok) {
            return 0;
        }
        List<CommonPushTargetDto> channelTargets = grouped.getOrDefault(channelCode, Collections.emptyList());
        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) {
            return;
        }
        Set<String> sentDeviceIds = sendResult.getSentTargets().stream()
                .map(CommonPushTargetDto::getDeviceId)
                .collect(Collectors.toSet());
        List<CommonPushTargetDto> allTargets = new ArrayList<>();
        allTargets.addAll(sendResult.getSentTargets());
        allTargets.addAll(sendResult.getFailedTargets());
        for (CommonPushTargetDto target : allTargets) {
            if (Boolean.TRUE.equals(target.getBroadcast()) || StringUtils.isBlank(target.getDeviceId())) {
                continue;
            }
            CommonPushTaskUser taskUser = new CommonPushTaskUser();
            taskUser.setPushTaskId(pushTaskId);
            taskUser.setUserId(target.getUserId());
            taskUser.setDeviceId(target.getDeviceId());
            taskUser.setRelType(1);
            taskUser.setStatus(sentDeviceIds.contains(target.getDeviceId()) ? 2 : 0);
            taskUser.setShowInfo(0);
            taskUser.setUserAdd(0);
            commonPushTaskUserService.save(taskUser);
        }
        for (CommonPushTargetDto target : sendResult.getSentTargets()) {
            if (!Boolean.TRUE.equals(target.getBroadcast())) {
                continue;
            }
            CommonPushTaskUser taskUser = new CommonPushTaskUser();
            taskUser.setPushTaskId(pushTaskId);
            taskUser.setRelType(1);
            taskUser.setStatus(2);
            taskUser.setShowInfo(0);
            taskUser.setUserAdd(0);
            commonPushTaskUserService.save(taskUser);
        }
    }

    /**
     * 按 targetConfig.deviceIds 解析目标（去重、trim 后查 life_user）。
     * <p>格式转换：</p>
     * <pre>
     * 转换前 JSONArray: [" HUAWEI_CN_IQAAA... ", "token2"]
     * 转换后 List&lt;String&gt;: ["HUAWEI_CN_IQAAA...", "token2"]
     * 再经 toTargetsFromUsers → List&lt;CommonPushTargetDto&gt;
     * </pre>
     */
    private List<CommonPushTargetDto> buildTargetsByDeviceIds(JSONArray deviceIds) {
        if (deviceIds == null || deviceIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> idList = new ArrayList<>();
        Set<String> exists = new HashSet<>();
        for (int i = 0; i < deviceIds.size(); i++) {
            String deviceId = deviceIds.getString(i);
            if (StringUtils.isNotBlank(deviceId) && exists.add(deviceId.trim())) {
                idList.add(deviceId.trim());
            }
        }
        if (idList.isEmpty()) {
            return Collections.emptyList();
        }
        List<LifeUser> users = lifeUserMapper.selectList(
                deviceIdNotBlankWrapper().in(LifeUser::getDeviceId, idList));
        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();
        }
        List<CommonPushTargetDto> targets = new ArrayList<>();
        for (LifeUser user : users) {
            if (user == null || StringUtils.isBlank(user.getDeviceId())) {
                continue;
            }
            CommonPushTargetDto target = new CommonPushTargetDto();
            target.setUserId(user.getId() != null ? user.getId().longValue() : null);
            target.setDeviceId(user.getDeviceId());
            // TODO 这个 platform 是根据 deviceId 自动判断的，这里先默认 android 了？怎么判断是 android 还是 ios？
            if (isIOSDeviceToken(user.getDeviceId())) {
                target.setPlatform("ios");
            } else {
                target.setPlatform("android");
            }
            targets.add(target);
        }
        return targets;
    }

    private boolean isIOSDeviceToken(String deviceId) {
        if (deviceId == null || deviceId.length() != 64) {
            return false;
        }
        // 正则：仅0-9、a-f小写
        return deviceId.matches("[0-9a-f]{64}");
    }

    /**
     * 按厂商渠道对推送目标分组，供后续逐渠道调用 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) {
        String preferredChannel = null;
        if (StringUtils.isNotBlank(targetConfigJson)) {
            JSONObject cfg = JSONObject.parseObject(targetConfigJson);
            if (cfg != null && StringUtils.isNotBlank(cfg.getString("vendorChannel"))) {
                preferredChannel = StringUtils.lowerCase(cfg.getString("vendorChannel"));
            }
        }
        Map<String, List<CommonPushTargetDto>> grouped = new HashMap<>();
        for (CommonPushTargetDto target : targets) {
            String channelCode = resolveChannelCode(target, channelMap, preferredChannel);
            if (channelCode == null) {
                continue;
            }
            grouped.computeIfAbsent(channelCode, k -> new ArrayList<>()).add(target);
        }
        return grouped;
    }

    /**
     * 为单个目标解析厂商渠道编码。
     * <p>优先级：targetConfig.vendorChannel → iOS 用 apns → Android 用 channelMap 中优先级最高渠道。</p>
     * <p>Android 优先级：huawei(1) &gt; honor(2) &gt; xiaomi(3) &gt; oppo(4) &gt; vivo(5) &gt; samsung(6)</p>
     */
    private String resolveChannelCode(CommonPushTargetDto target,
                                      Map<String, CommonPushChannelConfig> channelMap,
                                      String preferredChannel) {
        if (StringUtils.isNotBlank(preferredChannel) && channelMap.containsKey(preferredChannel)) {
            return preferredChannel;
        }
        String platform = StringUtils.lowerCase(StringUtils.defaultString(target.getPlatform()));
        if (platform.contains("ios") || "iphone".equals(platform)) {
            return channelMap.containsKey("apns") ? "apns" : null;
        }
        List<String> androidChannels = channelMap.keySet().stream()
                .filter(code -> !"apns".equals(code))
                .sorted(Comparator.comparingInt(this::channelPriority))
                .collect(Collectors.toList());
        return androidChannels.isEmpty() ? null : androidChannels.get(0);
    }

    /** Android 厂商渠道自动选择时的排序权重（越小越优先）。 */
    private int channelPriority(String channelCode) {
        switch (channelCode) {
            case "huawei":
                return 1;
            case "honor":
                return 2;
            case "xiaomi":
                return 3;
            case "oppo":
                return 4;
            case "vivo":
                return 5;
            case "samsung":
                return 6;
            default:
                return 99;
        }
    }

    private String normalizePlatform(String platform) {
        if (StringUtils.isBlank(platform)) {
            return "android";
        }
        return StringUtils.lowerCase(platform.trim());
    }

    /** 判断渠道是否已达 daily_quota 上限（0 表示不限）。 */
    private boolean isQuotaExceeded(CommonPushChannelConfig config) {
        if (config.getDailyQuota() == null || config.getDailyQuota() <= 0) {
            return false;
        }
        int usage = config.getTodayUsage() == null ? 0 : config.getTodayUsage();
        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);
        commonPushChannelConfigMapper.updateById(config);
    }
}
