From 03b452e4262c9cdd097ed778148be1d8afdbda3b Mon Sep 17 00:00:00 2001 From: Menci Date: Sun, 12 Jan 2025 03:53:57 +0800 Subject: [PATCH] [+] Mai2 music ranking --- build.gradle.kts | 14 ++++ .../sega/maimai2/Maimai2ServletController.kt | 2 +- .../maimai2/handler/GetGameRankingHandler.kt | 69 +++++++++++++++++++ .../icu/samnyan/aqua/spring/QuerydslConfig.kt | 18 +++++ .../V1000_32__maimai2_music_ranking_index.sql | 2 + 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetGameRankingHandler.kt create mode 100644 src/main/java/icu/samnyan/aqua/spring/QuerydslConfig.kt create mode 100644 src/main/resources/db/migration/mariadb/V1000_32__maimai2_music_ranking_index.sql diff --git a/build.gradle.kts b/build.gradle.kts index c922f8c1..41d937d7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ plugins { kotlin("plugin.jpa") version ktVer kotlin("plugin.serialization") version ktVer kotlin("plugin.allopen") version ktVer + kotlin("kapt") version ktVer id("io.freefair.lombok") version "8.6" id("org.springframework.boot") version "3.2.3" id("com.github.ben-manes.versions") version "0.51.0" @@ -55,6 +56,8 @@ dependencies { runtimeOnly("org.xerial:sqlite-jdbc:3.45.2.0") implementation("org.hibernate.orm:hibernate-core:6.4.4.Final") implementation("org.hibernate.orm:hibernate-community-dialects:6.4.4.Final") + implementation("io.github.openfeign.querydsl:querydsl-jpa:6.10.1") + kapt("io.github.openfeign.querydsl:querydsl-apt:6.10.1:jpa") // JSR305 for nullable implementation("com.google.code.findbugs:jsr305:3.0.2") @@ -122,6 +125,11 @@ hibernate { } } +kapt { + includeCompileClasspath = false + keepJavacAnnotationProcessors = true +} + allOpen { annotation("jakarta.persistence.Entity") annotation("jakarta.persistence.MappedSuperclass") @@ -153,3 +161,9 @@ tasks.withType { tasks.getByName("jar") { enabled = false } + +sourceSets { + main { + java.srcDir("${layout.buildDirectory.get()}/generated/source/kapt/main") + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt index 6834233a..b1101106 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -33,6 +33,7 @@ class Maimai2ServletController( val getUserFavoriteItem: GetUserFavoriteItemHandler, val getUserRivalMusic: GetUserRivalMusicHandler, val getUserCharacter: GetUserCharacterHandler, + val getGameRanking: GetGameRankingHandler, val repos: Mai2Repos ) { companion object { @@ -222,7 +223,6 @@ class Maimai2ServletController( val getUserIntimate = UserReqHandler { _, uid -> mapOf("userId" to uid, "length" to 0, "userIntimateList" to empty) } val getTransferFriend = UserReqHandler { _, uid -> mapOf("userId" to uid, "transferFriendList" to empty) } val getGameNgMusicId = BaseHandler { mapOf("length" to 0, "musicIdList" to empty) } - val getGameRanking = BaseHandler { mapOf("type" to it["type"].toString(), "gameRankingList" to empty) } val getGameTournamentInfo = BaseHandler { mapOf("length" to 0, "gameTournamentInfoList" to empty) } val getGameKaleidxScope = BaseHandler { mapOf("gameKaleidxScopeList" to empty) } val getUserKaleidxScope = UserReqHandler { _, uid -> mapOf("userId" to uid, "userKaleidxScopeList" to empty) } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetGameRankingHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetGameRankingHandler.kt new file mode 100644 index 00000000..cbd152f5 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetGameRankingHandler.kt @@ -0,0 +1,69 @@ +package icu.samnyan.aqua.sega.maimai2.handler + +import com.querydsl.jpa.impl.JPAQueryFactory +import ext.logger +import ext.thread +import icu.samnyan.aqua.sega.general.BaseHandler +import icu.samnyan.aqua.sega.maimai2.model.userdata.QMai2UserPlaylog +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.concurrent.Volatile + +/** + * @author samnyan (privateamusement@protonmail.com) + */ +@Component("Maimai2GetGameRankingHandler") +class GetGameRankingHandler( + private val queryFactory: JPAQueryFactory +) : BaseHandler { + private data class MusicRankingItem(val id: Int, val point: Long, val userName: String = "") + + @Volatile + private var musicRankingCache: List = emptyList() + + init { + // To make sure the cache is initialized before the first request, + // not using `initialDelay = 0` in `@Scheduled`. + thread { refreshMusicRankingCache() } + } + + @Scheduled(fixedDelay = 3600_000) + private fun refreshMusicRankingCache() { + // Get the play count of each music in the last N days + val queryAfter = LocalDateTime.now().minusDays(LOOK_BACK_DAYS) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val queryAfterStr = queryAfter.format(formatter) + + val qPlaylog = QMai2UserPlaylog.mai2UserPlaylog + val cMusicId = qPlaylog.musicId + val cUserCount = qPlaylog.user.id.countDistinct() + musicRankingCache = queryFactory + .select(cMusicId, cUserCount) + .from(qPlaylog) + .where(qPlaylog.userPlayDate.stringValue().goe(queryAfterStr)) + .groupBy(cMusicId) + .orderBy(cUserCount.desc()) + .limit(QUERY_LIMIT) + .fetch() + .map { MusicRankingItem(it.get(cMusicId)!!, it.get(cUserCount)!!) } + + log.info("Refreshed music ranking cache: ${musicRankingCache.size} items") + } + + override fun handle(request: Map): Any = mapOf( + "type" to request["type"], + "gameRankingList" to when(request["type"]) { + 1 -> musicRankingCache + else -> emptyList() + } + ) + + companion object { + val log = logger() + + const val LOOK_BACK_DAYS: Long = 7 + const val QUERY_LIMIT: Long = 50 + } +} diff --git a/src/main/java/icu/samnyan/aqua/spring/QuerydslConfig.kt b/src/main/java/icu/samnyan/aqua/spring/QuerydslConfig.kt new file mode 100644 index 00000000..1e0e35c0 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/spring/QuerydslConfig.kt @@ -0,0 +1,18 @@ +package icu.samnyan.aqua.spring + +import com.querydsl.jpa.impl.JPAQueryFactory +import jakarta.persistence.EntityManager +import jakarta.persistence.PersistenceContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class QuerydslConfig { + @PersistenceContext + private lateinit var entityManager: EntityManager + + @Bean + fun jpaQueryFactory(): JPAQueryFactory { + return JPAQueryFactory(entityManager) + } +} diff --git a/src/main/resources/db/migration/mariadb/V1000_32__maimai2_music_ranking_index.sql b/src/main/resources/db/migration/mariadb/V1000_32__maimai2_music_ranking_index.sql new file mode 100644 index 00000000..0acb213b --- /dev/null +++ b/src/main/resources/db/migration/mariadb/V1000_32__maimai2_music_ranking_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX idx_play_date_music_user +ON maimai2_user_playlog (user_play_date, music_id, user_id);