*谷粒随享**
学习目标:
购买包含分为:
在新用户第一次登录的时候,就进行了初始化账户余额信息操作!
当刷新主页的时候,会加载当前余额数据。
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.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() {
// 调用服务层方法
BigDecimal availableAmount = userAccountService.getAvailableAmount(AuthContextHolder.getUserId());
return Result.ok(availableAmount);
}
}
UserAccountService接口:
/**
* 获取账户可用余额
* @param userId
* @return
*/
BigDecimal getAvailableAmount(Long userId);
/**
* 根据用户Id 获取到可用余额对象
* @param userId
* @return
*/
UserAccount getUserAccountByUserId(Long userId);
UserAccountServiceImpl实现类:
@Override
public BigDecimal getAvailableAmount(Long userId) {
// 根据用户Id 获取到用户余额对象
UserAccount userAccount = this.getUserAccountByUserId(userId);
return userAccount.getAvailableAmount();
}
@Override
public UserAccount getUserAccountByUserId(Long userId) {
LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUserId, userId);
return this.getOne(queryWrapper);
}
在首页点击专辑封面标识为: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;
@Operation(summary = "获取全部VIP 服务配置信息")
@GetMapping("/vipServiceConfig/findAll")
public Result<List<VipServiceConfig>> findAll(){
// 调用服务层方法
List<VipServiceConfig> list = this.vipServiceConfigService.list();
return Result.ok(list);
}
}
在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
*/
@GuiGuLogin
@Operation(summary = "判断用户是否购买过专辑")
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId) {
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
Boolean flag = userInfoService.isPaidAlbum(userId, albumId);
return Result.ok(flag);
}
UserInfoService接口
/**
* 根据用户Id 与专辑Id 查询结果
* @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<UserPaidAlbum>()
.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId);
// 根据用户Id 与专辑Id 查询是否有记录
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);
熔断类:
@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
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) {
return Result.ok(vipServiceConfigService.getById(id));
}
在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) {
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.validation.annotation.Validated;
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({"unchecked", "rawtypes"})
public class OrderInfoApiController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 确认订单
* @param tradeVo
* @return
*/
@GuiGuLogin
@Operation(summary = "确认订单")
@PostMapping("/orderInfo/trade")
public Result<OrderInfoVo> trade(@RequestBody @Validated TradeVo tradeVo) {
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
OrderInfoVo orderInfoVo = orderInfoService.trade(tradeVo, userId);
// 返回数据
return Result.ok(orderInfoVo);
}
}
public interface OrderInfoService extends IService<OrderInfo> {
/**
* 确定订单
* @param tradeVo
* @param userId
* @return
*/
OrderInfoVo trade(TradeVo tradeVo, Long userId);
}
思路:
专辑订单
vip 订单
生成一个流水号存储到缓存,防止用户重复提交订单
给OrderInfoVo 实体类赋值
最后返回OrderInfoVo 对象
@Override
public OrderInfoVo trade(TradeVo tradeVo, Long userId) {
//获取用户信息
Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId);
Assert.notNull(userInfoVoResult);
UserInfoVo userInfoVo = userInfoVoResult.getData();
Assert.notNull(userInfoVo);
// 订单原始金额
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<>();
// 1001 专辑
if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_ALBUM)){
// 判断用户是否购买过专辑
Result<Boolean> isPaidAlbumResult = this.userInfoFeignClient.isPaidAlbum(tradeVo.getItemId());
Assert.notNull(isPaidAlbumResult);
Boolean isPaidAlbum = isPaidAlbumResult.getData();
Assert.notNull(isPaidAlbum);
if (isPaidAlbum){
throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
}
// 根据专辑Id 获取到专辑数据
Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(tradeVo.getItemId());
Assert.notNull(albumInfoResult,"返回专辑结果集不能为空");
AlbumInfo albumInfo = albumInfoResult.getData();
Assert.notNull(albumInfo,"专辑对象不能为空");
// 判断当前用户是否是vip
if (userInfoVo.getIsVip().intValue()==0){
// 非VIP 用户
originalAmount = albumInfo.getPrice();
// 判断是否打折 , 不等于-1 就是打折
if (albumInfo.getDiscount().intValue() != -1){
// 打折 100 8 100*0.8
derateAmount = originalAmount.multiply(new BigDecimal("10").subtract(albumInfo.getDiscount())).divide(new BigDecimal(10),2, RoundingMode.HALF_UP);
}
// 订单总价
orderAmount = originalAmount.subtract(derateAmount);
} else {
// VIP会员
originalAmount = albumInfo.getPrice();
//discount=-1,不打折,折扣如:8折 9.5折
if (albumInfo.getVipDiscount().intValue() != -1) {
derateAmount = albumInfo.getPrice().multiply(new BigDecimal(10).subtract(albumInfo.getVipDiscount())).divide(new BigDecimal(10), 2, RoundingMode.HALF_UP);
}
// 订单总价
orderAmount = originalAmount.subtract(derateAmount);
}
// 订单明细
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName(albumInfo.getAlbumTitle());
orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
orderDetailVo.setItemPrice(albumInfo.getPrice());
orderDetailVoList.add(orderDetailVo);
// 添加订单减免
if (originalAmount.subtract(orderAmount).doubleValue() != 0) {
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_ALBUM_DISCOUNT);
orderDerateVo.setDerateAmount(originalAmount.subtract(orderAmount));
orderDerateVoList.add(orderDerateVo);
}
} else if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_VIP)){
// 根据id 获取VIP 服务配置信息
Result<VipServiceConfig> vipServiceConfigResult = vipServiceConfigFeignClient.getVipServiceConfig(tradeVo.getItemId());
Assert.notNull(vipServiceConfigResult,"返回vip配置结果集不能为空");
VipServiceConfig vipServiceConfig = vipServiceConfigResult.getData();
Assert.notNull(vipServiceConfig,"返回vip配置对象不能为空");
originalAmount = vipServiceConfig.getPrice();
derateAmount = vipServiceConfig.getPrice().subtract(vipServiceConfig.getDiscountPrice());
orderAmount = originalAmount.subtract(derateAmount);
//订单明细
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName("VIP会员"+vipServiceConfig.getName());
orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
orderDetailVo.setItemPrice(vipServiceConfig.getDiscountPrice());
orderDetailVoList.add(orderDetailVo);
//添加订单减免
if (originalAmount.subtract(orderAmount).doubleValue() != 0) {
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setDerateAmount(originalAmount.subtract(orderAmount));
orderDerateVoList.add(orderDerateVo);
}
}
// 防重:生成一个唯一标识,保存到redis中一份
String tradeNoKey = "user:trade:" + userId;
// 定义一个流水号
String tradeNo = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(tradeNoKey, tradeNo);
//构造结果
OrderInfoVo orderInfoVo = new OrderInfoVo();
orderInfoVo.setItemType(tradeVo.getItemType());
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setTradeNo(tradeNo);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
orderInfoVo.setTimestamp(SignHelper.getTimestamp());
// 支付方式默认值 目的是防止用户在前端篡改金额数据
orderInfoVo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
// 生成签名
Map<String, Object> parameterMap = JSON.parseObject(JSON.toJSONString(orderInfoVo), Map.class);
String sign = SignHelper.getSign(parameterMap);
orderInfoVo.setSign(sign);
// 返回对象
return orderInfoVo;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94
service-user
模块
/**
* 根据专辑Id 获取到用户已支付声音Id列表
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "根据专辑id获取用户支付过的声音id列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackList(@PathVariable Long albumId){
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 根据用户Id 与 专辑Id 获取到已购买的声音Id 集合列表
List<Long> trackIdList = this.userInfoService.getUserPaidTrackList(userId,albumId);
// 返回声音Id 集合数据
return Result.ok(trackIdList);
}
/**
* 根据用户Id 与 专辑Id 获取到已购买的声音Id 集合列表
* @param userId
* @param albumId
* @return
*/
List<Long> getUserPaidTrackList(Long userId, Long albumId);
/**
* 根据用户Id 与专辑Id 查询已购买的声音ID列表
*
* @param userId
* @param albumId
* @return
*/
@Override
public List<Long> getUserPaidTrackList(Long userId, Long albumId) {
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getUserId, userId);
queryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
//获取已购买专辑列表
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper);
//获取已购买声音ID列表
if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
List<Long> trackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
return trackIdList;
}
return null;
}
service-user-client
模块中UserFeignClient提供Feign接口
/**
* 根据专辑Id 获取到用户已支付声音Id列表
* @param albumId
* @return
*/
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackList(@PathVariable Long albumId);
UserDegradeFeignClient熔断类
@Override
public Result<List<Long>> getUserPaidTrackList(Long albumId) {
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",0); // 记录购买集数
list.add(map);
/**
* 获取用户声音分集购买支付列表
* @param trackId
* @return
*/
@GuiGuLogin
@Operation(summary = "获取用户声音分集购买支付列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getUserTrackPaidList(@PathVariable Long trackId) {
// 获取购买记录集合
List<Map<String,Object>> map = trackInfoService.getUserTrackPaidList(trackId);
return Result.ok(map);
}
/**
* 根据声音Id 获取购买列表
* @param trackId
* @return
*/
List<Map<String, Object>> getUserTrackPaidList(Long trackId);
思路:
mybatis-plus 中的一些比较符号含义
lt:less than 小于 le:less than or equal to 小于等于 eq:equal to 等于 ne:not equal to 不等于 ge:greater than or equal to 大于等于 gt:greater than 大于
@Override
public List<Map<String, Object>> getUserTrackPaidList(Long trackId) {
//获取当前购买意向声音对象
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
//获取声音所属专辑对象
AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());
//获取专辑已支付的声音Id集合列表
Result<List<Long>> userPaidTrackListResult = userFeignClient.getUserPaidTrackList(albumInfo.getId());
List<Long> userPaidTrackIdList = userPaidTrackListResult.getData();
//获取当前声音且大于当前声音的全部声音ID
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
.gt(TrackInfo::getOrderNum, trackInfo.getOrderNum()).select(TrackInfo::getId);
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(queryWrapper);
if (!CollectionUtils.isEmpty(trackInfoList)) {
List<Long> trackIdAllList = trackInfoList.stream().map(TrackInfo::getId).collect(Collectors.toList());
//去掉用户已经支付的 剩余的是用户需要进行支付的声音ID
if (!CollectionUtils.isEmpty(userPaidTrackIdList)) {
trackIdAllList = trackIdAllList.stream().filter(trackId1 -> {
return !userPaidTrackIdList.contains(trackId1);
}).collect(Collectors.toList());
}
// 构造声音分集购买数据列表
List<Map<String, Object>> list = new ArrayList<>();
// 封装本集:需要付款的集数有
if (!CollectionUtils.isEmpty(trackIdAllList)) {
if (trackIdAllList.size() > 0) {
Map<String, Object> map = new HashMap<>();
map.put("name", "本专辑");
map.put("price", albumInfo.getPrice());
map.put("trackCount", 1);
list.add(map);
}
if (trackIdAllList.size() > 0 && trackIdAllList.size() <= 10) {
Map<String, Object> map = new HashMap<>();
int count = trackIdAllList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后" + trackIdAllList.size() + "集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
// 19
if (trackIdAllList.size() > 10) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(10));
map.put("name", "后10集");
map.put("price", price);
map.put("trackCount", 10);
list.add(map);
}
// 后20集
if (trackIdAllList.size() > 10 && trackIdAllList.size() <= 20) {
Map<String, Object> map = new HashMap<>();
int count = trackIdAllList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后" + count + "集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
if (trackIdAllList.size() > 20) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(20));
map.put("name", "后20集");
map.put("price", price);
map.put("trackCount", 20);
list.add(map);
}
//后30集
if (trackIdAllList.size() > 20 && trackIdAllList.size() <= 30) {
Map<String, Object> map = new HashMap<>();
int count = trackIdAllList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后" + count + "集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
if (trackIdAllList.size() > 30) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(30));
map.put("name", "后30集");
map.put("price", price);
map.put("trackCount", 30);
list.add(map);
}
//后50集
if (trackIdAllList.size() > 30 && trackIdAllList.size() <= 50) {
Map<String, Object> map = new HashMap<>();
int count = trackIdAllList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后" + count + "集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
// 最多购买50集;
if (trackIdAllList.size() > 50) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(50));
map.put("name", "后50集");
map.put("price", price);
map.put("trackCount", 50);
list.add(map);
}
return list;
}
}
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96
service-album
微服务中的TrackInfoApiController控制器添加代码
/**
* 根据声音ID+声音数量 获取下单付费声音列表
* @param trackId
* @param trackCount
* @return
*/
@Operation(summary = "批量获取下单付费声音列表")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getPaidTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount) {
// 调用服务层方法
List<TrackInfo> trackInfoList = trackInfoService.getPaidTrackInfoList(trackId, trackCount);
// 返回数据列表
return Result.ok(trackInfoList);
}
/**
* 根据声音ID+声音数量 获取下单付费声音列表
* @param trackId
* @param trackCount
* @return
*/
List<TrackInfo> findPaidTrackInfoList(Long trackId, Integer trackCount);
思路:
1. 根据声音Id 获取到当前声音对象
2. 获取已支付的声音Id 列表
3. 判断购买声音的声音集数是否大于0
1. 大于0
1. 查询当前专辑、并且 大于当前声音顺序号的声音、并且要按照序号进行升序排序,如果有已支付的声音Id,一定要除去已购买的声音Id 列表 并添加到返回的集合列表中
2. 等于0
1. 将查询的对象直接添加到的集合列表中
@Override
public List<TrackInfo> getPaidTrackInfoList(Long trackId, Integer trackCount) {
// 根据声音Id 获取到声音对象
TrackInfo trackInfo = this.getById(trackId);
Assert.notNull(trackCount, "声音为空");
Result<List<Long>> userPaidTrackListResult = userFeignClient.getUserPaidTrackList(trackInfo.getAlbumId());
Assert.notNull(userPaidTrackListResult);
List<Long> userPaidTrackList = userPaidTrackListResult.getData();
if (trackCount > 0) {
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum())
.orderByAsc(TrackInfo::getOrderNum);
if (!CollectionUtils.isEmpty(userPaidTrackList)) {
queryWrapper.notIn(TrackInfo::getId, userPaidTrackList);
}
queryWrapper.last("limit " + trackCount);
List<TrackInfo> trackInfoList = this.list(queryWrapper);
return trackInfoList;
}
// 获取已支付的声音id列表
return Arrays.asList(trackInfo);
}
/**
* 根据声音ID+声音数量 获取下单付费声音列表
* @param trackId
* @param trackCount
* @return
*/
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getPaidTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount);
熔断类:
@Override
public Result<List<TrackInfo>> getPaidTrackInfoList(Long trackId, Integer trackCount) {
return null;
}
思路:
判断用户是否购买过声音
未购买过:计算订单总价与订单明细
@Override
public OrderInfoVo trade(TradeVo tradeVo, Long userId) {
// 获取当前用户对象:
Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId);
UserInfoVo userInfoVo = userInfoVoResult.getData();
// 计算金额
// 订单原始金额
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<>();
// 判断是否购买专辑 1001
if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_ALBUM)) {
// 此处代码省略....
// 购买声音
} else if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_TRACK)) {
// 判断
if (tradeVo.getTrackCount().intValue() < 0) {
throw new GuiguException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
}
// 获取下单声音列表
Result<List<TrackInfo>> trackInfoListResult = trackInfoFeignClient.findPaidTrackInfoList(tradeVo.getItemId(), tradeVo.getTrackCount());
List<TrackInfo> trackInfoList = trackInfoListResult.getData();
// 判断用户是否购买过专辑声音
List<Long> trackIdList = trackInfoList.stream().map(TrackInfo::getId).collect(Collectors.toList());
// 远程调用获取结果
Result<Boolean> isPaidTrackResult = userInfoFeignClient.isPaidTrack(trackIdList);
Assert.notNull(isPaidTrackResult);
Boolean isPaidTrack = isPaidTrackResult.getData();
Assert.notNull(isPaidTrack);
if(isPaidTrack) {
throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
}
// 购买声音不支持折扣
Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(trackInfoList.get(0).getAlbumId());
AlbumInfo albumInfo = albumInfoResult.getData();
originalAmount = tradeVo.getTrackCount().intValue() > 0 ? albumInfo.getPrice().multiply(new BigDecimal(tradeVo.getTrackCount())) : albumInfo.getPrice();
// 计算订单总价
orderAmount = originalAmount;
// 循环遍历声音集合对象赋值订单明细
orderDetailVoList = trackInfoList.stream().map(trackInfo -> {
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(trackInfo.getId());
orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
orderDetailVo.setItemPrice(albumInfo.getPrice());
orderDetailVo.setItemName(trackInfo.getTrackTitle());
return orderDetailVo;
}).collect(Collectors.toList());
} else {
// 此处代码省略....
}
// 防重:生成一个唯一标识,保存到redis中一份
String tradeNoKey = "user:trade:" + userId;
// 定义一个流水号
String tradeNo = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(tradeNoKey, tradeNo);
//构造结果
OrderInfoVo orderInfoVo = new OrderInfoVo();
orderInfoVo.setItemType(tradeVo.getItemType());
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setTradeNo(tradeNo);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
orderInfoVo.setTimestamp(SignHelper.getTimestamp());
// 默认选择微信支付
//orderInfoVo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
Map map = JSON.parseObject(JSON.toJSONString(orderInfoVo), Map.class);
String sign = SignHelper.getSign(map);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
声音购买:
专辑购买:
VIP 购买:
在service-order
微服务中添加提交订单控制器
分集购买声音只支持余额支付
购买VIP或专辑分为余额,微信支付
我们将前端页面提交的数据统一封装到实体类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;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/98
service-account
微服务控制器UserAccountApiController 控制器
/**
* 检查锁定账户金额
* @param accountLockVo
* @return
*/
@GuiGuLogin
@Operation(summary = "检查与锁定账户金额")
@PostMapping("/userAccount/checkAndLock")
public Result<AccountLockResultVo> checkAndLock(@RequestBody AccountLockVo accountLockVo){
// 调用服务层方法
return this.userAccountService.checkAndLock(accountLockVo);
}
UserAccountService接口:
public interface UserAccountService extends IService<UserAccount> {
/**
* 检查与锁定账户金额
* @param accountLockVo
* @return
*/
Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo);
}
UserAccountServiceImpl实现类:
返回锁定对象,并将对象放入缓存中
@Autowired
private RedisTemplate redisTemplate;
/**
* 检查锁定余额;新增账户变动明细;将锁定结果写入Redis
*
* @param accountLockVo
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo) {
String reCheckLockKey = RedisConstant.ACCOUNT_MUTIPLE_CHECK + accountLockVo.getOrderNo();
Boolean reCheckLock = redisTemplate.opsForValue().setIfAbsent(reCheckLockKey, accountLockVo.getOrderNo(), 1, TimeUnit.HOURS);
if (!reCheckLock) {
//如果 isExist=false; 说明不是第一次执行,则直接将锁定结果对象从Redis查询返回
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + accountLockVo.getOrderNo();
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo != null) {
return Result.ok(accountLockResultVo);
} else {
//如果未产生锁定结果,需要再次提交
return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_RESULT_NULL);
}
}
//2.第一次执行账户锁定
//2.1 核对账户可用金额并锁定账户数据(悲观锁);查询返回的是满足要求的账户
UserAccount userAccount = userAccountMapper.check(accountLockVo.getUserId(), accountLockVo.getAmount());
if (userAccount == null) {
//说明锁定余额不足,删除重复锁定的Key
redisTemplate.delete(reCheckLockKey);
return Result.build(null, ResultCodeEnum.ACCOUNT_LESS);
}
//2.2 锁定账户金额
int lock = userAccountMapper.lock(accountLockVo.getUserId(), accountLockVo.getAmount());
if (lock == 0) {
//锁定失败
// 解除去重
this.redisTemplate.delete(reCheckLockKey);
return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_ERROR);
}
//2.3 账户检查且锁定成功,新增账号变动明细
this.saveAccountDetail(accountLockVo.getUserId(), "锁定:" + accountLockVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_LOCK, accountLockVo.getAmount(), "lock:" + accountLockVo.getOrderNo());
//2.4 封装锁定结果对象;将锁定结果对象存入Redis
AccountLockResultVo accountLockResultVo = new AccountLockResultVo();
accountLockResultVo.setUserId(accountLockVo.getUserId());
accountLockResultVo.setAmount(accountLockVo.getAmount());
accountLockResultVo.setContent(accountLockVo.getContent());
// 如果账户锁定成功的情况下,需要缓存锁定信息到redis。以方便将来解锁账户金额 或者 减账户金额
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + accountLockVo.getOrderNo();
redisTemplate.opsForValue().set(lockResultKey, accountLockResultVo, 1, TimeUnit.HOURS);
return Result.ok(accountLockResultVo);
}
UserAccountMapper.java
@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
/**
* 获取可用账户
* @param userId
* @param amount
* @return
*/
UserAccount check(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
UserAccountMapper.xml
<!--查询可用用户-->
<select id="check" resultMap="userAccountMap">
select * from user_account where user_id = #{userId} and available_amount >= #{amount} for update
</select>
UserAccountMapper.java
@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
/**
* 锁定账户金额
* @param userId
* @param amount
* @return
*/
Integer lock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
UserAccountMapper.xml
<!--锁定金额-->
<update id="lock">
update user_account
set lock_amount = lock_amount + #{amount}, available_amount = available_amount - #{amount}
where user_id = #{userId}
</update>
账户变动日志
@Autowired
private UserAccountDetailMapper userAccountDetailMapper;
/**
* 保存账号变动日志记录
* @param userId 用户ID
* @param title 变动详情
* @param tradeType 交易类型
* @param amount 变动金额
* @param orderNo 订单编号
*/
@Override
public void saveAccountDetail(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);
}
service-account-client
模块中添加 AccountFeignClient远程调用
package com.atguigu.tingshu.account.client;
import com.atguigu.tingshu.account.client.impl.AccountDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
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 qy
*/
@FeignClient(value = "service-account", path = "api/account", fallback = AccountDegradeFeignClient.class)
public interface AccountFeignClient {
/**
* 检查锁定账户金额
* @param accountLockVo
* @return
*/
@PostMapping("/userAccount/checkAndLock")
public Result<AccountLockResultVo> checkAndLock(@RequestBody AccountLockVo accountLockVo);
}
AccountDegradeFeignClient熔断类:
package com.atguigu.tingshu.account.client.impl;
import com.atguigu.tingshu.account.client.AccountFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import org.springframework.stereotype.Component;
@Component
public class AccountDegradeFeignClient implements AccountFeignClient {
@Override
public Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo) {
return null;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99
OrderInfoApiController
/**
* 提交订单
* @param orderInfoVo
* @return
*/
@GuiGuLogin
@Operation(summary = "提交订单")
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, Object>> submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) {
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
Map<String, Object> map = orderInfoService.submitOrder(orderInfoVo, userId);
// 返回数据
return Result.ok(map);
}
/**
* 提交保存订单
* @param orderInfoVo
* @param userId
* @return
*/
Map<String, Object> submitOrder(OrderInfoVo orderInfoVo, Long userId);
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private KafkaService kafkaService;
/**
* 提交保存订单
*
* @param orderInfoVo
* @param userId
* @return
*/
@Override
public Map<String, Object> submitOrder(OrderInfoVo orderInfoVo, Long userId) {
//获取支付方式存入变量,从入参中移除支付方式,避免验签失败
String payWay = orderInfoVo.getPayWay();
//1.验证签名
Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo);
paramsMap.remove("payWay");
SignHelper.checkSign(paramsMap);
//2.验证流水号,避免订单重复提交
String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
Boolean flag = (Boolean) redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(tradeNoKey), orderInfoVo.getTradeNo());
if (!flag) {
throw new GuiguException(ResultCodeEnum.ORDER_SUBMIT_REPEAT);
}
//3.保存订单-无论支付方式是哪种
String orderNo = this.saveOrderInfo(orderInfoVo, userId, payWay);
//3.判断支付类型-余额 1101-微信 1102-支付宝 1103-账户余额
if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
//3.1 保存订单及订单明细
try {
//3.2 账户余额检查及余额锁定
AccountLockVo accountLockVo = new AccountLockVo();
accountLockVo.setUserId(userId);
accountLockVo.setOrderNo(orderNo);
accountLockVo.setAmount(orderInfoVo.getOrderAmount());
accountLockVo.setContent(orderInfoVo.getOrderDetailVoList().get(0).getItemName());
Result<AccountLockResultVo> accountLockResultVoResult = accountFeignClient.checkAndLock(accountLockVo);
if (200 != accountLockResultVoResult.getCode()) {
//检查锁定余额失败
throw new GuiguException(accountLockResultVoResult.getCode(), accountLockResultVoResult.getMessage());
}
//3.3 支付成功扣减账户金额
kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_MINUS, orderNo);
//3.4 锁定成功-则认为余额扣减成功 修改订单状态为已支付
this.orderPaySuccess(orderNo);
} catch (Exception e) {
// 异常手动解锁账户
kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_UNLOCK, orderNo);
//抛出异常
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
} else {
//TODO 4.判断支付类型-微信
}
Map<String, Object> mapResult = new HashMap<>();
mapResult.put("orderNo", orderNo);
return mapResult;
}
业务逻辑
OrderInfoService接口
/**
* 保存订单
* @param orderInfoVo
* @param userId
* @param orderNo
* @param payWay
*/
String saveOrderInfo(OrderInfoVo orderInfoVo, Long userId, String payWay);
OrderInfoServiceImpl实现类
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderDerateMapper orderDerateMapper;
/**
* 保存订单
*
* @param orderInfoVo
* @param userId
* @param payWay
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String saveOrderInfo(OrderInfoVo orderInfoVo, Long userId, String payWay) {
//1.拷贝订单VO到订单PO对象
OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
//2.设置订单相关属性
//2.1 生成订单唯一编号
String orderNo = DateUtil.today().replace("-", "") + IdUtil.getSnowflakeNextIdStr();
orderInfo.setOrderNo(orderNo);
//2.2 设置订单名称
List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
orderInfo.setOrderTitle(orderDetailVoList.get(0).getItemName());
//2.3 设置用户ID
orderInfo.setUserId(userId);
//2.4 设置订单状态 未支付
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
//2.5 设置支付方式
orderInfo.setPayWay(payWay);
//2.5 保存订单
orderInfoMapper.insert(orderInfo);
//2.保存订单明细
if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
orderDetailVoList.forEach(orderDetailVo -> {
OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
orderDetail.setOrderId(orderInfo.getId());
orderDetailMapper.insert(orderDetail);
});
}
//3.保存订单减免明细
List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
orderDerateVoList.forEach(orderDerateVo -> {
OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
orderDerate.setOrderId(orderInfo.getId());
orderDerateMapper.insert(orderDerate);
});
}
return orderInfo.getOrderNo();
}
@Override
public void orderPaySuccess(String orderNo) {
// 根据orderNo 修改订单状态数据
OrderInfo orderInfoUpt = new OrderInfo();
orderInfoUpt.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
this.update(orderInfoUpt, new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
// 更新用户支付记录
OrderInfo orderInfo = this.getOrderInfoByOrderNo(orderNo);
List<Long> itemIdList = orderInfo.getOrderDetailList().stream().map(OrderDetail::getItemId).collect(Collectors.toList());
UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
userPaidRecordVo.setOrderNo(orderNo);
userPaidRecordVo.setUserId(orderInfo.getUserId());
userPaidRecordVo.setItemType(orderInfo.getItemType());
userPaidRecordVo.setItemIdList(itemIdList);
// 发送用户支付成功消息,监听并更新用户支付记录
kafkaService.sendMessage(KafkaConstant.QUEUE_USER_PAY_RECORD, JSON.toJSONString(userPaidRecordVo));
}
/**
* 根据订单orderNo 获取订单对象
* @param orderNo
* @return
*/
public OrderInfo getOrderInfoByOrderNo(String orderNo) {
// 获取订单对象
OrderInfo orderInfo = this.getOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderInfo.getId()));
List<OrderDerate> orderDerateList = orderDerateMapper.selectList(new LambdaQueryWrapper<OrderDerate>().eq(OrderDerate::getOrderId, orderInfo.getId()));
// 赋值订单明细
orderInfo.setOrderDetailList(orderDetailList);
// 赋值减免金额
orderInfo.setOrderDerateList(orderDerateList);
orderInfo.setOrderStatusName(getOrderStatusName(orderInfo.getOrderStatus()));
orderInfo.setPayWayName(getPayWayName(orderInfo.getPayWay()));
return orderInfo;
}
/**
* 根据订单状态获取订单状态名称
* @param orderStatus
* @return
*/
private String getOrderStatusName(String orderStatus) {
String orderStatusName = "";
if(orderStatus.equals(SystemConstant.ORDER_STATUS_UNPAID)) {
orderStatusName = "待支付";
} else if (orderStatus.equals(SystemConstant.ORDER_STATUS_PAID)) {
orderStatusName = "已支付";
} else {
orderStatusName = "已取消";
}
return orderStatusName;
}
/**
* 根据支付方式获取到支付名称
* @param payWay
* @return
*/
private String getPayWayName(String payWay) {
String payWayName = "";
if(payWay.equals(SystemConstant.ORDER_PAY_WAY_WEIXIN)) {
payWayName = "微信";
} else if (payWay.equals(SystemConstant.ORDER_PAY_WAY_ALIPAY)) {
payWayName = "支付宝";
} else {
payWayName = "余额";
}
return payWayName;
}
在service-user
模块中进行监听
package com.atguigu.tingshu.user.receiver;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class UserReceiver {
@Autowired
private UserInfoService userInfoService;
/**
* 更新用户支付记录
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_USER_PAY_RECORD)
public void updateUserPaidRecord(ConsumerRecord<String,String> record) {
UserPaidRecordVo userPaidRecordVo = JSON.parseObject(record.value(), UserPaidRecordVo.class);
log.info("更新用户支付记录: {}", JSON.toJSONString(userPaidRecordVo));
//通知更新用户账号
userInfoService.updateUserPaidRecord(userPaidRecordVo);
}
}
UserInfoService 接口中添加
/**
* 更新用户支付记录
* @param userPaidRecordVo
*/
void updateUserPaidRecord(UserPaidRecordVo userPaidRecordVo);
UserInfoServiceImpl实现类
@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
@Autowired
private AlbumFeignClient albumFeignClient;
/**
* 监听购买成功消息,新增用户购买记录
*
* @param userPaidRecordVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void processPaidRecord(UserPaidRecordVo userPaidRecordVo) {
//项目类型 1001-专辑 1002-声音 1003-vip会员
String itemType = userPaidRecordVo.getItemType();
//判断购买类型-VIP会员
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
//1.根据订单编号查询VIP购买记录
LambdaQueryWrapper<UserVipService> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userVipServiceMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.获取用户购买VIP的类别信息
Long vipServiceId = userPaidRecordVo.getItemIdList().get(0);
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipServiceId);
UserVipService userVipService = new UserVipService();
userVipService.setUserId(userPaidRecordVo.getUserId());
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
Date startTime = new Date();
//获取用户信息
UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
if (userInfo.getIsVip().intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) {
startTime = userInfo.getVipExpireTime();
}
DateTime expireTime = DateUtil.offsetMonth(startTime, vipServiceConfig.getServiceMonth());
userVipService.setExpireTime(expireTime);
userVipServiceMapper.insert(userVipService);
//更新用户vip信息
userInfo.setIsVip(1);
userInfo.setVipExpireTime(expireTime);
this.updateById(userInfo);
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
//判断购买类型-专辑
// 防止重复消费,如果有记录则直接停止
long count = userPaidAlbumMapper.selectCount(new LambdaQueryWrapper<UserPaidAlbum>().eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo()));
if (count > 0) return;
// 创建用户支付记录对象
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidAlbumMapper.insert(userPaidAlbum);
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
//判断购买类型-声音
// 防止重复消费
long count = userPaidTrackMapper.selectCount(new LambdaQueryWrapper<UserPaidTrack>().eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo()));
if (count > 0) return;
//远程调用专辑服务获取声音详情
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
//遍历购买项ID集合生成构建记录对象
userPaidRecordVo.getItemIdList().stream().forEach(itemId->{
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setAlbumId(trackInfo.getAlbumId());
userPaidTrack.setTrackId(itemId);
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidTrackMapper.insert(userPaidTrack);
});
}
}
service-album-client
模块中AlbumFeignClient添加远程调用
/**
* 根据Id 获取数据声音信息
*
* @param id
* @return
*/
@GetMapping("/trackInfo/getTrackInfo/{id}")
public Result<TrackInfo> getTrackInfo(@PathVariable Long id);
AlbumDegradeFeignClient熔断类:
@Override
public Result<TrackInfo> getTrackInfo(Long id) {
return null;
}
在service-account
模块中监听完成扣减账户余额或账户余额回滚。
/**
* 扣减锁定金额
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_MINUS)
public void minus(ConsumerRecord<String, String> record) {
String orderNo = record.value();
if (StringUtils.isEmpty(orderNo)) {
return;
}
//扣减锁定金额
userAccountService.minus(orderNo);
}
实现类:
/**
* 扣减锁定金额
*
* @param orderNo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void minus(String orderNo) {
//1.业务去重避免多次扣减
String reMinusKey = RedisConstant.BUSINESS_PREFIX + "minus:" + orderNo;
Boolean flag = redisTemplate.opsForValue().setIfAbsent(reMinusKey, orderNo, 1, TimeUnit.HOURS);
if (flag) {
//2.构建锁定金额对象的Key 从Redis中获取锁定结果对象
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo != null) {
int minus = userAccountMapper.minus(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
if(minus == 0) {
//解除去重
this.redisTemplate.delete(reMinusKey);
throw new GuiguException(ResultCodeEnum.ACCOUNT_MINUSLOCK_ERROR);
}
//记录日志
this.saveAccountDetail(accountLockResultVo.getUserId(), accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountLockResultVo.getAmount(), orderNo);
// 解锁账户金额之后,删除锁定缓存。以防止重复解锁
this.redisTemplate.delete(lockResultKey);
}
}
}
UserAccountMapper
/**
* 解锁账户
* @param userId
* @param amount
* @return
*/
Integer minus(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
UserAccountMapper.xml
<update id="minus">
update user_account
set lock_amount = lock_amount - #{amount}, total_amount = total_amount - #{amount}, total_pay_amount = total_pay_amount + #{amount}
where user_id = #{userId}
</update>
在service-account
微服务中的AccountReceiver 添加
/**
* 解锁锁定金额
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_UNLOCK)
public void unlock(ConsumerRecord<String, String> record) {
String orderNo = record.value();
if (StringUtils.isEmpty(orderNo)) {
return;
}
// 调用解除锁定
userAccountService.unlock(orderNo);
}
接口:
/**
* 解除锁定
* @param orderNo
*/
void unlock(String orderNo);
实现类:
@Override
@Transactional(rollbackFor = Exception.class)
public void unlock(String orderNo) {
//1.业务去重避免多次扣减
String reMinusKey = RedisConstant.BUSINESS_PREFIX + "unlock:" + orderNo;
Boolean flag = redisTemplate.opsForValue().setIfAbsent(reMinusKey, orderNo, 1, TimeUnit.HOURS);
if (!flag) {
return;
}
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo == null) {
return;
}
//恢复账户锁定
int unLock = userAccountMapper.unLock(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
if(unLock == 0) {
//解除去重
this.redisTemplate.delete(reMinusKey);
throw new GuiguException(ResultCodeEnum.ACCOUNT_UNLOCK_ERROR);
}
//记录账户变更明细
this.saveAccountDetail(accountLockResultVo.getUserId(), "解锁:"+accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_UNLOCK, accountLockResultVo.getAmount(), "unlock:"+orderNo);
// 解锁账户金额之后,删除锁定缓存。以防止重复解锁
this.redisTemplate.delete(lockResultKey);
}
Mapper.java
/**
* 调用解除锁定方法.
* @param userId
* @param amount
* @return
*/
int unLock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
Mapper.xml
<update id="unLock">
update user_account
set lock_amount = lock_amount - #{amount}, available_amount = available_amount + #{amount}
where user_id = #{userId}
</update>
当支付成功之后,点击查看订单
在service-order
微服务中添加
/**
* 查看我的订单
* @param orderNo
* @return
*/
@GuiGuLogin
@Operation(summary = "根据订单号获取订单信息")
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable String orderNo) {
OrderInfo orderInfo = orderInfoService.getOrderInfoByOrderNo(orderNo);
return Result.ok(orderInfo);
}
接口与实现
/**
* 查看我的订单
* @param orderNo
* @return
*/
OrderInfo getOrderInfoByOrderNo(String orderNo);
@Override
public OrderInfo getOrderInfoByOrderNo(String orderNo) {
OrderInfo orderInfo = this.getOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderInfo.getId()));
List<OrderDerate> orderDerateList = orderDerateMapper.selectList(new LambdaQueryWrapper<OrderDerate>().eq(OrderDerate::getOrderId, orderInfo.getId()));
orderInfo.setOrderDetailList(orderDetailList);
orderInfo.setOrderDerateList(orderDerateList);
orderInfo.setOrderStatusName(getOrderStatusName(orderInfo.getOrderStatus()));
orderInfo.setPayWayName(getPayWayName(orderInfo.getPayWay()));
return orderInfo;
}
service-order
的OrderInfoApiController 控制器 添加
/**
* 查看我的订单
*
* @param page
* @param limit
* @return
*/
@GuiGuLogin
@Operation(summary = "获取用户订单")
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result getUserOrderByPage(@PathVariable Long page, @PathVariable Long limit) {
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
Page<OrderInfo> pageInfo = new Page<>(page, limit);
// 调用服务层方法
orderInfoService.getUserOrderByPage(pageInfo, userId);
return Result.ok(pageInfo);
}
接口与实现
/**
* 查看我的订单
* @param pageParam
* @param userId
* @return
*/
void getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);
@Override
public IPage<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageParam, Long userId) {
// 调用mapper 层方法
IPage<OrderInfo> infoIPage = orderInfoMapper.getUserOrderByPage(pageParam,userId);
infoIPage.getRecords().forEach(item->{
// 设置状态名
item.setOrderStatusName(getOrderStatusName(item.getOrderStatus()));
item.setPayWayName(getPayWayName(item.getPayWay()));
});
return infoIPage;
}
mapper.java 接口
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
/**
* 查看分页订单列表
* @param pageParam
* @param userId
* @return
*/
IPage<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageParam, Long userId);
}
mapper.xml 映射文件
<?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 property="id" column="id"></id>
<collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" autoMapping="true">
<id property="id" column="detail_id"></id>
</collection>
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
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 detail_id,
od.item_id,
od.item_name,
od.item_url,
od.item_price
</sql>
<select id="getUserOrderByPage" resultMap="orderInfoMap">
select
<include refid="columns"></include>
from order_info oi inner join order_detail od on oi.id = od.order_id
where oi.user_id = #{userId}
order by oi.id desc
</select>
</mapper>
利用redissonClient 发送延迟消息。
/**
* 发送延迟消息
*/
private void sendDelayMessage(Long orderId) {
try {
// 创建一个队列
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(KafkaConstant.QUEUE_ORDER_CANCEL);
// 将队列放入延迟队列中
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
// 发送的内容
delayedQueue.offer(orderId.toString(), KafkaConstant.DELAY_TIME, TimeUnit.SECONDS);
log.info("添加延时队列成功 ,延迟时间:{},订单id:{}", KafkaConstant.DELAY_TIME, orderId);
} catch (Exception e) {
log.error("添加延时队列失败 ,延迟时间:{},订单id:{}", KafkaConstant.DELAY_TIME, orderId);
e.printStackTrace();
}
}
监听消息:
package com.atguigu.tingshu.order.handle;
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.redisson.api.RBlockingDeque;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Slf4j
@Component
public class RedisDelayHandle {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderInfoService orderInfoService;
@PostConstruct
public void listener() {
new Thread(()->{
while (true){
RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(KafkaConstant.QUEUE_ORDER_CANCEL);
try {
String orderId = blockingDeque.take();
if(!StringUtils.isEmpty(orderId)) {
log.info("接收延时队列成功,订单id:{}", orderId);
orderInfoService.orderCancel(Long.parseLong(orderId));
}
} catch (InterruptedException e) {
log.error("接收延时队列失败");
e.printStackTrace();
}
}
}).start();
}
}
接口:
public interface OrderInfoService extends IService<OrderInfo> {
/**
* 根据订单Id 取消订单
* @param orderId
*/
void orderCancel(Long orderId);
}
实现类:
@Override
public void orderCancel(Long orderId) {
OrderInfo orderInfoUpt = new OrderInfo();
orderInfoUpt.setId(orderId);
orderInfoUpt.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
this.updateById(orderInfoUpt);
}