feat(purchase): 新增主题购买全流程接口
新增主题购买功能,包括余额校验、订单生成、交易记录等完整流程。同时扩展错误码支持余额不足、主题不存在等场景。
This commit is contained in:
@@ -46,7 +46,11 @@ public enum ErrorCode {
|
|||||||
REPEATEDLY_ADDING_CHARACTER(50009, "重复添加键盘人设"),
|
REPEATEDLY_ADDING_CHARACTER(50009, "重复添加键盘人设"),
|
||||||
MAIL_SEND_BUSY(50010,"邮件发送频繁,1分钟后再试" ),
|
MAIL_SEND_BUSY(50010,"邮件发送频繁,1分钟后再试" ),
|
||||||
PASSWORD_CAN_NOT_NULL(50011, "密码不能为空" ),
|
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, "主题不可购买");
|
||||||
/**
|
/**
|
||||||
* 状态码
|
* 状态码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/chat/talk",
|
"/chat/talk",
|
||||||
"/chat/save_embed",
|
"/chat/save_embed",
|
||||||
"/themes/listByStyle",
|
"/themes/listByStyle",
|
||||||
"/wallet/balance"
|
"/wallet/balance",
|
||||||
|
"/themes/purchase"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
package com.yolo.keyborad.controller;
|
package com.yolo.keyborad.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yolo.keyborad.common.BaseResponse;
|
import com.yolo.keyborad.common.BaseResponse;
|
||||||
import com.yolo.keyborad.common.ResultUtils;
|
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.KeyboardThemeStylesRespVO;
|
||||||
import com.yolo.keyborad.model.vo.themes.KeyboardThemesRespVO;
|
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.KeyboardThemeStylesService;
|
||||||
import com.yolo.keyborad.service.KeyboardThemesService;
|
import com.yolo.keyborad.service.KeyboardThemesService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ public class ThemesController {
|
|||||||
@Resource
|
@Resource
|
||||||
private KeyboardThemeStylesService keyboardThemeStylesService;
|
private KeyboardThemeStylesService keyboardThemeStylesService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardThemePurchaseService themePurchaseService;
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/listByStyle")
|
@GetMapping("/listByStyle")
|
||||||
@Operation(summary = "按风格查询主题", description = "按主题风格查询主题列表接口")
|
@Operation(summary = "按风格查询主题", description = "按主题风格查询主题列表接口")
|
||||||
@@ -46,5 +50,12 @@ public class ThemesController {
|
|||||||
return ResultUtils.success(keyboardThemeStylesService.selectAllThemeStyles());
|
return ResultUtils.success(keyboardThemeStylesService.selectAllThemeStyles());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/purchase")
|
||||||
|
@Operation(summary = "购买主题", description = "购买主题接口,扣减用户余额")
|
||||||
|
public BaseResponse<ThemePurchaseRespVO> purchaseTheme(@RequestBody ThemePurchaseReq req) {
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
ThemePurchaseRespVO result = themePurchaseService.purchaseTheme(userId, req.getThemeId());
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.yolo.keyborad.service;
|
|||||||
|
|
||||||
import com.yolo.keyborad.model.entity.KeyboardThemePurchase;
|
import com.yolo.keyborad.model.entity.KeyboardThemePurchase;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.yolo.keyborad.model.vo.purchase.ThemePurchaseRespVO;
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
* @date: 2025/12/10 19:17
|
* @date: 2025/12/10 19:17
|
||||||
@@ -9,5 +10,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
|
|
||||||
public interface KeyboardThemePurchaseService extends IService<KeyboardThemePurchase>{
|
public interface KeyboardThemePurchaseService extends IService<KeyboardThemePurchase>{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买主题
|
||||||
|
*/
|
||||||
|
ThemePurchaseRespVO purchaseTheme(Long userId, Long themeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.yolo.keyborad.service;
|
|||||||
|
|
||||||
import com.yolo.keyborad.model.entity.KeyboardWalletTransaction;
|
import com.yolo.keyborad.model.entity.KeyboardWalletTransaction;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
* @date: 2025/12/10 18:54
|
* @date: 2025/12/10 18:54
|
||||||
@@ -9,5 +12,10 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
|
|
||||||
public interface KeyboardWalletTransactionService extends IService<KeyboardWalletTransaction>{
|
public interface KeyboardWalletTransactionService extends IService<KeyboardWalletTransaction>{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建钱包交易记录
|
||||||
|
*/
|
||||||
|
KeyboardWalletTransaction createTransaction(Long userId, Long orderId, BigDecimal amount,
|
||||||
|
Short type, BigDecimal beforeBalance,
|
||||||
|
BigDecimal afterBalance, String description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
package com.yolo.keyborad.service.impl;
|
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.stereotype.Service;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardThemePurchase;
|
import com.yolo.keyborad.model.entity.KeyboardThemePurchase;
|
||||||
import com.yolo.keyborad.mapper.KeyboardThemePurchaseMapper;
|
import com.yolo.keyborad.mapper.KeyboardThemePurchaseMapper;
|
||||||
@@ -15,4 +29,116 @@ import com.yolo.keyborad.service.KeyboardThemePurchaseService;
|
|||||||
@Service
|
@Service
|
||||||
public class KeyboardThemePurchaseServiceImpl extends ServiceImpl<KeyboardThemePurchaseMapper, KeyboardThemePurchase> implements KeyboardThemePurchaseService{
|
public class KeyboardThemePurchaseServiceImpl extends ServiceImpl<KeyboardThemePurchaseMapper, KeyboardThemePurchase> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.yolo.keyborad.service.impl;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yolo.keyborad.mapper.KeyboardWalletTransactionMapper;
|
import com.yolo.keyborad.mapper.KeyboardWalletTransactionMapper;
|
||||||
@@ -15,4 +17,20 @@ import com.yolo.keyborad.service.KeyboardWalletTransactionService;
|
|||||||
@Service
|
@Service
|
||||||
public class KeyboardWalletTransactionServiceImpl extends ServiceImpl<KeyboardWalletTransactionMapper, KeyboardWalletTransaction> implements KeyboardWalletTransactionService{
|
public class KeyboardWalletTransactionServiceImpl extends ServiceImpl<KeyboardWalletTransactionMapper, KeyboardWalletTransaction> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user