標籤: 網頁設計公司

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

    [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 是電子產業重要的元件?

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

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

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

  • 面向對象和面向過程詳解

    1.前言

    其實一直對面向過程和面向對象的概念和區別沒有很深入的理解,在自己不斷想完善自己的知識體系中,今天借這個時間,寫一篇博客。來深入的了解面向過程與面向對象!好記性不如爛筆頭!!  

    2.面向對象與面向過程的區別

    面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就可以了;面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描敘某個事物在整個解決問題的步驟中的行為。

    舉一個下五子棋通俗例子吧 哈哈哈 我感覺這兒例子很容易讓人理解

    面向過程的設計思路就是首先分析問題的步驟:

    1、開始遊戲,

    2、黑子先走,

    3、繪製畫面,

    4、判斷輸贏,

    5、輪到白子,

    6、繪製畫面,

    7、判斷輸贏,

    8、返回步驟2,

    9、輸出最後結果。

    把上面每個步驟用分別的函數來實現,問題就解決了。

    面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為

    1、黑白雙方,這兩方的行為是一模一樣的,

    2、棋盤系統,負責繪製畫面,

    3、規則系統,負責判定諸如犯規、輸贏等。第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子布局的變化,

    棋盤對象接收到了棋子的i變化就要負責在屏幕上面显示出這種變化,同時利用第三類對象(規則系統)來對棋局進行判定。

    可以明顯地看出,面向對象是以功能來劃分問題,而不是步驟。同樣是繪製棋局,這樣的行為在面向過程的設計中分散在了總多步驟中,很可能出現不同的繪製版本,因為通常設計人員會考慮到實際情況進行各種各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。

    功能上的統一保證了面向對象設計的可擴展性。比如要加入悔棋的功能,如果要改動面向過程的設計,那麼從輸入到判斷到显示這一連串的步驟都要改動,甚至步驟之間的循序都要進行大規模調整。如果是面向對象的話,只用改動棋盤對象就行了,棋盤系統保存了黑白雙方的棋譜,簡單回溯就可以了,而显示和規則判斷則不用顧及,同時整個對對象功能的調用順序都沒有變化,改動只是局部的。

    再比如我要把這個五子棋遊戲改為圍棋遊戲,如果你是面向過程設計,那麼五子棋的規則就分佈在了你的程序的每一個角落,要改動還不如重寫。但是如果你當初就是面向對象的設計,那麼你只用改動規則對象就可以了,五子棋和圍棋的區別不就是規則嗎?(當然棋盤大小好像也不一樣,但是你會覺得這是一個難題嗎?直接在棋盤對象中進行一番小改動就可以了。)而下棋的大致步驟從面向對象的角度來看沒有任何變化。

    當然,要達到改動只是局部的需要設計的人有足夠的經驗,使用對象不能保證你的程序就是面向對象,初學者或者很蹩腳的程序員很可能以面向對象之虛而行面向過程之實,這樣設計出來的所謂面向對象的程序很難有良好的可移植性和可擴展性

    三、面向過程與面向對象的優缺點
    很多資料上全都是一群很難理解的理論知識,整的我頭都大了,後來發現了一個比較好的文章,寫的真是太棒了,通俗易懂,想要不明白都難!

    用面向過程的方法寫出來的程序是一份蛋炒飯,而用面向對象寫出來的程序是一份蓋澆飯。所謂蓋澆飯,北京叫蓋飯,東北叫燴飯,廣東叫碟頭飯,就是在一碗白米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。我覺得這個比喻還是比較貼切的。

    蛋炒飯製作是把米飯和雞蛋混在一起炒勻。蓋澆飯呢,則是把米飯和蓋菜分別做好,你如果要一份紅燒肉蓋飯呢,就給你澆一份紅燒肉;如果要一份青椒土豆蓋澆飯,就給澆一份青椒土豆絲。

    蛋炒飯的好處就是入味均勻,吃起來香。如果恰巧你不愛吃雞蛋,只愛吃青菜的話,那麼唯一的辦法就是全部倒掉,重新做一份青菜炒飯了。蓋澆飯就沒這麼多麻煩,你只需要把上面的蓋菜撥掉,更換一份蓋菜就可以了。蓋澆飯的缺點是入味不均,可能沒有蛋炒飯那麼香。

    到底是蛋炒飯好還是蓋澆飯好呢?其實這類問題都很難回答,非要比個上下高低的話,就必須設定一個場景,否則只能說是各有所長。如果大家都不是美食家,沒那麼多講究,那麼從飯館角度來講的話,做蓋澆飯顯然比蛋炒飯更有優勢,他可以組合出來任意多的組合,而且不會浪費。

    蓋澆飯的好處就是”菜”“飯”分離,從而提高了製作蓋澆飯的靈活性。飯不滿意就換飯,菜不滿意換菜。用軟件工程的專業術語就是”可維護性“比較好,”飯” 和”菜”的耦合度比較低。蛋炒飯將”蛋”“飯”攪和在一起,想換”蛋”“飯”中任何一種都很困難,耦合度很高,以至於”可維護性”比較差。軟件工程追求的目標之一就是可維護性,可維護性主要表現在3個方面:可理解性、可測試性和可修改性。面向對象的好處之一就是顯著的改善了軟件系統的可維護性。
    看了這篇文章,簡單的總結一下!

    面向過程

    優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如嵌入式開發、 Linux/Unix等一般採用面向過程開發,性能是最重要的因素。
    缺點:沒有面向對象易維護、易復用、易擴展
    面向對象

    優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易於維護

    缺點:性能比面向過程低

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

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

  • 堡壘機的核心武器:WebSSH錄像實現

    堡壘機的核心武器:WebSSH錄像實現

    WebSSH終端錄像的實現終於來了

    前邊寫了兩篇文章和深入介紹了終端錄製工具Asciinema,我們已經可以實現在終端下對操作過程的錄製,那麼在WebSSH中的操作該如何記錄並提供後續的回放審計呢?

    一種方式是文章最後介紹的自動錄製審計日誌的方法,在主機上添加個腳本,每次連接自動進行錄製,但這樣不僅要在每台遠程主機添加腳本,會很繁瑣,而且錄製的腳本文件都是放在遠程主機上的,後續播放也很麻煩

    那該如何更好處理呢?下文介紹一種優雅的方式來實現,核心思想是不通過錄製命令進行錄製,而在Webssh交互執行的過程中直接生成可播放的錄像文件

    設計思路

    通過上邊兩篇文章的閱讀,我們已經知道了Asciinema錄像文件主要由兩部分組成:header頭和IO流數據

    header頭位於文件的第一行,定義了這個錄像的版本、寬高、開始時間、環境變量等參數,我們可以在websocket連接創建時將這些參數按照需要的格式寫入到文件

    header頭數據如下,只有開頭一行,是一個字典形式

    {"version": 2, "width": 213, "height": 55, "timestamp": 1574155029.1815443, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}, "title": "ops-coffee"}

    整個錄像文件除了第一行的header頭部分,剩下的就都是輸入輸出的IO流數據,從websocket連接建立開始,隨着操作的進行,IO流數據是不斷增加的,直到整個websocket長連接的結束,那就需要在整個WebSSH交互的過程中不斷的往錄像文件追加輸入輸出的內容

    IO流數據如下,每一行一條,列表形式,分別表示操作時間,輸入或輸出(這裏我們為了方便就寫固定字符串輸出),IO數據

    [0.2341010570526123, "o", "Last login: Tue Nov 19 17:11:30 2019 from 192.168.105.91\r\r\n"]

    似乎很完美,按照上邊的思路錄像文件就應該沒有問題了,但還有一些細節需要處理

    首先是需要歷史連接列表,在這個列表裡可以看到什麼時間,哪個用戶連接了哪台主機,當然也需要提供回放功能,新建一張表來記錄這些信息

    class Record(models.Model):
        create_time = models.DateTimeField(auto_now_add=True, verbose_name='創建時間')
    
        host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name='主機')
        user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用戶')
    
        filename = models.CharField(max_length=128, verbose_name='錄像文件名稱')
    
        def __str__(self):
            return self.host

    其次還需要考慮的一個問題是header和後續IO數據流要寫入同一個文件,這就需要在整個websocket的連接過程中有一個固定的文件名可被讀取,這裏我使用了主機+用戶+當前時間作為文件名,同一用戶在同一時間不能多次連接同一主機,這樣可保證文件名不重複,同時避免操作寫入錯誤的錄像文件,文件名在websocket建立時初始化

    def __init__(self, host, user, websocket):
        self.host = host
        self.user = user
    
        self.time = time.time()
        self.filename = '%s.%s.%d.cast' % (host, user, self.time)

    IO流數據會持續不斷的寫入文件,這裏以一個獨立的方法來處理寫入

    def record(self, type, data):
        RECORD_DIR = settings.BASE_DIR + '/static/record/'
        if not os.path.isdir(RECORD_DIR):
            os.makedirs(RECORD_DIR)
    
        if type == 'header':
            Record.objects.create(
                host=Host.objects.get(id=self.host),
                user=self.user,
                filename=self.filename
            )
    
            with open(RECORD_DIR + self.filename, 'w') as f:
                f.write(json.dumps(data) + '\n')
        else:
            iodata = [time.time() - self.time, 'o', data]
            with open(RECORD_DIR + self.filename, 'a', buffering=1) as f:
                f.write((json.dumps(iodata) + '\n'))

    record接收兩個參數type和data,type標識本次寫入的是header頭還是IO流,data則是具體的數據

    header只需要執行一次寫入,所以將其放在ssh的connect方法中,只在ssh連接建立時執行一次,在執行header寫入時同時往數據庫插入新的歷史記錄數據

    調用record方法寫入header

    def connect(self, host, port, username, authtype, password=None, pkey=None,
                term='xterm-256color', cols=80, rows=24):
        ...
    
        # 構建錄像文件header
        self.record('header', {
            "version": 2,
            "width": cols,
            "height": rows,
            "timestamp": self.time,
            "env": {
                "SHELL": "/bin/bash",
                "TERM": term
            },
            "title": "ops-coffee"
        })

    IO流數據則需要與返回給前端的數據保持一致,這樣就能保證前端显示什麼錄像就播放什麼了,所以所有需要返回前端數據的地方都同時寫入錄像文件即可

    調用record方法寫入io流數據

    def connect(self, host, port, username, authtype, password=None, pkey=None,
                term='xterm-256color', cols=80, rows=24):
        ...
    
        # 連接建立一次,之後交互數據不會再進入該方法
        for i in range(2):
            recv = self.ssh_channel.recv(65535).decode('utf-8', 'ignore')
            message = json.dumps({'flag': 'success', 'message': recv})
            self.websocket.send(message)
    
            self.record('iodata', recv)
    
    ...
    
    def _ssh_to_ws(self):
        try:
            with self.lock:
                while not self.ssh_channel.exit_status_ready():
                    data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
                    if len(data) != 0:
                        message = {'flag': 'success', 'message': data}
                        self.websocket.send(json.dumps(message))
    
                        self.record('iodata', data)
                    else:
                        break
        except Exception as e:
            message = {'flag': 'error', 'message': str(e)}
            self.websocket.send(json.dumps(message))
            self.record('iodata', str(e))
            
            self.close()

    由於命令執行與返回都是多線程的操作,這就會導致在寫入文件時出現文件亂序影響播放的問題,典型的操作有vim、top等,通過加鎖self.lock可以順利解決

    最後歷史記錄頁面,當用戶點擊播放按鈕時,調用js彈出播放窗口

    <div class="modal fade" id="modalForm">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-body" id="play">
          </div>
        </div>
      </div>
    </div>
    
    // 播放錄像
    function play(host,user,time,file) {
      $('#play').html(
        '<asciinema-player id="play" title="WebSSH Record" author="ops-coffee.cn" author-url="https://ops-coffee.cn" author-img-url="/static/img/logo.png" src="/static/record/'+file+'" speed="3" '+
        'idle-time-limit="2" poster="data:text/plain,\x1b[1;32m'+time+
        '\x1b[1;0m用戶\x1b[1;32m'+user+
        '\x1b[1;0m連接主機\x1b[1;32m'+host+
        '\x1b[1;0m的錄像記錄"></asciinema-player>'
      )
    
      $('#modalForm').modal('show');
    }

    asciinema-player標籤的詳細參數介紹可以看這篇文章

    演示與總結

    在寫入文件的方案中,考慮了實時寫入和一次性寫入,實時寫入就像上邊這樣,所有的操作都會實時寫入錄像文件,好處是錄像不丟失,且能在操作的過程中進行實時的播放,缺點也很明顯,就是會頻繁的寫文件,造成IO開銷

    一次性寫入可以在用戶操作的過程中將錄像數據寫入內存,在websocket關閉時一次性異步寫入到文件中,這種方案在最終寫入文件時可能因為種種原因而失敗,從而導致錄像丟失,還有個缺點是當你WebSSH操作時間過長時,會導致內存的持續增加

    兩種方案一種是對磁盤的消耗另一種是對內存的消耗,各有利弊,當然你也可以考慮批量寫入,例如每分鐘寫一次文件,一分鐘之內的保存在內存中,平衡內存和磁盤的消耗,期待你的實現

    相關文章推薦閱讀:

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

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • NetCore3.0 文件上傳與大文件上傳的限制

    NetCore文件上傳兩種方式

      NetCore官方給出的兩種文件上傳方式分別為“緩衝”、“流式”。我簡單的說說兩種的區別,

      1.緩衝:通過模型綁定先把整個文件保存到內存,然後我們通過IFormFile得到stream,優點是效率高,缺點對內存要求大。文件不宜過大。

      2.流式處理:直接讀取請求體裝載后的Section 對應的stream 直接操作strem即可。無需把整個請求體讀入內存,

    以下為官方微軟說法

    緩衝

      整個文件讀入 IFormFile,它是文件的 C# 表示形式,用於處理或保存文件。 文件上傳所用的資源(磁盤、內存)取決於併發文件上傳的數量和大小。 如果應用嘗試緩衝過多上傳,站點就會在內存或磁盤空間不足時崩潰。 如果文件上傳的大小或頻率會消耗應用資源,請使用流式傳輸。

    流式處理   

      從多部分請求收到文件,然後應用直接處理或保存它。 流式傳輸無法顯著提高性能。 流式傳輸可降低上傳文件時對內存或磁盤空間的需求。

    文件大小限制

      說起大小限制,我們得從兩方面入手,1應用服務器Kestrel 2.應用程序(我們的netcore程序),

    1.應用服務器Kestre設置

      應用服務器Kestrel對我們的限制主要是對整個請求體大小的限制通過如下配置可以進行設置(Program -> CreateHostBuilder),超出設置範圍會報 BadHttpRequestException: Request body too large 異常信息

    public static IHostBuilder CreateHostBuilder(string[] args) =>
               Host.CreateDefaultBuilder(args)
                   .ConfigureWebHostDefaults(webBuilder =>
                   {
                       webBuilder.ConfigureKestrel((context, options) =>
                       {
                           //設置應用服務器Kestrel請求體最大為50MB
                           options.Limits.MaxRequestBodySize = 52428800;
                       });
                       webBuilder.UseStartup<Startup>();
    });

    2.應用程序設置

      應用程序設置 (Startup->  ConfigureServices) 超出設置範圍會報InvalidDataException 異常信息

    services.Configure<FormOptions>(options =>
     {
                 options.MultipartBodyLengthLimit = long.MaxValue;
     });

    通過設置即重置文件上傳的大小限制。

    源碼分析

      這裏我主要說一下 MultipartBodyLengthLimit  這個參數他主要限制我們使用“緩衝”形式上傳文件時每個的長度。為什麼說是緩衝形式中,是因為我們緩衝形式在讀取上傳文件用的幫助類為 MultipartReaderStream 類下的 Read 方法,此方法在每讀取一次後會更新下讀入的總byte數量,當超過此數量時會拋出  throw new InvalidDataException($Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.);  主要體現在 UpdatePosition 方法對 _observedLength  的判斷

    以下為 MultipartReaderStream 類兩個方法的源代碼,為方便閱讀,我已精簡掉部分代碼

    Read

    public override int Read(byte[] buffer, int offset, int count)
     {
              
              var bufferedData = _innerStream.BufferedData;
          int read;
          read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));
              return UpdatePosition(read);
    }

    UpdatePosition

    private int UpdatePosition(int read)
            {
                _position += read;
                if (_observedLength < _position)
                {
                    _observedLength = _position;
                    if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault())
                    {
                        throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");
                    }
                }
                return read;
    }

    通過代碼我們可以看到 當你做了 MultipartBodyLengthLimit 的限制后,在每次讀取後會累計讀取的總量,當讀取總量超出

     MultipartBodyLengthLimit  設定值會拋出 InvalidDataException 異常,

    最終我的文件上傳Controller如下

      需要注意的是我們創建 MultipartReader 時並未設置 BodyLengthLimit  (這參數會傳給 MultipartReaderStream.LengthLimit )也就是我們最終的限制,這裏我未設置值也就無限制,可以通過 UpdatePosition 方法體現出來

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.WebUtilities;
    using Microsoft.Net.Http.Headers;
    using System.IO;
    using System.Threading.Tasks;
     
    namespace BigFilesUpload.Controllers
    {
        [Route("api/[controller]")]
        public class FileController : Controller
        {
            private readonly string _targetFilePath = "C:\\files\\TempDir";
     
            /// <summary>
            /// 流式文件上傳
            /// </summary>
            /// <returns></returns>
            [HttpPost("UploadingStream")]
            public async Task<IActionResult> UploadingStream()
            {
     
                //獲取boundary
                var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value;
                //得到reader
                var reader = new MultipartReader(boundary, HttpContext.Request.Body);
                //{ BodyLengthLimit = 2000 };//
                var section = await reader.ReadNextSectionAsync();
     
                //讀取section
                while (section != null)
                {
                    var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
                    if (hasContentDispositionHeader)
                    {
                        var trustedFileNameForFileStorage = Path.GetRandomFileName();
                        await WriteFileAsync(section.Body, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                    }
                    section = await reader.ReadNextSectionAsync();
                }
                return Created(nameof(FileController), null);
            }
     
            /// <summary>
            /// 緩存式文件上傳
            /// </summary>
            /// <param name=""></param>
            /// <returns></returns>
            [HttpPost("UploadingFormFile")]
            public async Task<IActionResult> UploadingFormFile(IFormFile file)
            {
                using (var stream = file.OpenReadStream())
                {
                    var trustedFileNameForFileStorage = Path.GetRandomFileName();
                    await WriteFileAsync(stream, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                }
                return Created(nameof(FileController), null);
            }
     
     
            /// <summary>
            /// 寫文件導到磁盤
            /// </summary>
            /// <param name="stream"></param>
            /// <param name="path">文件保存路徑</param>
            /// <returns></returns>
            public static async Task<int> WriteFileAsync(System.IO.Stream stream, string path)
            {
                const int FILE_WRITE_SIZE = 84975;//寫出緩衝區大小
                int writeCount = 0;
                using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write, FILE_WRITE_SIZE, true))
                {
                    byte[] byteArr = new byte[FILE_WRITE_SIZE];
                    int readCount = 0;
                    while ((readCount = await stream.ReadAsync(byteArr, 0, byteArr.Length)) > 0)
                    {
                        await fileStream.WriteAsync(byteArr, 0, readCount);
                        writeCount += readCount;
                    }
                }
                return writeCount;
            }
     
        }
    }

     

     總結:

    如果你部署 在iis上或者Nginx 等其他應用服務器 也是需要注意的事情,因為他們本身也有對請求體的限制,還有值得注意的就是我們在創建文件流對象時 緩衝區的大小盡量不要超過netcore大對象的限制。這樣在併發高的時候很容易觸發二代GC的回收.

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

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

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

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

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

  • 電動車電池成長,帶動致茂營收倍翻

    受惠於電動車動力電池需求增加,致茂電子的七月合併營收創下歷史新高。今年前七個月的累計營收更已與去年全年相當。

    致茂電子表示,因電動車動力電池製造的關鍵技術(turnkey solutions)銷售蓬勃,帶動七月營收大漲,合併營收達新台幣16.2億元,不僅較上月成長71%、更較去年七月成長101%,營收數字創下歷史新高。

    此外,母公司的7月單月營收也有128%的月增與172%的年增,達新台幣12.6億元。強勁的需求使致茂今年前七個月的合併營收年增27%來到69.1億新台幣;母公司前七個月的累計營收新台幣45億元,也已相當於去年全年水準。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • 傳蘋果電動汽車將採用韓國公司的電池技術

    傳蘋果電動汽車將採用韓國公司的電池技術

    根據行業消息,蘋果近期與一家韓國電池開發商簽署了保密協議,聯合為代號為“泰坦”的汽車專案開發電池。從今年初開始,他們一直在韓國做行政工作。一名蘋果員工一直在這家韓國公司進行參觀活動,他屬於與蘋果電動汽車電池開發相關的部門。  
      業界認為,這家韓國公司並不是唯一一家負責蘋果電池開發的公司。不過,有消息稱,儘管蘋果從一開始就從完全不同的設計、功能以及性能角度來開發電池,但是他們仍舊一直在挖掘創新技術。業界相信,蘋果專注于開發出只能存在於蘋果自動駕駛汽車的創新電池技術。   這家韓國電池開發商由大約20名電池專家組成,持有空芯電池的國際專利技術。這些電池是圓柱形鋰離子二次電池,有兩根手指那麼厚,不同於其它空芯電池。蘋果並未選擇當前電動汽車普遍使用的標準圓形或矩形電池,但計畫根據韓國公司的空芯電池技術為其電動汽車開發自主電池。   文章來源:鳳凰科技

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

  • 續航力600公里的特斯拉 即將問世?

    續航力600公里的特斯拉 即將問世?

    續航力是電動車最受關注的性能,而作為全球電動車龍頭廠商的特斯拉(Tesla),似乎也準備好要推出續航力更久的車款了。跟據了解,新的特斯拉電動車可能搭載100kW的電池,續航力最遠可達611公里。

    《癮科技》中文版指出,德國監管機關的資料中可查到Model S與Model X的100D與P100D型號的相關資訊;而根據特斯拉為車款型號命名的邏輯,這可能暗示特斯拉將推出搭載100kW電池的車款。

    100kW 的電池搭配Model S,預計最高續航里程可來到611公里,比90D的續航里程多了100公里以上。若搭配Model X,續航里程也可來到480 公里之多。這樣的續航力,將能有效減輕美國車主對駕駛特斯拉跨州旅行的疑慮。

    (照片來源:Tesla 臉書專頁)

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

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

  • 計算機圖形學—— 隱藏線和隱藏面的消除(消隱算法)

    計算機圖形學—— 隱藏線和隱藏面的消除(消隱算法)

     

    一、概述

    由於投影變換失去了深度信息,往往導致圖形的二義性。要消除二義性,就必須在繪製時消除被遮擋的不可見的線或面,習慣上稱作消除隱藏線和隱藏面(或可見線判定、可見面判定),或簡稱為消隱。經過消隱得到的投影圖稱為物體的真實感圖形。

    下面這個圖就很好體現了這種二義性。

    消隱后的效果圖:

    消隱算法的分類

    所有隱藏面消隱算法必須確定:
    在沿透視投影的投影中心或沿平行投影的投影方向看過去哪些邊或面是可見的

    兩種基本算法

    1、以構成圖像的每一個像素為處理單元,對場景中的所有表面,確定相對於觀察點是可見的表面,用該表面的顏色填充該像素.
    適於面消隱。

    算法步驟:

    a.在和投影點到像素連線相交的表面中,找到離觀察點最近的表面;
    b.用該表面上交點處的顏色填充該像素;

    2、以三維場景中的物體對象為處理單元,在所有對象之間進行比較,除去完全不可見的物體和物體上不可見的部分.適於面消隱也適於線消隱。

    算法步驟:
    a.判定場景中的所有可見表面;
    b.用可見表面的顏色填充相應的像素以構成圖形;

    提醒注意

    1.假定構成物體的面不能相互貫穿,也不能有循環遮擋的情況。
    2.假定投影平面是oxy平面,投影方向為z軸的負方向。

    如果構成物體的面不滿足該假定,可以把它們剖分成互不貫穿和不循環遮擋的情況。
    例如,用圖b中的虛線便可把原來循環遮擋的三個平面,分割成不存在循環遮擋的四個面。  

    二、可見面判斷的有效技術

    1、邊界盒

    指能夠包含該物體的一個幾何形狀(如矩形/圓/長方體等),該形狀有較簡單的邊界。

     

    邊界盒技術用於判斷兩條直線是否相交。

    進一步簡化判斷

    2、後向面消除(Back-face Removal)

    思路:把顯然不可見的面去掉,減少消隱過程中的直線求交數目

     

     

    如何判斷:根據定義尋找外(或內)法向,若外法向背離觀察者,或內法向指向觀察者,則該面為後向面。

     

     

     

     

     

     

     

     

     

     

    注意:如果多邊形是凸的,則可只取一個三角形計算有向面積sp。如果多邊形不是凸的,只取一個三角形計算有向面積sp可能會出現錯誤,即F所在的面為前向面也可能出現sp≥0的情況,因此,需按上式計算多邊形F的有向面積。如果sp ≥0,則F所在的面為後向面。

    3、非垂直投影轉化成垂直投影

    物體之間的遮擋關係與投影中心和投影方向有着密切的關係,因此,對物體的可見性判定也和投影方式有密切的關係。

    垂直投影的優點:進行投影時可以忽略z值,即:實物的(x,y)可直接做為投影后的二維平面上的坐標(x,y)

    上述討論說明,垂直投影比非垂直投影容易實現,並且計算量小。因此在進行消隱工作之前,首先應將非垂直投影轉換成垂直投影,從而降低算法的複雜性,提高運算速度。

    如何把透視投影變為垂直投影,其本質是把稜台變成長方體。

    三、基於窗口的子分算法(Warnack算法)

    是一種分而治之(Divide-Conquer)的算法。

     

    1、關係判斷

    2、可見性判斷

    3、分隔結束條件

    4、提高效率的有效的處理技術

    5、算法描述

    用多邊形的邊界對區域作劃分,其目的是盡量減少對區域劃分的次數--利用裁剪算法

     

    四、八叉樹算法

    為了生成真實感圖形,關鍵問題之一就是對圖像空間的每一個像素進行處理。從場景中所有的在該像素上有投影的表面中確定相對於觀察點是可見表面。為了提高算法效率,自然是希望從可能在像素上有投影的面片中尋找可見表面。八叉樹算法是快速尋找可見面的一種有效方法,是一種搜索算法。

    基本思想:將能夠包含整個場景的立方體,即八叉樹的根結點,按照x,y,z三個方向中的剖面分割成八個子立方體,稱為根結點的八個子結點。對每一個子立方體,如果它包含的表面片少於一個給定的值,則該子立方體為八叉樹的終端結點,否則為非終端結點並將其進一步分割成八個子立方體;重複上述過程,直到每個小立方體所包含的表面片少於一個給定的值,分割即告終止。

     

     

    那麼對於上述圖所示,視圖平面法向量(1,1,1)那麼此時它的一個排列是0,1,2,4,3,5,6,7,即最遠的八分體是0,與八分體0共享一個面的三個相鄰八分體是1,2和4,與最近八分體7的3個相鄰八分體是3,5和6。

     

    五、Z緩衝器算法

    1、算法描述

    z緩衝器算法是最簡單的隱藏面消除算法之一。
    基本思想:對屏幕上每一個像素點,過像素中心做一條投影線,找到此投影線與所有多邊形交點中離觀察者最近的點,此點的屬性(顏色或灰度)值即為這一屏幕像素點的屬性值。

    需要兩個緩衝器數組,即:z緩衝器數組和幀緩衝器數組,分別設為 Zdepth[ ][ ] 與  Frame[ ][ ]
    z緩衝器是一組存貯單元,其單元個數和屏幕上像素的個數相同,也和幀緩衝器的單元個數相同,它們之間一一對應。
    幀緩衝器每個單元存放對應像素的顏色值;z緩衝器每個單元存放對應像素的深度值;

    2、算法實現

     

    算法的複雜性正比於m*n*N,在屏幕大小即m*n一定的情況下,算法的計算量只和多邊形個數N成正比

    3、優缺點

     z-Buffer算法沒有利用圖形的相關性和連續性,這是z-Buffer算法的嚴重缺陷,更為嚴重的是,該算法是像素級上的消隱算法。

     六、掃描線z緩衝器算法

    1、算法描述

    將z緩衝器的單元數置為和一條掃描線上的像素數目相同。
    從最上面的一條掃描線開始工作,向下對每一條掃描線作如下處理:

     

    掃描線算法也屬於圖像空間消隱算法。該算法可以看作是多邊形區域填充里介紹過的邊相關掃描線填充算法的延伸。不同的是在消隱算法中處理的是多個面片,而多邊形填充中是對單個多邊形面進行填充。

    2、數據結構

    對每個多邊形,檢查它在oxy平面上的投影和當前掃描線是否相交?
    若不相交,則不考慮該多邊形。
    如果相交,則掃描線和多邊形邊界的交點是成對地出現
    每對交點中間的像素計算多邊形所在平面對應點的深度(即z值),並和z緩衝器中相應單元存放的深度值作比較
    若前者大於後者,則z緩衝器的相應單元內容要被求得的平面深度代替,幀緩衝器相應單元的內容也要換成該平面的屬性。
    對所有的多邊形都作上述處理后,幀緩衝器中這一行的值便反應了消隱后的圖形。
    對幀緩衝器每一行的單元都填上相應內容后就得到了整個消隱后的圖。

    每處理一條掃描線,都要檢查各多邊形是否和該線相交,還要計算多邊形所在平面上很多點的z值,需要花費很大的計算
    為了提高算法效率,採用跟多邊形掃描轉換中的掃描線算法類似的數據結構和算法.

    多邊形Y表

     

    實際上是一個指針數組 ,每個表的深度和显示屏幕行數相同.將所有多邊形存在多邊形Y表中,根據多邊形頂點中Y坐標最大值,插入多邊形Y表中的相應位置,多邊形Y表中保存多邊形的序號和其頂點的最大y坐標.

    邊Y表

     要注意:Δx是下一條掃描線與邊交點的x減去當前的掃描線與邊交點的x。

    多邊形活化表

    邊對活化表

    其實這裏最難理解的就是Δyl和Δxr了,這裏的意思就是當前掃描線所處的y值和與該掃描線相交邊的最小y值的差值。

    就比如說掃描線y=6,與第一個三角形有兩個交點,左交點(4,6),右交點(7,6)那麼Δyl=6-3  Δyr=6-3

    3、重溫算法目標

     對每一條掃描線,檢查對每個多邊形的投影是否相交,如相交則交點成對出現,對每對交點中間的每個像素計算多邊形所在平面對應點的深度(即z值),並和z緩衝器中相應單元存放的深度值作比較,若前者大於後者,則z緩衝器的相應單元內容要被求得的平面深度代替,幀緩衝器相應單元的內容也要換成該平面的屬性。
    對所有的多邊形都作上述處理后,幀緩衝器中這一行的值便反應了消隱后的圖形,對幀緩衝器每一行的單元都填上相應內容后也就得到了整個消隱后的圖。

    4、算法步驟

     

    算法描述如下

    七、優先級排序表算法

    1、算法思想

    優先級排序表算法按多邊形離觀察者的遠近來建立一個多邊形排序表,距觀察者遠的優先級低,放在表頭;近的優先級高,放在表尾
    從優先級低的多邊形開始,依次把多邊形的顏色填入幀緩衝存儲器中
    表中距觀察者近的元素覆蓋幀緩衝存儲器中原有的內容
    當優先級最高的多邊形的圖形送入幀緩衝器后,整幅圖形就形成了
    類似於油畫家繪畫過程,因此又稱為油畫家算法。

     

     

     

     

     

     

    2、算法的優缺點

    算法的優點:
    簡單,容易實現,並且可以作為實現更複雜算法的基礎;
    缺點:
    只能處理不相交的面,而且深度優先級表中面的順序可能出錯.

    該算法不能處理某些特殊情況。

     

     

    解決辦法:把P沿Q平面一分為二,從多邊形序列中把原多邊形P去掉,把分割P生成的兩個多邊形加入鏈表中。具體實現時,當離視點最遠的多邊形P和其他多邊形交換時,要對P做一標誌,當有標誌的多邊形再換成離視點最遠的多邊形時,則說明出現了上述的現象,可用分割方法進行處理 。

    用來解決動態显示問題時,可大大提高效率

    八、光線投射算法

    1、算法原理

    要處理的場景中有無限多條光線,從採樣的角度講我們僅對穿過像素的光線感興趣,因此,可考慮從像素出發,逆向追蹤射入場景的光線路徑

     

     

     

     

     

    2、算法實現

    由視點出發穿過觀察平面上一像素向場景發射一條射線
    求出射線與場景中各物體表面的交點
    離視點最近的交點的顏色即為像素要填的顏色。
    光線投射算法對於包含曲面,特別是包含球面的場景有很高的效率。

     

     

     

     

     

     

     

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

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • 從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十一Swagger使用一

    從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十一Swagger使用一

     一.未使用Swagger狀況

      相信無論是前端開發人員還是後端開發人員,都或多或少都被接口文檔折磨過,前端經常抱怨後端給的接口文檔或與實際情況不一致。後端又覺得編寫及維護接口文檔會耗費不少精力,經常來不及更新。 其實無論是前端調用後端,還是後端調用後端,都期望有一個好的接口文檔。但是這個接口文檔對於程序員來說,就跟註釋一樣,經常會抱怨別人寫的代碼沒有寫註釋,然而自己寫起代碼起來,最討厭的,也是寫註釋。 所以僅僅只通過強制來規範大家是不夠的,隨着時間推移,版本迭代,接口文檔往往很容易就跟不上代碼了

     二.使用Swagger狀況

      Swagger 提供了一個可視化的UI頁面展示描述文件,其中包括接口的調用,接口所需參數(header,body,url.params),接口說明,參數說明等。接口的調用方、測試、項目經理等都可以在該頁面中對相關接口進行查閱和做一些簡單的接口請求。只要在項目框架搭建時,對Swagger 進行了配置,後面持續迭代的時候,只會花很小代價去維護代碼、接口文檔以及Swagger描述文件。因為一旦接口發生改變,程序重新部署,接口文檔會重新生成對應新的文檔。

     三.如何使用?

      在NetCore項目中怎麼去使用Swagger來生成接口文檔呢?

      首先在 webApi 啟動項目 上 右鍵 點擊管理Nuget程序包, 安裝  Swashbuckle.AspNetCore ,然後到  Startup 中添加引用  using Swashbuckle.AspNetCore.Swagger; 

      在ConfigureServices方法中添加以下代碼

                #region Swagger
    
                services.AddSwaggerGen(options =>
                {
                    options.SwaggerDoc("v1", new Info
                    {
                        Version = "v1",
                        Title = "API Doc",
                        Description = "作者:Levy_w_Wang",
                        //服務條款
                        TermsOfService = "None",
                        //作者信息
                        Contact = new Contact
                        {
                            Name = "levy",
                            Email = "levy_w_wang@qq.com",
                            Url = "https://www.cnblogs.com/levywang"
                        },
                        //許可證
                        License = new License
                        {
                            Name = "tim",
                            Url = "https://www.cnblogs.com/levywang"
                        }
                    });
    
                    #region XmlComments
    
                    var basePath1 = Path.GetDirectoryName(typeof(Program).Assembly.Location);//獲取應用程序所在目錄(絕對,不受工作目錄(平台)影響,建議採用此方法獲取路徑)
                    //獲取目錄下的XML文件 显示註釋等信息
                    var xmlComments = Directory.GetFiles(basePath1, "*.xml", SearchOption.AllDirectories).ToList();
    
                    foreach (var xmlComment in xmlComments)
                    {
                        options.IncludeXmlComments(xmlComment);
                    }
                    #endregion
    
                    options.DocInclusionPredicate((docName, description) => true);
    
                    options.IgnoreObsoleteProperties();//忽略 有Obsolete 屬性的方法
                    options.IgnoreObsoleteActions();
                    options.DescribeAllEnumsAsStrings();
                });
                #endregion

    上面寫的循環是因為項目中可能有多個控制器類庫,為的是排除這種情況

    接下來,再到 Configure 方法中添加:

                #region Swagger
    
                app.UseSwagger(c => { c.RouteTemplate = "apidoc/{documentName}/swagger.json"; });
                app.UseSwaggerUI(c =>
                {
                    c.RoutePrefix = "apidoc";
                    c.SwaggerEndpoint("v1/swagger.json", "ContentCenter API V1");
                    c.DocExpansion(DocExpansion.Full);//默認文檔展開方式
                });
    
                #endregion

    這裏使用了 RoutePrefix  屬性,為的是改變原始打開接口文檔目錄,原始路徑為 swagger/index.html ,現在為 /apidoc/index.html 

    這個時候在需要輸出註釋的控制器類庫屬性 中設置如下信息,並添加上相關註釋

    然後運行起來,打開本地地址加上  /apidoc/index.html  就可以看到效果,

    特別提醒:如果打開下面這個界面能正常显示,但是提示  Fetch errorInternal Server Error v1/swagger.json  錯誤,說明有方法未指明請求方式,如 HttpGet HttpPost HttpPut 等,找到並指明,重新運行就正常了

     

      點擊方法右上角的 Try it out ,試下調用接口,然後點擊Exectue,執行查看結果,能得到後端方法返回結果就說明成功。

    特別說明:有接口不需要展示出去的時候,可以在方法上添加屬性 Obsolete ,這樣就不會显示出來。 前提:有前面ConfigureServices中 後面的 忽略 有Obsolete 屬性的方法 設置才行!!!

     

     最後可以看到接口返回數據正常,並且也能看到接口響應請求嘛等等信息,一個接口應該返回的信息也都有显示。

     

    總結:

    本文從為開發人員手寫api文檔的痛楚,從而引申出Swagger根據代碼自動生成出文檔和註釋,

    並且還可以將不需要的方法不显示等設置。然後進行了簡單的測試使用 。

    但是!!一般後端方法都有token等驗證,需要在header中添加token、sid等字段來驗證用戶,保障安全性,

    該設置將在下一章節中寫!

     下一章

    以上若有什麼不對或可以改進的地方,望各位指出或提出意見,一起探討學習~

    有需要源碼的可通過此 鏈接拉取 覺得還可以的給個 start 和點個 下方的推薦哦~~謝謝!

     

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

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

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

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

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

  • 高質量App的架構設計與思考!

    高質量App的架構設計與思考!

    最近在做一功能不大、業務也不複雜的小眾App,以往做App是發現自己從來沒有考慮過一些架構方面的問題,只是按照自己以往的習慣去寫代碼,忽略了App的設計。本次分享主要包含一些開發App的小經驗和技巧,來一次App開發與設計的分享。

    先和分享下一下實體類的設計與組織形式

    實體類的組織

    在做App開發的時候有很多的實體類,項目越複雜實體類就會越多,經過我的一番思考大致這可以將實體分為以下幾大數:

    • 面向數據庫的
    • 服務端返回的數據實體
    • 用於渲染View的實體(使用Databinding)

    一般情況下實體類的操作會經過以下步驟:

    1. App請求服務器獲取數據
    2. 將數據存入數據庫(可選)
    3. 渲染頁面展示數據

    現在的實體的產生只用在請求服務器數據的時候才需要新建,後續的數據庫、頁面渲染其實是可以使用一套實體:

    先不說這樣做的行不行,首先三個地方使用同一實體就會引起字段歧義比如服務器數據有Id、本地數據也有Id,那兩個id字段就有衝突了不得不改字段名。

    另一種情況渲染和數據本身並不會一一對應,有時候後端數據給的是一個純数字而前端頁面显示的是字符串兩個都對應不上,強行放在一起會起來更多的問題。

    所為實體類的的正確組織形式應該是:相互隔離、互不干擾

    數據實體的在渲染之前都需要準備好,比如在ViewModel中將int型的數據轉換成文本型的數據然後再使用Databinding+頁面渲染實體來渲染頁面。

    優雅的處理網絡數據

    現在Android開發使用的網絡庫大部分都是Okhttp + Retrofit,使用Retrofit網絡交互變的非常簡單一個Service接口就能搞定一切,美茲茲~~,現在大部分後端返回的數據都會是以下形式:

    {
        "code":0,
        "data": {},
        "msg": ""
    }

    雖然不能涵蓋所有,但還是可以非常贊的數據、消息、成功與否啥都有!對於前面主要是關注data字段,其他msgcode等都屬於輔助字段。前端對應的實體對象應該是這樣的(假代碼):

    public class ApiResponse<T> {
        private int code;
        private T data;
        private String msg;
    }

    對應的Service那就得定義成這樣(使用了RxJava):

    public intface UserService {
        @GET("/xx/{id}")
        Single<ApiResponse<UserInfo> getUserInfoById(@Path("id") Long userId);
    }

    從接口中可以看出來,方法的返回值就包了幾層,如果要拿data字段需要經過:ApiResponse -> UserInfo,而且在拿之前還要判斷code字段:

    
    ...
    
    if(ApiResponse.code == 0){
        UserInfo info = ApiResponse.getData();
    }
    
    ...
    

    為了消除這些冗餘的代碼可以使用CallAdapter來使Service方法返回的數據直接就是實體類:

    public intface UserService {
        @GET("/xx/{id}")
        Single<UserInfo> getUserInfoById(@Path("id") Long userId);
    }

    CallAdapter的代碼就不貼了,可以自行查找。這樣做帶來的另外一個問題就是業務代碼如何判斷接口是否成功或失敗,前端必需友好的把錯誤提示給用戶而不是一直搞個Loading在那裡瞎轉~~。現階段最方便的的錯誤傳遞方式是使用Java異常,前端可以定義業務異常網絡異常

    public class BizException extends RuntimeException {
        ...
    }

    CallAdapter中檢查ApiResponse的返回值是否成功:

    
    if(!ApiResponse != 0){
        throw new BizExcepiton(ApiResponse);
    }
    

    如果後端返回業務異常那前端就對應拋出一個BizExcepiton,如果是http錯誤如:404、400那可以拋出HttpException。除了BizExcepitonHttpException外還可使用特定的異常比如後端返回密碼錯誤異常:

    public class InvalidPasswordException extends BizException {
        ...
    }

    如需特殊處理,也可以滿足要求。

    健壯的數據層

    現在很多應用都開發使用MVVM開發模式數據層都使用Repository來表示,面向數據驅動的開發模式,頁面變化都需要隨着數據變更而更新,數據發生變化然後頁面再做出響應。Repository的拆分要細一點,不建議簡單的弄個UserRepository包含登陸、註冊、更新密碼等等操作,設計Repository的一些想法:

    1. 面向接口編程
    2. 保持單一原則
    3. 功能邊界要清晰(如:登陸、註冊可以分開)
    4. 業務邏輯盡可能的少(複雜的業務考慮Presenter)

    一個判斷是否是好的設計的辦法可以這樣:一個登陸頁面從Activive/Fragment到ViewModel再到Repository,有沒有多餘的代碼。比如上面說的UserRepository包含登陸、註冊但是在一個登陸頁面就不需要有註冊功能,從登陸頁面上來看註冊的代碼就是多餘的(有些App登陸/註冊在一個頁面的~~)。

    一個包含登陸、註冊的UserRepository簡單圖:

    另外一點是盡量將repository使用到的一些東西集中管理,可引入一個基礎的repository:

    public class SimpleRepository {
        
        protected final  <T> T getService(Class<T> clz){
            return Services.getService(clz);
        }
    }
    

    做為SimpleRepository的子類,就不需要考慮從哪裡獲取service的問題。

    簡潔的UI層

    UI層面可以分為ViewModel和View(Activity/Fragment), View的職責應當只有二點:

    1. 展示業務數據
    2. 收集業務數據

    例如一些數據的組織、判斷都不應該出現在View中比如:

     if (Strings.isNullOrEmpty(phone)) {
           ...
            return;
     }
    
     if (Strings.isNullOrEmpty(pwd)) {
            ...
            return;
      }

    像上面這類的代碼都不應該出現在View中,而在放置在ViewModel裏面,View只收集用戶數據傳遞給ViewModel由它來進行數據校驗。再比如像這樣的if/else代碼也應該放置在ViewModel中:

     int age = 10;
     String desc = "";
     if(age < 18){
        desc = "青年";
     }else if(age < 29){
        desc = "中年";
     }

    如果數據的显示和數據的收集過多,建議使用Databinding來進行雙向綁定數據。再搭配LiveData使View作為觀察者實時監聽數據變化:

    registerViewModel.getRegistryResult().observe(this, new SimpleObserver<RegistryInfo>(this));

    一旦數據發生變化LiveData就會通知Observer更新,通過DataBinding更新各個頁面數據。

    再說ViewModel應該只包含一些簡單的判斷、檢查、打通數據的代碼,如果業務過於複雜可以考慮加Presetner,如果真的超級複雜那可以反思下這個複雜的邏輯應不應該放在前端,能不能放在後端呢?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

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

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