Pārlūkot izejas kodu

会员管理代码

wwh 1 nedēļu atpakaļ
vecāks
revīzija
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 25
     public static final String UNKNOWN_CITY = "未知";
26 26
 
27
-    public static final String DATE_PATTERN = "yyyy-MM-dd";
28
-
29 27
     public static final String YEAR_PATTERN = "yyyy";
30 28
 
31
-    public static final int CACHE_TTL_CATEGORY_MIN = 5;
32
-
33 29
     public static final int CACHE_TTL_YEARLY_HOUR = 1;
34 30
 
35 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 23
     @Anonymous
24 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 30
     @Anonymous
31 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 37
     @Anonymous
@@ -64,10 +64,8 @@ public class MallStatsOpenController extends BaseController
64 64
 
65 65
     @Anonymous
66 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 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 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 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 20
     OrderTrendVO getOrderTrend(String statYear);
21 21
 
@@ -25,5 +25,5 @@ public interface IMallStatsOpenService
25 25
 
26 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 4
 import java.math.RoundingMode;
5 5
 import java.time.LocalDate;
6 6
 import java.time.ZoneId;
7
-import java.time.format.DateTimeFormatter;
8
-import java.time.format.DateTimeParseException;
9 7
 import java.util.ArrayList;
10 8
 import java.util.Collections;
11 9
 import java.util.Comparator;
@@ -44,8 +42,6 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
44 42
 {
45 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 45
     @Autowired
50 46
     private MallStatsOpenMapper statsMapper;
51 47
 
@@ -56,34 +52,34 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
56 52
     private ReviewWordCloudSupport wordCloudSupport;
57 53
 
58 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 59
         CategorySalesVO cached = redisCache.getCacheObject(cacheKey);
64 60
         if (cached != null)
65 61
         {
66 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 67
         return vo;
72 68
     }
73 69
 
74 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 75
         HotCategoryRankVO cached = redisCache.getCacheObject(cacheKey);
80 76
         if (cached != null)
81 77
         {
82 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 83
         return vo;
88 84
     }
89 85
 
@@ -151,11 +147,11 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
151 147
     }
152 148
 
153 149
     @Override
154
-    public StatsOverviewVO getOverview(String statDate, String statYear)
150
+    public StatsOverviewVO getOverview(String statYear)
155 151
     {
156 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 155
         overview.setOrderTrend(getOrderTrend(statYear));
160 156
         overview.setShopEntry(getShopEntry(statYear));
161 157
         overview.setRegionRank(getRegionRank(statYear));
@@ -163,10 +159,10 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
163 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 164
         CategorySalesVO vo = new CategorySalesVO();
169
-        vo.setStatDate(statDate);
165
+        vo.setStatYear(statYear);
170 166
         long totalQty = 0L;
171 167
         List<CategorySalesItemVO> items = new ArrayList<>();
172 168
         for (CategoryQtyRow row : rows)
@@ -186,12 +182,12 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
186 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 187
         rows.sort(Comparator.comparing(CategoryQtyRow::getQty, Comparator.nullsFirst(Long::compareTo)).reversed()
192 188
                 .thenComparing(row -> row.getCategoryId() != null ? row.getCategoryId() : OpenStatsConstants.UNCATEGORIZED_ID));
193 189
         HotCategoryRankVO vo = new HotCategoryRankVO();
194
-        vo.setStatDate(statDate);
190
+        vo.setStatYear(statYear);
195 191
         int limit = Math.min(OpenStatsConstants.HOT_CATEGORY_LIMIT, rows.size());
196 192
         for (int i = 0; i < limit; i++)
197 193
         {
@@ -363,22 +359,6 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
363 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 362
     private int resolveStatYear(String statYear)
383 363
     {
384 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 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 6
 public class CategorySalesVO
7 7
 {
8
-    private String statDate;
8
+    private int statYear;
9 9
 
10 10
     private long totalQty;
11 11
 
12 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 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 6
 public class HotCategoryRankVO
7 7
 {
8
-    private String statDate;
8
+    private int statYear;
9 9
 
10 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 22
     public List<HotCategoryRankItemVO> getItems()

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

@@ -2,7 +2,7 @@
2 2
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3 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 6
         select
7 7
             ifnull(c1.category_id, 0) as categoryId,
8 8
             ifnull(c1.category_name, '未分类') as categoryName,
@@ -15,7 +15,7 @@
15 15
         where o.order_status = '3'
16 16
           and o.order_status &lt;&gt; '5'
17 17
           and o.finish_time is not null
18
-          and date(o.finish_time) = #{statDate}
18
+          and year(o.finish_time) = #{statYear}
19 19
         group by ifnull(c1.category_id, 0), ifnull(c1.category_name, '未分类')
20 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 42
 @ExtendWith(MockitoExtension.class)
43 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 47
     @Mock
48 48
     private MallStatsOpenMapper statsMapper;
@@ -65,26 +65,26 @@ class MallStatsOpenServiceImplTest
65 65
     @Test
66 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 69
                 row(1L, "兽药", 500L),
70 70
                 row(2L, "饲料", 400L),
71 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 76
         assertEquals(1200L, vo.getTotalQty());
77 77
         assertEquals(3, vo.getItems().size());
78 78
         double ratioSum = vo.getItems().stream().mapToDouble(i -> i.getRatio()).sum();
79 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 84
     @Test
85 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 88
                 row(1L, "A", 100L),
89 89
                 row(2L, "B", 90L),
90 90
                 row(3L, "C", 80L),
@@ -92,8 +92,9 @@ class MallStatsOpenServiceImplTest
92 92
                 row(5L, "E", 60L),
93 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 98
         assertEquals(5, vo.getItems().size());
98 99
         assertEquals(1, vo.getItems().get(0).getRank());
99 100
         assertEquals("A", vo.getItems().get(0).getCategoryName());
@@ -164,14 +165,14 @@ class MallStatsOpenServiceImplTest
164 165
     @Test
165 166
     void overview_containsAllSections()
166 167
     {
167
-        when(statsMapper.selectCategoryQtyByFinishDate(anyString())).thenReturn(Collections.emptyList());
168
+        when(statsMapper.selectCategoryQtyByFinishYear(anyInt())).thenReturn(Collections.emptyList());
168 169
         when(statsMapper.selectOrderCountByYear(anyInt())).thenReturn(Collections.emptyList());
169 170
         when(statsMapper.selectShopCountByYear(anyInt())).thenReturn(Collections.emptyList());
170 171
         when(statsMapper.selectRegionAmountByYear(anyInt())).thenReturn(Collections.emptyList());
171 172
         when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.emptyList());
172 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 177
         assertNotNull(overview.getCategorySales());
177 178
         assertNotNull(overview.getHotCategoryRank());
@@ -179,7 +180,7 @@ class MallStatsOpenServiceImplTest
179 180
         assertNotNull(overview.getShopEntry());
180 181
         assertNotNull(overview.getRegionRank());
181 182
         assertNotNull(overview.getReviewWordCloud());
182
-        verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishDate(STAT_DATE);
183
+        verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishYear(STAT_YEAR);
183 184
     }
184 185
 
185 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 5
 3. 商城订单趋势: 按月统计当前年份每月的商城订单数(单),曲线图展示;
6 6
 4. 店铺入驻:按月统计当前年份每月入驻商城的店铺数(家),饼状图展示;
7 7
 5. 消费区域排名(前5): 根据农资商城的订单的收货城市分类统计各个城市的农资销售额(万),展示前5的消费区域;

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

@@ -5,7 +5,7 @@
5 5
 > **说明:** 本文档 **仅描述功能需求、统计口径与业务规则**;**不涉及** 数据库结构、接口路径、Token 实现机制及技术栈细节。  
6 6
 > **v1.0:** 首版定稿;对齐草稿 §1~§7;补全时间口径、分类归属、订单/店铺/评价边界与空数据规则;排除草稿 §8~§9。  
7 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 22
 | 3 | **商城订单趋势** | 折线:当年 **逐月订单笔数** |
23 23
 | 4 | **店铺入驻** | 饼图:当年 **逐月新入驻店铺** 占比 |
24 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 45
 | §3 商城订单趋势 · 按月 · 当年 · 折线 | §5.3 |
46 46
 | §4 店铺入驻 · 按月 · 当年 · 饼图 | §5.4 |
47 47
 | §5 消费区域排名 · 前 5 · 收货城市 · 销售额(万) | §5.5 |
@@ -67,9 +67,9 @@
67 67
 | 关联模块 | 关系 | 边界说明 |
68 68
 |----------|------|----------|
69 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 73
 | **店铺管理** v1.3.5 | **强关联** | 入驻统计 **以店铺创建时间** 为准;含 **入驻审核通过建店** 与 **平台代开店** |
74 74
 | **评价管理** | **口径依赖** | 词云 **仅 C 端可见、未删除** 的买家评价 **正文** |
75 75
 | **会员管理 / 资金概览** | **无直接** | 本模块 **不输出** 会员数、平台资金 |
@@ -99,10 +99,10 @@
99 99
 | 项 | 定稿 |
100 100
 |----|------|
101 101
 | 订单笔数(§5.3) | 按 **下单时间** 归属自然月;**排除已删除**;**含** 待支付/待发货/已发货/已完成/已关闭 |
102
-| 当日销量(§5.1、§5.2) | 按订单 **完成时间** 归属 **自然日**(**含手动确认** 与 **自动确认收货**);统计 **订单商品行销售件数** 之和 |
102
+| 当年销量(§5.1、§5.2) | 按订单 **完成时间** 归属 **自然年**(**含手动确认** 与 **自动确认收货**);统计 **订单商品行销售件数** 之和 |
103 103
 | 区域销售额(§5.5) | 按订单 **完成时间** 归属 **自然年**(**含手动确认** 与 **自动确认收货**);金额 = 订单 **实付金额**(与 O11 **累计消费金额** 同口径) |
104
-| 未完成订单 | **不计入** 当销量与区域销售额 |
105
-| 已关闭 | **不计入** 当销量与区域销售额;**可计入** 订单趋势笔数(若当月曾下单且未删) |
104
+| 未完成订单 | **不计入** 当销量与区域销售额 |
105
+| 已关闭 | **不计入** 当销量与区域销售额;**可计入** 订单趋势笔数(若当月曾下单且未删) |
106 106
 
107 107
 ### 2.3 与平台《商品分类功能需求》
108 108
 
@@ -127,7 +127,7 @@
127 127
 | 项 | 定稿 |
128 128
 |----|------|
129 129
 | 完成路径 | 订单 **已完成** 的 **完成时间** 来源:**买家手动确认收货** 或 **系统自动确认收货**(MC-O1~O3),**二者等价** |
130
-| 统计归属 | 以 **完成时间的日历日/日历年** 归属 **统计日**(§5.1、§5.2)或 **统计年完成侧指标**(§5.5);**不** 以发货时间代替 |
130
+| 统计归属 | 以 **完成时间的日历年** 归属 **统计年**(§5.1、§5.2、§5.5);**不** 以发货时间代替 |
131 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 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 187
 | 当年某月无订单/无新店 | 订单趋势/入驻 **该月记 0**,**仍返回 12 个月** |
189 188
 | 评价正文均空或无评价 | 词云 **空列表** |
190 189
 
@@ -205,12 +204,12 @@
205 204
 
206 205
 ### 5.1 农资品类销售
207 206
 
208
-**业务含义:** 展示 **统计日** 各 **平台一级品类** 的 **销售件数** 及 **占当日总销量比重**,供饼图渲染。
207
+**业务含义:** 展示 **统计年** 各 **平台一级品类** 的 **销售件数** 及 **占当年总销量比重**,供饼图渲染。
209 208
 
210 209
 **统计逻辑:**
211 210
 
212 211
 ```text
213
-取统计内「已完成」的全部订单(完成时间 = 手动确认 或 自动确认收货写入时刻)
212
+取统计内「已完成」的全部订单(完成时间 = 手动确认 或 自动确认收货写入时刻)
214 213
     → 展开订单商品行(goods_id、购买数量 qty)
215 214
     → 按 goods 的 category_id 找到平台二级分类
216 215
     → 归并至父级「一级品类」
@@ -230,21 +229,21 @@
230 229
 | 编号 | 规则 |
231 230
 |------|------|
232 231
 | **ST-C1** | **仅一级品类**;**不** 输出二级明细 |
233
-| **ST-C2** | 某品类当 **0 件** → **可不返回该项** 或 **返回 0**(实现 **须六项接口一致**) |
232
+| **ST-C2** | 某品类当 **0 件** → **可不返回该项** 或 **返回 0**(实现 **须六项接口一致**) |
234 233
 | **ST-C3** | **不** 含运费、**不** 按金额 |
235 234
 
236 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 243
 ```text
245 244
 与 §5.1 相同,先得到各一级品类 categoryQty
246 245
     → 按 categoryQty 降序排序
247
-    → 取 Top5(当有销量的品类不足 5 个则全部返回)
246
+    → 取 Top5(当有销量的品类不足 5 个则全部返回)
248 247
     → 输出:排名序号、一级品类 ID/名称、销售件数
249 248
 ```
250 249
 
@@ -252,7 +251,7 @@
252 251
 |------|------|
253 252
 | **ST-H1** | **仅一级品类**;**不** 输出二级、**不** 输出单品(商品) |
254 253
 | **ST-H2** | **并列第 5** 时:按 **一级品类 ID 升序** 取满 5 条(**或** 全部并列返回导致 >5 条,产品 **择一** 并 **文档化**) |
255
-| **ST-H3** | 某品类当 **0 件** **不参与** 排行 |
254
+| **ST-H3** | 某品类当 **0 件** **不参与** 排行 |
256 255
 | **ST-H4** | **不** 输出店铺、商户、商品明细 |
257 256
 
258 257
 ### 5.3 商城订单趋势
@@ -370,7 +369,7 @@
370 369
 |----|------|
371 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 394
 | 订单趋势、店铺入驻、区域 Top5 | **≤1 小时**(统计年指标) |
396 395
 | 评价词云 | **≤24 小时**(全量历史,变化慢) |
397 396
 
@@ -401,7 +400,7 @@
401 400
 
402 401
 | 事件 | 影响 |
403 402
 |------|------|
404
-| 订单完成后 | **当日/当年(完成侧)** 指标 **下一刷新周期** 纳入;**含系统自动确认收货** |
403
+| 订单完成后 | **统计年(完成侧)** 指标 **下一刷新周期** 纳入;**含系统自动确认收货** |
405 404
 | 订单改 **已删除** | 从 **订单趋势** 等 **剔除**(若实现按当前状态重算) |
406 405
 | 店铺逻辑删除 | **入驻统计不回退**;**不影响** 历史订单统计 |
407 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 444
 | **ST-TST4** | 2 月创建店铺 | **入驻饼图 2 月扇区 +1** |
446 445
 | **ST-TST5** | 北京、上海已完成实付 | **区域排名** 按 **万元** 降序 **Top5** |
447 446
 | **ST-TST6** | 100 条含「质量好」评价 | 词云 **含「质量好」** 且频次 **合理** |
@@ -458,5 +457,6 @@
458 457
 | **v1.0.1** | §5.2 热销排行改为 **一级品类 Top5**,与草稿及 §5.1 口径一致 |
459 458
 | **v1.0.2** | 完成时间 **含自动确认收货**;新增 §2.5、ST-R3、ST-TST3a |
460 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 4
 > **关联:** 《订单管理技术方案.md》v1.0.2、《商品分类功能需求》v1.5、《商城设置技术方案.md》v1.1  
5 5
 > **范围:** 对外 **只读** 六项统计;**无** 平台 Web 菜单;**不新建** 业务明细表,**聚合查询** 现有订单/分类/店铺/评价数据。  
6 6
 > **原则:** 统计 **不阻塞** 交易写路径;**准实时** + **Redis 缓存**;Token **与** 会员 JWT **隔离**。
@@ -103,7 +103,7 @@ biz_order_item oi
103 103
 
104 104
 ```sql
105 105
 o.order_status = '3'
106
-AND DATE(o.finish_time) = #{statDate}   -- 当天,Asia/Shanghai
106
+AND YEAR(o.finish_time) = #{statYear}   -- 当年,Asia/Shanghai
107 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 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 195
 | GET | `/orderTrend` | §5.3 订单趋势 | 1 h |
196 196
 | GET | `/shopEntry` | §5.4 店铺入驻 | 1 h |
197 197
 | GET | `/regionRank` | §5.5 区域 Top5 | 1 h |
198 198
 | GET | `/reviewWordCloud` | §5.6 词云 Top50 | 24 h |
199 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 207
 > v1 **不开放** 任意历史区间(对齐功能需求非本期);参数 **仅用于联调**,生产大屏 **可不传**。
209 208
 
@@ -213,7 +212,7 @@ WHERE del_flag = '0' AND show_flag = '1'
213 212
 
214 213
 ```json
215 214
 {
216
-  "statDate": "2026-05-26",
215
+  "statYear": 2026,
217 216
   "totalQty": 1200,
218 217
   "items": [
219 218
     { "categoryId": 1, "categoryName": "兽药", "qty": 500, "ratio": 41.7 }
@@ -225,7 +224,7 @@ WHERE del_flag = '0' AND show_flag = '1'
225 224
 
226 225
 ```json
227 226
 {
228
-  "statDate": "2026-05-26",
227
+  "statYear": 2026,
229 228
   "items": [
230 229
     { "rank": 1, "categoryId": 1, "categoryName": "兽药", "qty": 500 }
231 230
   ]
@@ -347,8 +346,8 @@ Header: X-Open-Token: {token}
347 346
 
348 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 351
 | `openstats:order:month:{year}` | 1 h |
353 352
 | `openstats:shop:month:{year}` | 1 h |
354 353
 | `openstats:region:{year}` | 1 h |
@@ -382,7 +381,7 @@ Header: X-Open-Token: {token}
382 381
 | ST-A* | §3.4 + `pay_amount` |
383 382
 | ST-RV* | §3.5 + `del_flag=0 AND show_flag=1` |
384 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 408
 | OS-T1 | 无 Token / 明文 UUID / 错误密文 → 401 |
410
-| OS-T2 | 当 3 品类完成单 → 占比合计 ≈100% |
409
+| OS-T2 | 当 3 品类完成单 → 占比合计 ≈100% |
411 410
 | OS-T3 | 品类销量第 6 → 不在 hotCategoryRank |
412
-| OS-T4 | 自动确认 `finish_time` 在今日 → 计入 categorySales |
411
+| OS-T4 | 自动确认 `finish_time` 在 **当年** → 计入 categorySales |
413 412
 | OS-T5 | `order_status=5` → 各接口均不计 |
414 413
 | OS-T6 | overview 六项结构与单接口一致 |
415 414
 | OS-T7 | 第二次请求命中 Redis(集成环境可观测) |
@@ -421,6 +420,7 @@ Header: X-Open-Token: {token}
421 420
 | 版本 | 说明 |
422 421
 |------|------|
423 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 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 9
 import org.springframework.context.annotation.Bean;
10 10
 import org.springframework.context.annotation.Configuration;
11 11
 import com.ruoyi.common.constant.Constants;
12
+import com.ruoyi.common.filter.PageHelperClearFilter;
12 13
 import com.ruoyi.common.filter.RefererFilter;
13 14
 import com.ruoyi.common.filter.RepeatableFilter;
14 15
 import com.ruoyi.common.filter.XssFilter;
@@ -65,6 +66,19 @@ public class FilterConfig
65 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 82
     @SuppressWarnings({ "rawtypes", "unchecked" })
69 83
     @Bean
70 84
     public FilterRegistrationBean someFilterRegistration()