*谷粒随享**
学习目标:
购买包含分为:
在新用户第一次登录的时候,就进行了初始化账户余额信息操作!
当刷新主页的时候,会加载当前余额数据。
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/93
service-account
微服务中UserAccountApiController控制器
package com.atguigu.tingshu.account.api;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.cache.GuiGuCache;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@Tag(name = "用户账户管理")
@RestController
@RequestMapping("api/account")
@SuppressWarnings({"all"})
public class UserAccountApiController {
@Autowired
private UserAccountService userAccountService;
/**
* 获取当前登录用户账户可用余额
*
* @return
*/
@GuiGuLogin
@Operation(summary = "获取当前登录用户账户可用余额")
@GetMapping("/userAccount/getAvailableAmount")
public Result<BigDecimal> getAvailableAmount() {
//获取当前登录用户ID
Long userId = AuthContextHolder.getUserId();
//调用业务逻辑获取账户可用金额
BigDecimal availableAmount = userAccountService.getAvailableAmount(userId);
return Result.ok(availableAmount);
}
}
UserAccountService接口:
/**
* 获取当前登录用户账户可用余额
*
* @param userId 用户ID
* @return
*/
BigDecimal getAvailableAmount(Long userId);
UserAccountServiceImpl实现类:
/**
* 获取当前登录用户账户可用余额
*
* @param userId 用户ID
* @return
*/
@Override
public BigDecimal getAvailableAmount(Long userId) {
//1.根据用户ID查询账户记录
LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserAccount::getUserId, userId);
UserAccount userAccount = userAccountMapper.selectOne(queryWrapper);
Assert.notNull(userAccount, "账户为空!");
//2.获取账户可用金额
return userAccount.getAvailableAmount();
}
在首页点击专辑封面标识为:VIP免费 会出现访问VIP服务配置管理接口
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/85
在service-user
微服务模块中的 VipServiceConfigApiController 控制器添加映射路径,返回 VipServiceConfig 实体类的集合数据。也就是查询vip_service_config表中的集合数据。
package com.atguigu.tingshu.user.api;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.service.VipServiceConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "VIP服务配置管理接口")
@RestController
@RequestMapping("api/user")
@SuppressWarnings({"all"})
public class VipServiceConfigApiController {
@Autowired
private VipServiceConfigService vipServiceConfigService;
/**
* 根据VIP套餐ID查询套餐详情
* @param id
* @return
*/
@Operation(summary = "根据VIP套餐ID查询套餐详情")
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id){
VipServiceConfig serviceConfig = vipServiceConfigService.getById(id);
return Result.ok(serviceConfig);
}
}
在service-util
微服务中添加远程 feign 远程调用拦截器,来获取token 数据。
如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。
package com.atguigu.tingshu.common.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class FeignInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate){
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步编排 与 MQ消费者端 为 null 避免出现空指针
if(null != requestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String token = request.getHeader("token");
requestTemplate.header("token", token);
}
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/90
service-user
微服务的UserInfoApiController控制器
/**
* 判断用户是否购买过指定专辑
*
* @param albumId
* @return
*/
@Operation(summary = "判断用户是否购买过指定专辑")
@GuiGuLogin //获取当前登录用户ID
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
Boolean isPaid = userInfoService.isPaidAlbum(userId, albumId);
return Result.ok(isPaid);
}
UserInfoService接口
/**
* 判断用户是否购买过指定专辑
* @param userId
* @param albumId
* @return
*/
Boolean isPaidAlbum(Long userId, Long albumId);
UserInfoServiceImpl实现类
/**
* 判断用户是否购买过指定专辑
*
* @param userId
* @param albumId
* @return
*/
@Override
public Boolean isPaidAlbum(Long userId, Long albumId) {
LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidAlbum::getUserId, userId);
queryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
return count > 0;
}
service-user-client
远程调用 UserFeignClient 添加
/**
* 判断用户是否购买过指定专辑
*
* @param albumId
* @return
*/
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId);
UserDegradeFeignClient熔断类:
@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
log.error("远程调用[用户服务]isPaidAlbum方法服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/91
在VipServiceConfigApiController
控制器中添加
/**
* 根据套餐ID查询VIP套现详情
*
* @param id
* @return
*/
@Operation(summary = "根据套餐ID查询VIP套现详情")
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id) {
VipServiceConfig vipServiceConfig = vipServiceConfigService.getById(id);
return Result.ok(vipServiceConfig);
}
在service-user-client
模块UserFeignClient中添加
/**
* 根据套餐ID查询VIP套餐详情
*
* @param id
* @return
*/
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id);
UserDegradeFeignClient熔断类:
@Override
public Result<VipServiceConfig> getVipServiceConfig(Long id) {
log.error("远程调用[用户服务]getVipServiceConfig方法服务降级");
return null;
}
订单流程图如下:
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92
访问订单控制器时,将提交的数据封装到当前实体对象中 TradeVo
@Schema(description = "订单确认对象")
public class TradeVo {
@NotEmpty(message = "付款项目类型不能为空")
@Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
private String itemType;
@Positive(message = "付款项目类型Id不能为空") //被标记的元素必须是正数
@Schema(description = "付款项目类型Id", required = true)
private Long itemId;
@Schema(description = "针对购买声音,购买当前集往后多少集", required = false)
private Integer trackCount;
}
将返回数据封装到OrderInfoVo
package com.atguigu.tingshu.vo.order;
import com.atguigu.tingshu.common.util.Decimal2Serializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(description = "订单对象")
public class OrderInfoVo {
@NotEmpty(message = "交易号不能为空")
@Schema(description = "交易号", required = true)
private String tradeNo;
@NotEmpty(message = "支付方式不能为空")
@Schema(description = "支付方式:1101-微信 1102-支付宝 1103-账户余额", required = true)
private String payWay;
@NotEmpty(message = "付款项目类型不能为空")
@Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
private String itemType;
/**
* value:最小值
* inclusive:是否可以等于最小值,默认true,>= 最小值
* message:错误提示(默认有一个错误提示i18n支持中文)
*
* @DecimalMax 同上
* @Digits integer: 整数位最多几位
* fraction:小数位最多几位
* message:同上,有默认提示
*/
@DecimalMin(value = "0.00", inclusive = false, message = "订单原始金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "订单原始金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "订单原始金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal originalAmount;
@DecimalMin(value = "0.00", inclusive = true, message = "减免总金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "减免总金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "减免总金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal derateAmount;
@DecimalMin(value = "0.00", inclusive = false, message = "订单总金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "订单总金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "订单总金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal orderAmount;
@Valid
@NotEmpty(message = "订单明细列表不能为空")
@Schema(description = "订单明细列表", required = true)
private List<OrderDetailVo> orderDetailVoList;
@Schema(description = "订单减免明细列表")
private List<OrderDerateVo> orderDerateVoList;
@Schema(description = "时间戳", required = true)
private Long timestamp;
@Schema(description = "签名", required = true)
private String sign;
}
在service-order
微服务中编写确定订单控制器 ,
package com.atguigu.tingshu.order.api;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "订单管理")
@RestController
@RequestMapping("api/order")
@SuppressWarnings({"all"})
public class OrderInfoApiController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 订单结算页面渲染-数据汇总
*
* @return
*/
@Operation(summary = "订单结算页面渲染-数据汇总")
@GuiGuLogin
@PostMapping("/orderInfo/trade")
public Result<OrderInfoVo> trade(@RequestBody TradeVo tradeVo) {
Long userId = AuthContextHolder.getUserId();
OrderInfoVo orderInfoVo = orderInfoService.trade(userId, tradeVo);
return Result.ok(orderInfoVo);
}
}
package com.atguigu.tingshu.order.service;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface OrderInfoService extends IService<OrderInfo> {
/**
* 订单结算页面渲染-数据汇总
*
* @return
*/
OrderInfoVo trade(Long userId, TradeVo tradeVo);
}
思路:
专辑订单
vip 订单
生成一个流水号存储到缓存,防止用户重复提交订单
给OrderInfoVo 实体类赋值
最后返回OrderInfoVo 对象
package com.atguigu.tingshu.order.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.order.helper.SignHelper;
import com.atguigu.tingshu.order.mapper.OrderInfoMapper;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.order.OrderDerateVo;
import com.atguigu.tingshu.vo.order.OrderDetailVo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private AlbumFeignClient albumFeignClient;
/**
* 订单确认页面数据渲染-展示“商品”结算页
*
* @return
*/
@Override
public OrderInfoVo orderTrade(Long userId, TradeVo tradeVo) {
//1.根据用户ID远程调用用户服务获取用户信息(服务提供方可能从Redis缓存)
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoById(userId).getData();
Assert.notNull(userInfoVo, "用户信息为空!");
//2.创建订单信息VO对象 初始化价格(原价、减免价、订单价、商品清单列表、优惠列表)
OrderInfoVo orderInfoVo = new OrderInfoVo();
orderInfoVo.setItemType(tradeVo.getItemType());
//TODO这里金额必须只能是 "0.00" 导致提交订单验签失败
BigDecimal originalAmount = new BigDecimal("0.00");
BigDecimal derateAmount = new BigDecimal("0.00");
BigDecimal orderAmount = new BigDecimal("0.00");
//声明订单商品明细列表
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//声明订单优惠明细列表
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//3.处理订单确认数据-VIP会员 购买项目类型:1003-vip会员 可以重复购买
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//3.1 远程调用“用户服务”获取预购套餐信息 得到各种价格
VipServiceConfig serviceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData();
Assert.notNull(serviceConfig, "套餐不存在!");
//3.2 为订单中各种价格赋值(原价、减免价、订单价)
originalAmount = serviceConfig.getPrice();
orderAmount = serviceConfig.getDiscountPrice();
derateAmount = originalAmount.subtract(orderAmount);
//3.3 封装订单包含商品明细列表
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName(serviceConfig.getName());
orderDetailVo.setItemUrl(serviceConfig.getImageUrl());
//订单中明细商品价格:展示原价
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//3.4 封装订单包含优惠明细列表
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("VIP套餐限时优惠:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//4.TODO 处理订单确认数据-专辑
//4.1 根据用户ID跟预购专辑ID判断是否重复购买
Boolean ifBuy = userFeignClient.isPaidAlbum(tradeVo.getItemId()).getData();
if (ifBuy) {
throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
}
//4.2 远程调用专辑服务获取专辑信息(服务提供方可能从Redis缓存)得到专辑价格跟折扣
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(tradeVo.getItemId()).getData();
Assert.notNull(albumInfo, "专辑不存在!");
//4.3 为不同身份用户计算价格(原价、订单价、减免价)
//4.3.1 获取专辑原价
originalAmount = albumInfo.getPrice();
orderAmount = originalAmount;
//4.3.2 处理普通用户折扣
if (albumInfo.getDiscount().intValue() != -1) {
//普通用户打折
if (userInfoVo.getIsVip().intValue() == 0 || new Date().after(userInfoVo.getVipExpireTime())) {
//除以 避免精度丢失=保留2位小数同时四舍五入
orderAmount = originalAmount
.multiply(albumInfo.getDiscount())
.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
derateAmount = originalAmount.subtract(orderAmount);
}
}
//4.3.3 处理VIP用户折扣
if (albumInfo.getVipDiscount().intValue() != -1) {
//普通用户打折
if (userInfoVo.getIsVip().intValue() == 1 && new Date().before(userInfoVo.getVipExpireTime())) {
//除以 避免精度丢失=保留2位小数同时四舍五入
orderAmount = originalAmount
.multiply(albumInfo.getVipDiscount())
.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
derateAmount = originalAmount.subtract(orderAmount);
}
}
//4.4 封装订单中商品明细列表(专辑)
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName(albumInfo.getAlbumTitle());
orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//4.5 封装订单中商品减免明细列表
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_ALBUM_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("专辑折扣,减免:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//5.TODO 处理订单确认数据-声音
}
//6.对订单VO中剩余其他属性统一处理:流水号、签名、时间戳 用户没选择:付款方式不处理
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//6.1 避免用户对同一订单重复多次提交,渲染订单确认页面时候生成流水号
String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
String tradeNo = IdUtil.randomUUID();
//存入服务端Redis中
redisTemplate.opsForValue().set(tradeNoKey, tradeNo, RedisConstant.ORDER_TRADE_EXPIRE, TimeUnit.MINUTES);
orderInfoVo.setTradeNo(tradeNo);
//6.2 避免风险订单确认页面任意参数及值发生变化,基于签名机制-生成签名
orderInfoVo.setTimestamp(DateUtil.current());
//调用工具方法基于现有参数生成签名: TODO 订单VO信息中以及签名参数Map不包含:支付方式
Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true);
String sign = SignHelper.getSign(paramsMap);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94
service-user
模块
/**
* 查询当前登录用户某个专辑已购声音ID列表
*
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "查询当前登录用户某个专辑已购声音ID列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackIdList(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
List<Long> userPaidTrackIdList = userInfoService.getUserPaidTrackIdList(userId, albumId);
return Result.ok(userPaidTrackIdList);
}
/**
* 查询当前登录用户某个专辑已购声音ID列表
*
* @param albumId
* @return
*/
List<Long> getUserPaidTrackIdList(Long userId, Long albumId);
/**
* 查询当前登录用户某个专辑已购声音ID列表
*
* @param albumId
* @return
*/
@Override
public List<Long> getUserPaidTrackIdList(Long userId, Long albumId) {
//1.根据用户ID+专辑ID查询已购声音表 得到已购声音集合
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getUserId, userId);
queryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper);
//2.遍历得到已购声音ID列表
if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
return userPaidTrackIdList;
}
return null;
}
service-user-client
模块中UserFeignClient提供Feign接口
/**
* 查询当前登录用户某个专辑已购声音ID列表
*
* @param albumId
* @return
*/
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackIdList(@PathVariable Long albumId);
UserDegradeFeignClient熔断类
@Override
public Result<List<Long>> getUserPaidTrackIdList(Long albumId) {
log.error("[用户服务]提供远程调用getUserPaidTrackIdList服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/95
TrackInfoApiController 控制器返回数据格式如下: 因为当前专辑只支持单集购买!专辑的价格是 album_info.price --> 当前专辑中的一条声音的价格 声音总价= album_info.price*trackCount
album_info 表中 price_type类型分为: 0201-单集 0202-整专辑
Map<String, Object> map = new HashMap<>();
map.put("name","本集"); // 显示文本
map.put("price",albumInfo.getPrice()); // 专辑声音对应的价格
map.put("trackCount",1); // 记录购买集数
list.add(map);
/**
* 提供给小程序端,用户点击某个待购声音后,展示待购分集对象
* @param trackId
* @return 【{"name":"本集",price:1, trackCount:1},{"name":"后10集",price:10, trackCount:10}】
*/
@GuiGuLogin
@Operation(summary = "获取当前用户分集购买集合")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getUserWaitBuyTrackPayList(@PathVariable Long trackId){
Long userId = AuthContextHolder.getUserId();
List<Map<String, Object>> list = trackInfoService.getUserWaitBuyTrackPayList(userId, trackId);
return Result.ok(list);
}
/**
* 提供给小程序端,用户点击某个待购声音后,展示待购分集对象
* @param trackId
* @return 【{"name":"本集",price:1, trackCount:1},{"name":"后10集",price:10, trackCount:10}】
*/
List<Map<String, Object>> getUserWaitBuyTrackPayList(Long userId, Long trackId);
思路:
/**
* 提供给小程序端,用户点击某个待购声音后,展示待购分集对象
*
* @param trackId 选择预购声音ID
* @return 【{"name":"本集",price:1, trackCount:1},{"name":"后10集",price:10, trackCount:10}】
*/
@Override
public List<Map<String, Object>> getUserWaitBuyTrackPayList(Long userId, Long trackId) {
//1.根据入参中声音ID查询待购声音对象 得到序号及专辑ID
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
//2.根据声音所属专辑ID+序号 获取大于当前声音序号所有待购声音列表
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
queryWrapper.orderByAsc(TrackInfo::getOrderNum);
List<TrackInfo> userWaitBuyTrackList = trackInfoMapper.selectList(queryWrapper);
//3.远程调用“用户服务”获取当前用户已购声音ID列表
List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(trackInfo.getAlbumId()).getData();
//4.将待购声音列表中包含已购买的声音排除掉
if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) {
userWaitBuyTrackList = userWaitBuyTrackList.stream()
.filter(waitBuyTrackInfo -> !userPaidTrackIdList.contains(waitBuyTrackInfo.getId()))
.collect(Collectors.toList());
}
//5.根据剩余待购声音列表长度动态构建分集购买对象
List<Map<String, Object>> mapArrayList = new ArrayList<>();
//5.1 构建本集分集购买对象
//5.2 获取声音单价
AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());
BigDecimal price = albumInfo.getPrice();
Map<String, Object> currJiMap = new HashMap<>();
currJiMap.put("name", "本集");
currJiMap.put("price", price);
currJiMap.put("trackCount", 1);
mapArrayList.add(currJiMap);
//5.3 剩余待购集为8 后8集/全集 剩余待购集为15 显示:后10集 全集 剩余待购集为25 显示:后10集 后20集 全集
int size = userWaitBuyTrackList.size();
for (int i = 10; i <= 50; i += 10) {
//判断如果size(待购声音数量)>i 固定显示后i集
if (size > i) {
Map<String, Object> map = new HashMap<>();
map.put("name", "后" + i + "集");
map.put("price", price.multiply(BigDecimal.valueOf(i)));
map.put("trackCount", i);
mapArrayList.add(map);
} else {
//直接显示全集
Map<String, Object> map = new HashMap<>();
map.put("name", "全集");
map.put("price", price.multiply(BigDecimal.valueOf(size)));
map.put("trackCount", size);
mapArrayList.add(map);
break;
}
}
return mapArrayList;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96
service-album
微服务中的TrackInfoApiController控制器添加代码
/**
* 渲染订单确认页面中待购声音(商品)集合
*
* @param trackId
* @param trackCount
* @return
*/
@GuiGuLogin
@Operation(summary = "渲染订单确认页面中待购声音(商品)集合")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getUserWaitPayTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount) {
List<TrackInfo> userWaitPayTrackInfoList = trackInfoService.getUserWaitPayTrackInfoList(trackId, trackCount);
return Result.ok(userWaitPayTrackInfoList);
}
/**
* 渲染订单确认页面中待购声音(商品)集合:根据选择声音ID跟数量作为条件获取待购声音列表
*
* @param trackId
* @param trackCount
* @return
*/
List<TrackInfo> getUserWaitPayTrackInfoList(Long trackId, Integer trackCount);
思路:
1. 根据声音Id 获取到当前声音对象
2. 根据声音ID+当前声音序号查询待购声音列表
3. 远程调用用户服务获取已购买声音ID列表,将已购声音进行排除
4. 返回待购声音集合对象返回
/**
* 渲染订单确认页面中待购声音(商品)集合:根据选择声音ID跟数量作为条件获取待购声音列表
*
* @param trackId 付费标识对应声音ID
* @param trackCount 购买数量
* @return
*/
@Override
public List<TrackInfo> getUserWaitPayTrackInfoList(Long trackId, Integer trackCount) {
Long userId = AuthContextHolder.getUserId();
//1.根据声音ID查询声音对象,得到声音所属专辑ID
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
//2.远程调用“用户服务”得到该专辑已购声音ID集合
List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(trackInfo.getAlbumId()).getData();
//3.构建查询待购买声音集合(如果存在已购买声音将其排除掉)
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
if(CollectionUtil.isNotEmpty(userPaidTrackIdList)){
queryWrapper.notIn(TrackInfo::getId, userPaidTrackIdList);
}
queryWrapper.orderByAsc(TrackInfo::getOrderNum);
queryWrapper.last("limit "+trackCount);
//避免暴露声音音频播放地址,固指定查询指定的列
queryWrapper.select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl);
return trackInfoMapper.selectList(queryWrapper);
}
AlbumFeignClient
/**
* 查询当前用户待购声音列表-用于渲染声音结算页面
*
* @param trackId
* @param trackCount
* @return
*/
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitPayTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount);
熔断类:
@Override
public Result<List<TrackInfo>> getWaitPayTrackInfoList(Long trackId, Integer trackCount) {
log.error("[专辑模块Feign调用]getWaitPayTrackInfoList异常");
return null;
}
/**
* 订单确认页面数据渲染-展示“商品”结算页
*
* @return
*/
@Override
public OrderInfoVo orderTrade(Long userId, TradeVo tradeVo) {
//1.根据用户ID远程调用用户服务获取用户信息(服务提供方可能从Redis缓存)
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoById(userId).getData();
Assert.notNull(userInfoVo, "用户信息为空!");
//2.创建订单信息VO对象 初始化价格(原价、减免价、订单价、商品清单列表、优惠列表)
OrderInfoVo orderInfoVo = new OrderInfoVo();
orderInfoVo.setItemType(tradeVo.getItemType());
//TODO这里金额必须只能是 "0.00" 导致提交订单验签失败
BigDecimal originalAmount = new BigDecimal("0.00");
BigDecimal derateAmount = new BigDecimal("0.00");
BigDecimal orderAmount = new BigDecimal("0.00");
//声明订单商品明细列表
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//声明订单优惠明细列表
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//3.处理订单确认数据-VIP会员 购买项目类型:1003-vip会员 可以重复购买
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//3.1 远程调用“用户服务”获取预购套餐信息 得到各种价格
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//4. 处理订单确认数据-专辑 ..省略
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//5.TODO 处理订单确认数据-声音
//5.1 远程调用“专辑服务”携带声音ID+购买集数 查询用户代购买声音列表(将已购买声音排除掉) LIst<TrackInfo>
List<TrackInfo> waitBuyTrackList = albumFeignClient.getUserWaitPayTrackInfoList(tradeVo.getItemId(), tradeVo.getTrackCount()).getData();
Assert.notNull(waitBuyTrackList, "无可购买声音!");
//5.2 远程调用“专辑服务”根据购买声音所属专辑ID查询专辑信息 得到声音价格(不支持折扣)
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(waitBuyTrackList.get(0).getAlbumId()).getData();
Assert.notNull(albumInfo, "参数异常!");
//5.3 计算价格 单价*集数=原始价格/订单价格
BigDecimal price = albumInfo.getPrice();
originalAmount = price.multiply(BigDecimal.valueOf(waitBuyTrackList.size()));
orderAmount = originalAmount;
//5.4 封装订单中商品明细列表(声音列表)
orderDetailVoList = waitBuyTrackList.stream().map(trackInfo -> {
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(trackInfo.getId());
orderDetailVo.setItemName(trackInfo.getTrackTitle());
orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
orderDetailVo.setItemPrice(price);
return orderDetailVo;
}).collect(Collectors.toList());
}
//6.对订单VO中剩余其他属性统一处理:流水号、签名、时间戳 用户没选择:付款方式不处理
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//6.1 避免用户对同一订单重复多次提交,渲染订单确认页面时候生成流水号
String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
String tradeNo = IdUtil.randomUUID();
//存入服务端Redis中
redisTemplate.opsForValue().set(tradeNoKey, tradeNo, RedisConstant.ORDER_TRADE_EXPIRE, TimeUnit.MINUTES);
orderInfoVo.setTradeNo(tradeNo);
//6.2 避免风险订单确认页面任意参数及值发生变化,基于签名机制-生成签名
orderInfoVo.setTimestamp(DateUtil.current());
//调用工具方法基于现有参数生成签名: TODO 订单VO信息中以及签名参数Map不包含:支付方式
Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true);
String sign = SignHelper.getSign(paramsMap);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
声音购买-仅支持余额付款:
专辑购买:
VIP 购买:
分集购买声音只支持余额支付
购买VIP或专辑分为余额,微信支付
这里采用Seata提供AT模式进行分布式事务管理。
分别在tingshu_account
、tingshu_order
、tingshu_user
三个数据库中新增undo_log日志表
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
分别在service-order
、service-account
、service-user
三个模块pom.xml中增加Seata启动依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!-- 默认seata客户端版本比较低,排除后重新引入指定版本-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
订单微服务service-order
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-order-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
账户微服务service-account
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-account-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
用户微服务service-user
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-user-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/127
service-account
账户服务UserAccountApiController
/**
* 扣减账户余额
* @param accountDeductVo
* @return
*/
@GuiGuLogin
@Operation(summary = "扣减账户余额")
@PostMapping("/userAccount/checkAndDeduct")
public Result checkAndDeduct(@RequestBody AccountLockVo accountDeductVo){
//获取当前登录用户ID
Long userId = AuthContextHolder.getUserId();
//调用业务逻辑获取账户可用金额
userAccountService.checkAndDeduct(userId, accountDeductVo);
return Result.ok();
}
业务接口UserAccountService
/**
* 扣减账户余额
* @param userId
* @param accountDeductVo
*/
void checkAndDeduct(Long userId, AccountLockVo accountDeductVo);
/**
* 新增账户变动历史记录
* @param userId 用户ID
* @param title 事由
* @param tradeType 类型
* @param amount 金额
* @param orderNo 订单编号
*/
void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo);
业务实现类UserAccountService
/**
* 扣减账户余额
*
* @param userId
* @param accountDeductVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void checkAndDeduct(Long userId, AccountLockVo accountDeductVo) {
//1.对账户扣减操作进行幂等性处理-避免同一个订单多次进行扣减
String key = "account:deduct:" + accountDeductVo.getOrderNo();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, accountDeductVo.getOrderNo(), 1, TimeUnit.HOURS);
if (flag) {
try {
//2.扣减账户余额
int count = userAccountMapper.checkAndDeduct(userId, accountDeductVo.getAmount());
if (count == 0) {
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
//3.新增账户变动日志
saveUserAccountDetail(userId, "消费:"+accountDeductVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountDeductVo.getAmount(), accountDeductVo.getOrderNo());
} catch (Exception e) {
//如果扣减余额或者新增账户异常:全局事务回回滚,再次进行余额付款 固将锁定Key删除
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
}
}
/**
* 新增账户变动历史记录
*
* @param userId 用户ID
* @param title 事由
* @param tradeType 类型
* @param amount 金额
* @param orderNo 订单编号
*/
@Override
public void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo) {
UserAccountDetail userAccountDetail = new UserAccountDetail();
userAccountDetail.setUserId(userId);
userAccountDetail.setTitle(title);
userAccountDetail.setTradeType(tradeType);
userAccountDetail.setAmount(amount);
userAccountDetail.setOrderNo(orderNo);
userAccountDetailMapper.insert(userAccountDetail);
}
UserAccountMapper
package com.atguigu.tingshu.account.mapper;
import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
/**
* 扣减账户余额
* @param userId 用户ID
* @param amount 金额
* @return
*/
int checkAndDeduct(@Param("userId") Long userId,@Param("amount") BigDecimal amount);
}
UserAccountMapper.xml
<!--扣减账户余额-->
<update id="checkAndDeduct">
UPDATE user_account
SET total_amount = total_amount - #{amount},
available_amount = available_amount - #{amount},
total_pay_amount = total_pay_amount + #{amount}
WHERE
user_id = #{userId}
AND available_amount >= #{amount}
</update>
在service-account-client
模块中提供Feign接口:AccountFeignClient
package com.atguigu.tingshu.account;
import com.atguigu.tingshu.account.impl.AccountDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* <p>
* 账号模块远程调用API接口
* </p>
*
* @author atguigu
*/
@FeignClient(value = "service-account",path = "api/account", fallback = AccountDegradeFeignClient.class)
public interface AccountFeignClient {
/**
* 扣减账户余额
* @param accountDeductVo
* @return
*/
@PostMapping("/userAccount/checkAndDeduct")
public Result checkAndDeduct(@RequestBody AccountLockVo accountDeductVo);
}
服务降级类:AccountDegradeFeignClient
package com.atguigu.tingshu.account.impl;
import com.atguigu.tingshu.account.AccountFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AccountDegradeFeignClient implements AccountFeignClient {
@Override
public Result checkAndDeduct(AccountLockVo accountDeductVo) {
log.error("[账户服务]提供远程调用接口checkAndDeduct服务降级");
return null;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/118
service-user
模块中UserInfoApiController提供处理购买记录Restful接口
UserInfoApiController控制器
/**
* 新增用户购买记录:VIP会员、专辑、声音
*
* @param userPaidRecordVo
* @return
*/
@GuiGuLogin(required = false) //微信回调不会携带Token导致该方法调用失败
@Operation(summary = "新增用户购买记录")
@PostMapping("/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo) {
Long userId = AuthContextHolder.getUserId();
if(userId!=null){
userPaidRecordVo.setUserId(userId);
}
userInfoService.savePaidRecord(userPaidRecordVo);
return Result.ok();
}
UserInfoService业务接口
/**
* 处理不同购买项:VIP会员,专辑、声音
*
* @param userPaidRecordVo
*/
void savePaidRecord(UserPaidRecordVo userPaidRecordVo);
业务实现类UserInfoServiceImpl
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
/**
* 处理不同购买项:VIP会员,专辑、声音
*
* @param userPaidRecordVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
//1.处理专辑购买记录
if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(userPaidRecordVo.getItemType())) {
//1.1 业务去重避免同一订单多次新增购买记录
LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//1.2 构建用户购买专辑对象
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
//1.3 保存用户购买记录记录
userPaidAlbumMapper.insert(userPaidAlbum);
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(userPaidRecordVo.getItemType())) {
//2.处理声音购买记录
//2.1 业务去重避免同一订单多次新增购买记录
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidTrackMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.2 构建用户购买声音对象(可能存在多条购买项)
//远程调用专辑服务获取声音信息得到所属专辑ID
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
userPaidRecordVo.getItemIdList().forEach(trackId -> {
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setTrackId(trackId);
userPaidTrack.setAlbumId(trackInfo.getAlbumId());
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
//2.3 保存用户购买记录记录
userPaidTrackMapper.insert(userPaidTrack);
});
} else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(userPaidRecordVo.getItemType())) {
//3.处理会员购买记录
//3.1 新增VIP会员购买记录
//3.1.0 获取会员套餐信息
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
//3.1.1 获取当前用户信息 得到当前用户VIP状态及VIP过期时间
UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
//3.1.2 动态获取会员生效时间及过期时间
Date now = new Date();
Date expireDate = new Date();
//3.1.3 构建用户购买VIP记录对象
UserVipService userVipService = new UserVipService();
if(userInfo.getIsVip().intValue()==1 && userInfo.getVipExpireTime().after(now)){
//用户身份已是VIP会员
expireDate = DateUtil.offsetMonth(userInfo.getVipExpireTime(), vipServiceConfig.getServiceMonth());
//本次购买VIP会员生效时间:是原来会员过期时间
userVipService.setStartTime(userInfo.getVipExpireTime());
//本次购买VIP会员过期时间:原来会员过期时间+购买VIP套餐月数
userVipService.setExpireTime(expireDate);
}else{
//普通用户
//本次购买VIP会员生效时间:当前时间
userVipService.setStartTime(now);
//本次购买VIP会员过期时间:当前时间+购买VIP套餐月数
userVipService.setExpireTime(DateUtil.offsetMonth(now, vipServiceConfig.getServiceMonth()));
}
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
//3.3.4 保存会员购买记录
userVipServiceMapper.insert(userVipService);
//3.2 更新用户VIP标识及会员过期时间
userInfo.setIsVip(1);
userInfo.setVipExpireTime(userVipService.getExpireTime());
userInfoMapper.updateById(userInfo);
}
}
在service-user
模块中Feign接口提供远程调用方法:UserFeignClient
/**
* 处理用户购买项目:VIP会员、专辑、声音
* @param userPaidRecordVo
*/
@PostMapping("/api/user/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo);
服务降级类:UserDegradeFeignClient
@Override
public Result savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.error("[用户服务]提供远程调用savePaidRecord服务降级");
return null;
}
策略模式(Strategy Pattern)又叫政策模式(Policy Pattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。属于行为型模式。
策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同实现。
优点:
缺点:
策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境(Context)类:用来操作策略的上下文环境,屏蔽高层模块(客户端)对策略、算法的直接访问,封装可能存在的变化。
策略接口
package com.atguigu.tingshu.user.strategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
/**
* 策略接口:提供抽象方法-处理各种类型购买记录
*/
public interface PaidRecordStrategy {
/**
* 处理购买记录
* @param userPaidRecordVo
*/
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo);
}
VIP购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.strategy.impl;
import cn.hutool.core.date.DateUtil;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.model.user.UserVipService;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserVipServiceMapper;
import com.atguigu.tingshu.user.mapper.VipServiceConfigMapper;
import com.atguigu.tingshu.user.strategy.PaidRecordStrategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author: atguigu
* @create: 2023-12-02 09:21
*/
@Slf4j
@Component("1003") //跟前端提交的VIP购买项目类型一致
public class VIPStrategy implements PaidRecordStrategy {
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private UserVipServiceMapper userVipServiceMapper;
/**
* 处理购买类型是VIP
* @param userPaidRecordVo
*/
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("[用户服务]处理购买类型是VIP");
//3.处理会员购买记录
//3.1 新增VIP会员购买记录
//3.1.0 获取会员套餐信息
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
//3.1.1 获取当前用户信息 得到当前用户VIP状态及VIP过期时间
UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
//3.1.2 动态获取会员生效时间及过期时间
Date now = new Date();
Date expireDate = new Date();
//3.1.3 构建用户购买VIP记录对象
UserVipService userVipService = new UserVipService();
if(userInfo.getIsVip().intValue()==1 && userInfo.getVipExpireTime().after(now)){
//用户身份已是VIP会员
expireDate = DateUtil.offsetMonth(userInfo.getVipExpireTime(), vipServiceConfig.getServiceMonth());
//本次购买VIP会员生效时间:是原来会员过期时间
userVipService.setStartTime(userInfo.getVipExpireTime());
//本次购买VIP会员过期时间:原来会员过期时间+购买VIP套餐月数
userVipService.setExpireTime(expireDate);
}else{
//普通用户
//本次购买VIP会员生效时间:当前时间
userVipService.setStartTime(now);
//本次购买VIP会员过期时间:当前时间+购买VIP套餐月数
userVipService.setExpireTime(DateUtil.offsetMonth(now, vipServiceConfig.getServiceMonth()));
}
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
//3.3.4 保存会员购买记录
userVipServiceMapper.insert(userVipService);
//3.2 更新用户VIP标识及会员过期时间
userInfo.setIsVip(1);
userInfo.setVipExpireTime(userVipService.getExpireTime());
userInfoMapper.updateById(userInfo);
}
}
专辑购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.strategy.impl;
import com.atguigu.tingshu.model.user.UserPaidAlbum;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.strategy.PaidRecordStrategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2023-12-02 09:21
*/
@Slf4j
@Component("1001")
public class AlbumStrategy implements PaidRecordStrategy {
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
/**
* 处理购买类型是:专辑
* @param userPaidRecordVo
*/
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("[用户服务]处理购买类型是专辑");
//1.1 业务去重避免同一订单多次新增购买记录
LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//1.2 构建用户购买专辑对象
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
//1.3 保存用户购买记录记录
userPaidAlbumMapper.insert(userPaidAlbum);
}
}
声音购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.strategy.impl;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.user.UserPaidTrack;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.strategy.PaidRecordStrategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2023-12-02 09:21
*/
@Slf4j
@Component("1002")
public class TrackStrategy implements PaidRecordStrategy {
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserPaidTrackMapper userPaidTrackMapper;
/**
* 处理购买类型是:声音
* @param userPaidRecordVo
*/
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("[用户服务]处理购买类型是声音");
//2.处理声音购买记录
//2.1 业务去重避免同一订单多次新增购买记录
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidTrackMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.2 构建用户购买声音对象(可能存在多条购买项)
//远程调用专辑服务获取声音信息得到所属专辑ID
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
userPaidRecordVo.getItemIdList().forEach(trackId -> {
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setTrackId(trackId);
userPaidTrack.setAlbumId(trackInfo.getAlbumId());
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
//2.3 保存用户购买记录记录
userPaidTrackMapper.insert(userPaidTrack);
});
}
}
提供工厂类组装所有的策略实现类
package com.atguigu.tingshu.user.strategy;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.user.strategy.impl.AlbumStrategy;
import com.atguigu.tingshu.user.strategy.impl.TrackStrategy;
import com.atguigu.tingshu.user.strategy.impl.VIPStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author: atguigu
* @create: 2023-12-02 09:24
*/
@Slf4j
@Component
public class StrategyFactory {
/**
* 将IOC容器中PaidRecordStrategy接口类型对象注入Map中
* Map中key是bean对象ID Map中Value是实现类对象
*/
@Autowired
private Map<String, PaidRecordStrategy> strategyMap;
/**
* 根据入参中类型自动返回策略实现类对象
*
* @param itemType
* @return
*/
public PaidRecordStrategy getPaidStrategy(String itemType) {
if (strategyMap.containsKey(itemType)) {
return strategyMap.get(itemType);
}
throw new GuiguException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
}
}
UserInfoServiceImpl业务调用工厂获取不同策略实现类完成方法调用
@Autowired
private StrategyFactory strategyFactory;
/**
* 处理不同购买项:VIP会员,专辑、声音
*
* @param userPaidRecordVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
//根据入参中购买项目类型获取不同策略实现类对象
PaidRecordStrategy paidStrategy = strategyFactory.getPaidStrategy(userPaidRecordVo.getItemType());
paidStrategy.savePaidRecord(userPaidRecordVo);
}
我们将前端页面提交的数据统一封装到实体类OrderInfoVo中
package com.atguigu.tingshu.vo.order;
import com.atguigu.tingshu.common.util.Decimal2Serializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(description = "订单对象")
public class OrderInfoVo {
@NotEmpty(message = "交易号不能为空")
@Schema(description = "交易号", required = true)
private String tradeNo;
@NotEmpty(message = "支付方式不能为空")
@Schema(description = "支付方式:1101-微信 1102-支付宝 1103-账户余额", required = true)
private String payWay;
@NotEmpty(message = "付款项目类型不能为空")
@Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
private String itemType;
/**
* value:最小值
* inclusive:是否可以等于最小值,默认true,>= 最小值
* message:错误提示(默认有一个错误提示i18n支持中文)
*
* @DecimalMax 同上
* @Digits integer: 整数位最多几位
* fraction:小数位最多几位
* message:同上,有默认提示
*/
@DecimalMin(value = "0.00", inclusive = false, message = "订单原始金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "订单原始金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "订单原始金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal originalAmount;
@DecimalMin(value = "0.00", inclusive = true, message = "减免总金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "减免总金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "减免总金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal derateAmount;
@DecimalMin(value = "0.00", inclusive = false, message = "订单总金额必须大于0.00")
@DecimalMax(value = "9999.99", inclusive = true, message = "订单总金额必须大于9999.99")
@Digits(integer = 4, fraction = 2)
@Schema(description = "订单总金额", required = true)
@JsonSerialize(using = Decimal2Serializer.class)
private BigDecimal orderAmount;
@Valid
@NotEmpty(message = "订单明细列表不能为空")
@Schema(description = "订单明细列表", required = true)
private List<OrderDetailVo> orderDetailVoList;
@Schema(description = "订单减免明细列表")
private List<OrderDerateVo> orderDerateVoList;
@Schema(description = "时间戳", required = true)
private Long timestamp;
@Schema(description = "签名", required = true)
private String sign;
}
在service-order
微服务中添加提交订单控制器
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99
OrderInfoApiController
/**
* 提交订单(包含余额付款) 如果是余额付款则进行扣费;反之则只需要保存订单即可
*
* @param orderInfoVo
* @return 新增后订单编号,1.根据订单编号查询订单详情 2.跟在线支付平台对接
*/
@GuiGuLogin
@Operation(summary = "提交订单(包含余额付款)")
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, String>> submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) {
Long userId = AuthContextHolder.getUserId();
String orderNo = orderInfoService.submitOrder(userId, orderInfoVo);
Map<String, String> map = new HashMap<>();
map.put("orderNo", orderNo);
return Result.ok(map);
}
/**
* 提交订单,返回订单编号
* @param userId
* @param orderInfoVo
* @return
*/
String submitOrder(Long userId, OrderInfoVo orderInfoVo);
/**
* 保存订单相关信息
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo);
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 提交订单
*
* @param userId
* @param orderInfoVo
* @return
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public String submitOrder(Long userId, OrderInfoVo orderInfoVo) {
//1.验证流水号,订单是否重复提交
//1.1 先获取服务器端存放在Redis中流水号
String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//1.2 跟前端提交流水比较,比对成功删除流水号(采用Lua脚本删除|)
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Boolean.class);
Boolean flag = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeNoKey), orderInfoVo.getTradeNo());
if (!flag) {
throw new GuiguException(ResultCodeEnum.ORDER_SUBMIT_REPEAT);
}
//2.验证订单签名,避免订单页面数据被篡改
//2.1 先将VO转为Map 当时签名时候“支付方式”没有参与签名 单前端提交
Map<String, Object> paramMap = BeanUtil.beanToMap(orderInfoVo);
//2.2 固将支付方式参数从Map中移除掉
paramMap.remove("payWay");
//2.3 调用工具方法进行验证签名
SignHelper.checkSign(paramMap);
//3.新增订单(订单,订单明细,优惠明细) 返回订单对象
OrderInfo orderInfo = this.saveOrder(userId, orderInfoVo);
//4.判断如果是余额付款则处理(扣减余额、新增用户购买记录)
if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) {
//4.1 远程调用“账户服务”扣减账户余额
AccountLockVo accountDeductVo = new AccountLockVo();
accountDeductVo.setOrderNo(orderInfo.getOrderNo());
accountDeductVo.setUserId(userId);
accountDeductVo.setAmount(orderInfo.getOrderAmount());
accountDeductVo.setContent(orderInfo.getOrderTitle());
Result deductResult = accountFeignClient.checkAndDeduct(accountDeductVo);
if (200 != deductResult.getCode()) {
//扣减余额失败:全局事务都需要回滚
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
//4.2 远程调用“用户服务”新增购买记录
UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
userPaidRecordVo.setOrderNo(orderInfo.getOrderNo());
userPaidRecordVo.setUserId(userId);
userPaidRecordVo.setItemType(orderInfo.getItemType());
List<Long> itemIdList = orderInfoVo.getOrderDetailVoList().stream().map(OrderDetailVo::getItemId).collect(Collectors.toList());
userPaidRecordVo.setItemIdList(itemIdList);
Result paidRecordResult = userFeignClient.savePaidRecord(userPaidRecordVo);
if (200 != paidRecordResult.getCode()) {
//新增购买记录失败:全局事务都需要回滚
throw new GuiguException(211, "新增购买记录异常");
}
//5.余额扣减成功、购买记录新增成功 更新订单支付状态:已支付
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
orderInfoMapper.updateById(orderInfo);
}
//6.响应新增订单后订单编号(全局唯一)
return orderInfo.getOrderNo();
}
业务处理:
OrderInfoService接口
/**
* 保存订单相关信息
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo);
OrderInfoServiceImpl实现类
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderDerateMapper orderDerateMapper;
/**
* 保存订单相关信息
*
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
@Override
public OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo) {
//1.保存订单信息
//1.1 将提交订单vo属性拷贝到订单PO对象中
OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
//1.2 生成订单编号(全局唯一趋势递增) 日期+趋势递增ID(分布式ID生成:雪花算法/Tinyid(滴滴)/Leaf(美团)/UidGenerator(百度))
String orderNo = DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
orderInfo.setOrderNo(orderNo);
//1.3 获取购买项第一商品名称给订单标题复制
orderInfo.setOrderTitle(orderInfoVo.getOrderDetailVoList().get(0).getItemName());
//1.4 初始订单状态:未支付 0901
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
//1.5 用户ID
orderInfo.setUserId(userId);
//1.6 保存订单
orderInfoMapper.insert(orderInfo);
//获取订单ID
Long orderId = orderInfo.getId();
//2.新增订单明细列表
List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
orderDetailVoList.stream().forEach(orderDetailVo -> {
//将VO转为PO对象
OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
//关联订单ID
orderDetail.setOrderId(orderId);
//保存订单明细
orderDetailMapper.insert(orderDetail);
});
}
//3.新增订单优惠列表
List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
orderDerateVoList.stream().forEach(orderDerateVo -> {
//将VO转为PO对象
OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
//关联订单ID
orderDerate.setOrderId(orderId);
//保存优惠明细
orderDerateMapper.insert(orderDerate);
});
}
return orderInfo;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100
当支付成功之后,点击查看订单
在service-order
微服务OrderInfoApiController中添加
/**
* 查询当前用户指定订单信息
*
* @param orderNo
* @return
*/
@GuiGuLogin
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable("orderNo") String orderNo) {
Long userId = AuthContextHolder.getUserId();
OrderInfo orderInfo = orderInfoService.getOrderInfo(userId, orderNo);
return Result.ok(orderInfo);
}
OrderInfoService接口
/**
* 获取订单信息
*
* @param userId
* @param orderNo
* @return
*/
OrderInfo getOrderInfo(Long userId, String orderNo);
OrderInfoServiceImpl实现
/**
* 根据订单编号查询订单详情(订单商品明细、订单优惠明细)
*
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderInfo(String orderNo) {
//1.根据订单编号查询订单信息
LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderInfo::getOrderNo, orderNo);
OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);
if(orderInfo!=null){
//2.跟订单ID查询订单明细列表
LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();
orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaQueryWrapper);
orderInfo.setOrderDetailList(orderDetailList);
//3.跟订单ID查询订单优惠列表
LambdaQueryWrapper<OrderDerate> orderDerateLambdaQueryWrapper = new LambdaQueryWrapper<>();
orderDerateLambdaQueryWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaQueryWrapper);
orderInfo.setOrderDerateList(orderDerateList);
//4.设置订单支付状态及支付方式:中文
orderInfo.setOrderStatusName(this.getOrderStatusName(orderInfo.getOrderStatus()));
orderInfo.setPayWayName(this.getPayWayName(orderInfo.getPayWay()));
return orderInfo;
}
return null;
}
private String getOrderStatusName(String orderStatus) {
if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus)) {
return "未支付";
} else if (SystemConstant.ORDER_STATUS_PAID.equals(orderStatus)) {
return "已支付";
} else if (SystemConstant.ORDER_STATUS_CANCEL.equals(orderStatus)) {
return "取消";
}
return null;
}
/**
* 根据支付方式编号得到支付名称
*
* @param payWay
* @return
*/
private String getPayWayName(String payWay) {
if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(payWay)) {
return "微信";
} else if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
return "余额";
} else if (SystemConstant.ORDER_PAY_WAY_ALIPAY.equals(payWay)) {
return "支付宝";
}
return "";
}
YAPI接口地址:
service-order
的OrderInfoApiController 控制器 添加
/**
* 分页获取用户订单列表
*
* @param page
* @param limit
* @return
*/
@GuiGuLogin
@Operation(summary = "分页获取用户订单列表")
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result<Page<OrderInfo>> getUserOrderByPage(@PathVariable Long page, @PathVariable Long limit) {
Long userId = AuthContextHolder.getUserId();
Page<OrderInfo> pageInfo = new Page<>(page, limit);
pageInfo = orderInfoService.getUserOrderByPage(pageInfo, userId);
return Result.ok(pageInfo);
}
OrderInfoService接口
/**
* 分页获取用户订单列表
*
* @param pageInfo
* @param userId
* @return
*/
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);
OrderInfoServiceImpl实现
/**
* 分页获取用户订单列表
*
* @param pageInfo
* @param userId
* @return
*/
@Override
public Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId) {
pageInfo = orderInfoMapper.getUserOrderByPage(pageInfo, userId);
//设置订单状态跟支付方式:中文
pageInfo.getRecords().forEach(orderInfo -> {
orderInfo.setOrderStatusName(this.getOrderStatusName(orderInfo.getOrderStatus()));
orderInfo.setPayWayName(this.getPayWayName(orderInfo.getPayWay()));
});
return pageInfo;
}
OrderInfoMapper 接口
package com.atguigu.tingshu.order.mapper;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, @Param("userId") Long userId);
}
OrderInfoMapper.xml 映射文件 存在mybatisPlus分页bug
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.tingshu.order.mapper.OrderInfoMapper">
<!--查询订单及订单明细配置自定义结果集:封装一对多-->
<resultMap id="orderInfoMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
<id column="id" property="id"></id>
<collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" autoMapping="true">
<id column="order_detial_id" property="id"></id>
</collection>
</resultMap>
<select id="getUserOrderByPage" resultMap="orderInfoMap">
SELECT
oi.id,
oi.user_id,
oi.order_title,
oi.order_no,
oi.order_status,
oi.original_amount,
oi.derate_amount,
oi.order_amount,
oi.item_type,
oi.pay_way,
od.id order_detial_id,
od.item_id,
od.item_name,
od.item_url,
od.item_price
FROM
order_info oi
INNER JOIN order_detail od ON od.order_id = oi.id
WHERE
oi.user_id = #{userId}
AND oi.is_deleted = 0
ORDER BY
oi.id DESC
</select>
</mapper>
解决:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.tingshu.order.mapper.OrderInfoMapper">
<!--订单及明细自定义结果集-->
<resultMap id="orderInfoMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
<id column="id" property="id"></id>
<collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" select="selectDetail" column="id" autoMapping="true">
</collection>
</resultMap>
<select id="selectDetail" resultType="com.atguigu.tingshu.model.order.OrderDetail">
select * from order_detail where order_id = #{orerId} and is_deleted = 0
</select>
<!--订单分页查询-->
<select id="getUserOrderByPge" resultMap="orderInfoMap">
select
oi.id,
oi.user_id,
oi.order_title,
oi.order_no,
oi.order_status,
oi.original_amount,
oi.order_amount,
oi.derate_amount,
oi.pay_way
from order_info oi
where oi.user_id = #{userId} and oi.is_deleted = 0
</select>
</mapper>
应用场景:
场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
service-util
利用redissonClient 发送延迟消息,提供公共业务
package com.atguigu.tingshu.common.delay;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2023-12-02 14:20
*/
@Slf4j
@Component
public class DelayMsgService {
@Autowired
private RedissonClient redissonClient;
/**
* 基于Redisson(Redis)实现延迟消息
*
* @param data 数据
* @param queueName 延迟队列名称
* @param ttl 延迟时间:单位s
*/
public void sendDelayMessage(String queueName, String data, int ttl) {
try {
//7.1 创建阻塞队列
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(queueName);
//7.2 基于阻塞队列创建延迟队列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
//7.3 发送延迟消息 测试阶段:设置为30s
delayedQueue.offer(data, ttl, TimeUnit.SECONDS);
log.info("发送延迟消息成功:{}", data);
} catch (Exception e) {
log.error("[延迟消息]发送异常:{}", data);
throw new RuntimeException(e);
}
}
}
service-order
订单模块中提交订单方法中
@Autowired
private DelayMsgService delayMsgService;
/**
* 提交订单
*
* @param userId
* @param orderInfoVo
* @return
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public String submitOrder(Long userId, OrderInfoVo orderInfoVo) {
//...省略
//7.发送延迟消息-完成订单延迟关单
delayMsgService.sendDelayMessage(KafkaConstant.QUEUE_ORDER_CANCEL, orderInfo.getId().toString(), 30);
//6.响应新增订单后订单编号(全局唯一)
return orderInfo.getOrderNo();
}
监听消息:
package com.atguigu.tingshu.order.delay;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.order.service.OrderInfoService;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: atguigu
* @create: 2023-12-02 11:56
*/
@Slf4j
@Component
public class DelayMsgConsumer {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderInfoService orderInfoService;
//项目启动后自动执行监听延迟队列消息方法
@PostConstruct
public void orderCanal() {
//开启线程 采用线程池工具类
log.info("开始监听延迟消息....");
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(()->{
RBlockingDeque<String> blockingQueue = redissonClient.getBlockingDeque(KafkaConstant.QUEUE_ORDER_CANCEL);
while (true) {
try {
String orderId = blockingQueue.take();
if (StringUtils.isNotBlank(orderId)) {
log.info("监听到延迟消息:{}", orderId);
orderInfoService.orderCancel(orderId);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
}
接口:
/**
* 取消订单
* @param orderId
*/
void orderCancel(String orderId);
实现类:
/**
* 关闭订单
* @param orderId
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void orderCancel(String orderId) {
//1.根据订单ID查询订单信息
OrderInfo orderInfo = orderInfoMapper.selectById(orderId);
if (orderInfo != null && SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) {
//2.判断是否满足订单关闭要求,订单为未支付状态
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
orderInfoMapper.updateById(orderInfo);
log.info("[订单服务],将未支付订单:{},取消。", orderId);
}
}