feat: add api recommond game list

This commit is contained in:
2025-12-19 01:48:56 +08:00
parent cafd2708b4
commit 621e5e577e
16 changed files with 575 additions and 24 deletions

48
pom.xml
View File

@@ -33,7 +33,7 @@
<lombok.version>1.18.30</lombok.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<mysql.version>8.0.33</mysql.version>
<mysql.version>8.0.33</mysql.version>
</properties>
<dependencies>
<!-- 核心依赖 -->
@@ -43,14 +43,14 @@
</dependency>
<!-- 数据库相关 -->
<dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
@@ -70,26 +70,26 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- 添加验证依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- 添加验证依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>

View File

@@ -0,0 +1,22 @@
package icu.sunway.ai_spring_example.Config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenAPI配置类
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Steam-like Game Platform API")
.version("1.0.0")
.description("类似Steam的游戏平台API文档提供游戏推荐等功能"));
}
}

View File

@@ -0,0 +1,39 @@
package icu.sunway.ai_spring_example.Controllers.GameController;
import icu.sunway.ai_spring_example.Controllers.GameController.VO.GameVO;
import icu.sunway.ai_spring_example.Service.IGameService;
import icu.sunway.ai_spring_example.Utils.PageUtils;
import icu.sunway.ai_spring_example.Utils.ResponseUtils;
import icu.sunway.ai_spring_example.Utils.ResponseUtils.Response;
import jakarta.annotation.Resource;
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;
@RestController
@RequestMapping("/api/games")
public class GameController {
@Resource
private IGameService gameService;
/**
* 获取游戏推荐列表
*
* @param page 当前页码
* @param limit 每页游戏数量
* @return 游戏推荐列表
*/
@GetMapping("/recommendations")
public Response<PageUtils.PaginationResult<GameVO>> getRecommendedGames(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer limit) {
try {
PageUtils.PaginationResult<GameVO> result = gameService.getRecommendedGames(page, limit);
return ResponseUtils.success("获取游戏推荐列表成功", result);
} catch (Exception e) {
return ResponseUtils.fail(ResponseUtils.INTERNAL_SERVER_ERROR, "获取游戏推荐列表失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,34 @@
package icu.sunway.ai_spring_example.Controllers.GameController.VO;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 游戏推荐列表VO只返回指定的信息
*/
@Data
@Builder
public class GameVO {
private String name;
private String mainImage;
private String supportedPlatforms;
private List<String> genres;
private BigDecimal price;
private Integer discountPercentage;
private LocalDateTime releaseDate;
private List<ScreenshotVO> screenshots;
/**
* 游戏截图VO
*/
@Data
@Builder
public static class ScreenshotVO {
private String mediaUrl;
private String thumbnailUrl;
}
}

View File

@@ -0,0 +1,64 @@
package icu.sunway.ai_spring_example.Entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("games")
public class Game {
private Integer id;
private Integer steamAppid;
private String name;
private String slug;
private String shortDescription;
private String detailedDescription;
private String aboutTheGame;
private LocalDateTime releaseDate;
private Boolean comingSoon;
private BigDecimal price;
private Integer discountPercentage;
private BigDecimal finalPrice;
private String currency;
private String paymentType;
private Integer metacriticScore;
private BigDecimal userRating;
private Integer ratingCount;
private Integer reviewScore;
private Integer positiveReviews;
private Integer negativeReviews;
private String supportedPlatforms;
private String gameEngine;
private String developerIds;
private String publisherIds;
private String genreIds;
private String tagIds;
private String headerImage;
private String capsuleImage;
private String website;
private Integer achievementsCount;
private Boolean controllerSupport;
private Boolean multiplayer;
private Boolean coOp;
private Boolean lobbies;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 非数据库字段用于返回API结果
@TableField(exist = false)
private String mainImage;
@TableField(exist = false)
private List<String> genres;
@TableField(exist = false)
private List<Screenshot> screenshots;
}

View File

@@ -0,0 +1,18 @@
package icu.sunway.ai_spring_example.Entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("game_genres")
public class GameGenre {
private Integer gameId;
private Integer genreId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,21 @@
package icu.sunway.ai_spring_example.Entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("genres")
public class Genre {
private Integer id;
private String name;
private String slug;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,25 @@
package icu.sunway.ai_spring_example.Entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("media")
public class Media {
private Integer id;
private Integer gameId;
private String mediaType;
private String mediaUrl;
private String thumbnailUrl;
private String description;
private Integer orderIndex;
private Boolean isPrimary;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,14 @@
package icu.sunway.ai_spring_example.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Screenshot {
private Integer id;
private String mediaUrl;
private String thumbnailUrl;
}

View File

@@ -0,0 +1,9 @@
package icu.sunway.ai_spring_example.Mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import icu.sunway.ai_spring_example.Entity.GameGenre;
@Mapper
public interface GameGenreMapper extends BaseMapper<GameGenre> {
}

View File

@@ -0,0 +1,10 @@
package icu.sunway.ai_spring_example.Mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import icu.sunway.ai_spring_example.Entity.Game;
@Mapper
public interface GameMapper extends BaseMapper<Game> {
}

View File

@@ -0,0 +1,9 @@
package icu.sunway.ai_spring_example.Mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import icu.sunway.ai_spring_example.Entity.Genre;
@Mapper
public interface GenreMapper extends BaseMapper<Genre> {
}

View File

@@ -0,0 +1,9 @@
package icu.sunway.ai_spring_example.Mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import icu.sunway.ai_spring_example.Entity.Media;
@Mapper
public interface MediaMapper extends BaseMapper<Media> {
}

View File

@@ -0,0 +1,13 @@
package icu.sunway.ai_spring_example.Service;
import com.baomidou.mybatisplus.extension.service.IService;
import icu.sunway.ai_spring_example.Controllers.GameController.VO.GameVO;
import icu.sunway.ai_spring_example.Entity.Game;
import icu.sunway.ai_spring_example.Utils.PageUtils;
public interface IGameService extends IService<Game> {
// 获取游戏推荐列表
PageUtils.PaginationResult<GameVO> getRecommendedGames(Integer page, Integer limit);
}

View File

@@ -0,0 +1,153 @@
package icu.sunway.ai_spring_example.Service.Implements;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import icu.sunway.ai_spring_example.Controllers.GameController.VO.GameVO;
import icu.sunway.ai_spring_example.Entity.Game;
import icu.sunway.ai_spring_example.Entity.GameGenre;
import icu.sunway.ai_spring_example.Entity.Genre;
import icu.sunway.ai_spring_example.Entity.Media;
import icu.sunway.ai_spring_example.Mapper.GameGenreMapper;
import icu.sunway.ai_spring_example.Mapper.GameMapper;
import icu.sunway.ai_spring_example.Mapper.GenreMapper;
import icu.sunway.ai_spring_example.Mapper.MediaMapper;
import icu.sunway.ai_spring_example.Service.IGameService;
import icu.sunway.ai_spring_example.Utils.PageUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class GameServiceImpl extends ServiceImpl<GameMapper, Game>
implements IGameService {
@Resource
private GameMapper gameMapper;
@Resource
private GameGenreMapper gameGenreMapper;
@Resource
private GenreMapper genreMapper;
@Resource
private MediaMapper mediaMapper;
@Override
public PageUtils.PaginationResult<GameVO> getRecommendedGames(Integer page, Integer limit) {
// 使用分页工具类创建Page对象
Page<Game> gamePage = PageUtils.createPage(page, limit);
QueryWrapper<Game> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("release_date");
// 获取游戏列表
Page<Game> pageResult = this.page(gamePage, queryWrapper);
List<Game> games = pageResult.getRecords();
if (games.isEmpty()) {
// 组装空结果返回
return PageUtils.createEmptyPaginationResult(page, limit);
}
// 获取游戏ID列表
List<Integer> gameIds = games.stream()
.map(Game::getId)
.collect(Collectors.toList());
// 使用MyBatis Plus查询游戏类型
// 1. 查询游戏与类型的关联关系
QueryWrapper<GameGenre> gameGenreWrapper = new QueryWrapper<>();
gameGenreWrapper.in("game_id", gameIds);
List<GameGenre> gameGenres = gameGenreMapper.selectList(gameGenreWrapper);
// 2. 获取所有类型ID
List<Integer> genreIds = gameGenres.stream()
.map(GameGenre::getGenreId)
.distinct()
.collect(Collectors.toList());
// 3. 查询类型信息
Map<Integer, String> genreMap = new HashMap<>();
if (!genreIds.isEmpty()) {
QueryWrapper<Genre> genreWrapper = new QueryWrapper<>();
genreWrapper.in("id", genreIds);
List<Genre> genres = genreMapper.selectList(genreWrapper);
genreMap = genres.stream()
.collect(Collectors.toMap(Genre::getId, Genre::getName));
}
// 4. 组装游戏类型映射
Map<Integer, List<String>> genresMap = new HashMap<>();
for (GameGenre gameGenre : gameGenres) {
Integer gameId = gameGenre.getGameId();
String genreName = genreMap.get(gameGenre.getGenreId());
if (genreName != null) {
genresMap.computeIfAbsent(gameId, k -> new ArrayList<>())
.add(genreName);
}
}
// 使用MyBatis Plus查询游戏主图
QueryWrapper<Media> mainImageWrapper = new QueryWrapper<>();
mainImageWrapper.in("game_id", gameIds)
.eq("is_primary", true)
.select("game_id", "media_url");
List<Media> mainImages = mediaMapper.selectList(mainImageWrapper);
Map<Integer, String> mainImagesMap = mainImages.stream()
.collect(Collectors.toMap(Media::getGameId, Media::getMediaUrl));
// 使用MyBatis Plus查询游戏截图列表
QueryWrapper<Media> screenshotWrapper = new QueryWrapper<>();
screenshotWrapper.in("game_id", gameIds)
.in("media_type", "screenshot", "image")
.orderByAsc("order_index")
.select("id", "game_id", "media_url", "thumbnail_url");
List<Media> screenshots = mediaMapper.selectList(screenshotWrapper);
// 组装游戏截图映射
Map<Integer, List<GameVO.ScreenshotVO>> screenshotsMap = new HashMap<>();
for (Media media : screenshots) {
Integer gameId = media.getGameId();
GameVO.ScreenshotVO screenshot = GameVO.ScreenshotVO.builder()
.mediaUrl(media.getMediaUrl())
.thumbnailUrl(media.getThumbnailUrl())
.build();
screenshotsMap.computeIfAbsent(gameId, k -> new ArrayList<>())
.add(screenshot);
}
// 将Game转换为GameVO
List<GameVO> gameVOs = games.stream()
.map(game -> {
Integer gameId = game.getId();
return GameVO.builder()
.name(game.getName())
.mainImage(mainImagesMap.getOrDefault(gameId, ""))
.supportedPlatforms(game.getSupportedPlatforms())
.genres(genresMap.getOrDefault(gameId, new ArrayList<String>()))
.price(game.getPrice())
.discountPercentage(game.getDiscountPercentage())
.releaseDate(game.getReleaseDate())
.screenshots(screenshotsMap.getOrDefault(gameId, new ArrayList<GameVO.ScreenshotVO>()))
.build();
})
.collect(Collectors.toList());
// 创建新的Page结果
Page<GameVO> voPageResult = new Page<>();
voPageResult.setRecords(gameVOs);
voPageResult.setTotal(pageResult.getTotal());
voPageResult.setCurrent(pageResult.getCurrent());
voPageResult.setSize(pageResult.getSize());
voPageResult.setPages(pageResult.getPages());
// 组装分页结果并返回
return PageUtils.createPaginationResult(voPageResult);
}
}

View File

@@ -0,0 +1,111 @@
package icu.sunway.ai_spring_example.Utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页工具类
*/
public class PageUtils {
/**
* 默认页码
*/
public static final Integer DEFAULT_PAGE = 1;
/**
* 默认每页数量
*/
public static final Integer DEFAULT_LIMIT = 10;
/**
* 最大每页数量
*/
public static final Integer MAX_LIMIT = 100;
/**
* 分页结果类
*
* @param <T> 数据类型
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class PaginationResult<T> {
private Integer currentPage;
private Integer pageSize;
private Long totalItems;
private Integer totalPages;
private List<T> list;
}
/**
* 验证和调整分页参数
*
* @param page 页码
* @param limit 每页数量
* @return 调整后的分页参数数组 [page, limit]
*/
public static Integer[] validatePageParams(Integer page, Integer limit) {
// 验证页码
if (page == null || page < 1) {
page = DEFAULT_PAGE;
}
// 验证每页数量
if (limit == null || limit < 1 || limit > MAX_LIMIT) {
limit = DEFAULT_LIMIT;
}
return new Integer[] { page, limit };
}
/**
* 创建Page对象
*
* @param page 页码
* @param limit 每页数量
* @return Page对象
*/
public static <T> Page<T> createPage(Integer page, Integer limit) {
Integer[] params = validatePageParams(page, limit);
return new Page<>(params[0], params[1]);
}
/**
* 将MyBatis Plus的Page结果转换为分页结果
*
* @param pageResult MyBatis Plus的Page结果
* @return 分页结果
*/
public static <T> PaginationResult<T> createPaginationResult(IPage<T> pageResult) {
return new PaginationResult<>(
(int) pageResult.getCurrent(),
(int) pageResult.getSize(),
pageResult.getTotal(),
(int) pageResult.getPages(),
pageResult.getRecords());
}
/**
* 创建空的分页结果
*
* @param page 页码
* @param limit 每页数量
* @return 空的分页结果
*/
public static <T> PaginationResult<T> createEmptyPaginationResult(Integer page, Integer limit) {
Integer[] params = validatePageParams(page, limit);
return new PaginationResult<>(
params[0],
params[1],
0L,
0,
List.of());
}
}