1.订单结算页
1.1 根据当前登录用户查询地址列表
需求: 根据当前登录用户查询地址列表 ;
表结构: tb_address
1). Controller
@GetMapping("/list")
public Result<List
//获取当前的登录人名称
String username = tokenDecode.getUserInfo().get(“username”);
//查询登录人相关的收件人地址信息
List addressList = addressService.list(username);
return new Result<>(true,StatusCode.OK,“查询成功”,addressList);
}
2). Service
@Override
public List
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
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 积分日志表有数据 ;