package com.ruoyi.other.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.ruoyi.account.api.dto.SendMessageDTO; import com.ruoyi.account.api.feignClient.AppUserClient; import com.ruoyi.account.api.feignClient.AppUserWhitelistClient; import com.ruoyi.account.api.feignClient.ChargingCardClient; import com.ruoyi.account.api.feignClient.VinManagementClient; import com.ruoyi.account.api.model.TAppUserWhitelist; import com.ruoyi.account.api.model.TChargingCard; import com.ruoyi.account.api.model.TVinManagement; import com.ruoyi.common.core.domain.R; import com.ruoyi.other.api.domain.TBalanceWarningRecord; import com.ruoyi.other.api.domain.TCompanyBalanceWarning; import com.ruoyi.other.api.domain.TGroup; import com.ruoyi.other.api.domain.TGroupCompany; import com.ruoyi.other.mapper.TCompanyBalanceWarningMapper; import com.ruoyi.other.mapper.TGroupCompanyMapper; import com.ruoyi.other.mapper.TGroupMapper; import com.ruoyi.other.service.TBalanceWarningRecordService; import com.ruoyi.other.service.TBalanceWarningScanService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** *

* 公司余额预警扫描服务实现: * 遍历预警配置,公司总账户余额/子账户余额低于预警线时发送提醒短信(按天去重)。 *

* * @author mxcd * @since 2026-10-10 */ @Slf4j @Service public class TBalanceWarningScanServiceImpl implements TBalanceWarningScanService { /** 预警范围:公司账户 */ private static final int WARN_SCOPE_COMPANY = 1; /** 预警范围:VIN码 */ private static final int WARN_SCOPE_VIN = 2; /** 预警范围:刷卡卡 */ private static final int WARN_SCOPE_CARD = 3; /** 预警范围:白名单用户 */ private static final int WARN_SCOPE_WHITELIST = 4; /** 通知对象:集团联系人 */ private static final int NOTIFY_OBJECT_GROUP_CONTACT = 1; /** 通知对象:公司联系人 */ private static final int NOTIFY_OBJECT_COMPANY_CONTACT = 2; /** 通知方式:短信 */ private static final int NOTIFY_TYPE_SMS = 1; /** 账户模式:分配模式 */ private static final int ACCOUNT_MODE_ALLOCATION = 2; /** 公司状态:启用 */ private static final int COMPANY_STATUS_ENABLED = 1; /** 短信类型:公司总账户余额不足 */ private static final int SMS_TYPE_COMPANY_BALANCE = 5; /** 短信类型:子账号余额不足 */ private static final int SMS_TYPE_SUB_BALANCE = 6; @Resource private TCompanyBalanceWarningMapper companyBalanceWarningMapper; @Resource private TGroupCompanyMapper groupCompanyMapper; @Resource private TGroupMapper groupMapper; @Resource private TBalanceWarningRecordService balanceWarningRecordService; @Resource private VinManagementClient vinManagementClient; @Resource private ChargingCardClient chargingCardClient; @Resource private AppUserWhitelistClient appUserWhitelistClient; @Resource private AppUserClient appUserClient; @Override public void scan() { List warnings = companyBalanceWarningMapper.selectList( Wrappers.lambdaQuery()); for (TCompanyBalanceWarning warning : warnings) { try { handleWarning(warning); } catch (Exception e) { log.error("余额预警扫描处理失败, companyId={}", warning.getCompanyId(), e); } } } /** * 处理单条预警配置 */ private void handleWarning(TCompanyBalanceWarning warning) { if (warning.getCompanyId() == null) { return; } TGroupCompany company = groupCompanyMapper.selectById(warning.getCompanyId()); // 公司不存在或已停用,跳过 if (company == null || !Integer.valueOf(COMPANY_STATUS_ENABLED).equals(company.getStatus())) { return; } // 仅支持短信通知 if (warning.getNotifyType() != null && warning.getNotifyType() != NOTIFY_TYPE_SMS) { return; } // 联系人手机号为空时跳过(记日志) String phone = resolveNotifyPhone(warning.getNotifyObject(), company); if (isBlank(phone)) { log.info("余额预警联系人手机号为空,跳过发送, companyId={}, notifyObject={}", warning.getCompanyId(), warning.getNotifyObject()); return; } // 公司总账户余额预警 if (warning.getCompanyWarnAmount() != null && company.getAvailableBalance() != null && company.getAvailableBalance().compareTo(warning.getCompanyWarnAmount()) <= 0) { sendCompanyWarn(warning, company, phone); } // 子账户余额预警(仅分配模式) if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(company.getAccountMode()) && warning.getSubWarnAmount() != null) { warnSubAccounts(warning, company, phone); } } /** * 解析通知对象手机号(1=集团联系人,2=公司联系人;默认集团联系人) */ private String resolveNotifyPhone(Integer notifyObject, TGroupCompany company) { int target = notifyObject == null ? NOTIFY_OBJECT_GROUP_CONTACT : notifyObject; if (target == NOTIFY_OBJECT_COMPANY_CONTACT) { return company.getContactPhone(); } if (company.getGroupId() == null) { return null; } TGroup group = groupMapper.selectById(company.getGroupId()); return group == null ? null : group.getContactPhone(); } /** * 发送公司总账户余额不足提醒(模板1:公司名称/当前余额/预警线) */ private void sendCompanyWarn(TCompanyBalanceWarning warning, TGroupCompany company, String phone) { if (balanceWarningRecordService.isNotifiedToday(warning.getCompanyId(), WARN_SCOPE_COMPANY, "")) { return; } SendMessageDTO dto = new SendMessageDTO(); dto.setType(SMS_TYPE_COMPANY_BALANCE); dto.setPhone(phone); dto.setCompanyName(company.getName()); dto.setBalance(formatAmount(company.getAvailableBalance())); dto.setWarnAmount(formatAmount(warning.getCompanyWarnAmount())); sendSms(dto, warning.getCompanyId(), WARN_SCOPE_COMPANY, "", phone); } /** * 扫描三类子账户(VIN/卡/白名单)余额,低于子账户预警线时发送提醒(模板2) */ private void warnSubAccounts(TCompanyBalanceWarning warning, TGroupCompany company, String phone) { Integer companyId = warning.getCompanyId(); BigDecimal subWarn = warning.getSubWarnAmount(); String companyName = company.getName(); for (TVinManagement vin : fetchVins(companyId)) { if (vin.getBalance() == null || vin.getBalance().compareTo(subWarn) > 0) { continue; } String identifier = vin.getVin(); if (isBlank(identifier)) { continue; } if (balanceWarningRecordService.isNotifiedToday(companyId, WARN_SCOPE_VIN, identifier)) { continue; } String userInfo = isBlank(vin.getPlateNumber()) ? identifier : vin.getPlateNumber(); sendSms(buildSubWarnDto(phone, companyName, userInfo, vin.getBalance(), subWarn), companyId, WARN_SCOPE_VIN, identifier, phone); } for (TChargingCard card : fetchCards(companyId)) { if (card.getBalance() == null || card.getBalance().compareTo(subWarn) > 0) { continue; } String identifier = card.getCardNo(); if (isBlank(identifier)) { continue; } if (balanceWarningRecordService.isNotifiedToday(companyId, WARN_SCOPE_CARD, identifier)) { continue; } sendSms(buildSubWarnDto(phone, companyName, identifier, card.getBalance(), subWarn), companyId, WARN_SCOPE_CARD, identifier, phone); } for (TAppUserWhitelist whitelist : fetchWhitelists(companyId)) { if (whitelist.getBalance() == null || whitelist.getBalance().compareTo(subWarn) > 0) { continue; } String identifier = whitelist.getPhone(); if (isBlank(identifier)) { continue; } if (balanceWarningRecordService.isNotifiedToday(companyId, WARN_SCOPE_WHITELIST, identifier)) { continue; } String userInfo = isBlank(whitelist.getName()) ? identifier : whitelist.getName(); sendSms(buildSubWarnDto(phone, companyName, userInfo, whitelist.getBalance(), subWarn), companyId, WARN_SCOPE_WHITELIST, identifier, phone); } } /** * 构建子账号余额不足短信 DTO */ private SendMessageDTO buildSubWarnDto(String phone, String companyName, String userInfo, BigDecimal balance, BigDecimal subWarn) { SendMessageDTO dto = new SendMessageDTO(); dto.setType(SMS_TYPE_SUB_BALANCE); dto.setPhone(phone); dto.setCompanyName(companyName); dto.setUserInfo(userInfo); dto.setBalance(formatAmount(balance)); dto.setWarnAmount(formatAmount(subWarn)); return dto; } /** * 发送短信;成功后写提醒记录(按天去重),失败不写以便下次扫描重试 */ private void sendSms(SendMessageDTO dto, Integer companyId, int warnScope, String subIdentifier, String phone) { try { R result = appUserClient.sendMessage(dto); if (R.isSuccess(result)) { saveNotifyRecord(companyId, warnScope, subIdentifier, phone); log.info("余额预警短信发送成功, companyId={}, scope={}, identifier={}, phone={}", companyId, warnScope, subIdentifier, phone); } else { log.warn("余额预警短信发送失败, companyId={}, scope={}, identifier={}, result={}", companyId, warnScope, subIdentifier, result); } } catch (Exception e) { log.error("余额预警短信发送异常, companyId={}, scope={}, identifier={}", companyId, warnScope, subIdentifier, e); } } /** * 保存提醒记录(唯一键兜底,重复插入忽略) */ private void saveNotifyRecord(Integer companyId, int warnScope, String subIdentifier, String phone) { try { TBalanceWarningRecord record = new TBalanceWarningRecord(); record.setCompanyId(companyId); record.setWarnScope(warnScope); record.setSubIdentifier(subIdentifier == null ? "" : subIdentifier); record.setNotifyPhone(phone); record.setWarnDate(LocalDate.now()); balanceWarningRecordService.save(record); } catch (Exception e) { log.warn("保存余额预警提醒记录失败(可能唯一键冲突), companyId={}, scope={}, identifier={}", companyId, warnScope, subIdentifier, e); } } /** * 金额格式化:保留2位小数;超过模板变量长度(10)时退回整数部分,仍超则截断 */ private String formatAmount(BigDecimal amount) { if (amount == null) { return "0"; } String value = amount.setScale(2, RoundingMode.HALF_UP).toPlainString(); if (value.length() > 10) { value = amount.setScale(0, RoundingMode.DOWN).toPlainString(); } return value.length() > 10 ? value.substring(0, 10) : value; } /** * 远程查询公司VIN码列表(降级返回空列表,保证扫描主流程不受影响) */ private List fetchVins(Integer companyId) { try { R> response = vinManagementClient.listByCompany(companyId); return (response != null && R.isSuccess(response) && response.getData() != null) ? response.getData() : new ArrayList<>(); } catch (Exception e) { return new ArrayList<>(); } } /** * 远程查询公司卡片列表(降级返回空列表) */ private List fetchCards(Integer companyId) { try { R> response = chargingCardClient.listByCompany(companyId); return (response != null && R.isSuccess(response) && response.getData() != null) ? response.getData() : new ArrayList<>(); } catch (Exception e) { return new ArrayList<>(); } } /** * 远程查询公司白名单列表(降级返回空列表) */ private List fetchWhitelists(Integer companyId) { try { R> response = appUserWhitelistClient.listByCompany(companyId); return (response != null && R.isSuccess(response) && response.getData() != null) ? response.getData() : new ArrayList<>(); } catch (Exception e) { return new ArrayList<>(); } } private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } }