分類
發燒車訊

[springboot 開發單體web shop] 7. 多種形式提供商品列表

上文回顧

我們實現了仿jd的輪播廣告以及商品分類的功能,並且講解了不同的注入方式,本節我們將繼續實現我們的電商主業務,商品信息的展示。

需求分析

首先,在我們開始本節編碼之前,我們先來分析一下都有哪些地方會對商品進行展示,打開jd首頁,鼠標下拉可以看到如下:

可以看到,在大類型下查詢了部分商品在首頁進行展示(可以是最新的,也可以是網站推薦等等),然後點擊任何一個分類,可以看到如下:

我們一般進到電商網站之後,最常用的一個功能就是搜索, 結果如下:

選擇任意一個商品點擊,都可以進入到詳情頁面,這個是單個商品的信息展示。
綜上,我們可以知道,要實現一個電商平台的商品展示,最基本的包含:

  • 首頁推薦/最新上架商品
  • 分類查詢商品
  • 關鍵詞搜索商品
  • 商品詳情展示

接下來,我們就可以開始商品相關的業務開發了。

首頁商品列表|IndexProductList

開發梳理

我們首先來實現在首頁展示的推薦商品列表,來看一下都需要展示哪些信息,以及如何進行展示。

  • 商品主鍵(product_id)
  • 展示圖片(image_url)
  • 商品名稱(product_name)
  • 商品價格(product_price)
  • 分類說明(description)
  • 分類名稱(category_name)
  • 分類主鍵(category_id)
  • 其他…

編碼實現

根據一級分類查詢

遵循開發順序,自下而上,如果基礎mapper解決不了,那麼優先編寫SQL mapper,因為我們需要在同一張表中根據parent_id遞歸的實現數據查詢,當然我們這裏使用的是錶鏈接的方式實現。因此,common mapper無法滿足我們的需求,需要自定義mapper實現。

Custom Mapper實現

和根據一級分類查詢子分類一樣,在項目mscx-shop-mapper中添加一個自定義實現接口com.liferunner.custom.ProductCustomMapper,然後在resources\mapper\custom路徑下同步創建xml文件mapper/custom/ProductCustomMapper.xml,此時,因為我們在上節中已經配置了當前文件夾可以被容器掃描到,所以我們添加的新的mapper就會在啟動時被掃描加載,代碼如下:

/**
 * ProductCustomMapper for : 自定義商品Mapper
 */
public interface ProductCustomMapper {

    /***
     * 根據一級分類查詢商品
     *
     * @param paramMap 傳遞一級分類(map傳遞多參數)
     * @return java.util.List<com.liferunner.dto.IndexProductDTO>
     */
    List<IndexProductDTO> getIndexProductDtoList(@Param("paramMap") Map<String, Integer> paramMap);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liferunner.custom.ProductCustomMapper">
    <resultMap id="IndexProductDTO" type="com.liferunner.dto.IndexProductDTO">
        <id column="rootCategoryId" property="rootCategoryId"/>
        <result column="rootCategoryName" property="rootCategoryName"/>
        <result column="slogan" property="slogan"/>
        <result column="categoryImage" property="categoryImage"/>
        <result column="bgColor" property="bgColor"/>
        <collection property="productItemList" ofType="com.liferunner.dto.IndexProductItemDTO">
            <id column="productId" property="productId"/>
            <result column="productName" property="productName"/>
            <result column="productMainImageUrl" property="productMainImageUrl"/>
            <result column="productCreateTime" property="productCreateTime"/>
        </collection>
    </resultMap>
    <select id="getIndexProductDtoList" resultMap="IndexProductDTO" parameterType="Map">
        SELECT
        c.id as rootCategoryId,
        c.name as rootCategoryName,
        c.slogan as slogan,
        c.category_image as categoryImage,
        c.bg_color as bgColor,
        p.id as productId,
        p.product_name as productName,
        pi.url as productMainImageUrl,
        p.created_time as productCreateTime
        FROM category c
        LEFT JOIN products p
        ON c.id = p.root_category_id
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        WHERE c.type = 1
        AND p.root_category_id = #{paramMap.rootCategoryId}
        AND pi.is_main = 1
        LIMIT 0,10;
    </select>
</mapper>

Service實現

serviceproject 創建com.liferunner.service.IProductService接口以及其實現類com.liferunner.service.impl.ProductServiceImpl,添加查詢方法如下:

public interface IProductService {

    /**
     * 根據一級分類id獲取首頁推薦的商品list
     *
     * @param rootCategoryId 一級分類id
     * @return 商品list
     */
    List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId);
    ...
}

---
    
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ProductServiceImpl implements IProductService {

    // RequiredArgsConstructor 構造器注入
    private final ProductCustomMapper productCustomMapper;

    @Transactional(propagation = Propagation.SUPPORTS)
    @Override
    public List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId) {
        log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId);
        Map<String, Integer> map = new HashMap<>();
        map.put("rootCategoryId", rootCategoryId);
        val indexProductDtoList = this.productCustomMapper.getIndexProductDtoList(map);
        if (CollectionUtils.isEmpty(indexProductDtoList)) {
            log.warn("ProductServiceImpl#getIndexProductDtoList未查詢到任何商品信息");
        }
        log.info("查詢結果:{}", indexProductDtoList);
        return indexProductDtoList;
    }
}

Controller實現

接着,在com.liferunner.api.controller.IndexController中實現對外暴露的查詢接口:

@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    ...
    @Autowired
    private IProductService productService;

    @GetMapping("/rootCategorys")
    @ApiOperation(value = "查詢一級分類", notes = "查詢一級分類")
    public JsonResponse findAllRootCategorys() {
        log.info("============查詢一級分類==============");
        val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============一級分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
    ...
}

Test API

編寫完成之後,我們需要對我們的代碼進行測試驗證,還是通過使用RestService插件來實現,當然,大家也可以通過Postman來測試,結果如下:

商品列表|ProductList

如開文之初我們看到的京東商品列表一樣,我們先分析一下在商品列表頁面都需要哪些元素信息?

開發梳理

商品列表的展示按照我們之前的分析,總共分為2大類:

  • 選擇商品分類之後,展示當前分類下所有商品
  • 輸入搜索關鍵詞后,展示當前搜索到相關的所有商品

在這兩類中展示的商品列表數據,除了數據來源不同以外,其他元素基本都保持一致,那麼我們是否可以使用統一的接口來根據參數實現隔離呢? 理論上不存在問題,完全可以通過傳參判斷的方式進行數據回傳,但是,在我們實現一些可預見的功能需求時,一定要給自己的開發預留後路,也就是我們常說的可拓展性,基於此,我們會分開實現各自的接口,以便於後期的擴展。
接着來分析在列表頁中我們需要展示的元素,首先因為需要分上述兩種情況,因此我們需要在我們API設計的時候分別處理,針對於
1.分類的商品列表展示,需要傳入的參數有:

  • 分類id
  • 排序(在電商列表我們常見的幾種排序(銷量,價格等等))
  • 分頁相關(因為我們不可能把數據庫中所有的商品都取出來)
    • PageNumber(當前第幾頁)
    • PageSize(每頁显示多少條數據)

2.關鍵詞查詢商品列表,需要傳入的參數有:

  • 關鍵詞
  • 排序(在電商列表我們常見的幾種排序(銷量,價格等等))
  • 分頁相關(因為我們不可能把數據庫中所有的商品都取出來)
    • PageNumber(當前第幾頁)
    • PageSize(每頁显示多少條數據)

需要在頁面展示的信息有:

  • 商品id(用於跳轉商品詳情使用)
  • 商品名稱
  • 商品價格
  • 商品銷量
  • 商品圖片
  • 商品優惠

編碼實現

根據上面我們的分析,接下來開始我們的編碼:

根據商品分類查詢

根據我們的分析,肯定不會在一張表中把所有數據獲取全,因此我們需要進行多表聯查,故我們需要在自定義mapper中實現我們的功能查詢.

ResponseDTO 實現

根據我們前面分析的前端需要展示的信息,我們來定義一個用於展示這些信息的對象com.liferunner.dto.SearchProductDTO,代碼如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SearchProductDTO {
    private String productId;
    private String productName;
    private Integer sellCounts;
    private String imgUrl;
    private Integer priceDiscount;
    //商品優惠,我們直接計算之後返回優惠后價格
}

Custom Mapper 實現

com.liferunner.custom.ProductCustomMapper.java中新增一個方法接口:

    List<SearchProductDTO> searchProductListByCategoryId(@Param("paramMap") Map<String, Object> paramMap);

同時,在mapper/custom/ProductCustomMapper.xml中實現我們的查詢方法:

<select id="searchProductListByCategoryId" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
        SELECT
        p.id as productId,
        p.product_name as productName,
        p.sell_counts as sellCounts,
        pi.url as imgUrl,
        tp.priceDiscount
        FROM products p
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        LEFT JOIN
        (
        SELECT product_id, MIN(price_discount) as priceDiscount
        FROM products_spec
        GROUP BY product_id
        ) tp
        ON tp.product_id = p.id
        WHERE pi.is_main = 1
        AND p.category_id = #{paramMap.categoryId}
        ORDER BY
        <choose>
            <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                p.sell_counts DESC
            </when>
            <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                tp.priceDiscount ASC
            </when>
            <otherwise>
                p.created_time DESC
            </otherwise>
        </choose>
    </select>

主要來說明一下這裏的<choose>模塊,以及為什麼不使用if標籤。
在有的時候,我們並不希望所有的條件都同時生效,而只是想從多個選項中選擇一個,但是在使用IF標籤時,只要test中的表達式為 true,就會執行IF 標籤中的條件。MyBatis 提供了 choose 元素。IF標籤是與(and)的關係,而 choose 是或(or)的關係。
它的選擇是按照順序自上而下,一旦有任何一個滿足條件,則選擇退出。

Service 實現

然後在servicecom.liferunner.service.IProductService中添加方法接口:

    /**
     * 根據商品分類查詢商品列表
     *
     * @param categoryId 分類id
     * @param sortby     排序方式
     * @param pageNumber 當前頁碼
     * @param pageSize   每頁展示多少條數據
     * @return 通用分頁結果視圖
     */
    CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize);

在實現類com.liferunner.service.impl.ProductServiceImpl中,實現上述方法:

    // 方法重載
    @Override
    public CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("categoryId", categoryId);
        paramMap.put("sortby", sortby);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val searchProductDTOS = this.productCustomMapper.searchProductListByCategoryId(paramMap);
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(searchProductDTOS)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

在這裏,我們使用到了一個mybatis-pagehelper插件,會在下面的福利講解中分解。

Controller 實現

繼續在com.liferunner.api.controller.ProductController中添加對外暴露的接口API:

@GetMapping("/searchByCategoryId")
    @ApiOperation(value = "查詢商品信息列表", notes = "根據商品分類查詢商品列表")
    public JsonResponse searchProductListByCategoryId(
        @ApiParam(name = "categoryId", value = "商品分類id", required = true, example = "0")
        @RequestParam Integer categoryId,
        @ApiParam(name = "sortby", value = "排序方式", required = false)
        @RequestParam String sortby,
        @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
        @RequestParam Integer pageNumber,
        @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
        @RequestParam Integer pageSize
    ) {
        if (null == categoryId || categoryId == 0) {
            return JsonResponse.errorMsg("分類id錯誤!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============根據分類:{} 搜索列表==============", categoryId);

        val searchResult = this.productService.searchProductList(categoryId, sortby, pageNumber, pageSize);
        return JsonResponse.ok(searchResult);
    }

因為我們的請求中,只會要求商品分類id是必填項,其餘的調用方都可以不提供,但是如果不提供的話,我們系統就需要給定一些默認的參數來保證我們的系統正常穩定的運行,因此,我定義了com.liferunner.api.controller.BaseController,用於存儲一些公共的配置信息。

/**
 * BaseController for : controller 基類
 */
@Controller
public class BaseController {
    /**
     * 默認展示第1頁
     */
    public final Integer DEFAULT_PAGE_NUMBER = 1;
    /**
     * 默認每頁展示10條數據
     */
    public final Integer DEFAULT_PAGE_SIZE = 10;
}

Test API

測試的參數分別是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5

可以看到,我們查詢到7條數據,總頁數totalPage為2,並且根據價格從小到大進行了排序,證明我們的編碼是正確的。接下來,通過相同的代碼邏輯,我們繼續實現根據搜索關鍵詞進行查詢。

根據關鍵詞查詢

Response DTO 實現

使用上面實現的com.liferunner.dto.SearchProductDTO.

Custom Mapper 實現

com.liferunner.custom.ProductCustomMapper中新增方法:

List<SearchProductDTO> searchProductList(@Param("paramMap") Map<String, Object> paramMap);

mapper/custom/ProductCustomMapper.xml中添加查詢SQL:

<select id="searchProductList" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
        SELECT
        p.id as productId,
        p.product_name as productName,
        p.sell_counts as sellCounts,
        pi.url as imgUrl,
        tp.priceDiscount
        FROM products p
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        LEFT JOIN
        (
        SELECT product_id, MIN(price_discount) as priceDiscount
        FROM products_spec
        GROUP BY product_id
        ) tp
        ON tp.product_id = p.id
        WHERE pi.is_main = 1
        <if test="paramMap.keyword != null and paramMap.keyword != ''">
            AND p.item_name LIKE "%${paramMap.keyword}%"
        </if>
        ORDER BY
        <choose>
            <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                p.sell_counts DESC
            </when>
            <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                tp.priceDiscount ASC
            </when>
            <otherwise>
                p.created_time DESC
            </otherwise>
        </choose>
    </select>

Service 實現

com.liferunner.service.IProductService中新增查詢接口:

    /**
     * 查詢商品列表
     *
     * @param keyword    查詢關鍵詞
     * @param sortby     排序方式
     * @param pageNumber 當前頁碼
     * @param pageSize   每頁展示多少條數據
     * @return 通用分頁結果視圖
     */
    CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize);

com.liferunner.service.impl.ProductServiceImpl實現上述接口方法:

    @Override
    public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("keyword", keyword);
        paramMap.put("sortby", sortby);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(searchProductDTOS)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

上述方法和之前searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize)唯一的區別就是它是肯定搜索關鍵詞來進行數據查詢,使用重載的目的是為了我們後續不同類型的業務擴展而考慮的。

Controller 實現

com.liferunner.api.controller.ProductController中添加關鍵詞搜索API:

    @GetMapping("/search")
    @ApiOperation(value = "查詢商品信息列表", notes = "查詢商品信息列表")
    public JsonResponse searchProductList(
        @ApiParam(name = "keyword", value = "搜索關鍵詞", required = true)
        @RequestParam String keyword,
        @ApiParam(name = "sortby", value = "排序方式", required = false)
        @RequestParam String sortby,
        @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
        @RequestParam Integer pageNumber,
        @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
        @RequestParam Integer pageSize
    ) {
        if (StringUtils.isBlank(keyword)) {
            return JsonResponse.errorMsg("搜索關鍵詞不能為空!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============根據關鍵詞:{} 搜索列表==============", keyword);

        val searchResult = this.productService.searchProductList(keyword, sortby, pageNumber, pageSize);
        return JsonResponse.ok(searchResult);
    }

Test API

測試參數:keyword : 西鳳,sortby : sell,pageNumber : 1,pageSize : 10

根據銷量排序正常,查詢關鍵詞正常,總條數32,每頁10條,總共3頁正常。

福利講解

在本節編碼實現中,我們使用到了一個通用的mybatis分頁插件mybatis-pagehelper,接下來,我們來了解一下這個插件的基本情況。

mybatis-pagehelper

如果各位小夥伴使用過:, 那麼對於這個就很容易理解了,它其實就是基於來實現的,當攔截到原始SQL之後,對SQL進行一次改造處理。
我們來看看我們自己代碼中的實現,根據springboot編碼三部曲:

1.添加依賴

        <!-- 引入mybatis-pagehelper 插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.12</version>
        </dependency>

有同學就要問了,為什麼引入的這個依賴和我原來使用的不同?以前使用的是:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.10</version>
</dependency>

答案就在這裏:

我們使用的是springboot進行的項目開發,既然使用的是springboot,那我們完全可以用到它的自動裝配特性,作者幫我們實現了這麼一個,我們只需要參考示例來編寫就ok了。

2.改配置

# mybatis 分頁組件配置
pagehelper:
  helperDialect: mysql #插件支持12種數據庫,選擇類型
  supportMethodsArguments: true

3.改代碼

如下示例代碼:

    @Override
    public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("keyword", keyword);
        paramMap.put("sortby", sortby);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(searchProductDTOS)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

在我們查詢數據庫之前,我們引入了一句PageHelper.startPage(pageNumber, pageSize);,告訴mybatis我們要對查詢進行分頁處理,這個時候插件會啟動一個攔截器com.github.pagehelper.PageInterceptor,針對所有的query進行攔截,添加自定義參數和添加查詢數據總數。(後續我們會打印sql來證明。)

當查詢到結果之後,我們需要將我們查詢到的結果通知給插件,也就是PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);com.github.pagehelper.PageInfo是對插件針對分頁做的一個屬性包裝,具體可以查看)。

至此,我們的插件使用就已經結束了。但是為什麼我們在後面又封裝了一個對象來對外進行返回,而不是使用查詢到的PageInfo呢?這是因為我們實際開發過程中,為了數據結構的一致性做的一次結構封裝,你也可不實現該步驟,都是對結果沒有任何影響的。

SQL打印對比

2019-11-21 12:04:21 INFO  ProductController:134 - ============根據關鍵詞:西鳳 搜索列表==============
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ff449ba] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1980420239 wrapping com.mysql.cj.jdbc.ConnectionImpl@563b22b1] will not be managed by Spring
==>  Preparing: SELECT count(0) FROM products p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN (SELECT product_id, MIN(price_discount) AS priceDiscount FROM products_spec GROUP BY product_id) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西鳳%" 
==> Parameters: 
<==    Columns: count(0)
<==        Row: 32
<==      Total: 1
==>  Preparing: SELECT p.id as productId, p.product_name as productName, p.sell_counts as sellCounts, pi.url as imgUrl, tp.priceDiscount FROM product p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN ( SELECT product_id, MIN(price_discount) as priceDiscount FROM products_spec GROUP BY product_id ) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西鳳%" ORDER BY p.sell_counts DESC LIMIT ? 
==> Parameters: 10(Integer)

我們可以看到,我們的SQL中多了一個SELECT count(0),第二條SQL多了一個LIMIT參數,在代碼中,我們很明確的知道,我們並沒有显示的去搜索總數和查詢條數,可以確定它就是插件幫我們實現的。

源碼下載

下節預告

下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!