Kaynağa Gözat

会员管理代码

wwh 2 hafta önce
ebeveyn
işleme
df2ca894c5

+ 35 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/constant/CategoryAppConstants.java

@@ -0,0 +1,35 @@
1
+package com.ruoyi.web.modules.home.constant;
2
+
3
+/**
4
+ * C 端商品分类常量
5
+ */
6
+public final class CategoryAppConstants
7
+{
8
+    private CategoryAppConstants()
9
+    {
10
+    }
11
+
12
+    public static final String DEFAULT_SORT = "sales_desc";
13
+
14
+    public static final String SORT_SALES_DESC = "sales_desc";
15
+
16
+    public static final String SORT_SALES_ASC = "sales_asc";
17
+
18
+    public static final String SORT_PRICE_DESC = "price_desc";
19
+
20
+    public static final String SORT_PRICE_ASC = "price_asc";
21
+
22
+    public static final String MSG_CATEGORY_REQUIRED = "请选择商品分类";
23
+
24
+    public static final String MSG_CATEGORY_INVISIBLE = "分类不存在或不可见";
25
+
26
+    public static final String MSG_SORT_INVALID = "排序参数无效";
27
+
28
+    public static boolean isValidSortBy(String sortBy)
29
+    {
30
+        return SORT_SALES_DESC.equals(sortBy)
31
+                || SORT_SALES_ASC.equals(sortBy)
32
+                || SORT_PRICE_DESC.equals(sortBy)
33
+                || SORT_PRICE_ASC.equals(sortBy);
34
+    }
35
+}

+ 49 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/controller/CategoryAppController.java

@@ -0,0 +1,49 @@
1
+package com.ruoyi.web.modules.home.controller;
2
+
3
+import java.util.List;
4
+import org.springframework.beans.factory.annotation.Autowired;
5
+import org.springframework.web.bind.annotation.GetMapping;
6
+import org.springframework.web.bind.annotation.PathVariable;
7
+import org.springframework.web.bind.annotation.RequestMapping;
8
+import org.springframework.web.bind.annotation.RestController;
9
+import com.ruoyi.common.annotation.Anonymous;
10
+import com.ruoyi.common.core.controller.BaseController;
11
+import com.ruoyi.common.core.domain.AjaxResult;
12
+import com.ruoyi.common.core.page.TableDataInfo;
13
+import com.ruoyi.web.modules.home.dto.CategoryGoodsQuery;
14
+import com.ruoyi.web.modules.home.service.ICategoryAppService;
15
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
16
+
17
+/**
18
+ * C 端商品分类(全部分类树、二级 Tab、分类商品列表)
19
+ */
20
+@RestController
21
+@RequestMapping("/api/category")
22
+public class CategoryAppController extends BaseController
23
+{
24
+    @Autowired
25
+    private ICategoryAppService categoryAppService;
26
+
27
+    @Anonymous
28
+    @GetMapping("/tree")
29
+    public AjaxResult tree()
30
+    {
31
+        return success(categoryAppService.listCategoryTree());
32
+    }
33
+
34
+    @Anonymous
35
+    @GetMapping("/{level1Id}/level2-tabs")
36
+    public AjaxResult level2Tabs(@PathVariable("level1Id") Long level1Id)
37
+    {
38
+        return success(categoryAppService.listLevel2Tabs(level1Id));
39
+    }
40
+
41
+    @Anonymous
42
+    @GetMapping("/goods")
43
+    public TableDataInfo goods(CategoryGoodsQuery query)
44
+    {
45
+        startPage();
46
+        List<HomeHotGoodsVO> list = categoryAppService.listCategoryGoods(query);
47
+        return getDataTable(list);
48
+    }
49
+}

+ 31 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/dto/CategoryGoodsQuery.java

@@ -0,0 +1,31 @@
1
+package com.ruoyi.web.modules.home.dto;
2
+
3
+/**
4
+ * C 端分类商品列表查询
5
+ */
6
+public class CategoryGoodsQuery
7
+{
8
+    private Long categoryId;
9
+
10
+    private String sortBy;
11
+
12
+    public Long getCategoryId()
13
+    {
14
+        return categoryId;
15
+    }
16
+
17
+    public void setCategoryId(Long categoryId)
18
+    {
19
+        this.categoryId = categoryId;
20
+    }
21
+
22
+    public String getSortBy()
23
+    {
24
+        return sortBy;
25
+    }
26
+
27
+    public void setSortBy(String sortBy)
28
+    {
29
+        this.sortBy = sortBy;
30
+    }
31
+}

+ 23 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/mapper/CategoryAppMapper.java

@@ -0,0 +1,23 @@
1
+package com.ruoyi.web.modules.home.mapper;
2
+
3
+import java.util.List;
4
+import org.apache.ibatis.annotations.Param;
5
+import com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO;
6
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
7
+
8
+/**
9
+ * C 端商品分类只读 Mapper
10
+ */
11
+public interface CategoryAppMapper
12
+{
13
+    /**
14
+     * 平台一级下可见二级 Tab
15
+     */
16
+    List<CategoryVisibleChildVO> selectPlatformLevel2Tabs(@Param("level1Id") Long level1Id);
17
+
18
+    /**
19
+     * 二级分类下出售中商品(分页由 PageHelper 控制)
20
+     */
21
+    List<HomeHotGoodsVO> selectGoodsByCategory(@Param("categoryId") Long categoryId,
22
+            @Param("sortBy") String sortBy);
23
+}

+ 19 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/service/ICategoryAppService.java

@@ -0,0 +1,19 @@
1
+package com.ruoyi.web.modules.home.service;
2
+
3
+import java.util.List;
4
+import com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO;
5
+import com.ruoyi.web.modules.category.vo.CategoryVisibleVO;
6
+import com.ruoyi.web.modules.home.dto.CategoryGoodsQuery;
7
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
8
+
9
+/**
10
+ * C 端商品分类 Service
11
+ */
12
+public interface ICategoryAppService
13
+{
14
+    List<CategoryVisibleVO> listCategoryTree();
15
+
16
+    List<CategoryVisibleChildVO> listLevel2Tabs(Long level1Id);
17
+
18
+    List<HomeHotGoodsVO> listCategoryGoods(CategoryGoodsQuery query);
19
+}

+ 89 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/home/service/impl/CategoryAppServiceImpl.java

@@ -0,0 +1,89 @@
1
+package com.ruoyi.web.modules.home.service.impl;
2
+
3
+import java.util.Collections;
4
+import java.util.List;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.stereotype.Service;
7
+import com.ruoyi.common.exception.ServiceException;
8
+import com.ruoyi.common.utils.StringUtils;
9
+import com.ruoyi.web.modules.category.constant.CategoryConstants;
10
+import com.ruoyi.web.modules.category.domain.BizGoodsCategory;
11
+import com.ruoyi.web.modules.category.facade.ICategoryFacade;
12
+import com.ruoyi.web.modules.category.mapper.BizGoodsCategoryMapper;
13
+import com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO;
14
+import com.ruoyi.web.modules.category.vo.CategoryVisibleVO;
15
+import com.ruoyi.web.modules.home.constant.CategoryAppConstants;
16
+import com.ruoyi.web.modules.home.dto.CategoryGoodsQuery;
17
+import com.ruoyi.web.modules.home.mapper.CategoryAppMapper;
18
+import com.ruoyi.web.modules.home.service.ICategoryAppService;
19
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
20
+
21
+/**
22
+ * C 端商品分类 Service 实现
23
+ */
24
+@Service
25
+public class CategoryAppServiceImpl implements ICategoryAppService
26
+{
27
+    @Autowired
28
+    private ICategoryFacade categoryFacade;
29
+
30
+    @Autowired
31
+    private BizGoodsCategoryMapper categoryMapper;
32
+
33
+    @Autowired
34
+    private CategoryAppMapper categoryAppMapper;
35
+
36
+    @Override
37
+    public List<CategoryVisibleVO> listCategoryTree()
38
+    {
39
+        List<CategoryVisibleVO> list = categoryFacade.listVisibleByShopId(null);
40
+        return list != null ? list : Collections.emptyList();
41
+    }
42
+
43
+    @Override
44
+    public List<CategoryVisibleChildVO> listLevel2Tabs(Long level1Id)
45
+    {
46
+        assertPlatformLevel1Visible(level1Id);
47
+        List<CategoryVisibleChildVO> list = categoryAppMapper.selectPlatformLevel2Tabs(level1Id);
48
+        return list != null ? list : Collections.emptyList();
49
+    }
50
+
51
+    @Override
52
+    public List<HomeHotGoodsVO> listCategoryGoods(CategoryGoodsQuery query)
53
+    {
54
+        if (query == null || query.getCategoryId() == null)
55
+        {
56
+            throw new ServiceException(CategoryAppConstants.MSG_CATEGORY_REQUIRED);
57
+        }
58
+        if (!categoryFacade.isCategoryVisible(query.getCategoryId()))
59
+        {
60
+            throw new ServiceException(CategoryAppConstants.MSG_CATEGORY_INVISIBLE);
61
+        }
62
+        String sortBy = StringUtils.isEmpty(query.getSortBy())
63
+                ? CategoryAppConstants.DEFAULT_SORT
64
+                : query.getSortBy();
65
+        if (!CategoryAppConstants.isValidSortBy(sortBy))
66
+        {
67
+            throw new ServiceException(CategoryAppConstants.MSG_SORT_INVALID);
68
+        }
69
+        List<HomeHotGoodsVO> list = categoryAppMapper.selectGoodsByCategory(query.getCategoryId(), sortBy);
70
+        return list != null ? list : Collections.emptyList();
71
+    }
72
+
73
+    private void assertPlatformLevel1Visible(Long level1Id)
74
+    {
75
+        if (level1Id == null)
76
+        {
77
+            throw new ServiceException(CategoryAppConstants.MSG_CATEGORY_INVISIBLE);
78
+        }
79
+        BizGoodsCategory level1 = categoryMapper.selectByIdIgnoreShop(level1Id);
80
+        if (level1 == null
81
+                || level1.getShopId() != null
82
+                || !CategoryConstants.LEVEL_ONE.equals(level1.getCategoryLevel())
83
+                || !CategoryConstants.SHOW_YES.equals(level1.getShowFlag())
84
+                || !CategoryConstants.DEL_FLAG_NORMAL.equals(level1.getDelFlag()))
85
+        {
86
+            throw new ServiceException(CategoryAppConstants.MSG_CATEGORY_INVISIBLE);
87
+        }
88
+    }
89
+}

+ 50 - 0
baqing-shop/src/main/resources/mapper/home/CategoryAppMapper.xml

@@ -0,0 +1,50 @@
1
+<?xml version="1.0" encoding="UTF-8" ?>
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.home.mapper.CategoryAppMapper">
4
+
5
+    <select id="selectPlatformLevel2Tabs" resultType="com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO">
6
+        select category_id as categoryId,
7
+               category_name as categoryName,
8
+               category_pic as categoryPic,
9
+               hot_flag as hotFlag,
10
+               sort_no as sortNo
11
+        from biz_goods_category
12
+        where shop_id is null
13
+          and category_level = '2'
14
+          and parent_id = #{level1Id}
15
+          and show_flag = '1'
16
+          and del_flag = '0'
17
+        order by sort_no asc, create_time asc
18
+    </select>
19
+
20
+    <select id="selectGoodsByCategory" resultType="com.ruoyi.web.modules.home.vo.HomeHotGoodsVO">
21
+        select g.goods_id as goodsId,
22
+               g.goods_sn as goodsSn,
23
+               g.goods_name as goodsName,
24
+               g.main_pic as mainPic,
25
+               g.sale_price as salePrice,
26
+               s.shop_id as shopId,
27
+               s.shop_name as shopName
28
+        from biz_goods g
29
+        inner join biz_shop s on g.shop_id = s.shop_id and s.del_flag = '0'
30
+        where g.goods_status = '2'
31
+          and g.del_flag = '0'
32
+          and g.category_id = #{categoryId}
33
+        order by
34
+        <choose>
35
+            <when test="sortBy == 'sales_asc'">
36
+                g.sales_count asc, g.goods_sn asc
37
+            </when>
38
+            <when test="sortBy == 'price_desc'">
39
+                g.sale_price desc, g.goods_sn asc
40
+            </when>
41
+            <when test="sortBy == 'price_asc'">
42
+                g.sale_price asc, g.goods_sn asc
43
+            </when>
44
+            <otherwise>
45
+                g.sales_count desc, g.goods_sn asc
46
+            </otherwise>
47
+        </choose>
48
+    </select>
49
+
50
+</mapper>

+ 218 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/home/controller/CategoryAppControllerTest.java

@@ -0,0 +1,218 @@
1
+package com.ruoyi.web.modules.home.controller;
2
+
3
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4
+import static org.junit.jupiter.api.Assertions.assertThrows;
5
+import static org.mockito.ArgumentMatchers.any;
6
+import static org.mockito.Mockito.when;
7
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
9
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
10
+import java.math.BigDecimal;
11
+import java.util.Arrays;
12
+import java.util.Collections;
13
+import org.junit.jupiter.api.BeforeEach;
14
+import org.junit.jupiter.api.Test;
15
+import org.junit.jupiter.api.extension.ExtendWith;
16
+import org.mockito.InjectMocks;
17
+import org.mockito.Mock;
18
+import org.mockito.junit.jupiter.MockitoExtension;
19
+import org.springframework.test.web.servlet.MockMvc;
20
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
21
+import org.springframework.web.util.NestedServletException;
22
+import com.ruoyi.common.exception.ServiceException;
23
+import com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO;
24
+import com.ruoyi.web.modules.category.vo.CategoryVisibleVO;
25
+import com.ruoyi.web.modules.home.constant.CategoryAppConstants;
26
+import com.ruoyi.web.modules.home.service.ICategoryAppService;
27
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
28
+
29
+/**
30
+ * C 端商品分类接口测试(CGC-API-001~020)
31
+ */
32
+@ExtendWith(MockitoExtension.class)
33
+class CategoryAppControllerTest
34
+{
35
+    @Mock
36
+    private ICategoryAppService categoryAppService;
37
+
38
+    @InjectMocks
39
+    private CategoryAppController controller;
40
+
41
+    private MockMvc mockMvc;
42
+
43
+    @BeforeEach
44
+    void setUp()
45
+    {
46
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
47
+    }
48
+
49
+    @Test
50
+    void tree_returns200WithoutToken() throws Exception
51
+    {
52
+        CategoryVisibleVO l1 = new CategoryVisibleVO();
53
+        l1.setCategoryId(1L);
54
+        l1.setCategoryName("兽药");
55
+        l1.setSortNo(0);
56
+        CategoryVisibleChildVO child = new CategoryVisibleChildVO();
57
+        child.setCategoryId(11L);
58
+        child.setCategoryName("抗生素");
59
+        l1.getChildren().add(child);
60
+        when(categoryAppService.listCategoryTree()).thenReturn(Collections.singletonList(l1));
61
+
62
+        mockMvc.perform(get("/api/category/tree"))
63
+                .andExpect(status().isOk())
64
+                .andExpect(jsonPath("$.code").value(200))
65
+                .andExpect(jsonPath("$.data[0].categoryId").value(1))
66
+                .andExpect(jsonPath("$.data[0].categoryName").value("兽药"))
67
+                .andExpect(jsonPath("$.data[0].children[0].categoryId").value(11))
68
+                .andExpect(jsonPath("$.data[0].children[0].categoryName").value("抗生素"));
69
+    }
70
+
71
+    @Test
72
+    void tree_emptyList() throws Exception
73
+    {
74
+        when(categoryAppService.listCategoryTree()).thenReturn(Collections.emptyList());
75
+
76
+        mockMvc.perform(get("/api/category/tree"))
77
+                .andExpect(status().isOk())
78
+                .andExpect(jsonPath("$.code").value(200))
79
+                .andExpect(jsonPath("$.data").isArray())
80
+                .andExpect(jsonPath("$.data").isEmpty());
81
+    }
82
+
83
+    @Test
84
+    void level2Tabs_returns200() throws Exception
85
+    {
86
+        CategoryVisibleChildVO tab1 = new CategoryVisibleChildVO();
87
+        tab1.setCategoryId(11L);
88
+        tab1.setCategoryName("抗生素");
89
+        tab1.setSortNo(0);
90
+        CategoryVisibleChildVO tab2 = new CategoryVisibleChildVO();
91
+        tab2.setCategoryId(12L);
92
+        tab2.setCategoryName("驱虫药");
93
+        tab2.setSortNo(1);
94
+        when(categoryAppService.listLevel2Tabs(1L)).thenReturn(Arrays.asList(tab1, tab2));
95
+
96
+        mockMvc.perform(get("/api/category/1/level2-tabs"))
97
+                .andExpect(status().isOk())
98
+                .andExpect(jsonPath("$.code").value(200))
99
+                .andExpect(jsonPath("$.data.length()").value(2))
100
+                .andExpect(jsonPath("$.data[0].categoryName").value("抗生素"));
101
+    }
102
+
103
+    @Test
104
+    void level2Tabs_invalidLevel1Rejected()
105
+    {
106
+        when(categoryAppService.listLevel2Tabs(99999L))
107
+                .thenThrow(new ServiceException(CategoryAppConstants.MSG_CATEGORY_INVISIBLE));
108
+
109
+        NestedServletException ex = assertThrows(NestedServletException.class,
110
+                () -> mockMvc.perform(get("/api/category/99999/level2-tabs")).andReturn());
111
+        assertInstanceOf(ServiceException.class, ex.getCause());
112
+    }
113
+
114
+    @Test
115
+    void goods_returns200WithPagination() throws Exception
116
+    {
117
+        when(categoryAppService.listCategoryGoods(any())).thenReturn(Collections.singletonList(goods(1L, "G001")));
118
+
119
+        mockMvc.perform(get("/api/category/goods")
120
+                .param("categoryId", "11")
121
+                .param("pageNum", "1")
122
+                .param("pageSize", "10"))
123
+                .andExpect(status().isOk())
124
+                .andExpect(jsonPath("$.code").value(200))
125
+                .andExpect(jsonPath("$.rows[0].goodsId").value(1))
126
+                .andExpect(jsonPath("$.rows[0].goodsSn").value("G001"))
127
+                .andExpect(jsonPath("$.rows[0].goodsName").value("测试商品"))
128
+                .andExpect(jsonPath("$.rows[0].mainPic").value("/pic/g1.jpg"))
129
+                .andExpect(jsonPath("$.rows[0].salePrice").value(88.50))
130
+                .andExpect(jsonPath("$.rows[0].shopId").value(101))
131
+                .andExpect(jsonPath("$.rows[0].shopName").value("测试店"));
132
+    }
133
+
134
+    @Test
135
+    void goods_missingCategoryIdRejected()
136
+    {
137
+        when(categoryAppService.listCategoryGoods(any()))
138
+                .thenThrow(new ServiceException(CategoryAppConstants.MSG_CATEGORY_REQUIRED));
139
+
140
+        NestedServletException ex = assertThrows(NestedServletException.class,
141
+                () -> mockMvc.perform(get("/api/category/goods")).andReturn());
142
+        assertInstanceOf(ServiceException.class, ex.getCause());
143
+    }
144
+
145
+    @Test
146
+    void goods_invisibleCategoryRejected()
147
+    {
148
+        when(categoryAppService.listCategoryGoods(any()))
149
+                .thenThrow(new ServiceException(CategoryAppConstants.MSG_CATEGORY_INVISIBLE));
150
+
151
+        NestedServletException ex = assertThrows(NestedServletException.class,
152
+                () -> mockMvc.perform(get("/api/category/goods").param("categoryId", "99")).andReturn());
153
+        assertInstanceOf(ServiceException.class, ex.getCause());
154
+    }
155
+
156
+    @Test
157
+    void goods_invalidSortRejected()
158
+    {
159
+        when(categoryAppService.listCategoryGoods(any()))
160
+                .thenThrow(new ServiceException(CategoryAppConstants.MSG_SORT_INVALID));
161
+
162
+        NestedServletException ex = assertThrows(NestedServletException.class,
163
+                () -> mockMvc.perform(get("/api/category/goods")
164
+                        .param("categoryId", "11")
165
+                        .param("sortBy", "foo")).andReturn());
166
+        assertInstanceOf(ServiceException.class, ex.getCause());
167
+    }
168
+
169
+    @Test
170
+    void goods_emptyList() throws Exception
171
+    {
172
+        when(categoryAppService.listCategoryGoods(any())).thenReturn(Collections.emptyList());
173
+
174
+        mockMvc.perform(get("/api/category/goods").param("categoryId", "11"))
175
+                .andExpect(status().isOk())
176
+                .andExpect(jsonPath("$.code").value(200))
177
+                .andExpect(jsonPath("$.rows").isArray())
178
+                .andExpect(jsonPath("$.rows").isEmpty());
179
+    }
180
+
181
+    @Test
182
+    void goods_returnsMultipleItems() throws Exception
183
+    {
184
+        when(categoryAppService.listCategoryGoods(any())).thenReturn(Arrays.asList(
185
+                goods(1L, "G001"),
186
+                goods(2L, "G002")));
187
+
188
+        mockMvc.perform(get("/api/category/goods").param("categoryId", "11"))
189
+                .andExpect(status().isOk())
190
+                .andExpect(jsonPath("$.rows.length()").value(2));
191
+    }
192
+
193
+    @Test
194
+    void goods_preservesOrder() throws Exception
195
+    {
196
+        when(categoryAppService.listCategoryGoods(any())).thenReturn(Arrays.asList(
197
+                goods(1L, "G001"),
198
+                goods(3L, "G003")));
199
+
200
+        mockMvc.perform(get("/api/category/goods").param("categoryId", "11"))
201
+                .andExpect(status().isOk())
202
+                .andExpect(jsonPath("$.rows[0].goodsId").value(1))
203
+                .andExpect(jsonPath("$.rows[1].goodsId").value(3));
204
+    }
205
+
206
+    private static HomeHotGoodsVO goods(Long goodsId, String goodsSn)
207
+    {
208
+        HomeHotGoodsVO vo = new HomeHotGoodsVO();
209
+        vo.setGoodsId(goodsId);
210
+        vo.setGoodsSn(goodsSn);
211
+        vo.setGoodsName("测试商品");
212
+        vo.setMainPic("/pic/g1.jpg");
213
+        vo.setSalePrice(new BigDecimal("88.50"));
214
+        vo.setShopId(101L);
215
+        vo.setShopName("测试店");
216
+        return vo;
217
+    }
218
+}

+ 251 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/home/service/CategoryAppServiceImplTest.java

@@ -0,0 +1,251 @@
1
+package com.ruoyi.web.modules.home.service;
2
+
3
+import static org.junit.jupiter.api.Assertions.assertEquals;
4
+import static org.junit.jupiter.api.Assertions.assertNotNull;
5
+import static org.junit.jupiter.api.Assertions.assertThrows;
6
+import static org.junit.jupiter.api.Assertions.assertTrue;
7
+import static org.mockito.Mockito.verify;
8
+import static org.mockito.Mockito.when;
9
+import java.math.BigDecimal;
10
+import java.util.Arrays;
11
+import java.util.Collections;
12
+import java.util.List;
13
+import org.junit.jupiter.api.Test;
14
+import org.junit.jupiter.api.extension.ExtendWith;
15
+import org.mockito.InjectMocks;
16
+import org.mockito.Mock;
17
+import org.mockito.junit.jupiter.MockitoExtension;
18
+import com.ruoyi.common.exception.ServiceException;
19
+import com.ruoyi.web.modules.category.constant.CategoryConstants;
20
+import com.ruoyi.web.modules.category.domain.BizGoodsCategory;
21
+import com.ruoyi.web.modules.category.facade.ICategoryFacade;
22
+import com.ruoyi.web.modules.category.mapper.BizGoodsCategoryMapper;
23
+import com.ruoyi.web.modules.category.vo.CategoryVisibleChildVO;
24
+import com.ruoyi.web.modules.category.vo.CategoryVisibleVO;
25
+import com.ruoyi.web.modules.home.constant.CategoryAppConstants;
26
+import com.ruoyi.web.modules.home.dto.CategoryGoodsQuery;
27
+import com.ruoyi.web.modules.home.mapper.CategoryAppMapper;
28
+import com.ruoyi.web.modules.home.service.impl.CategoryAppServiceImpl;
29
+import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
30
+
31
+/**
32
+ * C 端商品分类 Service 单元测试(CGC-UT-001~012)
33
+ */
34
+@ExtendWith(MockitoExtension.class)
35
+class CategoryAppServiceImplTest
36
+{
37
+    @Mock
38
+    private ICategoryFacade categoryFacade;
39
+
40
+    @Mock
41
+    private BizGoodsCategoryMapper categoryMapper;
42
+
43
+    @Mock
44
+    private CategoryAppMapper categoryAppMapper;
45
+
46
+    @InjectMocks
47
+    private CategoryAppServiceImpl categoryAppService;
48
+
49
+    @Test
50
+    void listCategoryTree_delegatesPlatformFacade()
51
+    {
52
+        CategoryVisibleVO l1 = visibleLevel1(1L, "兽药");
53
+        when(categoryFacade.listVisibleByShopId(null)).thenReturn(Collections.singletonList(l1));
54
+
55
+        List<CategoryVisibleVO> result = categoryAppService.listCategoryTree();
56
+
57
+        verify(categoryFacade).listVisibleByShopId(null);
58
+        assertEquals(1, result.size());
59
+        assertEquals("兽药", result.get(0).getCategoryName());
60
+    }
61
+
62
+    @Test
63
+    void listCategoryTree_emptyListWhenNoData()
64
+    {
65
+        when(categoryFacade.listVisibleByShopId(null)).thenReturn(Collections.emptyList());
66
+
67
+        List<CategoryVisibleVO> result = categoryAppService.listCategoryTree();
68
+
69
+        assertNotNull(result);
70
+        assertTrue(result.isEmpty());
71
+    }
72
+
73
+    @Test
74
+    void listCategoryTree_nullFacadeResultReturnsEmpty()
75
+    {
76
+        when(categoryFacade.listVisibleByShopId(null)).thenReturn(null);
77
+
78
+        List<CategoryVisibleVO> result = categoryAppService.listCategoryTree();
79
+
80
+        assertNotNull(result);
81
+        assertTrue(result.isEmpty());
82
+    }
83
+
84
+    @Test
85
+    void listLevel2Tabs_returnsVisibleChildren()
86
+    {
87
+        when(categoryMapper.selectByIdIgnoreShop(1L)).thenReturn(platformLevel1(1L));
88
+        CategoryVisibleChildVO tab1 = level2Tab(11L, "抗生素", 0);
89
+        CategoryVisibleChildVO tab2 = level2Tab(12L, "驱虫药", 1);
90
+        when(categoryAppMapper.selectPlatformLevel2Tabs(1L)).thenReturn(Arrays.asList(tab1, tab2));
91
+
92
+        List<CategoryVisibleChildVO> result = categoryAppService.listLevel2Tabs(1L);
93
+
94
+        assertEquals(2, result.size());
95
+        assertEquals("抗生素", result.get(0).getCategoryName());
96
+    }
97
+
98
+    @Test
99
+    void listLevel2Tabs_hiddenLevel1Rejected()
100
+    {
101
+        BizGoodsCategory hidden = platformLevel1(99L);
102
+        hidden.setShowFlag(CategoryConstants.SHOW_NO);
103
+        when(categoryMapper.selectByIdIgnoreShop(99L)).thenReturn(hidden);
104
+
105
+        ServiceException ex = assertThrows(ServiceException.class,
106
+                () -> categoryAppService.listLevel2Tabs(99L));
107
+
108
+        assertEquals(CategoryAppConstants.MSG_CATEGORY_INVISIBLE, ex.getMessage());
109
+    }
110
+
111
+    @Test
112
+    void listCategoryGoods_missingCategoryIdRejected()
113
+    {
114
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
115
+
116
+        ServiceException ex = assertThrows(ServiceException.class,
117
+                () -> categoryAppService.listCategoryGoods(query));
118
+
119
+        assertEquals(CategoryAppConstants.MSG_CATEGORY_REQUIRED, ex.getMessage());
120
+    }
121
+
122
+    @Test
123
+    void listCategoryGoods_invisibleCategoryRejected()
124
+    {
125
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
126
+        query.setCategoryId(20L);
127
+        when(categoryFacade.isCategoryVisible(20L)).thenReturn(false);
128
+
129
+        ServiceException ex = assertThrows(ServiceException.class,
130
+                () -> categoryAppService.listCategoryGoods(query));
131
+
132
+        assertEquals(CategoryAppConstants.MSG_CATEGORY_INVISIBLE, ex.getMessage());
133
+    }
134
+
135
+    @Test
136
+    void listCategoryGoods_defaultSalesDescSort()
137
+    {
138
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
139
+        query.setCategoryId(11L);
140
+        when(categoryFacade.isCategoryVisible(11L)).thenReturn(true);
141
+        when(categoryAppMapper.selectGoodsByCategory(11L, CategoryAppConstants.DEFAULT_SORT))
142
+                .thenReturn(Collections.singletonList(goods(1L, "G001")));
143
+
144
+        categoryAppService.listCategoryGoods(query);
145
+
146
+        verify(categoryAppMapper).selectGoodsByCategory(11L, CategoryAppConstants.SORT_SALES_DESC);
147
+    }
148
+
149
+    @Test
150
+    void listCategoryGoods_priceAscSort()
151
+    {
152
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
153
+        query.setCategoryId(11L);
154
+        query.setSortBy(CategoryAppConstants.SORT_PRICE_ASC);
155
+        when(categoryFacade.isCategoryVisible(11L)).thenReturn(true);
156
+        when(categoryAppMapper.selectGoodsByCategory(11L, CategoryAppConstants.SORT_PRICE_ASC))
157
+                .thenReturn(Collections.singletonList(goods(1L, "G001")));
158
+
159
+        categoryAppService.listCategoryGoods(query);
160
+
161
+        verify(categoryAppMapper).selectGoodsByCategory(11L, CategoryAppConstants.SORT_PRICE_ASC);
162
+    }
163
+
164
+    @Test
165
+    void listCategoryGoods_invalidSortRejected()
166
+    {
167
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
168
+        query.setCategoryId(11L);
169
+        query.setSortBy("invalid");
170
+        when(categoryFacade.isCategoryVisible(11L)).thenReturn(true);
171
+
172
+        ServiceException ex = assertThrows(ServiceException.class,
173
+                () -> categoryAppService.listCategoryGoods(query));
174
+
175
+        assertEquals(CategoryAppConstants.MSG_SORT_INVALID, ex.getMessage());
176
+    }
177
+
178
+    @Test
179
+    void listCategoryGoods_preservesMapperOrder()
180
+    {
181
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
182
+        query.setCategoryId(11L);
183
+        when(categoryFacade.isCategoryVisible(11L)).thenReturn(true);
184
+        HomeHotGoodsVO g1 = goods(1L, "G001");
185
+        HomeHotGoodsVO g3 = goods(3L, "G003");
186
+        when(categoryAppMapper.selectGoodsByCategory(11L, CategoryAppConstants.DEFAULT_SORT))
187
+                .thenReturn(Arrays.asList(g1, g3));
188
+
189
+        List<HomeHotGoodsVO> result = categoryAppService.listCategoryGoods(query);
190
+
191
+        assertEquals(1L, result.get(0).getGoodsId());
192
+        assertEquals(3L, result.get(1).getGoodsId());
193
+    }
194
+
195
+    @Test
196
+    void listCategoryGoods_emptyListWhenNoGoods()
197
+    {
198
+        CategoryGoodsQuery query = new CategoryGoodsQuery();
199
+        query.setCategoryId(11L);
200
+        when(categoryFacade.isCategoryVisible(11L)).thenReturn(true);
201
+        when(categoryAppMapper.selectGoodsByCategory(11L, CategoryAppConstants.DEFAULT_SORT))
202
+                .thenReturn(Collections.emptyList());
203
+
204
+        List<HomeHotGoodsVO> result = categoryAppService.listCategoryGoods(query);
205
+
206
+        assertNotNull(result);
207
+        assertTrue(result.isEmpty());
208
+    }
209
+
210
+    private static CategoryVisibleVO visibleLevel1(Long id, String name)
211
+    {
212
+        CategoryVisibleVO vo = new CategoryVisibleVO();
213
+        vo.setCategoryId(id);
214
+        vo.setCategoryName(name);
215
+        vo.setSortNo(0);
216
+        return vo;
217
+    }
218
+
219
+    private static BizGoodsCategory platformLevel1(Long id)
220
+    {
221
+        BizGoodsCategory c = new BizGoodsCategory();
222
+        c.setCategoryId(id);
223
+        c.setShopId(null);
224
+        c.setCategoryLevel(CategoryConstants.LEVEL_ONE);
225
+        c.setShowFlag(CategoryConstants.SHOW_YES);
226
+        c.setDelFlag(CategoryConstants.DEL_FLAG_NORMAL);
227
+        return c;
228
+    }
229
+
230
+    private static CategoryVisibleChildVO level2Tab(Long id, String name, int sortNo)
231
+    {
232
+        CategoryVisibleChildVO vo = new CategoryVisibleChildVO();
233
+        vo.setCategoryId(id);
234
+        vo.setCategoryName(name);
235
+        vo.setSortNo(sortNo);
236
+        return vo;
237
+    }
238
+
239
+    private static HomeHotGoodsVO goods(Long goodsId, String goodsSn)
240
+    {
241
+        HomeHotGoodsVO vo = new HomeHotGoodsVO();
242
+        vo.setGoodsId(goodsId);
243
+        vo.setGoodsSn(goodsSn);
244
+        vo.setGoodsName("商品" + goodsSn);
245
+        vo.setMainPic("/pic/" + goodsSn + ".jpg");
246
+        vo.setSalePrice(new BigDecimal("88.50"));
247
+        vo.setShopId(101L);
248
+        vo.setShopName("测试店");
249
+        return vo;
250
+    }
251
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 13 - 12
doc/消费者APP/商城首页/商城首页功能需求.md