Kaynağa Gözat

feat:店铺营业额预警发送邮箱通知(xxljob定时检测)

huangjinliang 3 gün önce
ebeveyn
işleme
d2f2cd10bc

+ 23 - 13
fuintBackend/configure/dev/application.properties

@@ -1,7 +1,9 @@
 # \u6570\u636E\u5E93\u914D\u7F6E
 spring.datasource.url=jdbc:mysql://101.126.146.250:3306/fuint-food?useUnicode=true&characterEncoding=UTF8&useSSL=false
+#spring.datasource.url=jdbc:mysql://localhost:3306/fuint-food?useUnicode=true&characterEncoding=UTF8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=XYY1q2w!
+#spring.datasource.password=123456
 
 # Redis\u914D\u7F6E
 spring.session.store-type=redis
@@ -108,23 +110,31 @@ weixin.subMessage.couponConfirm=[{'key':'name', 'name':'\u5361\u5238\u540D\u79F0
 weixin.subMessage.pointChange=[{'key':'amount', 'name':'\u53D8\u52A8\u6570\u91CF'},{'key':'time', 'name':'\u53D8\u52A8\u65F6\u95F4'},{'key':'remark', 'name':'\u5907\u6CE8\u4FE1\u606F'}]
 
 
-# \u90AE\u7BB1\u914D\u7F6E
 spring.mail.host=smtp.163.com
 spring.mail.port=465
 spring.mail.username=17633944959@163.com
-spring.mail.password=CHb8mqa9Hxst7V5G
+spring.mail.password=XKSzhmPt7X38ZkaB
 spring.mail.protocol=smtp
 spring.mail.properties.mail.smtp.ssl.enable=true
 spring.mail.properties.mail.smtp.auth=true
 
-# \u9884\u8B66\u914D\u7F6E\uFF08\u652F\u6301\u914D\u7F6E\u4E2D\u5FC3\uFF09
-alert.from-email=17633944959@163.com
-alert.to-email=huangjinliang35@gmail.com
-# \u6284\u9001
-#alert.email.cc=2822711420@qq.com
-# \u5BC6\u9001
-#alert.email.bcc=2822711420@qq.com
-# \u539F\u914D\u7F6E\uFF08\u6BCF\u59298\u70B9\uFF09
-#alert.cron-expression=0 0 8 * * ?
-# \u65B0\u914D\u7F6E\uFF08\u6BCF10\u79D2\u6267\u884C\uFF09
-alert.cron-expression=*/10 * * * * *
+alert.cron-expression=*/10 * * * * *
+
+#alert.mail.to=984267985@qq.com
+
+# \u8C03\u5EA6\u4E2D\u5FC3\u5730\u5740\u914D\u7F6E
+xxl.job.admin.addresses=http://192.168.2.3:8040/xxl-job-admin
+### xxl-job, access token
+xxl.job.accessToken=xxl-job
+
+### xxl-job executor appname
+xxl.job.executor.appname=xxl-job-executor-sample
+### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
+xxl.job.executor.address=
+### xxl-job executor server-info
+xxl.job.executor.ip=
+xxl.job.executor.port=9991
+### xxl-job executor log-path
+xxl.job.executor.logpath=
+### xxl-job executor log-retention-days
+xxl.job.executor.logretentiondays=30

+ 7 - 0
fuintBackend/fuint-application/pom.xml

@@ -176,6 +176,13 @@
             <artifactId>spring-boot-starter-mail</artifactId>
         </dependency>
 
+        <!-- xxl job -->
+        <dependency>
+            <groupId>com.xuxueli</groupId>
+            <artifactId>xxl-job-core</artifactId>
+            <version>2.4.0</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 56 - 0
fuintBackend/fuint-application/src/main/java/com/fuint/common/config/XxlJobConfig.java

@@ -0,0 +1,56 @@
+package com.fuint.common.config;
+
+import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.mybatis.logging.Logger;
+import org.mybatis.logging.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@Slf4j
+public class XxlJobConfig {
+    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
+
+    @Value("${xxl.job.admin.addresses}")
+    private String adminAddresses;
+
+    @Value("${xxl.job.accessToken}")
+    private String accessToken;
+
+    @Value("${xxl.job.executor.appname}")
+    private String appname;
+
+    @Value("${xxl.job.executor.address}")
+    private String address;
+
+    @Value("${xxl.job.executor.ip}")
+    private String ip;
+
+    @Value("${xxl.job.executor.port}")
+    private int port;
+
+    @Value("${xxl.job.executor.logpath}")
+    private String logPath;
+
+    @Value("${xxl.job.executor.logretentiondays}")
+    private int logRetentionDays;
+
+
+    @Bean
+    public XxlJobSpringExecutor xxlJobExecutor() {
+        log.info(">>>>>>>>>>> xxl-job config init.");
+        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
+        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
+        xxlJobSpringExecutor.setAppname(appname);
+        xxlJobSpringExecutor.setAddress(address);
+        xxlJobSpringExecutor.setIp(ip);
+        xxlJobSpringExecutor.setPort(port);
+        xxlJobSpringExecutor.setAccessToken(accessToken);
+        xxlJobSpringExecutor.setLogPath(logPath);
+        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
+
+        return xxlJobSpringExecutor;
+    }
+}

+ 21 - 0
fuintBackend/fuint-application/src/main/java/com/fuint/common/dto/ext/ShopAlertInfo.java

@@ -0,0 +1,21 @@
+package com.fuint.common.dto.ext;
+
+
+import com.fuint.common.entity.Order;
+import com.fuint.common.entity.Shop;
+import com.fuint.repository.model.MtOrder;
+import com.fuint.repository.model.MtStore;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+// 预警信息封装类
+@Data
+@AllArgsConstructor
+public class ShopAlertInfo {
+    private MtStore shop;          // 店铺信息
+    private BigDecimal totalSales; // 当日总销售额
+    private List<MtOrder> orders; // 新增订单明细字段
+}

+ 23 - 0
fuintBackend/fuint-application/src/main/java/com/fuint/common/job/ScheduleTask.java

@@ -0,0 +1,23 @@
+package com.fuint.common.job;
+
+import com.fuint.common.service.AlertService;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@Configuration
+public class ScheduleTask {
+
+    private AlertService alertService;
+
+    public ScheduleTask(AlertService alertService) {
+        this.alertService = alertService;
+    }
+
+    // 每天23:30执行
+//    @Scheduled( cron = "*/10 * * * * *")
+    @XxlJob("dailySalesCheck")
+    public void dailySalesCheck() {
+        alertService.checkDailySalesAndAlert();
+    }
+}

+ 8 - 0
fuintBackend/fuint-application/src/main/java/com/fuint/common/service/AlertService.java

@@ -0,0 +1,8 @@
+package com.fuint.common.service;
+
+public interface AlertService {
+    /**
+     * 检查当日营业额并发送预警邮件
+     */
+    void checkDailySalesAndAlert();
+}

+ 111 - 8
fuintBackend/fuint-application/src/main/java/com/fuint/common/service/EmailService.java

@@ -1,10 +1,113 @@
 package com.fuint.common.service;
 
-import com.fuint.common.dto.ext.EmailDto;
-
-/**
- * 邮件发送服务
- */
-public interface EmailService {
-    void sendAlertEmail(EmailDto emailDTO);
-}
+
+
+import com.fuint.common.dto.ext.ShopAlertInfo;
+import com.fuint.common.entity.Order;
+import com.fuint.common.entity.Shop;
+import com.fuint.repository.model.MtOrder;
+import com.fuint.repository.model.MtStore;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class EmailService {
+    private final JavaMailSender mailSender;
+    private final String from = "17633944959@163.com";
+
+
+    /**
+     * 邮件聚合发送方法
+     * @param to
+     * @param alerts
+     */
+    public void sendAggregatedAlertEmail(String to, List<ShopAlertInfo> alerts) {
+        try {
+            MimeMessage message = mailSender.createMimeMessage();
+            MimeMessageHelper helper = new MimeMessageHelper(message, true);
+
+            helper.setFrom(from);
+            helper.setTo(to);
+            helper.setSubject("【多店铺营业额预警】");
+
+            // 构建HTML表格内容
+            String htmlContent = buildAggregatedHtml(alerts);
+            helper.setText(htmlContent, true);
+
+            mailSender.send(message);
+        } catch (MessagingException e) {
+            log.error("聚合邮件发送失败: {}", to, e);
+        }
+    }
+
+    private String buildAggregatedHtml(List<ShopAlertInfo> alerts) {
+//        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");  LocalDateTime类型
+        // 创建时间格式化器
+        SimpleDateFormat timeFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        DecimalFormat df = new DecimalFormat("¥#,##0.00"); // 金额格式化
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("<h3 style='color: #dc3545;'>营业额预警明细</h3>");
+
+        alerts.forEach(info -> {
+            MtStore shop = info.getShop();
+            // 计算差额(阈值 - 实际销售额)
+            BigDecimal deficit = shop.getThreshold().subtract(info.getTotalSales());
+            List<MtOrder> orders = info.getOrders();
+            // 添加汇总统计
+            sb.append("<p style='color: #6c757d; margin-top: 10px;'>")
+                    .append("总订单数:<strong>").append(orders.size()).append("</strong> 笔")
+                    .append("</p>");
+
+            sb.append("<div style='margin-bottom: 30px; border: 1px solid #ddd; padding: 15px;'>")
+                    .append("<h4 style='color: #0d6efd;'>").append(shop.getName()).append("</h4>")
+                    .append("<p>目标阈值:<strong>").append(df.format(shop.getThreshold())).append("</strong></p>")
+                    .append("<p>实际销售额:<strong style='color: red;'>").append(df.format(info.getTotalSales())).append("</strong></p>")
+                    // 新增差额显示
+                    .append("<p>差额:<strong style='color: red; border-bottom: 2px solid #ffcccc;'>")
+                    .append(df.format(deficit))
+                    .append("</strong></p>");
+
+            // 订单明细表格
+            if (!info.getOrders().isEmpty()) {
+                sb.append("<table style='width: 100%; border-collapse: collapse; margin-top: 10px;'>")
+                        .append("<tr style='background-color: #f8f9fa;'>")
+                        .append("<th style='padding: 8px; border: 1px solid #dee2e6;'>订单号</th>")
+                        .append("<th style='padding: 8px; border: 1px solid #dee2e6;'>金额</th>")
+                        .append("<th style='padding: 8px; border: 1px solid #dee2e6;'>时间</th>")
+                        .append("</tr>");
+
+                info.getOrders().forEach(order -> {
+                    sb.append("<tr>")
+                            .append("<td style='padding: 8px; border: 1px solid #dee2e6;'>").append(order.getId()).append("</td>")
+                            .append("<td style='padding: 8px; border: 1px solid #dee2e6;'>").append(df.format(order.getAmount())).append("</td>")
+                            .append("<td style='padding: 8px; border: 1px solid #dee2e6;'>")
+                            .append(timeFormatter.format(order.getCreateTime()))
+                            .append("</td></tr>");
+                });
+
+                sb.append("</table>");
+            } else {
+                sb.append("<p style='color: #6c757d; margin-top: 10px;'>无当日订单记录</p>");
+            }
+            sb.append("</div>");
+
+        });
+
+
+        return sb.toString();
+    }
+}

+ 82 - 0
fuintBackend/fuint-application/src/main/java/com/fuint/common/service/impl/AlertServiceImpl.java

@@ -0,0 +1,82 @@
+package com.fuint.common.service.impl;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fuint.common.dto.ext.ShopAlertInfo;
+import com.fuint.common.service.AlertService;
+import com.fuint.common.service.EmailService;
+import com.fuint.repository.mapper.MtOrderMapper;
+import com.fuint.repository.mapper.MtStoreMapper;
+import com.fuint.repository.model.MtOrder;
+import com.fuint.repository.model.MtStore;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+@AllArgsConstructor
+public class AlertServiceImpl implements AlertService {
+    private EmailService emailService;
+
+    private MtStoreMapper mtStoreMapper;
+
+    private MtOrderMapper mtOrderMapper;
+
+
+    /**
+     * 检查当日营业额并发送预警邮件
+     */
+    @Override
+    @Transactional(readOnly = true)
+    public void checkDailySalesAndAlert() {
+        // 0. 获取时间范围(统一时间变量)
+        LocalDateTime start = LocalDate.now().atStartOfDay();
+        LocalDateTime end = LocalDateTime.now();
+
+        // 1. 获取所有店铺
+        List<MtStore> shops = mtStoreMapper.selectList(new QueryWrapper<MtStore>().isNotNull("mail"));
+        List<Long> shopIds = shops.stream().map(MtStore::getId).collect(Collectors.toList());
+
+        // 2. 批量查询所有相关订单(优化性能)
+        Map<Long, List<MtOrder>> ordersByShop = shopIds.isEmpty() ?
+                Collections.emptyMap() :
+                mtOrderMapper.selectList(
+                        new LambdaQueryWrapper<MtOrder>()
+                                .in(MtOrder::getStoreId, shopIds)
+                                .between(MtOrder::getCreateTime, start, end)
+                ).stream().collect(Collectors.groupingBy(MtOrder::getStoreId));
+
+        // 3. 构建预警数据(包含订单明细)
+        Map<String, List<ShopAlertInfo>> alertMap = shops.stream()
+                .map(shop -> {
+                    // 计算总销售额
+                    BigDecimal sales = Optional.ofNullable(ordersByShop.get(shop.getId()))
+                            .orElse(Collections.emptyList())
+                            .stream()
+                            .map(MtOrder::getAmount)
+                            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+                    // 注入订单明细
+                    return new ShopAlertInfo(
+                            shop,
+                            sales,
+                            ordersByShop.getOrDefault(shop.getId(), Collections.emptyList())
+                    );
+                })
+                .filter(info -> info.getTotalSales().compareTo(info.getShop().getThreshold()) < 0)
+                .collect(Collectors.groupingBy(info -> info.getShop().getMail()));
+
+        // 4. 发送邮件(逻辑保持不变)
+        alertMap.forEach((mail, list) -> emailService.sendAggregatedAlertEmail(mail, list));
+    }
+}

+ 0 - 55
fuintBackend/fuint-application/src/main/java/com/fuint/common/service/impl/EmailServiceImpl.java

@@ -1,55 +0,0 @@
-package com.fuint.common.service.impl;
-
-import com.fuint.common.config.AlertConfig;
-import com.fuint.common.dto.ext.EmailDto;
-import com.fuint.common.service.EmailService;
-import com.fuint.framework.exception.EmailException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.mail.javamail.MimeMessageHelper;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-import javax.mail.MessagingException;
-import javax.mail.internet.MimeMessage;
-
-/**
- * @author Survive
- * @date 2025/3/10
- * @description TODO 邮件发送服务
- */
-@Service
-@RequiredArgsConstructor
-public class EmailServiceImpl implements EmailService {
-    private final JavaMailSender mailSender;
-    private final AlertConfig alertConfig;
-    @Override
-    public void sendAlertEmail(EmailDto emailDTO) {
-        try {
-            MimeMessage message = mailSender.createMimeMessage();
-            MimeMessageHelper helper = new MimeMessageHelper(message, true);
-
-            // 设置发件人
-            helper.setFrom(alertConfig.getFromEmail());
-            // 解析多个收件人
-            String[] toEmails = emailDTO.getTo().split(",\\s*");
-            helper.setTo(toEmails);
-//            helper.setTo(emailDTO.getTo());
-            // 设置主题和内容
-            helper.setSubject(emailDTO.getSubject());
-            helper.setText(emailDTO.getContent());
-
-            // 抄送
-            if (StringUtils.hasText(alertConfig.getCcEmails())) {
-                helper.setCc(alertConfig.getCcEmails().split(",\\s*"));
-            }
-            // 密送
-            if (StringUtils.hasText(alertConfig.getBccEmails())) {
-                helper.setBcc(alertConfig.getBccEmails().split(",\\s*"));
-            }
-            mailSender.send(message);
-        } catch (MessagingException e) {
-            throw new EmailException("邮件发送失败", e);
-        }
-    }
-}

+ 5 - 0
fuintBackend/fuint-repository/src/main/java/com/fuint/repository/model/MtStore.java

@@ -122,4 +122,9 @@ public class MtStore implements Serializable {
     @ApiModelProperty("最后操作人")
     private String operator;
 
+    @ApiModelProperty("店铺负责人邮箱")
+    private String mail;
+    @ApiModelProperty("营业额阈值")
+    private BigDecimal threshold;
+
 }