2
0

feat: 流量和余额不足提醒

This commit is contained in:
caiyuchao
2025-06-16 18:00:33 +08:00
parent a2cb97ce4c
commit b70ac9d5cc
16 changed files with 239 additions and 48 deletions

View File

@@ -8,4 +8,10 @@ ADD COLUMN `invoice_file` varchar(255) NULL COMMENT '发票文件' AFTER `invoic
ALTER TABLE `wfc_user_db`.`u_bill`
ADD COLUMN `invoice_time` datetime NULL COMMENT '发票时间' AFTER `invoice_file`
ALTER TABLE `wfc_user_db`.`u_account`
ADD COLUMN `package_reminder` tinyint(4) NULL COMMENT '套餐提醒' AFTER `up_limit_enable`,
ADD COLUMN `balance_reminder` tinyint(4) NULL COMMENT '余额提醒' AFTER `package_reminder`
INSERT IGNORE INTO `wfc_system_db`.`sys_job` (`job_id`, `job_name`, `job_group`, `invoke_target`, `cron_expression`, `misfire_policy`, `concurrent`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3, 'Reminder Task', 'DEFAULT', 'reminderTask.reminderJob', '0/60 * * * * ?', '3', '1', '0', 'admin', '2025-06-16 11:26:10', '', NULL, '');
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -221,6 +221,7 @@ CREATE TABLE `sys_job` (
-- ----------------------------
INSERT INTO `sys_job` VALUES (1, 'Omada Sync Task', 'DEFAULT', 'omadaTask.syncJob', '0/30 * * * * ?', '3', '1', '0', 'admin', '2024-05-08 21:50:55', '', NULL, '');
INSERT INTO `sys_job` VALUES (2, 'Omada Initialization Task ', 'DEFAULT', 'omadaTask.initJob', '0 0 0/1 * * ? ', '3', '1', '0', 'admin', '2024-05-08 21:50:55', '', NULL, '');
INSERT INTO `sys_job` VALUES (3, 'Reminder Task', 'DEFAULT', 'reminderTask.reminderJob', '0/60 * * * * ?', '3', '1', '0', 'admin', '2025-06-16 11:26:10', '', NULL, '');
-- ----------------------------
-- Table structure for sys_job_log

View File

@@ -91,4 +91,7 @@ public interface RemoteUUserService
@GetMapping(value = "/order/{id}")
public R<UOrderVo> getOrderById(@PathVariable("id") Long id);
@PostMapping("/account/reminder")
public R<Boolean> sendReminderEMail();
}

View File

@@ -82,6 +82,11 @@ public class RemoteUUserFallbackFactory implements FallbackFactory<RemoteUUserSe
public R<UOrderVo> getOrderById(Long id) {
return R.fail("get order error:" + throwable.getMessage());
}
@Override
public R<Boolean> sendReminderEMail() {
return R.fail("send reminder email error:" + throwable.getMessage());
}
};
}
}

View File

@@ -0,0 +1,25 @@
package org.wfc.job.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.wfc.common.core.domain.R;
import org.wfc.user.api.RemoteUUserService;
/**
* @description: 警报任务
* @author: cyc
* @since: 2025-06-16
*/
@Slf4j
@Component("reminderTask")
public class ReminderTask {
@Autowired
private RemoteUUserService remoteUUserService;
public R<Boolean> reminderJob() {
return remoteUUserService.sendReminderEMail();
}
}

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wfc.common.core.domain.R;
import org.wfc.common.core.web.controller.BaseController;
import org.wfc.common.core.web.domain.AjaxResult;
import org.wfc.common.core.web.page.TableDataInfo;
@@ -71,4 +72,9 @@ public class UAccountController extends BaseController {
return toAjax(uAccountService.removeByIds(CollUtil.newArrayList(ids)));
}
@PostMapping("/reminder")
public R<Boolean> sendReminderEMail() {
boolean result = uAccountService.sendReminderEMail();
return R.ok(result);
}
}

View File

@@ -1,34 +1,26 @@
package org.wfc.user.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.itextpdf.html2pdf.HtmlConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.wfc.common.core.constant.Constants;
import org.wfc.common.core.constant.GlobalConstants;
import org.wfc.common.core.domain.R;
import org.wfc.common.core.utils.MessageUtils;
import org.wfc.common.core.utils.file.MultipartFileUtil;
import org.wfc.common.core.web.controller.BaseController;
import org.wfc.common.mail.config.properties.MailProperties;
import org.wfc.common.mail.utils.MailUtils;
import org.wfc.common.redis.service.RedisService;
import org.wfc.system.api.RemoteFileService;
import org.wfc.system.api.domain.SysFile;
import javax.validation.constraints.NotBlank;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
/**
@@ -46,7 +38,6 @@ public class UEmailController extends BaseController {
private final MailProperties mailProperties;
private final RedisService redisService;
private final TemplateEngine templateEngine;
private final RemoteFileService remoteFileService;
/**
* 邮箱验证码
@@ -65,46 +56,12 @@ public class UEmailController extends BaseController {
Context context = new Context();
context.setVariable("verificationCode", code);
context.setVariable("expirationTime", Constants.MAIL_CAPTCHA_EXPIRATION);
context.setVariable("username", "chason");
context.setVariable("email", "707821112@qq.com");
context.setVariable("invoiceNumber", "INV-2025-05-001");
context.setVariable("invoiceDate", "2025-05-28");
context.setVariable("hasDetails", "false");
context.setVariable("itemName", "Network Traffic Fee");
context.setVariable("qty", "1");
context.setVariable("unitPrice", "$50");
context.setVariable("total", "$50");
context.setVariable("currency", "USD");
context.setVariable("traffic", "200GB");
context.setVariable("speedCap", "Upload 30 Mbps / Download 100 Mbps");
context.setVariable("clientsNumber", "5");
context.setVariable("billingCycle", "Monthly");
String htmlStr = templateEngine.process("invoice", context);
// String basePath = System.getProperty("user.dir") + File.separator + "invoice.pdf";
String basePath = "D:\\documents\\projects\\wanfi\\invoice";
FileUtil.mkdir(basePath);
basePath = basePath + File.separator + "invoice.pdf";
File uploadFile = new File(basePath);
// HtmlConverter.convertToPdf(htmlStr, outputStream);
HtmlConverter.convertToPdf(htmlStr, Files.newOutputStream(Paths.get(basePath)));
// HtmlConverter.convertToPdf(htmlStr, Files.newOutputStream(Paths.get("D:\\projects\\pro\\be.wfc\\invoice.pdf")));
MultipartFile multipartFile = MultipartFileUtil.getMultipartFile(uploadFile);
R<SysFile> fileResult = remoteFileService.upload(multipartFile);
String url = fileResult.getData().getUrl();
log.info("qr code file: {}", url);
String htmlStr = templateEngine.process("mail", context);
String subject = mailProperties.getSubject();
if (StrUtil.isBlank(subject)) {
subject = "Your WANFI Verification Code";
}
// MailUtils.sendHtml(email, subject, htmlStr, uploadFile);
// FileUtil.del(uploadFile);
MailUtils.sendHtml(email, subject, htmlStr);
} catch (Exception e) {
log.error("email verification code send failed => {}", e.getMessage());
return R.fail(e.getMessage());

View File

@@ -105,4 +105,11 @@ public class UAccount extends BaseData {
@Schema(description = "上行限速启用")
private Boolean upLimitEnable;
@Schema(description = "套餐提醒")
private Integer packageReminder;
@Schema(description = "余额提醒")
private Integer balanceReminder;
}

View File

@@ -0,0 +1,21 @@
package org.wfc.user.domain.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @description: 警告状态
* @author: cyc
* @since: 2025-06-13
*/
@Getter
@AllArgsConstructor
public enum ReminderStatusEnum {
NO(0, "no"),
YES(1, "yes"),
;
private final Integer code;
private final String desc;
}

View File

@@ -1,4 +1,4 @@
package org.wfc.user.domain;
package org.wfc.user.domain.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "invoice")
public class InvoiceBean {
public class InvoiceProperties {
private String path;
}

View File

@@ -0,0 +1,30 @@
package org.wfc.user.domain.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* @description: 告警配置
* @author: cyc
* @since: 2025-06-13
*/
@Data
@Component
@ConfigurationProperties(prefix = "reminder")
public class ReminderProperties {
private Boolean trafficEnable;
private Integer trafficThreshold;
private Boolean balanceEnable;
private BigDecimal balanceThreshold;
private String trafficTitle;
private String balanceTitle;
}

View File

@@ -34,4 +34,5 @@ public interface IUAccountService extends IService<UAccount> {
void generateBill(Long userId, Long cdrHistoryId);
boolean sendReminderEMail();
}

View File

@@ -2,6 +2,7 @@ package org.wfc.user.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
@@ -11,9 +12,14 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.wfc.common.core.constant.CacheConstants;
import org.wfc.common.core.constant.WifiConstants;
import org.wfc.common.core.domain.LoginUser;
import org.wfc.common.core.utils.ResponseUtils;
import org.wfc.common.mail.utils.MailUtils;
import org.wfc.common.redis.service.RedisService;
import org.wfc.common.security.utils.SecurityUtils;
import org.wfc.omada.api.client.OmadaClientApi;
import org.wfc.omada.api.client.model.ClientInfo;
@@ -22,6 +28,7 @@ import org.wfc.omada.api.organization.OmadaSiteApi;
import org.wfc.omada.api.organization.model.OperationResponseGridVoSiteSummaryInfo;
import org.wfc.omada.api.organization.model.SiteSummaryInfo;
import org.wfc.user.api.IWifiApi;
import org.wfc.user.api.domain.UUser;
import org.wfc.user.api.domain.bo.UClientBo;
import org.wfc.user.api.omada.domain.convert.OmadaConvert;
import org.wfc.user.api.omada.domain.dto.ClientRateLimitSettingDto;
@@ -29,8 +36,10 @@ import org.wfc.user.domain.UAccount;
import org.wfc.user.domain.UBill;
import org.wfc.user.domain.UBillRule;
import org.wfc.user.domain.UClient;
import org.wfc.user.domain.constant.ReminderStatusEnum;
import org.wfc.user.domain.constant.OrderStatusEnum;
import org.wfc.user.domain.constant.OrderTypeEnum;
import org.wfc.user.domain.properties.ReminderProperties;
import org.wfc.user.domain.vo.UAccountDashboardVo;
import org.wfc.user.domain.vo.UCdrUserVo;
import org.wfc.user.domain.vo.UClientCurrentVo;
@@ -39,15 +48,20 @@ import org.wfc.user.mapper.UBillMapper;
import org.wfc.user.mapper.UBillRuleMapper;
import org.wfc.user.mapper.UCdrMapper;
import org.wfc.user.mapper.UClientMapper;
import org.wfc.user.mapper.UUserMapper;
import org.wfc.user.service.IUAccountService;
import org.wfc.user.service.IUClientService;
import org.wfc.user.util.AccountUtil;
import org.wfc.user.util.BillRuleUtil;
import org.wfc.user.util.TrafficConverter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -88,6 +102,21 @@ public class UAccountServiceImpl extends ServiceImpl<UAccountMapper, UAccount> i
@Lazy
private IUClientService uClientService;
@Autowired
private ReminderProperties reminderProperties;
@Autowired
private TemplateEngine templateEngine;
@Autowired
private UUserMapper userMapper;
@Autowired
private RedisService redisService;
private static final String DEFAULT_SYS_PAY_CURRENCY_SYMBOL_VALUE = "$";
private static final String DEFAULT_SYS_PAY_CURRENCY_VALUE = "USD";
@Transactional(rollbackFor = Exception.class)
public void statAndCancelAuthUser() {
// 定时任务查询所有未过期或刚过期(失效时间+定时任务间隔时间)的账户套餐,套餐过期/流量用完/时长用完则取消授权
@@ -288,4 +317,93 @@ public class UAccountServiceImpl extends ServiceImpl<UAccountMapper, UAccount> i
this.updateById(account);
}
public boolean sendReminderEMail() {
Date current = new Date();
List<UAccount> accounts = this.list(Wrappers.<UAccount>lambdaQuery()
.and(wrapper -> wrapper.isNotNull(UAccount::getBalance).gt(UAccount::getBalance, 0).or()
.le(UAccount::getStartTime, current).gt(UAccount::getEndTime, current))
.isNotNull(UAccount::getUserId));
List<UAccount> packageAccounts = accounts.stream().filter(account -> AccountUtil.isPackageAlertValid(account, current)
&& !Objects.equals(account.getPackageReminder(), ReminderStatusEnum.YES.getCode()))
.collect(Collectors.toList());
List<UAccount> balanceAccounts = accounts.stream().filter(account -> !Objects.equals(account.getBalanceReminder(), ReminderStatusEnum.YES.getCode()))
.collect(Collectors.toList());
if (reminderProperties.getTrafficEnable()) {
for (UAccount packageAccount : packageAccounts) {
if (packageAccount.getTrafficEnable()) {
try {
if ((packageAccount.getTraffic() - packageAccount.getTrafficUsed()) / packageAccount.getTraffic() <= reminderProperties.getTrafficThreshold() / 100) {
Context context = new Context();
context.setVariable("threshold", reminderProperties.getTrafficThreshold());
context.setVariable("totalTraffic", TrafficConverter.formatBytes(packageAccount.getTraffic()));
context.setVariable("usedTraffic", TrafficConverter.formatBytes(packageAccount.getTrafficUsed()));
String htmlStr = templateEngine.process("trafficReminder", context);
String subject = reminderProperties.getTrafficTitle();
if (StrUtil.isBlank(subject)) {
subject = "Package Reminder";
}
UUser user = userMapper.selectUserById(packageAccount.getUserId());
MailUtils.sendHtml(user.getEmail(), subject, htmlStr);
packageAccount.setPackageReminder(ReminderStatusEnum.YES.getCode());
}
} catch (Exception e) {
log.error("email traffic reminder send failed => {}", e.getMessage());
}
}
}
}
this.updateBatchById(packageAccounts);
if (reminderProperties.getBalanceEnable()) {
for (UAccount balanceAccount : balanceAccounts) {
try {
if (balanceAccount.getBalance().subtract(balanceAccount.getBalanceUsed()).compareTo(reminderProperties.getBalanceThreshold()) <= 0) {
Map<String, Object> cacheMap = redisService.getCacheMap(CacheConstants.SYS_PAY_CONFIG_KEY);
String currencySymbol = DEFAULT_SYS_PAY_CURRENCY_SYMBOL_VALUE;
String currency = DEFAULT_SYS_PAY_CURRENCY_VALUE;
if (CollUtil.isNotEmpty(cacheMap)) {
Object currencyObj = cacheMap.get(CacheConstants.SYS_PAY_CURRENCY_KEY);
if (currencyObj != null) {
currency = currencyObj.toString();
}
Object currencySymbolObj = cacheMap.get(CacheConstants.SYS_PAY_CURRENCY_SYMBOL_KEY);
if (currencySymbolObj != null) {
currencySymbol = currencySymbolObj.toString();
}
}
Context context = new Context();
context.setVariable("threshold", reminderProperties.getBalanceThreshold());
BigDecimal balanceUsed = Optional.ofNullable(balanceAccount.getBalanceUsed()).orElse(BigDecimal.ZERO);
BigDecimal balance = Optional.ofNullable(balanceAccount.getBalance()).orElse(BigDecimal.ZERO);
context.setVariable("currency", currency);
context.setVariable("currencySymbol", currencySymbol);
context.setVariable("balance", balance.compareTo(balanceUsed) >= 0 ? balance.subtract(balanceUsed).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
String htmlStr = templateEngine.process("balanceReminder", context);
String subject = reminderProperties.getBalanceTitle();
if (StrUtil.isBlank(subject)) {
subject = "Balance Reminder";
}
UUser user = userMapper.selectUserById(balanceAccount.getUserId());
MailUtils.sendHtml(user.getEmail(), subject, htmlStr);
balanceAccount.setBalanceReminder(ReminderStatusEnum.YES.getCode());
}
} catch (Exception e) {
log.error("email balance reminder send failed => {}", e.getMessage());
}
}
}
this.updateBatchById(balanceAccounts);
return true;
}
}

View File

@@ -27,6 +27,7 @@ import org.wfc.user.domain.URateLimit;
import org.wfc.user.domain.constant.OrderStatusEnum;
import org.wfc.user.domain.constant.OrderTypeEnum;
import org.wfc.user.domain.constant.PeriodTypeEnum;
import org.wfc.user.domain.constant.ReminderStatusEnum;
import org.wfc.user.domain.vo.UInvoiceGenVo;
import org.wfc.user.mapper.UAccountPackageMapper;
import org.wfc.user.mapper.UBillMapper;
@@ -141,6 +142,7 @@ public class UOrderServiceImpl extends ServiceImpl<UOrderMapper, UOrder> impleme
&& ObjectUtil.isNull(account.getBalance()) && account.getBalance().subtract(account.getBalanceUsed()).compareTo(BigDecimal.ZERO) < 0) {
account.setBalance(account.getBalance().add(account.getBalanceUsed().subtract(account.getBalance())));
}
account.setBalanceReminder(ReminderStatusEnum.NO.getCode());
}
account.setId(accountId);
@@ -225,6 +227,7 @@ public class UOrderServiceImpl extends ServiceImpl<UOrderMapper, UOrder> impleme
DateTime endTime = getEndTime(uPackage, current);
account.setStartTime(current);
account.setEndTime(endTime);
account.setPackageReminder(ReminderStatusEnum.NO.getCode());
}
}

View File

@@ -18,6 +18,7 @@ import org.wfc.user.domain.UAccount;
import org.wfc.user.domain.UPost;
import org.wfc.user.domain.UUserPost;
import org.wfc.user.domain.UUserRole;
import org.wfc.user.domain.constant.ReminderStatusEnum;
import org.wfc.user.mapper.UAccountMapper;
import org.wfc.user.mapper.UPostMapper;
import org.wfc.user.mapper.URoleMapper;
@@ -292,6 +293,7 @@ public class UUserServiceImpl implements IUUserService
account.setUserId(user.getUserId());
account.setBalance(BigDecimal.ZERO);
account.setBalanceUsed(BigDecimal.ZERO);
account.setBalanceReminder(ReminderStatusEnum.YES.getCode());
accountMapper.insert(account);
} catch (Exception e) {
log.error("register add account error {}", e.getMessage());

View File

@@ -69,4 +69,10 @@ public class AccountUtil {
&& (!account.getDurationEnable() || account.getDurationUsed() <= account.getDuration()))
&& (ObjectUtil.isNull(account.getBalanceUsed()) || account.getBalanceUsed().compareTo(BigDecimal.ZERO) == 0);
}
public static boolean isPackageAlertValid(UAccount account, Date current) {
// 套餐是否有效
return (DateUtil.compare(account.getStartTime(), current) <= 0 && DateUtil.compare(account.getEndTime(), current) > 0)
&& (!account.getDurationEnable() || account.getDurationUsed() < account.getDuration());
}
}