《Spring Boot 实战派》--15.实现一个类似“京东”的电子 商务商城
第15章 实现一个类似"京东"的电子商务商城
为了综合使用本书讲解的Spring Security、Redis、RabbitMQ. JPA、JWT技术,本章通 过实例来整合这些技术。
本章首先讲解如何整合管理系统和会员系统实现多用户系统;然后讲解如何实现会员系统的多 端、多方式注册和登录;最后讲解如何实现购物、下单、秒杀,以及订单自动取消功能。
本实例的源代码可以在,715/JD_Demo”目录下找到。
15.1用Spring Security实现会员系统
15.1.1实现会员实体
由于会员分为多种类型,因此需要创建会员角色表。也可以直接在会员表中加上角色字段来体 现,字段值可以是vipO、vip1、vip2。我们这里采用的是关联角色表。
1.实现会员实体
会员实体需要继承UserDetails接口来实现功能,然后关联角色表,见以下代码:
@Entity //@Table(name 二"user") 〃设置对应表名字 public class User implements UserDetails { 〃主键及自动增长 @ld //IDENTITY代表由数据库控制主键自増,auto代表程序统一控制主键自增 @GeneratedValue(strategy = GenerationType. IDENTITY) private long id; @NotEmpty(message ="昵称不能为空”) @Size(min = 1, max = 20) @Column(nullable = false, unique = true) private String name; @Column(nullable 二 false, unique = true) private String email; @Column(nullable = false, unique 二 true) private String mobile; private String password; private Integer active; @Column(nullable = true) private Long createTime; @Column(nullable = true) private Long lastModifyTime; @Column(nullable = true) private String outDate; //多对多映射,用户角色 @ManyToMany(cascade = (CascadeType.REFRESH), fetch = FetchType. EAGER) private Listroles;
2.实现会员角色实体
角色实体用于储存会员的角色信息,见以下代码:
?Entity /*@Table(name = "user_role")*/ public class UserRole { @ld @GeneratedValue private long id; private String rolename; private String cnname; 〃省略 }
15.1.2实现会员接口
这里采用了 JPA的自定义查询功能,并实现了自定义更新功能,见以下代码:
public interface UserRepository extends JpaRepository( User findByName(String name); User findByMobile(String mobile); User findByEmail(String email); *根据id集合查询用户,分页查询 * *@param ids *@return */ Page findByldln(List ids, Pageable pageable); /** *根据id集合查询用户,不分页 * *@param ids *@return 7 List findByldln(List ids); @Modifying(clearAutomatically=true) ?Transactional @Query("update User set outDate=:outDate, validataCode=:validataCode where email=:email") int setOutDateAndValidataCode(@Param("outDate") String outDate, @Param("validataCode") String validataCode, @Param("email") String email); @Modifying(clearAutomatically=true) ?Transactional @Query("update User set active=:active where email=:email") int setActive(@Param("active") Integer active, @Param("email") String email); )
15.1.3实现用户名、邮箱、手机号多方式注册功能
由于在注册时所有接口都要求用户填写用户名,因此这里不需要再编写用户名注册的接口,只 需要编写邮箱和手机号注册接口。
1.实现邮箱注册接口
如果需要验证注册的邮箱,则可以开启邮件验证功能,并用异步方式发送验证邮件到用户的 邮箱。
这里依然要实现密码加密,并需要从数据库中查询用户名、E-mail是否已经被注册。具体实现 见以下代码:
@Autowired AsyncSendEmailService asyncSendEmailService; @ResponseBody @RequestMapping(value = "/register/email", method = RequestMethod.POST) public Response registByEmail(User user) ( t「y{ User registUser = userRepository.findByEmail(user.getEmail()); if (null != registUser) ( return result(ExceptionMsg.EmailUsed); ) User userNamellser = userRepository.findByName(user.getNameO); SysUser admingusemame = adminUserRepository.findByName(user.getNameO); if (null != userNameUser || null != admingusemame) ( return result(ExceptionMsg.UserNameUsed); } BCryptPasswordEncoder encoder =new BCryptPasswordEncoder(); user.setPassword(encoder.encode(user.getPassword())); user.setCreateTime(DateUtils.getCurrentTimeO); user.setLastModifyTime(DateUtils.getCurrentTimeO); user.setProfilePicture("img/favicon.png"); Listroles = new ArrayList<>(); UserRole rolel = userRoleRepository.findByRolename("ROLE_USER"); roles.add(rolel); user.setRoles(roles); userRepository.save(user); 〃用异步方式发送邮件到用户邮箱,以验证用户邮箱的真实性或正确性 asyncSendEmailService.sendVerifyemail(user.getEmailO); ) catch (Exception e) ( //logger.error("create user failed,", e); return result(ExceptionMsg.FAILED); } return result(); }
2,实现手机号注册接口
手机号注册和邮箱注册的功能差不多,这里只是更改为验证手机号是否已经被注册。我们并没 有提供实现验证码的功能,验证码功能需要购买相应的通信服务商提供的接口(同时也会提供 Demo )□具体实现见以下代码:
@ResponseBody @RequestMapping(value = "/register/mobile", method = RequestMethod.POST) public Response regist(User user) ( try { User userNameUser = userRepository.findByName(user.getNameO); SysUser admingusemame = adminUserRepository.findByName(user.getNameO); if (null != userNameUser || null != admingusemame) ( return result(ExceptionMsg.UserNameUsed); ) User userMobile = userRepository.findByMobile(user.getMobile()); if (null != userMobile) ( return result(ExceptionMsg.MobileUsed); } 〃省略 }
15.1.4实现用RabbitMQ发送会员注册验证邮件
发送邮件可以使用JavaMailSender类,这里使用RabbitMQ来异步调用JavaMailSender类。
1、创建RabbitMQ配置类
下面创建配置类,用于配置队列,见以下代码:
?Configuration public class RabbitConfig ( @Bean public Queue regQueue() { return new Queue("reg_email"); } )
2、创建RabbitMQ监听器
下面创建监听器,以监听用户注册后的通知,见以下代码:
?Component @RabbitListener(queues 二"reg_email")//监听消息队列 public class RegEmailQueueReceiver { @RabbitHandler//@RabbitHandler 来实现具体消费 public void QueueReceiverfString reg_email) ( try( //send email System.out.println("Receiver:" + reg_email); ) catch (Exception e) ( e.printStackTrace(); } } }
3、注册控制器中写入RabbitMQ的发布者
在用户注册时,如果填写的信息都验证通过,贝"执行RabbitMQ的消息发送方法让RabbitMQ 消息的接收者接收消息,然后异步发送邮件,具体实现见以下代码:
@Autowired private AmqpTemplate rabbitTemplate; @ResponseBody @RequestMapping(value = "/register/email", method = RequestMethod.POST) public Response registByEmailfUser user) ( try( //此处省略部分上面的邮箱注册代码 rabbitTemplate.convertAndSend("reg_emair', user.getEmail());
15.1.5实现用户名、邮箱、手机号多方式登录功能
1、实现用户名、手机号、邮箱三种方式登录验证
用户的登录功能,需要继承UserDetailsService接口,然后加入登录判断。通过用户输入的 信息来判断数据库中是否存在匹配的用户名和密码对、手机号和密码对、邮箱和密码对,见以下代 码:
//@Service public class UserSecurityService implements UserDetailsService ( @Autowired private UserRepository userRepository; ?Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException ( User user = userRepository.findByName(name); if (user == null) ( User mobileUser = userRepository.findByMobile(name); if (mobileUser == null) ( User emailUser= userRepository.findByEmail(name); if(emailUser==null) ( throw new UsemameNotFoundException("ffi户名邮箱手机号不存在!”); } else( user=userRepository.findByEmail(name); } ) else ( user = userRepository.findByMobile(name); ) ) else if("locked".equals(user.getStatus())) (〃被锁定,无法登录 throw new LockedException("用户被锁定”); ) return user; ) )
2, 在安全配置类中配置自定义的UserDetailsService
下面配置自定义的UserDetailsService,以便进行验证和授权,见以下代码:
@Bean UserDetailsService UserService() { 。 return new UserSecurityServiceO; }
3.重写密码验证机制
下面重写密码机制来判断用户系统使用的加密方式。可以和后台系统使用一样的加密方式,也 可以自定义。可以看出,Spring Security非常灵活。具体实现见以下代码:
?Override protected void configure(AuthenticationManagerBuilder auth) throws Exception ( auth.userDetailsService(Service()).passwordEncoder(new BCryptPasswordEncoder() ( }); }
15.2整合会员系统(Web、APP多端、多方式注册登录)和后台系统
在移动网络时代,不仅需要提供Web端的用户注册/登录,还需要提供APP端的用户注册/ 登录。
如果要同时支特Web端和APP端注册/登录,则需要考虑以下几点:
?需要把Web有状态的验证和APP无状态的验证整合起来。
?支持多方式注册/登录,如,用户名+密码、手机号+密码、邮箱+密码。
?用户不需要区分APP端或Web端,两端需要共用一个会员系统,只是验证和授权方式不同。
?整合Web端和APP端用户验证和授权的安全配置
上面几点要求,如果自己实现稍微复杂,而使用Spring Security则非常简单。要整合在一起, 只需整合安全配置文件即可。具体配置见以下代码:
@Configuration//Ja 定为配置类 @EnableWebSecurity//指定为 Spring Security 配置类 @EnableGlobalMethodSecurity(prePostEnabled = t「ue) //启用方法安全设置 //@EnableGlobalAuthentication public class MultiHttpSecurityConfig ( ?Configuration @Order(1) public class WebSecurityConfigForAdmin extends WebSecurityConfigurerAdapter ( 〃配置后台安全设置 ) ?Configuration @Order(2) public class WebSecurityConfigForUser extends WebSecurityConfigurerAdapter ( 〃配置会员安全设置 } ?Configuration @Order(3) public class WebSecurityConfig3 extends WebSecurityConfigurerAdapter ( 〃配置会员JWT安全设置 ) ?Configuration @Order(4) public class WebSecurityConfig4 extends WebSecurityConfigurerAdapter ( 〃配置静态文件或其他安全设置 }
从上述代码可以看出,各系统的安全配置依然继承WebSecurityConfigurerAdapter接口,完 成单独的安全配置后,通过注解?Order来指定加载顺序。
如果前面的配置项包含了后面配置项的URL,则可能导致后面的验证不会生效,所以 需要注意各配置的http.antMatcher("URL")的URL值。
15.3 实现购物系统
15.3.1设计数据表
1. 了解功能需求
在实现购物系统前,需要明确以下需求。
(1) 将商品加入购物车,是否需要登录。
?可以在未登录前将购物车数据存储在Cookie中。因为所有对购物车的操作都是操作本地 Cookie,这样可以一定程度上降低服务器的数据库压力。结算时才提示登录,登录之后就可 以获取用户id,把本地的购物车数据追加到登录的id上。这样的缺点是:换一台电脑后购物 车的商品不能同步。
?登录之后才能有添加购物车功能的好处是:可以实时进行统计、分析用户购物行为,并根据 用户历史喜好向用户推送相关的产品。但是,这对于新用户可能不太友好,因为没有产生购 买欲之前就要有烦琐的填写资料注册、登录的操作。
(2) 计算购物车中商品的总价,当商品数量发生变化时需要重新计算。
(3) 用户可以删除购物车中的商品。
(4)用户下单后,删除购物车里的数据。
2.设计数据表
在了解需求之后就可以开始设计数据表。商品信息需要在数据库中标准化,以准确描述商品的 标准化最小单元,还要考虑库存计量单位,比如件、份、箱、本等。有些商品的特性并不是规范的, 所以还要建立一个规则表,用于存储产品的规则。
举个例子,一部华为P30手机是一个标准化的商品,“部”是它的最小库存计量单元,它的颜 色和容量需要建立一个单独的规则表来存放。当然,还有更复杂的分类、品牌、产地、用户群等 属性。
本章建立一个相对简单购物系统表,包含产品表、订单表、购物车表。具体建表方法不再讲解, 如果不会,请见本书的第8章。
15.3.2实现商品展示功能
1、在控制器中实现展示功能
产品的展示功能相对简单,只是需要获取用户信息。当用户有添加购物车动作时,会把商品和 相应的用户信息提交到购物车表中,同时结算时也需要用户信息,见以下代码:
@GetMapping("(id}") public ModelAndView showProduct(@PathVanable("id") long id) throws Exception( Product product = productRepository.findByid(id); ModelAndView mav = new ModelAndView("web/shop/show"); 〃产品信息 mav.addObject("product", product); 〃获取登录用户信息 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); mav.addObject("principals", principal); System.out.println(principal.toStringO); return mav; }
2、设计视图模板
下面代码是在视图中展示商品,并实现加入购物车功能。
产品 ID:
产品名称:
产品价格:〈span th:text=="$(product.price}"x/span>