rabbitMQ+redis+fescar+thymeleaf+springTask+springBoot+springCloud(电商平台商品生成订单)

在这里插入图片描述

1.订单结算页
1.1 根据当前登录用户查询地址列表
需求: 根据当前登录用户查询地址列表 ;
表结构: tb_address
1). Controller
@GetMapping("/list")
public Result<List

> list(){
//获取当前的登录人名称
String username = tokenDecode.getUserInfo().get(“username”);
//查询登录人相关的收件人地址信息
List
addressList = addressService.list(username);
return new Result<>(true,StatusCode.OK,“查询成功”,addressList);
}

2). Service
@Override
public List

list(String username) {
Address address = new Address();
address.setUsername(username);
List
addressList = addressMapper.select(address);//精确匹配
return addressList;
}

1.2 页面渲染

在这里插入图片描述
1.2.1 准备工作
1). 引入订单结算页资源
在这里插入图片描述

2). 定义controller
@Controller
@RequestMapping("/worder")
public class OrderController {
@RequestMapping("/ready/order")
public String readyOrder(Model model){
return “order”;
}
}

3). 服务网关路由规则
在这里插入图片描述

在这里插入图片描述

1.2.2 后端代码实现
Feign 接口
@FeignClient(name = “user”)
public interface AddressFeign {

@GetMapping("/address/list")
public Result<List<Address>> list();

}
@RequestMapping("/ready/order")
public String readyOrder(Model model){
//收件人的地址信息
List

addressList = addressFeign.list().getData();
model.addAttribute(“address”,addressList);

//购物车信息
Map map = cartFeign.list();
List<OrderItem> orderItemList = (List<OrderItem>) map.get("orderItemList");
Integer totalMoney = (Integer) map.get("totalMoney");
Integer totalNum = (Integer) map.get("totalNum");

model.addAttribute("carts",orderItemList);
model.addAttribute("totalMoney",totalMoney);
model.addAttribute("totalNum",totalNum);

return "order";

}

1.2.3 页面渲染
1). 收件人地址列表

默认地址

2). 商品清单

  • 7天无理由退货
  • X
  • 有货

3). 总计

件商品,总商品金额 ¥

1.2.4 收件人选择及支付方式选择
在这里插入图片描述

2.下单
2.1 后端实现
1). 流程
在这里插入图片描述

2). 表结构
tb_order : 订单表
tb_order_item : 订单明细表
3). order微服务代码实现
service:
逻辑 :
1). 从redis中获取到购物车列表数据 ;
2). 计算购物车列表数据总金额 , 总数量 ;
3). 组装订单数据, 保存到数据库 ;
4). 组装订单明细数据, 保存到数据库 ;
5). 删除购物车中的商品信息 ;
public void add(Order order){
//1.获取购物车的相关数据(redis)
Map cartMap = cartService.list(order.getUsername());
List orderItemList = (List) cartMap.get(“orderItemList”);

//2.统计计算:总金额,总数量
//3.填充订单数据并保存到tb_order
order.setTotalNum((Integer) cartMap.get("totalNum"));
order.setTotalMoney((Integer) cartMap.get("totalMoney"));
order.setPayMoney((Integer) cartMap.get("totalMoney"));
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
order.setBuyerRate("0"); // 0:未评价  1:已评价
order.setSourceType("1"); //1:WEB
order.setOrderStatus("0"); //0:未完成 1:已完成 2:已退货
order.setPayStatus("0"); //0:未支付 1:已支付
order.setConsignStatus("0"); //0:未发货 1:已发货
String orderId = idWorker.nextId()+"";
order.setId(orderId);
orderMapper.insertSelective(order);

//4.填充订单项数据并保存到tb_order_item
for (OrderItem orderItem : orderItemList) {
    orderItem.setId(idWorker.nextId()+"");
    orderItem.setIsReturn("0"); //0:未退货 1:已退货
    orderItem.setOrderId(orderId);
    orderItemMapper.insertSelective(orderItem);
}

//5.删除购物车数据(redis)
redisTemplate.delete("cart_"+order.getUsername());

}
Controller:
@PostMapping
public Result add(@RequestBody Order order){
//获取登录人名称
String username = tokenDecode.getUserInfo().get(“username”);
order.setUsername(username);
orderService.add(order);
return new Result(true,StatusCode.OK,“添加成功”);
}

4). order渲染服务代码实现
@Autowired
private OrderFeign orderFeign;

@PostMapping("/add")
@ResponseBody
public Result add(@RequestBody Order order){
Result result = orderFeign.add(order);
return result;
}

2.2 前端实现
1). 事件绑定
<a class=“sui-btn btn-danger btn-xlarge” href=“javascript:void(0)” @click=“add()”>提交订单

2). 提交订单
add:function () {
axios.post(’/api/worder/add’,this.order).then(function (response) {
if (response.data.flag){
//添加订单成功
alert(“添加订单成功”);
} else{
alert(“添加订单失败”);
}
})
}
3.下单减库存
3.1 扣减库存接口开发
controller
@PostMapping("/decr/count")
public Result decrCount(@RequestParam(“username”) String username){
skuService.decrCount(username);
return new Result(true,StatusCode.OK,“库存扣减成功”);
}
service
@Override
public void decrCount(String username) {
//1.获取购物车中的数据
List orderItemList = redisTemplate.boundHashOps(“cart_” + username).values();

//2.循环扣减库存并增加销量
for (OrderItem orderItem : orderItemList) {
int count = skuMapper.decrCount(orderItem);
if (count <= 0){
throw new RuntimeException(“库存不足,请重试”);
}
}
}

mapper接口
public interface SkuMapper extends Mapper {
//扣减库存并增加销量
@Update(“update tb_sku set num=num-#{num},sale_num=sale_num+#{num} where id=#{skuId} and num>=#{num}”)
int decrCount(OrderItem orderItem);// 3 --> 3
}

3.2 订单微服务调用接口
1). feign接口定义
@FeignClient(name = “goods”)
public interface SkuFeign {
@PostMapping("/sku/decr/count")
public Result decrCount(@RequestParam(“username”) String username);
}

2). feign拦截器声明
@Bean
public FeignInterceptor feignInterceptor(){
return new FeignInterceptor();
}

3). 调用接口
下订单时, 调用该接口
//6. 调用商品微服务扣减库存, 增加销量
skuFeign.decrCount(order.getUsername());

3.3 提交订单更新用户积分
3.3.1 环境准备
1). 引入分布式事务公共模块

在这里插入图片描述
2). 创建表结构
在这里插入图片描述

3). 安装启动fescar-server
在这里插入图片描述
4)需要添加分布式事务的微服务添加依赖
3.3.2 分布式事务实现

在这里插入图片描述
回滚日志 :

在这里插入图片描述
4.基于MQ来实现分布式事务
基于MQ保证事务的最终一致性 ;
4.1 流程
在这里插入图片描述

4.2 准备工作
1). 表结构
A. 订单微服务 changgou_order
tb_task 任务表
tb_task_his 历史任务表

B. 用户微服务 changgou_user
tb_point_log 积分表

2). 实体类
order : Task , TaskHis 直接拷贝进来即可 ;
user : PointLog 直接拷贝进来即可 ;
3). MQ声明
在这里插入图片描述
5. MQ实现分布式事务
5.1 下单添加任务消息
// 6.生成订单的时候写任务到消息表中
Task task = new Task();
task.setCreateTime(new Date());
task.setUpdateTime(new Date());
// 6.1设置交换机和RoutingKey
task.setMqExchange(RabbitMQConfig.EX_BUYING_ADDPOINTUSER);
task.setMqRoutingkey(RabbitMQConfig.CG_BUYING_ADDPOINT_KEY);
// 6.2因为要进行添加积分,所以把username 订单号,添加的积分添加到task中
// 便于后续获取到添加到积分表中
Map map = new HashMap();
map.put(“username”,order.getUsername());
map.put(“orderId”,orderId);
map.put(“point”,order.getPayMoney());
// 6.3 设置数据到task中
task.setRequestBody(JSON.toJSONString(map));
// 6.4将task任务加入到数据库中
taskMapper.insertSelective(task);

5.2 定时任务查询任务消息发送消息
5.2.1 SpringTask
A. @EnableScheduling --------> 开启定时任务

B. @Scheduled(cron="* * * * * ?") -------------> 声明当前是一个任务调度的方法
域 : 秒 分 时 日 月 周

符号 :

  • : 任意
    ? : 放弃指定
  • : 范围
    / : 每个多长时间执行一次
    , : 枚举
    5.2.2 查询当前时间以前的消息
    @Select("select * from tb_task where update_time< #{currentTime} ")
    @Results({@Result(column = “create_time”,property = “createTime”),
    @Result(column = “update_time”,property = “updateTime”),
    @Result(column = “delete_time”,property = “deleteTime”),
    @Result(column = “task_type”,property = “taskType”),
    @Result(column = “mq_exchange”,property = “mqExchange”),
    @Result(column = “mq_routingkey”,property = “mqRoutingkey”),
    @Result(column = “request_body”,property = “requestBody”),
    @Result(column = “status”,property = “status”),
    @Result(column = “errormsg”,property = “errormsg”)})
    List findTaskLessThanCurrentTime(Date currentTime);

5.2.3 发送消息
/**

  • @PackageName: com.changgou.order.task

  • @ClassName: QueryPointTask

  • @Author: raven

  • @Date: 2020/2/28 20:58

  • @Blame: liunian

  • @Description: 定义一个定时任务,定时查询数据库中是否有新增订单产生的任务数据

  • 如果有数据,则使用rabbitMQ往对应的队列中发送消息
    */
    @Component
    public class QueryPointTask {

    @Autowired
    private TaskMapper taskMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**

    • 定义该方法是一个定时任务的方法
      */
      @Scheduled(cron = “0/2 * * * * ?”)
      public void queryTask(){
      // 1.获取小于当前时间的所有任务数据
      List taskList = taskMapper.findTaskLessTanCurrentTime(new Date());
      // 2.如果task表中的数据不为空。则遍历发送数据到rabbitMQ中
      if (taskList != null){
      for (Task task : taskList) {
      // 3.发送任务到rabbitMQ队列中
      rabbitTemplate.convertAndSend(task.getMqExchange(), task.getMqRoutingkey(), JSON.toJSONString(task));
      System.out.println(“订单服务往新增积分的队列中发送了一条消息”);
      }
      }
      }
      }

实体类中注解说明 :
@Table(name=“tb_task”) //声明实体类与表的映射关系 ----> 操作这个实体类, 就会操作映射的表 ;

@Id // 声明主键

@Column // 声明类中的属性与表中的字段的映射关系;

5.3 用户微服务消息消费
5.3.1 监听消息队列
1). 引入MQ的配置类 RabbitMQConfig
2). 监听消息队列
TIP :
每处理一个添加积分的任务 , 就需要在redis中记录当前正在处理的任务 , 任务处理完成 , 删除redis中的任务标识 ;
避免消息的重复消费 ;
@Component
public class AddPointListener {

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private UserService userService;

@Autowired
private RabbitTemplate rabbitTemplate;
/**
 * 定义监听增加积分的队列
 *
 * @param message
 */
@RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT)
public void receiveManager(String message) {
    // 1.获取传递过来的消息对象
    Task task = JSON.parseObject(message, Task.class);
    //  2.如果消息中的对象为空,或者里面的任务数据没有进行封装则直接返回
    if (task == null || StringUtils.isEmpty(task.getRequestBody())) {
        return;
    }
    // 3.判断redis中是否有数据,如果当前的任务正在执行。查询到value 则直接返回
    Object value = redisTemplate.boundValueOps(task.getId()).get();
    if (value != null) {
        return;
    }
    // 4.更新添加积分数据 如果返回0 则代表添加积分失败
    int result = userService.updateUserPoint(task);
    if (result != 1){
        return;
    }
    // 5.往添加成功的队列中发送一条消息,并且把task返回告诉订单服务,添加订单成功,可以删除任务数据
    rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY,JSON.toJSONString(task));
    System.out.println("用户服务向完成添加积分队列发送了一条消息");
}

}

5.3.2 更新用户积分
逻辑:
A. 判断当前任务是否被操作过(mySQL) , 如果执行过, 直接返回 ;
B. 将任务信息存入Redis , 设置过期时间 ;
C. 修改用户积分 ;
D. 插入用户积分操作日志 mySQL;
E. 删除Redis中记录 ;

代码实现:
@Autowired
private PointLogMapper pointLogMapper;

@Autowired
private RedisTemplate redisTemplate;
/**

  • 用来进行积分的添加

  • @param task

  • @return 只有返回1 的时候证明积分添加成功
    */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public int updateUserPoint(Task task) {
    // 1.取出task任务中封装的积分数据
    String requestBody = task.getRequestBody();
    Map map = JSON.parseObject(requestBody, Map.class);
    String username = map.get(“username”).toString();
    String orderId = map.get(“orderId”).toString();
    String point = map.get(“point”).toString();

    // 2.在数据库中查询是否有所对应的积分数据 ,防止数据被多次消费
    PointLog pointLog = pointLogMapper.findPointLogByOrderId(orderId);
    if (pointLog != null){
    return 0;
    }
    // 3.将数据存进redis并且设置过期时间。防止代码发送异常,redis中数据一值存在。
    // 设置时间为30秒。30秒后redis中的数据过期
    redisTemplate.boundValueOps(task.getId()).set(“exist”,30, TimeUnit.SECONDS);
    // 4.修改用户的积分
    int result = userMapper.updateUserPoint(username, point);
    // 返回的结果是数据影响的行数,如果影响的行数小于等0 返回 结果 意味着修改用户积分失败
    if (result <= 0){
    return result;
    }
    // 5. 将修改用户积分的操作存入到所对应的操作数据库pointLog中
    pointLog = new PointLog();
    pointLog.setUserId(username);
    pointLog.setOrderId(orderId);
    pointLog.setPoint(Integer.parseInt(point));
    result = pointLogMapper.insertSelective(pointLog);
    if (result <= 0){
    return result;
    }
    // 6.积分添加完毕,删除redis中的数据
    redisTemplate.delete(task.getId());
    System.out.println(“用户服务完成了添加积分的操作”);
    return 1;
    }

理解两点:
1). 为什么在redis中判定一次, 还需要在MySQL中判定一次 ?
redis中记录的是正在执行的任务 , MySQL中记录的是已经执行完毕的任务 ; 由于生产者是通过定时任务来扫描消息任务表发送消息, 为了避免消息的重复消费, 所以需要判定 ;
2). 为什么往redis中存储正在执行任务的时候,需要设置过期时间?
避免更新积分或插入日志操作时, 抛出异常, redis中的key 不能及时删除 ; 如果设置了过期时间, 及时更新积分或插入日志操作时, 抛出异常, 过期时间到达之后, redis中的任务也可以删除, 不影响任务的继续执行
6 订单服务监听消息删除任务消息
逻辑 :
监听消息队列, 接收消息
插入历史任务消息
删除消息任务
代码实现:
@Component
public class DelTaskListener {

@Autowired
private TaskService taskService;

@RabbitListener(queues = RabbitMQConfig.CG_BUYING_FINISHADDPOINT)
public void receiveMessage(String message){
    // 1.获取到消息数据
    Task task = JSON.parseObject(message, Task.class);
    if (task != null){
        // 2.调用taskService删除任务表中的数据
        taskService.delTask(task);
    }
}

}

@Service
public class TaskServiceImpl implements TaskService {

@Autowired
private TaskHisMapper taskHisMapper;

@Autowired
private TaskMapper taskMapper;
/**
 * 根据主键删除task表的数据,并且添加到历史任务中
 * @param task
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void delTask(Task task) {
    // 1.设置删除数据时间
    task.setDeleteTime(new Date());
    // 2.获取task的id 为类的copy做准备
    Long taskId = task.getId();
    // 3.将task的主键设置为null 因为历史任务的主键为自增id
    task.setId(null);
    // 4.copy task到 taskHis
    TaskHis taskHis = new TaskHis();
    // 使用beanUtils进行copy必须确保属性完全一致
    BeanUtils.copyProperties(task,taskHis);
    // 5.记录历史任务数据
    taskHisMapper.insertSelective(taskHis);
    // 6.删除原来的task任务数据
    task.setId(taskId);
    taskMapper.deleteByPrimaryKey(task);
    System.out.println("订单服务完成了添加历史任务并删除原有任务的操作");
}

}

BeanUtils.copyProperties(task,taskHis);// 保证 source中属性名, 与target中的属性名一致;
7 测试
1). 启动的服务
虚拟机docker ;
fescar-server
在这里插入图片描述

2). 测试结果验证
tb_task 任务消息表无数据 , 表名任务已经执行完了 ;
tb_task_his 任务消息历史表有数据 ;
tb_point_log 积分日志表有数据 ;

发布了120 篇原创文章 · 获赞 9 · 访问量 7349

猜你喜欢

转载自blog.csdn.net/weixin_44993313/article/details/104583512