diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/GameLudoAppRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/GameLudoAppRestController.java index 03ccd7f7..2ee51a30 100644 --- a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/GameLudoAppRestController.java +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/game/GameLudoAppRestController.java @@ -1,5 +1,6 @@ package com.red.circle.other.adapter.app.game; +import com.red.circle.common.business.dto.cmd.AppExtCommand; import com.red.circle.common.business.dto.cmd.app.AppIdLongCmd; import com.red.circle.common.business.dto.cmd.app.AppRoomIdCmd; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; @@ -8,7 +9,10 @@ import com.red.circle.other.app.dto.cmd.game.GameLudoCreateCmd; import com.red.circle.other.app.dto.cmd.game.GameLudoEndCmd; import com.red.circle.other.app.dto.cmd.game.GameLudoJoinCmd; import com.red.circle.other.app.dto.cmd.game.GameLudoStartCmd; +import com.red.circle.other.app.dto.clientobject.game.GamePlayHistoryCO; +import com.red.circle.other.app.dto.cmd.game.GamePlayHistoryRecordCmd; import com.red.circle.other.app.service.game.GameLudoService; +import com.red.circle.other.app.service.game.GamePlayHistoryService; import com.red.circle.other.domain.gateway.user.UserProfileGateway; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import java.util.Arrays; @@ -39,6 +43,7 @@ import org.springframework.web.bind.annotation.RestController; public class GameLudoAppRestController { private final GameLudoService gameLudoService; + private final GamePlayHistoryService gamePlayHistoryService; private final UserProfileGateway userProfileGateway; private final UserProfileAppConvertor userProfileAppConvertor; @@ -161,7 +166,32 @@ public class GameLudoAppRestController { return userProfileAppConvertor.toUserProfileDTO( userProfileGateway.getByUserId(userId) ); + } + /** + * 记录游玩历史. + * + * @eo.name 记录游玩历史 + * @eo.url /history/record + * @eo.method post + * @eo.request-type json + */ + @PostMapping("/history/record") + public void recordHistory(@RequestBody @Validated GamePlayHistoryRecordCmd cmd) { + gamePlayHistoryService.record(cmd); + } + + /** + * 查询最近游玩历史(最多5条). + * + * @eo.name 查询最近游玩历史 + * @eo.url /history/recent + * @eo.method get + * @eo.request-type formdata + */ + @GetMapping("/history/recent") + public List listRecentHistory(@Validated AppExtCommand cmd) { + return gamePlayHistoryService.listRecent(cmd.requireReqSysOrigin(), cmd.requiredReqUserId()); } } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryQryExe.java new file mode 100644 index 00000000..8d6018dc --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryQryExe.java @@ -0,0 +1,46 @@ +package com.red.circle.other.app.command.game; + +import com.red.circle.other.app.dto.clientobject.game.GamePlayHistoryCO; +import com.red.circle.other.infra.database.mongo.entity.game.UserGamePlayHistory; +import com.red.circle.other.infra.database.mongo.service.game.UserGamePlayHistoryService; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 查询用户游玩历史. + * + * @author tf + */ +@Component +@RequiredArgsConstructor +public class GamePlayHistoryQryExe { + + private static final int RECENT_LIMIT = 5; + + private final UserGamePlayHistoryService userGamePlayHistoryService; + + public List execute(String sysOrigin, Long userId) { + List historyList = + userGamePlayHistoryService.listRecent(sysOrigin, userId, RECENT_LIMIT); + + return historyList.stream() + .filter(h -> Objects.nonNull(h.getGameInfo())) + .map(this::toCO) + .collect(Collectors.toList()); + } + + private GamePlayHistoryCO toCO(UserGamePlayHistory history) { + UserGamePlayHistory.GameInfo info = history.getGameInfo(); + GamePlayHistoryCO co = new GamePlayHistoryCO(); + co.setId(info.getId()); + co.setGameId(info.getGameId()); + co.setName(info.getName()); + co.setCover(info.getCover()); + co.setGameCode(info.getGameCode()); + co.setPlayTime(history.getPlayTime()); + return co; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryRecordCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryRecordCmdExe.java new file mode 100644 index 00000000..52080978 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/game/GamePlayHistoryRecordCmdExe.java @@ -0,0 +1,49 @@ +package com.red.circle.other.app.command.game; + +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.other.app.dto.cmd.game.GamePlayHistoryRecordCmd; +import com.red.circle.other.infra.database.mongo.entity.game.UserGamePlayHistory; +import com.red.circle.other.infra.database.mongo.entity.game.UserGamePlayHistory.GameInfo; +import com.red.circle.other.infra.database.mongo.service.game.UserGamePlayHistoryService; +import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; +import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService; +import com.red.circle.other.inner.asserts.GameErrorCode; +import com.red.circle.tool.core.date.TimestampUtils; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 记录用户游玩历史. + * + * @author tf + */ +@Component +@RequiredArgsConstructor +public class GamePlayHistoryRecordCmdExe { + + private final GameListConfigService gameListConfigService; + private final UserGamePlayHistoryService userGamePlayHistoryService; + + public void execute(GamePlayHistoryRecordCmd cmd) { + GameListConfig config = gameListConfigService.getById(cmd.getGameListConfigId()); + ResponseAssert.notNull(GameErrorCode.GAME_NOT_EXIST, config); + + GameInfo gameInfo = new GameInfo(); + gameInfo.setId(config.getId()); + gameInfo.setGameId(config.getGameId()); + gameInfo.setName(config.getName()); + gameInfo.setCover(config.getCover()); + gameInfo.setGameCode(config.getGameCode()); + + UserGamePlayHistory history = new UserGamePlayHistory(); + history.setSysOrigin(cmd.getReqSysOrigin().getOrigin()); + history.setUserId(cmd.requiredReqUserId()); + history.setGameListConfigId(cmd.getGameListConfigId()); + history.setGameInfo(gameInfo); + history.setPlayTime(TimestampUtils.now()); + history.setExpiredTime(TimestampUtils.nowPlusDays(90)); + + userGamePlayHistoryService.record(history); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryServiceImpl.java new file mode 100644 index 00000000..29e9dcc4 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryServiceImpl.java @@ -0,0 +1,30 @@ +package com.red.circle.other.app.service.game; + +import com.red.circle.other.app.command.game.GamePlayHistoryQryExe; +import com.red.circle.other.app.command.game.GamePlayHistoryRecordCmdExe; +import com.red.circle.other.app.dto.clientobject.game.GamePlayHistoryCO; +import com.red.circle.other.app.dto.cmd.game.GamePlayHistoryRecordCmd; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * @author tf + */ +@Service +@RequiredArgsConstructor +public class GamePlayHistoryServiceImpl implements GamePlayHistoryService { + + private final GamePlayHistoryRecordCmdExe gamePlayHistoryRecordCmdExe; + private final GamePlayHistoryQryExe gamePlayHistoryQryExe; + + @Override + public void record(GamePlayHistoryRecordCmd cmd) { + gamePlayHistoryRecordCmdExe.execute(cmd); + } + + @Override + public List listRecent(String sysOrigin, Long userId) { + return gamePlayHistoryQryExe.execute(sysOrigin, userId); + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GamePlayHistoryCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GamePlayHistoryCO.java new file mode 100644 index 00000000..e71d9690 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/game/GamePlayHistoryCO.java @@ -0,0 +1,28 @@ +package com.red.circle.other.app.dto.clientobject.game; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import java.sql.Timestamp; +import lombok.Data; + +/** + * 用户游戏游玩历史. + * + * @author tf + */ +@Data +public class GamePlayHistoryCO { + + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + private String gameId; + + private String name; + + private String cover; + + private String gameCode; + + private Timestamp playTime; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/game/GamePlayHistoryRecordCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/game/GamePlayHistoryRecordCmd.java new file mode 100644 index 00000000..2f980e2b --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/game/GamePlayHistoryRecordCmd.java @@ -0,0 +1,20 @@ +package com.red.circle.other.app.dto.cmd.game; + +import com.red.circle.common.business.dto.cmd.AppExtCommand; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 记录用户游玩游戏. + * + * @author tf + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class GamePlayHistoryRecordCmd extends AppExtCommand { + + /** GameListConfig 主键 */ + @NotNull(message = "gameListConfigId required.") + private Long gameListConfigId; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryService.java new file mode 100644 index 00000000..d87c65a1 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/game/GamePlayHistoryService.java @@ -0,0 +1,17 @@ +package com.red.circle.other.app.service.game; + +import com.red.circle.other.app.dto.clientobject.game.GamePlayHistoryCO; +import com.red.circle.other.app.dto.cmd.game.GamePlayHistoryRecordCmd; +import java.util.List; + +/** + * 游戏游玩历史服务. + * + * @author tf + */ +public interface GamePlayHistoryService { + + void record(GamePlayHistoryRecordCmd cmd); + + List listRecent(String sysOrigin, Long userId); +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/game/UserGamePlayHistory.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/game/UserGamePlayHistory.java new file mode 100644 index 00000000..c74348c3 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/entity/game/UserGamePlayHistory.java @@ -0,0 +1,58 @@ +package com.red.circle.other.infra.database.mongo.entity.game; + +import java.io.Serializable; +import java.sql.Timestamp; +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.CompoundIndexes; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * 用户游戏游玩历史. + * + * @author tf + */ +@Data +@Accessors(chain = true) +@Document("user_game_play_history") +@CompoundIndexes({ + @CompoundIndex(name = "idx_user_play_time", def = "{'userId': 1, 'playTime': -1}"), + @CompoundIndex(name = "idx_user_game", def = "{'userId': 1, 'gameListConfigId': 1}", unique = true) +}) +public class UserGamePlayHistory implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + private String id; + + private String sysOrigin; + + private Long userId; + + /** GameListConfig 主键 */ + private Long gameListConfigId; + + /** 游戏信息快照 */ + private GameInfo gameInfo; + + /** 最近游玩时间 */ + @Indexed + private Timestamp playTime; + + /** 过期时间,90天TTL自动清理 */ + @Indexed(expireAfterSeconds = 0) + private Timestamp expiredTime; + + @Data + public static class GameInfo { + private Long id; + private String gameId; + private String name; + private String cover; + private String gameCode; + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/UserGamePlayHistoryService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/UserGamePlayHistoryService.java new file mode 100644 index 00000000..f8faaea5 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/UserGamePlayHistoryService.java @@ -0,0 +1,22 @@ +package com.red.circle.other.infra.database.mongo.service.game; + +import com.red.circle.other.infra.database.mongo.entity.game.UserGamePlayHistory; +import java.util.List; + +/** + * 用户游戏游玩历史服务. + * + * @author tf + */ +public interface UserGamePlayHistoryService { + + /** + * 记录游玩(同一用户同一游戏 upsert,仅更新 playTime 和快照). + */ + void record(UserGamePlayHistory history); + + /** + * 查询用户最近游玩的游戏,按 playTime 倒序,最多 limit 条. + */ + List listRecent(String sysOrigin, Long userId, int limit); +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/impl/UserGamePlayHistoryServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/impl/UserGamePlayHistoryServiceImpl.java new file mode 100644 index 00000000..0aebcebc --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/game/impl/UserGamePlayHistoryServiceImpl.java @@ -0,0 +1,56 @@ +package com.red.circle.other.infra.database.mongo.service.game.impl; + +import com.red.circle.other.infra.database.mongo.entity.game.UserGamePlayHistory; +import com.red.circle.other.infra.database.mongo.service.game.UserGamePlayHistoryService; +import com.red.circle.tool.core.date.TimestampUtils; +import com.red.circle.tool.core.sequence.IdWorkerUtils; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; + +/** + * @author tf + */ +@Service +@RequiredArgsConstructor +public class UserGamePlayHistoryServiceImpl implements UserGamePlayHistoryService { + + private final MongoTemplate mongoTemplate; + + @Override + public void record(UserGamePlayHistory history) { + Query query = Query.query( + Criteria.where("userId").is(history.getUserId()) + .and("gameListConfigId").is(history.getGameListConfigId()) + ); + + boolean exists = mongoTemplate.exists(query, UserGamePlayHistory.class); + if (exists) { + mongoTemplate.updateFirst(query, + new Update() + .set("gameInfo", history.getGameInfo()) + .set("playTime", history.getPlayTime()) + .set("expiredTime", history.getExpiredTime()), + UserGamePlayHistory.class); + } else { + history.setId(String.valueOf(IdWorkerUtils.getId())); + history.setPlayTime(TimestampUtils.now()); + history.setExpiredTime(TimestampUtils.nowPlusDays(90)); + mongoTemplate.save(history); + } + } + + @Override + public List listRecent(String sysOrigin, Long userId, int limit) { + return mongoTemplate.find( + Query.query(Criteria.where("sysOrigin").is(sysOrigin).and("userId").is(userId)) + .with(Sort.by(Sort.Order.desc("playTime"))) + .limit(limit), + UserGamePlayHistory.class); + } +}