Procházet zdrojové kódy

会员管理代码

wwh před 1 týdnem
rodič
revize
db8610b134

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

@@ -10,6 +10,7 @@ import java.util.Comparator;
10
 import java.util.HashMap;
10
 import java.util.HashMap;
11
 import java.util.List;
11
 import java.util.List;
12
 import java.util.Map;
12
 import java.util.Map;
13
+import java.util.concurrent.ConcurrentHashMap;
13
 import java.util.concurrent.TimeUnit;
14
 import java.util.concurrent.TimeUnit;
14
 import org.springframework.beans.factory.annotation.Autowired;
15
 import org.springframework.beans.factory.annotation.Autowired;
15
 import org.springframework.stereotype.Service;
16
 import org.springframework.stereotype.Service;
@@ -42,6 +43,8 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
42
 {
43
 {
43
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
44
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
44
 
45
 
46
+    private final ConcurrentHashMap<Integer, Object> yearlyRefreshLocks = new ConcurrentHashMap<>();
47
+
45
     @Autowired
48
     @Autowired
46
     private MallStatsOpenMapper statsMapper;
49
     private MallStatsOpenMapper statsMapper;
47
 
50
 
@@ -55,110 +58,161 @@ public class MallStatsOpenServiceImpl implements IMallStatsOpenService
55
     public CategorySalesVO getCategorySales(String statYear)
58
     public CategorySalesVO getCategorySales(String statYear)
56
     {
59
     {
57
         int year = resolveStatYear(statYear);
60
         int year = resolveStatYear(statYear);
58
-        String cacheKey = StatsCacheKeys.categorySales(year);
59
-        CategorySalesVO cached = redisCache.getCacheObject(cacheKey);
60
-        if (cached != null)
61
-        {
62
-            return cached;
63
-        }
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);
67
-        return vo;
61
+        ensureYearlyStatsCached(year);
62
+        return redisCache.getCacheObject(StatsCacheKeys.categorySales(year));
68
     }
63
     }
69
 
64
 
70
     @Override
65
     @Override
71
     public HotCategoryRankVO getHotCategoryRank(String statYear)
66
     public HotCategoryRankVO getHotCategoryRank(String statYear)
72
     {
67
     {
73
         int year = resolveStatYear(statYear);
68
         int year = resolveStatYear(statYear);
74
-        String cacheKey = StatsCacheKeys.hotCategory(year);
75
-        HotCategoryRankVO cached = redisCache.getCacheObject(cacheKey);
76
-        if (cached != null)
77
-        {
78
-            return cached;
79
-        }
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);
83
-        return vo;
69
+        ensureYearlyStatsCached(year);
70
+        return redisCache.getCacheObject(StatsCacheKeys.hotCategory(year));
84
     }
71
     }
85
 
72
 
86
     @Override
73
     @Override
87
     public OrderTrendVO getOrderTrend(String statYear)
74
     public OrderTrendVO getOrderTrend(String statYear)
88
     {
75
     {
89
         int year = resolveStatYear(statYear);
76
         int year = resolveStatYear(statYear);
90
-        String cacheKey = StatsCacheKeys.orderTrend(year);
91
-        OrderTrendVO cached = redisCache.getCacheObject(cacheKey);
92
-        if (cached != null)
93
-        {
94
-            return cached;
95
-        }
96
-        List<MonthCountRow> rows = safeList(statsMapper.selectOrderCountByYear(year));
97
-        OrderTrendVO vo = buildOrderTrend(year, rows);
98
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
99
-        return vo;
77
+        ensureYearlyStatsCached(year);
78
+        return redisCache.getCacheObject(StatsCacheKeys.orderTrend(year));
100
     }
79
     }
101
 
80
 
102
     @Override
81
     @Override
103
     public ShopEntryVO getShopEntry(String statYear)
82
     public ShopEntryVO getShopEntry(String statYear)
104
     {
83
     {
105
         int year = resolveStatYear(statYear);
84
         int year = resolveStatYear(statYear);
106
-        String cacheKey = StatsCacheKeys.shopEntry(year);
107
-        ShopEntryVO cached = redisCache.getCacheObject(cacheKey);
108
-        if (cached != null)
109
-        {
110
-            return cached;
111
-        }
112
-        List<MonthCountRow> rows = safeList(statsMapper.selectShopCountByYear(year));
113
-        ShopEntryVO vo = buildShopEntry(year, rows);
114
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
115
-        return vo;
85
+        ensureYearlyStatsCached(year);
86
+        return redisCache.getCacheObject(StatsCacheKeys.shopEntry(year));
116
     }
87
     }
117
 
88
 
118
     @Override
89
     @Override
119
     public RegionRankVO getRegionRank(String statYear)
90
     public RegionRankVO getRegionRank(String statYear)
120
     {
91
     {
121
         int year = resolveStatYear(statYear);
92
         int year = resolveStatYear(statYear);
122
-        String cacheKey = StatsCacheKeys.regionRank(year);
123
-        RegionRankVO cached = redisCache.getCacheObject(cacheKey);
124
-        if (cached != null)
125
-        {
126
-            return cached;
127
-        }
128
-        List<RegionAmountRow> rows = safeList(statsMapper.selectRegionAmountByYear(year));
129
-        RegionRankVO vo = buildRegionRank(year, rows);
130
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
131
-        return vo;
93
+        ensureYearlyStatsCached(year);
94
+        return redisCache.getCacheObject(StatsCacheKeys.regionRank(year));
132
     }
95
     }
133
 
96
 
134
     @Override
97
     @Override
135
     public ReviewWordCloudVO getReviewWordCloud()
98
     public ReviewWordCloudVO getReviewWordCloud()
136
     {
99
     {
137
-        String cacheKey = StatsCacheKeys.wordCloud();
138
-        ReviewWordCloudVO cached = redisCache.getCacheObject(cacheKey);
139
-        if (cached != null)
140
-        {
141
-            return cached;
142
-        }
143
-        List<String> contents = safeList(statsMapper.selectReviewContentsForWordCloud());
144
-        ReviewWordCloudVO vo = wordCloudSupport.buildTopWords(contents);
145
-        redisCache.setCacheObject(cacheKey, vo, OpenStatsConstants.CACHE_TTL_WORDCLOUD_HOUR, TimeUnit.HOURS);
146
-        return vo;
100
+        ensureWordCloudCached();
101
+        return redisCache.getCacheObject(StatsCacheKeys.wordCloud());
147
     }
102
     }
148
 
103
 
149
     @Override
104
     @Override
150
     public StatsOverviewVO getOverview(String statYear)
105
     public StatsOverviewVO getOverview(String statYear)
151
     {
106
     {
107
+        int year = resolveStatYear(statYear);
108
+        ensureOverviewStatsCached(year);
152
         StatsOverviewVO overview = new StatsOverviewVO();
109
         StatsOverviewVO overview = new StatsOverviewVO();
153
-        overview.setCategorySales(getCategorySales(statYear));
154
-        overview.setHotCategoryRank(getHotCategoryRank(statYear));
155
-        overview.setOrderTrend(getOrderTrend(statYear));
156
-        overview.setShopEntry(getShopEntry(statYear));
157
-        overview.setRegionRank(getRegionRank(statYear));
158
-        overview.setReviewWordCloud(getReviewWordCloud());
110
+        overview.setCategorySales(redisCache.getCacheObject(StatsCacheKeys.categorySales(year)));
111
+        overview.setHotCategoryRank(redisCache.getCacheObject(StatsCacheKeys.hotCategory(year)));
112
+        overview.setOrderTrend(redisCache.getCacheObject(StatsCacheKeys.orderTrend(year)));
113
+        overview.setShopEntry(redisCache.getCacheObject(StatsCacheKeys.shopEntry(year)));
114
+        overview.setRegionRank(redisCache.getCacheObject(StatsCacheKeys.regionRank(year)));
115
+        overview.setReviewWordCloud(redisCache.getCacheObject(StatsCacheKeys.wordCloud()));
159
         return overview;
116
         return overview;
160
     }
117
     }
161
 
118
 
119
+    private void ensureOverviewStatsCached(int year)
120
+    {
121
+        if (isYearlyStatsCached(year) && isWordCloudCached())
122
+        {
123
+            return;
124
+        }
125
+        Object lock = yearlyRefreshLocks.computeIfAbsent(year, key -> new Object());
126
+        synchronized (lock)
127
+        {
128
+            if (!isYearlyStatsCached(year))
129
+            {
130
+                refreshYearlyStatsCache(year);
131
+            }
132
+            if (!isWordCloudCached())
133
+            {
134
+                refreshWordCloudCache();
135
+            }
136
+        }
137
+    }
138
+
139
+    private void ensureYearlyStatsCached(int year)
140
+    {
141
+        if (isYearlyStatsCached(year))
142
+        {
143
+            return;
144
+        }
145
+        Object lock = yearlyRefreshLocks.computeIfAbsent(year, key -> new Object());
146
+        synchronized (lock)
147
+        {
148
+            if (!isYearlyStatsCached(year))
149
+            {
150
+                refreshYearlyStatsCache(year);
151
+            }
152
+        }
153
+    }
154
+
155
+    private void ensureWordCloudCached()
156
+    {
157
+        if (isWordCloudCached())
158
+        {
159
+            return;
160
+        }
161
+        synchronized (MallStatsOpenServiceImpl.class)
162
+        {
163
+            if (!isWordCloudCached())
164
+            {
165
+                refreshWordCloudCache();
166
+            }
167
+        }
168
+    }
169
+
170
+    private boolean isYearlyStatsCached(int year)
171
+    {
172
+        for (String cacheKey : StatsCacheKeys.yearlyStatKeys(year))
173
+        {
174
+            if (redisCache.getCacheObject(cacheKey) == null)
175
+            {
176
+                return false;
177
+            }
178
+        }
179
+        return true;
180
+    }
181
+
182
+    private boolean isWordCloudCached()
183
+    {
184
+        return redisCache.getCacheObject(StatsCacheKeys.wordCloud()) != null;
185
+    }
186
+
187
+    private void refreshYearlyStatsCache(int year)
188
+    {
189
+        List<CategoryQtyRow> categoryRows = safeList(statsMapper.selectCategoryQtyByFinishYear(year));
190
+        CategorySalesVO categorySales = buildCategorySales(year, categoryRows);
191
+        HotCategoryRankVO hotCategory = buildHotCategoryRank(year, new ArrayList<>(categoryRows));
192
+        OrderTrendVO orderTrend = buildOrderTrend(year, safeList(statsMapper.selectOrderCountByYear(year)));
193
+        ShopEntryVO shopEntry = buildShopEntry(year, safeList(statsMapper.selectShopCountByYear(year)));
194
+        RegionRankVO regionRank = buildRegionRank(year, safeList(statsMapper.selectRegionAmountByYear(year)));
195
+
196
+        cacheYearlyStat(StatsCacheKeys.categorySales(year), categorySales);
197
+        cacheYearlyStat(StatsCacheKeys.hotCategory(year), hotCategory);
198
+        cacheYearlyStat(StatsCacheKeys.orderTrend(year), orderTrend);
199
+        cacheYearlyStat(StatsCacheKeys.shopEntry(year), shopEntry);
200
+        cacheYearlyStat(StatsCacheKeys.regionRank(year), regionRank);
201
+    }
202
+
203
+    private void refreshWordCloudCache()
204
+    {
205
+        List<String> contents = safeList(statsMapper.selectReviewContentsForWordCloud());
206
+        ReviewWordCloudVO vo = wordCloudSupport.buildTopWords(contents);
207
+        redisCache.setCacheObject(StatsCacheKeys.wordCloud(), vo,
208
+                OpenStatsConstants.CACHE_TTL_WORDCLOUD_HOUR, TimeUnit.HOURS);
209
+    }
210
+
211
+    private void cacheYearlyStat(String cacheKey, Object value)
212
+    {
213
+        redisCache.setCacheObject(cacheKey, value, OpenStatsConstants.CACHE_TTL_YEARLY_HOUR, TimeUnit.HOURS);
214
+    }
215
+
162
     private CategorySalesVO buildCategorySales(int statYear, List<CategoryQtyRow> rows)
216
     private CategorySalesVO buildCategorySales(int statYear, List<CategoryQtyRow> rows)
163
     {
217
     {
164
         CategorySalesVO vo = new CategorySalesVO();
218
         CategorySalesVO vo = new CategorySalesVO();

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

@@ -40,4 +40,15 @@ public final class StatsCacheKeys
40
     {
40
     {
41
         return PREFIX + "wordcloud";
41
         return PREFIX + "wordcloud";
42
     }
42
     }
43
+
44
+    public static String[] yearlyStatKeys(int statYear)
45
+    {
46
+        return new String[] {
47
+            categorySales(statYear),
48
+            hotCategory(statYear),
49
+            orderTrend(statYear),
50
+            shopEntry(statYear),
51
+            regionRank(statYear)
52
+        };
53
+    }
43
 }
54
 }

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

@@ -8,12 +8,15 @@ import static org.mockito.ArgumentMatchers.anyInt;
8
 import static org.mockito.ArgumentMatchers.anyString;
8
 import static org.mockito.ArgumentMatchers.anyString;
9
 import static org.mockito.ArgumentMatchers.eq;
9
 import static org.mockito.ArgumentMatchers.eq;
10
 import static org.mockito.Mockito.atLeastOnce;
10
 import static org.mockito.Mockito.atLeastOnce;
11
+import static org.mockito.Mockito.doAnswer;
11
 import static org.mockito.Mockito.times;
12
 import static org.mockito.Mockito.times;
12
 import static org.mockito.Mockito.verify;
13
 import static org.mockito.Mockito.verify;
13
 import static org.mockito.Mockito.when;
14
 import static org.mockito.Mockito.when;
14
 import java.math.BigDecimal;
15
 import java.math.BigDecimal;
15
 import java.util.Arrays;
16
 import java.util.Arrays;
16
 import java.util.Collections;
17
 import java.util.Collections;
18
+import java.util.HashMap;
19
+import java.util.Map;
17
 import java.util.concurrent.TimeUnit;
20
 import java.util.concurrent.TimeUnit;
18
 import org.junit.jupiter.api.BeforeEach;
21
 import org.junit.jupiter.api.BeforeEach;
19
 import org.junit.jupiter.api.Test;
22
 import org.junit.jupiter.api.Test;
@@ -21,6 +24,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
21
 import org.mockito.InjectMocks;
24
 import org.mockito.InjectMocks;
22
 import org.mockito.Mock;
25
 import org.mockito.Mock;
23
 import org.mockito.junit.jupiter.MockitoExtension;
26
 import org.mockito.junit.jupiter.MockitoExtension;
27
+import org.mockito.junit.jupiter.MockitoSettings;
28
+import org.mockito.quality.Strictness;
29
+import org.mockito.stubbing.Answer;
24
 import com.ruoyi.common.core.redis.RedisCache;
30
 import com.ruoyi.common.core.redis.RedisCache;
25
 import com.ruoyi.web.modules.openstats.constant.OpenStatsConstants;
31
 import com.ruoyi.web.modules.openstats.constant.OpenStatsConstants;
26
 import com.ruoyi.web.modules.openstats.dto.CategoryQtyRow;
32
 import com.ruoyi.web.modules.openstats.dto.CategoryQtyRow;
@@ -40,6 +46,7 @@ import com.ruoyi.web.modules.openstats.vo.StatsOverviewVO;
40
 import com.ruoyi.web.modules.openstats.vo.WordCloudItemVO;
46
 import com.ruoyi.web.modules.openstats.vo.WordCloudItemVO;
41
 
47
 
42
 @ExtendWith(MockitoExtension.class)
48
 @ExtendWith(MockitoExtension.class)
49
+@MockitoSettings(strictness = Strictness.LENIENT)
43
 class MallStatsOpenServiceImplTest
50
 class MallStatsOpenServiceImplTest
44
 {
51
 {
45
     private static final int STAT_YEAR = 2026;
52
     private static final int STAT_YEAR = 2026;
@@ -56,10 +63,20 @@ class MallStatsOpenServiceImplTest
56
     @InjectMocks
63
     @InjectMocks
57
     private MallStatsOpenServiceImpl service;
64
     private MallStatsOpenServiceImpl service;
58
 
65
 
66
+    private final Map<String, Object> cacheStore = new HashMap<>();
67
+
59
     @BeforeEach
68
     @BeforeEach
60
-    void stubCacheMiss()
69
+    void setUpCache()
61
     {
70
     {
62
-        when(redisCache.getCacheObject(anyString())).thenReturn(null);
71
+        cacheStore.clear();
72
+        when(redisCache.getCacheObject(anyString())).thenAnswer(
73
+                invocation -> cacheStore.get(invocation.getArgument(0)));
74
+        Answer<Object> saveCache = invocation -> {
75
+            cacheStore.put(invocation.getArgument(0), invocation.getArgument(1));
76
+            return null;
77
+        };
78
+        doAnswer(saveCache).when(redisCache).setCacheObject(anyString(), any(), anyInt(), any(TimeUnit.class));
79
+        stubYearlyMapperDefaults();
63
     }
80
     }
64
 
81
 
65
     @Test
82
     @Test
@@ -77,8 +94,8 @@ class MallStatsOpenServiceImplTest
77
         assertEquals(3, vo.getItems().size());
94
         assertEquals(3, vo.getItems().size());
78
         double ratioSum = vo.getItems().stream().mapToDouble(i -> i.getRatio()).sum();
95
         double ratioSum = vo.getItems().stream().mapToDouble(i -> i.getRatio()).sum();
79
         assertEquals(100.0, ratioSum, 0.05);
96
         assertEquals(100.0, ratioSum, 0.05);
80
-        verify(redisCache).setCacheObject(eq(StatsCacheKeys.categorySales(STAT_YEAR)), eq(vo),
81
-                eq(OpenStatsConstants.CACHE_TTL_YEARLY_HOUR), eq(TimeUnit.HOURS));
97
+        assertNotNull(cacheStore.get(StatsCacheKeys.orderTrend(STAT_YEAR)));
98
+        assertNotNull(cacheStore.get(StatsCacheKeys.hotCategory(STAT_YEAR)));
82
     }
99
     }
83
 
100
 
84
     @Test
101
     @Test
@@ -99,6 +116,22 @@ class MallStatsOpenServiceImplTest
99
         assertEquals(1, vo.getItems().get(0).getRank());
116
         assertEquals(1, vo.getItems().get(0).getRank());
100
         assertEquals("A", vo.getItems().get(0).getCategoryName());
117
         assertEquals("A", vo.getItems().get(0).getCategoryName());
101
         assertTrue(vo.getItems().stream().noneMatch(i -> "F".equals(i.getCategoryName())));
118
         assertTrue(vo.getItems().stream().noneMatch(i -> "F".equals(i.getCategoryName())));
119
+        assertNotNull(cacheStore.get(StatsCacheKeys.categorySales(STAT_YEAR)));
120
+    }
121
+
122
+    @Test
123
+    void yearlyStatsCache_refreshesAllMetricsTogether()
124
+    {
125
+        service.getOrderTrend("2026");
126
+
127
+        for (String cacheKey : StatsCacheKeys.yearlyStatKeys(STAT_YEAR))
128
+        {
129
+            assertNotNull(cacheStore.get(cacheKey), cacheKey);
130
+        }
131
+        verify(statsMapper, times(1)).selectCategoryQtyByFinishYear(STAT_YEAR);
132
+        verify(statsMapper, times(1)).selectOrderCountByYear(STAT_YEAR);
133
+        verify(statsMapper, times(1)).selectShopCountByYear(STAT_YEAR);
134
+        verify(statsMapper, times(1)).selectRegionAmountByYear(STAT_YEAR);
102
     }
135
     }
103
 
136
 
104
     @Test
137
     @Test
@@ -150,7 +183,6 @@ class MallStatsOpenServiceImplTest
150
         item.setWord("质量好");
183
         item.setWord("质量好");
151
         item.setCount(10L);
184
         item.setCount(10L);
152
         cached.getItems().add(item);
185
         cached.getItems().add(item);
153
-        when(redisCache.getCacheObject(StatsCacheKeys.wordCloud())).thenReturn(null, cached);
154
         when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.singletonList("质量很好"));
186
         when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.singletonList("质量很好"));
155
         when(wordCloudSupport.buildTopWords(any())).thenReturn(cached);
187
         when(wordCloudSupport.buildTopWords(any())).thenReturn(cached);
156
 
188
 
@@ -165,11 +197,6 @@ class MallStatsOpenServiceImplTest
165
     @Test
197
     @Test
166
     void overview_containsAllSections()
198
     void overview_containsAllSections()
167
     {
199
     {
168
-        when(statsMapper.selectCategoryQtyByFinishYear(anyInt())).thenReturn(Collections.emptyList());
169
-        when(statsMapper.selectOrderCountByYear(anyInt())).thenReturn(Collections.emptyList());
170
-        when(statsMapper.selectShopCountByYear(anyInt())).thenReturn(Collections.emptyList());
171
-        when(statsMapper.selectRegionAmountByYear(anyInt())).thenReturn(Collections.emptyList());
172
-        when(statsMapper.selectReviewContentsForWordCloud()).thenReturn(Collections.emptyList());
173
         when(wordCloudSupport.buildTopWords(any())).thenReturn(new ReviewWordCloudVO());
200
         when(wordCloudSupport.buildTopWords(any())).thenReturn(new ReviewWordCloudVO());
174
 
201
 
175
         StatsOverviewVO overview = service.getOverview("2026");
202
         StatsOverviewVO overview = service.getOverview("2026");
@@ -181,6 +208,20 @@ class MallStatsOpenServiceImplTest
181
         assertNotNull(overview.getRegionRank());
208
         assertNotNull(overview.getRegionRank());
182
         assertNotNull(overview.getReviewWordCloud());
209
         assertNotNull(overview.getReviewWordCloud());
183
         verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishYear(STAT_YEAR);
210
         verify(statsMapper, atLeastOnce()).selectCategoryQtyByFinishYear(STAT_YEAR);
211
+        verify(statsMapper, times(1)).selectReviewContentsForWordCloud();
212
+        for (String cacheKey : StatsCacheKeys.yearlyStatKeys(STAT_YEAR))
213
+        {
214
+            assertNotNull(cacheStore.get(cacheKey));
215
+        }
216
+        assertNotNull(cacheStore.get(StatsCacheKeys.wordCloud()));
217
+    }
218
+
219
+    private void stubYearlyMapperDefaults()
220
+    {
221
+        when(statsMapper.selectCategoryQtyByFinishYear(anyInt())).thenReturn(Collections.emptyList());
222
+        when(statsMapper.selectOrderCountByYear(anyInt())).thenReturn(Collections.emptyList());
223
+        when(statsMapper.selectShopCountByYear(anyInt())).thenReturn(Collections.emptyList());
224
+        when(statsMapper.selectRegionAmountByYear(anyInt())).thenReturn(Collections.emptyList());
184
     }
225
     }
185
 
226
 
186
     private CategoryQtyRow row(Long id, String name, Long qty)
227
     private CategoryQtyRow row(Long id, String name, Long qty)

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

@@ -356,8 +356,10 @@ Header: X-Open-Token: {token}
356
 | 规则 |
356
 | 规则 |
357
 |------|
357
 |------|
358
 | **Cache-Aside**:先读 Redis,miss 再查 DB 并写入 |
358
 | **Cache-Aside**:先读 Redis,miss 再查 DB 并写入 |
359
+| **年度指标捆绑刷新**:同一 `statYear` 下 **5 项年度指标**(品类销售、热销、订单趋势、入驻、区域)**任一缓存 miss 或不全** 时,**一次性重建并写入全部 5 个 key**,避免各接口数据时间点不一致 |
360
+| **`overview`**:在年度捆绑就绪且词云缓存存在时组装;**任一缺失** 时 **同步刷新年度捆绑 + 词云** 后再返回 |
359
 | 订单 **确认收货/自动确认** 后 **不主动删缓存**;靠 TTL **准实时**(对齐功能需求) |
361
 | 订单 **确认收货/自动确认** 后 **不主动删缓存**;靠 TTL **准实时**(对齐功能需求) |
360
-| **`overview`** 内部并行调 6 段逻辑,**共用** 各 key |
362
+| **`overview` / 单接口** 均 **复用** 上述子 key,不再单独刷新部分指标 |
361
 
363
 
362
 ### 5.3 性能约束
364
 ### 5.3 性能约束
363
 
365
 
@@ -420,7 +422,8 @@ Header: X-Open-Token: {token}
420
 | 版本 | 说明 |
422
 | 版本 | 说明 |
421
 |------|------|
423
 |------|------|
422
 | **v1.0** | 首版:RuoYi3.9.2 + MySQL5.7;复用业务表聚合;六项 Open API + overview;Redis 缓存 |
424
 | **v1.0** | 首版:RuoYi3.9.2 + MySQL5.7;复用业务表聚合;六项 Open API + overview;Redis 缓存 |
425
+| **v1.3** | 年度 5 项指标 **捆绑刷新**;`overview` miss 时 **同步更新** 年度指标 + 词云,避免部分缓存过期 |
423
 | **v1.2** | §5.1/§5.2 改为 **统计年**(`YEAR(finish_time)`);接口参数统一 `statYear`;缓存 key/TTL 对齐年度指标 |
426
 | **v1.2** | §5.1/§5.2 改为 **统计年**(`YEAR(finish_time)`);接口参数统一 `statYear`;缓存 key/TTL 对齐年度指标 |
424
 | **v1.1** | Token 改为 **AES-128-CBC 加密 UUID** 校验;**移除** `biz_open_api_token` 库表方案 |
427
 | **v1.1** | Token 改为 **AES-128-CBC 加密 UUID** 校验;**移除** `biz_open_api_token` 库表方案 |
425
 
428
 
426
-*文档版本:v1.2 · 关联《商城数据统计功能需求.md》v1.0.4 · 统计索引:`sql/biz_order.sql` → `idx_stats_finish`*
429
+*文档版本:v1.3 · 关联《商城数据统计功能需求.md》v1.0.4 · 统计索引:`sql/biz_order.sql` → `idx_stats_finish`*