diff --git "a/\351\273\221\351\251\254\345\225\206\345\237\216\345\256\236\346\210\230\347\255\224\346\241\210.md" "b/\351\273\221\351\251\254\345\225\206\345\237\216\345\256\236\346\210\230\347\255\224\346\241\210.md" index c8f4193d762e3a402652447b976eb9ea991e3943..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- "a/\351\273\221\351\251\254\345\225\206\345\237\216\345\256\236\346\210\230\347\255\224\346\241\210.md" +++ "b/\351\273\221\351\251\254\345\225\206\345\237\216\345\256\236\346\210\230\347\255\224\346\241\210.md" @@ -1,1403 +0,0 @@ -# 黑马商城实战-上课笔记 - -## 1.搭建运行环境 - -### 1.1 前端配置 - -```asciiarmor -1、nginx不要放到有中文的目录 -2、前端项目不要放到有中文的目录 -3、查看错误日志:logs\error.log -4、如果使用绝对路径,必须使用左斜杠: / -C:/work/nginx-1.18.0-hmall/hm-mall-admin -``` - -```nginx -server { - listen 9001; - server_name localhost; - location / { - root hm-mall-admin; - } -} - -server { - listen 9002; - server_name localhost; - location / { - root hm-mall-portal; - } -} -``` - -```sh -#强制关闭nginx -taskkill /f /im nginx.exe -``` - -管理端:http://localhost:9001 - -用户端:http://localhost:9002 - - - -### 1.2 网关配置 - -#### 1.2.1.创建网关服务gateway - -```xml - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-discovery - - - - org.springframework.cloud - spring-cloud-starter-gateway - - -``` -新建application.yml: - -```yml -server: - port: 10010 -``` - - - -#### 1.2.2.配置网关路由 - -```yml -spring: - application: - name: gateway - cloud: - gateway: - routes: - - id: userservice - uri: lb://userservice # 路由的地址,lb,负载均衡 - predicates: - - Path=/user/**,/address/** - - id: orderservice - uri: lb://orderservice - predicates: - - Path=/order/**,/pay/** - - id: itemservice - uri: lb://itemservice - predicates: - - Path=/item/** - - id: searchservice - uri: lb://searchservice - predicates: - - Path=/search/** -``` - -#### 1.2.3.在网关配置CORS - -```yml - #注意缩进:和routes同级 - globalcors: # 全局的跨域处理 - add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题 - corsConfigurations: - '[/**]': - allowedOrigins: # 允许哪些网站的跨域请求: *代表所有 - - "http://localhost:9001" - - "http://localhost:9002" - - "http://127.0.0.1:9001" - - "http://127.0.0.1:9002" - allowedMethods: # 允许的跨域ajax的请求方式 - - "GET" - - "POST" - - "DELETE" - - "PUT" - - "OPTIONS" - allowedHeaders: "*" # 允许在请求中携带的头信息 - allowCredentials: true # 是否允许携带cookie - maxAge: 360000 # 这次跨域检测的有效期 -``` - -#### 1.2.4.启动网关服务 - -编写启动类:com.hmall.gateway.GatewayApplication - -```java -package com.hmall.gateway; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class GatewayApplication { - - public static void main(String[] args) { - SpringApplication.run(GatewayApplication.class, args); - } -} -``` - -```asciiarmor -1、必须启动nacos -2、nacos不在本地的话,需要配置具体ip: - nacos: - server-addr: 虚拟机IP:8848 -``` - - - -## 2.商品管理业务 - -### 2.1.分页查询商品 - -添加分页拦截器: - -```java -package com.hmall.item.config; - -import com.baomidou.mybatisplus.annotation.DbType; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MyBatisConfig { - @Bean - public MybatisPlusInterceptor mybatisPlusInterceptor() { - MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); - //在sql之后追加limit - interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); - return interceptor; - } -} -``` - -表现层ItemController中新增方法: - -```java -@GetMapping("/list") -public PageDTO queryItemByPage(Integer page, Integer size) { - // 分页查询 - Page result = itemService.page(new Page<>(page, size)); - // 封装并返回 - return new PageDTO<>(result.getTotal(), result.getRecords()); -} -``` - -### 2.2.根据id查询商品 - -```java -@GetMapping("{id}") -public Item queryItemById(@PathVariable("id") Long id) { - return itemService.getById(id); -} -``` - -### 2.3.新增商品 - -```java -@PostMapping -public void saveItem(@RequestBody Item item) { - // 基本数据 - item.setCreateTime(new Date()); - item.setUpdateTime(new Date()); - item.setStatus(2); //默认下架 - // 新增 - itemService.save(item); -} -``` - -### 2.4.商品上架、下架 - -**业务层** - -```java -void updateStatus(Long id, Integer status); -``` - -```java -@Override -public void updateStatus(Long id, Integer status) { - - //update tb_item set status = ? where id = ? - this.update(Wrappers.lambdaUpdate() - .set(Item::getStatus, status) - .eq(Item::getId, id)); - - //另外一种实现方式 - //this.lambdaUpdate() - // .set(Item::getStatus, status) - // .eq(Item::getId, id).update(); -} -``` - -**表现层** - -```java -@PutMapping("/status/{id}/{status}") -public void updateItemStatus(@PathVariable("id") Long id, - @PathVariable("status") Integer status){ - itemService.updateStatus(id, status); -} -``` - -### 2.5.修改商品 - -```java -@PutMapping -public void updateItem(@RequestBody Item item) { - // 基本数据 - item.setUpdateTime(new Date()); - // 不允许修改商品状态,所以强制设置为null,更新时,就会忽略该字段 - item.setStatus(null); - // 更新 - itemService.updateById(item); -} -``` - -### 2.6.根据id删除商品 - -```java -@DeleteMapping("{id}") -public void deleteItemById(@PathVariable("id") Long id) { - itemService.removeById(id); -} -``` - - - -## 3.搜索业务 - -### 3.1.创建搜索服务 - -```asciiarmor -注意:搜索服务不需要查询MySQL数据库,因此不能在pom添加任何MP和MySQL相关的坐标 -原因:SpringBoot自动配置:引入一个jar,自动给你产生一些对象(driver class, username, password) -``` - -```xml - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-discovery - - - - org.springframework.boot - spring-boot-starter-web - - - - org.elasticsearch.client - elasticsearch-rest-high-level-client - - - com.alibaba - fastjson - 1.2.76 - - - - org.springframework.boot - spring-boot-starter-test - - - - com.hmall - feign-api - 1.0 - - - -``` - -```yml -server: - port: 8084 -spring: - application: - name: searchservice - cloud: - nacos: - server-addr: localhost:8848 -``` - -### 3.2.设计索引库数据结构 - -基本字段包括: - -- 用于关键字全文检索的字段,比如All,里面包含name、brand、category信息 -- 分类 -- 品牌 -- 价格 -- 销量 -- id -- name -- 评价数量 -- 图片 - -```json -PUT /item -{ - "mappings": { - "properties": { - "id":{ - "type": "keyword" - }, - "name":{ - "type": "text", - "analyzer": "ik_smart", - "copy_to": "all" - }, - "image":{ - "type": "keyword", - "index": false - }, - "price":{ - "type": "long" - }, - "brand":{ - "type": "keyword", - "copy_to": "all" - }, - "category":{ - "type": "keyword", - "copy_to": "all" - }, - "sold":{ - "type": "integer" - }, - "commentCount":{ - "type": "integer" - }, - "isAD":{ - "type": "boolean" - }, - "all":{ - "type": "text", - "analyzer": "ik_smart" - } - } - } -} -``` - - - -ItemDoc类: - -```java -package com.hmall.search.pojo; - -import com.hmall.common.dto.Item; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.beans.BeanUtils; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -public class ItemDoc { - private Long id; - private String name; - private Long price; - private String image; - private String category; - private String brand; - private Integer sold; - private Integer commentCount; - private Boolean isAD; - - public ItemDoc(Item item) { - // 属性拷贝 - BeanUtils.copyProperties(item, this); - } -} -``` - -### 3.3.数据导入 - -要把数据库数据导入到elasticsearch中,包括下面几步: - -1)将商品微服务中的分页查询商品接口定义为一个远程调用的接口,放到**feign-api模块**中 - -```java -package com.hmall.common.client; - -import com.hmall.common.dto.Item; -import com.hmall.common.dto.PageDTO; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(value = "itemservice") -public interface ItemClient { - - //@RequestParam("page")不能省略,必须添加到参数前边(open-feign的要求) - @GetMapping("/item/list") - PageDTO queryItemByPage(@RequestParam("page") Integer page, - @RequestParam("size") Integer size); - -} -``` - -在**搜索微服务**的引导类中:指定feign接口所在包 - -```java -package com.hmall.search; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.openfeign.EnableFeignClients; - -@SpringBootApplication -//==========注意导包 -@EnableFeignClients(basePackages = "com.hmall.common.client") -public class SearchApplication { - public static void main(String[] args) { - SpringApplication.run(SearchApplication.class, args); - } - - //指定es服务器的host和port - @Bean - public RestHighLevelClient restHighLevelClient() { - return new RestHighLevelClient(RestClient.builder( - HttpHost.create("http://192.168.200.130:9200") - )); - } - -} - -``` - -2)**搜索服务**编写一个单元测试,实现下面功能: - -- 在**搜索服务中**需要使用Feign远程调用订单微服务,添加如下坐标: - - ```xml - - - com.hmall - feign-api - 1.0 - - ``` - -- 调用item-service提供的FeignClient,分页查询商品 `PageDTO` - -- 将查询到的商品封装为一个`ItemDoc`对象,放入`ItemDoc`集合 - -- 将`ItemDoc`集合批量导入elasticsearch中 - -```asciiarmor -因为要远程调用商品微服务,因此需要: -1、启动Nacos -2、启动itemservice -``` - -```java -package com.hmall.search.feign; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.hmall.common.client.ItemClient; -import com.hmall.common.dto.Item; -import com.hmall.common.dto.PageDTO; -import com.hmall.search.pojo.ItemDoc; -import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.client.RestHighLevelClient; -import org.elasticsearch.common.xcontent.XContentType; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.io.IOException; -import java.util.List; - -@SpringBootTest(classes = SearchApplication.class) -public class FeignTest { - - @Autowired - private ItemClient itemClient; - - @Autowired - private RestHighLevelClient restHighLevelClient; - - @Test //注意导包,用长的那个:import org.junit.jupiter.api.Test; - public void testQueryItem() throws IOException { - // http://itemservice/item/list?page=1&size=5 - int page = 1, size = 100; - - while (true) { - //远程调用商品微服务进行分页查询: - PageDTO pageDTO = itemClient.queryItemByPage(page, size); - List list = pageDTO.getList(); - if (CollectionUtils.isEmpty(list) || page > 10) { - break; - } - // 1.准备BulkRequest - BulkRequest request = new BulkRequest(); - // 2.准备DSL - // 遍历 - for (Item item : list) { - if(item.getStatus() == 2){ - // 下架商品直接跳过 - continue; - } - // 把 Item 转为 ItemDoc - ItemDoc itemDoc = new ItemDoc(item); - //将itemDoc使用jackson转成json数据 - String json = JSON.toJSONString(itemDoc); - // 添加新增请求 - request.add(new IndexRequest("item") - .id(itemDoc.getId().toString()) - .source(json, XContentType.JSON) - ); - } - // 3.发请求,批量处理 - restHighLevelClient.bulk(request, RequestOptions.DEFAULT); - page++; - } - } -} -``` - - - -### 3.4.实现基本搜索功能 - -```json -# 查询 分页 高亮 排序 -GET /item/_search -{ - "query": { - "match": { - "all": "游戏" - } - }, - "from": 0, - "size": 20, - "sort": [ - { - "price": { - "order": "desc" - } - } - ], - "highlight": { - "fields": { - "name": { - "require_field_match": "false", - "pre_tags": [ - "" - ], - "post_tags": [ - "" - ] - } - } - } -} -``` - -| 请求方式 | POST | -| ---------- | ------------------------------------------------------------ | -| 请求路径 | /search/list | -| 请求参数 | RequestParams对象,
{
"key": "游戏手机",
"page": 2,
"size": 20,
"sortBy": "price",
"category": "手机",
"brand": "小米",
"minPrice": 1500,
"maxPrice": 999999
} | -| 返回值类型 | `PageDTO`分页结果。
{ "total": 200, "list": [{}, {} , {}]} | -| 接口说明 | 根据搜索条件搜索文档 | - - - -#### 1、根据接口定义方法 - -根据接口文档中参数,定义实体类接收: - -```java -package com.hmall.search.pojo; - -import lombok.Data; - -@Data -//SearchDTO -public class RequestParams { - private String key; - private Integer page = 1; - private Integer size = 5; - private String sortBy; - private String brand; - private String category; - private Long minPrice; - private Long maxPrice; -} -``` - -SearchController: - -```java -package com.hmall.search.web; - -@RestController -@RequestMapping("/search") -public class SearchController { - @PostMapping("list") - public PageDTO search(@RequestBody RequestParams params){ - //return searchService.search(params); - return null; - } -} -``` - -#### 2、业务层实现 - -> 接口:IService,实现类: Service -> -> 接口:Service,实现类:ServiceImpl - -接口: - -```java -public interface ISearchService { - PageDTO search(RequestParams params); -} -``` - -实现类: - -```java -@Service -public class SearchService implements ISearchService { - - @Autowired - private RestHighLevelClient restHighLevelClient; - - @Override - public PageDTO search(RequestParams params) { - try { - // 1.准备Request - SearchRequest request = new SearchRequest("item"); - - // 2.准备DSL - // 2.1.query条件 - buildBasicQuery(request, params); - - - // 2.2.分页 - int page = params.getPage(); - int size = params.getSize(); - request.source().from((page - 1) * size).size(size); - // 2.3.排序 - String sortBy = params.getSortBy(); - if ("sold".equals(sortBy)) { - request.source().sort(sortBy, SortOrder.DESC); - } else if ("price".equals(sortBy)) { - request.source().sort(sortBy, SortOrder.ASC); - } - // 2.4.高亮 - request.source().highlighter(new HighlightBuilder() - .field("name") - .requireFieldMatch(false) - .preTags("") - .postTags("")); - // 3.发请求 - SearchResponse response = - restHighLevelClient.search(request, RequestOptions.DEFAULT); - - // 4.解析结果 - SearchHits searchHits = response.getHits(); - // 4.1.total - long total = searchHits.getTotalHits().value; - // 4.2.数据 - SearchHit[] hits = searchHits.getHits(); - // 4.3.遍历 - List list = new ArrayList<>(hits.length); - for (SearchHit hit : hits) { - // 4.4.获取source - String json = hit.getSourceAsString(); - // 4.5.转Java - ItemDoc itemDoc = JSON.parseObject(json, ItemDoc.class); - // 4.6.获取高亮 - Map map = hit.getHighlightFields(); - if (map != null && map.size() > 0) { - HighlightField field = map.get("name"); - String value = field.getFragments()[0].string(); - itemDoc.setName(value); - } - list.add(itemDoc); - } - return new PageDTO<>(total, list); - } catch (IOException e) { - e.printStackTrace(); - } - } -} -``` - -```java -private void buildBasicQuery(SearchRequest request, RequestParams params) { - // 1.创建布尔查询 - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); - // 1.1. key - String key = params.getKey(); - if (StringUtils.isNotBlank(key)) { - // 非空 - boolQuery.must(QueryBuilders.matchQuery("all", key)); - } else { - // 空 - boolQuery.must(QueryBuilders.matchAllQuery()); - } - - // 1.2. brand - String brand = params.getBrand(); - if (StringUtils.isNotBlank(brand)) { - //精确查询:keyword类型的字符串(不需要分词) - boolQuery.filter(QueryBuilders.termQuery("brand", brand)); - } - // 1.3. category - String category = params.getCategory(); - if (StringUtils.isNotBlank(category)) { - boolQuery.filter(QueryBuilders.termQuery("category", category)); - } - // 1.4. price - Long minPrice = params.getMinPrice()0; - Long maxPrice = params.getMaxPrice(); - - if (minPrice != null && maxPrice != null) { - boolQuery.filter( - QueryBuilders.rangeQuery("price") - .gte(minPrice * 100) //MySQL和ES中记录的价格单位为分,前端传递的是元 - .lte(maxPrice * 100)); - } - - // 2.加入竞价排名(修改算法函数) - FunctionScoreQueryBuilder queryBuilder = QueryBuilders.functionScoreQuery( - boolQuery, //原始条件 - new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ - new FunctionScoreQueryBuilder.FilterFunctionBuilder( - QueryBuilders.termQuery("isAD", true), - ScoreFunctionBuilders.weightFactorFunction(100) //权重 - ) - } - ); - //输出最终ES执行DSL语句 - System.out.println(queryBuilder); - request.source().query(queryBuilder); -} -``` - -#### 3、完善调用&测试 - -```java -@PostMapping("list") -public PageDTO search(@RequestBody RequestParams params){ - return searchService.search(params); -} -``` - -测试基本搜索:http://localhost:9002 - -```asciiarmor -注意: -1、搜索服务启动后,要等成功注册到Nacos中才能在页面上测试查看 -2、测试竞价排名时,可以使用DSL将某些商品的isAD=true,再看效果 -POST /item/_update/626738 -{ - "doc": { - "isAD": true - } -} -``` - - - -## 4.登录用户信息获取 - -因为我们没有做登录功能,所以我们会默认用户已经登录。 - -但是微服务运行中,需要获取一个登录的用户身份,该怎么办呢? - - - -### 4.1.给所有请求添加用户身份 - -我们的要求是这样的: - -所有经过网关的请求,都在请求头中添加一个头信息:authorization = 2 - -> 提示:在网关中配置默认过滤器,给header中添加authorization = 2 -> -> 提示:SpringCloud-day02(网关默认过滤器配置) - -gateway: - -```yaml - default-filters: #注意层级,放到spring.cloud.gateway下 - - AddRequestHeader=authorization, 2 -``` - - - -### 4.2.微服务获取用户身份 - -网关已经给所有请求添加了用户身份,也就是authorization头信息。 - -- 在**订单微服务**中编写一个SpringMVC的拦截器:HandlerInterceptor - - > 提示:SpringMVC-day02(拦截器) - - ```java - package com.hmall.order.interceptors; - - import org.apache.commons.lang.StringUtils; - - - @Slf4j - public class UserInterceptor implements HandlerInterceptor { - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - //1、需要从请求header中获取authorization = 2 - //2、将userID放入ThreadLocal - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, Exception ex) throws Exception { - // 用完不要忘了清理 - } - } - ``` - - 指定拦截器拦截路径: - - ```java - package com.hmall.order.config; - - import com.hmall.order.interceptors.UserInterceptor; - import org.springframework.context.annotation.Configuration; - import org.springframework.web.servlet.config.annotation.InterceptorRegistry; - import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - - @Configuration - public class MvcConfig implements WebMvcConfigurer { - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new UserInterceptor()).addPathPatterns("/**"); - } - } - ``` - -- 在拦截器中获取请求头中的authorization信息,也就是userId,并保存到ThreadLocal中 - - > 提示:苍穹外卖-day02(新增员工时使用ThreadLocal) - > - - ```java - package com.hmall.order.utils; - - public class UserHolder { - - private static final ThreadLocal tl = new ThreadLocal<>(); - - public static void setUser(Long userId) { - tl.set(userId); - } - public static Long getUser() { - return tl.get(); - } - public static void removeUser(){ - tl.remove(); - } - } - ``` - - ```java - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - // 1.获取请求头:authorization = 2 - String authorization = request.getHeader("authorization"); - if(StringUtils.isBlank(authorization)){ - log.warn("非法用户访问!请求路径:{}", request.getRequestURI() ); - // 没有用户信息,未登录 403 禁止 - response.setStatus(403); - return false; //返回false,直接结束请求(不再到达Controller) - } - // 2.转换用户id - Long userId = Long.valueOf(authorization); - // 3.存入ThreadLocal - UserHolder.setUser(userId); - // 3.放行 - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - // 用完不要忘了清理 - UserHolder.removeUser(); - } - ``` - - -### 4.3.测试 - -在订单服务中,获取用户ID:OrderController - -```java -@GetMapping("/hi") -public String hi() { - Long userId = UserHolder.getUser(); - System.err.println(userId); - return "hi"; -} -``` - -http://localhost:10010/order/hi - -```asciiarmor -启动网关服务,订单服务即可 -``` - - - -## 5.用户相关业务 - -### 5.1.根据用户id查询地址列表 - -接口说明: - -| 请求方式 | GET | -| ---------- | ------------------------------------------------------------ | -| 请求路径 | /address/uid/{userId} | -| 请求参数 | userId:用户id | -| 返回值类型 | `List`
[
{
"id":61,
"userId":2,
"contact":"李佳星",
"mobile":"13301212233",
"province":"上海",
"city":"上海",
"town":"浦东新区",
"street":"航头镇航头路",
"isDefault":true
}
] | -| 接口说明 | 根据用户id查询地址列表 | - -AddressController - -```java -@GetMapping("/uid/{userId}") -public List
findAddressByUserId(@PathVariable("userId") Long userId) { - - //where user_id =? - return addressService.list( - //new LambdaQueryWrapper
().eq(Address::getUserId, userId) - Wrappers.
lambdaQuery().eq(Address::getUserId, userId) - ); - //return addressService.lambdaQuery().eq(Address::getUserId, userId).list(); -} -``` - -### 5.2.根据addressId查询Address - -接口说明: - -| 请求方式 | GET | -| ---------- | ------------------------------------------------------------ | -| 请求路径 | /address/{addressId} | -| 请求参数 | addressId:地址id | -| 返回值类型 | Adderss对象:
{
"id":61,
"userId":2,
"contact":"李佳星",
"mobile":"13301212233",
"province":"上海",
"city":"上海",
"town":"浦东新区",
"street":"航头镇航头路",
"isDefault":true
} | -| 接口说明 | 根据addressId查询地址。 | - -```java -@GetMapping("/{addressId}") -public Address findAddressById(@PathVariable("addressId") Long addressId) { - return addressService.getById(addressId); -} -``` - - - -## 6.下单业务 - -``` -MyBatisPlus提供代码生成器生成:实体类、数据层、业务层、表现层 -``` - -### 6.1 保存订单 - -| 请求方式 | POST | -| ---------- | ------------------------------------------------------------ | -| 请求路径 | /order | -| 请求参数 | {
"num": 1, # 代表购买数量
"paymentType": 3, # 代表付款方式
"addressId": 61, # 代表收货人地址id
"itemId": 100000003145 # 代表商品id
} | -| 返回值类型 | Order,订单对象 | -| 接口说明 | 创建订单 | - -创建订单业务比较复杂,流程如下: - -- 1)根据雪花算法生成订单id -- 2)商品微服务提供FeignClient,实现根据id查询商品的接口 -- 3)根据itemId查询商品信息 -- 4)基于商品价格、购买数量计算商品总价:totalFee -- 5)封装Order对象,初识status为未支付 -- 6)将Order写入数据库tb_order表中 -- 7)将商品信息、orderId信息封装为OrderDetail对象,写入tb_order_detail表 -- 8)将user-service的根据id查询地址接口封装为FeignClient -- 9)根据addressId查询user-service服务,获取地址信息 -- 10)将地址封装为OrderLogistics对象,写入tb_order_logistics表 -- 11)在item-service提供减库存接口,并编写FeignClient -- 12)调用item-service的减库存接口 - -#### 1、根据接口定义方法 - -在订单服务中定义接受参数类: - -```java -package com.hmall.order.pojo; - -import lombok.Data; - -@Data -//OrderDTO -public class RequestParams { - private Integer num; //购买数量 - private Long itemId; //商品ID - private Long addressId;//配送地址 - private Integer paymentType; //支付方式 -} -``` - -```java - @PostMapping - public Order createOrder(@RequestBody RequestParams requestParams){ - //return orderService.createOrder(requestParams); - - //返回订单ID - return null; - } -``` - -#### 2、业务层实现 - -创建订单业务比较复杂,流程如下: - -- 1)根据雪花算法生成订单id -- 2)商品微服务提供FeignClient,实现根据id查询商品的接口 -- 3)根据itemId查询商品信息 -- 4)基于商品价格、购买数量计算商品总价:totalFee -- 5)封装Order对象,初识status为未支付 -- 6)将Order写入数据库tb_order表中 -- 7)将商品信息、orderId信息封装为OrderDetail对象,写入tb_order_detail表 -- 8)将user-service的根据id查询地址接口封装为FeignClient -- 9)根据addressId查询user-service服务,获取地址信息 -- 10)将地址封装为OrderLogistics对象,写入tb_order_logistics表 -- 11)在item-service提供减库存接口,并编写FeignClient -- 12)调用item-service的减库存接口 - -要操作三张表,先把数据层创建出来: - -```java -package com.hmall.order.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.hmall.order.pojo.OrderDetail; - -public interface OrderDetailMapper extends BaseMapper { - -} -``` - -```java -package com.hmall.order.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.hmall.order.pojo.OrderLogistics; - -public interface OrderLogisticsMapper extends BaseMapper { - -} -``` - - - -在业务层新增:createOrder方法 - -```java -package com.hmall.order.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.hmall.order.pojo.Order; -import com.hmall.order.pojo.RequestParams; - -public interface IOrderService extends IService { - - Order createOrder(RequestParams requestParams); - -} -``` - - - -##### 2.1 指定雪花算法ID - -> 1、MyBatisPlus提供:@TableId(type = IdType.ASSIGN_ID) -> -> 2、雪花算法生成的ID比较长,返回给前端后精度会丢失,因此转换成字符串再返回 - -```java -@Data -@TableName("tb_order") -public class Order{ - /** - * 订单编号, 自动生成雪花算法ID - */ - @TableId(type = IdType.ASSIGN_ID) - @JsonSerialize(using = ToStringSerializer.class) - private Long id; - - //... -} -``` - -##### 2.2 计算订单价格 - -> 前端传递的只有商品ID,因此需要远程调用商品服务查询商品详情 - -1、在feign-api中添加远程调用接口: - -```java -package com.hmall.common.client; - -import com.hmall.common.dto.Item; -import com.hmall.common.dto.PageDTO; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(value = "itemservice") -public interface ItemClient { - - @GetMapping("/item/list") - PageDTO queryItemByPage(@RequestParam("page") Integer page, - @RequestParam("size") Integer size); - - @GetMapping("/item/{id}") - Item queryItemById(@PathVariable("id") Long id); - -} -``` - -需要扫描到上述接口所在包: - -```java -@MapperScan("com.hmall.order.mapper") -@SpringBootApplication -@EnableFeignClients(basePackages = "com.hmall.common.client") -public class OrderApplication { - - public static void main(String[] args) { - SpringApplication.run(OrderApplication.class, args); - } - -} -``` - -2、完善OrderService中的下单方法 - -```java -package com.hmall.order.service.impl; - -@Service -public class OrderService extends ServiceImpl implements IOrderService { - - @Autowired - private ItemClient itemClient; - @Autowired - private UserClient userClient; - - @Autowired - private OrderDetailMapper orderDetailMapper; - - @Autowired - private OrderLogisticsMapper orderLogisticsMapper; - - @Transactional - public Order createOrder(RequestParams requestParams) { - - // 1.查询商品:远程调用商品服务根据ID查询商品详情 - Item item = itemClient.queryItemById(requestParams.getItemId()); - // 2.基于商品价格 * 购买数量计算商品总价:totalFee - long totalFee = item.getPrice() * requestParams.getNum(); - - return null; - } -} -``` - -##### 2.3 保存订单信息 - -```java -@Transactional -public Order createOrder(RequestParams requestParams) { - - // 1.查询商品:远程调用商品服务根据ID查询商品详情 - Item item = itemClient.queryItemById(requestParams.getItemId()); - // 2.基于商品价格 * 购买数量计算商品总价:totalFee - long totalFee = item.getPrice() * requestParams.getNum(); - - - //=====================新增========================== - Order order = new Order(); - order.setTotalFee(totalFee); //总金额 - order.setPaymentType(requestParams.getPaymentType()); - order.setUserId(UserHolder.getUser()); //获取当前用户ID - order.setStatus(1); //设置状态为:未支付 - // 3.将Order写入数据库tb_order表中 - this.save(order); //保存成功后,框架底层:order.setId(xxx) - //获取订单id:order.getId(); - - // 4.将商品信息、orderId信息封装为OrderDetail对象,写入tb_order_detail表 - OrderDetail detail = new OrderDetail(); - //detail.setName(item.getName()); - //detail.setSpec(item.getSpec()); - //detail.setPrice(item.getPrice()); - //detail.setImage(item.getImage()); - //属性名一致,可以使用对象拷贝 - BeanUtils.copyProperties(item, detail); - //下面几个属性名不一致,必须使用set方法赋值 - detail.setOrderId(order.getId()); - detail.setItemId(item.getId()); - detail.setNum(requestParams.getNum()); - orderDetailMapper.insert(detail); - - return order; -} -``` - - - -##### 2.4 保存订单配送信息 - -> 前端传递的只有地址ID,因此需要远程调用用户服务查询地址详情 - -1、在feign-api模块中创建远程调用接口: - -```java -package com.hmall.common.client; - -import com.hmall.common.dto.Address; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; - -@FeignClient(value = "userservice") -public interface UserClient { - @GetMapping("/address/{id}") - Address findAddressById(@PathVariable("id") Long id); -} -``` - -2、完善OrderService中的下单方法 - -```java -//====================新增================ -// 5.远程调用:根据addressId查询user-service服务,获取地址信息 -Address address = userClient.findAddressById(requestParams.getAddressId()); -// 6.将地址封装为OrderLogistics对象,写入tb_order_logistics表 -OrderLogistics orderLogistics = new OrderLogistics(); -BeanUtils.copyProperties(address, orderLogistics); -//两个属性名称不一致:order.id, order_detai.orderId,必须自己手动设置 -orderLogistics.setOrderId(order.getId()); -orderLogisticsMapper.insert(orderLogistics); -``` - - - -##### 2.5 扣减库存 - -> 商品库存保存在商品表,因此需要远程调用**商品服务进行扣减** - -1、在**商品服务**中添加扣减库存实现 - -在**表现层**ItemController定义接口: - -```java -@PutMapping("/stock/{itemId}/{num}") -public void updateStock(@PathVariable("itemId") Long itemId, - @PathVariable("num") Integer num) { - itemService.deductStock(itemId, num); -} -``` - -**业务层**:IItemService - -```java -//接口:IItemService -void deductStock(Long itemId, Integer num); -``` - -```java -//实现类:ItemService -@Autowired -private ItemMapper itemMapper; - -@Override -public void deductStock(Long itemId, Integer num) { - try { - itemMapper.updateStock(itemId, num); - } catch (Exception e) { - throw new RuntimeException("库存不足!"); - } -} -``` - -**数据层**ItemMapper: - -```java -@Update("update tb_item set stock = stock + #{num} where id = #{itemId}") -void updateStock(@Param("itemId") Long itemId, @Param("num") Integer num); -``` - - - -2、在feign-api的ItemClient中添加远程调用接口: - -```java -//ItemClient -@PutMapping("/item/stock/{itemId}/{num}") -void updateStock(@PathVariable("itemId") Long itemId, @PathVariable("num") Integer num); -``` - - - -3、完善OrderService中的下单方法 - -```java -// 7.远程调用:扣减库存 -try { - //注意:传递负数,扣减库存 - itemClient.updateStock(requestParams.getItemId(), -requestParams.getNum()); -} catch (Exception e) { - throw new RuntimeException("库存不足!"); -} -``` - -#### 3、完善调用&测试 - -```java -@PostMapping -public Order createOrder(@RequestBody RequestParams requestParams){ - return orderService.createOrder(requestParams); -} -``` - -测试下单:http://localhost:9002 - -```asciiarmor -必须启动所有的微服务 -```