diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java index f2914e4..f46962b 100644 --- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java +++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java @@ -46,7 +46,11 @@ public enum ErrorCode { REPEATEDLY_ADDING_CHARACTER(50009, "重复添加键盘人设"), MAIL_SEND_BUSY(50010,"邮件发送频繁,1分钟后再试" ), PASSWORD_CAN_NOT_NULL(50011, "密码不能为空" ), - USER_HAS_EXISTED(50012, "用户已存在" ); + USER_HAS_EXISTED(50012, "用户已存在" ), + INSUFFICIENT_BALANCE(50013, "余额不足"), + THEME_NOT_FOUND(40410, "主题不存在"), + THEME_ALREADY_PURCHASED(50014, "主题已购买"), + THEME_NOT_AVAILABLE(50015, "主题不可购买"); /** * 状态码 */ diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java index 0e9e43f..85dc3e3 100644 --- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java +++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java @@ -87,7 +87,8 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/chat/talk", "/chat/save_embed", "/themes/listByStyle", - "/wallet/balance" + "/wallet/balance", + "/themes/purchase" }; } @Bean diff --git a/src/main/java/com/yolo/keyborad/controller/ThemesController.java b/src/main/java/com/yolo/keyborad/controller/ThemesController.java index c931a26..b1f1e48 100644 --- a/src/main/java/com/yolo/keyborad/controller/ThemesController.java +++ b/src/main/java/com/yolo/keyborad/controller/ThemesController.java @@ -1,19 +1,20 @@ package com.yolo.keyborad.controller; +import cn.dev33.satoken.stp.StpUtil; import com.yolo.keyborad.common.BaseResponse; import com.yolo.keyborad.common.ResultUtils; +import com.yolo.keyborad.model.dto.purchase.ThemePurchaseReq; +import com.yolo.keyborad.model.vo.purchase.ThemePurchaseRespVO; import com.yolo.keyborad.model.vo.themes.KeyboardThemeStylesRespVO; import com.yolo.keyborad.model.vo.themes.KeyboardThemesRespVO; +import com.yolo.keyborad.service.KeyboardThemePurchaseService; import com.yolo.keyborad.service.KeyboardThemeStylesService; import com.yolo.keyborad.service.KeyboardThemesService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -33,6 +34,9 @@ public class ThemesController { @Resource private KeyboardThemeStylesService keyboardThemeStylesService; + @Resource + private KeyboardThemePurchaseService themePurchaseService; + @GetMapping("/listByStyle") @Operation(summary = "按风格查询主题", description = "按主题风格查询主题列表接口") @@ -46,5 +50,12 @@ public class ThemesController { return ResultUtils.success(keyboardThemeStylesService.selectAllThemeStyles()); } + @PostMapping("/purchase") + @Operation(summary = "购买主题", description = "购买主题接口,扣减用户余额") + public BaseResponse purchaseTheme(@RequestBody ThemePurchaseReq req) { + Long userId = StpUtil.getLoginIdAsLong(); + ThemePurchaseRespVO result = themePurchaseService.purchaseTheme(userId, req.getThemeId()); + return ResultUtils.success(result); + } } diff --git a/src/main/java/com/yolo/keyborad/model/dto/purchase/ThemePurchaseReq.java b/src/main/java/com/yolo/keyborad/model/dto/purchase/ThemePurchaseReq.java new file mode 100644 index 0000000..749d867 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/dto/purchase/ThemePurchaseReq.java @@ -0,0 +1,14 @@ +package com.yolo.keyborad.model.dto.purchase; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 主题购买请求 + */ +@Data +public class ThemePurchaseReq { + + @Schema(description = "主题ID") + private Long themeId; +} diff --git a/src/main/java/com/yolo/keyborad/model/vo/purchase/ThemePurchaseRespVO.java b/src/main/java/com/yolo/keyborad/model/vo/purchase/ThemePurchaseRespVO.java new file mode 100644 index 0000000..0fe45ba --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/vo/purchase/ThemePurchaseRespVO.java @@ -0,0 +1,26 @@ +package com.yolo.keyborad.model.vo.purchase; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 主题购买响应 + */ +@Schema(description = "主题购买响应") +@Data +public class ThemePurchaseRespVO { + + @Schema(description = "订单号") + private String orderNo; + + @Schema(description = "主题ID") + private Long themeId; + + @Schema(description = "支付金额") + private BigDecimal paidAmount; + + @Schema(description = "剩余余额") + private BigDecimal remainingBalance; +} diff --git a/src/main/java/com/yolo/keyborad/service/KeyboardThemePurchaseService.java b/src/main/java/com/yolo/keyborad/service/KeyboardThemePurchaseService.java index 622a5bb..da09906 100644 --- a/src/main/java/com/yolo/keyborad/service/KeyboardThemePurchaseService.java +++ b/src/main/java/com/yolo/keyborad/service/KeyboardThemePurchaseService.java @@ -2,12 +2,16 @@ package com.yolo.keyborad.service; import com.yolo.keyborad.model.entity.KeyboardThemePurchase; import com.baomidou.mybatisplus.extension.service.IService; +import com.yolo.keyborad.model.vo.purchase.ThemePurchaseRespVO; /* * @author: ziin * @date: 2025/12/10 19:17 */ - + public interface KeyboardThemePurchaseService extends IService{ - + /** + * 购买主题 + */ + ThemePurchaseRespVO purchaseTheme(Long userId, Long themeId); } diff --git a/src/main/java/com/yolo/keyborad/service/KeyboardWalletTransactionService.java b/src/main/java/com/yolo/keyborad/service/KeyboardWalletTransactionService.java index 2f45cf9..7fef34d 100644 --- a/src/main/java/com/yolo/keyborad/service/KeyboardWalletTransactionService.java +++ b/src/main/java/com/yolo/keyborad/service/KeyboardWalletTransactionService.java @@ -2,12 +2,20 @@ package com.yolo.keyborad.service; import com.yolo.keyborad.model.entity.KeyboardWalletTransaction; import com.baomidou.mybatisplus.extension.service.IService; + +import java.math.BigDecimal; + /* * @author: ziin * @date: 2025/12/10 18:54 */ - + public interface KeyboardWalletTransactionService extends IService{ - + /** + * 创建钱包交易记录 + */ + KeyboardWalletTransaction createTransaction(Long userId, Long orderId, BigDecimal amount, + Short type, BigDecimal beforeBalance, + BigDecimal afterBalance, String description); } diff --git a/src/main/java/com/yolo/keyborad/service/impl/KeyboardThemePurchaseServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/KeyboardThemePurchaseServiceImpl.java index 3d1a66e..015d5a7 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/KeyboardThemePurchaseServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/KeyboardThemePurchaseServiceImpl.java @@ -1,8 +1,22 @@ package com.yolo.keyborad.service.impl; +import com.yolo.keyborad.common.ErrorCode; +import com.yolo.keyborad.exception.BusinessException; +import com.yolo.keyborad.model.entity.KeyboardThemes; +import com.yolo.keyborad.model.entity.KeyboardUserWallet; +import com.yolo.keyborad.model.entity.KeyboardWalletTransaction; +import com.yolo.keyborad.model.vo.purchase.ThemePurchaseRespVO; +import com.yolo.keyborad.service.KeyboardThemesService; +import com.yolo.keyborad.service.KeyboardUserWalletService; +import com.yolo.keyborad.service.KeyboardWalletTransactionService; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Autowired; -import java.util.List; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.UUID; + import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yolo.keyborad.model.entity.KeyboardThemePurchase; import com.yolo.keyborad.mapper.KeyboardThemePurchaseMapper; @@ -11,8 +25,120 @@ import com.yolo.keyborad.service.KeyboardThemePurchaseService; * @author: ziin * @date: 2025/12/10 19:17 */ - + @Service public class KeyboardThemePurchaseServiceImpl extends ServiceImpl implements KeyboardThemePurchaseService{ + @Autowired + private KeyboardThemesService themesService; + + @Autowired + private KeyboardUserWalletService walletService; + + @Autowired + private KeyboardWalletTransactionService transactionService; + + @Override + @Transactional(rollbackFor = Exception.class) + public ThemePurchaseRespVO purchaseTheme(Long userId, Long themeId) { + // 1. 验证主题是否存在且可购买 + // 从数据库获取主题信息 + KeyboardThemes theme = themesService.getById(themeId); + // 检查主题是否存在或已被删除 + if (theme == null || theme.getDeleted()) { + throw new BusinessException(ErrorCode.THEME_NOT_FOUND); + } + // 检查主题状态是否可用(上架状态) + if (!theme.getThemeStatus()) { + throw new BusinessException(ErrorCode.THEME_NOT_AVAILABLE); + } + + // 2. 检查是否已购买 + // 查询用户是否已经购买过该主题(支付状态为1表示已支付) + Long purchaseCount = this.lambdaQuery() + .eq(KeyboardThemePurchase::getUserId, userId) + .eq(KeyboardThemePurchase::getThemeId, themeId) + .eq(KeyboardThemePurchase::getPayStatus, (short) 1) + .count(); + // 如果已购买,抛出异常 + if (purchaseCount > 0) { + throw new BusinessException(ErrorCode.THEME_ALREADY_PURCHASED); + } + + // 3. 获取用户钱包 + // 查询用户钱包信息 + KeyboardUserWallet wallet = walletService.lambdaQuery() + .eq(KeyboardUserWallet::getUserId, userId) + .one(); + // 如果钱包不存在,抛出余额不足异常 + if (wallet == null) { + throw new BusinessException(ErrorCode.INSUFFICIENT_BALANCE); + } + + // 4. 检查余额是否充足 + // 获取主题价格 + BigDecimal themePrice = theme.getThemePrice(); + // 比较钱包余额和主题价格,余额不足则抛出异常 + if (wallet.getBalance().compareTo(themePrice) < 0) { + throw new BusinessException(ErrorCode.INSUFFICIENT_BALANCE); + } + + // 5. 扣减余额(使用乐观锁) + // 记录扣款前余额 + BigDecimal beforeBalance = wallet.getBalance(); + // 计算扣款后余额 + BigDecimal afterBalance = beforeBalance.subtract(themePrice); + // 更新钱包余额 + wallet.setBalance(afterBalance); + wallet.setUpdatedAt(new Date()); + // 执行更新操作,乐观锁机制确保并发安全 + boolean updateSuccess = walletService.updateById(wallet); + if (!updateSuccess) { + throw new BusinessException(ErrorCode.OPERATION_ERROR); + } + + // 6. 创建购买记录 + // 生成唯一订单号:ORDER_时间戳_8位UUID + String orderNo = "ORDER_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8); + // 构建购买记录对象 + KeyboardThemePurchase purchase = new KeyboardThemePurchase(); + purchase.setOrderNo(orderNo); // 订单号 + purchase.setUserId(userId); // 用户ID + purchase.setThemeId(themeId); // 主题ID + purchase.setCostPoints(themePrice.intValue()); // 消费积分 + purchase.setPaidPoints(themePrice.intValue()); // 实付积分 + purchase.setPayStatus((short) 1); // 支付状态:1-已支付 + purchase.setCreatedAt(new Date()); // 创建时间 + purchase.setPaidAt(new Date()); // 支付时间 + purchase.setUpdatedAt(new Date()); // 更新时间 + // 保存购买记录到数据库 + this.save(purchase); + + // 7. 创建钱包交易记录 + // 调用交易服务创建一条钱包交易记录 + KeyboardWalletTransaction transaction = transactionService.createTransaction( + userId, // 用户ID + purchase.getId(), // 关联的购买记录ID + themePrice.negate(), // 交易金额(负数表示支出) + (short) 1, // 交易类型:1-购买主题 + beforeBalance, // 交易前余额 + afterBalance, // 交易后余额 + "购买主题: " + theme.getThemeName() // 交易备注 + ); + + // 8. 更新购买记录的交易ID + // 将交易记录ID关联到购买记录中 + purchase.setWalletTxId(transaction.getId()); + this.updateById(purchase); + + // 9. 构造返回结果 + // 创建响应对象,封装购买结果信息 + ThemePurchaseRespVO respVO = new ThemePurchaseRespVO(); + respVO.setOrderNo(orderNo); // 订单号 + respVO.setThemeId(themeId); // 主题ID + respVO.setPaidAmount(themePrice); // 支付金额 + respVO.setRemainingBalance(afterBalance); // 剩余余额 + + return respVO; + } } diff --git a/src/main/java/com/yolo/keyborad/service/impl/KeyboardWalletTransactionServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/KeyboardWalletTransactionServiceImpl.java index d2ac966..87a7a66 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/KeyboardWalletTransactionServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/KeyboardWalletTransactionServiceImpl.java @@ -2,6 +2,8 @@ package com.yolo.keyborad.service.impl; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Autowired; +import java.math.BigDecimal; +import java.util.Date; import java.util.List; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yolo.keyborad.mapper.KeyboardWalletTransactionMapper; @@ -11,8 +13,24 @@ import com.yolo.keyborad.service.KeyboardWalletTransactionService; * @author: ziin * @date: 2025/12/10 18:54 */ - + @Service public class KeyboardWalletTransactionServiceImpl extends ServiceImpl implements KeyboardWalletTransactionService{ + @Override + public KeyboardWalletTransaction createTransaction(Long userId, Long orderId, BigDecimal amount, + Short type, BigDecimal beforeBalance, + BigDecimal afterBalance, String description) { + KeyboardWalletTransaction transaction = new KeyboardWalletTransaction(); + transaction.setUserId(userId); + transaction.setOrderId(orderId); + transaction.setAmount(amount); + transaction.setType(type); + transaction.setBeforeBalance(beforeBalance); + transaction.setAfterBalance(afterBalance); + transaction.setDescription(description); + transaction.setCreatedAt(new Date()); + this.save(transaction); + return transaction; + } }