黑马商城实战需求文档
黑马商城实战需求文档
1.搭建运行环境
实战资料包括三部分,如图:
1.1.导入SQL文件
找到资料中的SQL目录,其中包含一个sql脚本:
导入其中的SQL文件导入后,包括5张表:
其中:
- tb_address:用户地址表
- tb_user:用户表,其中包含用户的详细信息
- tb_item:商品表
- tb_order:用户订单表
- tb_order_detail:订单详情表,主要是订单中包含的商品信息
- tb_order_logistics:订单物流表,订单的收货人信息
1.2.导入Demo工程
hmall-cloud是微服务项目,导入IDEA后,结构如图:
解读:
- hmall-feign-api:是通用的api模块,一些feign的客户端、实体类、工具类都可以放这里
- hmall-model
- hmall-item-service:商品微服务,负责商品的CRUD
- hmall-order-service:订单微服务,负责订单的CRUD
- hmall-user-service:用户微服务,负责用户的CRUD、用户地址的CRUD
- hmall-search: 搜索微服务,负责搜索相关接口
注意:需要修改每个项目中的application.yml文件,其中的端口信息、数据库信息都需要修改为自己的信息。
1.3.前端页面
其中前端页面分为两部分:
- hm-mall-admin:后台的商品管理页面
- hm-mall-portal:用户入口,搜索、购买商品、下单的页面
前端页面需要通过nginx来部署,达到下面的目的:
server_name | port | 代理目标 |
---|---|---|
localhost | 9001 | hm-mall-admin |
localhost | 9002 | hm-mall-portal |
资料中的shop_nginx中已经配置好相关静态资源,拷贝到非中文目录 启动nginx即可
部署成功后,访问:http://localhost:9001,效果如图:
其中的axios发送请求地址已经配置为10010,也就是网关端口:
访问:http://localhost:9002,效果如图:
其中的axios发送请求地址已经配置为10010,也就是网关端口:
1.4.启动nacos
按照之前学习的方式,启动一台单节点运行的nacos服务。
docker run -d \
-e PREFER_HOST_MODE=hostname \
-e MODE=standalone \
-e JVM_XMS=256m \
-e JVM_XMX=256m \
-e JVM_XMN=128m \
-p 8848:8848 --restart=always \
--name nacos \
nacos/nacos-server:1.4.2
1.5.配置网关
基础代码中并未包含gateway网关,也没有注册到nacos注册中心。
1.5.1.创建网关服务
在hmall-parent中创建微服务:gateway,效果如图:
注意:gateway服务的端口要与前端axios的请求端口一致
1.5.2.配置网关路由
在gateway服务的application.yml文件中,配置各个微服务的路由信息
要求如下:
路径 | 目标微服务 |
---|---|
/item/** | itemservice |
/user/**,/address/** | userservice |
/order/**,/pay/** | orderservice |
/search/** | searchservice |
1.5.3.在网关配置CORS
因为使用了前后端分离,所以前端的ajax请求存在跨域问题,需要在网关配置跨域。
请参考SpringCloudGateway部分课程,配置CORS跨域,允许4个地址跨域:
2.商品管理业务
商品管理,主要业务集中在后台管理页面,如图:
另外,还有一个是购买商品的时候,需要根据id查询商品。
因此业务接口包括:
- 分页查询商品(基于mysql)
- 根据id查询商品
- 新增商品
- 修改商品
- 根据id删除商品(直接删除,不做逻辑删除)
- 上架、下架商品
商品的表结构如下:
CREATE TABLE `tb_item` (
`id` varchar(20) NOT NULL COMMENT '商品id',
`name` varchar(200) NOT NULL COMMENT 'SKU名称',
`price` bigint(20) NOT NULL COMMENT '价格(分)',
`stock` int(10) NOT NULL COMMENT '库存数量',
`image` varchar(200) DEFAULT NULL COMMENT '商品图片',
`category` varchar(200) DEFAULT NULL COMMENT '类目名称',
`brand` varchar(100) DEFAULT NULL COMMENT '品牌名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`sold` int(11) DEFAULT '0' COMMENT '销量',
`comment_count` int(11) DEFAULT '0' COMMENT '评论数',
`isAD` tinyint(1) DEFAULT '0' COMMENT '是否是推广广告,true/false',
`status` int(1) DEFAULT '1' COMMENT '商品状态 1-正常,2-下架,3-删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `status` (`status`),
KEY `updated` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
对应实体类在 model工程中已经定义
2.1.分页查询商品
请求信息可以在前端控制台看到,如图:
从这幅图中可以看到请求的各种信息。另外,返回值是分页结果,已经在model项目中定义了:
package com.hmall.common.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页结果对象
* @param <T>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<T> {
/**
* 总条数
*/
private Long total;
/**
* 当前页数据
*/
private List<T> list;
}
因此,整体的接口信息包括:
请求方式 | POST |
---|---|
请求路径 | /item/list |
请求参数(JSON) | { page:当前页面; size:每页大小, name: 商品名称, dateRange: 创建时间范围, brand: 品牌, category: 分类 } |
返回值类型 | PageDTO<Item> ,这里的Item就是商品 |
2.2.根据id查询商品
接口说明:
请求方式 | GET |
---|---|
请求路径 | /item/{id} |
请求参数 | id:商品的id |
返回值类型 | Item,商品信息 { "id": 100000003145, "name": "vivo X23 8GB+128GB 幻夜蓝 4G手机", "price": 95900, "stock": 10000, "image": "https://m.360buyimg.com/de.jpg", "category": "手机", "brand": "vivo", "spec": "{"颜色": "红色", "版本": "8GB+128GB"}", "sold": 11212, "commentCount": 1231312, "status": 1, "isAD": true } |
2.3.新增商品
新增商品的业务在前端依然可以看到,点击新增商品
按钮:
填写表单并点击确定按钮:
就会看到下面的请求:
接口说明:
请求方式 | POST |
---|---|
请求路径 | /item |
请求参数 | { "name": "Realme 手机", "category": "手机", "brand": "Realme", "price": "249900", "stock": "99", "spec": "{"颜色":"曜石黑", "内存":"6GB"}", "image": "http://www.xxx.com/1.jpg", "isAD": false, } |
返回值类型 | 无 |
接口说明 | 接收前端参数后,将数据保存到数据库中 |
2.4.商品上架、下架
点击删除、修改按钮时,页面会报错:
要修改、删除,必须先下架才可以。
点击页面中的上架、下架按钮:
可以让商品上架、或者下架。在控制台可以看到请求信息:
下架请求:
上架请求:
接口说明:
请求方式 | PUT |
---|---|
请求路径 | /item/status/{id}/{status} |
请求参数 | id:商品id,status:商品状态 1-代表上架 2-代表下架 |
返回值类型 | 无 |
接口说明 | 根据id修改商品状态 |
2.5.修改商品
点击商品后面的编辑按钮,即可编辑商品:
在弹出的表单中填写商品信息:
点击确定按钮后,即可提交信息。在控制台可以看到请求信息:
接口说明:
请求方式 | PUT |
---|---|
请求路径 | /item |
请求参数 | { "name": "Realme 手机", "category": "手机", "brand": "Realme", "price": "249900", "stock": "99", "spec": "{"颜色":"曜石黑", "内存":"6GB"}", "image": "http://www.xxx.com/1.jpg", "isAD": false, } |
返回值类型 | 无 |
接口说明 | 接收前端参数后,修改数据库商品信息 |
2.6.根据id删除商品
点击页面的删除按钮:
弹出的确认框中,点击确定按钮:
即可在控制台看到请求信息:
接口说明:
请求方式 | DELETE |
---|---|
请求路径 | /item/{id} |
请求参数 | 商品id |
返回值类型 | 无 |
接口说明 | 根据id删除商品信息 |
不用做逻辑删除,而是直接删除数据库数据
3.搜索业务
3.1.创建搜索服务
在hm-mall项目中,创建一个module:名为search-service。
如图:
在search-service中添加elasticsearch依赖、elasticsearch配置信息、注册elasticsearch的HighLevelRestClient对象。
搜索相关业务包括:
- 设计索引库数据结构
- 完成数据导入
- 实现搜索栏自动补全功能
- 实现过滤项聚合功能
- 实现基本搜索功能
- 数据同步
3.2.设计索引库数据结构
根据搜索页面的需求,判断索引库需要什么字段:
基本字段包括:
- 用于关键字全文检索的字段,比如All,里面包含name、brand、category信息
- 用于自动补全的字段,包括brand、category信息
- 分类
- 品牌
- 价格
- 销量
- id
- name
- 评价数量
- 图片
完成两件事情:
- 根据每个字段的特征,设计索引库结构 mapping。
- 根据索引库结构,设计文档对应的Java类:ItemDoc
ItemDoc
@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;
private List<String> suggestion = new ArrayList<>(2);
public ItemDoc(Item item) {
// 属性拷贝
BeanUtils.copyProperties(item, this);
// 补全
suggestion.add(item.getBrand());
suggestion.add(item.getCategory());
}
}
索引库映射
PUT /item
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_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": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
3.3.全量数据导入
要把数据库数据导入到elasticsearch中,包括下面几步:
@Test FeignClient 分页查询商品
- 1)将商品微服务中的分页查询商品接口定义为一个FeignClient,放到feign-api模块中
- 2)搜索服务编写一个业务,实现下面功能:
- 调用item-service提供的FeignClient,分页查询商品
PageDTO<Item>
- 将查询到的商品封装为一个
ItemDoc
对象,放入ItemDoc
集合 - 将
ItemDoc
集合批量导入elasticsearch中
- 调用item-service提供的FeignClient,分页查询商品
注意:数据库中的商品数量多达9万多个,不可查询索引导入。一定要分页导入。
# 浏览器中输入这个地址 查看导入数据总量
http://192.168.200.130:9200/_cat/indices/item?v
3.4.搜索栏自动补全功能
访问:http://localhost:9002可以看到搜索页面:
在搜索框输入任意字母,就会发起自动补全的请求:
接口说明:
请求方式 | GET |
---|---|
请求路径 | /search/suggestion |
请求参数 | key: 用户输入的词条前缀 |
返回值类型 | List<String> :自动补全的词条集合 |
接口说明 | 根据用户输入关键字做自动补全 |
3.5.过滤项聚合功能
在搜索页面的顶部,包含很多过滤项:
分类、品牌这些过滤项目前都是写死的。但是应该从索引库中聚合得到。
刷新页面即可看到查询过滤项的请求:
接口说明:
请求方式 | POST |
---|---|
请求路径 | /search/filters |
请求参数 | RequestParams对象, { "key": "游戏手机", "page": 2, "size": 20, "sortBy": "price", "category": "手机", "brand": "小米", "minPrice": 1500, "maxPrice": 999999 } |
返回值类型 | Map<String, List<String>> 过滤项集合。例如: ["分类": ["手机", "电视"], "品牌": ["小米", "华为"]] |
接口说明 | 根据搜索条件搜索文档,并对文档中的分类、品牌做聚合,返回聚合得到的分类、品牌信息 |
3.6.实现基本搜索功能
在输入框输入搜索关键字、点击页面过滤项、翻页、排序都会导致请求发出,查询商品信息:
控制台的请求信息如下:
需要实现的功能包括:
- 关键字搜索
- 分类、品牌、价格过滤
- 排序
- 分页
- 竞价排名
接口说明:
请求方式 | POST |
---|---|
请求路径 | /search/list |
请求参数 | RequestParams对象, { "key": "游戏手机", "page": 2, "size": 20, "sortBy": "price", "category": "手机", "brand": "小米", "minPrice": 1500, "maxPrice": 999999 } |
返回值类型 | PageDTO<ItemDoc> 分页结果。{ "total": 200, "list": [{}, {} , {}]} |
接口说明 | 根据搜索条件搜索文档 |
注意广告的处理
3.7.数据同步
基于RabbitMQ实现数据库、elasticsearch的数据同步,要求如下:
- 商品上架时:search-service新增商品到elasticsearch
- 商品下架时:search-service删除elasticsearch中的商品
4.登录用户信息获取
因为我们没有做登录功能,所以我们会默认用户已经登录。
但是微服务运行中,需要获取一个登录的用户身份,该怎么办呢?
4.1.给所有请求添加用户身份
我们的要求是这样的: default-filters:
- AddRequestHeader = authorization ,2
所有经过网关的请求,都在请求头中添加一个头信息:authorization = 2
后面的2就是用户id:
怎么给所有经过网关的请求都添加一个请求头呢??自己思考一下吧。
4.2.微服务获取用户身份
网关已经给所有请求添加了用户身份,也就是authorization头信息。
那么每一个微服务都需要获取这个请求头,怎么获取?
这里提供一个思路,大家自己实现: GlobalFilter
- 在每个微服务都编写一个SpringMVC的拦截器:HandlerInterceptor
- 在拦截器中获取请求头中的authorization信息,也就是userId,并保存到ThreadLocal中
- 在后续的业务中,可以直接从ThreadLocal中获取userId
/**
* 用于存储请求头中的userId信息
*/
public class ThreadLocalUtil {
private static final ThreadLocal<Long> local = new ThreadLocal<>();
/**
* 从当前线程中存储userId
* @param userId
*/
public static void setUserId(Long userId){
local.set(userId);
}
/**
* 从当前线程中获取userId
* @return
*/
public static Long getUserId(){
return local.get();
}
/**
* 从当前线程中清除userId
*/
public static void clear(){
local.remove();
}
}
获取当前登录用户:
ThreadLocalUtil.getUserId(); // 获取当前登录用户
5.用户相关业务
5.1.数据结构
用户表:
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(32) NOT NULL COMMENT '密码,加密存储',
`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`created` datetime NOT NULL COMMENT '创建时间',
`updated` datetime NOT NULL,
`status` int(1) DEFAULT '1' COMMENT '使用状态(1正常 2冻结)',
`balance` decimal(10,0) DEFAULT NULL COMMENT '账户余额',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='用户表';
用户地址表:
CREATE TABLE `tb_address` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`province` varchar(10) DEFAULT NULL COMMENT '省',
`city` varchar(10) DEFAULT NULL COMMENT '市',
`town` varchar(10) DEFAULT NULL COMMENT '县/区',
`mobile` varchar(255) DEFAULT NULL COMMENT '手机',
`street` varchar(255) DEFAULT NULL COMMENT '详细地址',
`contact` varchar(255) DEFAULT NULL COMMENT '联系人',
`is_default` varchar(1) DEFAULT NULL COMMENT '是否是默认 1默认 0否',
`notes` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;
一个用户,对应多个地址信息。
User实体类:
package com.hmall.user.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("tb_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String phone;
private Integer status;
private Long balance;
private Date createTime;
private Date updateTime;
}
Address实体类:
package com.hmall.user.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("tb_address")
public class Address {
private Long id;
private Long userId;
private String contact;// 收件人姓名
private String mobile;// 电话
private String province;// 省份
private String city;// 城市
private String town;// 区
private String street;// 街道地址
private Boolean isDefault;
}
另外,其它微服务可能来查询地址信息,因此在feign-api中也提供了一个Address,供其它微服务使用:
package com.hmall.common.dto;
import lombok.Data;
@Data
public class Address {
private Long id;
private Long userId;
private String contact;// 收件人姓名
private String mobile;// 电话
private String province;// 省份
private String city;// 城市
private String town;// 区
private String street;// 街道地址
private Boolean isDefault;
}
5.2.根据用户id查询地址列表
点击搜索页面的“立刻购买”按钮,就会进入下单页面:
在下单页面中,会需要用户地址信息:
控制台也会发出请求,查询地址列表:
接口说明:
请求方式 | GET |
---|---|
请求路径 | /address/uid |
请求参数 | 无 |
返回值类型 | List<Adderss> [ { "id":61, "userId":2, "contact":"李佳星", "mobile":"13301212233", "province":"上海", "city":"上海", "town":"浦东新区", "street":"航头镇航头路", "isDefault":true } ] |
接口说明 | 获取当前登录用户id,根据用户id查询地址列表 |
5.3.根据addressId查询Address
在下单的时候,需要根据addressId查询地址。所以要暴露这个接口:
接口说明:
请求方式 | GET |
---|---|
请求路径 | /address/{addressId} |
请求参数 | addressId:地址id |
返回值类型 | Adderss对象: { "id":61, "userId":2, "contact":"李佳星", "mobile":"13301212233", "province":"上海", "city":"上海", "town":"浦东新区", "street":"航头镇航头路", "isDefault":true } |
接口说明 | 根据addressId查询地址。 |
6.下单业务
6.1.数据结构
订单表:
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL COMMENT '订单id',
`total_fee` bigint(20) NOT NULL COMMENT '总金额,单位为分',
`payment_type` tinyint(1) unsigned zerofill NOT NULL COMMENT '支付类型,1、支付宝,2、微信,3、扣减余额',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`status` tinyint(1) DEFAULT NULL COMMENT '订单的状态,1、未付款 2、已付款,未发货 3、已发货,未确认 4、确认收货,交易成功 5、交易取消,订单关闭 6、交易结束,已评价',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
`consign_time` timestamp NULL DEFAULT NULL COMMENT '发货时间',
`end_time` timestamp NULL DEFAULT NULL COMMENT '交易完成时间',
`close_time` timestamp NULL DEFAULT NULL COMMENT '交易关闭时间',
`comment_time` timestamp NULL DEFAULT NULL COMMENT '评价时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `multi_key_status_time` (`status`,`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
订单详情表,就是订单中关联的商品信息:
CREATE TABLE `tb_order_detail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单详情id ',
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`item_id` bigint(20) NOT NULL COMMENT 'sku商品id',
`num` int(4) NOT NULL COMMENT '购买数量',
`name` varchar(256) NOT NULL COMMENT '商品标题',
`spec` varchar(1024) DEFAULT '' COMMENT '商品动态属性键值集',
`price` int(16) NOT NULL COMMENT '价格,单位:分',
`image` varchar(256) DEFAULT '' COMMENT '商品图片',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `key_order_id` (`order_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='订单详情表';
订单物流,就是订单的收货人信息:
CREATE TABLE `tb_order_logistics` (
`order_id` bigint(20) NOT NULL COMMENT '订单id,与订单表一对一',
`logistics_number` varchar(18) DEFAULT '' COMMENT '物流单号',
`logistics_company` varchar(18) DEFAULT '' COMMENT '物流公司名称',
`contact` varchar(32) NOT NULL COMMENT '收件人',
`mobile` varchar(11) NOT NULL COMMENT '收件人手机号码',
`province` varchar(16) NOT NULL COMMENT '省',
`city` varchar(32) NOT NULL COMMENT '市',
`town` varchar(32) NOT NULL COMMENT '区',
`street` varchar(256) NOT NULL COMMENT '街道',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
对应的实体类:
Order:
@Data
@TableName("tb_order")
public class Order{
/**
* 订单编号, 自动生成雪花算法ID
*/
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 商品金额
*/
private Long totalFee;
/**
* 付款方式:1:微信支付, 2:支付宝支付, 3:扣减余额
*/
private Integer paymentType;
/**
* 用户id
*/
private Long userId;
/**
* 订单状态,1、未付款 2、已付款,未发货 3、已发货,未确认 4、确认收货,交易成功 5、交易取消,订单关闭 6、交易结束
*/
private Integer status;
/**
* 创建订单时间
*/
private Date createTime;
/**
* 付款时间
*/
private Date payTime;
/**
* 发货时间
*/
private Date consignTime;
/**
* 确认收货时间
*/
private Date endTime;
/**
* 交易关闭时间
*/
private Date closeTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 更新时间
*/
private Date updateTime;
}
订单详情:
@Data
@TableName("tb_order_detail")
public class OrderDetail {
/**
* 订单编号
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 订单编号
*/
private Long orderId;
/**
* 商品id
*/
private Long itemId;
/**
* 商品购买数量
*/
private Integer num;
/**
* 商品标题
*/
private String name;
/**
* 商品单价
*/
private Long price;
/**
* 商品规格数据
*/
private String spec;
/**
* 图片
*/
private String image;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
订单物流:
@Data
@TableName("tb_order_logistics")
public class OrderLogistics{
/**
* 订单id,与订单表一对一
*/
@TableId(type = IdType.INPUT)
private Long orderId;
/**
* 物流单号
*/
private String logisticsNumber;
/**
* 物流名称
*/
private String logisticsCompany;
/**
* 收件人
*/
private String contact;
/**
* 手机号
*/
private String mobile;
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 区
*/
private String town;
/**
* 街道
*/
private String street;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
6.2.提交订单
在下单页面,可以选择三种支付方式:
最简单的模式,就是选择扣减余额。我们先选这种。
然后点击提交订单按钮:
即可在页面看到请求信息:
接口说明:
请求方式 | POST |
---|---|
请求路径 | /order |
请求参数 | { "num": 1, # 代表购买数量 "paymentType": 3, # 代表付款方式 "addressId": 61, # 代表收货人地址id "itemId": 100000003145 # 代表商品id } |
返回值类型 | Long,订单id |
接口说明 | 创建订单 |
创建订单业务比较复杂,流程如下:
- 1)根据雪花算法生成订单id**(mybatisplus自带)**
- 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的减库存接口
6.2.1.扣减库存接口
需要在item-service中声明扣库存接口,接口规范可以自己约定,例如:
请求方式 | PUT |
---|---|
请求路径 | /item/stock/{itemId}/{num} |
请求参数 | Long itemId; 商品id Integer num; 商品数量 |
返回值类型 | 无 |
接口说明 | 扣减库存 |
6.2.2. 简单模拟支付
一旦下单成功,就会进入支付页面,如图所示:
支付功能在项目三才会详细讲解,暂时模拟支付效果
用于输入支付密码 点击确认支付 会调用后台接口
请求方式 | GET |
---|---|
请求路径 | /order/{orderId} |
请求参数 | Long orderId; 订单id |
返回值类型 | Order |
接口说明 | 查询订单 |
请求方式 | PUT |
---|---|
请求路径 | /pay/{orderId} |
请求参数 | 路径参数 : Long orderId; 订单id json参数: {password : 输入的密码} |
返回值类型 | ResultDTO |
接口说明 | 支付订单 |
支付订单业务流程:
根据订单id查询订单信息
检查订单状态是否为1 如果为1继续 如果为其它状态结束请求
获取当前登录用户id, 使用feign查询用户信息
判断输入的密码是否正确: 密码使用md5加密
如果密码正确, 使用feign扣减用户账户余额 并更改订单状态为已支付
public static void main(String[] args) {
// 密码原文 123123
// md5加密后 4297f44b13955235245b2497399d7a93
String passwordMd5 = DigestUtils.md5DigestAsHex("123123".getBytes());
System.out.println(passwordMd5);
}
6.3.清理超时未支付订单
如果用户迟迟未支付订单, 超时30分钟后,订单就会自动取消。(练习时 2分钟未支付即可设置取消状态)
不过这里是页面写的假的计时功能,我们需要在服务端实现超时取消订单,怎么做?
方案1: 使用xxl-job 实现定时器功能
- 每隔1分钟查询一次订单表
- 查询订单状态为 1 (未支付) 且订单创建时间大于等于30分钟的订单
- 修改订单状态为5 (取消订单)
- 调用item-service,根据商品id、商品数量恢复库存
xxljob容器部署
准备数据库:
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;
SET NAMES utf8mb4;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
`address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表,多地址逗号分隔',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `order`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 1, 0, NULL);
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;
启动容器:
注意修改数据库的配置信息
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.200.130:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root" -p 8888:8080 -v /tmp:/data/applogs --name xxljob --privileged=true -id xuxueli/xxl-job-admin:2.2.0
默认登录账号 “admin / 123456”, 登录后运行界面如下图所示。
方案2: 使用RabbitMQ的延时队列功能,使用延时消息
- 创建订单后,立即发送一条延迟消息(携带订单id),延迟时间(TTL)为30分钟
- 编写监听者,监听延迟队列
- 当监听者收到消息后,一定是下单30分钟后。根据订单id查询订单信息,判断status是否已经支付:
- 如果未支付:肯定是超时未支付订单,将其status修改为5,取消订单,恢复扣减的库存
- 如果是已支付,则丢弃消息不管
注意,这里监听到超时信息后,业务的流程包括:
- 根据orderId查询订单
- 判断订单status是否为1
- 不为1则丢弃
- 为1则继续
- 根据orderId查询订单详情,得到商品购买数量
- 根据orderId修改订单status为5(取消),注意幂等判断,避免重复消息
- 调用item-service,根据商品id、商品数量恢复库存