本项目用于辅助SpringCloud相关技术的学习,通过代码实践进行学习。
设计 经过对SpringCloud的基础知识学习后,希望将它应用到酒店管理系统这个项目中去。我的规划如下:
在功能需求已知的情况下,分析并将业务拆解成多个模块实现微服务架构
进行初步的实体类设计,数据库设计
优先使用已掌握技能完成基础功能模块,如处理秒杀库存暂时不考虑使用分布式锁,目前计划使用synchronized解决
当前阻力:
前端设计
用户权限控制管理这块没有系统的学习,只能基础的使用JWT进行token解析
Flask框架并没有实际上手写过,也并没有使用Python进行过与数据库交互的项目开发,没有编写过脚本
消息中间件没有过多实际编码经验,尚无法确认能否顺利投入到项目使用中
理想状态:
2023.09.13
redis和SpringSecurity在后续学习的时候再用于这个项目
2024.11.22
把我自己都看笑了,太能🕊了
模块设计 仅针对Java部分的SpringCloud微服务设计
用户管理模块:负责处理用户的注册、登录、基本信息维护等功能。
酒店信息管理模块:负责处理酒店列表、基本信息、细节信息的维护等功能。
订单管理模块:负责处理用户预定的订单,包括浏览订单等功能。
管理员客户管理模块:负责帮助客户重置密码,生成新的随机密码,删除用户
UML 针对酒店管理系统的业务进行数据库设计和类图的设计
数据库设计 t_user
字段名
类型
长度
注释
id
int
用户id
username
varchar
255
用户名
password
varchar
255
密码
salt
varchar
255
盐值
sex
int
性别(女0,男1)
type
int
类型(0未成年人,1成年人)
identity
int
身份(0为普通用户,1为用户管理员,2为酒店管理人员,3为酒店管理员)
idcn
char
18
身份证号码
real_name
varchar
255
真实姓名
phone
varchar
255
电话号码
email
varchar
255
邮箱
created_at
datetime
创建时间
updated_at
datetime
修改时间
t_hotel
字段名
类型
长度
注释
id
int
用户id
name
varchar
255
酒店名称
address
varchar
255
酒店地址
introduction
varchar
255
酒店介绍
phone
varchar
联系方式
stars
int
星级
brand
varchar
255
品牌
business_district
varchar
255
所属商圈
created_at
datetime
创建时间
updated_at
datetime
修改时间
t_order
字段名
类型
长度
注释
id
int
订单id
user_id
int
255
用户id
room_id
int
255
房间id
check_in_date
datetime
255
入住日期
check_out_date
datetime
退房日期
room_num
int
房间数量
expected_guest_num
int
预期入住人数
has_children
int
18
是否有儿童(0无1有)
status
int
255
订单状态(0未支付,1已支付,2已取消)
price
decimal
255
订单价格
created_at
datetime
创建时间
updated_at
datetime
修改时间
t_room
字段名
类型
长度
注释
id
int
房间id
hotel_id
int
所属酒店id
num
int
剩余数量
type
varchar
255
房间类型(0单人间,1双床房,2大床房)
original_price
decimal
原价
price
decimal
当前价格
created_at
datetime
创建时间
updated_at
datetime
修改时间
t_facilities
字段名
类型
长度
注释
id
int
设施id
name
varchar
255
设施名称
is_free
int
是否免费(0付费,1免费)
created_at
datetime
创建时间
updated_at
datetime
修改时间
t_hotel_facilities
字段名
类型
长度
注释
id
int
hotel_id
int
酒店id
facilities_id
int
设施id
created_at
datetime
创建时间
updated_at
datetime
修改时间
2023.09.13
t_facilities设计有问题,is_free字段应该放在t_hotel_facilities中而不是t_facilities中;其次为用户增加了一个hotel_id的字段,提供给酒店管理人员标识其管理的酒店
类图 有机会再补吧,先🕊了
2023.09.13
不会再补了,彻底🕊了
用户业务 权限认证 以前写项目的时候根本没有怎么考虑过权限问题,很简单的做了一个token验证就结束了,现在自己开始考虑到这些问题发现自己根本想不到什么好的解决方案。
首先担心的是同角色之间资源的相互访问问题,也许可以通过验证用户的角色身份来解决一部分问题,但是如果同角色试图访问其他用户的资源,不对token中的用户id进行解析是没法解决的。这样的话我在Gateway的微服务中还需要添加与数据库的连接,我不知道是不是必须如此…我主观上是这么想的,确实想不到更优雅的办法,但是感觉这样子会很笨。
@Order(-1) @Component public class AuthorizationFilter implements GlobalFilter { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); String requestPath = request.getURI().getPath(); if (tokenShouldCheck(requestPath)) { if (!headers.containsKey("Authorization" )) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } String token = headers.getFirst("Authorization" ); Claims claims = JwtUtils.getClaimsByToken(token); if (claims.getExpiration().getTime() < new Date ().getTime()) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } else { } } return chain.filter(exchange); } public Boolean tokenShouldCheck (String path) { return !path.equals("/users/login" ) && !path.equals("/users/register" ); } }
整段代码我只能说,和优雅是完全不沾边,感觉过滤器只学到了皮毛…
2023/09/06
在昨晚写下博客之后又自己捣鼓了好久,发现我确实学的不精…
我最开始的思路是用feign来获取用户信息,但是我居然认为feign的远程调用要经过http,所以我放弃了这个想法。如果真是这样的话,那微服务中使用feign调用其他微服务来实现功能都没法成立了,毕竟你在实现中哪来的token(如果要做token验证)
于是使用feign解决了,但是确实不算优雅
@Order(-1) @Component public class AuthorizationFilter implements GlobalFilter { @Autowired UserClient userClient; @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); String requestPath = request.getURI().getPath(); if (tokenShouldCheck(requestPath)) { if (!headers.containsKey("Authorization" )) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } String token = headers.getFirst("Authorization" ); if (token == null ) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } Claims claims = JwtUtils.getClaimsByToken(token); if (claims.getExpiration().getTime() < new Date ().getTime()) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } else { Integer id = Integer.valueOf(claims.getSubject()); User user = userClient.findUserById(id); if (!hasAuthority(user, requestPath)) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); } } } return chain.filter(exchange); } public Boolean tokenShouldCheck (String path) { return !path.equals("/users/login" ) && !path.equals("/users/register" ) && !path.equals("/users/sendVerifyCode" ); } public Boolean hasAuthority (User user, String requestPath) { if (requestPath.equals("/users" )) { return user.getIdentity() == Identity.Admin.ordinal(); } else if (requestPath.startsWith("/users/" )) { String paramId = requestPath.split("/" )[2 ]; String id = String.valueOf(user.getId()); if (paramId.equals(id)) { return true ; } else { return user.getIdentity() == Identity.Admin.ordinal(); } } else { return true ; } } }
其他 其他功能以前都写烂了,没什么好说的感觉,不能很好的完成权限认证是我认为这项目开局的败笔😡
酒店业务 Feign 这两天几乎遇到的所有难题都和Feign有关,可以说被Feign狠狠拷打了…
错误一
feign.codec.DecodeException: Error while extracting response for type [java.util.ArrayList<com.magus.api.entity.Room>] and content type [application/json];
这两天见到的最多的就是这句话,然而造成这个错误的原因是多种多样的。
我第一次遭遇这个错误是返回值不匹配造成的,起因是我在网上看到了和我同样的操作,在Controller中返回自定义的Json类,他说他在修改为Java自封装类型List之后就解决了(其实自定义类完全是可以的,但是需要创建无参构造函数)。因此我进行了尝试,以至于后面改的太乱了,有的地方是JsonResult,有的地方是User,不匹配造成了这个错误。
然后还有一个原因则是我的Feign中缺少一个配置
package com.magus.base.config;import java.util.stream.Collectors;import org.springframework.beans.factory.ObjectProvider;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.autoconfigure.http.HttpMessageConverters;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.HttpMessageConverter;@Configuration public class FeignConfig { @Bean @ConditionalOnMissingBean public HttpMessageConverters messageConverters (ObjectProvider<HttpMessageConverter<?>> converters) { return new HttpMessageConverters (converters.orderedStream().collect(Collectors.toList())); } }
HttpMessageConverters在一些版本中无法被装配,因此我们添加一个Feign的配置类对他进行@Bean装配
错误二
这个错误又难找又逆天,网上也搜罗不到相关的信息,我只能一点点的推测来判断。
@FeignClient(value = "roomProvider") public interface RoomClient { @GetMapping("/rooms/{id}") Room findRoomById (@PathVariable Integer id) ; @GetMapping("/rooms") ArrayList<Room> findAllRooms () ; @GetMapping("/rooms/roomsOfHotel") ArrayList<Room> roomsOfHotel (@RequestParam("hotelId") Integer hotelId) ; }
如果FeignClient中定义的接口需要带参数的话,而且参数本身不是在url上,那么一定要添加注解@RequestParam,不然是无法识别到对应的controller接口的
@RequestMapping("roomsOfHotel") public ArrayList<Room> roomsOfHotel (Integer hotelId) { return roomService.findRoomByHotel(hotelId); }
可以看到我的controller接口名称,返回值,参数名,一切都是吻合的,但是依然要加@RequestParam注解,不然收获一小时调试hhh
查询酒店详细信息 这个函数的实现很好的使用了Feign来跨服务调用
@Override public HotelDetail getDetail (Integer id) { Hotel hotel = hotelMapper.selectById(id); Integer hotelId = hotel.getId(); HotelDetail detail = new HotelDetail (hotel); ArrayList<Room> rooms = roomClient.roomsOfHotel(hotelId); detail.setRooms(rooms); ArrayList<Facility> facilities = facilityClient.facilitiesOfHotel(hotelId); detail.setFacilities(facilities); return detail; }
设计了一个dto来展示酒店详细信息
@Data public class HotelDetail { private String address; private String brand; private String businessDistrict; private Integer id; private String introduction; private String name; private String phone; private Integer stars; ArrayList<Facility> facilities; ArrayList<Room> rooms; public HotelDetail (Hotel hotel) { this .stars = hotel.getStars(); this .phone = hotel.getPhone(); this .name = hotel.getName(); this .introduction = hotel.getIntroduction(); this .id = hotel.getId(); this .businessDistrict = hotel.getBusinessDistrict(); this .brand = hotel.getBrand(); this .address = hotel.getAddress(); } }
订单业务 下单 下单功能的设计和代码编写过程中都遭遇不少困难。
设计 首先是对于库存进行加锁来避免多线程下出现库存负数的情况,在设计方面目前考虑到单服务器部署下使用jvm的synchronized来解决。
其次是异步方面的设计,要求是“将耗时的操作放到后台,而界面可以显示提示信息,并且响应等的不耐烦的用户的取消操作。”
前端设计应该需要用到异步和回调,在响应到达之前进入一个正在下单的界面,并且可以选择取消,响应到达则关闭这个界面。针对此,后端需要设计的只是取消这个功能,所以我在数据库设计方面对订单状态增加了一个下单中的状态OrderStatus.CREATING,订单在插入数据库时默认是这个状态。在用户发送取消的请求后,取消服务的实现方面,我首先会对订单的状态进行查询,因为并不确认此时的下单业务是否完成,订单状态可能处于下单中,下单失败,待支付三种状态。如果处于下单中的状态,将其修改为取消状态,是有问题的,因为代表着下单服务中的检查库存、修改订单状态还没有发生,即使将其设置为取消状态,后续也会被覆盖。
因此我认为比较合理的设计是多次询问订单状态,直至变化为下单失败或待支付,如果多次询问仍为下单中,则抛出取消失败的异常返回给前端。
Feign 在Feign的使用上,之前学习并没有学到参数传递方面的知识点,现在已经弄清楚了。结合上一篇酒店模块中的进行一个整理
路径中的参数 当请求路径中代有参数,并且想要将其提取出来,需要使用@PathVariable注解,同时参数名和类型与Controller中保持一致
FeignClient
@GetMapping("/rooms/{id}") Room findRoomById (@PathVariable Integer id) ;
Controller
@RequestMapping("{id}") public Room findRoomById (@PathVariable Integer id) { return roomService.findRoomById(id); }
方法中的参数 当接口方法中有参数时,使用@RequestParam注解,同时参数名和类型与Controller中保持一致,Controller中不需要@RequestParam注解
FeignClient
@GetMapping("/rooms/roomsOfHotel") ArrayList<Room> roomsOfHotel (@RequestParam("hotelId") Integer hotelId) ;
Controller
@RequestMapping("roomsOfHotel") public ArrayList<Room> roomsOfHotel (Integer hotelId) { return roomService.findRoomByHotel(hotelId); }
方法中自定义类 当接口方法中想要传递可序列化类时,使用@RequestBody注解,同时参数名和类型与Controller中保持一致,Controller中需要@RequestBody注解 ,很重要,一定要加
FeignClient
@GetMapping("/rooms/updateRoom") JsonResult updateRoom (@RequestBody Room room) ;
Controller
@RequestMapping("updateRoom") public JsonResult updateRoom (@RequestBody Room room) { roomService.updateRoom(room); JsonResult result = new JsonResult (OK); result.setMessage("更新房间信息成功" ); return result; }
代码 OrderService @Override public String addOrder (Order order) { LocalDate start = order.getCheckInDate(); LocalDate end = order.getCheckOutDate(); long days = ChronoUnit.DAYS.between(start, end); if (days <= 0 ) { throw new OrderDateException ("预定日期错误" ); } Room room = roomClient.findRoomById(order.getRoomId()); Integer roomNum = order.getRoomNum(); Double price = roomNum * days * room.getPrice(); order.setPrice(price); Date date = new Date (); order.setCreatedAt(date); order.setUpdatedAt(date); order.setStatus(OrderStatus.CREATING.ordinal()); if (orderMapper.insert(order) != 1 ) { throw new AddOrderException ("创建订单失败" ); } String result; if (checkStorage(room.getId(), roomNum)) { order.setStatus(OrderStatus.UNPAID.ordinal()); result = "订单待支付" ; } else { order.setStatus(OrderStatus.FAIL.ordinal()); result = "下单失败,库存不足" ; } if (orderMapper.update(order) != 1 ) { throw new UpdateOrderException ("更新订单状态失败" ); } return result; } @Override public void cancel (Integer id) { Order order = orderMapper.selectById(id); while (order.getStatus() == OrderStatus.CREATING.ordinal()) { order = orderMapper.selectById(id); } if (order.getStatus() != OrderStatus.FAIL.ordinal()) { if (order.getStatus() != OrderStatus.UNPAID.ordinal()) { throw new OrderStatusException ("订单状态错误" ); } Room room = roomClient.findRoomById(order.getRoomId()); room.setNum(room.getNum() + order.getRoomNum()); roomClient.updateRoom(room); } order.setStatus(OrderStatus.CANCEL.ordinal()); if (orderMapper.update(order) != 1 ) { throw new UpdateOrderException ("取消订单失败" ); } } @Override public synchronized Boolean checkStorage (Integer roomId, Integer roomNum) { Room room = roomClient.findRoomById(roomId); if (room.getNum() >= roomNum) { room.setNum(room.getNum() - roomNum); JsonResult result = roomClient.updateRoom(room); return result.getCode() == 200 ; } return false ; }
只能说都能达到想要的效果,但是效率都不够高,多次的访问数据库进行查询就意味着效率的低下。另外就是cancel中询问订单状态应该设置一定的重试次数,使用while循环是有死循环的风险的(我这里图省事了hhh)
总结 前端 前端可以说完全没做,除了flask用了一些别人写好的模板,用了一下bootstrap和jQuery,可以说是很简陋。如果有机会的话想学习一下React,之前有过Vue的入门经历,还是想对这些东西都有个初步的认识(最好都能看懂),为了在以后的前后端沟通中更加方便吧
后端 Java
通过这个项目自主学习和实践了一下SpringCloud的基本用法,包括Nacos,Eureka,GateWay,Feign等。也在实践中理解了一些新的概念,像是负载均衡,反向代理等。
美中不足的是没有做服务熔断降级这一块,原计划是再对Sentinel进行一下学习然后使用到这个项目中的;同时MQ和Redis也没有用到,除了使用JVM处理并发以外的其他方法也没有尝试,像用Zookeeper的锁,用乐观锁悲观锁这些都没有尝试,有机会希望都能把这些知识补上
Python
通过这个项目也是把python开发实践了一下,之前只做过简单的爬虫,或者是小模型的训练,没有做过Web应用这方面的。Flask框架确实很方便,配合jinja2可以很快速的搭建一些简易的平台,在前端中直接使用Python语句确实很爽。但是现在主流还是前后端分离吧,专业的人干专业的事,前端还是掌握的太少了。