您的位置:首页 > 编程语言 > Java开发

Java【SpringBoot实战—微信点餐系统学习总结】

2017-10-25 19:46 961 查看



SpringBoot实战——微信点餐系统


导语:

通过这段时间的学习,我从0到1,终于完成了一个比较成熟的SpringBoot实战项目,这个项目的意义有两点:第一、让我接用当下比较流行框架搭建一个完整项目,见识到实际项目中的一些问题,接下来我主要会详细介绍我在写项目过程中遇到的问题以及解决方法。第二、这个项目也让我对面向对象语言有了更深一步的认识与了解,比如在原来稍微理解的一些java特性,最简单的例如java三大特性:多态、继承、封装还有框架的一些思想:IOC(控制反转)和DI(依赖注入)。接下来我都会在笔记中详细的写出理解。


一、项目简介——技术要点


前端和后端:




后端主要技术:




微信接口技术

微信支付
微信扫码登录
微信模板消息推送


开发环境



但实际上我用的环境和这上面还是有点不一样,我服务器用的是win,到时候我会详细说明在win上怎么部署。


前置知识

JavaWeb基础
Maven构建项目
SpringBoot基础


功能分析




项目部署




项目目录:



还是按照传统的三层结构:Controller、Service、Dao(repository)编写代码,我们这里按照从底往上的顺序进行编写并以Order类的操作为例,首先开发Dao层


Dao层

我们这里采用的是JPA的方式操作数据库
/**
* 查找订单详情
* Created by KHM
* 2017/7/27 9:59
*/
public interface OrderDetailRepository extends JpaRepository<OrderDetail, String>{

List<OrderDetail> findByOrderId(String orderId);
}
1
2
3
4
5
6
7
8
9
10


Service层接口和Impl

/**
* 订单service层
* Created by KHM
* 2017/7/27 11:04
*/
public interface OrderService {

//创建订单
OrderDTO create(OrderDTO orderDTO);

//查询单个订单详情
OrderDTO findOne(String orderId);

//查询订单总列表(买家用)
Page<OrderDTO> findList(String buyerOpenid, Pageable pageable);

//取消订单
OrderDTO cancel(OrderDTO orderDTO);

//完结订单
OrderDTO finish(OrderDTO orderDTO);

//支付订单
OrderDTO paid(OrderDTO orderDTO);

//查询订单列表(卖家管理系统用的)
Page<OrderDTO> findList(Pageable pageable);

}
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
/**
* Created by KHM
* 2017/7/27 11:28
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

@Autowired
private ProductService productService;

@Autowired
private OrderDetailRepository orderDetailRepository;

@Autowired
private OrderMasterRepository orderMasterRepository;

@Autowired
private PayService payService;

@Autowired
private WebSocket webSocket;

@Override
@Transactional//事务管理,一旦失败就回滚
public OrderDTO create(OrderDTO orderDTO) {
//设置下订单id(是个随机,这里调用了根据时间产生6位随机数的方法)
String orderId = KeyUtil.genUniqueKey();
//给总价赋值
BigDecimal orderAmount = new BigDecimal(BigInteger.ZERO);

//List<CartDTO> cartDTOList = new ArrayList<>();

//1.查询商品(数量,价格)
for (OrderDetail orderDetail : orderDTO.getOrderDetailList()){
ProductInfo productInfo = productService.findOne(orderDetail.getProductId());
if(productInfo == null){
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//2.计算总价=单价*数量+orderAmount
orderAmount = productInfo.getProductPrice()
.multiply(new BigDecimal(orderDetail.getProductQuantity()))
.add(orderAmount);

//3.订单详情入库(OrderMaster和orderDetail)
//利用BeanUtils方法把前端查找出来的productInfo商品信息复制给订单详情
BeanUtils.copyProperties(productInfo, orderDetail);//先复制,再赋值
orderDetail.setDetailId(KeyUtil.genUniqueKey());
orderDetail.setOrderId(orderId);

orderDetailRepository.save(orderDetail);

/* CartDTO cartDTO = new CartDTO(orderDetail.getProductId(), orderDetail.getProductQuantity());
cartDTOList.add(cartDTO);*/
}

//3.订单总表入库(OrderMaster和orderDetail)
OrderMaster orderMaster = new OrderMaster();
orderDTO.setOrderId(orderId);
BeanUtils.copyProperties(orderDTO, orderMaster);
orderMaster.setOrderAmount(orderAmount);//是一个整个订单的总价,所以在foe循环之外设置
orderMaster.setOrderStatus(OrderStatusEnum.New.getCode());
orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
orderMasterRepository.save(orderMaster);

//4.扣库存
List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream().map(e ->
new CartDTO(e.getProductId(), e.getProductQuantity())
).collect(Collectors.toList());
productService.decreaseStock(cartDTOList);

//发送websocket消息
webSocket.sendMessage(orderDTO.getOrderId());

return 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
这里需要注意的问题主要是Impl层逻辑的编写,我们需要用到的注解有:
1
2
@Service
@Slf4j日志需要
@Transactional//事务管理,一旦失败就回滚(主要用在要对数据库进行操作的方法上)
然后是一些技巧:善于使用工具类做开发,当需要例如随机数或者特殊字段的时候最好声明在方法开始,方便之后的调用
多多关注java的新特性,对代码的优化很有帮助
最重要的,写一个方法前先对这个方法的逻辑做个列举,第一步是什么、第二步是什么,再去编写具体的代码


Controller层

/**
* 购买操作
* Created by KHM
* 2017/7/30 16:48
*/
@RestController
@RequestMapping("/buyer/order")
@Slf4j
public class BuyerOrderController {

@Autowired
private OrderService orderService;

@Autowired
private BuyerService buyerService;

//创建订单
@PostMapping(value = "/create")
public ResultVO<Map<String, String>> creat(@Valid OrderForm orderForm,
BindingResult bindingResult){
if(bindingResult.hasErrors()){
log.error("【创建订单】 参数不正确, orderForm={}", orderForm);
throw new SellException(ResultEnum.PARAM_ERROR.getCode(),
bindingResult.getFieldError().getDefaultMessage());
}

OrderDTO orderDTO = OrderFormZOrderDTOConverter.convert(orderForm);
if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
log.error("【创建订单】 购物车不能为空");
throw new SellException(ResultEnum.CART_EMPTY);
}
OrderDTO createResult = orderService.create(orderDTO);

Map<String, String> map = new HashMap<>();
map.put("orderId", createResult.getOrderId());
return ResultVOUtil.success(map);
}

//订单列表
@GetMapping(value = "/list")
public ResultVO<List<OrderDTO>> list(@RequestParam("openid") String openid,
@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size){
if(StringUtils.isNullOrEmpty(openid)){
log.error("【查询订单列表】 openid为空");
throw new SellException(ResultEnum.PARAM_ERROR);
}

PageRequest request = new PageRequest(page, size);
Page<OrderDTO> orderDTOPage = orderService.findList(openid, request);
//只用返回当前页面的数据集合就行了,因为前端传过来的就是第几页和每一页的size(一般都会定好)
return ResultVOUtil.success(orderDTOPage.getContent());
}

//订单详情
@GetMapping("/detail")
public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
@RequestParam("orderId") String orderId){
/* //TODO 不安全的做法,改进
OrderDTO orderDTO = orderService.findOne(orderId);*/
OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);

return ResultVOUtil.success(orderDTO);
}

//取消订单
@PostMapping("/cancel")
public ResultVO cancel(@RequestParam("openid") String openid,
@RequestParam("orderId") String orderId){
/* //TODO 不安全的做法,改进
OrderDTO orderDTO =  orderService.findOne(orderId);
orderService.cancle(orderDTO);*/
buyerService.cancelOrder(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
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
Controller需要注意的问题是:
1
2
请求方式一定要写清楚Post和Get不要弄错了
需要前端做表单验证的时候就要用@Valid+前端表单类
做好几乎所有的出现null或者错误的if判断去抛出异常
返回要用包装类VO去返回


关于Exception、几种包装类、工具类的介绍

一般情况下有两种注册异常类的方法:
1
2
/**
* Created by KHM
* 2017/7/27 17:34
*/
@Getter
public class SellException extends RuntimeException {

private Integer code;

public SellException(ResultEnum resultEnum) {
//把枚举中自己定义的message传到父类的构造方法里,相当于覆盖message
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}

//而这个是需要自己去填写code的新的meg,不一定是枚举中的模糊的说法,可以把具体的错误信息信使出来
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
善用工具类,反正只要是能单独拆分出来使代码看上去更优雅的代码都可以单独写出来当做工具类
1
2



包装类:但凡是需要把原dataobject(数据库对应的实体类)组合或者拆分的新类我们都用包装类来代替,可大致
分为以下几种包装类:
1
2
3
返回给前端的VO对象,主要按照前端API开发
/**
* 需要返回的商品详情
* Created by KHM
* 2017/7/26 17:54
*/
@Data
public class ProductInfoVO implements Serializable {

private static final long serialVersionUID = -3013889380494680036L;

//为了防止多个name造成混淆,所以要细起名,但为了和返回对象名一致,所以用这个注解
//其实也不是造成混淆,主要原因还是为了和原productId对象中属性名一致并且为了和前端API一致,才要在这里起别名,让他在返回时实例化成别的名字
@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;

}
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
前端和后端都包装的DTO对象,主要也是按照前端API开发,但是这个和VO的区别是DTO包含了整个原始对象,而VO缩减了一些属性
/**
* DTO类用来关联dataobject中有联系的类,比如创建订单就需要订单总表和订单详情表两种数据,
* 所以就需要一种包含了这两种实体的包装类把他们联系起来
* 因为用dataobject来关联的话会破坏映射的数据库的关系
* Created by KHM
* 2017/7/27 11:10
*/
@Data
//@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
//@JsonInclude(JsonInclude.Include.NON_NULL)//可以让为null属性不返回
public class OrderDTO {

//订单id
//@Id,不需要此注解了,因为这不是关联数据库的类
private String orderId;

//买家姓名
private String buyerName;

//买家手机号
private String buyerPhone;

//买家地址
private String buyerAddress;

//买家微信openid
private String buyerOpenid;

//订单总金额
private BigDecimal orderAmount;

//订单状态,默认为0新下单
private Integer orderStatus;

//支付状态,默认为0未支付
private Integer payStatus;

//创建时间
@JsonSerialize(using = Date2LongSerializer.class)
private Date createTime;

//更新时间
@JsonSerialize(using = Date2LongSerializer.class)
private Date updateTime;

//@Transient//为了方便关联订单总表和详情表,把此字段加在这.用此注解就可以让程序在与数据库关联时忽略此字段,但是更规范的写法就是创建新的DTO
private List<OrderDetail> orderDetailList; //= new ArrayList<>();(配置中配置了如果为null就不返回)

@JsonIgnore//在返回json的时候回忽略这个属性
public OrderStatusEnum getOrderStatusEnum() {
return EnumUtil.getByCode(orderStatus, OrderStatusEnum.class);
}

@JsonIgnore
public PayStatusEnum getPayStatusEnum() {
return EnumUtil.getByCode(payStatus, PayStatusEnum.class);
}

#配置了这个就不会返回为NULL的参数
jackson:
default-property-inclusion: non_null
}
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
总结:这些常规的后台开发最主要要的问题在于逻辑,开发每一个方法前要先想清楚这个方法的步奏,怎样写最方便,什么
时候该用到工具类,返回时要不要用包装对象,时刻在可能出现错误的地方打上日志和抛出异常,单元测试一定要贯穿三层
,Dao和Service层用Slf4j,Controller用Postman。这样才能保证开发不会出问题找不到。
1
2
3
4


微信支付篇:http://blog.csdn.net/qq_31783173/article/details/77618374


关于FreeMarker+ibootstrap+ModelAndView

假如你是个后端人员又需要自己写后台管理界面的时候,这套组合可谓是快速开发的神器了,用ibootstrap完成html
代码的实现,放在FreeMarker中,不用AJAX即可完成数据的交互,详情请查看FreeMarker:?
1
2
3


用分布式Session完成用户信息的登录判断


什么是分布式系统:

旨在支持应用程序和服务的开发,可以利用物理架构
由多个自治的处理元素,不共享主内存,但通过网络发送消息合作。
--Leslie Lamport
1
2
3
4


三个特点和三个概念




Session

广义的session:会话控制,不是普通的Http的Session
可以理解为一种Key-value的机制
它的关键点在于怎么设置Key和获取对应的value
第一种:SessionId客户端在请求服务端的时候,服务端会在Http的Header里面设置key和value,而客户端的cookie会把这个保存,后续的请求里面会自动的带上
第二种:token,我们需要手动在Http的Header或Url里设置token这个字段,服务器获得请求后在从Url或者Header里取出token进行验证,安全要求比较严格的时候需要配合签名一起使用
共同点:区局唯一


分布式系统中的session问题

当我们使用分布式系统运行时,会有多台服务器,怎么放呢?有两种方式
水平扩展:就是在多台服务器上部署一样的程序,就是集群
垂直扩展:其实就是拆分服务,不同Url负载均衡到不同的服务器上去
然后,当用户进行登录时,第一次可能在A服务器上,第二次可能就跑到B服务器上了,B服务器没有用户的Session,就以为没有登录,IPHash的解决方案还是优缺点不适用,真正的解决方案是什么呢?
加一台服务器装上Redis来专门保存用户的session信息,当其他服务器需要session信息的时候都去找他要

我们知道常规的登录、登录就是验证信息,存储浏览状态和让浏览状态失效,我们这里使用的第二种:token的方式,自己设置一个token字段,然后手动添加到cookie中,还有失效时间;登出的时候先清除redis的token,之后我们在访问其他页面的时候就可以通过cookie和redis的验证了,但这里似乎没有做关闭浏览器清除session的设置,具体代码实现:
/**
* 卖家用户登录管理
* Created by Akk_Mac
* Date: 2017/8/30 上午9:31
*/
@Controller
@RequestMapping("/seller")
public class SellerUserController {

@Autowired
private SellerService sellerService;

//redis的service,这里主要用stringredis
@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private ProjectUrlConfig projectUrlConfig;

@RequestMapping("/login")
public ModelAndView login(@RequestParam(value = "username", required=false ) String username,
@RequestParam(value = "password", required = false) String password,
HttpServletResponse response,
Map<String, Object> map) {
//1. 由于我们这里没有申请微信开放平台,所以就不用扫码登录了
if(username == null && password == null){
return new ModelAndView("common/login");
}

SellerInfo sellerInfo = sellerService.findSellerInfoByUsername(username);
if(sellerInfo == null && !sellerInfo.getPassword().equals(password)) {
map.put("msg", ResultEnum.LGOIN_FAIL.getMessage());
//map.put("url", "/sell/seller/order/list");
return new ModelAndView("common/login", map);
}
//2. 设置token至redis(用什么UUID设置)
String token = UUID.randomUUID().toString();
Integer expire = RedisConstant.EXPIRE;//token过期时间
//(key:token_ 为开头的格式String.format是格式设置方法, value=这里先设置为username, 过期时间, 时间单位)
redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), username, expire, TimeUnit.SECONDS);

//3. 设置token至cookie
CookieUtil.set(response, CookieConstant.TOKEN, token, expire);

//这里不是跳转到模板而是地址所以要用redirect,而且跳转最好用绝对地址
return new ModelAndView("redirect:" + projectUrlConfig.getSell()  + "/sell/seller/order/list");

}

@RequestMapping("/logout")
public ModelAndView logout(HttpServletRequest request,
HttpServletResponse response,
Map<String, Object> map) {
//1. 从cookie中查询
Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
if(cookie != null) {
//2. 清除redis
redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));

//3. 清除cookie
CookieUtil.set(response, CookieConstant.TOKEN, null, 0);
}
map.put("msg", ResultEnum.LOGOUT_SUCCESS.getMessage());
map.put("url", "/sell/seller/login");
return new ModelAndView("common/success", map);
}
}
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

AOP切面编程
/**
* AOP切面编程验证登录
* Created by Akk_Mac
* Date: 2017/8/30 上午11:05
*/
@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {

@Autowired
private StringRedisTemplate redisTemplate;

//拦截除了登录登出之外的操作,这是设置拦截范围
@Pointcut("execution(public * com.akk.controller.Seller*.*(..))" +
"&& !execution(public * com.akk.controller.SellerUserController.*(..))")
public void verify(){}

@Before("verify()")
public void doVerify() {

ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

//1. 查询Cookie
Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
if(cookie == null) {
log.warn("【登录校验】Cookie中查不到token");
throw new SellerAuthorizeException();
}

//2. 根据cookie查redis
String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
if(StringUtils.isEmpty(tokenValue)) {
log.warn("【登录校验】Redis中查不到token");
throw new SellerAuthorizeException();
}
}
}
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


项目部署

原本SpringBoot的部署是最好把项目打包成war包放在tomcat上,但我发现,我这里用打war的方法,要
去掉spring-boot-starter-web包中的内置tomcat容器,这样会使websocket类出现问题,检测不到javax
包,试了几次没有办法,就用了另一种方法,打成Jar包的形式,先用控制台运行的方式在服务器运行,这样是有
点隐患的,但这个冲突还没有解决。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: