Sell Project

前言:

微信点餐项目实践——慕课网

项目介绍

前端是由Vue.js构建的WebApp,后端由Spring Boot打造,后端的前台页面使用Bootstrap+Freemarker+JQuery构建,后端和前端通过RESTful风格的接口相连。

image-20200123151733508

数据库方面使用Spring Boot+JPA,兼顾Spring Boot+Mybatis;缓存方面,使用Spring Boot+Redis;基于Redis,应对分布式Session和锁;消息推送方面,使用WebSocket。

image-20200123151751523

项目设计

角色划分

  • 买家(手机端):由微信公众号提供的一个服务。
  • 卖家(PC端):一个简单的商家管理系统

功能模块划分

  • 功能分析

image-20191226132632562

  • 关系图

image-20191226132747143

部署

买家端在手机端,卖家端在PC端,两端都会发出数据请求,请求首先到达nginx服务器,如果请求的是后端接口,nginx服务器会进行一个转发,转发到后面的Tomcat服务器,即我们的Java项目所在,如果这个接口作了缓存,那么就会访问redis服务器,如果没有缓存,就会访问我们的MySQL数据库。值得注意的是我们的应用是支持分布式部署的,也就是说图上的Tomcat表示的是多台服务器,多个应用。

image-20191226132922967

数据库

共5个表,表之间的关系如下,其中商品表存放的就是商品的名称、价格、库存、图片链接等信息;类目表含有类目id、类目名字等信息,一个类目下有多种商品,类目表和商品表之间是一对多的关系;订单详情表含有购买的商品名称、数量、所属订单的订单号等信息;订单主表包含包含该订单的订单号、买家的信息、订单的支付状态等信息,订单主表和订单详情表之间是一对多的关系;最后是卖家信息表,存放的卖家的账号和密码等信息,作为卖家后台管理的权限认证。

image-20191226134017529

项目使用的主要技术栈

  • Spring Boot的相关特性
    • Spring Boot+JPA
    • Spring Boot+Redis
    • Spring Boot+WebSocket
  • 微信相关特征
    • 微信支付、退款
    • 微信授权登陆
    • 微信模板消息推送
    • 使用微信相关的开源SDK
  • 利用Redis应用分布式Session和锁
    • 对用户的登陆信息使用分布式Session存储
    • 利用一个抢购商品的例子,来对Redis分布式锁进行详细的说明

开发环境及工具

  • IDEA
  • Maven
  • Git
  • MySQL
  • Nginx
  • Redis
  • Postman模拟微信订单创建订单
  • Fiddler对手机请求抓包
  • Natapp内网穿透
  • Apache ab模拟高并发,抢购一个商品

项目开发

买家端商品类目

买家端类目模块的开发,按照dao->service->api的顺序开发。

create类目表

1
2
3
4
5
6
7
8
9
-- 类目
create table `product_category` (
`category_id` int not null auto_increment,
`category_name` varchar(64) not null comment '类目名字',
`category_type` int not null comment '类目编号',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key (`category_id`)
);

ProductCategory实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity			// 将数据库表单映射成对象加上注解
@Data // 使用插件lombok自己生成get、set、toString等方法
@DynamicUpdate // 动态更新的意思,属性中的两个**时间字段**会随着自动更新
public class ProductCategory {
/** 类目id. */
@Id // 类属性主键上
@GeneratedValue
private Integer categoryId;
/** 类目名字. */
private String categoryName;
/** 类目编号. */
private Integer categoryType;
private Date createTime;
private Date updateTime;
public ProductCategory() {
}
public ProductCategory(String categoryName, Integer categoryType) {
this.categoryName = categoryName;
this.categoryType = categoryType;
}
}

ProductCategoryDAO

1
2
3
4
// 继承SpringBootJPA,泛型为实体对象和其对应主键类型
public interface ProductCategoryRepository extends JpaRepository<ProductCategory, Integer> {
List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
}
dao单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductCategoryRepositoryTest {
@Autowired
private ProductCategoryRepository repository;
@Test
public void findOneTest() {
ProductCategory productCategory = repository.findOne(1);
System.out.println(productCategory.toString());
}
@Test
@Transactional
public void saveTest() {
ProductCategory productCategory = new ProductCategory("男生最爱", 4);
ProductCategory result = repository.save(productCategory);
Assert.assertNotNull(result);
// Assert.assertNotEquals(null, result);
}
@Test
public void findByCategoryTypeInTest() {
List<Integer> list = Arrays.asList(2,3,4);
List<ProductCategory> result = repository.findByCategoryTypeIn(list);
Assert.assertNotEquals(0, result.size());
}
@Test
public void updateTest() {
// ProductCategory productCategory = repository.findOne(4);
// productCategory.setCategoryName("男生最爱1");
ProductCategory productCategory = new ProductCategory("男生最爱", 4);
ProductCategory result = repository.save(productCategory);
Assert.assertEquals(productCategory, result);
}
}

CategoryService

接口:

1
2
3
4
5
6
7
8
9
10
public interface  CategoryService {
// 通过类别id查询(系统方向)
ProductCategory findOne(Integer categoryId);
// 查询所有
List<ProductCategory> findAll();
// 通过类别查询(客户方向)
List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
// 新增/更新操作
ProductCategory save(ProductCategory productCategory);
}

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private ProductCategoryDao productCategoryDao;
@Override
public ProductCategory findOne(Integer categoryId) {
return productCategoryDao.findOne(categoryId);
}
@Override
public List<ProductCategory> findAll() {
return productCategoryDao.findAll();
}
@Override
public List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList) {
return productCategoryDao.findByCategoryTypeIn(categoryTypeList);
}
@Override
public ProductCategory save(ProductCategory productCategory) {
return productCategoryDao.save(productCategory);
}
}
service单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductCategoryRepositoryTest {

@Autowired
private ProductCategoryRepository repository;

@Test
public void findOneTest() {
ProductCategory productCategory = repository.findOne(1);
System.out.println(productCategory.toString());
}

@Test
@Transactional
public void saveTest() {
ProductCategory productCategory = new ProductCategory("男生最爱", 4);
ProductCategory result = repository.save(productCategory);
Assert.assertNotNull(result);
// Assert.assertNotEquals(null, result);
}

@Test
public void findByCategoryTypeInTest() {
List<Integer> list = Arrays.asList(2,3,4);

List<ProductCategory> result = repository.findByCategoryTypeIn(list);
Assert.assertNotEquals(0, result.size());
}

@Test
public void updateTest() {
// ProductCategory productCategory = repository.findOne(4);
// productCategory.setCategoryName("男生最爱1");
ProductCategory productCategory = new ProductCategory("男生最爱", 4);
ProductCategory result = repository.save(productCategory);
Assert.assertEquals(productCategory, result);
}
}

image-20200516223402309

买家端商品信息

create商品表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 商品
create table `product_info` (
`product_id` varchar(32) not null,
`product_name` varchar(64) not null comment '商品名称',
`product_price` decimal(8,2) not null comment '单价',
`product_stock` int not null comment '库存',
`product_description` varchar(64) comment '描述',
`product_icon` varchar(512) comment '小图',
`product_status` tinyint(3) DEFAULT '0' COMMENT '商品状态,0正常1下架',
`category_type` int not null comment '类目编号',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key (`product_id`)
);

ProductInfo实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity			// 将数据库表单映射成对象加上注解
@Data // 使用插件lombok自己生成get、set、toString等方法
@DynamicUpdate // 动态更新的意思,属性中的两个**时间字段**会随着自动更新
public class ProductInfo {
@Id // 类属性主键上
private String productId;
/** 名字. */
private String productName;
/** 单价. */
private BigDecimal productPrice;
/** 库存. */
private Integer productStock;
/** 描述. */
private String productDescription;
/** 小图. */
private String productIcon;
/** 状态, 0正常1下架. */
private Integer productStatus = ProductStatusEnum.UP.getCode();
/** 类目编号. */
private Integer categoryType;
private Date createTime;
private Date updateTime;
@JsonIgnore
public ProductStatusEnum getProductStatusEnum() {
return EnumUtil.getByCode(productStatus, ProductStatusEnum.class);
}
}

ProductInfoDAO

1
2
3
4
public interface ProductInfoDao extends JpaRepository<ProductInfo, String> {
// 根据状态查询商品
List<ProductInfo> findByProductStatus(Integer productStatus);
}
dao单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductInfoRepositoryTest {

@Autowired
private ProductInfoRepository repository;

@Test
public void saveTest() {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductId("123456");
productInfo.setProductName("皮蛋粥");
productInfo.setProductPrice(new BigDecimal(3.2));
productInfo.setProductStock(100);
productInfo.setProductDescription("很好喝的粥");
productInfo.setProductIcon("http://xxxxx.jpg");
productInfo.setProductStatus(0);
productInfo.setCategoryType(2);

ProductInfo result = repository.save(productInfo);
Assert.assertNotNull(result);
}

@Test
public void findByProductStatus() throws Exception {

List<ProductInfo> productInfoList = repository.findByProductStatus(0);
Assert.assertNotEquals(0, productInfoList.size());
}
}

ProductService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface ProductService {

ProductInfo findOne(String productId);

// 查询在售商品(用户端)
List<ProductInfo> findUpAll();

// 查询所有商品(商家管理后台展示)
// 注意这里会有分页
// 使用的是pageable分页,返回的是一个page对象
Page<ProductInfo> findAll(Pageable pageable);

ProductInfo save(ProductInfo productInfo);

// 库存操作,参数是前端传入参数DTO
void increaseStock(List<CartDTO> cartDTOList);
void decreaseStock(List<CartDTO> cartDTOList);

// 上下架方法
ProductInfo onSale(String productId);
ProductInfo offSale(String productId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@Service
public class ProductServiceImpl implements ProductService {

@Autowired
private ProductInfoDao productInfoDao;

@Override
public ProductInfo findOne(String productId) {
return productInfoDao.findOne(productId);
}

@Override
public List<ProductInfo> findUpAll() {
// 这里的就是枚举类使用
return productInfoDao.findByProductStatus(ProductStatusEnum.UP.getCode());
}

@Override
public Page<ProductInfo> findAll(Pageable pageable) {
return productInfoDao.findAll(pageable);
}

@Override
public ProductInfo save(ProductInfo productInfo) {
return productInfoDao.save(productInfo);
}

@Override
@Transactional
public void decreaseStock(List<CartDTO> cartDTOList) {
// 遍历购物车
for (CartDTO cartDTO : cartDTOList) {
// 定位购物车中商品
ProductInfo productInfo = productInfoDao.findOne(cartDTO.getProductId());

if (productInfo == null) {
throw new SellException(ResultEnum.PRODUCT_NO_EXIST);
}

Integer result = productInfo.getProductStock() - cartDTO.getProductQuantity();

if (result < 0) {
throw new SellException(ResultEnum.PRODUCT_STOCK_ERROR);
}

productInfo.setProductStock(result);
productInfoDao.save(productInfo);
}
}

@Override
public void increaseStock(List<CartDTO> cartDTOList) {
// 遍历购物车列表
for (CartDTO cartDTO : cartDTOList) {
ProductInfo productInfo = productInfoDao.findOne(cartDTO.getProductId());

if (productInfo == null) {
throw new SellException(ResultEnum.PRODUCT_NO_EXIST);
}

Integer result = productInfo.getProductStock() + cartDTO.getProductQuantity();

productInfo.setProductStock(result);
productInfoDao.save(productInfo);
}

}

@Override
public ProductInfo onSale(String productId) {
ProductInfo productInfo = productInfoDao.findOne(productId);
if (productInfo == null) {
throw new SellException(ResultEnum.PRODUCT_NO_EXIST);
}
if (productInfo.getProductStatusEnum() == ProductStatusEnum.UP) {
throw new SellException(ResultEnum.PRODUCT_STATUS_ERROR);
}

//更新
productInfo.setProductStatus(ProductStatusEnum.UP.getCode());
return productInfoDao.save(productInfo);
}

@Override
public ProductInfo offSale(String productId) {
ProductInfo productInfo = productInfoDao.findOne(productId);
if (productInfo == null) {
throw new SellException(ResultEnum.PRODUCT_NO_EXIST);
}
if (productInfo.getProductStatusEnum() == ProductStatusEnum.DOWN) {
throw new SellException(ResultEnum.PRODUCT_STATUS_ERROR);
}

//更新
productInfo.setProductStatus(ProductStatusEnum.DOWN.getCode());
return productInfoDao.save(productInfo);
}
}

上面的代码会有一些目前还未涉及到的,这里先放上。同时这里涉及到编程中的一些类的包装和优化。

枚举类包装商品状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter		// 引入lombok.Getter自动生成get()方法
public enum ProductStatusEnum implements CodeEnum{
UP(0,"在售"),
DOWN(1,"下架")
;

private Integer code;
private String msg;

ProductStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
service单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductServiceImplTest {

@Autowired
private ProductServiceImpl productService;

@Test
public void findOne() throws Exception {
ProductInfo productInfo = productService.findOne("123456");
Assert.assertEquals("123456", productInfo.getProductId());
}

@Test
public void findUpAll() throws Exception {
List<ProductInfo> productInfoList = productService.findUpAll();
Assert.assertNotEquals(0, productInfoList.size());
}

@Test
public void findAll() throws Exception {
PageRequest request = new PageRequest(0, 2);
Page<ProductInfo> productInfoPage = productService.findAll(request);
// System.out.println(productInfoPage.getTotalElements());
Assert.assertNotEquals(0, productInfoPage.getTotalElements());
}

@Test
public void save() throws Exception {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductId("123457");
productInfo.setProductName("皮皮虾");
productInfo.setProductPrice(new BigDecimal(3.2));
productInfo.setProductStock(100);
productInfo.setProductDescription("很好吃的虾");
productInfo.setProductIcon("http://xxxxx.jpg");
productInfo.setProductStatus(ProductStatusEnum.DOWN.getCode());
productInfo.setCategoryType(2);

ProductInfo result = productService.save(productInfo);
Assert.assertNotNull(result);
}

@Test
public void onSale() {
ProductInfo result = productService.onSale("123456");
Assert.assertEquals(ProductStatusEnum.UP, result.getProductStatusEnum());
}

@Test
public void offSale() {
ProductInfo result = productService.offSale("123456");
Assert.assertEquals(ProductStatusEnum.DOWN, result.getProductStatusEnum());
}
}

BuyerProductController

商品列表api
1
GET /sell/buyer/product/list

参数

1

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 这里需要分析文档
{
// 在外层字段1
"code": 0,
// 在外层字段2
"msg": "成功",
// 在外层字段3
"data": [ // data字段里层是个list
// list的一个元素,或者说是一个对象,但不是一个商品对象,又是一层包装类
{
// 这是类目名称
"name": "热榜",
// 这是类目号
"type": 1,
// 这是才是真正商品信息,同样这个字段也是一个list
"foods": [
// 这是里面的一个元素
{
// 下面的各个字段是商品信息字段
"id": "123456",
"name": "皮蛋粥",
"price": 1.2,
"description": "好吃的皮蛋粥",
"icon": "http://xxx.com",
}
]
},
// list的第二个元素
{
"name": "好吃的",
"type": 2,
"foods": [
{
"id": "123457",
"name": "慕斯蛋糕",
"price": 10.9,
"description": "美味爽口",
"icon": "http://xxx.com",
}
]
}
]
}

通过对文档的分析,发现首先需要对数据进行封装

包装返回到前端的视图对象VO

ViewObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 最外层
@Data
public class ResultVo<T> implements Serializable {
private static final long serialVersionUID = 8445738735149690659L;

// 错误码
private Integer code;
// 错误提示
private String msg;
// 返回实体
private T data;
}

// 次外层(商品信息包含类目)
@Data
public class ResultDataVo implements Serializable {
private static final long serialVersionUID = -8482248794818454115L;

// 商品类别名,为防止混淆以及和API对应,加上注解
// 因为返回前端是name
@JsonProperty("name")
private String categoryName;
@JsonProperty("type")
private Integer categoryType;
@JsonProperty("food")
private List<ResultDataDetailVo> resultDataDetailVolist;
}

// 最里层(商品的部分信息)
@Data
public class ResultDataDetailVo implements Serializable {
private static final long serialVersionUID = 7670881173673410125L;

@JsonProperty("id")
private String productId;
@JsonProperty("name")
private String productName;
@JsonProperty("price")
private BigDecimal productPrice;
@JsonProperty("description")
private String productDescription;
@JsonProperty("icon")
private String productIcon;
}
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RestController						// 表示返回json格式
@RequestMapping("/buyer/product") // url设置,这里注意是由文档来的,而且可以看到和类名很相似
public class BuyerProductController {

@Autowired
private ProductService productService;

@Autowired
private CategoryService categoryService;

@GetMapping("/list")
public ResultVo getList(@RequestParam("sellerId") String sellerId){

// 1.查询所有上架的商品
List<ProductInfo> productInfoList = productService.findUpAll();

// 2.查询类目()
List<Integer> categoryTypeList = new ArrayList<>();
// 传统方法
for (ProductInfo productInfo : productInfoList) {
categoryTypeList.add(productInfo.getCategoryType());
}
// lambda方法
List<Integer> categoryTypeList = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());

List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypeList);

// 3.数据拼装

List<ResultDataVo> ResultDataVoList = new ArrayList<>();
for (ProductCategory productCategory: productCategoryList) {
ResultDataVo ResultDataVo = new ResultDataVo();
ResultDataVo.setCategoryType(productCategory.getCategoryType());
ResultDataVo.setCategoryName(productCategory.getCategoryName());

List<ResultDataDetailVo> ResultDataDetailVoList = new ArrayList<>();
for (ProductInfo productInfo: productInfoList) {
if (productInfo.getCategoryType().equals(productCategory.getCategoryType())) {
ResultDataDetailVo ResultDataDetailVo = new ResultDataDetailVo();
// 对象拷贝工具
BeanUtils.copyProperties(productInfo, ResultDataDetailVo);
ResultDataDetailVoList.add(ResultDataDetailVo);
}
}
ResultDataVo.setResultDataDetailVolist(ResultDataDetailVoList);
ResultDataVoList.add(ResultDataVo);
}

return ResultVoUtil.success(ResultDataVoList);
}
}
包装返回前端提示工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ResultVoUtil {
// 成功时返回
public static ResultVo success(Object object) {
ResultVo ResultVo = new ResultVo();
ResultVo.setData(object);
ResultVo.setCode(0);
ResultVo.setMsg("成功");
return ResultVo;
}
// 无数据时
public static ResultVo success() {
return success(null);
}
// 出错时
public static ResultVo error(Integer code, String msg) {
ResultVo ResultVo = new ResultVo();
ResultVo.setCode(code);
ResultVo.setMsg(msg);
return ResultVo;
}
}

买家端订单信息

create订单表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 订单主表
create table `order_master` (
`order_id` varchar(32) not null,
`buyer_name` varchar(32) not null comment '买家名字',
`buyer_phone` varchar(32) not null comment '买家电话',
`buyer_address` varchar(128) not null comment '买家地址',
`buyer_openid` varchar(64) not null comment '买家微信openid',
`order_amount` decimal(8,2) not null comment '订单总金额',
`order_status` tinyint(3) not null default '0' comment '订单状态, 默认为新下单',
`pay_status` tinyint(3) not null default '0' comment '支付状态, 默认未支付',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key (`order_id`),
key `idx_buyer_openid` (`buyer_openid`)
);

-- 订单详情
create table `order_detail` (
`detail_id` varchar(32) not null,
`order_id` varchar(32) not null,
`product_id` varchar(32) not null,
`product_name` varchar(64) not null comment '商品名称',
`product_price` decimal(8,2) not null comment '当前价格,单位分',
`product_quantity` int not null comment '数量',
`product_icon` varchar(512) comment '小图',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key (`detail_id`),
key `idx_order_id` (`order_id`)
);

OrderMaster实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entity
@Data
@DynamicUpdate
public class OrderMaster {
// 订单id
@Id
private String orderId;
// 买家用户名
private String buyerName;
// 买家地址
private String buyerAddress;
// 买家手机号
private String buyerPhone;
// 买家微信openid
private String buyerOpenid;
// 订单总金额
private BigDecimal orderAmount;
// 订单状态,默认为新下单,0
private Integer orderStatus = OrderStatusEnum.NEW.getCode();
// 订单支付状态,默认未支付,0
private Integer payStatus = PayStatusEnum.WAIT.getCode();
// 订单创建时间
private Date createTime;
// 订单更新时间
private Date updateTime;
}

@Entity
@Data
public class OrderDetail {
@Id
private String detailId;
// 订单id
private String orderId;
// 商品id
private String productId;
// 商品名称
private String productName;
// 商品单价
private BigDecimal productPrice;
// 商品数量
private Integer productQuantity;
// 商品小图
private String productIcon;
}
包装订单状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
public enum OrderStatusEnum implements CodeEnum {
NEW(0,"新订单"),
FINISHED(1,"已完成"),
CANCEL(2,"已取消");

private Integer code;

private String msg;

OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

// 需要一个根据传入code值返回订单状态的方法:getOrderStatusEnum()
// 问题在于代码的冗余,这样写每个枚举类都要加上
// 解决在于,抽象一下,让每个类实现一个接口,接口的方法为获取状态
}

OrderMasterDAO

1
2
3
4
5
6
7
8
9
10
11
public interface OrderMasterDao extends JpaRepository<OrderMaster, String> {

// 根据买家微信id查询主订单,分页显示
Page<OrderMaster> findByBuyerOpenid(String buyerOpenid, Pageable pageable);
}

public interface OrderDetailDao extends JpaRepository<OrderDetail, String> {

// 根据orderId查询订单详情
List<OrderDetail> findByOrderId(String orderId);
}
dao单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderMasterRepositoryTest {

@Autowired
private OrderMasterRepository repository;

private final String OPENID = "110110";

@Test
public void saveTest() {
OrderMaster orderMaster = new OrderMaster();
orderMaster.setOrderId("1234567");
orderMaster.setBuyerName("师兄");
orderMaster.setBuyerPhone("123456789123");
orderMaster.setBuyerAddress("幕课网");
orderMaster.setBuyerOpenid(OPENID);
orderMaster.setOrderAmount(new BigDecimal(2.5));

OrderMaster result = repository.save(orderMaster);
Assert.assertNotNull(result);
}

@Test
public void findByBuyerOpenid() throws Exception {
PageRequest request = new PageRequest(1, 3);
Page<OrderMaster> result = repository.findByBuyerOpenid(OPENID, request);
Assert.assertNotEquals(0, result.getTotalElements());
}
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderDetailRepositoryTest {

@Autowired
private OrderDetailRepository repository;

@Test
public void saveTest() {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setDetailId("1234567810");
orderDetail.setOrderId("11111112");
orderDetail.setProductIcon("http://xxxx.jpg");
orderDetail.setProductId("11111112");
orderDetail.setProductName("皮蛋粥");
orderDetail.setProductPrice(new BigDecimal(2.2));
orderDetail.setProductQuantity(3);

OrderDetail result = repository.save(orderDetail);
Assert.assertNotNull(result);
}

@Test
public void findByOrderId() throws Exception {
List<OrderDetail> orderDetailList = repository.findByOrderId("11111111");
Assert.assertNotEquals(0, orderDetailList.size());
}
}

OrderService

包装数据传输对象(DTO)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Data
// 下面这句注释的作用是当返回给前端的属性值为null时就不返回该属性(字段)
// @JsonInclude(JsonInclude.Include.NON_NULL)
// 当有很多对象都与前端相关,需要一个一个设置时可以使用全局配置,在application.yml文件中添加
public class OrderDTO {
// 订单id
private String orderId;
// 买家用户名
private String buyerName;
// 买家地址
private String buyerAddress;
// 买家手机号
private String buyerPhone;
// 买家微信openid
private String buyerOpenid;
// 订单总金额
private BigDecimal orderAmount;
// 订单状态,默认为新下单,0
private Integer orderStatus;
// 订单支付状态,默认未支付,0
private Integer payStatus;
// 订单创建时间
// 这里的注解是为了将Date时间改为Long类型,使用的是我们自己写的Date2LongSerializer中重写的方法
@JsonSerialize(using = Date2LongSerializer.class)
private Date createTime;
// 订单更新时间
@JsonSerialize(using = Date2LongSerializer.class)
private Date updateTime;

// 和订单详情关联
List<OrderDetail> orderDetailList;

// 增加通过code值获取订单状态枚举值的方法
// 这个注解的作用是对象转为Json时该属性/方法忽略
@JsonIgnore
public OrderStatusEnum getOrderStatusEnum() {
return EnumUtil.getByCode(orderStatus, OrderStatusEnum.class);
}
@JsonIgnore
public PayStatusEnum getPayStatusEnum() {
return EnumUtil.getByCode(payStatus, PayStatusEnum.class);
}
}
定义异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SellException extends RuntimeException{

private Integer code;

public SellException(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.code = resultEnum.getCode();
}

public SellException(Integer code, String message) {
super(message);
this.code = code;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 异常信息结果枚举类
@Getter
public enum ResultEnum {
PARAM_ERROR(1,"表单参数不正确"),
PRODUCT_NO_EXIST(10, "商品不存在"),
PRODUCT_STOCK_ERROR(11, "商品库存不足"),
ORDER_NO_EXIST(12, "订单不存在"),
ORDERDETAIL_NO_EXIST(13, "订单详情不存在"),
ORDER_STATUS_ERROR(14,"订单状态不正确"),
ORDER_UPDATE_ERROR(15,"订单更改失败"),
ORDER_EMPTY_ERROR(16,"订单详情为空"),
ORDER_PAY_STATUS_ERROR(17,"订单支付状态不正确"),
CART_EMPTY(18,"购物车为空"),
ORDER_OWNER_ERROR(19,"该订单不属于当前用户"),
WECHAT_MP_ERROR(20,"微信公众账号异常"),
WXPAY_NOTIFY_MONEY_VERIFY_ERROR(21, "微信支付异步通知金额校验不通过"),
ORDER_CANCEL_SUCCESS(22, "订单取消成功"),
ORDER_FINISH_SUCCESS(23, "订单完结成功"),
PRODUCT_STATUS_ERROR(24, "商品状态不正确"),
LOGIN_FAIL(25, "登录失败, 登录信息不正确"),
LOGOUT_SUCCESS(26, "登出成功"),
;

private Integer code;
private String msg;

ResultEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
随机数生成主键
1
2
3
4
5
6
7
8
9
10
11
12
public class GenKeyUtil {
/* @description 生成唯一主键方法(时间+随机数),避免多线程出现问题,加上synchronized关键字
* @param
* @return java.lang.String
* @info zw 2020/1/6 16:23
*/
public static synchronized String genUniqueKey(){
Random random = new Random();
Integer number = random.nextInt(900000) + 100000;
return System.currentTimeMillis() + String.valueOf(number);
}
}
购物车DTO
1
2
3
4
5
6
7
8
9
10
@Getter
public class CartDTO {
private String productId;
private Integer productQuantity;

public CartDTO(String productId, Integer productQuantity) {
this.productId = productId;
this.productQuantity = productQuantity;
}
}
数据类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderMaster2OrderDTOConverter {

public static OrderDTO convert(OrderMaster orderMaster){
OrderDTO orderDTO = new OrderDTO();
BeanUtils.copyProperties(orderMaster, orderDTO);
return orderDTO;
}

public static List<OrderDTO> convert(List<OrderMaster> orderMasterList){
return orderMasterList.stream().map(e ->
convert(e)
).collect(Collectors.toList());
}
}
service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface OrderService {
// 新建订单,这里返回的是数据传输对象,在各个层中传输
OrderDTO create(OrderDTO orderDTO);
// 查询单个订单
OrderDTO findOne(String orderId);
// 查询订单列表
// 这里是查询单个卖家订单
Page<OrderDTO> findListByBuyerOpenid(String buyerOpenid, Pageable pageable);
// 查询所有订单列表
Page<OrderDTO> findAllList(Pageable pageable);
// 取消订单
OrderDTO cancel(OrderDTO orderDTO);
// 完结订单
OrderDTO finish(OrderDTO orderDTO);
// 支付订单
OrderDTO paid(OrderDTO orderDTO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDetailDao orderDetailDao;
@Autowired
private ProductService productService;
@Autowired
private OrderMasterDao orderMasterDao;

private PushMsgService pushMsgService;

// 创建订单的实现
@Override
@Transactional // 事务注解
public OrderDTO create(OrderDTO orderDTO) {

String orderId = GenKeyUtil.genUniqueKey();
BigDecimal orderAmount = new BigDecimal(BigInteger.ZERO);
// 1. 查询商品(数量,单价)
for (OrderDetail orderDetail : orderDTO.getOrderDetailList()) {
// 查商品信息
ProductInfo productInfo = productService.findOne(orderDetail.getProductId());
// 判断商品是否存在
if (productInfo == null) {
throw new SellException(ResultEnum.PRODUCT_NO_EXIST);
}
// 判断数量放在库存
// 2. 计算订单总价
// bug1:注意是orderDetail是错的,要用productInfo
// orderAmount = orderAmount.add(orderDetail.getProductPrice().multiply(new BigDecimal(orderDetail.getProductQuantity())));
orderAmount = orderAmount
.add(productInfo.getProductPrice()
.multiply(new BigDecimal(orderDetail.getProductQuantity())));

// 属性拷贝,补全orderDetail的属性,注意拷贝先后顺序,拷贝中productInfo的orderId/detailId的null值也会拷贝进来
// bug2:先拷贝完,再对个别值赋值。
BeanUtils.copyProperties(productInfo, orderDetail);
// 3. 订单性情入库,前端传进来的只有两个重要信息:商品Id和购买数量,所以在入库时要补全订单详情字段
orderDetail.setDetailId(GenKeyUtil.genUniqueKey());
orderDetail.setOrderId(orderId);
// 3.1 订单详情入库(OrderDetail)
orderDetailDao.save(orderDetail);
}

// 3.2 写入订单表(OrderMaster)
OrderMaster orderMaster = new OrderMaster();
// 同bug2
// controller层create函数无orderId出错bug处
orderDTO.setOrderId(orderId);
BeanUtils.copyProperties(orderDTO, orderMaster);
orderMaster.setOrderAmount(orderAmount);
// bug3:额外的属性被覆盖,重新设置
orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
orderMasterDao.save(orderMaster);

// 4. 扣库存
// 使用Lambert表达式生成cartDTOList
List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream().map(e ->
new CartDTO(e.getProductId(), e.getProductQuantity())
).collect(Collectors.toList());
productService.decreaseStock(cartDTOList);

return orderDTO;
}

// 查找单个订单
@Override
public OrderDTO findOne(String orderId) {

// 查询主订单
OrderMaster orderMaster = orderMasterDao.findOne(orderId);
// 判断是否为空
if (orderMaster == null) {
throw new SellException(ResultEnum.ORDER_NO_EXIST);
}

// 查询订单详情
List<OrderDetail> orderDetailList = orderDetailDao.findByOrderId(orderId);
if (CollectionUtils.isEmpty(orderDetailList)) {
throw new SellException(ResultEnum.ORDERDETAIL_NO_EXIST);
}

// 查询结果用OrderDTO封装
OrderDTO orderDTO = new OrderDTO();
BeanUtils.copyProperties(orderMaster, orderDTO);
orderDTO.setOrderDetailList(orderDetailList);
return orderDTO;
}

// 查询订单列表(单个用户的订单列表)
@Override
public Page<OrderDTO> findListByBuyerOpenid(String buyerOpenid, Pageable pageable) {
// Page类不了解,明天看一看
// 查询
Page<OrderMaster> orderMasterPage =
orderMasterDao.findByBuyerOpenid(buyerOpenid, pageable);

// 转换
List<OrderDTO> orderDTOList =
OrderMaster2OrderDTOConverter.convert(orderMasterPage.getContent());

// 包装
Page<OrderDTO> orderDTOPage =
new PageImpl<OrderDTO>(orderDTOList, pageable, orderDTOList.size());

return orderDTOPage;
}

// 查询所有订单列表
@Override
public Page<OrderDTO> findAllList(Pageable pageable) {
Page<OrderMaster> orderMasterPage = orderMasterDao.findAll(pageable);

List<OrderDTO> orderDTOList =
OrderMaster2OrderDTOConverter.convert(orderMasterPage.getContent());

Page<OrderDTO> orderDTOPage =
new PageImpl<OrderDTO>(orderDTOList, pageable, orderDTOList.size());

return orderDTOPage;
}

// 取消订单
@Override
@Transactional
public OrderDTO cancel(OrderDTO orderDTO) {

// 1.判断订单状态,这里判断只有新订单可以取消
if (!orderDTO.getOrderStatus().equals(OrderStatusEnum.NEW.getCode())) {
// 使用日志,记录下订单号和订单状态,并抛出异常
log.error("[取消订单] 订单状态不正确,orderId={}, orderStatus={}",
orderDTO.getOrderId(), orderDTO.getOrderStatus());
throw new SellException(ResultEnum.ORDER_STATUS_ERROR);
}

// 2.修改订单状态(save)
OrderMaster orderMaster = new OrderMaster();
// 2.1 改状态为"取消"
// bug4 同对象拷贝前后的问题,先将DTO的状态修改完成再拷贝到Master中,
// 因为后续操作都在Master上
orderDTO.setOrderStatus(OrderStatusEnum.CANCEL.getCode());
BeanUtils.copyProperties(orderDTO, orderMaster);
// 2.2 save函数参数为OrderMaster,所以上面有个转型
OrderMaster updateResult = orderMasterDao.save(orderMaster);
if (updateResult == null) {
log.error("[取消订单] 更新失败,orderMaster={}", orderMaster);
throw new SellException(ResultEnum.ORDER_UPDATE_ERROR);
}

// 3.返还库存
// 3.1 判断订单内是否有商品
if (CollectionUtils.isEmpty(orderDTO.getOrderDetailList())) {
log.error("[取消订单] 订单内无商品,orderDTO={}", orderDTO);
throw new SellException(ResultEnum.ORDER_EMPTY_ERROR);
}

// 3.2 返回(增加)库存
// 将订单OrderDTO转型为CartDTO,因为其参数格式为之
List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream()
.map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()))
.collect(Collectors.toList());
productService.increaseStock(cartDTOList);

// 4.退还已支付金额
if (orderDTO.getOrderStatus().equals(PayStatusEnum.SUCCESS.getCode())) {
//TODO
}

return orderDTO;
}

// 完结订单
@Override
@Transactional
public OrderDTO finish(OrderDTO orderDTO) {

// 判断订单状态(只有新下单时候可执行)
// 不是新订单就报错
if (!orderDTO.getOrderStatus().equals(OrderStatusEnum.NEW.getCode())) {
log.error("[完结订单] 订单状态不正确,orderId={}, orderStatus={}",
orderDTO.getOrderId(), orderDTO.getOrderStatus());
throw new SellException(ResultEnum.ORDER_STATUS_ERROR);
}

// 修改订单状态(修改为已完结)
orderDTO.setOrderStatus(OrderStatusEnum.FINISHED.getCode());
OrderMaster orderMaster = new OrderMaster();
BeanUtils.copyProperties(orderDTO, orderMaster);
OrderMaster resultOrderMaster = orderMasterDao.save(orderMaster);
// 判断是否修改成功
if (resultOrderMaster == null) {
log.error("[完结订单] 更新失败,orderMaster={}", orderMaster);
throw new SellException(ResultEnum.ORDER_UPDATE_ERROR);
}

// 推送微信模板消息
pushMsgService.orderStatus(orderDTO);

return orderDTO;
}

// 支付订单
@Override
@Transactional
public OrderDTO paid(OrderDTO orderDTO) {

// 判断订单状态
if (!orderDTO.getOrderStatus().equals(OrderStatusEnum.NEW.getCode())) {
log.error("[支付订单完成] 订单状态不正确,orderId={}, orderStatus={}",
orderDTO.getOrderId(), orderDTO.getOrderStatus());
throw new SellException(ResultEnum.ORDER_STATUS_ERROR);
}

// 判断支付状态
if (!orderDTO.getPayStatus().equals(PayStatusEnum.WAIT.getCode())) {
log.error("[订单支付完成] 订单支付状态不正确,orderDTO={}", orderDTO);
throw new SellException(ResultEnum.ORDER_PAY_STATUS_ERROR);
}

// 修改支付状态
orderDTO.setPayStatus(PayStatusEnum.SUCCESS.getCode());
OrderMaster orderMaster = new OrderMaster();
BeanUtils.copyProperties(orderDTO, orderMaster);
OrderMaster resultOrderMaster = orderMasterDao.save(orderMaster);
// 判断是否修改成功
if (resultOrderMaster == null) {
log.error("[订单支付完成] 订单支付状态不正确,orderDTO={}", orderDTO);
throw new SellException(ResultEnum.ORDER_UPDATE_ERROR);
}
return orderDTO;
}
}

BuyerOrderController

api
创建订单
1
POST /sell/buyer/order/create

参数

1
2
3
4
5
6
7
8
name: "张三"
phone: "18868822111"
address: "慕课网总部"
openid: "ew3euwhd7sjw9diwkq" //用户的微信openid
items: [{
productId: "1423113435324",
productQuantity: 2 //购买数量
}]

返回

1
2
3
4
5
6
7
{
"code": 0,
"msg": "成功",
"data": {
"orderId": "147283992738221"
}
}
订单列表
1
GET /sell/buyer/order/list

参数

1
2
3
openid: 18eu2jwk2kse3r42e2e
page: 0 //从第0页开始
size: 10

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"code": 0,
"msg": "成功",
"data": [
{
"orderId": "161873371171128075",
"buyerName": "张三",
"buyerPhone": "18868877111",
"buyerAddress": "慕课网总部",
"buyerOpenid": "18eu2jwk2kse3r42e2e",
"orderAmount": 0,
"orderStatus": 0,
"payStatus": 0,
"createTime": 1490171219,
"updateTime": 1490171219,
"orderDetailList": null
},
{
"orderId": "161873371171128076",
"buyerName": "张三",
"buyerPhone": "18868877111",
"buyerAddress": "慕课网总部",
"buyerOpenid": "18eu2jwk2kse3r42e2e",
"orderAmount": 0,
"orderStatus": 0,
"payStatus": 0,
"createTime": 1490171219,
"updateTime": 1490171219,
"orderDetailList": null
}]
}
查询订单详情
1
GET /sell/buyer/order/detail

参数

1
2
openid: 18eu2jwk2kse3r42e2e
orderId: 161899085773669363

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"code": 0,
"msg": "成功",
"data": {
"orderId": "161899085773669363",
"buyerName": "李四",
"buyerPhone": "18868877111",
"buyerAddress": "慕课网总部",
"buyerOpenid": "18eu2jwk2kse3r42e2e",
"orderAmount": 18,
"orderStatus": 0,
"payStatus": 0,
"createTime": 1490177352,
"updateTime": 1490177352,
"orderDetailList": [
{
"detailId": "161899085974995851",
"orderId": "161899085773669363",
"productId": "157875196362360019",
"productName": "招牌奶茶",
"productPrice": 9,
"productQuantity": 2,
"productIcon": "http://xxx.com",
"productImage": "http://xxx.com"
}
]
}
}
取消订单
1
POST /sell/buyer/order/cancel

参数

1
2
openid: 18eu2jwk2kse3r42e2e
orderId: 161899085773669363

返回

1
2
3
4
5
{
"code": 0,
"msg": "成功",
"data": jsonnull
}
表单验证

包装一个订单数据表单数据类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class OrderForm {

// 注意这里的属性都要通过表单验证才可用

// 买家手机
@NotEmpty(message = "姓名必填")
private String name;
// 买家手机
@NotEmpty(message = "手机号必填")
private String phone;
// 买家地址
@NotEmpty(message = "地址必填")
private String address;
// 买家微信openid
@NotEmpty(message = "openid必填")
private String openid;
// 买家购物车信息
// 这里搞不懂为什么是String类型的
@NotEmpty(message = "购物车必填")
private String items;
}
数据类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j
public class OrderForm2OrderDTOConverter {

// 对象转型函数
public static OrderDTO convert(OrderForm orderForm) {

OrderDTO orderDTO = new OrderDTO();
orderDTO.setBuyerName(orderForm.getName());
orderDTO.setBuyerPhone(orderForm.getPhone());
orderDTO.setBuyerAddress(orderForm.getAddress());
orderDTO.setBuyerOpenid(orderForm.getOpenid());

// 上面四个属性很容易设置,最后一个字符串修改较为麻烦
// 购物车items是个json格式,这里利用一个gson的转格式依赖加入pom文件中
// 目的是将json转成List,存入orderDTO的List<OrderDetail>属性中
Gson gson = new Gson();

List<OrderDetail> orderDetailList = new ArrayList<>();

try {
// 使用gson的方法转换
// try-catch一下是否有问题
orderDetailList = gson.fromJson(orderForm.getItems(),
new TypeToken<List<OrderDetail>>(){}.getType());
} catch (Exception e){
log.error("[对象转换] 错误,string={}", orderForm.getItems());
throw new SellException(ResultEnum.PARAM_ERROR);
}

// 没问题时,进行set操作
orderDTO.setOrderDetailList(orderDetailList);
return orderDTO;
}
}
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@RestController
@RequestMapping("/buyer/order")
@Slf4j
public class BuyerOrderController {
@Autowired
private OrderService orderService;
@Autowired
private BuyerService buyerService;

/* @description 创建订单,这里传入参数需要验证使用注解@Valid,参数验证结果返回为BindingResult接受
* @param orderForm 表单验证包装类
* @param bindingResult 验证结果
*/
@PostMapping("/create")
public ResultVo<Map<String, String>> create(@Valid OrderForm orderForm,
BindingResult bindingResult){
// 判断验证结果
if(bindingResult.hasErrors()){
log.error("[创建订单] 参数不正确 orderForm={}",orderForm);
// 这里抛出异常会抛出是哪个参数不正确
throw new SellException(ResultEnum.PARAM_ERROR.getCode(),
bindingResult.getFieldError().getDefaultMessage());
}

// 验证通过,开始创建订单
// 注意create()函数的参数是orderDTO类型,所以这里又需要对象转型
OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);

// 判断转换后是否还有问题
// 这里是判断购物车是否为空
if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
log.error("[创建订单] 购物车不能为空");
// 这里抛出异常会抛出是哪个参数不正确
throw new SellException(ResultEnum.CART_EMPTY);
}
// 创建订单
OrderDTO createOrder = orderService.create(orderDTO);

// 包装结果
Map<String,String> map = new HashMap<>();
map.put("orderId",createOrder.getOrderId());
return ResultVoUtil.success(map);
}

/* @description 根据买家微信openid查询订单列表,分页
* @param openid 买家微信openid
* @param page 其实页数
* @param size 每页订单个数
* @return com.imooc.sell.viewobject.ResultVo<java.util.List<com.imooc.sell.dto.OrderDTO>>
* @info zw 2020/1/8 21:45
*/
@GetMapping("/list")
public ResultVo<List<OrderDTO>> list(@RequestParam("openid") String openid,
@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size){
// 判断openid是否为空
if(StringUtils.isEmpty(openid)){
log.error("[查询订单列表] openid为空");
throw new SellException(ResultEnum.PARAM_ERROR);
}

// 通过验证,开始查询
PageRequest pageRequest = new PageRequest(page,size);
Page<OrderDTO> orderDTOPage = orderService.findListByBuyerOpenid(openid,pageRequest);

return ResultVoUtil.success(orderDTOPage.getContent());
}

// 订单详情(单个)
@GetMapping("/detail")
public ResultVo<OrderDTO> detail(@RequestParam("openid") String openid,
@RequestParam("orderId") String orderId){
// 不安全做法,因为没有openid和orderId的对比验证,随便两个字符串就可以传进去
// 做法是在这里加入验证逻辑,这样的话代码显得臃肿,
// 正确做法是将查询单个订单和取消订单用另一个Service实现(这里是BuyerService)
// OrderDTO orderDTO = orderService.findOne(orderId);

// 改进
OrderDTO orderDTO = buyerService.findOneBuyerSelfOrder(openid,orderId);
return ResultVoUtil.success(orderDTO);
}

// 取消订单
@PostMapping("/cancel")
public ResultVo<OrderDTO> cancel(@RequestParam("openid") String openid,
@RequestParam("orderId") String orderId){
// 不安全做法
// 先查询订单
// OrderDTO orderDTO = orderService.findOne(orderId);
// orderService.cancel(orderDTO);

// 改进
buyerService.cancelBuyerSelfOrder(openid,orderId);
return ResultVoUtil.success();
}
}
查询和取消订单安全做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface BuyerService {

// 查询一个订单,有验证
OrderDTO findOneBuyerSelfOrder(String openid, String orderId);

// 取消一个订单,有验证
OrderDTO cancelBuyerSelfOrder(String openid, String orderId);
}

@Service
@Slf4j
public class BuyerServiceImpl implements BuyerService {

@Autowired
private OrderService orderService;

@Override
public OrderDTO findOneBuyerSelfOrder(String openid, String orderId) {
// 先查询出orderDTO
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
return null;
}
// 判断openid和orderId订单中的openid是否一致
if(!orderDTO.getBuyerOpenid().equalsIgnoreCase(openid)){
log.error("[订单查询] openid不一致,openid={},orderDTO={}",openid,orderDTO);
throw new SellException(ResultEnum.ORDER_OWNER_ERROR);
}
return orderDTO;
}

@Override
public OrderDTO cancelBuyerSelfOrder(String openid, String orderId) {
// 先查询出orderDTO
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
log.error("【取消订单】查不到修改订单, orderId={}", orderId);
throw new SellException(ResultEnum.ORDER_NO_EXIST);
}
// 判断openid和orderId订单中的openid是否一致
if(!orderDTO.getBuyerOpenid().equalsIgnoreCase(openid)){
log.error("[订单查询] openid不一致,openid={},orderDTO={}",openid,orderDTO);
throw new SellException(ResultEnum.ORDER_OWNER_ERROR);
}
return orderService.cancel(orderDTO);
}
}

微信端开发

项目总结

RESTfulAPI

  • REST的全称是Representational State Transfer,翻译下来就是“表现层状态转移”。
  • REST的名称”表现层状态转化”中,省略了主语Resource。”表现层”其实指的是”资源”(Resources)的”表现层”。“资源”是REST架构或者说整个网络处理的核心。
  • Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。
  • 对资源的添加,修改,删除等操作,用HTTP协议里的动词来实现。如下:

GET 用来获取资源
POST 用来新建资源(也可以用于更新资源)
PUT 用来更新资源
DELETE 用来删除资源

  • Server和Client之间传递某资源的一个表现形式,比如用JSON,XML传输文本。
  • 用 HTTP Status Code传递Server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。

结合本项目做具体阐述:

Spring中也有Restful请求相关的注解@RestController,它是Spring4.0新添加的,是@Controller@ResponseBody的组合注解。该注解用于controller上,该注解下的方法返回的对象(可序列化的)可直接响应给客户端,以json格式展示,也可用于前后端分离的项目(返回的对象模型可以填充到前端界面中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RestController
@RequestMapping("/buyer/product")
public class BuyerProductController {

@Autowired
private ProductService productService;

@Autowired
private CategoryService categoryService;

@GetMapping("/list")
public ResultVO list(){

//1.查询所有的上架的商品
List<ProductInfo> productInfoList=productService.findUpAll();

//2.查询在架商品所属类目(一次性查询)
// List<Integer> categoryTypeList=new ArrayList<>();
// //传统方法
// for(ProductInfo productInfo: productInfoList){
// categoryTypeList.add(productInfo.getCategoryType());
// }
//精简方法lamba表达式
List<Integer> categoryTypeList=productInfoList.stream()
.map(e->e.getCategoryType()).collect(Collectors.toList());

List<ProductCategory> productCategoryList=categoryService.findByCategoryTypeIn(categoryTypeList);

//3. 数据拼装
List<ProductVO> productVOList=new ArrayList<>();
for(ProductCategory productCategory: productCategoryList){
ProductVO productVO=new ProductVO();
productVO.setCategoryName(productCategory.getCategoryName());
productVO.setCategoryType(productCategory.getCategoryType());

List<ProductInfoVO> productInfoVOList=new ArrayList<>();
for(ProductInfo productInfo: productInfoList){
if(productInfo.getCategoryType().equals(productCategory.getCategoryType())){
ProductInfoVO productInfoVO=new ProductInfoVO();
BeanUtils.copyProperties(productInfo,productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}

// ResultVO resultVO=new ResultVO();
// resultVO.setData(productVOList);
// resultVO.setCode(0);
// resultVO.setMsg("成功");
ResultVO resultVO=ResultVOUtil.success(productVOList);
return resultVO;
}

}

ResultVO的定义如下:

1
2
3
4
5
6
7
8
9
10
11
@Data
//@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultVO<T> implements Serializable{
private static final long serialVersionUID = 3068837394742385883L;
/**错误码**/
private Integer code;
/**提示信息**/
private String msg;
/**具体内容**/
private T data;
}

对于上面商品列表的接口,http://127.0.0.1:8080/sell/buyer/order/list?openid=abcabcvc

首先,我们用浏览器访问,会返回如下的json数据:

rest1.PNG

使用前端界面访问,就会得到如下画面:

rest2.PNG

所以对于前后端分离的项目,前后端人员可以共同协商写一份API文档(包含访问接口,和返回数据格式),然后后端人员,就会根据根据接口进行开发,并按API文档要求返回指定格式的数据。


详情参考:

日志使用

什么是日志日志框架

  • 一套能够实现日志输出的工具包。
  • 能够描述系统运行状态的所有时间都可以算作日志,包括用户下线,接口超时,数据库崩溃。

日志框架的能力

  • 定制输出目标。
  • 定制输出格式。
  • 日志携带的上下文信息,如时间戳、类路径、线程、调用对象等等。
  • 运行时的选择性输出,比如现在的系统正常,我就只关心正常的日志,假如现在系统运行特别慢,那我可能就比较关心数据库访问层的问题,也就是DAO层的细节,这时候如果能够把这部分相关的日志打印出来,就很重要了。
  • 灵活的配置。
  • 优异的性能。

常用的日志框架

1
2
3
4
5
日志门面        日志实现
JCL Log4j
SLF4J Log4j2
jboss Logback
JUL

进过比较,我们选用SLF4j和Logback。Spring Boot里面用的就是SLF4j和Logback,在使用Spring Boot构建web项目是,我们引入了,下面的起步依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

这个起步依赖已经引入了SLF4j和Logback,所以我们不必再显示引入。

  • 使用方法: 如果在A类中使用日志,先以该类为参数构造一个对象,如下:
1
private final Logger logger=LoggerFactory.getLogger(A.class);

然后就可以使用:

1
2
3
4
log.debug("debug......");
log.info("name: {} , password: {}",name,password);
log.error("error......");
log.warn("warning.....");

更简洁的使用技巧

上面每次使用都要构造一个Logger对象,有没有很麻烦。
其实在IEDA里,我们可以有一点小技巧。可以不用构造Logger对象。

首先,添加以下依赖。

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>

然后,在IDEA中安装lombok插件,然后在需要打印日志的类上用@Slf4j注解。
这样在该类中就可以直接使用log对象了。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class LoggerTest {
// private final Logger logger= LoggerFactory.getLogger(LoggerTest.class);
@Test
public void test1(){
String name="imooc";
String password="123456";
//要安裝Lombok插件,才能直接用log
log.debug("debug......");
log.info("name: {} , password: {}",name,password);
log.error("error......");
log.warn("warning.....");

}
}
  • 在日志里输出变量,用占位符。
1
2
String name="Mary";
log.info("name: {}",name);

Logback的配置

  • 方法一:在application.yml中配置,这里能配置的东西比较简单,只能配置日志文件的路径,日志输出格式等一些简单的配置。
1
2
3
4
5
6
logging:
pattern:
console: "%d - %msg%n" #配置日志的打印格式
file: F:/日志/sell.log
level:
com.imooc.LoggerTest: debug #将日志级别指定到一个类
  • 方法二:在logback-spring.xml配置,可以进行一些复杂的配置。比如有下面的需求:
    • 区分info和error日志
    • 每天产生一个日志文件

resource目录下建立logback-spring.xml文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!--1. 控制台日志输出的配置-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%d - %msg%n
</pattern>
</layout>
</appender>

<!--2. 日志输出文件,infor级别-->
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>
%msg%n
</pattern>
</encoder>
<!-- 滚动策略,按时间滚动,每天一个日志-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>F:/日志/info.%d.log</fileNamePattern>
</rollingPolicy>
</appender>

<!--3. 日志输出文件,error级别-->
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>
%msg%n
</pattern>
</encoder>
<!-- 滚动策略,按时间滚动,每天一个日志-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>F:/日志/error.%d.log</fileNamePattern>
</rollingPolicy>
</appender>

<!--下面声明把以上的配置用在哪里,root即对整个项目都适用-->
<root level="info">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
</root>
</configuration>

对于每个配置项的含义,已用注解作简要说明。不过我们还是有必要了解一下日志级别,在package org.slf4j.event;有一个Level的枚举类,它定义了日志级别,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum Level {

ERROR(ERROR_INT, "ERROR"),
WARN(WARN_INT, "WARN"),
INFO(INFO_INT, "INFO"),
DEBUG(DEBUG_INT, "DEBUG"),
TRACE(TRACE_INT, "TRACE");

private int levelInt;
private String levelStr;

Level(int i, String s) {
levelInt = i;
levelStr = s;
}

public int toInt() {
return levelInt;
}

/**
* Returns the string representation of this Level.
*/
public String toString() {
return levelStr;
}

}

里面定义的TRACE、DEBUG、INFO、WARN、ERROR,表明严重性逐渐增加。

lombok其他的作用

上面引入的lombok依赖还有其他的作用,我们在项目中与许多Entity和DTO,它们有许多字段,而且需要get、set方法。我们可以在该实体类上使用@Data注解,那么实体类就不需要显示写get、set方法了。

1
2
3
4
5
6
7
8
9
10
11
12
import lombok.Data;

@Data
public class CategoryForm {

private Integer categoryId;
/**类目名字**/
private String categoryName;
/** 类目编号**/
private Integer categoryType;

}

如果该实体类只需get或set方法,那么就可以使用@Getter或@Setter注解。

Java8特性

项目中BuyerProductController.java中有这么一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1.查询所有的上架的商品
List<ProductInfo> productInfoList=productService.findUpAll();

//2.查询在架商品所属类目(一次性查询)
// List<Integer> categoryTypeList=new ArrayList<>();
// //传统方法
// for(ProductInfo productInfo: productInfoList){
// categoryTypeList.add(productInfo.getCategoryType());
// }
//精简方法lamba表达式
List<Integer> categoryTypeList=productInfoList.stream()
.map(e->e.getCategoryType()).collect(Collectors.toList());
//或者像下面这样使用方法引用来简化lambda表达式
// List<Integer> categoryTypeList=productInfoList.stream()
// .map(ProductInfo::getCategoryType).collect(Collectors.toList());

上面代码中涉及了一些java8的知识:

  1. Stream(流)
  2. lambda表达式
  3. 使用方法引用来简化lambda表达式
  4. 使用Stream操作集合

在学习lambda表达式之前,要先了解函数式接口。

函数式接口

函数式接口是只含有一个抽象方法的接口,比如下面就是一个函数式接口:

1
2
3
4
@FunctionalInterface
interface MyFunInterface {
int test(String s);
}

我们还可以使用@FunctionalInterface注解函数式接口,使用该注解后,该接口就只能定义一个抽象方法。

lambda表达式

我们可以使用lambda表达式来实现一个函数式接口,如下:

1
2
3
4
5
6
7
public class MyTest {
public static void main(String[] args) {
MyFunInterface lengthCal = s -> s.length();
int len=lengthCal.test("hello");
System.out.println(len);
}
}

方法引用和构造器引用

lambda表达式还是比较常用,很简洁。不过还有比lambda表达式更简洁的写法,那就是方法引用,先上代码:

1
2
3
//1.上面lambda可以用:引用类方法简化
MyFunInterface lengthCal1=String::length;
int len1=lengthCal.test("hello");

以上是使用方法引用来简化lambda表达式。如果lambda表达式的方法体只有一个方法调用,可以使用方法引用来简化lambda表达式。

下面对几种方法引用的方式总结:

种类 使用方式
引用类方法 类名::类方法
引用类的实例方法 类名::实例方法
引用特定对象的实例方法 特定对象::实例方法
引用构造器 类名::new

下面一段代码用例子说明了上面四种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@FunctionalInterface
interface MyFunInterface {
int test(String s);
}

@FunctionalInterface
interface MyFunInterface1 {
String subStr(String s,int begin,int end);
}

@FunctionalInterface
interface MyFunInterface2{
JFrame win(String title);
}
public class MyTest {
public static void main(String[] args) {
// 下面是对4种方式的举例
MyFunInterface intValConvertor = from->Integer.valueOf(from);
int intVal = intValConvertor.test("2018");
// 1.上面个lambda可以用:引用类方法简化
MyFunInterface intValConvertor1=Integer::valueOf;
intVal=intValConvertor1.test("2018");
System.out.println(intVal);

MyFunInterface1 subStrUtil=(a, b, c)->a.substring(b,c);
String sub=subStrUtil.subStr("hello world",2,4);
//2.上面lambda可以用:引用类的实例方法简化
MyFunInterface1 subStrUtil1=String::substring;
String sub1=subStrUtil1.subStr("hello world",2,4);
System.out.println(sub);

MyFunInterface begIdxCal= s->"hello world".indexOf(s);
int begIdx=begIdxCal.test("lo");
//3.上面lambda可以用:引用特定对象的实例方法简化
MyFunInterface begIdxCal1="hello world"::indexOf;
int begIdx1=begIdxCal1.test("lo");
System.out.println(begIdx1);

MyFunInterface2 jFrame=a->new JFrame(a);
JFrame jf=jFrame.win("我的窗口");
//4.上面的lambda可以用:引用构造器简化
MyFunInterface2 jFrame2=JFrame::new;
JFrame jf2=jFrame2.win("我的窗口");
System.out.println(jf2);
}
}

使用Stream来操作集合

现在主要讲解流中和集合操作相关的的知识。

本文的开头productInfoList.stream()返回一个Stream对象,下面挑选stream中一个典型的方法分析一下使用方式。

  • 下面是map方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param <R> The element type of the new stream
* @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* function to apply to each element
* @return the new stream
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

上面注释就是说该方法返回一个流,该流是一个中间操作,它包含通过mapper函数运算后结果。

比如productInfoList.stream().map(e->e.getCategoryType())就是将集合中商品的种类映射为一个Stream,它是一个中间流,再看后面一部分collect(Collectors.toList()),它表示把这个中间流变成一个List。
下面我们对Collectors这个类进行探究。

Collectors

下面是Collectors类注释的截图:

使用方式说的很明白,详情参见:Collectors的API文档

下面用一个例子对上面的文档进行详细说明,建议在在IDEA里debug模式下运行,查看个变量的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class MyTest1 {
public static void main(String[] args) {
List<People> peopleList=new ArrayList<>();
peopleList.add(new People("sun","male",23,8000.0));
peopleList.add(new People("li","female",21,7600.1));
peopleList.add(new People("wang", "male", 32, 9000));
peopleList.add(new People("fan","female",18,5000));

//将姓名收集到一个list
List<String> nameList = peopleList.stream().map(People::getName).collect(Collectors.toList());

//将姓名收集到一个set
Set<String> nameSet=peopleList.stream().map(People::getName).collect(Collectors.toSet());

//将姓名以逗号为分隔符连接
String nameJoined = peopleList.stream().map(People::getName).collect(Collectors.joining(", "));

//计算总年龄
int totalAge=peopleList.stream().collect(Collectors.summingInt(People::getAge));

//以性别对人员分组
Map<String, List<People>> bySex = peopleList.stream().collect(Collectors.groupingBy(People::getSex));

//计算各性别的总薪水
Map<String,Double> totalBySex=peopleList.stream().collect(Collectors.groupingBy(People::getSex,
Collectors.summingDouble(People::getSalary)));

//以6000薪水分割线对人员分组
Map<Boolean, List<People>> pass6000 = peopleList.stream().collect(Collectors.partitioningBy(people -> people.getSalary() > 6000));

}

}
class People{
private String name;
private String sex;
private int age;
private double salary;
public People(String name, String sex, int age, double salary) {
this.name = name;
this.sex = sex;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}