M5 (project) - 01 - development document of distributed foundation of Shangsi Valley grain mall project

Posted by sidney on Fri, 04 Mar 2022 15:31:16 +0100

M5 (project) - 01 - development document of distributed foundation of Shangsi Valley grain mall project

Distributed Foundation

1, Environment construction

  1. Installation of various development software

Virtual machine: docker,mysql,redis

Host: maven, idea (back end), vscode (front end), git,NodeJS

Version and installation process

  1. Quickly build the basic front and rear frames

​ (1). Front end (vscode): Download and import Renren fast vue scaffold project of vue end of Renren open source background management system

Pit 1: when importing the Ren fast Vue project with vscode, you must pay attention to whether your NodeJS version matches the package If the node sass version in JSON does not match, the login page cannot be loaded. My NodeJS version is V12 18.2,package. * * node sass in JSON: "^ 4.14.1"**

​ (2). Back end (idea): Download and import Renren fast, Renren open source background management system and Renren generator for reverse engineering to quickly generate basic CRUD codes of Dao, entity, service and controller and mapper mapping files.

Before creating each micro service module, create a public module gulimall common to carry the public dependencies, beans, tool classes, etc. of each micro service. Each module only needs to be in POM This module can greatly simplify the common XML code. Due to the use of Renren fast, most dependency and tool classes and beans should refer to Renren fast and extract the required parts from Renren fast.

After setting up the public module, create each micro service module. Don't forget to create it in the POM of the parent project gulimall Reference each module in XML. Different microservice modules use reverse engineering to change the Renren generator module application Table name of url in YML and generator The module name and table prefix in the properties file. Finally, start the module respectively to test whether the interface can be accessed normally.

bug and tip:

① prompt - when using the Ren generator to generate quickly, be sure to see the following pages, and ensure that all tables are displayed on one page, so that the generated code will not be missing after selecting all.

② bug1 - when configuring yml files for each module, due to carelessness, the url in the datasource is indented incorrectly (the space after url: colon is deleted), which causes an exception to be thrown after running. Conclusion: the format of yml file must be paid attention to

③ bug2 - due to the VMware I use, I found that the IP of centos7 has been changing after switching on and off, and the host access is very troublesome. Set a fixed IP address for Baidu centos7 and lock the IP. However, after a while, it was found that xshell and sqlyog could not connect to the virtual machine normally. After opening VMware - > Edit - > virtual network editor, it was found that there were problems with the subnet IP of VMnet8 and the gateway IP in NAT settings (check whether the first three numbers are consistent with their own fixed IP). The problem was solved after modification. [the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-xpiwwno7-1646403290736) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 164492852234. PNG)]

  1. Build microservice components using SpringCloud, Alibaba and SpringCloud

![KP%I78%XQ_D[KRJNY_]ZPK](D:\life\qq\2073412335\FileRecv\MobileFile\Image\KP%I78%XQ_D[KRJNY_]ZPK.png)

3.1 configuring the registry using springcloud Alibaba Nacos (service discovery / registration)

① service discovery is a function that every micro service needs to include, so in the common module POM Create the dependency management of spring cloud Alibaba in XML to control the version, and rely on spring cloud starter Alibaba Nacos discovery in dependency

② secondly, configure the server address of service discovery in the yml file of each micro service module. The default is port 8848, spring cloud. nacos. discovery. Server addr: 127.0.0.1:8848 and spring application. Name service name. And use the annotation * * @ EnableDiscoveryClient on the startup class. In download (fast download address) http://8090top.cn-sh2.ufileos.com/centos/nacos-server-1.3.1.zip )And double-click startup. Under the bin directory of the Nacos server CMD start the Nacos server, and then start the micro service. Visit 127.0.0.1:8848/nacos * * log in with account code Nacos to check whether the micro service is registered in the registration center

3.2 use springcloud feign to remotely call other micro Services - member module to call the demo of coupon coupon module

Steps for calling other services remotely(Test call here coupon Coupon service)
 * 1.pom.xml Import from file spring-cloud-starter-openfeign,Make the service have the ability to call other micro services remotely
 * 2.Create a Feign package(The interface that holds the remote call service)And write an interface under the package, using annotations@FeignClient("Remote service name")tell springcloud The interface requires a service to be called remotely, and each method of the interface tells which request to call the service, that is, it is to sign the controller method corresponding to the service. Note that the request path is complete
 * 3.Enable the function of calling other services remotely, and use annotations on the startup class@EnableFeignClients(basePackages = "feign The full package name of the package"),This will automatically scan once the service is started feign Use under package@FeignClient Annotated interface
 * 4.Write a method to remotely call other services in its own control layer and inject feign The interface of other services needs to be called under the package. In this controller method, the injected interface can be used to call the target method to obtain the data returned by the target request in the remote call of other services. demo As shown below

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG lboklids-1646403290738) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644591989939. PNG)]

3.3 using springcloud Alibaba Nacos to realize dynamic configuration management

① import dependency. Nacos server takes into account both the registration center and the configuration center. Each micro service needs dynamic configuration management. It relies on spring cloud starter Alibaba Nacos config in the common module

② create a bootstrap under the resources of the service Properties file, which has higher execution priority than application properties. And configure the Nacos Config metadata: service name and configuration center address

spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

③ create a new configuration in the configuration list of Nacos server. The Data ID is the name of the configuration file (the default is the service name. properties). Select the configuration format properties and fill in the configuration information that needs dynamic management before publishing.

④ add @ RefreshScope annotation to the Controller (Controller layer) using the configuration file, so that you can dynamically modify the configuration file and publish it by clicking Edit in the configuration list of the Nacos server configuration center. Note: if the configuration center and applicaiton If properties are configured with the same item, the value of the configuration center takes precedence.

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-xzfq4xol-1646403290739) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646384322154. PNG)]

Add several concepts:

Namespace - the role of isolation configuration. Add different namespaces, idea 1 - apply to different environmental requirements, such as prop production environment, test test environment and dev development environment. The default is public, and the configuration under public takes priority to read by default. Idea 2 - configuration isolation applied to different micro services. If you need to declare the namespace to be read first, you need to use bootstrap Define spring in properties cloud. nacos. config. Namespace = namespace ID / namespace name (just fill in the name of the new version of Nacos, and the ID of the namespace needs to be filled in the old version)

Configuration set - a collection of all configurations
The configuration set ID-Data ID is similar to the file name. The official document naming convention is service name - environment name yml/properties

Configuration grouping - by default, all configuration sets belong to DEFAULT_GROUP. Different groups can be added as required, such as double 11618. Bootstrap can be modified in special periods Spring. In properties cloud. nacos. config. Group to specify the group.

To sum up - the combination of namespace and configuration grouping can distinguish between production environment and micro service. The official recommendation is to use namespaces to distinguish between production environments and to distinguish micro services through configuration grouping. Of course, you can also declare a namespace with the name of the microservice for each microservice, and define different production environments dev,prod, etc. under the configuration group, that is, use the namespace to distinguish the microservices and configure the group to distinguish the production environment.

If the content of the configuration file is too large and needs to be split, you can use bootstrap Using spring.com in properties cloud. nacos. config. Ext config [index 0 start] Data id = name of split configuration file, spring cloud. nacos. config. Ext config [index 0 start] Group = split profile group, spring cloud. nacos. config. Ext config [index 0 start] refresh=true/false whether to refresh automatically

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-djhdvdb7-1646403290741) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644648737804. PNG)]

3.4 using spring cloud gateway as API gateway (Webflux programming mode)

① first create the gulimall gateway module, add the gateway module, and introduce the gulimall common public dependency module into the pom file

Note - if mybatis plus is introduced into the public module, the data source must be defined in the configuration file, but the gateway module does not need the function here. You can exclude the automatic configuration @ springbootapplication related to the database by adding the exclude attribute to the startup class annotation (exclude = {datasourceautoconfiguration. Class}). Of course, you can also manually add the required dependencies in the pom file without introducing the common module.

② add * * @ EnableDiscoveryClient annotation on the startup class to enable the function of discovery service and set it in application Define the service registry address in properties spring cloud. nacos. discovery. Server addr = 127.0.0.1:8848 * *, application name and port number.

# apply name
spring.application.name=gulimall-gateway
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=88

Add the configuration center configuration file bootstrap Properties, defining application name / service name, configuration center address, namespace and other information.

spring.application.name=gulimall-gateway
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=dev
spring.cloud.nacos.config.group=DEFAULT_GROUP

2, Design and description of database table in basic part

1.gulimall_pms Library - commodity module

Property sheet pms_attr

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-s61wweaa-1646403290743) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646389682443. PNG)]

The specification parameter | basic attribute (attr_type=1) and sales attribute (attr_type=0) used to store goods, with both category Id field, i.e. category_ Id

Attribute grouping table pms_attr_group

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG ipsckk5y-1646403290745) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646389624761. PNG)]

It is used to store the grouping information of attributes, and also has the category Id field, that is, category_ Id. The relationship between attribute grouping and attribute is one to many, that is, an attribute grouping can have many attributes, but an attribute has only one attribute grouping.

Attribute and attribute grouping association table pms_attr_attrgroup_relation

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-sjka0ipi-1646403290747) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 164638998312. PNG)]

It is used to store the association relationship between attributes and attribute groups. The original intention of designing the intermediate table is not to use foreign keys.

Classification table pms_category

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-qbqnyot0-1646403290748) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646390279742. PNG)]

Used to store all classification information. The designed classification information is stored in a three-tier parent-child tree structure

Brand table pms_brand

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-qc0nmkqq-1646403290750) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646390543905. PNG)]

Used to store all brand information

Classification and brand association table pms_category_brand_relation

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-6ipoqumf-1646403290751) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646390633545. PNG)]

It is used to store the association between brands and classifications. Classifications and brands are one to many relationships, that is, a classification has multiple brands, but a brand has only one category. In addition, the brand name brand is redundantly designed_ Name and category name catelog_name, which reduces the demand and pressure of database search, but may face the distortion of brand name and category name. It is necessary to cascade update the intermediate table information when updating brand name and category name.

Standardized product unit Spu subject information table pms_spu_info

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-k28qq4ya-1646403290753) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 1646391410662. PNG)]

It is used to store spu subject information, including the classification Id and brand Id of spu.

Spu atlas table pms_spu_images

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-yr31zdcv-1646403290753) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646391687373. PNG)]

Pictures of goods used to store spu

Spu profile picture table pms_spu_info_desc

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG okdiaalb-1646403290754) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646391807415. PNG)]

Introduction picture for storing spu

Spu attribute and attribute value table pms_spu_attr_value

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG kyundo bj-1646403290755) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646395030716. PNG)]

It is used to set the specification parameter | the value of the basic attribute when receiving the spu, and whether the setting is displayed quickly.

Inventory unit Sku entity information table pms_sku_info
[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-cfft9qa3-1646403290756) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646391960684. PNG)]

It is used to store the subject information of the issued goods, and also has sku_id field. The relationship between standardized product unit spu and inventory unit sku is one to many, that is, a spu can have multiple corresponding SKUs, but a sku has only one corresponding spu.

Sku issued product atlas table pms_sku_images

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-cyg6frna-1646403290756) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 164639415229. PNG)]

Picture collection used to store issued goods, img_ The picture link stored in the URL field, default_img field 1 or 0 indicates whether it is the default display picture

A sku_id means that a sku product can correspond to multiple pictures, but a picture can only correspond to one sku_id

Sku issued goods property sheet pms_sku_sale_attr_value

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-e02vpvyq-1646403290757) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646395377653. PNG)]

It is used to receive the value of the sales attribute actually set when setting the issued goods.

2.gulimall_sms Library - coupon module

SKU full minus information table sms_sku_full_reduciton

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-bqpqfcwu-1646403290759) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646397192476. PNG)]

Used to store the full minus information of sku

SKU discount table sms_sku_ladder

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-ifjs7hdo-1646403290760) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646397310063. PNG)]

Discount information for storing sku

sku member price list sms_member_price

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-jdzbszpm-1646403290761) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 164639748583. PNG)]

Used to store members' sku price information

SPU integral table sms_spu_bounds

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-dt7vomn0-1646403290761) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 1646397539290. PNG)]

Used to store the growth points and gold coins of spu

3.gulimall_wms Library - inventory module

3, Business realization

1. Commodity module - three level menu - PMS_ CRUD of category table

1.1 pms_category commodity classification table insert data omitted
1.2 modifying the CategoryController method list() of the gulimall product module

Change the original request path from / list to / list/tree, and return the obtained classification data in a parent-child tree structure

/**
 * Find out all classifications and sub classifications and assemble them in a tree structure
 */
@RequestMapping("/list/tree")
//@RequiresPermissions("product:category:list")
public R list(){
    //The list () method can find all the classifications, but we need to find all the classifications and use the parent-child classification to assemble the categoryservice in a tree structure list();
    //So you need to customize a method listWithTree()
    List<CategoryEntity> entities = categoryService.listWithTree();
    return R.ok().put("data", entities);
}
1.3 create the CategoryService interface method listWithTree() and implement it in the implementation class CategoryServiceImpl
@Override
public List<CategoryEntity> listWithTree() {
    //1. Find out all classifications
    /*The custom method listWithTree() needs to query all categories with categoryDao, which can be injected manually by @ Autowire,
    However, because the implementation class CategoryServiceImpl inherits ServiceImpl and passes CategoryDao in
    So at this time, baseMapper points to CategoryDao. The selectList() method can be called just like baseMapper
    Query all classifications*/
    List<CategoryEntity> entities = baseMapper.selectList(null);
    //2. Assemble into a parent-child tree structure
    //2.1 find all first level classifications - the parent classification id of the feature is 0, i.e. parent_cid=0
    //Use the flow method to filter the collection data that does not meet the requirements according to the requirements and collect it into a new collection
    /*Subclass data is not queried recursively in the CategoryEntity custom children property
    List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity -> {
        return categoryEntity.getParentCid() == 0;
    })).collect(Collectors.toList());*/

    //The children attribute is defined. There are requirements for setting the children attribute and sorting. map() and sort() are used
    List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity -> {
        return categoryEntity.getParentCid() == 0;
    })).map(menu -> {
        //Menu is the first level menu item traversed
        menu.setChildren(getChildrens(menu,entities));
        return menu;
    }).sorted((menu1,menu2)->{
        //Here, sort is an Interger type, which may be null. Null pointer exceptions may occur. Use ternary expressions to exclude this situation
        return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
    }).collect(Collectors.toList());

    return level1Menus;
}

//Recursively find the submenus of all menus. root is the current menu and all is the menu set that needs to be filtered - that is, all the classification label data queried
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
    List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
        //Filtering idea - view the category id of the current menu, that is, cat_ Is the id equal to the parent menu id of the element to be filtered, that is, parent_cid
        //return categoryEntity.getParentCid() == root.getCatId();
        //The = = judgment cannot be used because the id is a Long type wrapper class. If the id exceeds 127, the result is wrong (wrapper class cache pool)
        return categoryEntity.getParentCid().equals(root.getCatId());
    }).map(categoryEntity -> {
        //Find submenu
        categoryEntity.setChildren(getChildrens(categoryEntity, all));
        return categoryEntity;
    }).sorted((menu1, menu2) -> {
        //Here, sort is an Interger type, which may be null. Null pointer exceptions may occur. Use ternary expressions to exclude this situation
        return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
    }).collect(Collectors.toList());

    return children;
}

After starting the service, test whether the data returned by the interface is correct.

1.4 start Ren fast Vue with npm run dev in the vscode terminal, open the rapid development platform, and create the commodity system directory and classification maintenance menu

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-cak48tkh-1646403290762) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644764764717248. PNG)]

It is suggested to save the picture directly from the external link (fkc-72image-6490g, fkc-64png-image-6490g) (upload the picture directly to the external link)

Refer to the settings of other menus under the system management directory, write the menu URL, create the product folder under the corresponding src/views/modules, and create the category Because the route configuration of Vue files is quickly configured by the platform, the category will be automatically rendered after clicking the category maintenance of commodity system Vue module.

It is suggested to save the picture directly from the external link (lrkp-644g-image-3290g) (typw-647user, typw-3290g) (typw-164g)

1.5 refer to other modules, such as role Vue write category Vue file, add getMenus() method to get the background classification data information and call it in the hook function created().

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-g7ipilh9-1646403290764) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644813949127. PNG)]

Click the category maintenance menu of the development platform to send the request, and check the Network. It is found that there is a problem with the address of the request. It should be sent to localhost:10001/product/category/list/tree, but it is actually as follows

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-lznnqnma-1646403290765) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 16448114101472. PNG)]

So you should modify static \ config \ index The benchmark path baseUrl in the JS file (Note: add a request prefix api to all requests sent through the front end) sends all requests to the gateway for processing

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-2v1pvrbf-1646403290766) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644814471993. PNG)]

Re refresh the page to view the request address. It is found that the verification code request is directly sent to the gateway, and the verification code is the function of the Ren fast service, so you need to add the Ren fast service to the service registry.

1.6 add Ren fast service to the registry

Add the public module gulimall common (service registration to the registry) to the Ren fast service, modify the yml file, add the service name and registry address, and then add the service discovery annotation * * @ EnableDiscoveryClient to the startup class**

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG bhhat7w3-1646403290766) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644814582667. PNG)]

1.7 modify the configuration file of gulimall gateway and temporarily load balance all requests sent by the front end to the Ren fast service. The requests sent by specific modules will be refined later

Then modify the application of the gateway YML file to load balance all requests sent by the front end to the Ren fast service

The assertion defines all front-end requests / api / * *. The filter rewrites the path, removes the / api front-end item prefix, and adds the item name and address of / Ren fast

spring:
  cloud:
    gateway:
      routes:        
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
## Front end project request prefix / api
## Requests sent http://localhost:88/api/captcha.jpg The actual request without path rewriting is http://localhost:8080/api/captcha.jpg
## Need to be converted into http://localhost:8080/renren-fast/captcha. Jpg / Ren fast is the application name and address of the Ren fast module that needs to be added when accessing
## server.servlet.context-path=/renren-fast

The path of the gateway is rewritten here. Note the gateway version

3.x of gateway Path rewriting- RewritePath=/api/?(?<segment>.*),/renren-fast\{segment}
2.x of gateway Path rewriting- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
1.8 solve the cross domain problem - add a configuration class to return CorsWebFilter to the container, so as to add a response header to the pre inspection request to realize the cross domain of the request

There was a cross domain problem when filling in the verification code and clicking login

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-5jkgkczq-1646403290767) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644819190993. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-mvn7mzaq-1646403290767) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644819858499. PNG)]

Solution: ① use the proxy server nginx ② write the cross domain configuration class, let the server respond to the pre inspection request OPTIONS, and allow the request to cross domain. ② is adopted for the project

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-rvbrfxdi-1646403290768) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644819977068. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-13wokmar-1646403290769) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644820154233. PNG)]

Create the config package and configuration class for the gulimall gateway service, configure the CorsWebFilter provided by springboot (this filter configuration class is used to configure the response header of the pre check request) and store it in the container

Note that the reactive package is used when creating UrlBasedCorsConfigurationSource. In addition, because the Ren fast project also sets cross domain processing, see Ren fast / SRC / main / Java / Io / Ren / config / corsconfig Java, shield the code and re run the gateway and renfast to see if the cross domain problem is solved

@Configuration
public class GulimallCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter(){
        //To create a CorsWebFilter object, you need to pass in the CorsConfigurationSource configuration source
        //CorsConfigurationSource is an interface, and the implementation class is UrlBasedCorsConfigurationSource under the reactive package
        // org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource responsive programming
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //Register cross domain configuration parameter 1 set the cross domain request parameter 2 pass in the cross domain configuration class
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");//Which request headers are allowed to cross domains
        corsConfiguration.addAllowedMethod("*");//What requests are allowed across domains
        corsConfiguration.addAllowedOrigin("*");//Which request sources are allowed to cross domains
        corsConfiguration.setAllowCredentials(true);//Allow carrying cookie s across domains
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}
1.9 refine the routes configuration of the gateway and advance the requests sent to the product commodity service module (the precise route is placed before the rough route)

After the cross domain problem is solved, it can be found in the applicaiton of gulimall gateway When the / product / * * api is configured in the front-end, the request will be sent to the / product / * * api. Otherwise, the request will be sent to the / product / * * api in the front-end

spring:
  cloud:
    gateway:
      routes:
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}


## Front end project request prefix / api
## Requests sent http://localhost:88/api/captcha.jpg The actual request without path rewriting is http://localhost:8080/api/captcha.jpg
## Need to be converted into http://localhost:8080/renren-fast/captcha. Jpg / Ren fast is the application name and address of the Ren fast module that needs to be added when accessing
## server.servlet.context-path=/renren-fast

## Refine the requests sent by front-end projects, such as goods and services / api/product. In addition, the precise route needs to be in front of the rough route
## Front end classification maintenance function sends request http://localhost:88/api/product/category/list/tree
## The requests that the backend needs to receive are http://localhost:10001/product/category/list/tree Just remove the api prefix
1.10 verify whether the front end normally obtains the classification data sent by the back end, import the tree control, and bind the tag display value and the sub tree of the tag

After configuration, restart the project to check whether the correct three-level classification data can be obtained. Then enter VScode, modify the function after sending ajax request response successfully, and deconstruct {data} in the response data, so that data Data is the tree classification data classified and assembled by the back-end.

<script>
export default {
  data() {
    return {
      menus: [],
      defaultProps: {
          //Label: which attribute needs to be displayed as the value of the label, and children: which attribute needs to be used as the subtree of the label
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    handleNodeClick(menus) {
      console.log(menus);
    },
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then( ({data})  => {
        //{data} deconstruction deconstructs data from the response object Data is the queried data set
        console.log("Successfully obtained menu data..", data.data);
        this.menus = data.data;
      });
    },
  },
  created() {
    this.getMenus();
  },
};
</script>

Then import it into the template according to the instructions for the tree control in the official document of elementUI

  <div>
    <el-tree
      :data="menus"
      :props="defaultProps"
      @node-click="handleNodeClick"
    ></el-tree>
  </div>

And modify the label and children values of defaultProps in data, as shown above

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-8zg3f9pw-1646403290771) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644847950427. PNG)]

1.11 add and delete buttons to tree controls

Add append add and delete delete options to the tree control, and configure properties for El tree: expand on click node = "false" click will not automatically expand and collapse, and the box that can be selected show checkbox (do not assign true, otherwise an error will be reported) and node key = "catid" each tree node is used as the property of unique identification, and delete the previous click event, And define the append & delete method in methods

    <el-tree 
        :data="menus" 
        :props="defaultProps" 
        :expand-on-click-node="false" 
        show-checkbox
        node-key="catId"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button v-if="node.level<=2" type="text" size="mini" @click="() => append(data)">
            Append
          </el-button>
          <el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">
            Delete
          </el-button>
        </span>
      </span>
    </el-tree>
    //The tag data appended to the tree control is the real content / data of the node obtained from the database
    //Node id parent node cid all child nodes children, etc
    append(data) {
        console.log("append:",data)

    },

    //The tree space removal label node is the current node data (checked, expanded, level, etc.),
    //data is the same as append
    remove(node, data) {
        console.log("remove:",node,data)

    },

In addition, due to the limitation of three-level classification, the classification label can be added only when there are level 1 and 2 nodes, and the current node can be deleted only when there are no child nodes. Therefore, add v-if to control the display scene in the attributes of the append and delete buttons

1.12 modify the reverse generated delete method in the backend CategoryController class, add business layer interface method and business layer implementation class method implementation, and integrate MP to realize logical deletion during the process.

CategoryController class

/**
   * Delete-
   * @RequestBody Get request body must send post request
   * SpringMVC Automatically convert the data of the request body (stored in json form) into the corresponding object
   */
  @RequestMapping("/delete")
  //@RequiresPermissions("product:category:delete")
  public R delete(@RequestBody Long[] catIds){
      System.out.println("Menu to be deleted id:"+catIds);
	   //categoryService.removeByIds(Arrays.asList(catIds));
      //1. Check whether the currently deleted menu is referenced elsewhere
      categoryService.removeMenuByIds(Arrays.asList(catIds));
      return R.ok();
  }

CategoryService interface class

public interface CategoryService extends IService<CategoryEntity> {

    PageUtils queryPage(Map<String, Object> params);

    List<CategoryEntity> listWithTree();
	//Custom delete menu method
    void removeMenuByIds(List<Long> asList);
}

CategoryServiceImpl interface class

@Override
public void removeMenuByIds(List<Long> asList) {
    //Before TODO calls batch deletion, check whether the menu is referenced elsewhere
    //Batch deletion method - logical deletion is used
    baseMapper.deleteBatchIds(asList);
}

Since the place of subsequent reference is uncertain, TODO is used to add it to the to-do list. You can check the to-do list in the TODO column under the IDEA.

In addition, SpringBoot integrates MP to realize logical deletion, which requires the following steps:

2,Logical deletion
 *   1)Configure global logical deletion rules(Can be omitted)
 * mybatis-plus:
 *   global-config:
 *     db-config:
 *       logic-delete-field: flag # Global logically deleted entity field name (since 3.3.0, can be ignored after configuration, step 2 is not configured)
 *       logic-delete-value: 1 # Logical deleted value (default = 1)
 *       logic-not-delete-value: 0 # Logical undeleted value (default is 0)
 *   2)Register logical deletion components MP 3.1.1 This step is not required to start--Now you can't see this requirement on the official website(Can be omitted)
 *   3)Add logical deletion annotation to the entity class field@TableLogic And@TableLogic(value = "1",delval = "0")You can set values for logical deletion and non deletion
 *   If it conflicts with the default global configuration

Effective configuration is to add logical deletion annotation @ TableLogic to the entity class field

/**
 * Whether to display the definition in the table [0-not display, 1 display]
 * MP defaults to 0 and 1, so the attribute value of the TableLogic should be
 */
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
1.13 after the back-end is adjusted, enter the front-end to modify the method of removing the menu, and add the confirmation message pop-up box and message prompt component of ElmentUI

Because Ren fast Vue encapsulates sending ajax requests, the template for sending httpget and httppost requests is written into the global code snippet here to facilitate subsequent quick use.

	"http-get request": {
		"prefix": "httpget",
		"body": [
			"this.\\$http({",
			"url:this.\\$http.adornUrl(''),",
			"method:'get',",
			"params:this.\\$http.adornParams({})",
			"}).then(({data})=>{",
			"})"
		],
		"description": "renren-fast-vue - httpGet request"
	},
	"http-post request": {
		"prefix": "httppost",
		"body": [
			"this.\\$http({",
			"url:this.\\$http.adornUrl(''),",
			"method:'post',",
			"data:this.\\$http.adornData(data,false)",
			"}).then(({data})=>{",
			"})"
		],
		"description": "renren-fast-vue - httpPost request"
	},

The remove() method is finally written as follows. The points to note are as follows:

① You can use the difference expression ${} to get the value in the text wrapped in back quotation marks. There is no need to spell the string. See the message prompt part of confirm to delete the pop-up box to echo the menu name

② If the menu structure before deletion needs to be maintained after confirmation of deletion, the El tree control needs to be dynamically bound * *: default expanded keys = "expandedKey" * *. The dynamically bound attribute value expandedKey is stored in the form of array. Note that it should be defined in data() and the initial value should be empty. In this way, the menu structure before deletion can be maintained only by assigning the parent node id of the menu to be deleted when the menu is refreshed after deletion

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG iswcbnii-1646403290771) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1644920004890. PNG)]

//The tree space removal label node is the current node data (checked, expanded, level, etc.),
    //data is the same as append
    remove(node, data) {
      console.log("remove:", node, data);
      //Spell the cartId in the current node data into an array
      var ids = [data.catId];
      //Use the elementUI messageBox message pop-up box to confirm whether to delete it. You can use the difference expression in the text without spelling ` ${data.name}`
      this.$confirm(
        `This action will permanently delete the menu[ ${data.name}], Continue?`,
        "Tips",
        {
          confirmButtonText: "determine",
          cancelButtonText: "cancel",
          type: "warning",
        }
      )
        .then(() => {
          //Confirm deletion and send ajax request
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            //Use the message prompt component of elementUI
            this.$message({
              message: "Menu deleted successfully",
              type: "success",
            });
            //Refresh menu
            this.getMenus();
            //Set the parent node id to maintain the expanded structure before deletion
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          //Cancel deletion without any processing, but it must be retained, otherwise an error will be reported
        });
1.14 modify and add the back-end part of the menu. There is no need to modify the method of obtaining menu information according to catId. Change the key name stored in the user-defined response class R to "data", mainly because the front-end part needs to add modal box | dialog box and event handling function

template part of the code transformation menu, add the edit button, add and modify the dialog box of the new menu

Note: ① after the dialog box pops up, by default, clicking outside the modal box will close. You need to set close on click modal to false, but setting close on click modal = "false" will report an error. Parameter transmission error needs to be set to dynamic binding: close on click modal = "false"

  <div>
    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="() => append(data)"
          >
            Append
          </el-button>
          <el-button type="text" size="mini" @click="() => edit(data)">
            Edit
          </el-button>
          <el-button
            v-if="node.childNodes.length == 0"
            type="text"
            size="mini"
            @click="() => remove(node, data)"
          >
            Delete
          </el-button>
        </span>
      </span>
    </el-tree>

    <el-dialog :title="dialogType" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
      <el-form :model="category">
        <el-form-item label="Classification name">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="Icon">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="Unit of measurement">
          <el-input v-model="category.productUnit" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">Cancel</el-button>
        <el-button type="primary" @click="submitData">determine</el-button>
      </div>
    </el-dialog>
  </div>

Notes for code writing and transformation of script are as follows:

① Dialog reuse tag dialogType

② String to numeric string * 1 this category. catLevel = data. catLevel * 1 + 1;

③ Application of object Deconstruction - structure useful object attributes, such as deconstruction category, and deconstruct the attributes to be sent
var { catId, name, icon, productUnit } = this.category;

④ When modifying the menu echo data, resend the request to obtain the latest data, which slightly avoids the distortion of the echo data caused by other administrators modifying the same information during the modification process, but this problem is not completely solved. You can consider using optimistic lock to solve this problem later

⑤ Modify and add reuse the same dialog box will appear. First click Modify to pop up the dialog box category assignment and then cancel the modification. Then click Add to echo the information obtained from the previous modification menu. In this case, other information should be set as the initial information when append(data)

export default {
  data() {
    return {
      menus: [],
      //The expandedKey menu defaults to the expanded structure state and passes in the parent node id
      expandedKey: [],
      //dialogVisible controls whether the dialog box / Modal box is displayed. It is not displayed by default
      dialogVisible: false,
      //Modify the basis of the new reuse dialog box edit|append
      dialogType: "",
      //The data object bound by the form in the dialog box. The menu ID catid is the basis for modifying and adding reuse in the dialog box
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        icon: "",
        productUnit: "",
        catId: null,
      },
      defaultProps: {
        //Label: which attribute needs to be displayed as the value of the label, and children: which attribute needs to be used as the subtree of the label
        children: "children",
        label: "name",
      },
    };
  },
  methods: {
    //Get classification data
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        //{data} deconstruction deconstructs data from the response object Data is the queried data set
        console.log("Successfully obtained menu data..", data.data);
        this.menus = data.data;
      });
    },

    //The event data triggered by clicking append is the real content / data of the node obtained from the database
    //Node id parent node cid all child nodes children, etc
    append(data) {
      //console.log("append:", data);
      //Set dialog type
      this.dialogType = "append";
      //Click Add tag to display the dialog box
      this.dialogVisible = true;
      //Assign a value to the data object bound by the additional label
      this.category.parentCid = data.catId; //Parent tag id
      this.category.catLevel = data.catLevel * 1 + 1; //Classification level the currently clicked node level + 1 times 1 converts a string into a number
     //Set other attributes as initial to prevent the situation that clicking the modify pop-up dialog box to assign a value and then canceling the modification, and then clicking Add will echo the information obtained from the previous modification menu
      this.category.catId = null;
        this.category.name = "";
      this.category.icon = "";
      this.category.productUnit = "";
      this.category.sort = 0;
      this.category.showStatus = 1;
    },

    //The event triggered by removing the tag and clicking remove is the data of the current node (whether it is checked, whether it is expanded, level, etc.),
    //data is the same as append
    remove(node, data) {
      //console.log("remove:", node, data);
      //Spell the cartId in the current node data into an array
      var ids = [data.catId];
      //Use the elementUI messageBox message pop-up box to confirm whether to delete it. You can use the difference expression in the text without spelling ` ${data.name}`
      this.$confirm(`This action will permanently delete the menu[ ${data.name}], Continue?`, "Tips", {
        confirmButtonText: "determine",
        cancelButtonText: "cancel",
        type: "warning",
      })
        .then(() => {
          //Confirm deletion and send ajax request
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            //Use the message prompt component of elementUI
            this.$message({
              message: "Menu deleted successfully",
              type: "success",
            });
            //Refresh menu
            this.getMenus();
            //Set the parent node id to maintain the expanded structure before deletion
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          //Cancel deletion without any processing, but it must be retained, otherwise an error will be reported
        });
    },

    //Click the edit button of the modify menu to trigger the event
    edit(data) {
      //console.log("current data to be modified", data);
      //Modify dialog type
      this.dialogType = "edit";
      //open a dialog box
      this.dialogVisible = true;
      //Echo data sending request to get the latest data again to prevent the data from being changed by other administrators during update
      //This problem has not been completely avoided, and an optimistic lock can be added in the future
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
      }).then(({ data }) => {
        console.log("data.data: ",data.data);//Pay attention to data Data is the required menu information
        //The request successfully modifies the data level and other attributes without changing them. In the later stage, the drag function is used to directly change the structure
        this.category.name = data.data.name;
        this.category.catId = data.data.catId;
        this.category.icon = data.data.icon;
        this.category.productUnit = data.data.productUnit;
        //In order to display the parent menu after modification, it is necessary to echo the parent menu id
        this.category.parentCid = data.data.parentCid;
      });
    },

    submitData() {
      if (this.dialogType == "append") {
        //Call the method to add the label
        this.appendCategory();
      }
      if (this.dialogType == "edit") {
        //Call the method to modify the label
        this.editCategory();
      }
    },

    //Dialog box confirmation button click - modify tag to send a request to the background
    editCategory() {
      //Send the category data collected by the front-end to the back-end. Because some unmodified contents do not need to be sent to the back-end
      //Therefore, to deconstruct the category, deconstruct the to be sent
      var { catId, name, icon, productUnit } = this.category;
      //The key value names are the same and can be omitted
      //var data = {catId,name,icon,productUnit};// If you are too lazy to declare a variable, you can directly put the result of the structure into the request body
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        //Background response succeeded
        //close dialog boxes
        this.dialogVisible = false;
        //Prompt friendly information
        this.$message({
          message: "Menu modified successfully",
          type: "success",
        });
        //Refresh information
        this.getMenus();
        //Show original structure
        this.expandedKey = [this.category.parentCid];
      });
    },

    //Dialog box confirmation button click - append tag to send request to background
    appendCategory() {
      //console.log("submitted three-level classification data:", this.category);
      //Send the category data collected by the front end to the back end
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        //Background response succeeded
        //close dialog boxes
        this.dialogVisible = false;
        //Prompt friendly information
        this.$message({
          message: "Menu saved successfully",
          type: "success",
        });
        //Refresh information
        this.getMenus();
        //Show original structure
        this.expandedKey = [this.category.parentCid];
      });
    },
  },
  created() {
    this.getMenus();
  },
};
1.15 add node drag function

After successful debugging, the node drag function is added, that is, the function of changing the parent-child relationship and hierarchy by dragging nodes. You only need to add the attribute draggable to the El tree to drag and drop the node, but it will exceed the level limit of the set three-tier menu. You need to add an additional attribute * *: allow draw = "allowDrop", and define the method allowDrop(draggingNode,dropNode,type) in the method, where draggingNode represents the current node, dropNode is the target node, and the type has three conditions: 'prev', 'inner' and 'next' respectively indicate that the tag returned by allowDrop determines whether it can be placed before, during and after the target node. In addition, an additional countNodeLevel(node) * * method is extracted to recursively call and calculate the maximum number of child nodes of the current node. The specific methods are as follows:

This function needs to pay attention to the whole implementation idea

① Basis for allowing drag and drop: the depth of the currently dragged node + the level of the target node cannot be greater than the maximum level 3 of the menu

Generally speaking, the depth of the current node is the sum of the current node, child nodes and child nodes of child nodes. There are several layers of menus in total

Depth of the current node (how many layers are there for the node to be dragged plus child nodes) = max level of the child nodes of the current node - catlevel of the current node + 1

② countNodeLevel(node), the method to calculate the maximum level of child nodes of the current node, uses recursion

    //Judge whether a node can be dragged. draggingNode represents the current node, dropNode is the target node, and there are three type s:
    //'prev','inner' and 'next' respectively indicate that it is placed in front of, in and after the target node
    allowDrop(draggingNode, dropNode, type) {
      //The judgment is based on the depth of the currently dragged node + the level of the target node cannot be greater than 3
      //Depth of the current node (how many layers are there for the node to be dragged plus child nodes) = max level of the child nodes of the current node - catlevel of the current node + 1
      //console.log("allowDrop", draggingNode, dropNode, type);
      //1. Calculate the depth of the current node, that is, the maximum level maxLevel of the child node of the current node - the level catlevel of the current node + 1
      //1.1 calculate the maximum level value of the child node of the current node and update it in this In MAXLEVEL
      // draggingNode.data is the static information obtained from the background database in the Node node. It is not used here because there is no update level change, and the data here may be distorted
      this.countNodeLevel(draggingNode);
      //console.log("maximum level of child node of current node", this.maxLevel)
      //1.2 calculation depth
      let deep = this.maxLevel - draggingNode.level + 1;
      //console.log("current drag node depth", deep)
      //2 judge whether you can drag to the target node or both before and after
      if (type == "inner") {
        //Drag to the target node. You only need the current node depth + target node level < = 3
        let isDrag = deep + dropNode.level <= 3;
        console.log(`Drag type ${type}: Maximum level of child nodes of the current node:${this.maxLevel}--Current node level:${draggingNode.level}
        --Current node depth:${deep}--Target node level:${dropNode.level}--Allow drag:${isDrag}`);
        //After judgment, assign an initial value to maxLevel in data
        this.maxLevel = 0;
        //this.updateNodes = [];
        return isDrag;
      } else {
        //Before or after dragging to the target node, you only need to judge the current node depth + parent node level of the target node < = 3
        let isDrag = deep + dropNode.parent.level <= 3;
        console.log(`Drag type ${type}: Maximum level of child nodes of the current node:${this.maxLevel}--Current node level:${draggingNode.level}
        --Current node depth:${deep}--Target node parent node level:${dropNode.parent.level}--Allow drag:${isDrag}`);
        //After judgment, assign an initial value to maxLevels in data
        this.maxLevel = 0;
        //this.updateNodes = [];
        return isDrag;
      }
    },

    //Calculate the maximum number of child nodes of the current node
    countNodeLevel(node) {
      //console.log("current node information", node)
      //Find out all child nodes and find the maximum level of child nodes
      if (node.childNodes != null && node.childNodes.length > 0) {
        //With child nodes, traversal
        for (let i = 0; i < node.childNodes.length; i++) {
          if (node.childNodes[i].level > this.maxLevel) {
            //The exchange value updates the maximum level of the child node of the current node
            this.maxLevel = node.childNodes[i].level;
          }
          //Recursively call to check whether the child nodes of the current node have child nodes
          this.countNodeLevel(node.childNodes[i]);
        }
      } else {
        //No child nodes set maxLevel to the current node level in order to correctly calculate the current node depth
        //console.log("maxlevel setting without child nodes", node.level)
        this.maxLevel = node.level;
      }
    },

After the above method is implemented, after dragging the menu, El tree will automatically calculate the level and child nodes that should be in after dragging, but the node information in the static data obtained from the database has not been updated, First, process the dragged new node data through the event function * * handleDrop(draggingNode, dropNode, dropType, ev) * * triggered after the drag menu is successful, and encapsulate the dragged new node data as a node object array updateNodes: [] pass it into the back end to update the node data in the database.

Note * * update idea: * * the latest parent node id of the current drag node | the latest order of the current drag node - traverse the sibling node array | the latest level of the current drag node

    //Event function triggered after dragging menu successfully
    //draggingNode the node currently being dragged dropNode target node reference node
    //Where is the dropType dragged to the reference node ev event object
    handleDrop(draggingNode, dropNode, dropType, ev) {
      //console.log("tree drop: ", draggingNode, dropNode, dropType);
      //1. The latest parent node id of the current drag node is determined according to the method
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        //The parent id should be the parent id of the sibling node and the target node
        //pCid = dropNode.parent.data.catId;
        //A small bug is avoided here. If you move to the first level menu, there is no data in the parent node of the previous level menu
        //Therefore, after moving, pCid will become undefined, and a ternary judgment is added here
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;

        //The sibling node of the current drag node is the child node of the parent node of the target node - note that childNodes is the new value automatically changed after dragging
        //It is different from the static value of children obtained in the background in data
        siblings = dropNode.parent.childNodes;
      } else {
        //inner
        //The parent ID is the ID of the target node
        pCid = dropNode.data.catId;
        //The sibling node of the current drag node is the child node of the target node
        siblings = dropNode.childNodes;
      }
      //Assign a value to the global pCid
      this.pCid.push(pCid);

      //2. The latest order of dragging nodes - traversing the sibling node array
      //3. Drag the latest level of the current node
      for (let i = 0; i < siblings.length; i++) {
        //Traverse to the current drag node
        if (siblings[i].data.catId == draggingNode.data.catId) {
          //push the node information into updateNodes. In addition to the sorting change, the parent id and level (as the case may be) should also be changed
          //Judge whether the hierarchy has changed. Here, judge the siblings [i] Level will change automatically after dragging - that is, the target value | the correct value
          //Draggingnode data. Catlevel is the static data stored in the database. If they are not equal, they need to be encapsulated
          let catLevel = draggingNode.data.catLevel;
          if (siblings[i].level != catLevel) {
            //The current drag node level changes
            catLevel = siblings[i].level;
            //The child node level of the current node changes, and the drag node currently traversed is passed into the parameter. Its childNodes is a method to extract the child nodes
            this.updateChildrenNodeLevel(siblings[i]);
          }
          this.updateNodes.push({
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          //Traverse to other nodes
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }

      //Print the latest updated nodes
      console.log("updateNodes:", this.updateNodes);
    },

    //After dragging, the level changes, and the child node level of the current dragged node changes
    updateChildrenNodeLevel(node) {
      //ergodic
      for (let i = 0; i < node.childNodes.length; i++) {
        //let cNode = node.childNodes[i].data;// Traverse to the back-end node data stored in the current child node
        //cNode.catId = cNode.catId;// id to be updated
        //cNode.catLevel = node.childNodes[i].level / / the backend catlevel level to be updated
        //console.log("child node ID to be updated", node. ChildNodes [i]. Data. Catid)
        //console.log("back end catLevel level level of child node to be updated", node.childNodes[i].level)
        this.updateNodes.push({
          catId: node.childNodes[i].data.catId,
          catLevel: node.childNodes[i].level,
        });
        //Recursive call
        this.updateChildrenNodeLevel(node.childNodes[i]);
      }
    },

In order to reduce the number of interactions with the database, a button to save batch dragging is defined, and then a data update request is sent to the back end. In addition, a reset button to clear the node update array is also defined

//After batch dragging, submit the latest node information to the background
    batchSave() {
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.updateNodes, false),
      }).then(({ data }) => {
        //Response successfully sent friendly message
        this.$message({
          message: "Menu structure modified",
          type: "success",
        });

        //Refresh menu
        this.getMenus();

        //Set the default expanded menu s
        this.expandedKey = this.pCid;
        //Set as initial value
        this.updateNodes = [];
        //this.pCid = [];
      });
    },

    //Cancel batch drag
    cancelBatchDrag() {
      //Refresh menu
      this.getMenus();

      //Set the default expanded menu s
      this.expandedKey = this.pCid;
      //Set as initial value
      this.updateNodes = [];
      this.pCid = [];
    },
  },

Don't forget to add batch update nodes to the backend. Com atguigu. gulimall. product. controller. CategoryController#updateSort

/**
 * The user-defined batch modification method is used to update requirements during dragging
 * category Is the array of nodes to be updated collected by the front end, which is automatically mapped to list < categoryentity > by spring MVC
 */
@RequestMapping("/update/sort")
//@RequiresPermissions("product:category:update")
public R updateSort(@RequestBody List<CategoryEntity> category){
    categoryService.updateBatchById(category);
    return R.ok();
}
1.16 batch deletion

Click the import batch delete button El button to send the array composed of the selected node id to the backend for batch deletion

Mainly note that the method defined inside the Tree component in El Tree is used here. To use the method defined inside the component, you need to define a reference ID ref attribute for the component first. In addition, the calling method is this$ refs. Ref identification In component method

    <el-button
      type="primary"
      size="mini"
      round
      @click="batchDelete"
      >Batch delete</el-button
    >
   //Batch delete menu
    //Here, the method getcheckedmenu() defined inside the Tree component in El Tree is used, which returns an array of all selected nodes
    //To use the method defined in the component, you need to define a reference ID ref attribute for the component. In addition, the calling method is this$ refs. Ref identification In component method
    //Where this$ Refs is to get all the current components The ref flag is to get the tree control
    batchDelete(){
      //let checkedNodes = this.$refs.treeMenu.getCheckedNodes();
      //console.log("all selected nodes:", checkedNodes)
      //Here, map this$ refs. treeMenu. The selected node object obtained by getcheckednodes() is mapped to an array consisting only of its catId
      let checkedNodesId = this.$refs.treeMenu.getCheckedNodes().map(node => node.catId);
      let checkedNodesName = this.$refs.treeMenu.getCheckedNodes().map(node => node.name);
      let checkedNodesNameSlice = [];
      if(checkedNodesName.length > 5){
        checkedNodesNameSlice = checkedNodesName.slice(0,5)
      }
      //console.log("truncated name", checkedNodesNameSlice)
      //console.log("all selected nodes catid:", checkednodesid ")
      //Use the elementUI messageBox message pop-up box to confirm whether to delete it. You can use the difference expression in the text without spelling ` ${data.name}`
      this.$confirm(`Delete the following menus in batch[ ${checkedNodesName.length <= 5?checkedNodesName:checkedNodesNameSlice}]${checkedNodesNameSlice.length == 0?"":"etc."}?`, "Tips", {
        confirmButtonText: "determine",
        cancelButtonText: "cancel",
        type: "warning",
      })
        .then(() => {
          //Confirm deletion and send ajax request
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(checkedNodesId, false),
          }).then(({ data }) => {
            //Use the message prompt component of elementUI
            this.$message({
              message: "Menu batch deletion succeeded",
              type: "success",
            });
            //Refresh menu
            this.getMenus();
            
          });
        })
        .catch(() => {
          //Cancel deletion without any processing, but it must be retained, otherwise an error will be reported
        });
      

    },

2. Commodity module - brand service - PMS_ CRUD of brand table

Forerunner - the vue file Ren generator required by the basic CRUD has been reverse generated. You only need to create the menu and service to be implemented on the fast open platform and configure the menu URL

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-b4uv6jd7-1646403290772) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 164528323066. PNG)]

Then copy and paste the generated vue file in the directory corresponding to the front-end project file, such as the brand service of the commodity module

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-icibuedr-1646403290773) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645283073314. PNG)]

After importing, restart the front-end project and open the newly added menu. It is found that there are only query functions and no new functions. This is the permission verification written by Renren fast Vue. Only when the management identity is opened can there be new and delete permissions. In the development stage, first modify the code of the permission verification. The permission verification method is in SRC \ utils \ index Export in JS

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-3mwxmofm-1646403290774) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645283540711. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG ssyofxpi-1646403290774) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645283562486. PNG)]

After the above modifications are completed, the reverse generated code should be adjusted as needed

2.1 modify the access type input - > switch of brand showStatus and add @ change to monitor the status change and change the database data in time

tips:

① Since the showStatus brand intends to use the switch switch switch component, you need to refer to the usage method of the user-defined column template in El table.

Under each column of El table, the custom column template needs to use the template label, and the attribute slot scope = "scope" must be defined. Other components that need to be combined can be used in the template, and scope can be used in other components Row gets the data of the current row. The following is the modification of the showStatus column

② Show stored in backend database_ The status column uses 0 | 1 to bind whether it is on or off, and the switch defaults to true and fast control switches, so manually set the active value and inactive value of El switch to map with the database. Remember that 1 and 0 here are numbers, so you must add a colon:, otherwise 1,0 will be judged as a string

      <el-table-column
        prop="showStatus"
        header-align="center"
        align="center"
        label="Display status"
      >
        <template slot-scope="scope">
          <el-switch
            v-model="scope.row.showStatus"
            @change="updateBrandStatus(scope.row)"
            :active-value="1"
            :inactive-value="0"
          ></el-switch>
        </template>
      </el-table-column>

And the event processing function triggered after the status change - change the background data ① structure row data rowData ② background showStatus is stored with 0 and 1. Here, use ternary to convert true|fasle into 1|0

    //Event triggered after the brand display switch is changed
    updateBrandStatus(rowData) {
      //console. Log ("latest rowdata after status change", rowdata);
      //Deconstruct brandId and showStatus
      let {brandId,showStatus} = rowData;
      this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        //Note that the background of showStatus is 0,1. Ternary conversion data is used here
        data: this.$http.adornData({brandId:brandId,showStatus:showStatus?1:0}, false),
      }).then(({ data }) => {
        //Response successful
        this.$message({
          message:"Status change successful",
          type:"success"
        })
      });
    },
2.2 file upload function of brand logo

Forerunner - necessity of using object storage service (OSS)

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-obgfy23w-1646403290776) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645368614646. PNG)]

The usage steps of Alibaba cloud object storage service. Please pay attention to the version. There are changes in the new version of springboot

use aliyun Object storage OSS Steps for
 * 1)Import dependency
 *         <dependency>
 *             <groupId>com.alibaba.cloud</groupId>
 *             <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
 *             <version>2.2.0.RELEASE</version>
 *         </dependency>
 * 2)stay yml Alicloud is configured in the file openAPI of endpoint access-key secret-key
 * 3)injection OSSClient adopt OSSClient.putObject afferent bucket Stream custom cloud file name with suffix and local file name FileinputStream parameter
 *    You can upload the file to Alibaba cloud bucket list

Create a new module called gulimall third party to hold some third-party services and put Alibaba cloud object storage OSS into it

The image is stored in Alibaba cloud OSS by the way of server-side signature direct transmission, that is, the signature is completed through Java code on the server-side (and the upload callback is set), and then the data is directly transmitted to OSS through a form. The steps are as follows

①pom.xml import OSS dependency

② Edit application YML file

Configure the registry address, application name, port name and Alibaba cloud oss service attributes access key, sercret key and oss Endpoint and customized cloud storage space name bucket.

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
      access-key: LTAI5tPPeS9KtR539wrrL8WG
      secret-key: bFeVRHpecpHt1ASHG5blyrrjL3df20
      oss:
        endpoint: oss-cn-hangzhou.aliyuncs.com
        bucket: gulimall-chenk
  application:
    name: gulimall-third-party

server:
  port: 30000

③ Create and edit bootstrap Properties file

Set the configuration center address namespace and load the configuration of the configuration center

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=0bcdfaec-accd-44e9-8d4f-fd92b49bcd10

#Load the configuration center to configure OSS yml oss. endpoint access-key sercret-key
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true

④ Create the controller package and write the OSScontroller interface method policy

Pay attention to tips

a. Difference between * * @ Autowired and @ Resource for injection**

b. Read the property * * @ Value("${property name}") in the configuration file**

@RestController
public class OssController {

    //Auto inject OssClient
    //Method 1: @ Autowired annotation should be injected into OSS interface
    //If an OSSClient is injected, an error field OSSClient in com will be reported atguigu. gulimall. thirdparty. controller
    // .OssController required a bean of type 'com.aliyun.oss.OSSClient' that could not be found.
//    @Autowired
//    OSS ossClient;
    //Method 2: @ Resource annotation can be directly injected into OSSClient, because @ Resource annotation can be found by name
    @Resource
    OSSClient ossClient;

    //Get attributes such as endpoint bucket from the configuration file, where bucket is a custom attribute
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;

    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;

    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;

//    @Value("${spring.cloud.alicloud.secret-key}")
//    private String accessKey;

    /**
     *
     * @return Map<String, String> respMap Package is R 
     * accessid-
     * policy-strategy
     * signature-Alibaba cloud signature verification basis
     * dir-Directory prefix when uploading files
     * host-Host address uploaded to
     * expire-Expiration time
     * respMap The value is "hbnjw9njnjwjom9pwwjom9pww9mijojojom9pww9pwww9mijojojojo9pww9pww9pww9pww9pww9wi9pww9pww9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9wi9
     * Y29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIyL
     * TAyLTIxLyJdXX0=","signature":"+VkovFEcgQxoCkoOhzyBBlB6MG4=","dir":"2022-02-21/",
     * "host":"https://gulimall-chenk.oss-cn-hangzhou.aliyuncs.com","expire":"1645413397"}
     */
    @RequestMapping("/oss/policy")
    public R policy(){
        //The final access path of object storage service should be the storage space name endpoint / customization file contains suffix
        // https://gulimall-chenk.oss-cn-hangzhou.aliyuncs.com/testup.jpg
        //Get attributes such as endpoint bucket from the configuration file
        //String accessId = "<yourAccessKeyId>"; //  Please fill in your AccessKeyId.
        //String accessKey = "<yourAccessKeySecret>"; //  Please fill in your AccessKeySecret.
        //String endpoint = "oss-cn-hangzhou.aliyuncs.com"; //  Please fill in your endpoint.
        //String bucket = "bucket-name"; //  Please fill in your bucket name.
        String host = "https://" + bucket + ". "+ endpoint; / / the format of host is bucketname.endpoint
        //For the upload callback, it is not necessary to comment out that the callbackUrl is the URL of the upload callback server. Please configure the following IP and Port as your own real information.
        //String callbackUrl = "http://88.88.88.88:8888";
        //The directory prefix of the file is set here to collect the pictures every day into a folder
        String currentDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String dir = currentDate; // Prefix specified when the user uploads the file. Whether to splice depends on whether to splice when the front end is obtained

        Map<String, String> respMap = null;
        // Create an OSSClient instance. Automatic injection is omitted here
        //OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            // The maximum supported file size requested by PostObject is 5 GB, i.e. CONTENT_LENGTH_RANGE is 5 * 1024 * 1024 * 1024.
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));
            /* Some cross domain gateways are solved uniformly. This part is deleted
            JSONObject jasonCallback = new JSONObject();
            jasonCallback.put("callbackUrl", callbackUrl);
            jasonCallback.put("callbackBody",
                    "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
            String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
            respMap.put("callback", base64CallbackBody);

            JSONObject ja1 = JSONObject.fromObject(respMap);
            // System.out.println(ja1.toString());
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST");
            response(request, response, ja1.toString());*/

        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally {
            ossClient.shutdown();
        }
        return R.ok().put("data",respMap);
    }
}

⑤ Add a new routing address to the configuration file of the gateway - note that different versions of the gateway have different path rewriting methods

Load balance all requests of / api/thirdparty / sent by the front end to the gulimall third party service, and rewrite the path to remove the front end item prefix / api/thirdparty

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-df8ki5d7-1646403290777) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 16454156045. PNG)]

⑥ Joint commissioning front end

The front-end uses the upload component to upload files, including the encapsulated file upload components (single file upload and multi file upload) and the extracted policy of the back-end signature method JS under src\components

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-dpcf8tyu-1646403290777) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 164515839753. PNG)]

Modify the action attribute value in El upload to Bucket domain name = "gulimall-chenk.oss-cn-hangzhou.aliyuncs.com"

Then follow these steps to import the components correctly

①stay<script>Import components from @express src/  
import SingleUpload from"@/components/upload/singleUpload";
②stay<tempalte>Where components need to be imported through <Component name(Allow hump named calls)></Component name>Import components 
Pay attention to the binding attribute, otherwise it will not be echoed
<el-form-item label="brand logo address" prop="logo">
	<!-- <el-input v-model="dataForm.logo" placeholder="brand logo address"></el-input> -->
	<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
③stay<script>Default export componets Property to register the component
components: { SingleUpload:SingleUpload },

After importing, restart the project, click Add and upload files to enter the debugging mode, check NetWordk, and find a cross domain problem (your own server and Alibaba cloud server)

Secondly, pay attention to the level of the data returned by the back-end - when a single file is uploaded, in the beforeUpload(file) method, the response data responded by the back-end should have a data key, and the value corresponding to the data key is the encapsulated protocol signature and other data. In other words, the signature and other data obtained by the back-end should be encapsulated again after being encapsulated into a respmap. The key name is "data" and the value is respmap, that is, return r.ok() put(“data”,respMap);

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-zf7mcbay-1646403290778) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645427689632. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-egskch1q-1646403290779) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645427813244. PNG)]

Create cross domain rules for the current OSS bucket

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG yxobwgfb-1646403290779) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645426726643. PNG)]

After completion, open add or modify to successfully upload the file, and then add brand Insert the user-defined template into the brand logo column of Vue component, and assign the logo address to the src of img. In this way, you can echo the picture instead of the logo address

<el-table-column
    prop="logo"
    header-align="center"
    align="center"
    label="brand logo address"
>
    <template slot-scope="scope">
    	<img :src="scope.row.logo" style="width: 100px; height: 80px">
    </template>
</el-table-column>
2.3 use of user-defined verification rules - front end verification

The Form component in elementUI provides the function of Form verification. You only need to pass in the agreed verification rules through the rules attribute and set the prop attribute of Form item to the field name to be verified

In addition, it also provides a method to customize the verification rules. You can use the custom rules to verify the form by binding the function for verification to the custom validator validator in the verification rules. The following are the custom verification rules for the properties of the first letter retrieval field and sorting field

        firstLetter: [
          {
            validator: (rule, value, callback) => {
              if (value == "") {
                callback(new Error("Initial cannot be empty"));
              } else if (!/^[a-zA-Z]$/.test(value)) {
                callback(new Error("The initials are unique and must be in a-z perhaps A-Z between"));
              }else{
                callback();
              }
            },
            trigger: "blur",
          },
        ],
        sort: [
          {
            validator: (rule, value, callback) => {
              if (value == null) {
                callback(new Error("Sorting field must be filled in"));
              } else if (!Number.isInteger(value)) {
                callback(new Error("The sort field must be an integer greater than or equal to 0,The smaller the, the higher the priority"));
              }else if (value < 0){
                callback(new Error("The sort field must be an integer greater than or equal to 0"));
              }else{
                callback();
              }
            },
            trigger: "blur",
          },
        ],
2.4 JSR303 back end verification
use JSR303 check-To import dependencies manually for higher versions validation-api and springboot integration validation Startup class for
 *         <dependency>
 *             <groupId>javax.validation</groupId>
 *             <artifactId>validation-api</artifactId>
 *             <version>2.0.1.Final</version>
 *         </dependency>
 *
 *         <dependency>
 *             <groupId>org.springframework.boot</groupId>
 *             <artifactId>spring-boot-starter-validation</artifactId>
 *             <version>2.6.3</version>
 *         </dependency>
 * 1)to Bean Before field(as BrandEntity)Add annotation of verification rule@NotBlank etc.(See details javax.validation.constraints),
 *   And add custom message Information, such as@NotBlank(message = "Brand name must be submitted"),If not added, the default prompt is used
 *   In addition, if there are some other requirements, they can be used@Pattern And pass in regexp Value matching custom verification rule(regular),as
 *  @Pattern(regexp = "/^[a-zA-Z]$/",message = "The search initial must be a letter")
 * 	private String firstLetter;
 * 2)stay Controller Verification is required in the control method of layer Bean Add verification comments before the method parameters@Valid Turn on verification, such as public R save
 * (@Valid @RequestBody BrandEntity brand)
 * 3)If you don't want to use the default verification result, you can check it in the Bean Parameter followed by BindingResult result,The variable encapsulates the verification result
 * as public R save(@Valid @RequestBody BrandEntity brand, BindingResult result)
  4) Check grouping-There are different verification requirements according to different application scenarios, such as adding brands and modifying brand pairs Bean The field qualification requirements are different
 *  ①First, create different grouping interfaces to distinguish application scenarios. Here common The module creates the following enclosure
 *  com.atguigu.common.validator.group And create it first AddGroup Group and UpdateGroup Groups distinguish between adding and modifying scenes
 *  ②to Bean When adding annotation on the field of entity class groups Attribute values, such as groups={AddGroup.class,UpdateGroup.class}Indicates a comment
 *  Validation rule validation scenario
 *  ③use@Validated({Effective scene tag interface class.class})And pass in the packet interface bytecode file instead@Valid Annotation enable group verification
 5) User defined verification annotation omitted reference P69 video
   ①,Write a custom verification annotation
   ②,Write a custom verifier constraintValidator
   ③,Associate custom validators and custom validation annotations
   *@Documented
   *Constraint(validatedBy = { ListValueConstraintValidator.cLass[You can specify multiple different validators
  @Target({METHOD,FIELD,ANNOTATION_TYPE, CoNSTRUCTOR,PARAMETER,TYPE_uSE })
 @Retention(RUNTIME)
public @interface ListVaLue i
 

Taking the save method of saving brand as an example, the code after transformation is as follows (refer to the part without comments)

In summary

① Javax is used in Entity class Entity validation. The annotation under constraints adds verification rules to the field. If there is a need for group verification, pass in the value of the groups attribute, which is the byte code file array of the effective group tag interface.

② Add * * @ Valid annotation before receiving the parameters of the corresponding entity class to enable verification. @ Validated({effective scenario tag interface class. Class}) is used for group verification**

③ If there is a need to process exception information, that is, the default exception information encapsulation format is not used. After receiving the parameters of the corresponding entity class, BindingResult receives the encapsulated exception information. Generally, exceptions are handled in a centralized manner. Just learn about it here.

    /**
     * preservation
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result Exceptions are handled in a unified way and are not received separately here*/){
/*      Exception information is not handled here, and it is handed over to com atguigu. gulimall. product. exception. GulimallExceptionControllerAdvice
        if (result.hasErrors()){
            //1 Create a map collection that contains errors
            HashMap<String, String> map = new HashMap<>();
            //2 Traverse the error set, extract the error fields and prompt information, and store them in the map
            result.getFieldErrors().forEach((error)->{
                String message = error.getDefaultMessage();
                String field = error.getField();
                map.put(field,message);
            });
            //3 Return after encapsulation into R
            return R.error(400,"The submitted data is illegal. "). put("data",map);
        }else {
            //There is no error in the verification result
            brandService.save(brand);
            return R.ok();
        }*/
        brandService.save(brand);
        return R.ok();
    }
2.5 basic implementation steps of user-defined annotation

Here you can customize a verification annotation @ ListValue{vals = {allowed value array}}

① Write a custom verification annotation

/**
 * Custom verification annotation - only values passed by the annotation are allowed to be entered
 * Three attributes must be filled in
 * message() Error message search after error
 * groups() Support group verification
 * payload() Customize some load information
 * And mark the following notes
 * @Constraint To specify the validator, you need to create a ListValConstraintValidator
 * @Target Annotation can identify the location of
 * @Retention Verification annotations can be obtained at run time
 *
 */
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface ListValue {

    //The default error message is validationmessages Com.com in properties atguigu. common. validator. ListValue. Message value
    //Under resources, create a validationmessages Properties and give it to com atguigu. common. validator. ListValue. Message assignment
    String message() default "{com.atguigu.common.validator.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    //Specify incoming value
    int[] vals() default {};

}

② Specify error messages or create a validationmessages Properties file and define the error message

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-wgsrlwdd-1646403290780) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645596687988. PNG)]

③ Write a custom validator, ConstraintValidator

/**
 * Validator for custom annotation ListVal
 * Implement constraintvalidator < A, t >
 * A The annotation T passed in for verification can be the type of tag
 * And implement two methods
 */
public class ListValConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    //Customize a Set set
    private Set<Integer> set = new HashSet<>();


    //Initialization method
    @Override
    public void initialize(ListValue constraintAnnotation) {
        //Get details get the specified value from the vals() method defined by ListValue
        int[] vals = constraintAnnotation.vals();
        //Traverse vals and add to set
        for (int val : vals) {
            set.add(val);
        }
    }

    //Judge whether the verification is successful
    //Where value is the value to be verified obtained from the annotation
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //Judgment basis: if set does not contain value with check value, return false; otherwise, return true
        return set.contains(value);
    }
}

④ Associate custom validators and custom validation annotations

Specify the validatedBy value of annotation @ Constraint in the custom annotation class

//The specified verifier is associated with a custom verifier
@Constraint(validatedBy = { ListValConstraintValidator.class })
2.6 centralized exception handling and user-defined error and error information enumeration classes

With the deepening of business, the status code returned to the front end needs to be planned to define the error and error information enumeration class com in the common module atguigu. common. exception. BizCodeEnume. Supplement later

package com.atguigu.common.exception;

/**
 * Error code and error message definition class
 * 1.The definition rule of error code is 5 digits
 * 2.The first two digits represent the business scenario, and the last three digits represent the error reason | exception type. Such as 10001, where
 *   10 Indicates common, and 001 indicates abnormal parameter format verification
 * 3.The error information description is required to maintain the error code, which is defined as an enumeration type
 * 4.Error code list
 *   10: currency
 *      001: Parameter format verification
 *   11: Goods and services
 *   12: order
 *   13: Shopping Cart
 *   14: logistics
 */
public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"Abnormal system unknown"),
    VALID_EXCEPTION(10001,"Parameter format verification failed");

    private int code;
    private String msg;

    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}

Then on COM atguigu. gulimall. product. exception. Gulamallexeptioncontrolleradvice defines exception handling classes to handle exceptions in a centralized manner

tips:

① The standard exception handling class should use * * @ ControllerAdvice and configure the basic scanning package. In addition, since @ ResponseBody * * is used to return JSON format data, the composite annotation @ RestControllerAdvice(basePackages = "") can be used

② Use the annotation * * @ Slf4j * * to print exceptions using the log, and use log in the method body Error ("exception {} in data verification, exception type {}", e.getMessage(),e.getClass()); You can define regular classes precisely in this way

③ The execution of exception handling method is based on the value value annotated by matching * * @ ExceptionHandler(value = exception class. class) * *

If there is an exact match, it will be executed first. If there is no exact match, it will be executed to match the processing method containing the specified exception class.

/**
 * Centralized processing of all exceptions
 */
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
//Compound annotation
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    //Data verification exception
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e){
        log.error("Abnormal data verification{},Exception type{}",e.getMessage(),e.getClass());
        //Get the error result set of data verification
        BindingResult result = e.getBindingResult();
        //Create custom error collection
        HashMap<String, String> errorMap = new HashMap<>();
        //Traverse the error result set, take out the error information and error fields and put them into the user-defined error set
        result.getFieldErrors().forEach((error)->{
            errorMap.put(error.getField(),error.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data",errorMap);
    }

    //Any other exception
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
}
2.6 brand table pms_brand's query service supplement - fuzzy query

front end

  1. Add the El input box of fuzzy query on the header, bind a fuzzy query v-model="dataForm.key" value, add a query button, press and send a get request to the back end, and carry the bound fuzzy query key value into the request parameter params.

back-end

  1. Transform the queryPage method in brandServiceImpl and add fuzzy query conditions
    //Transform queryPage method
    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        //1. Get the key of fuzzy query from the request parameters
        String key = (String) params.get("key");
        //2. Create search bar
        QueryWrapper<BrandEntity> wrapper = new QueryWrapper<>();
        //3. Judge whether the key has a value
        if (!StringUtils.isEmpty(key)){
            //Fuzzy retrieval conditions of valued stitching
            wrapper.eq("brand_id",key).or().like("name",key);
        }
        //4. Construct paging information
        IPage<BrandEntity> page = this.page(
                new Query<BrandEntity>().getPage(params),
                wrapper
        );
        //5. Return the pagination information after packaging
        return new PageUtils(page);
    }
2.7 brand association classification table PMS_ category_ brand_ CRUD of relation
2.7.1 query

front end

  1. At brand Add the operation El button Association classification button in the Vue component, and add the modal box component of the association table that pops up after pressing the button. Add the required components in the modal box

back-end

  1. Add the controller method cateloglist to the CategoryBrandRelationController to obtain the information of all categories associated with the brand,

And implemented in the business layer

CategoryBrandRelationController

/**
     * Get the information of all categories associated with the brand
     */
    //@RequestMapping(value = "/catelog/list",method = RequestMethod.GET)
    @GetMapping("/catelog/list")
    //@RequiresPermissions("product:categorybrandrelation:list")
    public R cateloglist(@RequestParam("brandId") Long brandId){
        //Query current table PMS_ category_ brand_ Information of the current brand brandId of the relationship
        List<CategoryBrandRelationEntity> data = categoryBrandRelationService.getRelationCatlog(brandId);

        return R.ok().put("data", data);
    }

CategoryBrandRelationServiceImpl

//Get the classification information associated with the current brand
@Override
public List<CategoryBrandRelationEntity> getRelationCatlog(Long brandId) {
    return this.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
}
2.7.2 add Association classification

front end

  1. Click Add and send a request to the backend. The request parameters are brandId and catalogid, excluding brand name and category name

back-end

  1. Transform the save() method of reverse generation. The front end only passes the brand ID and category ID, so the method of automatic generation cannot be used, because the brand name and category name will not be saved.

    Pay attention to PMS_ category_ brand_ The reason why the brand name and category name fields are also redundantly designed in the relation table is to reduce the number of joint table queries and reduce the loss of database performance

2.7.3 distortion of brand name and category name obtained by querying Association classification

When adding an association classification, the corresponding brand name and classification name are queried and stored in the association table, so the information saved before is queried when obtaining the association information. If the subsequent brand name and classification name | tag name are changed, the information will be distorted.

Therefore, in order to ensure the consistency of these redundant information, the modification business of brand and label classification before transformation is needed.

brand

It interacts with the database by calling the encapsulated business layer method of Ren fast

BrandController

    /**
     * modify
     * Since the brand name is quoted in other places, it is not only necessary to change the brand name here
     * Brand names quoted elsewhere should also be transformed
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:brand:update")
    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
        //brandService.updateById(brand);
        brandService.updateDetail(brand);
        return R.ok();
    }

The BrandServiceImpl interface is slightly marked as a transaction method

    //When modifying the details of the brand, we should also change the place where the brand name is quoted
	//The MP configuration class marked as the transaction method is either modified successfully or not changed. It is put into the container before use
    //@EnableTransactionManagement / / start transaction
    @Transactional
    @Override
    public void updateDetail(BrandEntity brand) {
        //Ensure the consistency of redundant fields
        //1. Modify itself
        this.updateById(brand);
        //2. Check whether the brand name is empty. If not, there is a need to change the reference
        if (!StringUtils.isEmpty(brand.getName())){
            //3. Synchronously update references of other related tables
            //3.1 brand label association table reference injection CategoryBrandRelationService
            //Define a method to modify brand information according to brand ID and brand name
            categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());

            //Other data referenced by tobrand
        }

    }

CategoryBrandRelationService interface

    //Update data based on brand ID and brand name
    //Used to update the brand name previously saved in the table after the brand name is changed
    @Override
    public void updateBrand(Long brandId, String name) {
        //Construct the bean entity to be updated
        CategoryBrandRelationEntity entity = new CategoryBrandRelationEntity();
        entity.setBrandName(name);
        entity.setBrandId(brandId);
        //All brands whose brand relation id is updated
        this.update(entity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));

    }

Label classification

Interact with the database through Dao's custom update method and corresponding mapper file SQL statement

CategoryController#update

  /**
   * modify
   * Transform the user-defined cascade update method to ensure the consistency of redundant fields
   */
  @RequestMapping("/update")
  //@RequiresPermissions("product:category:update")
  public R update(@RequestBody CategoryEntity category){
//categoryService.updateById(category);
      categoryService.updateCascader(category);
      return R.ok();
  }

CategoryServiceImpl#updateCascader service layer interface is slightly marked as transaction method

/**
 * Custom cascading updates ensure the consistency of redundant fields, that is, the update synchronization of other places that reference tag names
 * @param category
 */
@Transactional
@Override
public void updateCascader(CategoryEntity category) {
    this.updateById(category);
    //Brand association tag table service interface reference injection
  categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

CategoryBrandRelationDao

@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

    //Customize the method of updating according to tag Id and tag name to write mapper statement
    //It is recommended to name the parameter, which is easy to #{get from mapper file}
    void updateCategory(@Param("catId") Long catId,@Param("name") String name);
}

mapper/product/CategoryBrandRelationDao.xml

<!--Customize and uniformly modify label names and labels ID Condition is label ID Take etc-->
    <update id="updateCategory">
        UPDATE `pms_category_brand_relation` SET `catelog_name` = #{name} WHERE `catelog_id` = #{catId}
    </update>

3. Commodity module - other services

Forerunner - several basic concepts

① Three level classification

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-kxawrrme-1646403290781) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645516857184. PNG)]

The primary classification finds the secondary classification data, and the secondary classification finds the tertiary classification data

Corresponding data table PMS category

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-imbbryqf-1646403290782) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645516960236. PNG)]

② SPU and SKU

SPU: Standard Product Unit

SKU: stock keeping unit

For example:

IPhoneX is SPU and MI8 is SPU

IPhoneX 64G obsidian is a SKU

MIX8 + 64G is SKU

③ Basic attributes (specification parameters) and sales attributes

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-vnxjkigg-1646403290782) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 16455117112292. PNG)]

[attribute grouping - specification parameter - Sales attribute - three level classification] association relationship

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-k3h0qaxo-1646403290783) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645518457098. PNG)]

[SPU SKU property sheet]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG uowqdo33-1646403290784) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645518493569. PNG)]

In this part, the designed vue file is directly introduced into the front end, and the gulimall under SQLyog is displayed_ The admin library runs sys_menu.sql creates the required menus and directories.

⑤ Object partition

  1. Po (persistent object) persistent object

Po is a record corresponding to a table in the database. Multiple records can use the collection of Po. The PO should not contain any operations to the database

  1. Do (domain object) domain object

It is a tangible or intangible business entity abstracted from the real world

  1. TO (Transfer Object)

Transfer between different applications

  1. DTO (Data Transfer Object)

This concept comes from the design pattern of J2EE. The original purpose is to provide coarse-grained data entities for EJB distributed applications, so as to reduce the number of distributed calls, so as to improve the performance of fractional calls and reduce the network load. However, here, it generally refers to the data transmission object between the display layer and the service layer

  1. VO(value object)

It is usually used for data transfer between business layers. Like PO, it only contains data, but it should be an abstract business object, which can correspond to the table or not. It is created with the new keyword according to the needs of the business and recycled by GC

view Object view object

Accept the data transmitted from the page, encapsulate the object, and encapsulate the data needed by the page

  1. BO(business object)

From the perspective of business model, see the domain object in UML original domain model, which encapsulates business logic and java object. business object is used to encapsulate business logic into an object by calling DAO method and combining PO VO. This object includes one or more objects, such as a resume, with educational experience, Work experience, social relations, etc. we can map education experience to a PO, work experience to a PO, and social relations to a PO, and establish a BO object corresponding to the resume to process the resume. Each BO includes these Po. In this way, we can deal with BO when dealing with business logic

  1. POJO (plain sequential java object) is a simple and irregular java object

Java objects in the traditional sense, that is, in some object / relationship mapping tools, the persistent object that can maintain database table records is a pure Java object that conforms to the Java Bean specification without adding other properties and methods. Our understanding is that the most basic java bean has only property field setter and getter methods

POJO is the general term of DO/DTO/BO/VO

  1. DAO (data access object)

It is a standard j2ee design pattern of sun. One interface of this pattern is DAO. It operates in the negative persistence layer and provides an interface for the business layer. This object is used to access the database. It is usually used in combination with PO. DAO contains various database operation methods. Through its methods, it carries out relevant operations on the database in combination with PO, Sandwiched between business logic and database resources, it cooperates with VO to provide CRUD function of database

3.1 attribute grouping table PMS_ attr_ CRUD of group
3.1.1 query of attribute grouping table (front end: separation of components and transmission of information using parent-child components; back end: Transformation of the method of obtaining attribute grouping information set according to id)

front end

  1. The three-level classification component category vue removes unnecessary functions and only retains the basic query function of the three-level classification component, and then extracts it into a vue file under the common module

  2. Create attgroup under product Vue file.

    1. The layout page is divided into two parts through El row and El col components. The three-level classification tree structure accounts for 6, and the damage adding and inspection function accounts for 18 (24 in total),

    In El row, you can configure the gutter attribute (for example, gutter=20) to specify the interval between each column. The default interval is 0

    1. Introduce category. On the left page Vue component (step 3: script import - components registration in default export - reference location is marked by lowercase component name)
    2. Parent child components transfer data

    Process: the child component transmits data to the parent component. The event mechanism is that the child component sends an event to the parent component and carries the data// this.$emit("event name", data carried...). The parent component defines * * @ event name = "event handler" * * * in the reference tag to obtain the data transmitted by the child component and receive the child component.

    Specifically, referring to the tree component document, we found that Events events and node click Events (Events triggered after a node is clicked) are more in line with our business requirements. In the sub component category Vue's tree control defines * * @ node Click = "nodeclick" * *, and sends Events and data to the parent component in the method.

    	//Event handling function after the sub component category node is clicked
        //data - node information really encapsulated in the database
        //Node - the current node data is encapsulated by the elementUI tree control
        //Component - the whole tree component
        //Through this$ emit("tree-node-click", data, node, component); Pass events to parent components and carry data
        nodeclick(data, node, component) {
          //console.log("the node of the sub component category is clicked", data, node, component);
          //Send events to the parent component;
          this.$emit("tree-node-click", data, node, component);
        }
    

    When the parent component references the child component, configure the events passed by the child component and the corresponding event handling function

    <category @tree-node-click="treenodeclick"></category>
    
    	//Perception tree node is clicked
        treenodeclick(data, node, component) {
          //console.log("sensing that the tree node of the sub component is clicked, the returned data is:", data,node,component);
          //Modify the catId after the third level node is clicked
          if (node.level == 3) {
            this.catId = data.catId;
            this.getDataList(); //Re query
          }
        },
    

back-end

  1. Under AttrGroupController, modify the reverse generated method list() to obtain the attribute grouping data set
/**
     * list
     * Modify the original method and add the path parameter catelogId three-level menu id
     * And add attrgroupservice Querypage (params, catalogid) method
     */
    @RequestMapping("/list/{catelogId}")
    //@RequiresPermissions("product:attrgroup:list")
    public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){
        // Comment out the original method and add a parameter
        // PageUtils page = attrGroupService.queryPage(params);
        PageUtils page = attrGroupService.queryPage(params,catelogId);
        return R.ok().put("page", page);
    }
  1. Define querypage (params, catalogid) in AttrGroupService interface and implement it in AttrGroupServiceImpl

tips:

① Renren fast encapsulates paging and conditional query here. Refer to other methods to imitate and transform

② Due to the requirement of fuzzy query, the key should be obtained from the request parameter params. In addition, when splicing query conditions, pay attention to if the spliced query conditions have an extension, such as select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name like ‘%key%’). Then you need to use functional programming to splice the conditions in parentheses into an object and return.

    @Override
    public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
       //① First get the search keyword key from params and see PMS_ attr_ Whether the fields in the group table match exactly or vaguely
        String key = (String) params.get("key");
        //② Create entity class correspondence table in query criteria generic
        QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
        //③ If there is a fuzzy query, the fuzzy query will be spliced into the condition
        if (!StringUtils.isEmpty(key)){
            //key is not empty. Splice query conditions. Because and splices a bracket, you need to use functional programming to return an object
            wrapper.and((obj)->{
                obj.eq("attr_group_id",key).or().like("attr_group_name",key);
            });
        }
        //④ Generate paging information as appropriate and return
        //If catelogId == 0, query all
        if (catelogId == 0){
            //this.page(IPage paging information, QueryWrapper query criteria)
            IPage<AttrGroupEntity> page = this.page(
                    new Query<AttrGroupEntity>().getPage(params),
                    wrapper
            );
            return new PageUtils(page);
        }else {
            //The final query statement to be sent should be
            //select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name like '%key%')
            //Splicing
            wrapper.eq("catelog_id",catelogId);
            //③ Query paging data and return
            IPage<AttrGroupEntity> page = this.page(
                    new Query<AttrGroupEntity>().getPage(params),
                    wrapper
            );
            return new PageUtils(page);
        }
    }

postman test results

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-vp1ob4dn-1646403290784) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645539410611. PNG)]

3.1.2 addition and modification of attribute grouping table (front end: use of cascading components; back end: modify the return of custom attribute Children in CategoryEntity to avoid cascading display exceptions after Children return empty)

front end

  1. At attr add or update The selected grouping column of the Vue component uses the cascading selector Cascader component. Refer to the documentation. To use this component, you mainly need to configure three attributes v-model = "paths": options = "categories" and: props = "setting". v-model: the selected item is bound with the value (usually an array, which should be the cascaded parent-child catId array), options is bound with the optional data source, and the key name can be configured through the props attribute. The props configuration option is bound with a json and given to value (the value of the specified option is the value dynamically bound by the cascaded component, and the catId used here), Label (the attribute bound by the display name) and children (the value associated with the sub option) bind values

  2. After adding a node path at the back end, modify the binding value of v-model, and then add and modify the normal echo. After that, in order to prevent the path array from storing data after point modification

    However, canceling the modification causes the path array not to be emptied, resulting in a new error echo. Check the dialog callback function to listen for the closing event * * @ closed**="dialogClose", so that the full path this is emptied every time it is closed catelogPath = [];

  3. By adding filterable to El cascade selector, you can quickly find it by entering keywords. placeholder = "try searching: mobile phone" attribute can set the default text prompt

back-end

  1. Modify the return of the custom attribute Children in the CategoryEntity to avoid cascading display exceptions after Children return empty. Used * * @ JsonInclude annotation**
	//If the children collection is empty, this property is not returned
	@JsonInclude(JsonInclude.Include.NON_EMPTY)
	//User defined attribute to store subcategories. Annotated @ TableField(exist = false) cannot be found in the table
	@TableField(exist = false)
	private List<CategoryEntity> children;
  1. Through reading the document, it is found that in the use of cascading components, v-model needs a parent-child catId array of a node, that is, the complete catId path, in order to bind data normally and echo during modification. Therefore, it is necessary to define a new user-defined attribute catelogPath for AttrGroupEntity at the back end to store the complete path, transform the control layer method, and add a new method to the business layer to obtain the complete path

AttrGroupEntity

	/**
	 * Custom attribute catalogpath
	 * Used to save the full path and cascade v-model binding
	 */
	@TableField(exist = false)
	private Long[] catelogPath;

AttrGroupController

    /**
     * information
     * Transform - the custom attribute catelogPath
     * Get the full path injected into CategoryService through categorypath
     */
    @RequestMapping("/info/{attrGroupId}")
    //@RequiresPermissions("product:attrgroup:info")
    public R info(@PathVariable("attrGroupId") Long attrGroupId){
		AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
		//Get the current tag catalogid
        Long catelogId = attrGroup.getCatelogId();
        //You need to get the complete path of CategoryService through categorypath
        Long[] catelogPath = categoryService.findCatelogPath(catelogId);
        //Put the full path in
        attrGroup.setCatelogPath(catelogPath);
        return R.ok().put("attrGroup", attrGroup);
    }

CategoryServiceImpl interface omitted

    /**
     * Customize to obtain the complete expansion path of the ID according to the current catId, that is, [parent, child, grandson] is used by cascade components for basic attribute functions
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        ArrayList<Long> paths = new ArrayList<>();
        //In general, query the complete information according to the current catId to see whether there is a parent node
        //If yes, continue to query until there is no parent node. Here, it is extracted into a recursive method
        //Iterations can also be used
        List<Long> parentPath = findParentPath(catelogId,paths);
        //Reverse order
        Collections.reverse(parentPath);
        //Convert to array
        return (Long[]) parentPath.toArray(new Long[parentPath.size()]);
    }

    private List<Long> findParentPath(Long catelogId, ArrayList<Long> paths) {
        //1. Collect the current node
        paths.add(catelogId);
        //2. Query current catId details
        CategoryEntity category = this.getById(catelogId);
        //3. Check whether there is a parent node and recursion if there is one
        if (category.getParentCid() != 0){
            findParentPath(category.getParentCid(),paths);
        }
        return paths;
    }
  1. Because the MP paging plug-in is used, you need to create a config package in the product module and put a paging PaginationInterceptor in the MybatisConfig container. Note that the paging beans of higher versions are different
/**
 * MybatisPlus Configuration class of -- the new version may need to replace the returned paging Bean
 * MybatisPlusInterceptor
 */
@Configuration
@EnableTransactionManagement//Open transaction
@MapperScan("com.atguigu.gulimall.product.dao")//Scan Mapper interface
public class MybatisConfig {

    //Introduce paging plug-in
    @Bean
    public PaginationInterceptor paginationInterceptor(){
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        //Set the requested page number to be greater than the operation after the last page. If true, the first page will be returned. If false, continue to request the default false
        paginationInterceptor.setOverflow(true);
        //Set the maximum number of single page. The default number is 500 - 1 unlimited
        paginationInterceptor.setLimit(1000);

        return paginationInterceptor;
    }
}

The function of associating attributes in the attribute grouping table is added later

3.2 attribute table PMS_ Addition, deletion, modification and query of attr
3.2.1 addition, deletion, modification and query of basic attributes
1. Add attribute

Leading knowledge

Due to the display requirements, bind an attribute GroupId grouping Id that is not in the table to the new attribute, add an attribute to attrenty as before, and mark it as a custom display attribute with @ TableField(exist = false), which is not standardized. That's why you need to create vo packages.

VO(value object) - use view Object to understand more vividly

It is usually used for data transfer between business layers. Like PO, it only contains data, but it should be an abstract business object, which can correspond to the table or not. It is created with the new keyword according to the needs of the business and recycled by GC

view Object view object

On the one hand, it accepts the data transmitted from the page and encapsulates it into objects; On the other hand, the objects processed by the business are encapsulated into the data needed by the page

back-end

  1. Create vo package and AttrVO copy AttrEntity attribute here
/**
 * Property Vo object
 * On the one hand, it accepts the data transmitted from the page and encapsulates it into objects;
 * On the other hand, the objects processed by the business are encapsulated into the data needed by the page
 * The object used is the Vo object
 *
 */
@Data
public class AttrVO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * Attribute id
     */
    //@TableId / / no need to annotate the database
    private Long attrId;
    /**
     * Attribute name
     */
    private String attrName;
    /**
     * Need to retrieve [0-not needed, 1-needed]
     */
    private Integer searchType;
    /**
     * Attribute Icon
     */
    private String icon;
    /**
     * List of optional values [separated by commas]
     */
    private String valueSelect;
    /**
     * Attribute type [0-sales attribute, 1-basic attribute, 2-both sales attribute and basic attribute]
     */
    private Integer attrType;
    /**
     * Enabled status [0 - disabled, 1 - enabled]
     */
    private Long enable;
    /**
     * Classification
     */
    private Long catelogId;
    /**
     * Quick display [display on introduction; 0-no, 1-yes], which can still be adjusted in sku
     */
    private Integer showDesc;
    
    //Page data attribute grouping ID
    private Long attrGroupId;
}
  1. Transformation com atguigu. gulimall. product. controller. Attrcontroller#save method
  /**
   * preservation
   * Transform the request body into a voatt object in the page and save it
   */
  @RequestMapping("/save")
  //@RequiresPermissions("product:attr:save")
  public R save(@RequestBody AttrVO attr){
//attrService.save(attr);
      attrService.saveAttr(attr);
      return R.ok();
  }
  1. New.com atguigu. gulimall. product. service. impl. Attrserviceimpl#saveattr method interface
//Custom save properties
@Transactional
@Override
public void saveAttr(AttrVO attr) {
    //1. Create po objects and save them to the database
    AttrEntity attrEntity = new AttrEntity();
    //Use the BeanUtils provided by spring Copyproperties (source, target) copies the vo property value to po
    //Use premise - the attribute name in Vo is the same as that in Po
    BeanUtils.copyProperties(attr,attrEntity);
    this.save(attrEntity);

    //2. Save association relationship - inject attratrgrouprelationdao
    AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
    //It mainly receives attribute ID and attribute group ID
    relationEntity.setAttrGroupId(attr.getAttrGroupId());
    relationEntity.setAttrId(attrEntity.getAttrId());
    attrAttrgroupRelationDao.insert(relationEntity);

}

After restarting the project and adding two attributes, it is found that not only the data returned by the front end is stored in the attribute table

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-jv2mxwj6-1646403290785) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 1645629593559. PNG)]

And the association relationship is also stored in the attribute and attribute grouping association table

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-bq8q74cr-1646403290786) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645629652958. PNG)]

2 query properties

back-end

  1. Create a new query method to match the request sent by the front end. Com atguigu. gulimall. product. controller. AttrController#baseList

    /**
     * Create a query list method
     * Carry the path parameter catalogid and three-level classification label Id
     * Request parameters (paging parameters & fuzzy query key)
     * Query all attributes and current node attributes
     */
    @GetMapping("/base/list/{catelogId}")
    //@RequiresPermissions("product:attr:list")
    public R baseList(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){
        //Custom method
        PageUtils page = attrService.queryBaseListPage(params,catelogId);
    
        return R.ok().put("page", page);
    }
    
  2. Create com atguigu. gulimall. product. service. impl. Attrserviceimpl#querybaselistpage method interface method omitted

    @Override
    public PageUtils queryBaseListPage(Map<String, Object> params, Long catelogId) {
        //① Create query condition
        QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
        //② Category discussion splicing condition catelogId==0?params.key isEmpty?
        if (catelogId != 0){
            //Query the attributes under the specified three-level classification label
            queryWrapper.eq("catelog_id",catelogId);
        }
        String key = (String) params.get("key");
        if (!StringUtils.isEmpty(key)){
            //attr_id  attr_name
            queryWrapper.and((wrapper)->{
                wrapper.eq("attr_id",key).or().like("attr_name",key);
            });
        }
        //③ Create paging information and pass in the request parameter set and query criteria containing paging information
        IPage<AttrEntity> page = this.page(
                new Query<AttrEntity>().getPage(params),
                queryWrapper
        );
        //④ Return the queried paging data after packaging
        return new PageUtils(page);
    }
    
  3. As shown in the above query method, only PMS can be found_ The information defined in the attr attribute table, in which there is only the category ID and no group ID, but the group name group required by the front end_ Name and category name catelog_name is not in the paging information.

    In this case, because the join table query is not recommended (in the case of a large amount of attribute table data, even if the number of groups is small, the join table query is a great loss of database resources), there is no redundant field, so we can only transform the business layer query method com atguigu. gulimall. product. service. impl. AttrServiceImpl#queryBaseListPage

    [the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-yvqz2syo-1646403290787) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645696757997. PNG)]

    [the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-70eid8tk-1646403290787) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645696834475. PNG)]

    Before transforming the query method, create AttrRespVO class to collect the attribute table information required by the front end.

    /**
     * Property Vo object
     * On the one hand, it accepts the data transmitted from the page and encapsulates it into objects;
     * On the other hand, the objects processed by the business are encapsulated into the data needed by the page
     * The object used is the Vo object
     *
     * Here is to encapsulate the data processed by the business into the data response required by the page
     *
     */
    @Data
    public class AttrRespVO extends AttrVO {
        /**
         *  Two additional member variables that need to respond back
         *  catelogName Category name, such as "mobile / digital / mobile"
         *  groupName Group name, such as "principal"
         */
        private String catelogName;
    
        private String groupName;
    }
    

    queryBaseListPage

@Override
public PageUtils queryBaseListPage(Map<String, Object> params, Long catelogId) {
    //① Create query condition
    QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
    //② Category discussion splicing condition catelogId==0?params.key isEmpty?
    if (catelogId != 0){
        //Query the attributes under the specified three-level classification label
        queryWrapper.eq("catelog_id",catelogId);
    }
    String key = (String) params.get("key");
    if (!key.isEmpty()){
        //attr_id  attr_name
        queryWrapper.and((wrapper)->{
            wrapper.eq("attr_id",key).or().like("attr_name",key);
        });
    }
    //③ Create paging information and pass in the request parameter set and query criteria containing paging information
    IPage<AttrEntity> page = this.page(
            new Query<AttrEntity>().getPage(params),
            queryWrapper
    );
    //④ Get the records queried in the database from the page and encapsulate them again. Encapsulate the group name and classification name into attrRespVO
    List<AttrEntity> records = page.getRecords();
    //Streaming programming
    List<AttrRespVO> respVOS = records.stream().map((attrEntity -> {
        //Create attrRespVO object and copy information
        AttrRespVO attrRespVO = new AttrRespVO();
        BeanUtils.copyProperties(attrEntity, attrRespVO);
        //Query and set the group name
        //First find out the group Id according to the association table, and then find the group name according to the group Dao and set it
        AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
        //Non empty judgment: because the grouping of attributes can not be selected when adding, it may be empty
        if (relationEntity != null && relationEntity.getAttrGroupId()!=null) {
            //Get the group ID and set the group name
            Long attrGroupId = relationEntity.getAttrGroupId();
            AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrGroupId);
            if (attrGroupEntity!=null){
                attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName());
            }
        }

        //Query and set the classification name
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity != null) {
            attrRespVO.setCatelogName(categoryEntity.getName());
        }

        return attrRespVO;
    })).collect(Collectors.toList());

    //⑤ Encapsulate the page information and reset the result set, and set the resealed respVOS as the result set
    PageUtils pageUtils = new PageUtils(page);
    pageUtils.setList(respVOS);

    return pageUtils;
}
3 modify attributes

back-end

Echo –

  1. Add a member variable private Long[] catelogPath to AttrRespVO; Because cascading menu echo requires a full path

  2. Modify the interface and business layer method for obtaining attribute data echo after modification

    com.atguigu.gulimall.product.controller.AttrController#info

      /**
       * information
       *  Transform and improve the data needed for echo
       */
      @RequestMapping("/info/{attrId}")
      //@RequiresPermissions("product:attr:info")
      public R info(@PathVariable("attrId") Long attrId){
    //AttrEntity attr = attrService.getById(attrId);
          AttrRespVO attrRespVO = attrService.getAttrInfo(attrId);
          return R.ok().put("attr", attrRespVO);
      }
    

    com.atguigu.gulimall.product.service.impl.AttrServiceImpl#getAttrInfo

    @Override
    public AttrRespVO getAttrInfo(Long attrId) {
        //Query PO
        AttrEntity attrEntity = this.getById(attrId);
        //Create VO and copy
        AttrRespVO attrRespVO = new AttrRespVO();
        BeanUtils.copyProperties(attrEntity,attrRespVO);
        //grouping
        AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
        if (relationEntity!=null && relationEntity.getAttrGroupId()!=null){
            attrRespVO.setAttrGroupId(relationEntity.getAttrGroupId());
            AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
            System.out.println("attrGroupEntity:"+attrGroupEntity);
            if (attrGroupEntity!=null){
                attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName());
            }
        }
        //Tag name
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity!=null){
            attrRespVO.setCatelogName(categoryEntity.getName());
        }
        //Full path
        //Previously, the Category business layer has customized a method to obtain the method injection carrying the complete path according to the Category catId
        //categoryService for convenience, the injected service is slightly nonstandard
        Long[] catelogPath = categoryService.findCatelogPath(categoryEntity.getCatId());
        attrRespVO.setCatelogPath(catelogPath);
    
        return attrRespVO;
    }
    

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-3ukgmq96-1646403290788) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1645708856354. PNG)]

Modification –

  1. Modification method of modification interface com atguigu. gulimall. product. controller. AttrController#update

      /**
       * modify
       * AttrVO receiving for transformation
       */
      @RequestMapping("/update")
      //@RequiresPermissions("product:attr:update")
      public R update(@RequestBody AttrVO attr){
    attrService.updateAttr(attr);
    
          return R.ok();
      }
    

2. The business layer implements com atguigu. gulimall. product. service. impl. AttrServiceImpl#updateAttr

@Transactional
@Override
public void updateAttr(AttrVO attr) {
    //PO
    AttrEntity attrEntity = new AttrEntity();
    BeanUtils.copyProperties(attr,attrEntity);
    this.updateById(attrEntity);
    //Modify group association table
    //Note that if you have not set grouping for the current attribute before, this operation is to add
    AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
    relationEntity.setAttrId(attr.getAttrId());
    relationEntity.setAttrGroupId(attr.getAttrGroupId());
    QueryWrapper<AttrAttrgroupRelationEntity> queryWrapper = new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId());
    Integer count = attrAttrgroupRelationDao.selectCount(queryWrapper);
    if (count > 0){
        //Modify operation
        attrAttrgroupRelationDao.update(relationEntity, queryWrapper);
    }else {
        //Add operation
        attrAttrgroupRelationDao.insert(relationEntity);
    }
}
3.2.2 addition, deletion, modification and query of sales attributes

Forerunner-

Due to the component saleattr of sales attribute in the front end Components importing basic attributes between vues baseattr Vue simply sets the parameter attrtype to 0. View component baseattr Vue, the query request address is url: this h t t p . a d o r n U r l ( ' / p r o d u c t / a t t r / http.adornUrl(`/product/attr/ http.adornUrl('/ product/attr/{type}/list/${this.catId}'), which carries the attrtype parameter and defaults to 1

In a word, the sales attribute module realizes the reuse with the basic attribute module through the attrtype attribute, so the methods in the back-end attrController and attrService should consider the changes after reuse: query all - splice condition modification query single information - grouping judgment save sales attribute - grouping judgment modify sales attribute - grouping judgment

1. Query sales attributes

back-end

  1. Transformation com atguigu. gulimall. product. controller. AttrController#baseList

        /**
         * Create a query list method
         * Carry the path parameter catalogid and three-level classification label Id
         * Request parameters (paging parameters & fuzzy query key)
         * Query all attributes and current node attributes
         *
         * Further transformation, because the request address for obtaining the sales attribute is / sale / list / {catalogid}
         * The request address of the basic attribute is / base / list / {catalogid}, so this method can be reused
         */
        @GetMapping("/{attrType}/list/{catelogId}")
        //@RequiresPermissions("product:attr:list")
        public R baseList(@RequestParam Map<String, Object> params,
                          @PathVariable("catelogId") Long catelogId,
                          @PathVariable("attrType") String type){
            //Custom method
            //PageUtils page = attrService.queryBaseListPage(params,catelogId);
            //Further modification after adding path parameter attrType
            PageUtils page = attrService.queryBaseListPage(params,catelogId,type);
            return R.ok().put("page", page);
        }
    
  2. Add a constant package under the common module and create a ProductConstant to store constants required for goods and services

    public class ProductConstant {
    
        //Property related enumeration
        public enum AttrEnum{
            ATTR_TYPE_BASE(1,"Basic properties"),ATTR_TYPE_SALE(0,"Sales attributes");
    
            private int typeCode;
            private String typeMsg;
    
            AttrEnum(int typeCode, String typeMsg) {
                this.typeCode = typeCode;
                this.typeMsg = typeMsg;
            }  
            
            public int getTypeCode() {
                return typeCode;
            }
    
            public String getTypeMsg() {
                return typeMsg;
            }
        }
    }
    
  3. Transformation com atguigu. gulimall. product. service. impl. AttrServiceImpl#queryBaseListPage

        //① Create query criteria
        //Further transform the basic attribute module of sales attribute reuse
        //A new condition for further splicing after adding the type parameter -- Ternary splicing for parameter type
        QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type", "base".
                equalsIgnoreCase(type) ? ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode() :
                ProductConstant.AttrEnum.ATTR_TYPE_SALE.getTypeCode());
Modify sales attributes
  1. How to get property details when modifying. Com atguigu. gulimall. product. service. impl. AttrServiceImpl#getAttrInfo

Discuss when setting grouping information

        //The transformation of reuse discusses whether it is the basic attribute call
        if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode()){
            //grouping
            AttrAttrgroupRelationEntity relationEntity = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
            if (relationEntity!=null && relationEntity.getAttrGroupId()!=null){
                attrRespVO.setAttrGroupId(relationEntity.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
                System.out.println("attrGroupEntity:"+attrGroupEntity);
                if (attrGroupEntity!=null){
                    attrRespVO.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
        }
  1. Modification method com atguigu. gulimall. product. service. impl. AttrServiceImpl#updateAttr

Judge whether it is a basic attribute before modifying the grouping association table

        //Further transform the reuse of basic attributes and sales attributes
        if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode()){
            //Modify group association table
            //Note that if you have not set grouping for the current attribute before, this operation is to add
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
            relationEntity.setAttrId(attr.getAttrId());
            relationEntity.setAttrGroupId(attr.getAttrGroupId());
            QueryWrapper<AttrAttrgroupRelationEntity> queryWrapper = new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId());
            Integer count = attrAttrgroupRelationDao.selectCount(queryWrapper);
            if (count > 0){
                //Modify operation
                attrAttrgroupRelationDao.update(relationEntity, queryWrapper);
            }else {
                //Add operation
                attrAttrgroupRelationDao.insert(relationEntity);
            }
        }
Add sales attribute
@Transactional
@Override
public void saveAttr(AttrVO attr) {
    //1. Create po objects and save them to the database
    AttrEntity attrEntity = new AttrEntity();
    //Use the BeanUtils provided by spring Copyproperties (source, target) copies the vo property value to po
    //Use premise - the attribute name in Vo is the same as that in Po
    BeanUtils.copyProperties(attr,attrEntity);
    this.save(attrEntity);

    //2. Save association relationship - inject attratrgrouprelationdao
    //2.1 further transform and discuss whether it is a basic attribute call. If it is a basic attribute, save the association relationship
    //Note that the attribute group Id should also be blank
        if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode() && attr.getAttrGroupId() != null){
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        //It mainly receives attribute ID and attribute group ID
        relationEntity.setAttrGroupId(attr.getAttrGroupId());
        relationEntity.setAttrId(attrEntity.getAttrId());
        attrAttrgroupRelationDao.insert(relationEntity);
    }


}
3.3 attribute grouping table PMS_ attr_ Addition, deletion, modification and query of group association attribute
3.3.1 query all associated attributes of the current attribute group Id

back-end

  1. New method com atguigu. gulimall. product. controller. AttrGroupController#attrRelation
/**
 * New method
 * Get all associated basic attributes (specification parameters) according to the current grouping Id
 */
@RequestMapping("/{attrgroupId}/attr/relation")
//@RequiresPermissions("product:attrgroup:list")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId){
    //Inject AttrService to create a method to obtain the attribute PO list according to the attribute grouping Id
    List<AttrEntity> attrEntities = attrService.getRelationAttr(attrgroupId);
    //After packaging, it is called data
    return R.ok().put("data", attrEntities);
}
  1. New.com atguigu. gulimall. product. service. impl. AttrServiceImpl#getRelationAttr
//Get the information of the associated attribute through the attribute grouping Id. basic attribute | specification parameters
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
    //Find out all associated entity classes of the current group Id through the association table
    List<AttrAttrgroupRelationEntity> relationEntities = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
    //Get all attribute IDS
    List<Long> attrIds = relationEntities.stream().map((relationEntity) -> {
        return relationEntity.getAttrId();
    }).collect(Collectors.toList());
    //Judge whether the attribute ID array is empty. Attrids cannot be used= Null batch break 
    if (attrIds.size()>0){
        //Find out all attribute sets according to attribute Ids
        Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
        return (List<AttrEntity>) attrEntities;
    }
    //The property ID collection is empty
    return null;
}
3.3.2 remove the association between the current attribute group and the current attribute
  1. New.com atguigu. gulimall. product. vo. Attrgrouprelationvo data format
@Data
public class AttrGroupRelationVO {
    //Collect attribute ID and attribute grouping ID for removing attribute and attribute grouping Association
    private Long attrId;
    private Long attrGroupId;
}
  1. New interface method com atguigu. gulimall. product. controller. AttrGroupController#deleteRelation

    Note that the JSON data sent by the POST request acquisition front end sent here should be encapsulated into a custom object, and the @ RequestBody annotation should be added

    /**
     * New method
     * Remove the association between the current attribute group and the current attribute
     * 
     * Note: for the POST request sent here, the JSON data sent by the front-end should be encapsulated into a custom object, and the @ RequestBody annotation should be added
     */
    @PostMapping("/attr/relation/delete")
    public R deleteRelation(@RequestBody AttrGroupRelationVO[] vos){
        //Add or delete association method
        attrService.deleteRelation(vos);
        return R.ok();
    }
  1. New business method com atguigu. gulimall. product. service. impl. AttrServiceImpl#deleteRelation
    //Delete association relationship according to AttrGroupRelationVO []
    @Override
    public void deleteRelation(AttrGroupRelationVO[] vos) {
        //If you delete them one by one in the following way, why do you need to access the database many times and occupy too many resources.
//        attrAttrgroupRelationDao.delete(new QueryWrapper<AttrAttrgroupRelationEntity>().
                eq("attr_id",1L).and().eq("attr_group_id",1L));
        //Dao layer customizes a method of batch deletion
        //Because the Dao layer operates on PO classes, it remaps vos to POs -- normative operation
        List<AttrAttrgroupRelationEntity> relationEntities = Arrays.asList(vos).stream().map((vo) -> {
            AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
            BeanUtils.copyProperties(vo, relationEntity);
            return relationEntity;
        }).collect(Collectors.toList());
        attrAttrgroupRelationDao.deleteBatchRelation(relationEntities);

    }
  1. Add association table Dao method com atguigu. gulimall. product. dao. AttrAttrgroupRelationDao#deleteBatchRelation
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {

    //Note that @ Params("attribute name") is marked to facilitate Mapper file value
    void deleteBatchRelation(@Param("entities") List<AttrAttrgroupRelationEntity> relationEntities);
}

5. Add Mapper file SQL statement

The ideal SQL statement to be sent should be IN the following way - (the reason for not checking the attribute Id through IN here is unknown)

DELETE FROM pms_attr_attrgroup_relation WHERE

(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId}) OR

(attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId}) OR omitted

In addition, pay attention to the space before and after traversing the separator setting of splicing query conditions

    <!--When the user-defined batch delete association method traverses the association set, each pair of association data must be added OR  Pay attention to the space before and after-->
    <delete id="deleteBatchRelation">
        DELETE FROM `pms_attr_attrgroup_relation` WHERE 
        <foreach collection="entities" item="item" separator=" OR ">
            (attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId})
        </foreach>
    </delete>
3.3.3 add the associated attribute of the current attribute group

Find out the basic attributes of the current group without associated attributes –

  1. New interface com atguigu. gulimall. product. controller. AttrGroupController#attrNoRelation

    /**
     * New method
     * Gets the property of the current group that is not associated with the property group
     */
    @RequestMapping("/{attrgroupId}/noattr/relation")
    public R attrNoRelation(@RequestParam Map<String, Object> params,
            @PathVariable("attrgroupId") Long attrgroupId){
        //Create a method to get the attributes of the group with no associated attributes of the current group, and return the page information because you want to get the paging data
        PageUtils page = attrService.getNoRelation(params,attrgroupId);
        return R.ok().put("page", page);
    }
    
  2. New.com atguigu. gulimall. product. service. impl. AttrServiceImpl#getNoRelation

    tips:

    ① Idea: find out the classification Id of the current attribute group Id - > find out all attribute groups of the current classification Id - > find out the intermediate beans with associated attributes in all attribute groups in the intermediate table and take out the associated attribute IDS - > find the attribute whose attribute is the current classification and is a basic attribute and whose Id is not in the associated attribute IDs (don't forget to judge whether params.key and IDs are empty when encapsulating conditions - > encapsulate them into page information and return

    ② Remember to do non empty processing when querying splicing conditions

/**
 * Gets the property of the unassociated state of the current group
 * @param params
 * @param attrgroupId
 * @return
 */
@Override
public PageUtils getNoRelation(Map<String, Object> params, Long attrgroupId) {
    //1. The current group can only associate all the attributes in the category to which it belongs. Find out the category Id first
    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
    Long catelogId = attrGroupEntity.getCatelogId();

    //2. The current group can only be associated with attributes that are not referenced by other groups
    //2.1 find out all attribute groups under the current classification
    List<AttrGroupEntity> allAttrGroupEntities = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().
            eq("catelog_id", catelogId));
    List<Long> allAttrGrpIds = allAttrGroupEntities.stream().map((attrGrp -> {
        return attrGrp.getAttrGroupId();
    })).collect(Collectors.toList());

    //2.2 find the attribute Id associated with all attribute groups through the attribute group association table
    //First find out the associated intermediate Bean from the intermediate table
    List<AttrAttrgroupRelationEntity> anotherAttrAgRels = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", allAttrGrpIds));
    //Streaming programming takes out attrId
    List<Long> anotherAttrIds = anotherAttrAgRels.stream().map((attrAg) -> {
        return attrAg.getAttrId();
    }).collect(Collectors.toList());

    //2.3 find the attributes that can be associated with the current attribute group, that is, all attrId of the current group is not in otherattrids
    //this.baseMapper is equivalent to injecting attrDao, which has been injected here
    //Construct query criteria ① find out all basic attributes under the current classification ID ② those whose attribute ID is not in otherattrids are bindable attributes
    QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().
            eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getTypeCode());
    //Splice notIn("attr_id", anotherAttrIds); Before, judge whether otherattrids is empty and the array length is greater than 0
    if (anotherAttrIds != null && anotherAttrIds.size()>0){
        queryWrapper.notIn("attr_id", anotherAttrIds);
    }
    //There is a key in the request parameter to judge whether the key is empty, and then assemble new query conditions
    String key = (String) params.get("key");
    if (!StringUtils.isEmpty(key)){
        //Splicing fuzzy query conditions, pay attention to and (functional programming)
        queryWrapper.and((wrapper)->{
            wrapper.eq("attr_id",key).or().like("attr_name",key);
        });
    }
    //Call the page method and encapsulate it as ipeage
    IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), queryWrapper);
    return new  PageUtils(page);
}

Save the selected attribute to associate –

  1. New interface com atguigu. gulimall. product. controller. AttrGroupController#addRelation
/**
 * New method
 * Add attribute grouping and attribute Association
 *
 */
@PostMapping("/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVO> vos){
    //Add association method
    relationService.saveBatch(vos);
    return R.ok();
}

2. Add middle table business layer method com atguigu. gulimall. product. service. impl. AttrAttrgroupRelationServiceImpl#saveBatch

@Override
public void saveBatch(List<AttrGroupRelationVO> vos) {
    //Use streaming programming to map vos to attrattrgroupentries
    List<AttrAttrgroupRelationEntity> relationEntities = vos.stream().map((vo) -> {
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        BeanUtils.copyProperties(vo, relationEntity);
        return relationEntity;
    }).collect(Collectors.toList());

    this.saveBatch(relationEntities);
}
3.4 realization of the function of publishing goods - big save
3.4.1 realize the interface to obtain the current classification and associated brands

back-end

  1. New interface method com atguigu. gulimall. product. controller. CategoryBrandRelationController#relationBrandslist

    Extension Controller function positioning

    /**
     * New method
     * Get all brands associated with the current category
     *
     * Extended description - functional positioning of Controller
     * 1.Process requests, receive and verify data
     * 2.Hand over the verified data to the Service for business processing
     * 3.Receive the data processed by the Service, encapsulate the VO specified on the page and other requirements (paging, unified return, etc.)
     */
    @GetMapping("/brand/list")
    public R relationBrandslist(@RequestParam(value = "catId",required = true) Long catId){
        //The user-defined method finds out brandentities and then creates BrandVO to map PO to VO
        List<BrandEntity> brandEntities = categoryBrandRelationService.getBrandsByCatId(catId);
        List<BrandVO> vos = brandEntities.stream().map((brandEntity -> {
            BrandVO brandVO = new BrandVO();
            //Because the brand name settings of PO and VO are different here, the attribute copy method cannot be used
            brandVO.setBrandId(brandEntity.getBrandId());
            brandVO.setBrandName(brandEntity.getName());
            return brandVO;
        })).collect(Collectors.toList());
    
        return R.ok().put("data", vos);
    }
    
  2. Add business layer method com atguigu. gulimall. product. service. impl. CategoryBrandRelationServiceImpl#getBrandsByCatId

/**
 *
 * @param catId
 * @return
 */
@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
    //Find out the association table Bean of the current classification ID in the association table
    List<CategoryBrandRelationEntity> relationEntities = this.baseMapper.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
    //Find out the brand of the corresponding ID
    List<BrandEntity> brandEntities = relationEntities.stream().map((relation) -> {
        Long brandId = relation.getBrandId();
        BrandEntity brandEntity = brandService.getById(brandId);
        return brandEntity;
    }).collect(Collectors.toList());
    return brandEntities;
}
3.4.2 implement the interface to obtain the current classification attribute group and attribute set
  1. New.com atguigu. gulimall. product. vo. Attrgroupwithattrsvo encapsulation class

    @Data
    public class AttrGroupWithAttrsVO {
        /**
         * Packet id
         */
        private Long attrGroupId;
        /**
         * Group name
         */
        private String attrGroupName;
        /**
         * sort
         */
        private Integer sort;
        /**
         * describe
         */
        private String descript;
        /**
         * Group icon
         */
        private String icon;
        /**
         * Category id
         */
        private Long catelogId;
    
        //attrs
        private List<AttrEntity> attrs;
    }
    
  2. New interface method com atguigu. gulimall. product. controller. AttrGroupController#getAttrGroupWithAttrs

    /**
     * New method
     * Get the current classification attribute group and attribute
     */
    @GetMapping("/{catelogId}/withattr")
    public R getAttrGroupWithAttrs(@PathVariable("catelogId") Long catelogId){
        //User defined business layer interface method
        List<AttrGroupWithAttrsVO> vos = attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId);
    
        return R.ok().put("data",vos);
    }
    
  3. Implement business layer method com atguigu. gulimall. product. service. impl. AttrGroupServiceImpl#getAttrGroupWithAttrsByCatelogId

    /**
     * Find out all attribute groups and the associated attributes in these groups according to the classification Id
     * @param catelogId
     * @return
     */
    @Override
    public List<AttrGroupWithAttrsVO> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
        //1. Find out all attribute groups
        List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
        //2. Find out the associated attributes in all groups
        List<AttrGroupWithAttrsVO> vos = attrGroupEntities.stream().map((attrGroupEntity -> {
            AttrGroupWithAttrsVO vo = new AttrGroupWithAttrsVO();
            BeanUtils.copyProperties(attrGroupEntity, vo);
            //Before injecting AttrService, I wrote a method to obtain all basic attributes under the current classification ID through the current group ID
            //Circular table lookup??
            List<AttrEntity> relationAttr = attrService.getRelationAttr(attrGroupEntity.getAttrGroupId());
            if (relationAttr != null){
                //Non empty processing
                vo.setAttrs(relationAttr);
            }
            return vo;
        })).collect(Collectors.toList());
        return vos;
    }
    
3.4.3 save the current product information - publish the product menu
  1. First, the SpuSaveVo of the front-end product information is generated through the online format website of Jason, and the member variable types are transformed, such as ID - > long, decimal field - > BigDecimal, etc

  2. Modify the saved interface method com atguigu. gulimall. product. controller. SpuInfoController#save

      /**
       * preservation
       * Modify the saving method and receive it with SpuSaveVo
       * And create a new business layer saving method
       */
      @RequestMapping("/save")
      //@RequiresPermissions("product:spuinfo:save")
      public R save(@RequestBody SpuSaveVo vo){
    spuInfoService.saveSpuInfo(vo);
          return R.ok();
      }
    
  3. New business layer saving method - big save com atguigu. gulimall. product. service. impl. SpuInfoServiceImpl#saveSpuInfo

    /**
     * //TODO Advanced part improvement
     * @param vo
     */
    @Transactional
    @Override
    public void saveSpuInfo(SpuSaveVo vo) {
    
        //1. Save SPU basic information pms_spu_info
        //Field ID SPU_ name  spu_ description  catalog_ id  brand_ id  weight  publish_ status  create_ time  update_ time
        SpuInfoEntity infoEntity = new SpuInfoEntity();
        BeanUtils.copyProperties(vo,infoEntity);
        infoEntity.setCreateTime(new Date());
        infoEntity.setUpdateTime(new Date());
        this.saveBaseSpuInfo(infoEntity);
    
        //2. Save the description picture PMS of Spu_ Spu_ info_ desc
        //Field spu_id  decript
        List<String> decript = vo.getDecript();
        SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
        descEntity.setSpuId(infoEntity.getId());
        //Divide the description information (picture path) with ","
        descEntity.setDecript(String.join(",",decript));
        spuInfoDescService.saveSpuInfoDesc(descEntity);
    
    
        //3. Save SPU's picture set pms_spu_images
        //Field ID SPU_ id  img_ name  img_ url  img_ sort  default_ img
        List<String> images = vo.getImages();
        //The method of creating spuImagesService batch saving pictures is passed into spu_id and picture set
        spuImagesService.saveImages(infoEntity.getId(),images);
    
    
        //4. Save the specification parameters (basic attributes) of spu; pms_product_attr_value
        //    id  spu_id  attr_id  attr_name  attr_value  attr_sort  quick_show
        List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
        //Create a batch save method in ProductAttrValueService
        productAttrValueService.saveProductAttr(baseAttrs,infoEntity.getId());
    
    
        //5. Save the integral information of SPU; Remote call of coupon service gulimall_ sms->sms_ spu_ bounds
        //The parameter growth points, groundboundaries, shopping points and buyboundaries are encapsulated in boundaries
        //Additional spuId needs to be submitted
        //In this way, the data transferred between services can be encapsulated into a to object, and the to package can be created in common
        Bounds bounds = vo.getBounds();
        SpuBoundTo spuBoundTo = new SpuBoundTo();
        BeanUtils.copyProperties(bounds,spuBoundTo);
        spuBoundTo.setSpuId(infoEntity.getId());
        R r = couponFeignService.saveSpuBounds(spuBoundTo);
        if(r.getCode() != 0){
            log.error("Remote save spu Integration information failed");
        }
    
    
        //6. Save all sku information corresponding to the current spu;
        //Get all information sku
        List<Skus> skus = vo.getSkus();
        //Non empty processing
        //if(skus!=null && skus.size()>0){
        if(!CollectionUtils.isEmpty(skus)){
            skus.forEach(item->{
                //6.1) basic information of SKU; pms_sku_info
                //Get the default picture, traverse the atlas and check whether the defaultImg field is 1. If it is 1, set the default picture URL
                String defaultImg = "";
                //forEach traversal is not used here because forEach is thread unsafe
                for (Images image : item.getImages()) {
                    if(image.getDefaultImg() == 1){
                        defaultImg = image.getImgUrl();
                    }
                }
                //Create SkuInfoEntity class
                SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
                //Fields required by SkuInfoEntity
                //sku_id  spu_id  sku_name  sku_desc  catalog_id  brand_id
                // sku_default_img  sku_title  sku_subtitle   price  sale_count
                //Fields in Item
                //attr skuName skuTitle skuSubtitle images descar fullCount discount countStatus
                //fullPrice reducePrice priceStatus memberPrice
                //Attribute copy
                BeanUtils.copyProperties(item,skuInfoEntity);
                //     No fields in item
                // brand_id catalog_id sale_count spu_id skuName sku_default_img
                // price skuTitle skuSubtitle
                skuInfoEntity.setBrandId(infoEntity.getBrandId());
                skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
                skuInfoEntity.setSaleCount(0L);
                skuInfoEntity.setSpuId(infoEntity.getId());
                //The search for the default picture is handled at the beginning of the code
                skuInfoEntity.setSkuDefaultImg(defaultImg);
                //Save the encapsulated sku basic information
                skuInfoService.saveSkuInfo(skuInfoEntity);
    
    
                //6.2) picture information of SKU; pms_sku_image
                //Get the skuId for subsequent use
                Long skuId = skuInfoEntity.getSkuId();
                //Gets the fields stored by SkuImagesEntities SkuImagesEntity
                //id  sku_id  img_url  img_sort  default_img
                List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> {
                    SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
                    skuImagesEntity.setSkuId(skuId);
                    skuImagesEntity.setImgUrl(img.getImgUrl());
                    skuImagesEntity.setDefaultImg(img.getDefaultImg());
                    return skuImagesEntity;
                }).filter(entity->{
                    //TODO does not need to save the image path. It has been solved and filtered through the filter
                    //Returning true is the need, and false is the elimination
                    return !StringUtils.isEmpty(entity.getImgUrl());
                }).collect(Collectors.toList());
                //Batch save
                skuImagesService.saveBatch(imagesEntities);
    
    
    
                //6.3) sales attribute information of SKU: pms_sku_sale_attr_value
                //    id  sku_id  attr_id  attr_name  attr_value  attr_sort
                //Get the sales attributes stored in sku
                //attrId attrName attrValue
                //Missing sku_id field
                List<Attr> attr = item.getAttr();
                List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
                    SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
                    BeanUtils.copyProperties(a, attrValueEntity);
                    attrValueEntity.setSkuId(skuId);
                    return attrValueEntity;
                }).collect(Collectors.toList());
                skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
    
                //6.4) SKU's discount, full discount and other information; gulimall_ sms->sms_ sku_ ladder\sms_ sku_ full_ reduction\sms_ member_ price
                //Remote service operation required
                //Review - prerequisites for invoking remote services through feign
                //1. The remote service must be online and placed in the service registry - here, the coupon coupon service is in bootstrap In properties
                //The configuration center has been configured, and there are relevant registry address and other configurations in the associated configuration center configuration file
                //2. The remote service must enable service registration and discovery - here, the startup class of coupon coupon service has been annotated with @ EnableDiscoveryClient
                //3. Define a feign package in the module that wants to call the remote service and write an interface class with @ FeignClient("remote service name")
                //The class indicates the remote service method to be called (no method body needs to be written) and the request path @ RequestMapping
                SkuReductionTo skuReductionTo = new SkuReductionTo();
                BeanUtils.copyProperties(item,skuReductionTo);
                
                skuReductionTo.setSkuId(skuId);
                //Comparison method of BigDeciaml type variables
                if(skuReductionTo.getFullCount() >0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
                    R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
                    if(r1.getCode() != 0){
                        log.error("Remote save sku Offer information failed");
                    }
                }
            });
        }
    }
    
  4. The above big save method calls the service of Coupon module, so feign package is created and com.com is defined atguigu. gulimall. product. feign. CouponFeignService. Two methods are customized in the Coupon module to save points and full discount information

    @FeignClient("gulimall-coupon")
    public interface CouponFeignService {
        //Remotely call the method of saving points in coupon service - be sure to fill in the complete path
        //In addition, @ RequestBody is used to send Post requests and encapsulate them into custom data types
        @PostMapping("/coupon/spubounds/save")
        R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
    
        //Remote call of customized full discount discount method
        @PostMapping("/coupon/skufullreduction/saveinfo")
        R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
    }
    

    The method to save the integral can call the method generated inversely, and the parameter SpuBoundTo of the method is passed in json. When the Coupon module matching request is received in spuboundsententity, the field will be converted.

    The saving full discount method needs to add an interface and method and a new data encapsulation format

    Interface com atguigu. gulimall. coupon. controller. SkuFullReductionController#saveInfo

    /**
     * The custom full discount method is used for remote calling of Product module
     * @param skuReductionTo
     * @return
     */
    @PostMapping("/saveinfo")
    R saveInfo(@RequestBody SkuReductionTo skuReductionTo){
        //Custom method
        skuFullReductionService.saveSkuReduction(skuReductionTo);
        return R.ok();
    }
    

    Full discount To - package of data transfer between services. Com atguigu. common. To. SkuReductionTo

    /**
     * Coupon service full minus TO
     */
    @Data
    public class SkuReductionTo {
    
        private Long skuId;
        private int fullCount;
        private BigDecimal discount;
        private int countStatus;
        private BigDecimal fullPrice;
        private BigDecimal reducePrice;
        private int priceStatus;
        private List<MemberPrice> memberPrice;
    }
    

    Add business layer method com atguigu. gulimall. coupon. service. impl. SkuFullReductionServiceImpl#saveSkuReduction

    /**
     * Save customized discount information
     * @param skuReductionTo
     */
    @Override
    public void  saveSkuReduction(SkuReductionTo skuReductionTo) {
    
        //1. Save step price
        //sms_sku_ladder table
        SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
        skuLadderEntity.setSkuId(skuReductionTo.getSkuId());
        skuLadderEntity.setFullCount(skuReductionTo.getFullCount());
        skuLadderEntity.setDiscount(skuReductionTo.getDiscount());
        skuLadderEntity.setAddOther(skuReductionTo.getCountStatus());
        //Price after discount is the final price when placing an order
        //Judge whether there is a discount price before saving
        if (skuReductionTo.getFullCount()>0){
            skuLadderService.save(skuLadderEntity);
        }
    
        //2. Save full minus information
        //sms_sku_full_reduction table
        SkuFullReductionEntity skuFullReductionEntity = new SkuFullReductionEntity();
        //Attribute copy
        BeanUtils.copyProperties(skuReductionTo,skuFullReductionEntity);
        //The AddOther field used to overlay information in SkuFullReductionEntity, while the CountStatus is stored in skuReductionTo
            skuFullReductionEntity.setAddOther(skuReductionTo.getCountStatus());
        //Judge whether there is a full minus full minus full minus price > 0 to save
        if (skuFullReductionEntity.getFullPrice().compareTo(new BigDecimal("0")) == 1){
            this.save(skuFullReductionEntity);
    
        }
    
        //3. Member price
        //sms_member_price table
        List<MemberPrice> memberPrices = skuReductionTo.getMemberPrice();
        List<MemberPriceEntity> memberPriceEntities = memberPrices.stream().map((memberPrice) -> {
            MemberPriceEntity memberPriceEntity = new MemberPriceEntity();
            memberPriceEntity.setSkuId(skuReductionTo.getSkuId());
            memberPriceEntity.setMemberLevelId(memberPrice.getId());
            memberPriceEntity.setMemberLevelName(memberPrice.getName());
            memberPriceEntity.setMemberPrice(memberPrice.getPrice());
            //Default overlay other offers
            memberPriceEntity.setAddOther(1);
            return memberPriceEntity;
        }).filter(item->{
            //Filter out entities without membership price
            return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
        }).collect(Collectors.toList());
        memberPriceService.saveBatch(memberPriceEntities);
    
    
    }
    
  5. Use debug mode to com atguigu. gulimall. product. service. impl. Spuinfoserviceimpl#savespuinfo method break point to check whether the results of each sub method are correct.

3.4.4 spu management menu

back-end

Query spu according to conditions

  1. Modification interface method com atguigu. gulimall. product. controller. SpuInfoController#list

    /**
     * list
     * Modify query criteria
     */
    @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params){
        //PageUtils page = spuInfoService.queryPage(params);
        PageUtils page = spuInfoService.queryPageByCondition(params);
        return R.ok().put("page", page);
    }
    
  2. New implementation method com atguigu. gulimall. product. service. impl. SpuInfoServiceImpl#queryPageByCondition

    /**
     * Find commodity information according to query criteria
     * @param params
     * @return
     */
    @Override
    public PageUtils queryPageByCondition(Map<String, Object> params) {
    
        QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
        //Splicing query condition key status brandId catelogId
        //Fuzzy query keyword
        String key = (String) params.get("key");
        if(!StringUtils.isEmpty(key)){
            wrapper.and((w)->{
                w.eq("id",key).or().like("spu_name",key);
            });
        }
        // status=1 and (id=1 or spu_name like xxx)
        String status = (String) params.get("status");
        if(!StringUtils.isEmpty(status)){
            wrapper.eq("publish_status",status);
        }
    
        String brandId = (String) params.get("brandId");
        if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(brandId)){
            wrapper.eq("brand_id",brandId);
        }
    
        String catelogId = (String) params.get("catelogId");
        if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){
            wrapper.eq("catelog_id",catelogId);
        }
        
        IPage<SpuInfoEntity> page = this.page(
                new Query<SpuInfoEntity>().getPage(params),
                wrapper
        );
    
        return new PageUtils(page);
    }
    

In addition, due to the return of date type data, it can be found in the configuration file application YML through spring jackson. Date format sets the date return format. If the formatting date is incorrect, you can set the time zone

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

Obtain spu specification information - echo for clicking specification operation

  1. New interface com atguigu. gulimall. product. controller. AttrController#baseAttrListForSpu
@GetMapping("/base/listforspu/{spuId}")
public R baseAttrListForSpu(@PathVariable("spuId") Long spuId){
    //Inject ProductAttrValueService and create a method to obtain specification parameters according to spuId
    List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrListForSpu(spuId);
    return R.ok().put("data",entities);
}
  1. The business layer implements com atguigu. gulimall. product. service. impl. ProductAttrValueServiceImpl#baseAttrListForSpu
/**
 * Obtain specification parameters according to spuId
 * @param spuId
 * @return
 */
@Override
public List<ProductAttrValueEntity> baseAttrListForSpu(Long spuId) {
    List<ProductAttrValueEntity> productAttrValueEntities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
    return productAttrValueEntities;
}

Update spu specification information - used to save the modified spu specification parameters

  1. New interface method com atguigu. gulimall. product. controller. AttrController#updateSpuAttr

    /**
     * Modify - batch modify basic attributes according to spuId
     * Note that the front end places the data in the request body in the way of Json
     * So the @ RequestBody annotation is added when the backend receives
     * In addition, the number of fields passed by the front end is not the same as that of ProductAttrValueEntity
     * vo is supposed to be created, but it is omitted here
     */
    @PostMapping("/update/{spuId}")
    public R updateSpuAttr(@PathVariable("spuId") Long spuId,
                           @RequestBody List<ProductAttrValueEntity> entities){
        //Inject productAttrValueService and define batch update method
        productAttrValueService.updateSpuAttr(spuId,entities);
        return R.ok();
    }
    
  2. The business layer implements com atguigu. gulimall. product. service. impl. ProductAttrValueServiceImpl#updateSpuAttr

/**
 * Batch update based on spuId and ProductAttrValueentities
 * @param spuId
 * @param entities
 */
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
    //Since there is no batch update method, for the sake of laziness, you should first delete in batch and then save in batch. You should write your own batch update method for the actual business
    //delete
    this.remove(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
    //Traverse the entities collection and set spuId
    entities.forEach((entity)->{
        entity.setSpuId(spuId);
    });
    //Batch save
    this.saveBatch(entities);
}
3.4.5 query sku- commodity management menu according to conditions

back-end

  1. Modification interface method com atguigu. gulimall. product. controller. SkuInfoController#list
/**
 * list
 * Modify the query method of new conditions
 */
@RequestMapping("/list")
//@RequiresPermissions("product:skuinfo:list")
public R list(@RequestParam Map<String, Object> params){
    //PageUtils page = skuInfoService.queryPage(params);
    PageUtils page = skuInfoService.queryPageByCondition(params);
    return R.ok().put("page", page);
}
  1. Add business layer method com atguigu. gulimall. product. service. impl. SkuInfoServiceImpl#queryPageByCondition

    Pay attention to the processing of the default value of the front end, and pay attention to exclusion when splicing conditions

/**
 * Method of adding sku condition query
 * @param params
 * @return
 */
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
    //Create conditions
    QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
    //Condition field params to be spliced
    //key catelogId brandId min max
    //Splicing key: note that fuzzy query has two query situations, so use and to splice first
    String key = (String) params.get("key");
    if (!StringUtils.isEmpty(key)){
        queryWrapper.and((wrapper)->{
            wrapper.eq("sku_id",key).or().like("sku_name",key);
        });
    }
    //Note that the processing of the front-end transmission default value is not 0, then splicing
    //Splice catalogid
    String catelogId = (String) params.get("catelogId");
    if (!StringUtils.isEmpty(key) && !"0".equalsIgnoreCase(catelogId)){
        queryWrapper.eq("catelog_id",catelogId);
    }
    //Note that the processing of the front-end transmission default value is not 0, then splicing
    //Splice brandId
    String brandId = (String) params.get("brandId");
    if (!StringUtils.isEmpty(key) && !"0".equalsIgnoreCase(brandId)){
        queryWrapper.eq("brand_id",brandId);
    }
    //The price ge() in the min max price comparison table is greater than or equal to
    //Splicing min
    String min = (String) params.get("min");
    if (!StringUtils.isEmpty(key)){
        queryWrapper.ge("price",min);
    }
    //Because the front end passed in the default value of 0 to both min and max
    //When no value is given, the sql statement sent becomes to query the goods with price=0, and the goods will never be found
    //Therefore, the code should be further modified to consider the case of max=0
    //The price le() in the min max price comparison table is less than or equal to
    //Splicing max
    /*String max = (String) params.get("max");
    if (!StringUtils.isEmpty(key)){
        queryWrapper.le("price",max);
    }*/
    String max = (String) params.get("max");
    if (!StringUtils.isEmpty(key)){
        //Changing the key to BigDecimal excludes 0. Secondly, users may bypass it
        //The max of the non numeric string sent by the front end leads to the creation of BigDecimal exception, which is caught here
        try {
            BigDecimal bigDecimal = new BigDecimal(key);
            //If it is larger than 0, the splicing condition will be queried, otherwise it will not be spliced
            if (bigDecimal.compareTo(new BigDecimal(0)) == 1){
                queryWrapper.le("price",max);
            }
        }catch (Exception e){

        }
    }

    IPage<SkuInfoEntity> page = this.page(
            new Query<SkuInfoEntity>().getPage(params),
            queryWrapper
    );

    return new PageUtils(page);
}

4. Membership module - gulimall_ums Library

Forerunner-

  1. Configure the routing address of the gateway and route the request sent by the front-end member module to the member module
spring:
  cloud:
    gateway:
      routes:
        - id: member_route
          uri: lb://gulimall-member
          predicates:
            - Path=/api/member/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
  1. Check the application of the member module YML file and bootstrap Properties file

    application. Check the service registry application name, data source port number, MyBatisPlus integration configuration, etc. in the YML file

    server:
      port: 8000
    spring:
      datasource:
        username: root
        data-password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.58.149:3306/gulimall_ums
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
      application:
        name: gulimall-member
    mybatis-plus:
      mapper-locations: classpath:/mapper/**/*.xml
      global-config:
        db-config:
          id-type: auto
    

bootstrap.properties mainly sets the address of the configuration center and pulls the configuration information of the configuration center

  1. Check the necessary annotations of the main startup class

    /**
     * Steps for remotely calling other services (test and call coupon coupon service here)
     *  * 1.pom.xml Spring cloud starter openfeign is introduced into the file to enable the service to call other micro services remotely
     *  * 2.Create a Feign package (containing the interface of remote calling service) and write an interface under the package, using the annotation @ FeignClient("remote service name")
     *  * Tell springcloud that the interface needs to be called remotely, and each method of the interface tells which request to call the service is corresponding to the service
     *  * Controller method signature. Note that the request path is complete
     *  * 3.Enable the function of calling other services remotely, and use the annotation @ enablefeignclients (basepackages = "full package name of feign package") on the startup class,
     *  * In this way, once the service is started, the interface annotated with @ FeignClient under feign package will be automatically scanned
     */
    @MapperScan("com.atguigu.gulimall.member.dao")
    @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")
    @EnableDiscoveryClient
    @SpringBootApplication
    public class GulimallMemberApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GulimallMemberApplication.class, args);
        }
    
    }
    

5. Inventory module - gulimall_wms Library

Forerunner-

① check whether the registry, application name and other configurations are added to the configuration file

Check the SQL statement to configure the debug ging under the specified package of logging level

logging:
  level:
    com.atguigu: debug

② check whether the necessary annotation of the main startup class is added. The transaction @ EnableTransactionManagement depends on the requirements

@MapperScan("com.atguigu.gulimall.ware.dao")
@EnableDiscoveryClient

③ add a new gateway route

spring:
  cloud:
    gateway:
      routes:
        - id: ware_route
          uri: lb://gulimall-ware
          predicates:
            - Path=/api/ware/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
5.1 implementation of warehouse maintenance menu
5.1.1 condition query implementation
  1. Transform the business layer method com atguigu. gulimall. ware. service. impl. WareInfoServiceImpl#queryPage
/**
 * Modification method adding condition query
 * @param params
 * @return
 */
@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<WareInfoEntity> queryWrapper = new QueryWrapper<>();
    String key = (String) params.get("key");
    if(!StringUtils.isEmpty(key)){
        queryWrapper.eq("id",key).or().
                like("name",key).or().
                like("address",key).or().like("areacode",key);
    }
    IPage<WareInfoEntity> page = this.page(
            new Query<WareInfoEntity>().getPage(params),
            queryWrapper
    );

    return new PageUtils(page);
}

Other functions can use the reverse generated code

5.2 realization of commodity inventory menu
5.2.1 implementation of condition query
  1. Transform the query method of business layer com atguigu. gulimall. ware. service. impl. WareSkuServiceImpl#queryPage
/**
 * Condition query: query according to skuId and wareId
 * @param params
 * @return
 */
@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();
    String skuId = (String) params.get("skuId");
    if (!StringUtils.isEmpty(skuId) && !"0".equalsIgnoreCase(skuId)){
        wrapper.eq("sku_id",skuId);
    }

    String wareId = (String) params.get("wareId");
    if (!StringUtils.isEmpty(wareId) && !"0".equalsIgnoreCase(wareId)){
        wrapper.eq("ware_id", wareId);
    }

    IPage<WareSkuEntity> page = this.page(
            new Query<WareSkuEntity>().getPage(params),
    );

    return new PageUtils(page);
}

In the actual scenario, the inventory of goods cannot be added or modified from the current menu, but through a whole set of purchase process. This is only for the purpose of adding and modifying the settings in the test stage. The specific inventory increase is realized by the later purchase order maintenance directory.

5.3 realization of purchase order maintenance directory
5.3.1 purchase demand menu wms_purchase_detail

Forerunner-

There are two ways to create a purchase demand: ① manually add a purchase demand from the background; ② if the inventory is too low, the system sends a low inventory warning and automatically generates a purchase demand The following is a brief process of procurement

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-jeikcytb-1646403290789) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646199827197. PNG)]

  1. Purchase demand query.com atguigu. gulimall. ware. service. impl. PurchaseDetailServiceImpl#queryPage
/**
 * Modification of WMS method for querying purchase demand_ purchase_ detail
 * status Status wareId warehouse ID key fuzzy query keyword
 * Carry three query criteria
 * @param params
 * @return
 */
@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<PurchaseDetailEntity> wrapper = new QueryWrapper<>();
    String key = (String) params.get("key");
    if (!StringUtils.isEmpty(key)){
        wrapper.and((w)->{
            w.eq("purchase_id",key).or().eq("sku_id",key)
        });
    }
    String status = (String) params.get("status");
    if (!StringUtils.isEmpty(status)){
        wrapper.eq("status",status);
    }
    String wareId = (String) params.get("wareId");
    if (!StringUtils.isEmpty(status) && !"0".equalsIgnoreCase(wareId)){
        wrapper.eq("ware_id",wareId);
    }

    IPage<PurchaseDetailEntity> page = this.page(
            new Query<PurchaseDetailEntity>().getPage(params),
            wrapper
    );

    return new PageUtils(page);
}
  1. Add an interface for querying unclaimed purchase orders - see 5.3.2 purchase order menu WMS for details_ purchase
  2. Consolidated purchase order - the premise is to create a purchase order and use the new method of reverse generation - see 5.3.2 purchase order menu WMS for details_ purchase
5.3.2 purchase order menu wms_purchase
  1. Interface and business layer implementation method for adding and querying unclaimed purchase orders

    com.atguigu.gulimall.ware.controller.PurchaseController#unreceivelist

    /**
     * Add an interface to query unclaimed purchase orders
     * list
     */
    @RequestMapping("/unreceive/list")
    //@RequiresPermissions("ware:purchase:list")
    public R unreceivelist(@RequestParam Map<String, Object> params){
        //New business layer method
        PageUtils page = purchaseService.queryPageUnreceivePurchase(params);
        return R.ok().put("page", page);
    }
    

com.atguigu.gulimall.ware.service.impl.PurchaseServiceImpl#queryPageUnreceivePurchase

/**
 * Add and query unclaimed purchase orders
 * @param params
 * @return
 */
@Override
public PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {
    QueryWrapper<PurchaseEntity> wrapper = new QueryWrapper<>();
    //Splicing status condition 0 - purchase order newly created 1 - the purchase order has just been assigned to someone and has not been received
    wrapper.eq("status",0).or().eq("status",1);
    IPage<PurchaseEntity> page = this.page(
            new Query<PurchaseEntity>().getPage(params),
            wrapper
    );
    return new PageUtils(page);
}
  1. Consolidated purchase order

    1. Create mergevo to receive the data sent by the front end after clicking the consolidated Po. Com atguigu. gulimall. ware. vo. MergeVo

      @Data
      public class MergeVo {
          private Long purchaseId;//Po Id
          private List<Long> items;//Merge item collection
      }
      
    2. New interface method com atguigu. gulimall. ware. controller. PurchaseController#merge

    /**
     * Add interface consolidation Po
     * post The request data is in the request body
     */
    @PostMapping("/merge")
    //@RequiresPermissions("ware:purchase:list")
    public R merge(@RequestBody MergeVo mergeVo){
        //New business layer method
        purchaseService.mergePurchase(mergeVo);
        return R.ok();
    }
    
    1. Create enumeration class of inventory module COM atguigu. common. constant. WareConstant

      public class WareConstant {
          //Enumeration related to purchase status
          public enum PuchaseStatusEnum{
              CREATED(0,"newly build"),ASSIGNED(1,"Assigned"),
              RECEIVE(2,"Received"),FINISH(3,"Completed"),
              HASERROR(4,"Abnormal"),;
      
              private int typeCode;
              private String typeMsg;
      
              PuchaseStatusEnum(int typeCode, String typeMsg) {
                  this.typeCode = typeCode;
                  this.typeMsg = typeMsg;
              }
      
              public int getTypeCode() {
                  return typeCode;
              }
      
              public String getTypeMsg() {
                  return typeMsg;
              }
          }
          
              //Enumeration related to the status of purchase demand
          public enum PuchaseDetailStatusEnum{
              CREATED(0,"newly build"),ASSIGNED(1,"Assigned"),
              BUYING(2,"Purchasing"),FINISH(3,"Completed"),
              HASERROR(4,"Purchase failure"),;
      
              private int typeCode;
              private String typeMsg;
      
              PuchaseDetailStatusEnum(int typeCode, String typeMsg) {
                  this.typeCode = typeCode;
                  this.typeMsg = typeMsg;
              }
      
              public int getTypeCode() {
                  return typeCode;
              }
      
              public String getTypeMsg() {
                  return typeMsg;
              }
          }
      }
      
    2. Business layer method for adding consolidated purchase orders. Com atguigu. gulimall. ware. service. impl. PurchaseServiceImpl#mergePurchase

        /**
         * Consolidated purchase order
         * @param mergeVo
         */
        @Transactional
        @Override
        public void mergePurchase(MergeVo mergeVo) {
            //If the purchaseId is null, you need to create a new one
            Long purchaseId = mergeVo.getPurchaseId();
            if (purchaseId == null){
                //Purchase order needs to be created
                PurchaseEntity purchaseEntity = new PurchaseEntity();
                //Give default values for creation time and modification time
                purchaseEntity.setCreateTime(new Date());
                purchaseEntity.setUpdateTime(new Date());
                //Purchase order status
                purchaseEntity.setStatus(WareConstant.PuchaseStatusEnum.CREATED.getTypeCode());
                this.save(purchaseEntity);
                //Set Po Id
                purchaseId = purchaseEntity.getId();
            }
    
            //If there is a purchase order id, i.e. purchaseId, check whether the purchase order status is 0 | 1. If not, consolidation is not allowed
            PurchaseEntity byId = this.getById(purchaseId);
            if (byId.getStatus() == WareConstant.PuchaseStatusEnum.CREATED.getTypeCode() ||
            byId.getStatus() == WareConstant.PuchaseStatusEnum.ASSIGNED.getTypeCode()){
                //The purchase order consolidation function of purchase order demand is essentially a modification WMS_ purchase_ In the detail table
                // id is the purchase of the element value traversed in items_ id and status
                //Gets the Id set of the purchase order to be changed
                List<Long> items = mergeVo.getItems();
                //Create a tag, which is the basis for whether the collection is available
                Boolean isCorrect = true;
                for (Long item : items) {
                    //Find out the PurchaseDetailEntity corresponding to the current Id
                    PurchaseDetailEntity purDetailById = purchaseDetailService.getById(item);
                    //PurchaseDetailEntity cannot be found or exists according to Id There is a problem with the status of status. End the method directly
                    if (purDetailById == null || (purDetailById.getStatus() != WareConstant.PuchaseDetailStatusEnum.CREATED.getTypeCode() &&
                            purDetailById.getStatus() != WareConstant.PuchaseDetailStatusEnum.ASSIGNED.getTypeCode())){
                        System.out.println("unjustified purchaseDetailEntity: "+purDetailById);
                        isCorrect = false;
                        return;
                    }
                }
                if (isCorrect){
                    //Traverse and collect as purchaseDetailEntity collection
                    //System.out.println("all purchasedetailentities passed forEach are reasonable");
                    Long finalPurchaseId = purchaseId;
                    List<PurchaseDetailEntity> purchaseDetailEntities = items.stream().map(i -> {
                        PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
                        purchaseDetailEntity.setId(i);
                        //Here, purchaseId must be of type final
                        //The reason is that when a local internal class references an external variable, the external variable becomes final by default
                        purchaseDetailEntity.setPurchaseId(finalPurchaseId);
                        purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.ASSIGNED.getTypeCode());
                        return purchaseDetailEntity;
                    }).collect(Collectors.toList());
                    //Inject purchaseDetailService and call the method of batch modification
                    purchaseDetailService.updateBatchById(purchaseDetailEntities);
                    //Modify the modification time of the current Po
                    if (purchaseId != null){
                        PurchaseEntity p = new PurchaseEntity();
                        p.setId(purchaseId);
                        p.setUpdateTime(new Date());
                        this.updateById(p);
                    }
                }
    
            }
    
        }
    
  2. Receive purchase order

    1. New interface com atguigu. gulimall. ware. controller. PurchaseController#reveived

      /**
       * Add interface to receive purchase order
       * post The request data is in the request body
       * A PO id set is received here
       */
      @PostMapping("/reveived")
      public R reveived(@RequestBody List<Long> ids){
          //If you don't find out the details of an employee's purchase order, you don't consider it here
          // Employees can only get their own purchase orders, etc
          //New method
          purchaseService.reveived(ids);
          return R.ok();
      }
      
    2. Implementation method com atguigu. gulimall. ware. service. impl. PurchaseServiceImpl#reveived

    /**
     * Collect Po ids Po set
     * @param ids
     */
    @Override
    public void reveived(List<Long> ids) {
        //1. Confirm that the current Po is in the status of new or allocated
        List<PurchaseEntity> purchaseEntities = ids.stream().map(id -> {
            return this.getById(id);
        }).filter(item -> {
            //Only new or allocated purchase orders are left in the filter status
            if (item.getStatus() == WareConstant.PuchaseStatusEnum.CREATED.getTypeCode() ||
                    item.getStatus() == WareConstant.PuchaseStatusEnum.ASSIGNED.getTypeCode()) {
                return true;
            }
            return false;
        }).map(item->{
            //Continue mapping change state
            item.setStatus(WareConstant.PuchaseStatusEnum.RECEIVE.getTypeCode());
            item.setUpdateTime(new Date());
            return item;
        }).collect(Collectors.toList());
        //2. Change the status of purchase order
        this.updateBatchById(purchaseEntities);
        //3. Change the status of purchase items in purchase requirements
        purchaseEntities.forEach(item->{
            //Inject purchaseDetailService and add listDetailByPurchaseId method
            // Obtain the purchase demand list according to the purchase order ID, i.e. purchaseId
            List<PurchaseDetailEntity> purchaseDetailEntities = purchaseDetailService.listDetailByPurchaseId(item.getId());
            //To update the purchase demand status, only set the purchase demand Id and purchase status
            List<PurchaseDetailEntity> collect = purchaseDetailEntities.stream().map(entity -> {
                PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
                purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.BUYING.getTypeCode());
                purchaseDetailEntity.setId(entity.getId());
                return purchaseDetailEntity;
            }).collect(Collectors.toList());
                        //Judgment of non empty status of batch update purchase demand
                if (!CollectionUtils.isEmpty(collect)){
                    purchaseDetailService.updateBatchById(collect);
                }
        });
    }
    

​ com.atguigu.gulimall.ware.service.impl.PurchaseDetailServiceImpl#listDetailByPurchaseId

/**
 * Obtain the purchase demand list according to the purchase order ID, i.e. purchaseId
 * @param id
 * @return
 */
@Override
public List<PurchaseDetailEntity> listDetailByPurchaseId(Long id) {
    List<PurchaseDetailEntity> purchaseDetailEntitiesByPurchaseId = this.list(new QueryWrapper<PurchaseDetailEntity>().eq("purchase_id", id));
    return purchaseDetailEntitiesByPurchaseId;
}
  1. Use postman test to get purchase order

    [the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-ygmdy8mb-1646403290791) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646207526264. PNG)]

    [the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG rpnphbxv-1646403290791) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 1646207496089. PNG)]

  2. Complete procurement

    1. Create a new data encapsulation format com atguigu. gulimall. ware. vo. Purchasedonevo and com atguigu. gulimall. ware. vo. PurchaseDoneItemVo

      @Data
      public class PurchaseDoneVo {
          @NotNull
          private Long id;//Po id
          private List<PurchaseDoneItemVo> items;//Purchase item collection
      }
      
      @Data
      public class PurchaseDoneItemVo {
          private Long itemId;//Purchase demand item ID
          private Integer status;//Purchase status 0-4
          private String reason;//Reasons for purchase failure
      }
      
    2. Create interface method com atguigu. gulimall. ware. controller. PurchaseController#finish

      /**
       * Add interface to complete purchase order
       * post The request data is in the request body
       * A PO id set is received here
       */
      @PostMapping("/done")
      public R finish(@RequestBody PurchaseDoneVo doneVo){
          //New method
          purchaseService.done(doneVo);
          return R.ok();
      }
      
    3. New implementation method com atguigu. gulimall. ware. service. impl. PurchaseServiceImpl#done

      /**
       * Processing of purchase completion
       * @param doneVo
       */
      @Transactional
      @Override
      public void done(PurchaseDoneVo doneVo) {
      
          //1. Change the status of purchase items and purchase requirements
          List<PurchaseDoneItemVo> items = doneVo.getItems();
          //Define a flag. Once there is an error in a purchase item, the purchase order will be considered as a purchase failure
          Boolean flag = true;
          //Create a collection to collect processed purchase items
          List<PurchaseDetailEntity> detailList = new ArrayList<>();
          //Traverse the items to separate the successful purchase items from the failed purchase items
          for (PurchaseDoneItemVo item : items) {
              //Create purchase item entity class
              PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
              //Purchase item failed
              if (item.getStatus() == WareConstant.PuchaseDetailStatusEnum.HASERROR.getTypeCode()){
                  //Purchase order purchase failed
                  flag = false;
                  //Failed to set purchase item status
                  purchaseDetailEntity.setStatus(item.getStatus());
              }else {
                  //Successful procurement
                  //Set purchase item status
                  purchaseDetailEntity.setStatus(WareConstant.PuchaseDetailStatusEnum.FINISH.getTypeCode());
      
                  //2. Warehousing of successfully purchased goods
                  //Query the purchase demand according to the purchase item ID for subsequent SKU acquisition_ id ware_ id stock
                  PurchaseDetailEntity purDetailById = purchaseDetailService.getById(item.getItemId());
                  //Inject wareSkuService and according to sku_id, commodity id,ware_id warehouse id and stock add inventory to stock in goods
                  wareSkuService.addStock(purDetailById.getSkuId(), purDetailById.getWareId(), purDetailById.getSkuNum());
      
              }
              //Set purchase item Id
              purchaseDetailEntity.setId(item.getItemId());
              //Add to collection
              detailList.add(purchaseDetailEntity);
          }
          //Batch update purchase item collection
          if (!CollectionUtils.isEmpty(detailList)){
              purchaseDetailService.updateBatchById(detailList);
          }
      
          //3. Change purchase order status
          //Get Po Id
          Long purchaseId = doneVo.getId();
          PurchaseEntity purchaseEntity = new PurchaseEntity();
          purchaseEntity.setId(purchaseId);
          purchaseEntity.setStatus(flag?WareConstant.PuchaseStatusEnum.FINISH.getTypeCode()
                  :WareConstant.PuchaseStatusEnum.HASERROR.getTypeCode());
          purchaseEntity.setUpdateTime(new Date());
          this.updateById(purchaseEntity);
      
      }
      
      1. New wareskuservice Addstock method - Dao layer interface is omitted, and Mapper method is omitted

        When obtaining the Product name here, the remote end calls the Product module

            /**
             * Update inventory according to skuId wareId skuNum
             * @param skuId Item Id
             * @param wareId Warehouse Id
             * @param skuNum New commodity quantity
             */
            @Override
            public void addStock(Long skuId, Long wareId, Integer skuNum) {
        
                //Judge whether there is this record in the inventory. If not, add it and modify it
                List<WareSkuEntity> list = this.list(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
                if (list == null || list.size() == 0){
                    //No such record is found and added
                    WareSkuEntity wareSkuEntity = new WareSkuEntity();
                    wareSkuEntity.setSkuId(skuId);
                    wareSkuEntity.setStock(skuNum);
                    wareSkuEntity.setWareId(wareId);
                    //Inventory is not locked by default
                    wareSkuEntity.setStockLocked(0);
                    //Remote calling interface productFeignService for injecting goods and services
                    //Here, you can only call the service remotely to get a skuName. If it fails, rollback after throwing an exception is not worth the loss
                    //Use here to catch the exception directly and ensure that it will not be rolled back just because the skuName acquisition fails
                    //TODO has another method that can be optimized in the advanced section without rolling back transactions
                    try {
                        R r = productFeignService.info(skuId);
                        // The useful data transmitted from goods and services is a map < string, Object >
                        Map<String,Object> skuInfo = (Map<String, Object>) r.get("skuInfo");
                        if (r.getCode() == 0){
                            //Query successfully set skuName 
                            wareSkuEntity.setSkuName((String) skuInfo.get("skuName"));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    this.baseMapper.insert(wareSkuEntity);
                }else {
                    //Modify inventory
                    //Custom dao layer method
                    this.baseMapper.addStock(skuId,wareId,skuNum);
                }
        
            }
        
      2. The configuration class of MP is added to ensure the normal use of paging plug-in. Transaction startup and Dao interface scanning are also migrated from the main startup class

        @Configuration
        @EnableTransactionManagement
        @MapperScan("com.atguigu.gulimall.ware.dao")
        public class WareMyBatisConfig {
        
            //Introduce paging plug-in
            @Bean
            public PaginationInterceptor paginationInterceptor(){
                PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
                //Set the requested page number to be greater than the operation after the last page. If true, the first page will be returned. If false, continue to request the default false
                paginationInterceptor.setOverflow(true);
                //Set the maximum number of single page. The default number is 500 - 1 unlimited
                paginationInterceptor.setLimit(1000);
        
                return paginationInterceptor;
            }
        }
        
      3. Step 4: the emphasis of the method of remote calling the gulimall product module

        Create feign package and interface com atguigu. gulimall. ware. entity. feign. ProductFeignService

        If the main startup class starts feign-@EnableFeignClients, does it need to be written differently by the gateway

        @FeignClient("gulimall-gateway")
        public interface ProductFeignService {
         /**
             * Matching path / product/skuinfo/info/{skuId} vs /api/product/skuinfo/info/{skuId}
             * 1.Let all requests pass through the gateway (security, load balancing, etc.)
             *   1)Matching path / api/product/skuinfo/info/{skuId}
             *   2)The service name @ feignclient ("gulimall gateway") sends a request to the machine where the gateway service is located
             * 2.Directly let the background specify service processing
             *   1)Matching path / product/skuinfo/info/{skuId}
             *   2)The service name @ feignclient ("gulimall product") sends a request to the machine where the goods and services are located
             * @param skuId
             * @return
             */
            //@RequestMapping("/product/skuinfo/info/{skuId}")
            @RequestMapping("/api/product/skuinfo/info/{skuId}")
            public R info(@PathVariable("skuId") Long skuId);
        }
        
      4. Simulate the data sent by purchasing with postman

    [the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-cot7xws-1646403290792) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 1646227808777. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-5o60pm7y-1646403290793) (C: \ users \ CK \ appdata \ roaming \ typora user images \ 1646234302774. PNG)]

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-zhdt02ze-1646403290794) (C: \ users \ CK \ appdata \ roaming \ typora \ typora user images \ 16462345179. PNG)]

4, Distributed foundation chapter summary

1. Distributed basic concept

Microservices – the biggest feature is that each microservice is independent and autonomous. Different functional modules can be drawn into a microservice, allowing different developers to develop in parallel to improve development efficiency.

Registration Center (Nacos) – after the project is divided into various micro services, different services may need to call each other. Therefore, a registration center needs to know the location of each micro service in real time. When this service calls other micro services, its registration list can be obtained through the registration center.

Configuration center (Nacos) – whether it is a single service or a micro service, it is recommended to use the configuration center after going online. In this way, the configuration file can be modified online through a visual configuration center interface without changing the configuration file of the local source code and then going online. After the modification, the micro service automatically updates the latest configuration information.

**Feign – * * in the development stage, there is a need to call other services remotely. Here, feign is used to send a request to other services to call the functions of other services.

④ it is also required to use the tag name of the remote controller to import the remote service (@ feign @ feign) to the remote controller (② it is also required to use the tag name of the remote controller to register the remote service) (@ feign @ feign) to start the remote service.

Gateway – all service requests must pass through the gateway and load balance to the specified service through the gateway. Therefore, the gateway can make many unified settings. For example, the front end sends Ajax requests to the back end. Due to different service addresses, there is a cross domain problem. The basic chapter solves the cross domain problem by adding configuration classes to the gateway and setting CorsWebFiter.

2. Basic development

SpringBoot 2.0 - the basic part does not reflect it for the time being, only some configuration information names of the configuration file have been changed. But spring boot 2 0 introduces Reactor (reactive programming) based on spring 5. Webflux brought by Reactor can easily create a high-performance and concurrent web application. The CorsWebFiter used in the gateway service configuration cross domain uses Webflux

**SpringCloud – * * the basic chapter only uses some annotations, such as opening service registration and discovery @ EnableDiscoveryClient, and opening service remote call @ EnableFeignClients(basePackage = "feign package")

**Mybatis plus – * * only the configuration class is set, the PaginationInterceptor required by the paging plug-in is added, the package scanning @ mappercan (basepackage = "dao package") is set, and the transaction @ EnableTransactionManagement is started.

**Vue componentization – * * the front-end uses Renren fast Vue to quickly build the front-end framework, understand the Vue componentization development process, and use ElementUI components such as tree controls, dialog boxes, cascade selectors, etc.

**Alibaba cloud object storage OSS – * * experienced the calling process of third-party services

3. Environment

VMware,Linux,Docker,Mysql,Redis, Reverse Engineering & Renren open source

4. Development specifications

Data verification JSR303, global exception handling, global unified return, global cross domain processing

Enumeration status, business status code, VO & to & Po division, logical deletion – (integrate MP, by adding logical deletion annotation * * @ TableLogic * * and @ TableLogic(value = "1", delval = "0") to the entity class field, you can set the values of logical deletion and non deletion)

Lombok–@Data @Slf4j(log.error())

FeignService
//Here, you can only call the service remotely to get a skuName. If it fails, rollback after throwing an exception is not worth the loss
//Use here to catch the exception directly and ensure that it will not be rolled back just because the skuName acquisition fails
//TODO has another method that can be optimized in the advanced section without rolling back transactions
try {
R r = productFeignService.info(skuId);
//The useful data transmitted from goods and services is a map < string, Object >
Map<String,Object> skuInfo = (Map<String, Object>) r.get("skuInfo");
if (r.getCode() == 0){
//Query successfully set skuName
wareSkuEntity.setSkuName((String) skuInfo.get("skuName"));
}
} catch (Exception e) {
e.printStackTrace();
}
this.baseMapper.insert(wareSkuEntity);
}else {
//Modify inventory
//Custom dao layer method
this.baseMapper.addStock(skuId,wareId,skuNum);
}

         }
     ```

  5. newly added MP The configuration class of ensures the normal use of the paging plug-in and transaction startup Dao The scanning of the interface is also migrated from the main startup class

     ```java
     @Configuration
     @EnableTransactionManagement
     @MapperScan("com.atguigu.gulimall.ware.dao")
     public class WareMyBatisConfig {
     
         //Introduce paging plug-in
         @Bean
         public PaginationInterceptor paginationInterceptor(){
             PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
             //Set the requested page number to be greater than the operation after the last page. If true, the first page will be returned. If false, continue to request the default false
             paginationInterceptor.setOverflow(true);
             //Set the maximum number of single page. The default number is 500 - 1 unlimited
             paginationInterceptor.setLimit(1000);
     
             return paginationInterceptor;
         }
     }
     ```

  6. Step 4 remote call gulimall-product Emphasis of modular approach

     establish feign Package and interface com.atguigu.gulimall.ware.entity.feign.ProductFeignService

     Main startup class on feign-**@EnableFeignClients**,Do you need different ways to write gateway

     ```java
     @FeignClient("gulimall-gateway")
     public interface ProductFeignService {
      /**
          * Matching path / product/skuinfo/info/{skuId} vs /api/product/skuinfo/info/{skuId}
          * 1.Let all requests pass through the gateway (security, load balancing, etc.)
          *   1)Matching path / api/product/skuinfo/info/{skuId}
          *   2)The service name @ feignclient ("gulimall gateway") sends a request to the machine where the gateway service is located
          * 2.Directly let the background specify service processing
          *   1)Matching path / product/skuinfo/info/{skuId}
          *   2)The service name @ feignclient ("gulimall product") sends a request to the machine where the goods and services are located
          * @param skuId
          * @return
          */
         //@RequestMapping("/product/skuinfo/info/{skuId}")
         @RequestMapping("/api/product/skuinfo/info/{skuId}")
         public R info(@PathVariable("skuId") Long skuId);
     }
     ```

     

  7. use postman Data sent by simulated purchase

[external chain picture transferring... (img-cot7xws-1646403290792)]

[external chain picture transferring... (img-5o60pM7y-1646403290793)]

[external chain picture transferring... (img-ZhdT02ZE-1646403290794)]

4, Distributed foundation chapter summary

1. Distributed basic concept

Microservices – the biggest feature is that each microservice is independent and autonomous. Different functional modules can be drawn into a microservice, allowing different developers to develop in parallel to improve development efficiency.

Registration Center (Nacos) – after the project is divided into various micro services, different services may need to call each other. Therefore, a registration center needs to know the location of each micro service in real time. When this service calls other micro services, its registration list can be obtained through the registration center.

Configuration center (Nacos) – whether it is a single service or a micro service, it is recommended to use the configuration center after going online. In this way, the configuration file can be modified online through a visual configuration center interface without changing the configuration file of the local source code and then going online. After the modification, the micro service automatically updates the latest configuration information.

**Feign – * * in the development stage, there is a need to call other services remotely. Here, feign is used to send a request to other services to call the functions of other services.

④ it is also required to use the tag name of the remote controller to import the remote service (@ feign @ feign) to the remote controller (② it is also required to use the tag name of the remote controller to register the remote service) (@ feign @ feign) to start the remote service.

Gateway – all service requests must pass through the gateway and load balance to the specified service through the gateway. Therefore, the gateway can make many unified settings. For example, the front end sends Ajax requests to the back end. Due to different service addresses, there is a cross domain problem. The basic chapter solves the cross domain problem by adding configuration classes to the gateway and setting CorsWebFiter.

2. Basic development

SpringBoot 2.0 - the basic part does not reflect it for the time being, only some configuration information names of the configuration file have been changed. But spring boot 2 0 introduces Reactor (reactive programming) based on spring 5. Webflux brought by Reactor can easily create a high-performance and concurrent web application. The CorsWebFiter used in the gateway service configuration cross domain uses Webflux

**SpringCloud – * * the basic chapter only uses some annotations, such as opening service registration and discovery @ EnableDiscoveryClient, and opening service remote call @ EnableFeignClients(basePackage = "feign package")

**Mybatis plus – * * only the configuration class is set, the PaginationInterceptor required by the paging plug-in is added, the package scanning @ mappercan (basepackage = "dao package") is set, and the transaction @ EnableTransactionManagement is started.

**Vue componentization – * * the front-end uses Renren fast Vue to quickly build the front-end framework, understand the Vue componentization development process, and use ElementUI components such as tree controls, dialog boxes, cascade selectors, etc.

**Alibaba cloud object storage OSS – * * experienced the calling process of third-party services

3. Environment

VMware,Linux,Docker,Mysql,Redis, Reverse Engineering & Renren open source

4. Development specifications

Data verification JSR303, global exception handling, global unified return, global cross domain processing

Enumeration status, business status code, VO & to & Po division, logical deletion – (integrate MP, by adding logical deletion annotation * * @ TableLogic * * and @ TableLogic(value = "1", delval = "0") to the entity class field, you can set the values of logical deletion and non deletion)

Lombok–@Data @Slf4j(log.error())

Topics: Java Front-end