Bläddra i källkod

会员管理代码

wwh 1 vecka sedan
förälder
incheckning
f72c6b4349

+ 0 - 4
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/constant/OpenStatsConstants.java

@@ -24,12 +24,8 @@ public final class OpenStatsConstants
24
 
24
 
25
     public static final String UNKNOWN_CITY = "未知";
25
     public static final String UNKNOWN_CITY = "未知";
26
 
26
 
27
-    public static final String DATE_PATTERN = "yyyy-MM-dd";
28
-
29
     public static final String YEAR_PATTERN = "yyyy";
27
     public static final String YEAR_PATTERN = "yyyy";
30
 
28
 
31
-    public static final int CACHE_TTL_CATEGORY_MIN = 5;
32
-
33
     public static final int CACHE_TTL_YEARLY_HOUR = 1;
29
     public static final int CACHE_TTL_YEARLY_HOUR = 1;
34
 
30
 
35
     public static final int CACHE_TTL_WORDCLOUD_HOUR = 24;
31
     public static final int CACHE_TTL_WORDCLOUD_HOUR = 24;

+ 6 - 8
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/controller/MallStatsOpenController.java

@@ -22,16 +22,16 @@ public class MallStatsOpenController extends BaseController
22
 
22
 
23
     @Anonymous
23
     @Anonymous
24
     @GetMapping("/categorySales")
24
     @GetMapping("/categorySales")
25
-    public AjaxResult categorySales(@RequestParam(value = "statDate", required = false) String statDate)
25
+    public AjaxResult categorySales(@RequestParam(value = "statYear", required = false) String statYear)
26
     {
26
     {
27
-        return success(mallStatsOpenService.getCategorySales(statDate));
27
+        return success(mallStatsOpenService.getCategorySales(statYear));
28
     }
28
     }
29
 
29
 
30
     @Anonymous
30
     @Anonymous
31
     @GetMapping("/hotCategoryRank")
31
     @GetMapping("/hotCategoryRank")
32
-    public AjaxResult hotCategoryRank(@RequestParam(value = "statDate", required = false) String statDate)
32
+    public AjaxResult hotCategoryRank(@RequestParam(value = "statYear", required = false) String statYear)
33
     {
33
     {
34
-        return success(mallStatsOpenService.getHotCategoryRank(statDate));
34
+        return success(mallStatsOpenService.getHotCategoryRank(statYear));
35
     }
35
     }
36
 
36
 
37
     @Anonymous
37
     @Anonymous
@@ -64,10 +64,8 @@ public class MallStatsOpenController extends BaseController
64
 
64
 
65
     @Anonymous
65
     @Anonymous
66
     @GetMapping("/overview")
66
     @GetMapping("/overview")
67
-    public AjaxResult overview(
68
-            @RequestParam(value = "statDate", required = false) String statDate,
69
-            @RequestParam(value = "statYear", required = false) String statYear)
67
+    public AjaxResult overview(@RequestParam(value = "statYear", required = false) String statYear)
70
     {
68
     {
71
-        return success(mallStatsOpenService.getOverview(statDate, statYear));
69
+        return success(mallStatsOpenService.getOverview(statYear));
72
     }
70
     }
73
 }
71
 }

+ 1 - 1
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/mapper/MallStatsOpenMapper.java

@@ -11,7 +11,7 @@ import com.ruoyi.web.modules.openstats.dto.RegionAmountRow;
11
  */
11
  */
12
 public interface MallStatsOpenMapper
12
 public interface MallStatsOpenMapper
13
 {
13
 {
14
-    List<CategoryQtyRow> selectCategoryQtyByFinishDate(@Param("statDate") String statDate);
14
+    List<CategoryQtyRow> selectCategoryQtyByFinishYear(@Param("statYear") int statYear);
15
 
15
 
16
     List<MonthCountRow> selectOrderCountByYear(@Param("statYear") int statYear);
16
     List<MonthCountRow> selectOrderCountByYear(@Param("statYear") int statYear);
17
 
17
 

+ 3 - 3
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/service/IMallStatsOpenService.java

@@ -13,9 +13,9 @@ import com.ruoyi.web.modules.openstats.vo.StatsOverviewVO;
13
  */
13
  */
14
 public interface IMallStatsOpenService
14
 public interface IMallStatsOpenService
15
 {
15
 {
16
-    CategorySalesVO getCategorySales(String statDate);
16
+    CategorySalesVO getCategorySales(String statYear);
17
 
17
 
18
-    HotCategoryRankVO getHotCategoryRank(String statDate);
18
+    HotCategoryRankVO getHotCategoryRank(String statYear);
19
 
19
 
20
     OrderTrendVO getOrderTrend(String statYear);
20
     OrderTrendVO getOrderTrend(String statYear);
21
 
21
 
@@ -25,5 +25,5 @@ public interface IMallStatsOpenService
25
 
25
 
26
     ReviewWordCloudVO getReviewWordCloud();
26
     ReviewWordCloudVO getReviewWordCloud();
27
 
27
 
28
-    StatsOverviewVO getOverview(String statDate, String statYear);
28
+    StatsOverviewVO getOverview(String statYear);
29
 }
29
 }

+ 19 - 39
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/service/impl/MallStatsOpenServiceImpl.java

@@ -4,8 +4,6 @@ import java.math.BigDecimal;
4
 import java.math.RoundingMode;
4
 import java.math.RoundingMode;
5
 import java.time.LocalDate;
5
 import java.time.LocalDate;
6
 import java.time.ZoneId;
6
 import java.time.ZoneId;
7
-import java.time.format.DateTimeFormatter;
8
-import java.time.format.DateTimeParseException;
9
 import java.util.ArrayList;
7
 import java.util.ArrayList;
10
 import java.util.Collections;
8
 import java.util.Collections;
11
 import java.util.Comparator;
9
 import java.util.Comparator;
@@ -44,8 +42,6 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
44
 {
42
 {
45
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
43
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
46
 
44
 
47
-    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern(OpenStatsConstants.DATE_PATTERN);
48
-
49
     @Autowired
45
     @Autowired
50
     private MallStatsOpenMapper statsMapper;
46
     private MallStatsOpenMapper statsMapper;
51
 
47
 
@@ -56,34 +52,34 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
56
     private ReviewWordCloudSupport wordCloudSupport;
52
     private ReviewWordCloudSupport wordCloudSupport;
57
 
53
 
58
     @Override
54
     @Override
59
-    public CategorySalesVO getCategorySales(String statDate)
55
+    public CategorySalesVO getCategorySales(String statYear)
60
     {
56
     {
61
-        String date = resolveStatDate(statDate);
62
-        String cacheKey = StatsCacheKeys.categorySales(date);
57
+        int year = resolveStatYear(statYear);
58
+        String cacheKey = StatsCacheKeys.categorySales(year);
63
         CategorySalesVO cached = redisCache.getCacheObject(cacheKey);
59
         CategorySalesVO cached = redisCache.getCacheObject(cacheKey);
64
         if (cached != null)
60
         if (cached != null)
65
         {
61
         {
66
             return cached;
62
             return cached;
67
         }
63
         }
68
-        List<CategoryQtyRow> rows = safeList(statsMapper.selectCategoryQtyByFinishDate(date));
69
-        CategorySalesVO vo = buildCategorySales(date, rows);
70
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_CATEGORY_MIN, TimeUnit.MINUTES);
64
+        List<CategoryQtyRow> rows = safeList(statsMapper.selectCategoryQtyByFinishYear(year));
65
+        CategorySalesVO vo = buildCategorySales(year, rows);
66
+        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
71
         return vo;
67
         return vo;
72
     }
68
     }
73
 
69
 
74
     @Override
70
     @Override
75
-    public HotCategoryRankVO getHotCategoryRank(String statDate)
71
+    public HotCategoryRankVO getHotCategoryRank(String statYear)
76
     {
72
     {
77
-        String date = resolveStatDate(statDate);
78
-        String cacheKey = StatsCacheKeys.hotCategory(date);
73
+        int year = resolveStatYear(statYear);
74
+        String cacheKey = StatsCacheKeys.hotCategory(year);
79
         HotCategoryRankVO cached = redisCache.getCacheObject(cacheKey);
75
         HotCategoryRankVO cached = redisCache.getCacheObject(cacheKey);
80
         if (cached != null)
76
         if (cached != null)
81
         {
77
         {
82
             return cached;
78
             return cached;
83
         }
79
         }
84
-        List<CategoryQtyRow> rows = safeList(statsMapper.selectCategoryQtyByFinishDate(date));
85
-        HotCategoryRankVO vo = buildHotCategoryRank(date, rows);
86
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_CATEGORY_MIN, TimeUnit.MINUTES);
80
+        List<CategoryQtyRow> rows = safeList(statsMapper.selectCategoryQtyByFinishYear(year));
81
+        HotCategoryRankVO vo = buildHotCategoryRank(year, rows);
82
+        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
87
         return vo;
83
         return vo;
88
     }
84
     }
89
 
85
 
@@ -151,11 +147,11 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
151
     }
147
     }
152
 
148
 
153
     @Override
149
     @Override
154
-    public StatsOverviewVO getOverview(String statDate, String statYear)
150
+    public StatsOverviewVO getOverview(String statYear)
155
     {
151
     {
156
         StatsOverviewVO overview = new StatsOverviewVO();
152
         StatsOverviewVO overview = new StatsOverviewVO();
157
-        overview.setCategorySales(getCategorySales(statDate));
158
-        overview.setHotCategoryRank(getHotCategoryRank(statDate));
153
+        overview.setCategorySales(getCategorySales(statYear));
154
+        overview.setHotCategoryRank(getHotCategoryRank(statYear));
159
         overview.setOrderTrend(getOrderTrend(statYear));
155
         overview.setOrderTrend(getOrderTrend(statYear));
160
         overview.setShopEntry(getShopEntry(statYear));
156
         overview.setShopEntry(getShopEntry(statYear));
161
         overview.setRegionRank(getRegionRank(statYear));
157
         overview.setRegionRank(getRegionRank(statYear));
@@ -163,10 +159,10 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
163
         return overview;
159
         return overview;
164
     }
160
     }
165
 
161
 
166
-    private CategorySalesVO buildCategorySales(String statDate, List<CategoryQtyRow> rows)
162
+    private CategorySalesVO buildCategorySales(int statYear, List<CategoryQtyRow> rows)
167
     {
163
     {
168
         CategorySalesVO vo = new CategorySalesVO();
164
         CategorySalesVO vo = new CategorySalesVO();
169
-        vo.setStatDate(statDate);
165
+        vo.setStatYear(statYear);
170
         long totalQty = 0L;
166
         long totalQty = 0L;
171
         List<CategorySalesItemVO> items = new ArrayList<>();
167
         List<CategorySalesItemVO> items = new ArrayList<>();
172
         for (CategoryQtyRow row : rows)
168
         for (CategoryQtyRow row : rows)
@@ -186,12 +182,12 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
186
         return vo;
182
         return vo;
187
     }
183
     }
188
 
184
 
189
-    private HotCategoryRankVO buildHotCategoryRank(String statDate, List<CategoryQtyRow> rows)
185
+    private HotCategoryRankVO buildHotCategoryRank(int statYear, List<CategoryQtyRow> rows)
190
     {
186
     {
191
         rows.sort(Comparator.comparing(CategoryQtyRow::getQty, Comparator.nullsFirst(Long::compareTo)).reversed()
187
         rows.sort(Comparator.comparing(CategoryQtyRow::getQty, Comparator.nullsFirst(Long::compareTo)).reversed()
192
                 .thenComparing(row -> row.getCategoryId() != null ? row.getCategoryId() : OpenStatsConstants.UNCATEGORIZED_ID));
188
                 .thenComparing(row -> row.getCategoryId() != null ? row.getCategoryId() : OpenStatsConstants.UNCATEGORIZED_ID));
193
         HotCategoryRankVO vo = new HotCategoryRankVO();
189
         HotCategoryRankVO vo = new HotCategoryRankVO();
194
-        vo.setStatDate(statDate);
190
+        vo.setStatYear(statYear);
195
         int limit = Math.min(OpenStatsConstants.HOT_CATEGORY_LIMIT, rows.size());
191
         int limit = Math.min(OpenStatsConstants.HOT_CATEGORY_LIMIT, rows.size());
196
         for (int i = 0; i < limit; i++)
192
         for (int i = 0; i < limit; i++)
197
         {
193
         {
@@ -363,22 +359,6 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
363
         return monthMap;
359
         return monthMap;
364
     }
360
     }
365
 
361
 
366
-    private String resolveStatDate(String statDate)
367
-    {
368
-        if (StringUtils.isEmpty(statDate))
369
-        {
370
-            return LocalDate.now(ZONE_SHANGHAI).format(DATE_FMT);
371
-        }
372
-        try
373
-        {
374
-            return LocalDate.parse(statDate.trim(), DATE_FMT).format(DATE_FMT);
375
-        }
376
-        catch (DateTimeParseException ex)
377
-        {
378
-            return LocalDate.now(ZONE_SHANGHAI).format(DATE_FMT);
379
-        }
380
-    }
381
-
382
     private int resolveStatYear(String statYear)
362
     private int resolveStatYear(String statYear)
383
     {
363
     {
384
         if (StringUtils.isEmpty(statYear))
364
         if (StringUtils.isEmpty(statYear))

+ 4 - 4
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/support/StatsCacheKeys.java

@@ -11,14 +11,14 @@ public final class StatsCacheKeys
11
     {
11
     {
12
     }
12
     }
13
 
13
 
14
-    public static String categorySales(String statDate)
14
+    public static String categorySales(int statYear)
15
     {
15
     {
16
-        return PREFIX + "cat:sales:" + statDate;
16
+        return PREFIX + "cat:sales:" + statYear;
17
     }
17
     }
18
 
18
 
19
-    public static String hotCategory(String statDate)
19
+    public static String hotCategory(int statYear)
20
     {
20
     {
21
-        return PREFIX + "cat:hot:" + statDate;
21
+        return PREFIX + "cat:hot:" + statYear;
22
     }
22
     }
23
 
23
 
24
     public static String orderTrend(int statYear)
24
     public static String orderTrend(int statYear)

+ 5 - 5
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/vo/CategorySalesVO.java

@@ -5,20 +5,20 @@ import java.util.List;
5
 
5
 
6
 public class CategorySalesVO
6
 public class CategorySalesVO
7
 {
7
 {
8
-    private String statDate;
8
+    private int statYear;
9
 
9
 
10
     private long totalQty;
10
     private long totalQty;
11
 
11
 
12
     private List<CategorySalesItemVO> items = new ArrayList<>();
12
     private List<CategorySalesItemVO> items = new ArrayList<>();
13
 
13
 
14
-    public String getStatDate()
14
+    public int getStatYear()
15
     {
15
     {
16
-        return statDate;
16
+        return statYear;
17
     }
17
     }
18
 
18
 
19
-    public void setStatDate(String statDate)
19
+    public void setStatYear(int statYear)
20
     {
20
     {
21
-        this.statDate = statDate;
21
+        this.statYear = statYear;
22
     }
22
     }
23
 
23
 
24
     public long getTotalQty()
24
     public long getTotalQty()

+ 5 - 5
baqing-shop/src/main/java/com/ruoyi/web/modules/openstats/vo/HotCategoryRankVO.java

@@ -5,18 +5,18 @@ import java.util.List;
5
 
5
 
6
 public class HotCategoryRankVO
6
 public class HotCategoryRankVO
7
 {
7
 {
8
-    private String statDate;
8
+    private int statYear;
9
 
9
 
10
     private List<HotCategoryRankItemVO> items = new ArrayList<>();
10
     private List<HotCategoryRankItemVO> items = new ArrayList<>();
11
 
11
 
12
-    public String getStatDate()
12
+    public int getStatYear()
13
     {
13
     {
14
-        return statDate;
14
+        return statYear;
15
     }
15
     }
16
 
16
 
17
-    public void setStatDate(String statDate)
17
+    public void setStatYear(int statYear)
18
     {
18
     {
19
-        this.statDate = statDate;
19
+        this.statYear = statYear;
20
     }
20
     }
21
 
21
 
22
     public List<HotCategoryRankItemVO> getItems()
22
     public List<HotCategoryRankItemVO> getItems()

+ 2 - 2
baqing-shop/src/main/resources/mapper/openstats/MallStatsOpenMapper.xml

@@ -2,7 +2,7 @@
2
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
2
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3
 <mapper namespace="com.ruoyi.web.modules.openstats.mapper.MallStatsOpenMapper">
3
 <mapper namespace="com.ruoyi.web.modules.openstats.mapper.MallStatsOpenMapper">
4
 
4
 
5
-    <select id="selectCategoryQtyByFinishDate" resultType="com.ruoyi.web.modules.openstats.dto.CategoryQtyRow">
5
+    <select id="selectCategoryQtyByFinishYear" resultType="com.ruoyi.web.modules.openstats.dto.CategoryQtyRow">
6
         select
6
         select
7
             ifnull(c1.category_id, 0) as categoryId,
7
             ifnull(c1.category_id, 0) as categoryId,
8
             ifnull(c1.category_name, '未分类') as categoryName,
8
             ifnull(c1.category_name, '未分类') as categoryName,
@@ -15,7 +15,7 @@
15
         where o.order_status = '3'
15
         where o.order_status = '3'
16
           and o.order_status &lt;&gt; '5'
16
           and o.order_status &lt;&gt; '5'
17
           and o.finish_time is not null
17
           and o.finish_time is not null
18
-          and date(o.finish_time) = #{statDate}
18
+          and year(o.finish_time) = #{statYear}
19
         group by ifnull(c1.category_id, 0), ifnull(c1.category_name, '未分类')
19
         group by ifnull(c1.category_id, 0), ifnull(c1.category_name, '未分类')
20
     </select>
20
     </select>
21
 
21
 

+ 12 - 11
baqing-shop/src/test/java/com/ruoyi/web/modules/openstats/service/MallStatsOpenServiceImplTest.java

@@ -42,7 +42,7 @@ import com.ruoyi.web.modules.openstats.vo.WordCloudItemVO;
42
 @ExtendWith(MockitoExtension.class)
42
 @ExtendWith(MockitoExtension.class)
43
 class MallStatsOpenServiceImplTest
43
 class MallStatsOpenServiceImplTest
44
 {
44
 {
45
-    private static final String STAT_DATE = "2026-05-26";
45
+    private static final int STAT_YEAR = 2026;
46
 
46
 
47
     @Mock
47
     @Mock
48
     private MallStatsOpenMapper statsMapper;
48
     private MallStatsOpenMapper statsMapper;
@@ -65,26 +65,26 @@ class MallStatsOpenServiceImplTest
65
     @Test
65
     @Test
66
     void categorySales_ratioSumNear100()
66
     void categorySales_ratioSumNear100()
67
     {
67
     {
68
-        when(statsMapper.selectCategoryQtyByFinishDate(STAT_DATE)).thenReturn(Arrays.asList(
68
+        when(statsMapper.selectCategoryQtyByFinishYear(STAT_YEAR)).thenReturn(Arrays.asList(
69
                 row(1L, "兽药", 500L),
69
                 row(1L, "兽药", 500L),
70
                 row(2L, "饲料", 400L),
70
                 row(2L, "饲料", 400L),
71
                 row(3L, "农药", 300L)));
71
                 row(3L, "农药", 300L)));
72
 
72
 
73
-        CategorySalesVO vo = service.getCategorySales(STAT_DATE);
73
+        CategorySalesVO vo = service.getCategorySales("2026");
74
 
74
 
75
-        assertEquals(STAT_DATE, vo.getStatDate());
75
+        assertEquals(STAT_YEAR, vo.getStatYear());
76
         assertEquals(1200L, vo.getTotalQty());
76
         assertEquals(1200L, vo.getTotalQty());
77
         assertEquals(3, vo.getItems().size());
77
         assertEquals(3, vo.getItems().size());
78
         double ratioSum = vo.getItems().stream().mapToDouble(i -> i.getRatio()).sum();
78
         double ratioSum = vo.getItems().stream().mapToDouble(i -> i.getRatio()).sum();
79
         assertEquals(100.0, ratioSum, 0.05);
79
         assertEquals(100.0, ratioSum, 0.05);
80
-        verify(redisCache).setCacheObject(eq(StatsCacheKeys.categorySales(STAT_DATE)), eq(vo),
81
-                eq(OpenStatsConstants.CACHE_TTL_CATEGORY_MIN), eq(TimeUnit.MINUTES));
80
+        verify(redisCache).setCacheObject(eq(StatsCacheKeys.categorySales(STAT_YEAR)), eq(vo),
81
+                eq(OpenStatsConstants.CACHE_TTL_YEARLY_HOUR), eq(TimeUnit.HOURS));
82
     }
82
     }
83
 
83
 
84
     @Test
84
     @Test
85
     void hotCategoryRank_onlyTop5()
85
     void hotCategoryRank_onlyTop5()
86
     {
86
     {
87
-        when(statsMapper.selectCategoryQtyByFinishDate(STAT_DATE)).thenReturn(Arrays.asList(
87
+        when(statsMapper.selectCategoryQtyByFinishYear(STAT_YEAR)).thenReturn(Arrays.asList(
88
                 row(1L, "A", 100L),
88
                 row(1L, "A", 100L),
89
                 row(2L, "B", 90L),
89
                 row(2L, "B", 90L),
90
                 row(3L, "C", 80L),
90
                 row(3L, "C", 80L),
@@ -92,8 +92,9 @@ class MallStatsOpenServiceImplTest
92
                 row(5L, "E", 60L),
92
                 row(5L, "E", 60L),
93
                 row(6L, "F", 50L)));
93
                 row(6L, "F", 50L)));
94
 
94
 
95
-        HotCategoryRankVO vo = service.getHotCategoryRank(STAT_DATE);
95
+        HotCategoryRankVO vo = service.getHotCategoryRank("2026");
96
 
96
 
97
+        assertEquals(STAT_YEAR, vo.getStatYear());
97
         assertEquals(5, vo.getItems().size());
98
         assertEquals(5, vo.getItems().size());
98
         assertEquals(1, vo.getItems().get(0).getRank());
99
         assertEquals(1, vo.getItems().get(0).getRank());
99
         assertEquals("A", vo.getItems().get(0).getCategoryName());
100
         assertEquals("A", vo.getItems().get(0).getCategoryName());
@@ -164,14 +165,14 @@ class MallStatsOpenServiceImplTest
164
     @Test
165
     @Test
165
     void overview_containsAllSections()
166
     void overview_containsAllSections()
166
     {
167
     {
167
-        when(statsMapper.selectCategoryQtyByFinishDate(anyString())).thenReturn(Collections.emptyList());
168
+        when(statsMapper.selectCategoryQtyByFinishYear(anyInt())).thenReturn(Collections.emptyList());
168
         when(statsMapper.selectOrderCountByYear(anyInt())).thenReturn(Collections.emptyList());
169
         when(statsMapper.selectOrderCountByYear(anyInt())).thenReturn(Collections.emptyList());
169
         when(statsMapper.selectShopCountByYear(anyInt())).thenReturn(Collections.emptyList());
170
         when(statsMapper.selectShopCountByYear(anyInt())).thenReturn(Collections.emptyList());
170
         when(statsMapper.selectRegionAmountByYear(anyInt())).thenReturn(Collections.emptyList());
171
         when(statsMapper.selectRegionAmountByYear(anyInt())).thenReturn(Collections.emptyList());
171
         when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.emptyList());
172
         when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.emptyList());
172
         when(wordCloudSupport.buildTopWords(any())).thenReturn(new ReviewWordCloudVO());
173
         when(wordCloudSupport.buildTopWords(any())).thenReturn(new ReviewWordCloudVO());
173
 
174
 
174
-        StatsOverviewVO overview = service.getOverview(STAT_DATE, "2026");
175
+        StatsOverviewVO overview = service.getOverview("2026");
175
 
176
 
176
         assertNotNull(overview.getCategorySales());
177
         assertNotNull(overview.getCategorySales());
177
         assertNotNull(overview.getHotCategoryRank());
178
         assertNotNull(overview.getHotCategoryRank());
@@ -179,7 +180,7 @@ class MallStatsOpenServiceImplTest
179
         assertNotNull(overview.getShopEntry());
180
         assertNotNull(overview.getShopEntry());
180
         assertNotNull(overview.getRegionRank());
181
         assertNotNull(overview.getRegionRank());
181
         assertNotNull(overview.getReviewWordCloud());
182
         assertNotNull(overview.getReviewWordCloud());
182
-        verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishDate(STAT_DATE);
183
+        verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishYear(STAT_YEAR);
183
     }
184
     }
184
 
185
 
185
     private CategoryQtyRow row(Long id, String name, Long qty)
186
     private CategoryQtyRow row(Long id, String name, Long qty)

+ 2 - 2
doc/平台后台/外部接口/商城数据统计功能需求-草稿.md

@@ -1,7 +1,7 @@
1
 做一个商城数据统计模块:
1
 做一个商城数据统计模块:
2
 
2
 
3
-1. 农资品类销售:根据农资商城中农资品类分类统计各个品类销售量与总销售量的占比;饼状图展示;农资品类分类以农资商城一级分类进行统计;时间为当前
4
-2. 热销农资排行(前5):根据农资商城中农资品类分类统计各个品类销售量,展示销售量前5的热销农资;农资品类分类以农资商城一级分类进行统计;时间为当前
3
+1. 农资品类销售:根据农资商城中农资品类分类统计各个品类销售量与总销售量的占比;饼状图展示;农资品类分类以农资商城一级分类进行统计;时间为当前年份
4
+2. 热销农资排行(前5):根据农资商城中农资品类分类统计各个品类销售量,展示销售量前5的热销农资;农资品类分类以农资商城一级分类进行统计;时间为当前年份
5
 3. 商城订单趋势: 按月统计当前年份每月的商城订单数(单),曲线图展示;
5
 3. 商城订单趋势: 按月统计当前年份每月的商城订单数(单),曲线图展示;
6
 4. 店铺入驻:按月统计当前年份每月入驻商城的店铺数(家),饼状图展示;
6
 4. 店铺入驻:按月统计当前年份每月入驻商城的店铺数(家),饼状图展示;
7
 5. 消费区域排名(前5): 根据农资商城的订单的收货城市分类统计各个城市的农资销售额(万),展示前5的消费区域;
7
 5. 消费区域排名(前5): 根据农资商城的订单的收货城市分类统计各个城市的农资销售额(万),展示前5的消费区域;

+ 37 - 37
doc/平台后台/外部接口/商城数据统计功能需求.md

@@ -5,7 +5,7 @@
5
 > **说明:** 本文档 **仅描述功能需求、统计口径与业务规则**;**不涉及** 数据库结构、接口路径、Token 实现机制及技术栈细节。  
5
 > **说明:** 本文档 **仅描述功能需求、统计口径与业务规则**;**不涉及** 数据库结构、接口路径、Token 实现机制及技术栈细节。  
6
 > **v1.0:** 首版定稿;对齐草稿 §1~§7;补全时间口径、分类归属、订单/店铺/评价边界与空数据规则;排除草稿 §8~§9。  
6
 > **v1.0:** 首版定稿;对齐草稿 §1~§7;补全时间口径、分类归属、订单/店铺/评价边界与空数据规则;排除草稿 §8~§9。  
7
 > **v1.0.1:** §5.2 **热销农资排行** 回归草稿口径——按 **平台一级品类** 当日销量 **Top5**(非商品 Top5)。  
7
 > **v1.0.1:** §5.2 **热销农资排行** 回归草稿口径——按 **平台一级品类** 当日销量 **Top5**(非商品 Top5)。  
8
-> **v1.0.2:** 明示 **完成时间** 含 **买家手动确认收货** 与 **系统自动确认收货**(《商城设置》)写入的时点,口径一致
8
+> **v1.0.4:** §5.1、§5.2 **统计口径改为当前自然年**(与草稿一致);按 **完成时间** 归属 **统计年** 汇总一级品类销量
9
 
9
 
10
 ---
10
 ---
11
 
11
 
@@ -17,8 +17,8 @@
17
 
17
 
18
 | 序号 | 指标(草稿) | 展示形态(建议) |
18
 | 序号 | 指标(草稿) | 展示形态(建议) |
19
 |:----:|--------------|------------------|
19
 |:----:|--------------|------------------|
20
-| 1 | **农资品类销售** | 饼图:各 **一级品类** 当日销量占比 |
21
-| 2 | **热销农资排行(前 5)** | 列表/条形:当日销量 **Top5 一级品类** |
20
+| 1 | **农资品类销售** | 饼图:各 **一级品类** **当年**销量占比 |
21
+| 2 | **热销农资排行(前 5)** | 列表/条形:**当年**销量 **Top5 一级品类** |
22
 | 3 | **商城订单趋势** | 折线:当年 **逐月订单笔数** |
22
 | 3 | **商城订单趋势** | 折线:当年 **逐月订单笔数** |
23
 | 4 | **店铺入驻** | 饼图:当年 **逐月新入驻店铺** 占比 |
23
 | 4 | **店铺入驻** | 饼图:当年 **逐月新入驻店铺** 占比 |
24
 | 5 | **消费区域排名(前 5)** | 列表:当年 **Top5 收货城市** 销售额(万元) |
24
 | 5 | **消费区域排名(前 5)** | 列表:当年 **Top5 收货城市** 销售额(万元) |
@@ -40,8 +40,8 @@
40
 
40
 
41
 | 草稿条目 | 定稿处理 |
41
 | 草稿条目 | 定稿处理 |
42
 |----------|----------|
42
 |----------|----------|
43
-| §1 农资品类销售 · 饼图 · 一级分类 · 当天 | §5.1 |
44
-| §2 热销农资排行 · 前 5 · 一级分类 · 当天 | §5.2(与 §5.1 **同口径**,输出 **销量 Top5 一级品类** 排行) |
43
+| §1 农资品类销售 · 饼图 · 一级分类 · **当前年份** | §5.1 |
44
+| §2 热销农资排行 · 前 5 · 一级分类 · **当前年份** | §5.2(与 §5.1 **同口径**,输出 **销量 Top5 一级品类** 排行) |
45
 | §3 商城订单趋势 · 按月 · 当年 · 折线 | §5.3 |
45
 | §3 商城订单趋势 · 按月 · 当年 · 折线 | §5.3 |
46
 | §4 店铺入驻 · 按月 · 当年 · 饼图 | §5.4 |
46
 | §4 店铺入驻 · 按月 · 当年 · 饼图 | §5.4 |
47
 | §5 消费区域排名 · 前 5 · 收货城市 · 销售额(万) | §5.5 |
47
 | §5 消费区域排名 · 前 5 · 收货城市 · 销售额(万) | §5.5 |
@@ -67,9 +67,9 @@
67
 | 关联模块 | 关系 | 边界说明 |
67
 | 关联模块 | 关系 | 边界说明 |
68
 |----------|------|----------|
68
 |----------|------|----------|
69
 | **商品分类** v1.5 | **口径依赖** | 品类统计 **仅平台一级分类**(`shop_id` 为空);商品挂 **二级**,**归并至父级一级** |
69
 | **商品分类** v1.5 | **口径依赖** | 品类统计 **仅平台一级分类**(`shop_id` 为空);商品挂 **二级**,**归并至父级一级** |
70
-| **商品管理** v1.3.3 | **间接** | 当销量 **按订单商品行汇总**;**不得** 用商品档案 **累计销量** 代替(见 §4.2) |
71
-| **订单管理** v1.0.1 | **强关联** | 订单趋势、区域销售额 **以订单为准**;**已完成** 参与金额/当销量;**已删除** **不参与** |
72
-| **商城设置** v1.1 | **口径依赖** | **自动确认收货** 写入的 **完成时间** 与 **手动确认** **等价**,纳入 **统计日/统计年(完成侧)** 指标;C 端 **是否展示销量** **不影响** 本模块统计 |
70
+| **商品管理** v1.3.3 | **间接** | 当销量 **按订单商品行汇总**;**不得** 用商品档案 **累计销量** 代替(见 §4.2) |
71
+| **订单管理** v1.0.1 | **强关联** | 订单趋势、区域销售额 **以订单为准**;**已完成** 参与金额/当销量;**已删除** **不参与** |
72
+| **商城设置** v1.1 | **口径依赖** | **自动确认收货** 写入的 **完成时间** 与 **手动确认** **等价**,纳入 **统计年(完成侧)** 指标;C 端 **是否展示销量** **不影响** 本模块统计 |
73
 | **店铺管理** v1.3.5 | **强关联** | 入驻统计 **以店铺创建时间** 为准;含 **入驻审核通过建店** 与 **平台代开店** |
73
 | **店铺管理** v1.3.5 | **强关联** | 入驻统计 **以店铺创建时间** 为准;含 **入驻审核通过建店** 与 **平台代开店** |
74
 | **评价管理** | **口径依赖** | 词云 **仅 C 端可见、未删除** 的买家评价 **正文** |
74
 | **评价管理** | **口径依赖** | 词云 **仅 C 端可见、未删除** 的买家评价 **正文** |
75
 | **会员管理 / 资金概览** | **无直接** | 本模块 **不输出** 会员数、平台资金 |
75
 | **会员管理 / 资金概览** | **无直接** | 本模块 **不输出** 会员数、平台资金 |
@@ -99,10 +99,10 @@
99
 | 项 | 定稿 |
99
 | 项 | 定稿 |
100
 |----|------|
100
 |----|------|
101
 | 订单笔数(§5.3) | 按 **下单时间** 归属自然月;**排除已删除**;**含** 待支付/待发货/已发货/已完成/已关闭 |
101
 | 订单笔数(§5.3) | 按 **下单时间** 归属自然月;**排除已删除**;**含** 待支付/待发货/已发货/已完成/已关闭 |
102
-| 当日销量(§5.1、§5.2) | 按订单 **完成时间** 归属 **自然日**(**含手动确认** 与 **自动确认收货**);统计 **订单商品行销售件数** 之和 |
102
+| 当年销量(§5.1、§5.2) | 按订单 **完成时间** 归属 **自然年**(**含手动确认** 与 **自动确认收货**);统计 **订单商品行销售件数** 之和 |
103
 | 区域销售额(§5.5) | 按订单 **完成时间** 归属 **自然年**(**含手动确认** 与 **自动确认收货**);金额 = 订单 **实付金额**(与 O11 **累计消费金额** 同口径) |
103
 | 区域销售额(§5.5) | 按订单 **完成时间** 归属 **自然年**(**含手动确认** 与 **自动确认收货**);金额 = 订单 **实付金额**(与 O11 **累计消费金额** 同口径) |
104
-| 未完成订单 | **不计入** 当销量与区域销售额 |
105
-| 已关闭 | **不计入** 当销量与区域销售额;**可计入** 订单趋势笔数(若当月曾下单且未删) |
104
+| 未完成订单 | **不计入** 当销量与区域销售额 |
105
+| 已关闭 | **不计入** 当销量与区域销售额;**可计入** 订单趋势笔数(若当月曾下单且未删) |
106
 
106
 
107
 ### 2.3 与平台《商品分类功能需求》
107
 ### 2.3 与平台《商品分类功能需求》
108
 
108
 
@@ -127,7 +127,7 @@
127
 | 项 | 定稿 |
127
 | 项 | 定稿 |
128
 |----|------|
128
 |----|------|
129
 | 完成路径 | 订单 **已完成** 的 **完成时间** 来源:**买家手动确认收货** 或 **系统自动确认收货**(MC-O1~O3),**二者等价** |
129
 | 完成路径 | 订单 **已完成** 的 **完成时间** 来源:**买家手动确认收货** 或 **系统自动确认收货**(MC-O1~O3),**二者等价** |
130
-| 统计归属 | 以 **完成时间的日历日/日历年** 归属 **统计日**(§5.1、§5.2)或 **统计年完成侧指标**(§5.5);**不** 以发货时间代替 |
130
+| 统计归属 | 以 **完成时间的日历年** 归属 **统计年**(§5.1、§5.2、§5.5);**不** 以发货时间代替 |
131
 | 与 O7 | 与 **订单管理 O7**、**商城设置** 自动确认规则 **一致**;平台登记「送达」**不** 写入完成时间 |
131
 | 与 O7 | 与 **订单管理 O7**、**商城设置** 自动确认规则 **一致**;平台登记「送达」**不** 写入完成时间 |
132
 
132
 
133
 ---
133
 ---
@@ -136,8 +136,7 @@
136
 
136
 
137
 | 概念 | 说明 |
137
 | 概念 | 说明 |
138
 |------|------|
138
 |------|------|
139
-| 统计日 | **当前自然日**(服务器 **Asia/Shanghai** 日历日,下同) |
140
-| 统计年 | **当前自然年**(1 月 1 日~12 月 31 日) |
139
+| 统计年 | **当前自然年**(1 月 1 日~12 月 31 日,服务器 **Asia/Shanghai**) |
141
 | 平台一级品类 | 商品分类模块中 **平台维护**、**无上级** 的分类节点 |
140
 | 平台一级品类 | 商品分类模块中 **平台维护**、**无上级** 的分类节点 |
142
 | 销售件数 | 某维度下 **已完成订单商品行** 的 **购买数量** 合计 |
141
 | 销售件数 | 某维度下 **已完成订单商品行** 的 **购买数量** 合计 |
143
 | 完成时间 | 订单进入 **已完成** 时写入的时点;含 **手动确认收货** 与 **自动确认收货**(《商城设置》),**口径相同** |
142
 | 完成时间 | 订单进入 **已完成** 时写入的时点;含 **手动确认收货** 与 **自动确认收货**(《商城设置》),**口径相同** |
@@ -156,18 +155,18 @@
156
 
155
 
157
 | 指标组 | 时间范围 | 归属字段(业务语义) |
156
 | 指标组 | 时间范围 | 归属字段(业务语义) |
158
 |--------|----------|----------------------|
157
 |--------|----------|----------------------|
159
-| 农资品类销售、热销排行 | **统计日 = 当天** | 订单 **已完成** 的 **完成时间**(**含自动确认收货** 写入时刻) |
158
+| 农资品类销售、热销排行 | **统计年 = 当年** | 订单 **已完成** 的 **完成时间**(**含自动确认收货** 写入时刻) |
160
 | 订单趋势、店铺入驻 | **统计年 = 当年** | 订单 **下单时间** / 店铺 **创建时间** |
159
 | 订单趋势、店铺入驻 | **统计年 = 当年** | 订单 **下单时间** / 店铺 **创建时间** |
161
 | 消费区域排名 | **统计年 = 当年** | 订单 **已完成** 的 **完成时间**(**含自动确认收货** 写入时刻) |
160
 | 消费区域排名 | **统计年 = 当年** | 订单 **已完成** 的 **完成时间**(**含自动确认收货** 写入时刻) |
162
 | 消费者评价词云 | **全量历史**(截至请求时刻) | 评价 **提交时间** **不参与** 过滤(草稿未限定时效) |
161
 | 消费者评价词云 | **全量历史**(截至请求时刻) | 评价 **提交时间** **不参与** 过滤(草稿未限定时效) |
163
 
162
 
164
-### 4.2 当销量 vs 商品累计销量
163
+### 4.2 当销量 vs 商品累计销量
165
 
164
 
166
-| 维度 | 当销量(§5.1、§5.2) | 商品档案 `sales_count` |
165
+| 维度 | 当销量(§5.1、§5.2) | 商品档案 `sales_count` |
167
 |------|------------------------|-------------------------|
166
 |------|------------------------|-------------------------|
168
-| 用途 | 本模块 **当** 排行/占比 | C 端展示 **累计** 销量 |
169
-| 口径 | **仅统计日当天完成** 的件数 | **历史全部已完成** 累计 |
170
-| 关系 | **不得** 用 `sales_count` 直接代替当销量 | — |
167
+| 用途 | 本模块 **当** 排行/占比 | C 端展示 **累计** 销量 |
168
+| 口径 | **仅统计年年内完成** 的件数 | **历史全部已完成** 累计 |
169
+| 关系 | **不得** 用 `sales_count` 直接代替当销量 | — |
171
 
170
 
172
 ### 4.3 数据范围与排除
171
 ### 4.3 数据范围与排除
173
 
172
 
@@ -183,8 +182,8 @@
183
 
182
 
184
 | 场景 | 定稿 |
183
 | 场景 | 定稿 |
185
 |------|------|
184
 |------|------|
186
-| 当无任何完成订单 | 品类销售 **空列表或全 0**;热销 **空列表**;占比 **不展示** 或 **无饼图** |
187
-| 占比计算 | 各品类销量 / **当有销量的品类销量合计** × 100%;**保留 1 位小数**;合计 **100%**(四舍五入误差 **归最大项或「其他」**,实现 **须一致**) |
185
+| 当无任何完成订单 | 品类销售 **空列表或全 0**;热销 **空列表**;占比 **不展示** 或 **无饼图** |
186
+| 占比计算 | 各品类销量 / **当有销量的品类销量合计** × 100%;**保留 1 位小数**;合计 **100%**(四舍五入误差 **归最大项或「其他」**,实现 **须一致**) |
188
 | 当年某月无订单/无新店 | 订单趋势/入驻 **该月记 0**,**仍返回 12 个月** |
187
 | 当年某月无订单/无新店 | 订单趋势/入驻 **该月记 0**,**仍返回 12 个月** |
189
 | 评价正文均空或无评价 | 词云 **空列表** |
188
 | 评价正文均空或无评价 | 词云 **空列表** |
190
 
189
 
@@ -205,12 +204,12 @@
205
 
204
 
206
 ### 5.1 农资品类销售
205
 ### 5.1 农资品类销售
207
 
206
 
208
-**业务含义:** 展示 **统计日** 各 **平台一级品类** 的 **销售件数** 及 **占当日总销量比重**,供饼图渲染。
207
+**业务含义:** 展示 **统计年** 各 **平台一级品类** 的 **销售件数** 及 **占当年总销量比重**,供饼图渲染。
209
 
208
 
210
 **统计逻辑:**
209
 **统计逻辑:**
211
 
210
 
212
 ```text
211
 ```text
213
-取统计内「已完成」的全部订单(完成时间 = 手动确认 或 自动确认收货写入时刻)
212
+取统计内「已完成」的全部订单(完成时间 = 手动确认 或 自动确认收货写入时刻)
214
     → 展开订单商品行(goods_id、购买数量 qty)
213
     → 展开订单商品行(goods_id、购买数量 qty)
215
     → 按 goods 的 category_id 找到平台二级分类
214
     → 按 goods 的 category_id 找到平台二级分类
216
     → 归并至父级「一级品类」
215
     → 归并至父级「一级品类」
@@ -230,21 +229,21 @@
230
 | 编号 | 规则 |
229
 | 编号 | 规则 |
231
 |------|------|
230
 |------|------|
232
 | **ST-C1** | **仅一级品类**;**不** 输出二级明细 |
231
 | **ST-C1** | **仅一级品类**;**不** 输出二级明细 |
233
-| **ST-C2** | 某品类当 **0 件** → **可不返回该项** 或 **返回 0**(实现 **须六项接口一致**) |
232
+| **ST-C2** | 某品类当 **0 件** → **可不返回该项** 或 **返回 0**(实现 **须六项接口一致**) |
234
 | **ST-C3** | **不** 含运费、**不** 按金额 |
233
 | **ST-C3** | **不** 含运费、**不** 按金额 |
235
 
234
 
236
 ### 5.2 热销农资排行(前 5)
235
 ### 5.2 热销农资排行(前 5)
237
 
236
 
238
-**业务含义:** 展示 **统计** **销售件数最多** 的 **前 5 个平台一级品类**(草稿「根据农资品类分类统计各个品类销售量,展示销售量前 5」),供 **排行列表 / 条形图** 渲染。
237
+**业务含义:** 展示 **统计** **销售件数最多** 的 **前 5 个平台一级品类**(草稿「根据农资品类分类统计各个品类销售量,展示销售量前 5」),供 **排行列表 / 条形图** 渲染。
239
 
238
 
240
-> **与 §5.1 的关系:** 二者 **统计口径相同**(均为统计、已完成订单、归并至 **一级品类** 的销售件数)。**§5.1** 面向 **全量品类结构 + 占比(饼图)**;**§5.2** 面向 **销量 Top5 品类排行**,**不重复输出占比**(排行以 **销售件数** 为主字段)。
239
+> **与 §5.1 的关系:** 二者 **统计口径相同**(均为统计、已完成订单、归并至 **一级品类** 的销售件数)。**§5.1** 面向 **全量品类结构 + 占比(饼图)**;**§5.2** 面向 **销量 Top5 品类排行**,**不重复输出占比**(排行以 **销售件数** 为主字段)。
241
 
240
 
242
 **统计逻辑:**
241
 **统计逻辑:**
243
 
242
 
244
 ```text
243
 ```text
245
 与 §5.1 相同,先得到各一级品类 categoryQty
244
 与 §5.1 相同,先得到各一级品类 categoryQty
246
     → 按 categoryQty 降序排序
245
     → 按 categoryQty 降序排序
247
-    → 取 Top5(当有销量的品类不足 5 个则全部返回)
246
+    → 取 Top5(当有销量的品类不足 5 个则全部返回)
248
     → 输出:排名序号、一级品类 ID/名称、销售件数
247
     → 输出:排名序号、一级品类 ID/名称、销售件数
249
 ```
248
 ```
250
 
249
 
@@ -252,7 +251,7 @@
252
 |------|------|
251
 |------|------|
253
 | **ST-H1** | **仅一级品类**;**不** 输出二级、**不** 输出单品(商品) |
252
 | **ST-H1** | **仅一级品类**;**不** 输出二级、**不** 输出单品(商品) |
254
 | **ST-H2** | **并列第 5** 时:按 **一级品类 ID 升序** 取满 5 条(**或** 全部并列返回导致 >5 条,产品 **择一** 并 **文档化**) |
253
 | **ST-H2** | **并列第 5** 时:按 **一级品类 ID 升序** 取满 5 条(**或** 全部并列返回导致 >5 条,产品 **择一** 并 **文档化**) |
255
-| **ST-H3** | 某品类当 **0 件** **不参与** 排行 |
254
+| **ST-H3** | 某品类当 **0 件** **不参与** 排行 |
256
 | **ST-H4** | **不** 输出店铺、商户、商品明细 |
255
 | **ST-H4** | **不** 输出店铺、商户、商品明细 |
257
 
256
 
258
 ### 5.3 商城订单趋势
257
 ### 5.3 商城订单趋势
@@ -370,7 +369,7 @@
370
 |----|------|
369
 |----|------|
371
 | 场景 | 外部大屏 **定时刷新**(如 1~5 分钟)可能 **并发** 拉取 |
370
 | 场景 | 外部大屏 **定时刷新**(如 1~5 分钟)可能 **并发** 拉取 |
372
 | 要求 | 单次请求 **应在可接受等待时间内** 返回完整结果;**不** 因统计查询 **阻塞** 下单/支付/发货等 **交易主流程** |
371
 | 要求 | 单次请求 **应在可接受等待时间内** 返回完整结果;**不** 因统计查询 **阻塞** 下单/支付/发货等 **交易主流程** |
373
-| 策略 | 允许 **预聚合、缓存** 统计结果;**当日/当年** 指标 **准实时** 即可(完成订单后 **下一次刷新周期内** 可见) |
372
+| 策略 | 允许 **预聚合、缓存** 统计结果;**统计年** 指标 **准实时** 即可(完成订单后 **下一次刷新周期内** 可见) |
374
 | 降级 | 极端情况下 **可返回空数据或上次缓存** 并 **记录告警**(**不** 返回错误业务数) |
373
 | 降级 | 极端情况下 **可返回空数据或上次缓存** 并 **记录告警**(**不** 返回错误业务数) |
375
 
374
 
376
 ---
375
 ---
@@ -391,7 +390,7 @@
391
 
390
 
392
 | 指标 | 建议刷新频率 |
391
 | 指标 | 建议刷新频率 |
393
 |------|--------------|
392
 |------|--------------|
394
-| 品类销售、热销 Top5 | **≤5 分钟**(统计日指标) |
393
+| 品类销售、热销 Top5 | **≤1 小时**(统计年指标) |
395
 | 订单趋势、店铺入驻、区域 Top5 | **≤1 小时**(统计年指标) |
394
 | 订单趋势、店铺入驻、区域 Top5 | **≤1 小时**(统计年指标) |
396
 | 评价词云 | **≤24 小时**(全量历史,变化慢) |
395
 | 评价词云 | **≤24 小时**(全量历史,变化慢) |
397
 
396
 
@@ -401,7 +400,7 @@
401
 
400
 
402
 | 事件 | 影响 |
401
 | 事件 | 影响 |
403
 |------|------|
402
 |------|------|
404
-| 订单完成后 | **当日/当年(完成侧)** 指标 **下一刷新周期** 纳入;**含系统自动确认收货** |
403
+| 订单完成后 | **统计年(完成侧)** 指标 **下一刷新周期** 纳入;**含系统自动确认收货** |
405
 | 订单改 **已删除** | 从 **订单趋势** 等 **剔除**(若实现按当前状态重算) |
404
 | 订单改 **已删除** | 从 **订单趋势** 等 **剔除**(若实现按当前状态重算) |
406
 | 店铺逻辑删除 | **入驻统计不回退**;**不影响** 历史订单统计 |
405
 | 店铺逻辑删除 | **入驻统计不回退**;**不影响** 历史订单统计 |
407
 | 分类调整层级 | **历史商品** **仍按档案 category_id** 归并 |
406
 | 分类调整层级 | **历史商品** **仍按档案 category_id** 归并 |
@@ -438,10 +437,10 @@
438
 
437
 
439
 | 编号 | 场景 | 预期 |
438
 | 编号 | 场景 | 预期 |
440
 |------|------|------|
439
 |------|------|------|
441
-| **ST-TST1** | 统计有 3 个一级品类完成销量 | 饼图 **三项占比合计 ≈100%** |
442
-| **ST-TST2** | 某一级品类当销量排第 6 | **不出现在** Top5 排行 |
443
-| **ST-TST3** | 3 月下单、4 月手动完成 | **订单趋势计 3 月**;**当日销量计 4 月完成日** |
444
-| **ST-TST3a** | 已发货超期 **自动确认收货** 于 5 月 2 日完成 | **当日销量/区域金额** 计 **5 月 2 日**(**非** 发货日) |
440
+| **ST-TST1** | 统计有 3 个一级品类完成销量 | 饼图 **三项占比合计 ≈100%** |
441
+| **ST-TST2** | 某一级品类当销量排第 6 | **不出现在** Top5 排行 |
442
+| **ST-TST3** | 3 月下单、4 月手动完成 | **订单趋势计 3 月**;**当年销量计 4 月完成年** |
443
+| **ST-TST3a** | 已发货超期 **自动确认收货** 于 5 月 2 日完成 | **当年销量/区域金额** 计 **完成年**(**非** 发货日) |
445
 | **ST-TST4** | 2 月创建店铺 | **入驻饼图 2 月扇区 +1** |
444
 | **ST-TST4** | 2 月创建店铺 | **入驻饼图 2 月扇区 +1** |
446
 | **ST-TST5** | 北京、上海已完成实付 | **区域排名** 按 **万元** 降序 **Top5** |
445
 | **ST-TST5** | 北京、上海已完成实付 | **区域排名** 按 **万元** 降序 **Top5** |
447
 | **ST-TST6** | 100 条含「质量好」评价 | 词云 **含「质量好」** 且频次 **合理** |
446
 | **ST-TST6** | 100 条含「质量好」评价 | 词云 **含「质量好」** 且频次 **合理** |
@@ -458,5 +457,6 @@
458
 | **v1.0.1** | §5.2 热销排行改为 **一级品类 Top5**,与草稿及 §5.1 口径一致 |
457
 | **v1.0.1** | §5.2 热销排行改为 **一级品类 Top5**,与草稿及 §5.1 口径一致 |
459
 | **v1.0.2** | 完成时间 **含自动确认收货**;新增 §2.5、ST-R3、ST-TST3a |
458
 | **v1.0.2** | 完成时间 **含自动确认收货**;新增 §2.5、ST-R3、ST-TST3a |
460
 | **v1.0.3** | Token 认证改为 **调用方 AES 加密 UUID**;ST-T2/T3、§7 流程与边界对齐技术方案 v1.1 |
459
 | **v1.0.3** | Token 认证改为 **调用方 AES 加密 UUID**;ST-T2/T3、§7 流程与边界对齐技术方案 v1.1 |
460
+| **v1.0.4** | §5.1、§5.2 统计口径改为 **当前自然年**(`finish_time` 归属统计年);与草稿一致 |
461
 
461
 
462
-*文档版本:v1.0.3 · 关联《商城数据统计功能需求-草稿》、《关联需求分析.md》v1.6、《商品分类功能需求》v1.5、《订单管理功能需求》v1.0.1、《商城设置功能需求》v1.1、《店铺管理功能需求》v1.3.5、《评价管理功能需求》、《我的订单功能需求》v1.1*
462
+*文档版本:v1.0.4 · 关联《商城数据统计功能需求-草稿》、《关联需求分析.md》v1.6、《商品分类功能需求》v1.5、《订单管理功能需求》v1.0.1、《商城设置功能需求》v1.1、《店铺管理功能需求》v1.3.5、《评价管理功能需求》、《我的订单功能需求》v1.1*

+ 15 - 15
doc/平台后台/外部接口/商城数据统计技术方案.md

@@ -1,6 +1,6 @@
1
 # 商城数据统计 — 技术方案
1
 # 商城数据统计 — 技术方案
2
 
2
 
3
-> **依据:** 《商城数据统计功能需求.md》v1.0.3  
3
+> **依据:** 《商城数据统计功能需求.md》v1.0.4  
4
 > **关联:** 《订单管理技术方案.md》v1.0.2、《商品分类功能需求》v1.5、《商城设置技术方案.md》v1.1  
4
 > **关联:** 《订单管理技术方案.md》v1.0.2、《商品分类功能需求》v1.5、《商城设置技术方案.md》v1.1  
5
 > **范围:** 对外 **只读** 六项统计;**无** 平台 Web 菜单;**不新建** 业务明细表,**聚合查询** 现有订单/分类/店铺/评价数据。  
5
 > **范围:** 对外 **只读** 六项统计;**无** 平台 Web 菜单;**不新建** 业务明细表,**聚合查询** 现有订单/分类/店铺/评价数据。  
6
 > **原则:** 统计 **不阻塞** 交易写路径;**准实时** + **Redis 缓存**;Token **与** 会员 JWT **隔离**。
6
 > **原则:** 统计 **不阻塞** 交易写路径;**准实时** + **Redis 缓存**;Token **与** 会员 JWT **隔离**。
@@ -103,7 +103,7 @@ biz_order_item oi
103
 
103
 
104
 ```sql
104
 ```sql
105
 o.order_status = '3'
105
 o.order_status = '3'
106
-AND DATE(o.finish_time) = #{statDate}   -- 当天,Asia/Shanghai
106
+AND YEAR(o.finish_time) = #{statYear}   -- 当年,Asia/Shanghai
107
 AND o.order_status <> '5'
107
 AND o.order_status <> '5'
108
 ```
108
 ```
109
 
109
 
@@ -190,20 +190,19 @@ WHERE del_flag = '0' AND show_flag = '1'
190
 
190
 
191
 | 方法 | 路径 | 功能需求 | 缓存 TTL |
191
 | 方法 | 路径 | 功能需求 | 缓存 TTL |
192
 |------|------|----------|----------|
192
 |------|------|----------|----------|
193
-| GET | `/categorySales` | §5.1 品类销售 | 5 min |
194
-| GET | `/hotCategoryRank` | §5.2 热销 Top5 品类 | 5 min |
193
+| GET | `/categorySales` | §5.1 品类销售 | 1 h |
194
+| GET | `/hotCategoryRank` | §5.2 热销 Top5 品类 | 1 h |
195
 | GET | `/orderTrend` | §5.3 订单趋势 | 1 h |
195
 | GET | `/orderTrend` | §5.3 订单趋势 | 1 h |
196
 | GET | `/shopEntry` | §5.4 店铺入驻 | 1 h |
196
 | GET | `/shopEntry` | §5.4 店铺入驻 | 1 h |
197
 | GET | `/regionRank` | §5.5 区域 Top5 | 1 h |
197
 | GET | `/regionRank` | §5.5 区域 Top5 | 1 h |
198
 | GET | `/reviewWordCloud` | §5.6 词云 Top50 | 24 h |
198
 | GET | `/reviewWordCloud` | §5.6 词云 Top50 | 24 h |
199
 | GET | `/overview` | **组合** 上述六项(大屏一次拉取) | 取各子项最小 TTL |
199
 | GET | `/overview` | **组合** 上述六项(大屏一次拉取) | 取各子项最小 TTL |
200
 
200
 
201
-**Query(可选,默认当天/当年):**
201
+**Query(可选,默认当年):**
202
 
202
 
203
 | 参数 | 适用 | 默认 |
203
 | 参数 | 适用 | 默认 |
204
 |------|------|------|
204
 |------|------|------|
205
-| `statDate` | categorySales、hotCategoryRank | 今天 `yyyy-MM-dd` |
206
-| `statYear` | orderTrend、shopEntry、regionRank | 今年 `yyyy` |
205
+| `statYear` | categorySales、hotCategoryRank、orderTrend、shopEntry、regionRank、overview | 今年 `yyyy` |
207
 
206
 
208
 > v1 **不开放** 任意历史区间(对齐功能需求非本期);参数 **仅用于联调**,生产大屏 **可不传**。
207
 > v1 **不开放** 任意历史区间(对齐功能需求非本期);参数 **仅用于联调**,生产大屏 **可不传**。
209
 
208
 
@@ -213,7 +212,7 @@ WHERE del_flag = '0' AND show_flag = '1'
213
 
212
 
214
 ```json
213
 ```json
215
 {
214
 {
216
-  "statDate": "2026-05-26",
215
+  "statYear": 2026,
217
   "totalQty": 1200,
216
   "totalQty": 1200,
218
   "items": [
217
   "items": [
219
     { "categoryId": 1, "categoryName": "兽药", "qty": 500, "ratio": 41.7 }
218
     { "categoryId": 1, "categoryName": "兽药", "qty": 500, "ratio": 41.7 }
@@ -225,7 +224,7 @@ WHERE del_flag = '0' AND show_flag = '1'
225
 
224
 
226
 ```json
225
 ```json
227
 {
226
 {
228
-  "statDate": "2026-05-26",
227
+  "statYear": 2026,
229
   "items": [
228
   "items": [
230
     { "rank": 1, "categoryId": 1, "categoryName": "兽药", "qty": 500 }
229
     { "rank": 1, "categoryId": 1, "categoryName": "兽药", "qty": 500 }
231
   ]
230
   ]
@@ -347,8 +346,8 @@ Header: X-Open-Token: {token}
347
 
346
 
348
 | Redis Key 示例 | TTL |
347
 | Redis Key 示例 | TTL |
349
 |----------------|-----|
348
 |----------------|-----|
350
-| `openstats:cat:sales:{statDate}` | 5 min |
351
-| `openstats:cat:hot:{statDate}` | 5 min |
349
+| `openstats:cat:sales:{year}` | 1 h |
350
+| `openstats:cat:hot:{year}` | 1 h |
352
 | `openstats:order:month:{year}` | 1 h |
351
 | `openstats:order:month:{year}` | 1 h |
353
 | `openstats:shop:month:{year}` | 1 h |
352
 | `openstats:shop:month:{year}` | 1 h |
354
 | `openstats:region:{year}` | 1 h |
353
 | `openstats:region:{year}` | 1 h |
@@ -382,7 +381,7 @@ Header: X-Open-Token: {token}
382
 | ST-A* | §3.4 + `pay_amount` |
381
 | ST-A* | §3.4 + `pay_amount` |
383
 | ST-RV* | §3.5 + `del_flag=0 AND show_flag=1` |
382
 | ST-RV* | §3.5 + `del_flag=0 AND show_flag=1` |
384
 | ST-T* | Filter + AES 解密 + UUID 校验(`OpenStatsTokenCryptoSupport`) |
383
 | ST-T* | Filter + AES 解密 + UUID 校验(`OpenStatsTokenCryptoSupport`) |
385
-| ST-TST3a | 自动确认写入的 `finish_time` 参与 §3.1 日期过滤 |
384
+| ST-TST3a | 自动确认写入的 `finish_time` 参与 §3.1 **统计年** 过滤 |
386
 
385
 
387
 ---
386
 ---
388
 
387
 
@@ -407,9 +406,9 @@ Header: X-Open-Token: {token}
407
 | 编号 | 场景 |
406
 | 编号 | 场景 |
408
 |------|------|
407
 |------|------|
409
 | OS-T1 | 无 Token / 明文 UUID / 错误密文 → 401 |
408
 | OS-T1 | 无 Token / 明文 UUID / 错误密文 → 401 |
410
-| OS-T2 | 当 3 品类完成单 → 占比合计 ≈100% |
409
+| OS-T2 | 当 3 品类完成单 → 占比合计 ≈100% |
411
 | OS-T3 | 品类销量第 6 → 不在 hotCategoryRank |
410
 | OS-T3 | 品类销量第 6 → 不在 hotCategoryRank |
412
-| OS-T4 | 自动确认 `finish_time` 在今日 → 计入 categorySales |
411
+| OS-T4 | 自动确认 `finish_time` 在 **当年** → 计入 categorySales |
413
 | OS-T5 | `order_status=5` → 各接口均不计 |
412
 | OS-T5 | `order_status=5` → 各接口均不计 |
414
 | OS-T6 | overview 六项结构与单接口一致 |
413
 | OS-T6 | overview 六项结构与单接口一致 |
415
 | OS-T7 | 第二次请求命中 Redis(集成环境可观测) |
414
 | OS-T7 | 第二次请求命中 Redis(集成环境可观测) |
@@ -421,6 +420,7 @@ Header: X-Open-Token: {token}
421
 | 版本 | 说明 |
420
 | 版本 | 说明 |
422
 |------|------|
421
 |------|------|
423
 | **v1.0** | 首版:RuoYi3.9.2 + MySQL5.7;复用业务表聚合;六项 Open API + overview;Redis 缓存 |
422
 | **v1.0** | 首版:RuoYi3.9.2 + MySQL5.7;复用业务表聚合;六项 Open API + overview;Redis 缓存 |
423
+| **v1.2** | §5.1/§5.2 改为 **统计年**(`YEAR(finish_time)`);接口参数统一 `statYear`;缓存 key/TTL 对齐年度指标 |
424
 | **v1.1** | Token 改为 **AES-128-CBC 加密 UUID** 校验;**移除** `biz_open_api_token` 库表方案 |
424
 | **v1.1** | Token 改为 **AES-128-CBC 加密 UUID** 校验;**移除** `biz_open_api_token` 库表方案 |
425
 
425
 
426
-*文档版本:v1.1 · 关联《商城数据统计功能需求.md》v1.0.3 · 统计索引:`sql/biz_order.sql` → `idx_stats_finish`*
426
+*文档版本:v1.2 · 关联《商城数据统计功能需求.md》v1.0.4 · 统计索引:`sql/biz_order.sql` → `idx_stats_finish`*

+ 30 - 0
ruoyi-common/src/main/java/com/ruoyi/common/filter/PageHelperClearFilter.java

@@ -0,0 +1,30 @@
1
+package com.ruoyi.common.filter;
2
+
3
+import java.io.IOException;
4
+import javax.servlet.Filter;
5
+import javax.servlet.FilterChain;
6
+import javax.servlet.ServletException;
7
+import javax.servlet.ServletRequest;
8
+import javax.servlet.ServletResponse;
9
+import com.github.pagehelper.PageHelper;
10
+
11
+/**
12
+ * 清理 PageHelper 线程变量,避免 Tomcat 线程复用导致分页参数泄漏到后续请求。
13
+ */
14
+public class PageHelperClearFilter implements Filter
15
+{
16
+    @Override
17
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
18
+            throws IOException, ServletException
19
+    {
20
+        PageHelper.clearPage();
21
+        try
22
+        {
23
+            chain.doFilter(request, response);
24
+        }
25
+        finally
26
+        {
27
+            PageHelper.clearPage();
28
+        }
29
+    }
30
+}

+ 14 - 0
ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java

@@ -9,6 +9,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
9
 import org.springframework.context.annotation.Bean;
9
 import org.springframework.context.annotation.Bean;
10
 import org.springframework.context.annotation.Configuration;
10
 import org.springframework.context.annotation.Configuration;
11
 import com.ruoyi.common.constant.Constants;
11
 import com.ruoyi.common.constant.Constants;
12
+import com.ruoyi.common.filter.PageHelperClearFilter;
12
 import com.ruoyi.common.filter.RefererFilter;
13
 import com.ruoyi.common.filter.RefererFilter;
13
 import com.ruoyi.common.filter.RepeatableFilter;
14
 import com.ruoyi.common.filter.RepeatableFilter;
14
 import com.ruoyi.common.filter.XssFilter;
15
 import com.ruoyi.common.filter.XssFilter;
@@ -65,6 +66,19 @@ public class FilterConfig
65
         return registration;
66
         return registration;
66
     }
67
     }
67
 
68
 
69
+    @SuppressWarnings({ "rawtypes", "unchecked" })
70
+    @Bean
71
+    public FilterRegistrationBean pageHelperClearFilterRegistration()
72
+    {
73
+        FilterRegistrationBean registration = new FilterRegistrationBean();
74
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
75
+        registration.setFilter(new PageHelperClearFilter());
76
+        registration.addUrlPatterns("/*");
77
+        registration.setName("pageHelperClearFilter");
78
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
79
+        return registration;
80
+    }
81
+
68
     @SuppressWarnings({ "rawtypes", "unchecked" })
82
     @SuppressWarnings({ "rawtypes", "unchecked" })
69
     @Bean
83
     @Bean
70
     public FilterRegistrationBean someFilterRegistration()
84
     public FilterRegistrationBean someFilterRegistration()