|
@@ -1197,15 +1197,13 @@ public OrderInfoVo trade(TradeVo tradeVo) {
|
|
|
```java
|
|
|
/**
|
|
|
* 扣减账户余额
|
|
|
- *
|
|
|
* @param accountDeductVo
|
|
|
* @return
|
|
|
*/
|
|
|
-@Operation(summary = "扣减账户余额")
|
|
|
+@Operation(summary = "检查扣减账户余额")
|
|
|
@PostMapping("/userAccount/checkAndDeduct")
|
|
|
public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo) {
|
|
|
- //调用业务层完成余额扣减
|
|
|
- userAccountService.checkAndDeduct(accountDeductVo);
|
|
|
+ userAccountService.deduct(accountDeductVo);
|
|
|
return Result.ok();
|
|
|
}
|
|
|
```
|
|
@@ -1214,31 +1212,41 @@ public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo) {
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
- * 扣减账户余额
|
|
|
- *
|
|
|
+ * 检查扣减账户余额
|
|
|
* @param accountDeductVo
|
|
|
- * @return
|
|
|
*/
|
|
|
-void checkAndDeduct(AccountDeductVo accountDeductVo);
|
|
|
+void deduct(AccountDeductVo accountDeductVo);
|
|
|
```
|
|
|
|
|
|
业务实现类**UserAccountService**
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
- * 扣减账户余额
|
|
|
+ * 检查扣减账户余额
|
|
|
+ * 避免账户余额超扣,采用分布式锁;数据库锁(悲观锁或乐观锁)
|
|
|
*
|
|
|
* @param accountDeductVo
|
|
|
- * @return
|
|
|
*/
|
|
|
@Override
|
|
|
-public void checkAndDeduct(AccountDeductVo accountDeductVo) {
|
|
|
- //1.扣减账户余额
|
|
|
- int rows = userAccountMapper.checkAndDeduct(accountDeductVo);
|
|
|
- if (rows == 0) {
|
|
|
- throw new RuntimeException("账户余额不足");
|
|
|
+//@Transactional(rollbackFor = Exception.class) //TODO 引入Seata一定要去掉,否则会导致分布式事务失效
|
|
|
+public void deduct(AccountDeductVo accountDeductVo) {
|
|
|
+ //1.采用数据库悲观锁,检查账户余额是否满足扣款需求
|
|
|
+ BigDecimal amount = accountDeductVo.getAmount();
|
|
|
+ Long userId = accountDeductVo.getUserId();
|
|
|
+ UserAccount userAccount = userAccountMapper.checkDeduction(userId, amount);
|
|
|
+ if (userAccount == null) {
|
|
|
+ throw new GuiguException(500, "账户余额不足!");
|
|
|
}
|
|
|
- //2.新增账户变动日志
|
|
|
+ //2.扣减账户余额
|
|
|
+ userAccountMapper.update(
|
|
|
+ null,
|
|
|
+ new LambdaUpdateWrapper<UserAccount>()
|
|
|
+ .eq(UserAccount::getUserId, userId)
|
|
|
+ .setSql("total_amount = total_amount -" + amount)
|
|
|
+ .setSql("available_amount = available_amount -" + amount)
|
|
|
+ .setSql("total_pay_amount = total_pay_amount +" + amount)
|
|
|
+ );
|
|
|
+ //3.新增账户变动日志
|
|
|
this.saveUserAccountDetail(accountDeductVo.getUserId(), accountDeductVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountDeductVo.getAmount(), accountDeductVo.getOrderNo());
|
|
|
}
|
|
|
```
|
|
@@ -1249,35 +1257,38 @@ public void checkAndDeduct(AccountDeductVo accountDeductVo) {
|
|
|
package com.atguigu.tingshu.account.mapper;
|
|
|
|
|
|
import com.atguigu.tingshu.model.account.UserAccount;
|
|
|
-import com.atguigu.tingshu.vo.account.AccountDeductVo;
|
|
|
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 accountDeductVo
|
|
|
+ * 采用悲观锁,检查账户余额是否满足扣款需求
|
|
|
+ * @param userId
|
|
|
+ * @param amount
|
|
|
+ * @return
|
|
|
*/
|
|
|
- int checkAndDeduct(@Param("vo") AccountDeductVo accountDeductVo);
|
|
|
+ UserAccount checkDeduction(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**UserAccountMapper.xml**
|
|
|
|
|
|
```xml
|
|
|
-<!--扣减余额-->
|
|
|
-<update id="checkAndDeduct">
|
|
|
- UPDATE user_account
|
|
|
- SET total_amount = total_amount - #{vo.amount},
|
|
|
- available_amount = available_amount - #{vo.amount},
|
|
|
- total_pay_amount = total_pay_amount + #{vo.amount}
|
|
|
+<!--采用悲观锁,检查账户余额是否满足扣款需求-->
|
|
|
+<select id="checkDeduction" resultType="com.atguigu.tingshu.model.account.UserAccount">
|
|
|
+ SELECT
|
|
|
+ *
|
|
|
+ FROM
|
|
|
+ user_account
|
|
|
WHERE
|
|
|
- user_id = #{vo.userId}
|
|
|
- AND available_amount >= #{vo.amount}
|
|
|
-</update>
|
|
|
+ user_id = #{userId}
|
|
|
+ AND available_amount >= #{amount} FOR UPDATE
|
|
|
+</select>
|
|
|
```
|
|
|
|
|
|
在`service-account-client`模块中提供Feign接口:**AccountFeignClient**
|
|
@@ -1302,6 +1313,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|
|
@FeignClient(value = "service-account", path = "api/account", fallback = AccountDegradeFeignClient.class)
|
|
|
public interface AccountFeignClient {
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
* 扣减账户余额
|
|
|
*
|
|
@@ -1310,9 +1322,7 @@ public interface AccountFeignClient {
|
|
|
*/
|
|
|
@PostMapping("/userAccount/checkAndDeduct")
|
|
|
public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo);
|
|
|
-
|
|
|
}
|
|
|
-
|
|
|
```
|
|
|
|
|
|
服务降级类:**AccountDegradeFeignClient**
|
|
@@ -1327,18 +1337,19 @@ import com.atguigu.tingshu.vo.account.AccountDeductVo;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
-@Component
|
|
|
@Slf4j
|
|
|
+@Component
|
|
|
public class AccountDegradeFeignClient implements AccountFeignClient {
|
|
|
|
|
|
@Override
|
|
|
public Result checkAndDeduct(AccountDeductVo accountDeductVo) {
|
|
|
+ log.error("【用户服务】提供checkAndDeduct远程调用失败");
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### 2.3.2 新增购买记录
|
|
|
+### 2.3.2 虚拟物品发货
|
|
|
|
|
|
> YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/118
|
|
|
|
|
@@ -1348,13 +1359,13 @@ public class AccountDegradeFeignClient implements AccountFeignClient {
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
- * 用户付款成功后虚拟物品(VIP,专辑,声音)发货
|
|
|
+ * 用户支付成功后,虚拟物品发货
|
|
|
* @param userPaidRecordVo
|
|
|
* @return
|
|
|
*/
|
|
|
-@Operation(summary = "用户付款成功后虚拟物品发货")
|
|
|
+@Operation(summary = "用户支付成功后,虚拟物品发货")
|
|
|
@PostMapping("/userInfo/savePaidRecord")
|
|
|
-public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo){
|
|
|
+public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo) {
|
|
|
userInfoService.savePaidRecord(userPaidRecordVo);
|
|
|
return Result.ok();
|
|
|
}
|
|
@@ -1383,95 +1394,97 @@ private UserVipServiceMapper userVipServiceMapper;
|
|
|
private VipServiceConfigMapper vipServiceConfigMapper;
|
|
|
|
|
|
/**
|
|
|
- * 保存用户不同购买项目类型的购买记录
|
|
|
+ * 用户支付成功后,虚拟物品发货
|
|
|
*
|
|
|
* @param userPaidRecordVo
|
|
|
+ * @return
|
|
|
*/
|
|
|
@Override
|
|
|
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
|
|
|
- //1001-专辑 1002-声音 1003-vip会员
|
|
|
+ //TODO 策略模式+工厂模式优化
|
|
|
+ //付款项目类型: 1001-专辑 1002-声音 1003-vip会员
|
|
|
String itemType = userPaidRecordVo.getItemType();
|
|
|
- //1.处理购买项目类型-声音
|
|
|
- if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
|
|
|
- //1.根据订单编号查询是否处理过声音购买记录
|
|
|
- LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
|
|
- userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
|
|
|
- userPaidTrackLambdaQueryWrapper.select(UserPaidTrack::getId);
|
|
|
- if (userPaidTrackMapper.selectCount(userPaidTrackLambdaQueryWrapper) > 0) {
|
|
|
+ //1.处理虚拟物品"专辑"发货逻辑
|
|
|
+ if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
|
|
|
+ //1.1 根据订单编号查询已购专辑记录,如果存在,则返回
|
|
|
+ Long count = userPaidAlbumMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<UserPaidAlbum>()
|
|
|
+ .eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo())
|
|
|
+ );
|
|
|
+ if (count > 0) {
|
|
|
+ log.info("已存在该订单:{}的已购专辑记录", userPaidRecordVo.getOrderNo());
|
|
|
return;
|
|
|
}
|
|
|
- //2.保存声音购买记录
|
|
|
- //2.1 根据声音ID查询声音信息获取所属专辑ID
|
|
|
- List<Long> trackIdList = userPaidRecordVo.getItemIdList();
|
|
|
- if (CollectionUtil.isNotEmpty(trackIdList)) {
|
|
|
- TrackInfo trackInfo = albumFeignClient.getTrackInfo(trackIdList.get(0)).getData();
|
|
|
- Long albumId = trackInfo.getAlbumId();
|
|
|
- for (Long trackId : trackIdList) {
|
|
|
- UserPaidTrack userPaidTrack = new UserPaidTrack();
|
|
|
- userPaidTrack.setAlbumId(albumId);
|
|
|
- userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
- userPaidTrack.setTrackId(trackId);
|
|
|
- userPaidTrack.setUserId(userPaidRecordVo.getUserId());
|
|
|
- userPaidTrackMapper.insert(userPaidTrack);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
|
|
|
- //2.TODO 处理购买项目类型-专辑
|
|
|
- //2.1 根据订单编号查询是否处理过专辑购买记录
|
|
|
- LambdaQueryWrapper<UserPaidAlbum> paidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
|
|
- paidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
|
|
|
- if (userPaidAlbumMapper.selectCount(paidAlbumLambdaQueryWrapper) > 0) {
|
|
|
+ //1.2 如果不存在,则构建专辑已购记录完成保存
|
|
|
+ UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
|
|
|
+ userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
+ userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
|
|
|
+ userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
|
|
|
+ userPaidAlbumMapper.insert(userPaidAlbum);
|
|
|
+ } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
|
|
|
+ //2. 处理虚拟物品"声音"发货逻辑
|
|
|
+ //2.1 根据订单编号查询已购声音记录,如果存在,则返回
|
|
|
+ Long count = userPaidTrackMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<UserPaidTrack>()
|
|
|
+ .eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo())
|
|
|
+ );
|
|
|
+ if (count > 0) {
|
|
|
+ log.info("已存在该订单:{}的已购声音记录", userPaidRecordVo.getOrderNo());
|
|
|
return;
|
|
|
}
|
|
|
- //2.2 保存专辑购买记录
|
|
|
- if (CollectionUtil.isNotEmpty(userPaidRecordVo.getItemIdList())) {
|
|
|
- Long albumId = userPaidRecordVo.getItemIdList().get(0);
|
|
|
- UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
|
|
|
- userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
- userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
|
|
|
- userPaidAlbum.setAlbumId(albumId);
|
|
|
- userPaidAlbumMapper.insert(userPaidAlbum);
|
|
|
+ //2.2 如果不存在,则构建声音已购记录完成保存
|
|
|
+ TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
|
|
|
+ List<Long> trackIdList = userPaidRecordVo.getItemIdList();
|
|
|
+ for (Long trackId : trackIdList) {
|
|
|
+ UserPaidTrack userPaidTrack = new UserPaidTrack();
|
|
|
+ userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
+ userPaidTrack.setUserId(userPaidRecordVo.getUserId());
|
|
|
+ userPaidTrack.setAlbumId(trackInfo.getAlbumId());
|
|
|
+ userPaidTrack.setTrackId(trackId);
|
|
|
+ userPaidTrackMapper.insert(userPaidTrack);
|
|
|
}
|
|
|
} else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
|
|
|
- //3.处理购买项目类型-VIP会员
|
|
|
- //3.1 根据订单编号查询是否处理过VIP购买记录
|
|
|
- LambdaQueryWrapper<UserVipService> userVipServiceLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
|
|
- userVipServiceLambdaQueryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo());
|
|
|
- Long count = userVipServiceMapper.selectCount(userVipServiceLambdaQueryWrapper);
|
|
|
+ //3. 处理虚拟物品"会员"发货逻辑
|
|
|
+ //3.1 根据订单编号查询已购会员记录,如果存在,则返回
|
|
|
+ Long count = userVipServiceMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<UserVipService>()
|
|
|
+ .eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo())
|
|
|
+ );
|
|
|
if (count > 0) {
|
|
|
return;
|
|
|
}
|
|
|
- //3.2 保存本次会员购买记录
|
|
|
- UserVipService userVipService = new UserVipService();
|
|
|
- //3.2.1 获取当前用户VIP身份
|
|
|
+ //3.2 判断当前用户是否为会员
|
|
|
Boolean isVIP = false;
|
|
|
- UserInfoVo userInfoVo = getUserInfo(userPaidRecordVo.getUserId());
|
|
|
- if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) {
|
|
|
+ UserInfoVo userInfoVo = this.getUserInfo(userPaidRecordVo.getUserId());
|
|
|
+ Date now = new Date();
|
|
|
+ if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(now)) {
|
|
|
isVIP = true;
|
|
|
}
|
|
|
- //3.2.2 计算本次会员开始及过期时间
|
|
|
- //3.2.3 获取VIP套餐信息
|
|
|
+ //3.3 新增会员购买记录
|
|
|
+ UserVipService userVipService = new UserVipService();
|
|
|
+ userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
+ userVipService.setUserId(userPaidRecordVo.getUserId());
|
|
|
+ //3.3.1 获取会员套餐信息得到服务月数
|
|
|
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
|
|
|
Integer serviceMonth = vipServiceConfig.getServiceMonth();
|
|
|
-
|
|
|
- Date startTime = new Date();
|
|
|
+ //3.3.2 计算本次会员购买记录生效时间
|
|
|
+ //3.3.3 计算本次会员购买记录失效时间
|
|
|
if (!isVIP) {
|
|
|
- //普通用户
|
|
|
- userVipService.setStartTime(startTime);
|
|
|
- userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth));
|
|
|
+ //普通用户,生效时间为当前时间 失效时间=当前时间+购买会员套餐月数
|
|
|
+ userVipService.setStartTime(now);
|
|
|
+ DateTime expireTime = DateUtil.offsetMonth(now, serviceMonth);
|
|
|
+ userVipService.setExpireTime(expireTime);
|
|
|
} else {
|
|
|
- startTime = DateUtil.offsetDay(userInfoVo.getVipExpireTime(), 1);
|
|
|
- userVipService.setStartTime(startTime);
|
|
|
- userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth));
|
|
|
+ //会员用户,生效时间为=会员的到期时间+1 失效时间=会员的到期时间+购买会员套餐月数
|
|
|
+ Date vipExpireTime = userInfoVo.getVipExpireTime(); //6.14
|
|
|
+ userVipService.setStartTime(DateUtil.offsetDay(vipExpireTime, 1)); //6.15
|
|
|
+ userVipService.setExpireTime(DateUtil.offsetMonth(userVipService.getStartTime(), serviceMonth)); //7.15
|
|
|
}
|
|
|
- userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
|
|
|
- userVipService.setUserId(userPaidRecordVo.getUserId());
|
|
|
userVipServiceMapper.insert(userVipService);
|
|
|
- //3.3 修改用户VIP标识及过期时间
|
|
|
- UserInfo userInfo = new UserInfo();
|
|
|
- userInfo.setId(userInfoVo.getId());
|
|
|
- userInfo.setIsVip(1);
|
|
|
- userInfo.setVipExpireTime(userVipService.getExpireTime());
|
|
|
+ //3.4 更新用户会员标识以及更新会员过期时间
|
|
|
+ userInfoVo.setIsVip(1);
|
|
|
+ userInfoVo.setVipExpireTime(userVipService.getExpireTime());
|
|
|
+ UserInfo userInfo = BeanUtil.copyProperties(userInfoVo, UserInfo.class);
|
|
|
userInfoMapper.updateById(userInfo);
|
|
|
}
|
|
|
}
|