feat(service): 新增多语言支持,按语言筛选主题

- 在查询主题相关接口中增加 local 参数,支持按语言过滤
- 新增 RequestLocaleUtils 工具类解析 Accept-Language 头
- 重构缓存与数据库查询逻辑,优先按语言匹配
- 更新所有涉及主题列表、详情、推荐、搜索的 Service 与 Controller
This commit is contained in:
2026-03-09 11:21:27 +08:00
parent 5e9873bf72
commit 147c05a6f0
7 changed files with 194 additions and 112 deletions

View File

@@ -12,6 +12,7 @@ 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 com.yolo.keyborad.utils.RequestLocaleUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -42,9 +43,14 @@ public class ThemesController {
@GetMapping("/listByStyle")
@Operation(summary = "按风格查询主题", description = "按主题风格查询主题列表接口")
public BaseResponse<List<KeyboardThemesRespVO>> listByStyle(@RequestParam("themeStyle") Long themeStyleId) {
public BaseResponse<List<KeyboardThemesRespVO>> listByStyle(
@RequestParam("themeStyle") Long themeStyleId,
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage
) {
Long userId = StpUtil.getLoginIdAsLong();
return ResultUtils.success(themesService.selectThemesByStyle(themeStyleId,userId));}
String local = RequestLocaleUtils.resolveLanguage(acceptLanguage);
return ResultUtils.success(themesService.selectThemesByStyle(themeStyleId, userId, local));
}
@GetMapping("/listAllStyles")
@Operation(summary = "查询所有主题风格", description = "查询所有主题风格列表接口")
@@ -70,33 +76,48 @@ public class ThemesController {
@GetMapping("/purchased")
@Operation(summary = "查询已购买的主题", description = "查询当前用户已购买的主题列表")
public BaseResponse<List<KeyboardThemesRespVO>> getPurchasedThemes() {
public BaseResponse<List<KeyboardThemesRespVO>> getPurchasedThemes(
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage
) {
Long userId = StpUtil.getLoginIdAsLong();
List<KeyboardThemesRespVO> result = themePurchaseService.getUserPurchasedThemes(userId);
String local = RequestLocaleUtils.resolveLanguage(acceptLanguage);
List<KeyboardThemesRespVO> result = themePurchaseService.getUserPurchasedThemes(userId, local);
return ResultUtils.success(result);
}
@GetMapping("/detail")
@Operation(summary = "查询主题详情", description = "根据主题ID查询主题详情")
public BaseResponse<KeyboardThemesRespVO> getThemeDetail(@RequestParam Long themeId) {
public BaseResponse<KeyboardThemesRespVO> getThemeDetail(
@RequestParam Long themeId,
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage
) {
Long userId = StpUtil.getLoginIdAsLong();
KeyboardThemesRespVO result = themesService.getThemeDetail(themeId, userId);
String local = RequestLocaleUtils.resolveLanguage(acceptLanguage);
KeyboardThemesRespVO result = themesService.getThemeDetail(themeId, userId, local);
return ResultUtils.success(result);
}
@GetMapping("/recommended")
@Operation(summary = "推荐主题列表", description = "按真实下载数量降序返回推荐主题")
public BaseResponse<List<KeyboardThemesRespVO>> getRecommendedThemes(@RequestParam(required = false) Long themeId) {
public BaseResponse<List<KeyboardThemesRespVO>> getRecommendedThemes(
@RequestParam(required = false) Long themeId,
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage
) {
Long userId = StpUtil.getLoginIdAsLong();
List<KeyboardThemesRespVO> result = themesService.getRecommendedThemes(userId, themeId);
String local = RequestLocaleUtils.resolveLanguage(acceptLanguage);
List<KeyboardThemesRespVO> result = themesService.getRecommendedThemes(userId, themeId, local);
return ResultUtils.success(result);
}
@GetMapping("/search")
@Operation(summary = "搜索主题", description = "根据主题名称模糊搜索主题")
public BaseResponse<List<KeyboardThemesRespVO>> searchThemes(@RequestParam String themeName) {
public BaseResponse<List<KeyboardThemesRespVO>> searchThemes(
@RequestParam String themeName,
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage
) {
Long userId = StpUtil.getLoginIdAsLong();
List<KeyboardThemesRespVO> result = themesService.searchThemesByName(themeName, userId);
String local = RequestLocaleUtils.resolveLanguage(acceptLanguage);
List<KeyboardThemesRespVO> result = themesService.searchThemesByName(themeName, userId, local);
return ResultUtils.success(result);
}

View File

@@ -28,7 +28,7 @@ public interface KeyboardThemePurchaseService extends IService<KeyboardThemePurc
/**
* 查询用户已购买的主题列表
*/
List<KeyboardThemesRespVO> getUserPurchasedThemes(Long userId);
List<KeyboardThemesRespVO> getUserPurchasedThemes(Long userId, String local);
/**
* 恢复已删除的主题

View File

@@ -17,31 +17,35 @@ public interface KeyboardThemesService extends IService<KeyboardThemes>{
* 按主题风格查询主题列表(未删除且上架),包含用户购买状态
* @param themeStyle 主题风格
* @param userId 用户ID
* @param local 语言标识
* @return 主题列表
*/
List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId);
List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId, String local);
/**
* 查询主题详情
* @param themeId 主题ID
* @param userId 用户ID
* @param local 语言标识
* @return 主题详情
*/
KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId);
KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId, String local);
/**
* 推荐主题列表(按真实下载数量降序)
* @param userId 用户ID
* @param local 语言标识
* @return 推荐主题列表
*/
List<KeyboardThemesRespVO> getRecommendedThemes(Long userId,Long themeId);
List<KeyboardThemesRespVO> getRecommendedThemes(Long userId, Long themeId, String local);
/**
* 根据主题名称模糊搜索主题
* @param themeName 主题名称关键字
* @param userId 用户ID
* @param local 语言标识
* @return 主题列表
*/
List<KeyboardThemesRespVO> searchThemesByName(String themeName, Long userId);
List<KeyboardThemesRespVO> searchThemesByName(String themeName, Long userId, String local);
}

View File

@@ -17,6 +17,7 @@ import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.Date;
@@ -204,7 +205,7 @@ public class KeyboardThemePurchaseServiceImpl extends ServiceImpl<KeyboardThemeP
* @param userId 用户ID
* @return 用户已购买的主题详情列表
*/
public List<KeyboardThemesRespVO> getUserPurchasedThemes(Long userId) {
public List<KeyboardThemesRespVO> getUserPurchasedThemes(Long userId, String local) {
// 1. 从用户主题表查询主题ID列表
List<Long> themeIds = userThemesService.lambdaQuery()
.eq(KeyboardUserThemes::getUserId, userId)
@@ -224,6 +225,7 @@ public class KeyboardThemePurchaseServiceImpl extends ServiceImpl<KeyboardThemeP
return themesService.lambdaQuery()
.in(KeyboardThemes::getId, themeIds) // 根据主题ID列表查询
.eq(KeyboardThemes::getDeleted, false) // 排除已删除的主题
.eq(StringUtils.hasText(local), KeyboardThemes::getLocal, local)
.list()
.stream()
.map(theme -> {

View File

@@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Set;
@@ -29,6 +30,7 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
private static final String THEME_STYLE_KEY = "theme:style:";
private static final String THEME_ALL_KEY = "theme:style:all";
private static final Long ALL_THEME_STYLE = 9999L;
@Resource
@Lazy // 延迟加载,打破循环依赖
@@ -55,60 +57,9 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
*/
@Override
@SuppressWarnings("unchecked")
public List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId) {
// 尝试从Redis缓存读取
String cacheKey = themeStyle == 9999 ? THEME_ALL_KEY : THEME_STYLE_KEY + themeStyle;
List<KeyboardThemesRespVO> themesList = null;
try {
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
themesList = (List<KeyboardThemesRespVO>) cached;
log.debug("从缓存读取风格{}的主题列表,共{}个", themeStyle, themesList.size());
}
} catch (Exception e) {
log.warn("读取主题缓存失败,将从数据库查询", e);
}
// 缓存未命中,从数据库查询
if (themesList == null) {
List<KeyboardThemes> themesFromDb;
if (themeStyle == 9999) {
// 查询所有主题,按排序字段升序
themesFromDb = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.orderByAsc(KeyboardThemes::getSort)
.list();
} else {
// 查询指定风格的主题
themesFromDb = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.eq(KeyboardThemes::getThemeStyle, themeStyle)
.list();
}
themesList = themesFromDb.stream()
.map(theme -> BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class))
.collect(Collectors.toList());
log.debug("从数据库读取风格{}的主题列表,共{}个", themeStyle, themesList.size());
}
// 查询用户已购买的主题ID集合
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.list()
.stream()
.map(KeyboardThemePurchase::getThemeId)
.collect(Collectors.toSet());
// 设置购买状态并返回
return themesList.stream().map(theme -> {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
vo.setIsPurchased(purchasedThemeIds.contains(theme.getId()));
return vo;
}).collect(Collectors.toList());
public List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId, String local) {
List<KeyboardThemesRespVO> themesList = getThemesByStyle(themeStyle, local);
return markPurchasedThemes(themesList, userId);
}
/**
@@ -120,12 +71,10 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
* @return 主题详情VO包含主题信息和购买状态如果主题不存在或已删除则返回null
*/
@Override
public KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId) {
public KeyboardThemesRespVO getThemeDetail(Long themeId, Long userId, String local) {
// 查询主题详情
KeyboardThemes theme = this.lambdaQuery()
KeyboardThemes theme = baseThemeQuery(local)
.eq(KeyboardThemes::getId, themeId)
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.one();
// 主题不存在或已删除返回null
@@ -134,16 +83,10 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
}
// 查询用户是否购买该主题
boolean isPurchased = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getThemeId, themeId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.exists();
boolean isPurchased = isThemePurchased(userId, themeId);
// 转换为VO并设置购买状态
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
vo.setIsPurchased(isPurchased);
return vo;
return toThemeResp(theme, isPurchased);
}
@Override
@@ -155,11 +98,9 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
@param themeId 当前主题ID需要从推荐列表中排除
* @return 推荐主题列表,包含主题详情和购买状态
*/
public List<KeyboardThemesRespVO> getRecommendedThemes(Long userId, Long themeId) {
public List<KeyboardThemesRespVO> getRecommendedThemes(Long userId, Long themeId, String local) {
// 构建查询器
LambdaQueryChainWrapper<KeyboardThemes> queryWrapper = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
LambdaQueryChainWrapper<KeyboardThemes> queryWrapper = baseThemeQuery(local)
.orderByDesc(KeyboardThemes::getRealDownloadCount);
// 排除传入的当前主题ID
@@ -171,48 +112,126 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
List<KeyboardThemes> themesList = queryWrapper.list();
// 查询用户已购买的主题ID集合
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.list()
.stream()
.map(KeyboardThemePurchase::getThemeId)
.collect(Collectors.toSet());
Set<Long> purchasedThemeIds = getPurchasedThemeIds(userId);
// 只取前8条数据并设置购买状态
return themesList.stream()
.limit(8)
.map(theme -> {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
// 设置主题的实际购买状态
vo.setIsPurchased(purchasedThemeIds.contains(theme.getId()));
return vo;
}).collect(Collectors.toList());
.map(theme -> toThemeResp(theme, purchasedThemeIds.contains(theme.getId())))
.collect(Collectors.toList());
}
@Override
public List<KeyboardThemesRespVO> searchThemesByName(String themeName, Long userId) {
public List<KeyboardThemesRespVO> searchThemesByName(String themeName, Long userId, String local) {
// 根据主题名称模糊搜索
List<KeyboardThemes> themesList = this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
List<KeyboardThemes> themesList = baseThemeQuery(local)
.like(KeyboardThemes::getThemeName, themeName)
.list();
// 查询用户已购买的主题ID集合
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
Set<Long> purchasedThemeIds = getPurchasedThemeIds(userId);
// 转换为VO并设置购买状态
return themesList.stream()
.map(theme -> toThemeResp(theme, purchasedThemeIds.contains(theme.getId())))
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
private List<KeyboardThemesRespVO> getThemesByStyle(Long themeStyle, String local) {
List<KeyboardThemesRespVO> cachedThemes = getCachedThemes(themeStyle, local);
if (cachedThemes != null) {
return cachedThemes;
}
List<KeyboardThemes> themesFromDb = queryThemesByStyle(themeStyle, local);
List<KeyboardThemesRespVO> themesList = themesFromDb.stream()
.map(theme -> BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class))
.collect(Collectors.toList());
log.debug("从数据库读取风格{}且语言{}的主题列表,共{}个", themeStyle, local, themesList.size());
return themesList;
}
@SuppressWarnings("unchecked")
private List<KeyboardThemesRespVO> getCachedThemes(Long themeStyle, String local) {
try {
Object cached = redisTemplate.opsForValue().get(buildCacheKey(themeStyle));
if (cached == null) {
return null;
}
List<KeyboardThemesRespVO> themesList = (List<KeyboardThemesRespVO>) cached;
List<KeyboardThemesRespVO> filteredThemes = filterThemesByLocal(themesList, local);
log.debug("从缓存读取风格{}且语言{}的主题列表,共{}个", themeStyle, local, filteredThemes.size());
return filteredThemes;
} catch (Exception e) {
log.warn("读取主题缓存失败,将从数据库查询", e);
return null;
}
}
private List<KeyboardThemes> queryThemesByStyle(Long themeStyle, String local) {
LambdaQueryChainWrapper<KeyboardThemes> queryWrapper = baseThemeQuery(local);
if (ALL_THEME_STYLE.equals(themeStyle)) {
return queryWrapper.orderByAsc(KeyboardThemes::getSort).list();
}
return queryWrapper.eq(KeyboardThemes::getThemeStyle, themeStyle).list();
}
private LambdaQueryChainWrapper<KeyboardThemes> baseThemeQuery(String local) {
return this.lambdaQuery()
.eq(KeyboardThemes::getDeleted, false)
.eq(KeyboardThemes::getThemeStatus, true)
.eq(StringUtils.hasText(local), KeyboardThemes::getLocal, local);
}
private List<KeyboardThemesRespVO> filterThemesByLocal(List<KeyboardThemesRespVO> themesList, String local) {
if (!StringUtils.hasText(local)) {
return themesList;
}
return themesList.stream()
.filter(theme -> local.equalsIgnoreCase(theme.getLocal()))
.collect(Collectors.toList());
}
private List<KeyboardThemesRespVO> markPurchasedThemes(List<KeyboardThemesRespVO> themesList, Long userId) {
Set<Long> purchasedThemeIds = getPurchasedThemeIds(userId);
return themesList.stream()
.map(theme -> toThemeResp(theme, purchasedThemeIds.contains(theme.getId())))
.collect(Collectors.toList());
}
private Set<Long> getPurchasedThemeIds(Long userId) {
return purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.list()
.stream()
.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()));
return vo;
}).collect(Collectors.toList());
private boolean isThemePurchased(Long userId, Long themeId) {
return purchaseService.lambdaQuery()
.eq(KeyboardThemePurchase::getUserId, userId)
.eq(KeyboardThemePurchase::getThemeId, themeId)
.eq(KeyboardThemePurchase::getPayStatus, (short) 1)
.exists();
}
private KeyboardThemesRespVO toThemeResp(KeyboardThemes source, boolean isPurchased) {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(source, KeyboardThemesRespVO.class);
vo.setIsPurchased(isPurchased);
return vo;
}
private KeyboardThemesRespVO toThemeResp(KeyboardThemesRespVO source, boolean isPurchased) {
KeyboardThemesRespVO vo = BeanUtil.copyProperties(source, KeyboardThemesRespVO.class);
vo.setIsPurchased(isPurchased);
return vo;
}
private String buildCacheKey(Long themeStyle) {
return ALL_THEME_STYLE.equals(themeStyle) ? THEME_ALL_KEY : THEME_STYLE_KEY + themeStyle;
}
}

View File

@@ -0,0 +1,36 @@
package com.yolo.keyborad.utils;
import org.springframework.util.StringUtils;
import java.util.Locale;
public final class RequestLocaleUtils {
private RequestLocaleUtils() {
}
public static String resolveLanguage(String acceptLanguage) {
if (!StringUtils.hasText(acceptLanguage)) {
return defaultLanguage();
}
String preferredLanguage = acceptLanguage.split(",")[0].split(";")[0].trim();
if (!StringUtils.hasText(preferredLanguage)) {
return defaultLanguage();
}
int separatorIndex = preferredLanguage.indexOf('-');
if (separatorIndex < 0) {
separatorIndex = preferredLanguage.indexOf('_');
}
String language = separatorIndex > 0
? preferredLanguage.substring(0, separatorIndex)
: preferredLanguage;
return language.toLowerCase(Locale.ROOT);
}
private static String defaultLanguage() {
return Locale.getDefault().getLanguage().toLowerCase(Locale.ROOT);
}
}

View File

@@ -2,8 +2,8 @@ spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/keyborad_db
username: root
password: 123asd
username: keyborad_db
password: LjwYPLKKRm4Rz5r7
# 生产环境日志配置
logging: