feat(themes): 新增推荐主题与用户主题批量删除功能

- 新增 getRecommendedThemes:按真实下载量降序返回8个未购买主题
- 新增 batchDeleteUserThemes:支持用户批量逻辑删除已购主题
- 补充接口注释与 Swagger 文档,开放 /themes/recommended 免鉴权路径
This commit is contained in:
2025-12-11 14:58:30 +08:00
parent 567a8bf165
commit e8ef359fcf
9 changed files with 165 additions and 5 deletions

View File

@@ -91,7 +91,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/themes/purchase",
"/themes/purchased",
"/themes/purchase/list",
"/themes/detail"
"/themes/detail",
"/themes/recommended"
};
}
@Bean

View File

@@ -83,4 +83,12 @@ public class ThemesController {
return ResultUtils.success(result);
}
@GetMapping("/recommended")
@Operation(summary = "推荐主题列表", description = "按真实下载数量降序返回推荐主题")
public BaseResponse<List<KeyboardThemesRespVO>> getRecommendedThemes() {
Long userId = StpUtil.getLoginIdAsLong();
List<KeyboardThemesRespVO> result = themesService.getRecommendedThemes(userId);
return ResultUtils.success(result);
}
}

View File

@@ -0,0 +1,34 @@
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.usertheme.BatchDeleteUserThemesReq;
import com.yolo.keyborad.service.KeyboardUserThemesService;
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.*;
/*
* @author: ziin
* @date: 2025/12/11
*/
@RestController
@Slf4j
@RequestMapping("/user-themes")
@Tag(name = "用户主题")
public class UserThemesController {
@Resource
private KeyboardUserThemesService userThemesService;
@PostMapping("/batch-delete")
@Operation(summary = "批量删除用户主题", description = "批量逻辑删除用户主题")
public BaseResponse<Boolean> batchDeleteUserThemes(@RequestBody BatchDeleteUserThemesReq req) {
Long userId = StpUtil.getLoginIdAsLong();
userThemesService.batchDeleteUserThemes(userId, req.getThemeIds());
return ResultUtils.success(true);
}
}

View File

@@ -0,0 +1,20 @@
package com.yolo.keyborad.model.dto.usertheme;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/*
* @author: ziin
* @date: 2025/12/11
*/
@Schema(description = "批量删除用户主题请求")
@Data
public class BatchDeleteUserThemesReq {
/**
* 主题ID列表
*/
@Schema(description = "主题ID列表")
private List<Long> themeIds;
}

View File

@@ -29,4 +29,11 @@ public interface KeyboardThemesService extends IService<KeyboardThemes>{
*/
KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId);
/**
* 推荐主题列表(按真实下载数量降序)
* @param userId 用户ID
* @return 推荐主题列表
*/
List<KeyboardThemesRespVO> getRecommendedThemes(Long userId);
}

View File

@@ -2,12 +2,20 @@ package com.yolo.keyborad.service;
import com.yolo.keyborad.model.entity.KeyboardUserThemes;
import com.baomidou.mybatisplus.extension.service.IService;
/*
import java.util.List;
/*
* @author: ziin
* @date: 2025/12/11 13:31
*/
public interface KeyboardUserThemesService extends IService<KeyboardUserThemes>{
/**
* 批量删除用户主题(逻辑删除)
* @param userId 用户ID
* @param themeIds 主题ID列表
*/
void batchDeleteUserThemes(Long userId, List<Long> themeIds);
}

View File

@@ -27,16 +27,34 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
@Lazy // 延迟加载,打破循环依赖
private KeyboardThemePurchaseService purchaseService;
/**
* 根据风格查询主题列表
* <p>查询规则:</p>
* <ul>
* <li>当themeStyle为9999时查询所有主题并按排序字段升序排列</li>
* <li>其他情况下,查询指定风格的主题</li>
* <li>查询结果均过滤已删除和未启用的主题</li>
* <li>返回的主题列表包含用户的购买状态</li>
* </ul>
*
* @param themeStyle 主题风格ID9999表示查询所有风格
* @param userId 用户ID用于判断主题购买状态
* @return 主题列表,包含主题详情和购买状态
*/
@Override
public List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId) {
// 根据风格参数查询主题列表
List<KeyboardThemes> themesList;
if (themeStyle == 9999) {
// 查询所有主题,按排序字段升序
themesList = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.orderByAsc(KeyboardThemes::getSort)
.list();
} else {
// 查询指定风格的主题
themesList = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
@@ -44,6 +62,7 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
.list();
}
// 查询用户已购买的主题ID集合
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
@@ -52,6 +71,7 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
.map(KeyboardThemePurchase::getThemeId)
.collect(Collectors.toSet());
// 转换为VO并设置购买状态
return themesList.stream().map(theme -> {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
vo.setIsPurchased(purchasedThemeIds.contains(theme.getId()));
@@ -59,26 +79,73 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
}).collect(Collectors.toList());
}
/**
* 获取主题详情
* <p>查询指定ID的主题详情并返回用户对该主题的购买状态</p>
*
* @param themeId 主题ID
* @param userId 用户ID用于判断购买状态
* @return 主题详情VO包含主题信息和购买状态如果主题不存在或已删除则返回null
*/
@Override
public KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId) {
// 查询主题详情
KeyboardThemes theme = this.lambdaQuery()
.eq(KeyboardThemes::getId, themeId)
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.one();
// 主题不存在或已删除返回null
if (theme == null) {
return null;
}
// 查询用户是否购买该主题
boolean isPurchased = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getThemeId, themeId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.exists();
// 转换为VO并设置购买状态
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
vo.setIsPurchased(isPurchased);
return vo;
}
@Override
/*
获取推荐主题列表
<p>推荐规则根据真实下载量降序排序排除用户已购买的主题最多返回8个主题</p>
@param userId 用户ID
* @return 推荐主题列表,包含主题详情和购买状态(推荐列表中的主题购买状态均为未购买)
*/
public List<KeyboardThemesRespVO> getRecommendedThemes(Long userId) {
// 查询用户已购买的主题ID集合
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.list()
.stream()
.map(KeyboardThemePurchase::getThemeId)
.collect(Collectors.toSet());
// 查询推荐主题列表未删除、已启用、未购买、按真实下载量降序、限制8条
return this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.notIn(!purchasedThemeIds.isEmpty(), KeyboardThemes::getId, purchasedThemeIds)
.orderByDesc(KeyboardThemes::getRealDownloadCount)
.last("LIMIT 8")
.list()
.stream()
.map(theme -> {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
// 推荐列表中的主题均为未购买状态
vo.setIsPurchased(false);
return vo;
}).collect(Collectors.toList());
}
}

View File

@@ -1,7 +1,6 @@
package com.yolo.keyborad.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.model.entity.KeyboardUserThemes;
@@ -15,4 +14,19 @@ import com.yolo.keyborad.service.KeyboardUserThemesService;
@Service
public class KeyboardUserThemesServiceImpl extends ServiceImpl<KeyboardUserThemesMapper, KeyboardUserThemes> implements KeyboardUserThemesService{
/**
* 批量删除用户主题(逻辑删除)
*
* @param userId 用户ID
* @param themeIds 主题ID列表
*/
@Override
public void batchDeleteUserThemes(Long userId, List<Long> themeIds) {
this.lambdaUpdate()
.eq(KeyboardUserThemes::getUserId, userId)
.in(KeyboardUserThemes::getThemeId, themeIds)
.set(KeyboardUserThemes::getViewDeleted, true)
.update();
}
}

View File

@@ -9,6 +9,7 @@ import com.yolo.keyborad.model.entity.KeyboardUserWallet;
import com.yolo.keyborad.service.KeyboardUserWalletService;
import java.math.BigDecimal;
import java.math.RoundingMode;
/*
* @author: ziin
@@ -33,9 +34,9 @@ public class KeyboardUserWalletServiceImpl extends ServiceImpl<KeyboardUserWalle
private String formatBalance(BigDecimal balance) {
if (balance.compareTo(new BigDecimal("10000")) >= 0) {
BigDecimal kValue = balance.divide(new BigDecimal("1000"), 2, BigDecimal.ROUND_HALF_UP);
BigDecimal kValue = balance.divide(new BigDecimal("1000"), 2, RoundingMode.HALF_UP);
return kValue.stripTrailingZeros().toPlainString() + "K";
}
return balance.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString();
return balance.setScale(2, RoundingMode.HALF_UP).toPlainString();
}
}