StatisticsTask.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. package com.haha.admin.task;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.haha.entity.*;
  4. import com.haha.mapper.*;
  5. import com.haha.common.constant.OrderConstants;
  6. import com.haha.common.utils.TraceIdUtils;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.scheduling.annotation.Scheduled;
  10. import org.springframework.stereotype.Component;
  11. import java.math.BigDecimal;
  12. import java.math.RoundingMode;
  13. import java.time.LocalDate;
  14. import java.time.LocalDateTime;
  15. import java.time.LocalTime;
  16. import java.util.*;
  17. import java.util.stream.Collectors;
  18. @Slf4j
  19. @Component
  20. public class StatisticsTask {
  21. @Autowired
  22. private OrderMapper orderMapper;
  23. @Autowired
  24. private OrderItemMapper orderItemMapper;
  25. @Autowired
  26. private DeviceMapper deviceMapper;
  27. @Autowired
  28. private ShopMapper shopMapper;
  29. @Autowired
  30. private UserMapper userMapper;
  31. @Autowired
  32. private StatCategoryDailyMapper statCategoryDailyMapper;
  33. @Autowired
  34. private StatProductDailyMapper statProductDailyMapper;
  35. @Autowired
  36. private StatDeviceDailyMapper statDeviceDailyMapper;
  37. @Autowired
  38. private StatShopDailyMapper statShopDailyMapper;
  39. @Autowired
  40. private StatUserRepurchaseMapper statUserRepurchaseMapper;
  41. @Scheduled(cron = "0 5 0 * * ?")
  42. public void aggregateDailyStatistics() {
  43. // 生成独立的 TraceId 用于日志追踪
  44. String traceId = TraceIdUtils.generateTraceId();
  45. TraceIdUtils.setTraceId(traceId);
  46. LocalDate yesterday = LocalDate.now().minusDays(1);
  47. log.info("开始执行每日统计汇总任务,统计日期:{}", yesterday);
  48. try {
  49. aggregateCategoryDaily(yesterday);
  50. aggregateProductDaily(yesterday);
  51. aggregateDeviceDaily(yesterday);
  52. aggregateShopDaily(yesterday);
  53. aggregateUserRepurchase(yesterday);
  54. log.info("每日统计汇总任务执行完成");
  55. } catch (Exception e) {
  56. log.error("每日统计汇总任务执行失败", e);
  57. } finally {
  58. // 清理 MDC 上下文
  59. TraceIdUtils.removeTraceId();
  60. }
  61. }
  62. private void aggregateCategoryDaily(LocalDate statDate) {
  63. LocalDateTime startDateTime = statDate.atStartOfDay();
  64. LocalDateTime endDateTime = statDate.atTime(LocalTime.MAX);
  65. List<OrderItem> items = orderItemMapper.selectList(
  66. new LambdaQueryWrapper<OrderItem>()
  67. .ge(OrderItem::getPayTime, startDateTime)
  68. .le(OrderItem::getPayTime, endDateTime)
  69. );
  70. Map<String, List<OrderItem>> categoryMap = items.stream()
  71. .filter(item -> item.getProductType() != null)
  72. .collect(Collectors.groupingBy(OrderItem::getProductType));
  73. for (Map.Entry<String, List<OrderItem>> entry : categoryMap.entrySet()) {
  74. String category = entry.getKey();
  75. List<OrderItem> categoryItems = entry.getValue();
  76. StatCategoryDaily stat = new StatCategoryDaily();
  77. stat.setStatDate(statDate);
  78. stat.setCategory(category);
  79. stat.setShopId(null);
  80. stat.setQuantity(categoryItems.stream().mapToInt(i -> i.getQuantity() != null ? i.getQuantity() : 0).sum());
  81. stat.setOrderCount((int) categoryItems.stream().map(OrderItem::getOrderId).distinct().count());
  82. BigDecimal salesAmount = categoryItems.stream()
  83. .map(i -> i.getTotalAmount() != null ? i.getTotalAmount() : BigDecimal.ZERO)
  84. .reduce(BigDecimal.ZERO, BigDecimal::add);
  85. stat.setSalesAmount(salesAmount);
  86. BigDecimal costAmount = categoryItems.stream()
  87. .map(i -> {
  88. if (i.getCostPrice() != null && i.getQuantity() != null) {
  89. return i.getCostPrice().multiply(BigDecimal.valueOf(i.getQuantity()));
  90. }
  91. return BigDecimal.ZERO;
  92. })
  93. .reduce(BigDecimal.ZERO, BigDecimal::add);
  94. stat.setCostAmount(costAmount);
  95. stat.setProfitAmount(salesAmount.subtract(costAmount));
  96. stat.setCreateTime(LocalDateTime.now());
  97. statCategoryDailyMapper.insert(stat);
  98. }
  99. log.info("品类统计汇总完成,共{}条记录", categoryMap.size());
  100. }
  101. private void aggregateProductDaily(LocalDate statDate) {
  102. LocalDateTime startDateTime = statDate.atStartOfDay();
  103. LocalDateTime endDateTime = statDate.atTime(LocalTime.MAX);
  104. List<OrderItem> items = orderItemMapper.selectList(
  105. new LambdaQueryWrapper<OrderItem>()
  106. .ge(OrderItem::getPayTime, startDateTime)
  107. .le(OrderItem::getPayTime, endDateTime)
  108. );
  109. Map<Long, List<OrderItem>> productMap = items.stream()
  110. .filter(item -> item.getProductId() != null)
  111. .collect(Collectors.groupingBy(OrderItem::getProductId));
  112. int count = 0;
  113. for (Map.Entry<Long, List<OrderItem>> entry : productMap.entrySet()) {
  114. List<OrderItem> productItems = entry.getValue();
  115. OrderItem first = productItems.get(0);
  116. Map<String, List<OrderItem>> deviceGroup = productItems.stream()
  117. .filter(i -> i.getDeviceId() != null)
  118. .collect(Collectors.groupingBy(OrderItem::getDeviceId));
  119. if (deviceGroup.isEmpty()) {
  120. StatProductDaily stat = createProductStat(statDate, first, productItems, null, null);
  121. statProductDailyMapper.insert(stat);
  122. count++;
  123. } else {
  124. for (Map.Entry<String, List<OrderItem>> deviceEntry : deviceGroup.entrySet()) {
  125. StatProductDaily stat = createProductStat(statDate, first, deviceEntry.getValue(),
  126. deviceEntry.getKey(), null);
  127. statProductDailyMapper.insert(stat);
  128. count++;
  129. }
  130. }
  131. }
  132. log.info("商品统计汇总完成,共{}条记录", count);
  133. }
  134. private StatProductDaily createProductStat(LocalDate statDate, OrderItem first,
  135. List<OrderItem> items, String deviceId, Long shopId) {
  136. StatProductDaily stat = new StatProductDaily();
  137. stat.setStatDate(statDate);
  138. stat.setProductId(first.getProductId());
  139. stat.setProductCode(first.getProductCode());
  140. stat.setProductName(first.getProductName());
  141. stat.setCategory(first.getProductType());
  142. stat.setShopId(shopId != null ? shopId : first.getShopId());
  143. stat.setDeviceId(deviceId);
  144. stat.setQuantity(items.stream().mapToInt(i -> i.getQuantity() != null ? i.getQuantity() : 0).sum());
  145. stat.setOrderCount((int) items.stream().map(OrderItem::getOrderId).distinct().count());
  146. BigDecimal salesAmount = items.stream()
  147. .map(i -> i.getTotalAmount() != null ? i.getTotalAmount() : BigDecimal.ZERO)
  148. .reduce(BigDecimal.ZERO, BigDecimal::add);
  149. stat.setSalesAmount(salesAmount);
  150. BigDecimal costAmount = items.stream()
  151. .map(i -> {
  152. if (i.getCostPrice() != null && i.getQuantity() != null) {
  153. return i.getCostPrice().multiply(BigDecimal.valueOf(i.getQuantity()));
  154. }
  155. return BigDecimal.ZERO;
  156. })
  157. .reduce(BigDecimal.ZERO, BigDecimal::add);
  158. stat.setCostAmount(costAmount);
  159. stat.setProfitAmount(salesAmount.subtract(costAmount));
  160. stat.setCreateTime(LocalDateTime.now());
  161. return stat;
  162. }
  163. private void aggregateDeviceDaily(LocalDate statDate) {
  164. LocalDateTime startDateTime = statDate.atStartOfDay();
  165. LocalDateTime endDateTime = statDate.atTime(LocalTime.MAX);
  166. List<Order> orders = orderMapper.selectList(
  167. new LambdaQueryWrapper<Order>()
  168. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  169. .ge(Order::getPayTime, startDateTime)
  170. .le(Order::getPayTime, endDateTime)
  171. );
  172. Map<String, List<Order>> deviceOrderMap = orders.stream()
  173. .filter(o -> o.getDeviceId() != null)
  174. .collect(Collectors.groupingBy(Order::getDeviceId));
  175. List<Device> devices = deviceMapper.selectList(null);
  176. Map<String, Device> deviceMap = devices.stream()
  177. .collect(Collectors.toMap(Device::getDeviceId, d -> d, (a, b) -> a));
  178. int count = 0;
  179. for (Device device : devices) {
  180. StatDeviceDaily stat = new StatDeviceDaily();
  181. stat.setStatDate(statDate);
  182. stat.setDeviceId(device.getDeviceId());
  183. stat.setDeviceName(device.getName());
  184. stat.setShopId(device.getShopId());
  185. if (device.getShopId() != null) {
  186. Shop shop = shopMapper.selectById(device.getShopId());
  187. if (shop != null) {
  188. stat.setShopName(shop.getName());
  189. }
  190. }
  191. List<Order> deviceOrders = deviceOrderMap.getOrDefault(device.getDeviceId(), Collections.emptyList());
  192. stat.setOrderCount(deviceOrders.size());
  193. stat.setUserCount((int) deviceOrders.stream().map(Order::getUserId).filter(Objects::nonNull).distinct().count());
  194. BigDecimal salesAmount = deviceOrders.stream()
  195. .map(o -> o.getTotalAmount() != null ? o.getTotalAmount() : BigDecimal.ZERO)
  196. .reduce(BigDecimal.ZERO, BigDecimal::add);
  197. stat.setSalesAmount(salesAmount);
  198. stat.setCostAmount(BigDecimal.ZERO);
  199. stat.setProfitAmount(salesAmount);
  200. stat.setCreateTime(LocalDateTime.now());
  201. statDeviceDailyMapper.insert(stat);
  202. count++;
  203. }
  204. log.info("设备统计汇总完成,共{}条记录", count);
  205. }
  206. private void aggregateShopDaily(LocalDate statDate) {
  207. LocalDateTime startDateTime = statDate.atStartOfDay();
  208. LocalDateTime endDateTime = statDate.atTime(LocalTime.MAX);
  209. List<Order> orders = orderMapper.selectList(
  210. new LambdaQueryWrapper<Order>()
  211. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  212. .ge(Order::getPayTime, startDateTime)
  213. .le(Order::getPayTime, endDateTime)
  214. );
  215. Map<Long, List<Order>> shopOrderMap = orders.stream()
  216. .filter(o -> o.getShopId() != null)
  217. .collect(Collectors.groupingBy(Order::getShopId));
  218. List<Shop> shops = shopMapper.selectList(null);
  219. List<Device> devices = deviceMapper.selectList(null);
  220. Map<Long, Long> shopDeviceCount = devices.stream()
  221. .filter(d -> d.getShopId() != null)
  222. .collect(Collectors.groupingBy(Device::getShopId, Collectors.counting()));
  223. Set<Long> allUserIds = orders.stream()
  224. .map(Order::getUserId)
  225. .filter(Objects::nonNull)
  226. .collect(Collectors.toSet());
  227. Map<Long, LocalDate> userFirstOrderDate = new HashMap<>();
  228. for (Long userId : allUserIds) {
  229. List<Order> userOrders = orderMapper.selectList(
  230. new LambdaQueryWrapper<Order>()
  231. .eq(Order::getUserId, userId)
  232. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  233. .orderByAsc(Order::getPayTime)
  234. .last("LIMIT 1")
  235. );
  236. if (!userOrders.isEmpty() && userOrders.get(0).getPayTime() != null) {
  237. userFirstOrderDate.put(userId, userOrders.get(0).getPayTime().toLocalDate());
  238. }
  239. }
  240. int count = 0;
  241. for (Shop shop : shops) {
  242. StatShopDaily stat = new StatShopDaily();
  243. stat.setStatDate(statDate);
  244. stat.setShopId(shop.getId());
  245. stat.setShopName(shop.getName());
  246. stat.setProvince(shop.getProvince());
  247. stat.setCity(shop.getCity());
  248. stat.setDistrict(shop.getDistrict());
  249. stat.setDeviceCount(shopDeviceCount.getOrDefault(shop.getId(), 0L).intValue());
  250. List<Order> shopOrders = shopOrderMap.getOrDefault(shop.getId(), Collections.emptyList());
  251. stat.setOrderCount(shopOrders.size());
  252. Set<Long> shopUserIds = shopOrders.stream()
  253. .map(Order::getUserId)
  254. .filter(Objects::nonNull)
  255. .collect(Collectors.toSet());
  256. stat.setUserCount(shopUserIds.size());
  257. int newUserCount = 0;
  258. for (Long userId : shopUserIds) {
  259. LocalDate firstDate = userFirstOrderDate.get(userId);
  260. if (firstDate != null && firstDate.equals(statDate)) {
  261. newUserCount++;
  262. }
  263. }
  264. stat.setNewUserCount(newUserCount);
  265. BigDecimal salesAmount = shopOrders.stream()
  266. .map(o -> o.getTotalAmount() != null ? o.getTotalAmount() : BigDecimal.ZERO)
  267. .reduce(BigDecimal.ZERO, BigDecimal::add);
  268. stat.setSalesAmount(salesAmount);
  269. stat.setCostAmount(BigDecimal.ZERO);
  270. stat.setProfitAmount(salesAmount);
  271. BigDecimal profitRate = salesAmount.compareTo(BigDecimal.ZERO) > 0
  272. ? BigDecimal.valueOf(100)
  273. : BigDecimal.ZERO;
  274. stat.setProfitRate(profitRate);
  275. stat.setCreateTime(LocalDateTime.now());
  276. statShopDailyMapper.insert(stat);
  277. count++;
  278. }
  279. log.info("门店统计汇总完成,共{}条记录", count);
  280. }
  281. private void aggregateUserRepurchase(LocalDate statDate) {
  282. LocalDateTime startDateTime = statDate.atStartOfDay();
  283. LocalDateTime endDateTime = statDate.atTime(LocalTime.MAX);
  284. List<Order> todayOrders = orderMapper.selectList(
  285. new LambdaQueryWrapper<Order>()
  286. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  287. .ge(Order::getPayTime, startDateTime)
  288. .le(Order::getPayTime, endDateTime)
  289. );
  290. Map<Long, List<Order>> userOrderMap = todayOrders.stream()
  291. .filter(o -> o.getUserId() != null)
  292. .collect(Collectors.groupingBy(Order::getUserId));
  293. int count = 0;
  294. for (Map.Entry<Long, List<Order>> entry : userOrderMap.entrySet()) {
  295. Long userId = entry.getKey();
  296. List<Order> todayUserOrders = entry.getValue();
  297. List<Order> allUserOrders = orderMapper.selectList(
  298. new LambdaQueryWrapper<Order>()
  299. .eq(Order::getUserId, userId)
  300. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  301. .orderByAsc(Order::getPayTime)
  302. );
  303. if (allUserOrders.isEmpty()) continue;
  304. StatUserRepurchase stat = new StatUserRepurchase();
  305. stat.setStatDate(statDate);
  306. stat.setUserId(userId);
  307. stat.setShopId(null);
  308. stat.setOrderCount(todayUserOrders.size());
  309. stat.setTotalOrderCount(allUserOrders.size());
  310. Order firstOrder = allUserOrders.get(0);
  311. Order lastOrder = allUserOrders.get(allUserOrders.size() - 1);
  312. stat.setFirstOrderDate(firstOrder.getPayTime() != null ? firstOrder.getPayTime().toLocalDate() : null);
  313. stat.setLastOrderDate(lastOrder.getPayTime() != null ? lastOrder.getPayTime().toLocalDate() : null);
  314. BigDecimal totalAmount = allUserOrders.stream()
  315. .map(o -> o.getTotalAmount() != null ? o.getTotalAmount() : BigDecimal.ZERO)
  316. .reduce(BigDecimal.ZERO, BigDecimal::add);
  317. stat.setTotalAmount(totalAmount);
  318. BigDecimal avgOrderAmount = allUserOrders.size() > 0
  319. ? totalAmount.divide(BigDecimal.valueOf(allUserOrders.size()), 2, RoundingMode.HALF_UP)
  320. : BigDecimal.ZERO;
  321. stat.setAvgOrderAmount(avgOrderAmount);
  322. if (allUserOrders.size() >= 2) {
  323. long totalDays = 0;
  324. for (int i = 1; i < allUserOrders.size(); i++) {
  325. if (allUserOrders.get(i-1).getPayTime() != null && allUserOrders.get(i).getPayTime() != null) {
  326. long days = java.time.temporal.ChronoUnit.DAYS.between(
  327. allUserOrders.get(i-1).getPayTime().toLocalDate(),
  328. allUserOrders.get(i).getPayTime().toLocalDate()
  329. );
  330. totalDays += days;
  331. }
  332. }
  333. stat.setRepurchaseDays((int) (totalDays / (allUserOrders.size() - 1)));
  334. } else {
  335. stat.setRepurchaseDays(0);
  336. }
  337. stat.setCreateTime(LocalDateTime.now());
  338. statUserRepurchaseMapper.insert(stat);
  339. count++;
  340. }
  341. log.info("用户复购统计汇总完成,共{}条记录", count);
  342. }
  343. }