《Spring Boot 实战派》--15.实现一个类似“京东”的电子 商务商城


第15章 实现一个类似"京东"的电子商务商城

为了综合使用本书讲解的Spring Security、Redis、RabbitMQ. JPA、JWT技术,本章通 过实例来整合这些技术。

本章首先讲解如何整合管理系统和会员系统实现多用户系统;然后讲解如何实现会员系统的多 端、多方式注册和登录;最后讲解如何实现购物、下单、秒杀,以及订单自动取消功能。

本实例的源代码可以在,715/JD_Demo”目录下找到。

15.1Spring 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 List roles;

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");
List roles = 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整合会员系统(WebAPP多端、多方式注册登录)和后台系统

在移动网络时代,不仅需要提供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>

“加入购物车”按钮实际上是提交表单动作。

15.3.3实现购物车功能

1、实现保存购物车数据功能

下面代码是用于处理获取到的商品id和用户ido

@PostMapping("")
public String save(Cart cart) throws Exception(
cartRepository.save(cart);
return "redirect:/cart/?user_id="+cart.getUser_id();
)

2、自定义JPA原生查询接口

为了限定用户只能查看自己购物车中的商品,在接口处自定义了根据用户id查询数据的原生 SQL,见以下代码:

public interface CartRepository extends JpaRepository{
Cart findByid(long id);
@Query(value = "select * from cart c where c.user_id=:user_id", nativeQuery = true) ListfindCartByldNative(@Param("userJd")long userjd);
}

3、添加商品之后跳转到购物车列表页面

下面代码是添加商品之后跳转到购物车列表的页面。"@PreAuthorize("principal.id.equals (#user_id)") ”限制用户只能查看自己的购物车数据。

@GetMapping("")
*购物车不用分页,可以用list
* @PreAuthorize("principal.id == #user_id")
*必须要限制登录,否则会报错 Failed to evaluate expression 'principal.id.equals(#user_id)' */
@PreAuthorize("principal.id.equals(#user_id)")
public ModelAndView cartlist(Long userjd, Principal principal) (
List cartList = cartRepository.findCartByldNative(userJd);
ModelAndView mav = new ModelAndView("web/shop/cart/list"); mav.addObject("cartList", cartList);
return mav;
}

15.3.4用Redis实现购物车数据持久化

如果会员数量庞大,则会存在大量的频繁对购物车数据进行増加、删除和修改的操作。如果使 MySQL进行存储,则效率会很低。所以在实际的应用中,需要根据用户群体规模,考虑是否使 Redis对购物车数据进行存储。

1、了解需求

如果使用Redis,则需要考虑存储哪些数据。Redis是内存高速缓存,不可能把所有数据都存 储。所以,对于购物系统,可以利用Redis来缓存用户登录数据、用户最近浏览的商品、加入购物 车的商品、高访问量页面、秒杀页面。

将用户和购物车信息都存储在Redis里,可以提高反应速度。

很多大型商城都提供了这样的功能:“在查看过这件商品的用户当中,有X%的用户最终购买了 这件商品,购买了这件商品的用户也购买了下面的商品”。这些信息可以帮助用户查找其他相关的商 品,提升网站的销售业绩。这些数据可以存储在Redis中,也可以通过异步方式来加载。

2、购物车加入Redis缓存功能

在实际使用中,可以用Map存储登录用户,用无序集合存储最近登录的用户、最近被浏览的 item,但幵发人员可以根据自己的喜欢或特定情况去设计。

这里用Map存储用户的购物车数据,主要代码如下:

@PostMapping("")
public String save(Cart cart) throws Exception(
cartRepository.save(cart);	-
〃获取保存后的数据表自增id
Long id=cart.getld();
〃用Redis存储购物车数据
Map hashMap = new HashMapO;
/**
*?Description:产品 id
7
hashMap.put("ProductJd", cart.getProduct_id());
/**
*?Description:产品名称
7
hashMap.put("Product_name", cart.getProduct_name());
/**
*?Description:用户 id
7
hashMap.put("User_id", cart.getUser_id());
/**
*?Description:购买数量
7
hashMap.put("Product_num", cart.getProduct_num());
/**
*?Description:购物车对应的MySQL主键id
*/
hashMap.put("Cart_id", id);
JSONObject itemJSONObj 二 JSONObject.parseObject(JSON.toJSONString(hashMap));
System.out.println(itemJSONObj);
String valueStr = JSONObject.toJSONString(itemJSONObj);
long timestamp = System.currentTimeMillis()/1000;
/**
*?Description:键
7
String sname=cart.getUser_idO-toString(); redisTemplate.boundZSetOps(sname).add(valueStr,timestamp);
System.out.println(redisTemplate.opsForZSet().range(sname,0,-1));
System.out.println(redisTemplate.opsForZSet().size(sname));
return ,Tedirect:/cart/?userJd="+cart.getUser_id();
}

Redis存储的是购物车数据,如图15-1所示。

从图15-1可以看出,Redis中存储的购物车数据包含商品id、商品名称、购买的商品数量、 用户的id、MySQL储存的数据(商品)主键值。

如果要限制购物车商品最大数量,则可以用“opsFo『ZSet().size”获取当前储存的数据量,以 判断是否可以新添加商品到购物车。

在用Spring Boot操作Redis时,key值可能出现“\xAC\xED\x00\x05t\x00T”之类的值,这 是由序列化方式所导致的,不影响程序读写。redisTemplate默认的序列化方式为JdkSerializeable,

StringRedisTemplate的默认序列化方式为StringRedisSerializer,可以通过手动配置将 redisTemplate的序列化方式进行更改。

 

15.4Redis实现分布式秒杀系统

15.4.1实现抢购功能,解决并发超卖问题

1.分析功能需求

抢购或秒杀活动是商城很常见的一个应用场景,这个功能主要需要解决高并发竞争下产生的超 卖错误。

我们通过一个实例来理解超卖问题。

假设库存是100个单位,现在已经有99个人抢购成功,又来了 10个用户同时抢购。因为这时 查询到库存等于1,满足执行生成订单的条件,所以这10个用户都能抢购成功,但实际上只剩下1 件库存可以抢了,这样就存在了 9个“超”的问题。

由此可见,在高并发下,很多看似设计编码逻辑合理的流程都可能会出现问题。要解决“超” 问题,核心在于:保证检查库存时的操作是依次执行的。应使用“单线程”任务,这样即使有很多 用户同时请求下单操作,也需要一个个排队检查条件,然后进行处理。

这里可以使用Redis的原子操作来实现这个“单线”。用Redis实现秒杀功能,可以这样 设计:

(1)把库存数据存在Redis的列表(list)中。假设有10件库存,就往列表中添加(Push)10个数,每个数代表1件库存。

(2) 抢购幵始后,接收用户提交,然后从列表中移除(Pop方法)出一个数,表示用户抢购 成功。

(3) 用Redis创建另一个列表,用于存放下单成功的用户的数据。

(4) 如果列表为空,则表示已经被抢光了,返回提示消息。

因为列表的移除(Pop方法)操作是原子的,即使有很多用户同时到达,也是依次执行的。

2、创建秒杀库存列表

在抢购开始前,先把库存放入Redis的列表(list)中,见以下代码:

@GetMapping("createSeckillStockCount")
public void seckillStockCount() (
〃有10个库存
Integer count = 10;
//添加到Redis列表中
for (Integer i = 0; i < count; i++) (
redisTemplate.opsForList().leftPush("slist", 1);
)
System.out.println(redisTemplate.opsForList().range("slist", 0, -1));
}

3、实现抢购处理功能

以下代码可以实现抢购处理功能,关键是列表的Pop操作来保证原子性。在秒杀完全结束后, 还需要同步Redis数据到数据库。

@GetMapping("seckill")
public void seckillQ {
〃判断计数器
if (redisTemplate.opsForList().leftPop("slist").equals(1)) (
〃抢购成功的用户id,这里简单的设置为1个
long userjd = 1903;
redisTemplate.opsForList().leftPush("ulist", userjd);
)
System.out.println(redisTemplate.opsForList().range("slist", 0, -1));
System.out.println(redisTemplate.opsForList().range("ulist", 0, T));
)

4、AB测试

这里用Apache的AB测试工具来测试模拟高并发情况。要使用Apache的AB测试,需要先 下载它的安装包。进入Apache官网下载Apache,然后解压文件。进入解压目录,在地址栏输入

“cmd”命令,按Enter键,打开cmd命令窗口,输入以下命令,如图15-2所示

ab -n 1000 -c 100 http://localhost:8080/seckill/seckill

其中,-n表示请求数,-c表示并发数即可进行并发压力测试。

在进行多次测试后,发现每次的测试结果都一样,不存在“超"问题,如图15-3所示(1903 是用户id )

对于更复杂的秒杀系统还可以考虑以下办法:

?提前预热数据,把秒杀信息用Redis保存。

?把商品列表用Redis的list数据类型保存。

?把商品的详情数据用Redis hash保存,并设置过期时间。

?把商品的库存数据用Redis sorted set保存。

?把用户的地址信息用Redis set保存。

?把订单产生扣库存操作通过Redis制造分布式锁,库存同步扣除。

?把订单产生后的发货数据用Redis的list保存,然后通过消息队列处理。

?在秒杀结束后,再把Redis中的数据和数据库进行同步。

 

15.4.2缓存页面和限流

秒杀功能是本实例的核心功能。为保证其正确性,不岀现超卖情况之后,还要考虑秒杀前出现 的大量访问,大量的参与者涌入,如果不(好应对措施,则可能会导致服务器资源枯竭。可以采取 下几种办法。

1、缓存或静态化秒杀的页面

需要对秒杀页面进行缓存。因为在秒杀时大量用户会访问秒杀页面,导致请求暴涨,如果这个 页面存在数据库的I/O操作,则会严重影响性能。可以采取静态化或使用Redis来进行缓存。

2、实现限流

可以通过验证码和预约功能进行限流,让访问不要过于集中爆发。比如,会员在登录时输入验 证码的过程就拖长了服务的时间。而预约功能让会员提前进行预约,如果没有预约,则不能参与秒 杀。这里对于没有强烈购买需求,仅仅是凑热闹的会员是一种过滤,能降低无效的参与量。

3、秒杀地址隐藏

在秒杀设计中,隐藏秒杀地址也是重要的手段,以防止大量“肉鸡”的自动化抢购。在秒杀前 隐藏真实的秒杀地址或参数,可以适当防止黑客事先调试机器,在秒杀开始后,通过大量“肉” 来参与秒杀。

除这些手段外,还可以加入其他办法,比如IP限制、账户等级限制等。但真实的应用场景会比 现在讲解得要复杂,比如如何防止“應羊毛”群体、如何保证公平抢购、如何防止大量的DDOS 击等问题。

 

15.5RabbitMQ实现订单过期取消功能

购物系统会遇到很多下单之后不支付、不完成订单的情况。如果库存有限,则会影响其他用户 购买。所以,需要对订单的有效期进行限定,并对过期订单进行自动处理。

可以用RabbitMQ实现消息队列延迟功能。延迟队列存储的是对应的延时消息。"延时消息” 是指,消息被发送以后,消费者不会立即拿到消息,而是等到指定时间(条件)后,消费者才拿到 这个消息进行消费。

网上购物时经常遇到这种情况:商城要求下单后30分钟内完成支付,到时间没有支付的订单会 被取消,库存会被释放。这种功能大多是通过延迟队列来完成的一一将订单信息发送到延时队列中。

也可以根据业务场景使用其他方式,比如死信、计时器、定时任务。具体实现见下面步骤。

(1) 配置消息延迟队列,见以下代码:

/**
*订单取消
7
@Bean
public CustomExchange delayExchange() (
Map args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange("delayed_exchange", "x-delayed-message", true, false, args); }
@Bean
public Queue queue() (
Queue queue = new Queue("delay_queue_1", true);
return queue;
}
@Bean
public Binding bindingO (
return BindingBuilder.bind(queue()).to(delayExchange()).with("delay_queue_1 ").hoargs();
}

(2) 编写延迟队列发送服务。这里设置延迟10s,见以下代码:

?Service
public class CancelOrderSender (
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String queueName, Integer msg) (
SimpleDateFormat sdf = new SimpleDateFormatC'yyyy-MM-dd HH:mm:ss"); System.out.println("Sender:" + sdf.format(new Date()));
rabbitTemplate.convertAndSend("delayed_exchange", queueName, msg, new MessagePostProcessor() (
?Override
public Message postProcessMessage(Message message) throws AmqpException {
//限定延迟时间
int delay_time = 100000;
message.getMessageProperties().setHeader("x-delay", delay_time); return message;
}
});
)

(3) 编写延迟队列接收服务,见以下代码:

?Component
public class CancelOrderReceiver (
@RabbitListener(queues 二"delay_queue_1") public void receive(String msg) {
SimpleDateFormat sdf 二 new SimpleDateFo「mat(”yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()));
System.out.println("Receiver:执行取消订单"+msg);
〃省略取消订单和增加库存代码
}
}

(4) 发送订单取消通知。

客户下单成功后,需要立即执行延迟队列,见以下代码:

@Autowired
private CancelOrderSender cancelOrderSender;
int orderld = 1;
@GetMapping("/customSend")
public void send() (
cancelOrderSender.sendMsg("delay_queue_1", orderld);
}

15.6实现结算和支付功能

15.6.1实现结算生成订单功能

购物车和秒杀系统初步完成之后,接下来需要构建结算支付系统。用户单击“结算”按钮后生 成订单,订单一般包含商品id、购买数量' 购买价格(商品的价格信息会变动,所以结算的价格要 放入订单表)、总价等,然后根据订单信息跳转到支付页面进行付款,当付款状态确认完成后完 成订单。

这个功能就是很简单的增加数据功能,见以下代码:

@RequestMapping(value = "/createOrder")
public String createOrder(Order order) throws Exception {
Product p = productRepository.findByid(order.getProduct_id());
order.setStatus(true);
〃价格信息要从库中获取,否则可能被黑客伪造。如果被用于购买虚拟商品,就损失巨大,因为虚拟商品一般自动 发货
〃为便于演示,这里获取的是一个商品的价格 〃对于多个商品价格的叠加请读者自行编写
order.setAmount(p.getPriceO); orderRepository.save(order);
〃获取保存到数据库后该数据在数据表的自增id
Long order_id = order.getldQ;
〃传递给支付页面的值
return "redirect:/pay/?order_id=" + orderjd;

这里要注意的是,一定不要从用户发送的数据中获取价格来进行计算,而是要根据后台的价格 来进行计算,否则可能会遭遇黑客伪造数据的情况。

在订单创建完成后,跳转到支付接口进行支付,当支付成功后,自动确认订单(也可以由管理 员手动确认)o

15.6.2集成支付

现在的主要支付方式有银联、微信和支付宝等,它们的集成方式都差不多。查看官方文档和演 示代码(Demo)就可以轻松集成它们。这里以集成支付宝支付为例介绍集成的流程。

(1) 访问“https://open.alipay.com”注册为商家并认证。

(2) 进入“文档中心”页面,在“开发工具”下找到“SDK&DEMO",单击进入页面,下载 Java 版的 SDK Demo。

(3) 将SDK加入项目中。

(4) 在“文档中心”页面的“开发工具”下找到“签名工具”,单击进入页面,下载RSA签名 验签工具,生成公钥、私钥并保存。

  生成的私钥需妥善保管,避免遗失或泄露,因为私钥需填写到代码中供签名时使用。

(5)开发接口。

这里是开发环境,所以采用使用沙箱环境,上线后会使用真实环境。

将支付宝的一些参数放到配置文件alipay-dev.properties里,它的配置方式有两种:

?通过配置文件配置参数。

?创建配置类。官方Dem。采用的方法是:先定义AlipayConfig类,然后将其全部定义成静 态变量。在实际的项目中,可以直接复制官方Dem。中的参数,然后根据需要进行修改。

(6)创建表。

根据业务需求,需要创建支付记录表来记录支付记录,还需要记录支付过程产生的日志信息。

(7 )与支付宝对接交互。

将本地生成的订单信息提交给支付宝,在支付宝处理完支付后,会通过异步方式获取用户给支 付宝反馈的信息,确定是否支付成功。在开发过程中应注意支付确认(异步)的逻辑处理。

支付宝的集成比较简单,按照官方文档来集成即可,一般比较顺利。

不过一定要注意签名,有时即使是与文档是一模一样的,也可能存在签名不对的情况,因为 orderinfo的拼接顺序跟签名的顺序有时是不一样的。

如需集成其他支付方式,请查阅其官方文档,并下载官方提供的Demo进行测试,这里不再 讲解。