[+] Card Timestamp

This commit is contained in:
Menci
2025-12-10 04:27:54 +08:00
committed by Azalea
parent 5eee6505f9
commit 7c72348016
25 changed files with 158 additions and 63 deletions

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext
import org.apache.tika.Tika
import org.apache.tika.mime.MimeTypes
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity.BodyBuilder
@@ -264,3 +265,6 @@ val <S> Pair<*, S>.r get() = component2()
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
joinToString("\n") { it.joinToString(",") }
// DI
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }

View File

@@ -21,15 +21,15 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
else -> 400 - "Invalid boolean value ${parser.text}"
}
})
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.LocalDateTime>() {
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
// First try standard formats via asDateTime() method
parser.text.asDateTime() ?: try {
parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
} catch (e: Exception) {
400 - "Invalid date time value ${parser.text}"
}
} }
})
val JACKSON = jacksonObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)

View File

@@ -101,8 +101,7 @@ class CardController(
val games = migrate.split(',')
cardGameService.migrate(card, games)
fedy.onCardLinked(card.luid, oldExtId = card.extId, ghostExtId = u.ghostCard.extId,
games.map { Fedy.getGameName(it) }.filterNotNull())
fedy.onCardLinked(card.luid, oldExtId = card.extId, ghostExtId = u.ghostCard.extId, games)
log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}")
@@ -207,7 +206,8 @@ class CardGameService(
val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
val safety: AquaNetSafetyService,
val cardRepo: CardRepository,
val em: EntityManager
val em: EntityManager,
val cardService: CardService
) {
companion object {
val log = logger()
@@ -225,7 +225,9 @@ class CardGameService(
val remainingGames = dataRepos.keys.toMutableSet()
games.forEach { game ->
val dataRepo = dataRepos[game] ?: return@forEach
migrateCard(game, dataRepo, cardRepo, crd)
if (migrateCard(game, dataRepo, cardRepo, crd))
// Update timestamp for the ghost card (data migrated in)
cardService.updateCardTimestamp(crd.aquaUser!!.ghostCard, game, resetCreatedAt = true)
remainingGames.remove(game)
}
// For remaining games, orphan the data by assigning them to a dummy card

View File

@@ -27,7 +27,9 @@ import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
import org.springframework.context.ApplicationContext
import org.springframework.web.multipart.MultipartFile
import java.time.Instant
import java.util.concurrent.CompletableFuture
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isRegularFile
@@ -41,7 +43,7 @@ class FedyProps {
var remote: String = ""
}
data class UserProfilePicture(val url: Str, val lastUpdatedMs: Long)
data class UserProfilePicture(val url: Str, val updatedAtMs: Long)
data class UserBasicInfo(
val auId: Long, val ghostExtId: Long, val registrationTimeMs: Long,
val username: Str, val displayName: Str, val email: Str, val passwordHash: Str, val profileBio: Str,
@@ -66,21 +68,23 @@ private data class FedyEvent(
@API("/api/v2/fedy", consumes = ["multipart/form-data"])
class Fedy(
val jwt: JWT,
val us: AquaUserServices,
val emailProps: EmailProperties,
val cardRepo: CardRepository,
val cardService: CardService,
val mai2Import: Mai2Import,
val mai2UserDataRepo: Mai2UserDataRepo,
val mai2UploadUserPlaylog: Mai2UploadUserPlaylogHandler,
val mai2UpsertUserAll: Mai2UpsertUserAllHandler,
val chu3UserDataRepo: Chu3UserDataRepo,
val ongekiUserDataRepo: OgkUserDataRepo,
val waccaUserDataRepo: WcUserRepo,
val props: FedyProps,
val paths: PathProps,
val transactionManager: PlatformTransactionManager
val transactionManager: PlatformTransactionManager,
ctx: ApplicationContext
) {
val us by ctx.lazy<AquaUserServices>()
val cardService by ctx.lazy<CardService>()
val mai2Import by ctx.lazy<Mai2Import>()
val mai2UploadUserPlaylog by ctx.lazy<Mai2UploadUserPlaylogHandler>()
val mai2UpsertUserAll by ctx.lazy<Mai2UpsertUserAllHandler>()
val transaction by lazy { TransactionTemplate(transactionManager) }
private fun Str.checkKey() {
@@ -157,25 +161,30 @@ class Fedy(
?.let { paths.aquaNetPortrait.path() / it }?.takeIf { it.isRegularFile() }
?.let { UserProfilePicture(
url = "/uploads/net/portrait/${profilePicture}",
lastUpdatedMs = it.getLastModifiedTime().toMillis()
updatedAtMs = it.getLastModifiedTime().toMillis()
) }
)
data class DataPullReq(val extId: Long, val game: Str, val exportOptions: ExportOptions)
data class DataPullRes(val error: FedyErr? = null, val result: Any? = null)
data class DataPullReq(val extId: Long, val game: Str, val createdAtMs: Long, val updatedAtMs: Long, val exportOptions: ExportOptions)
data class DataPullResult(val data: Any?, val createdAtMs: Long, val updatedAtMs: Long, val isRebased: Bool)
data class DataPullRes(val error: FedyErr? = null, val result: DataPullResult? = null)
@API("/data/pull")
fun handleDataPull(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPullReq): DataPullRes = handleFedy(key) {
val card = cardRepo.findByExtId(req.extId).orElse(null)
?: (404 - "Card with extId ${req.extId} not found")
val cardTimestamp = cardService.getCardTimestamp(card, req.game)
if (cardTimestamp.updatedAt.toEpochMilli() == req.updatedAtMs) return@handleFedy DataPullRes(error = null, result = null) // No changes
val isRebased = req.createdAtMs > 0 && cardTimestamp.createdAt.toEpochMilli() > req.createdAtMs
val exportOptions = if (!isRebased) { req.exportOptions } else { req.exportOptions.copy(playlogAfter = null) }
{
DataPullRes(result = when (req.game) {
"mai2" -> mai2Import.export(card, req.exportOptions)
DataPullRes(result = DataPullResult(data = when (req.game) {
"mai2" -> mai2Import.export(card, exportOptions)
else -> 406 - "Unsupported game"
})
}, createdAtMs = cardTimestamp.createdAt.toEpochMilli(), updatedAtMs = cardTimestamp.updatedAt.toEpochMilli(), isRebased = isRebased))
} caught { DataPullRes(error = it) }
}
data class DataPushReq(val extId: Long, val game: Str, val data: JDict, val removeOldData: Bool)
data class DataPushReq(val extId: Long, val game: Str, val data: JDict, val removeOldData: Bool, val updatedAtMs: Long)
@Suppress("UNCHECKED_CAST")
@API("/data/push")
fun handleDataPush(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPushReq): Any = handleFedy(key) {
@@ -188,6 +197,7 @@ class Fedy(
repo.flush()
}
}
val card = cardRepo.findByExtId(extId).orElse(null) ?: (404 - "Card not found")
transaction.execute { when (req.game) {
"mai2" -> {
if (req.removeOldData) { removeOldData(mai2UserDataRepo) }
@@ -198,7 +208,7 @@ class Fedy(
}
else -> 406 - "Unsupported game"
} }
cardService.updateCardTimestamp(card, req.game, now = Instant.ofEpochMilli(req.updatedAtMs), resetCreatedAt = req.removeOldData)
SUCCESS
}
@@ -221,9 +231,9 @@ class Fedy(
var pairedCard = cardService.tryLookup(req.pairedLuid)?.maybeGhost()
if (pairedCard?.extId != card?.extId) {
var isGhost = pairedCard?.isGhost == true
var isFresh = pairedCard != null && isCardFresh(pairedCard)
if (isGhost && isFresh) isPairedLuidDiverged = true
else if (!isGhost && card?.isGhost == true) {
var isNonFresh = pairedCard != null && !isCardFresh(pairedCard)
if (isGhost || isNonFresh) isPairedLuidDiverged = true
else if (card?.isGhost == true) {
// Ensure paired card is linked, if the main card is linked
// If the main card is not linked, there's nothing Fedy can do. It's Fedy's best effort.
if (pairedCard == null) { pairedCard = cardService.registerByAccessCode(req.pairedLuid, card.aquaUser) }
@@ -326,7 +336,7 @@ class Fedy(
// Apparently existing cards could possibly be fresh and never used in any game. Treat them as new cards.
private fun isCardFresh(c: Card): Bool {
fun <T : IUserData> checkForGame(repo: GenericUserDataRepo<T>, card: Card): Bool = repo.findByCard(card) == null
fun <T : IUserData> checkForGame(repo: GenericUserDataRepo<T>, card: Card): Bool = repo.findByCard(card) != null
return when {
checkForGame(mai2UserDataRepo, c) -> false
checkForGame(chu3UserDataRepo, c) -> false
@@ -346,17 +356,5 @@ class Fedy(
const val REQ_PART = "request"
const val PFP_PART = "profilePicture"
val log = logger()
fun getGameName(gameId: Str) = when (gameId) {
"mai2" -> "mai2"
"SDEZ" -> "mai2"
"chu3" -> "chu3"
"SDHD" -> "chu3"
"ongeki" -> "mu3"
"SDDT" -> "mu3"
"wacca" -> "wacca"
"SDFE" -> "wacca"
else -> null // Not supported
}
}
}

View File

@@ -3,22 +3,15 @@ package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.net.components.*
import icu.samnyan.aqua.net.db.*
import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.time.Instant
import java.time.LocalDateTime
import kotlin.io.path.writeBytes
@RestController
@@ -28,6 +21,7 @@ class UserRegistrar(
val hasher: PasswordEncoder,
val turnstileService: TurnstileService,
val emailService: EmailService,
val fedy: Fedy,
val geoIP: GeoIP,
val jwt: JWT,
val confirmationRepo: EmailConfirmationRepo,
@@ -37,7 +31,6 @@ class UserRegistrar(
val emailProps: EmailProperties,
final val paths: PathProps
) {
@Autowired @Lazy lateinit var fedy: Fedy
val portraitPath = paths.aquaNetPortrait.path()
companion object {

View File

@@ -5,6 +5,7 @@ import icu.samnyan.aqua.net.BotProps
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
@@ -29,6 +30,8 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
abstract val settableFields: Map<String, (T, String) -> Unit>
open val gettableFields: Set<String> = setOf()
@Autowired lateinit var cardService: CardService
@API("trend")
abstract suspend fun trend(@RP username: String): List<TrendOut>
@API("user-summary")
@@ -138,6 +141,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found")
prop(user, value)
async { userDataRepo.save(user) }
cardService.updateCardTimestamp(u.ghostCard, name)
SUCCESS
}
}

View File

@@ -7,6 +7,7 @@ import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.NoRepositoryBean
@@ -17,7 +18,6 @@ import java.util.*
import kotlin.io.path.Path
import kotlin.io.path.writeText
import kotlin.reflect.KClass
import org.springframework.context.annotation.Lazy
data class ExportOptions(
val playlogAfter: String? = null
@@ -50,6 +50,7 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
* Import controller for a game
*
* @param game: 4-letter Game ID
* @param gameName: mai2/chu3/ongeki
* @param exportFields: Mapping of type names to variables in the export model
* (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList)
* @param exportRepos: Mapping of variables to repositories that can be used to find the data
@@ -57,6 +58,7 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
*/
abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>(
val game: String,
val gameName: String,
val exportClass: KClass<ExportModel>,
val exportFields: Map<String, Var<ExportModel, Any>>,
val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>,
@@ -71,7 +73,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
@Autowired lateinit var netProps: AquaNetProps
@Autowired lateinit var transManager: PlatformTransactionManager
val trans by lazy { TransactionTemplate(transManager) }
@Autowired @Lazy lateinit var fedy: Fedy
@Autowired lateinit var cardService: CardService
init {
artemisRenames.values.forEach {
@@ -148,7 +150,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
}
}
Fedy.getGameName(game)?.let { fedy.onDataUpdated(u.ghostCard.extId, it, true) }
cardService.updateCardTimestamp(u.ghostCard, gameName, resetCreatedAt = true)
SUCCESS
}

View File

@@ -17,7 +17,7 @@ import kotlin.reflect.full.declaredMembers
class Chu3Import(
val repos: Chu3Repos,
) : ImportController<Chu3DataExport, Chu3UserData>(
"SDHD", Chu3DataExport::class,
"SDHD", "chu3", Chu3DataExport::class,
exportFields = Chu3DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase()
},

View File

@@ -19,7 +19,7 @@ import kotlin.reflect.full.declaredMembers
class Mai2Import(
val repos: Mai2Repos,
) : ImportController<Maimai2DataExport, Mai2UserDetail>(
"SDEZ", Maimai2DataExport::class,
"SDEZ", "mai2", Maimai2DataExport::class,
exportFields = Maimai2DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase()
},

View File

@@ -3,6 +3,7 @@ package icu.samnyan.aqua.net.games.mai2
import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail
import org.springframework.web.bind.annotation.PostMapping
@@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController
class Mai2MusicDetailImport(
val us: AquaUserServices,
val repos: Mai2Repos,
val cardService: CardService,
) {
@PostMapping("import-music-detail")
suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u ->
@@ -39,6 +41,7 @@ class Mai2MusicDetailImport(
}
}
repos.userMusicDetail.saveAll(data)
cardService.updateCardTimestamp(card, "mai2")
SUCCESS
}
}

View File

@@ -110,6 +110,7 @@ class Maimai2(
val user = userDataRepo.findByCard(card) ?: (404 - "User not found")
user.userName = newNameFull
userDataRepo.save(user)
cardService.updateCardTimestamp(card, "mai2")
}
mapOf("newName" to newNameFull)
}
@@ -139,6 +140,7 @@ class Maimai2(
loginBonus.add(newBonus)
}
repos.userLoginBonus.saveAll(loginBonus)
cardService.updateCardTimestamp(card, "mai2")
}
SUCCESS
}
@@ -172,6 +174,7 @@ class Maimai2(
myRival.propertyValue = myRivalList.joinToString(",")
repos.userGeneralData.save(myRival)
cardService.updateCardTimestamp(myCard, "mai2")
}
SUCCESS
}

View File

@@ -10,6 +10,7 @@ import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.sega.util.jackson.StringMapper
import icu.samnyan.aqua.spring.Metrics
@@ -29,6 +30,7 @@ class ChusanController(
val cmMapper: BasicMapper,
val db: Chu3Repos,
val us: AquaUserServices,
val cardService: CardService,
val versionHelper: ChusanVersionHelper,
val props: ChusanProps,
val pop: GameMusicPopularity,

View File

@@ -75,6 +75,8 @@ fun ChusanController.cmApiInit() {
)
}
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf(
"returnCode" to 1,
"apiName" to "CMUpsertUserGachaApi",
@@ -85,13 +87,15 @@ fun ChusanController.cmApiInit() {
"CMUpsertUserPrintCancel" {
val orderIdList: List<Long> = cmMapper.convert<List<Long>>(parsing { data["orderIdList"]!! })
db.userCardPrintState.saveAll(orderIdList.mapNotNull {
val states = db.userCardPrintState.saveAll(orderIdList.mapNotNull {
// TODO: The original code by Eori writes findById but I don't think that is correct...
db.userCardPrintState.findById(it)()?.apply {
hasCompleted = true
}
})
states.firstOrNull()?.user?.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintCancelApi")
}
@@ -114,6 +118,8 @@ fun ChusanController.cmApiInit() {
db.userCardPrintState.save(this)
}
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintSubtractApi")
}
}

View File

@@ -14,6 +14,7 @@ fun ChusanController.upsertApiInit() {
charge.user = db.userData.findByCard_ExtId(uid)() ?: (400 - "User not found")
charge.id = db.userCharge.findByUser_Card_ExtIdAndChargeId(uid, charge.chargeId)?.id ?: 0
db.userCharge.save(charge)
charge.user.card?.let { cardService.updateCardTimestamp(it, "chu3") }
"""{"returnCode":"1"}"""
}
@@ -192,6 +193,8 @@ fun ChusanController.upsertApiInit() {
}.also { db.userCMissionProgress.save(it) }
}
}
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
}
"""{"returnCode":1}"""

View File

@@ -0,0 +1,32 @@
package icu.samnyan.aqua.sega.general.model
import ext.Str
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.Instant
@Entity(name = "SegaCardTimestamp")
@Table(name = "sega_card_timestamp")
class CardTimestamp(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
@Column(nullable = false)
var updatedAt: Instant = Instant.now(),
var game: Str,
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "card_id", unique = true)
var card: Card? = null,
)
@Repository
interface CardTimestampRepo : JpaRepository<CardTimestamp, Long> {
fun findByCardIdAndGame(cardId: Long, game: Str): CardTimestamp?
}

View File

@@ -1,10 +1,16 @@
package icu.samnyan.aqua.sega.general.service
import ext.Bool
import ext.Str
import ext.minus
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardTimestamp
import icu.samnyan.aqua.sega.general.model.CardTimestampRepo
import org.springframework.stereotype.Service
import java.time.Instant
import java.time.LocalDateTime
import java.util.*
import java.util.concurrent.ThreadLocalRandom
@@ -14,7 +20,7 @@ import kotlin.jvm.optionals.getOrNull
* @author samnyan (privateamusement@protonmail.com)
*/
@Service
class CardService(val cardRepo: CardRepository)
class CardService(val cardRepo: CardRepository, val cardTimestampRepo: CardTimestampRepo, val fedy: Fedy)
{
/**
* Find a card by External ID
@@ -106,4 +112,13 @@ class CardService(val cardRepo: CardRepository)
}
return eid
}
fun getCardTimestamp(card: Card, game: Str, now: Instant = Instant.now()) =
cardTimestampRepo.findByCardIdAndGame(card.id, game) ?: CardTimestamp(game = game, card = card, createdAt = now, updatedAt = now);
fun updateCardTimestamp(card: Card, game: Str, now: Instant = Instant.now(), resetCreatedAt: Bool = false) {
cardTimestampRepo.save(getCardTimestamp(card, game, now).apply { updatedAt = now }
.apply { if (resetCreatedAt) createdAt = now });
fedy.onDataUpdated(card.extId, game, resetCreatedAt)
}
}

View File

@@ -143,6 +143,7 @@ fun Maimai2ServletController.initApis() {
regionId = region
}
db.userRegions.save(region)
// d.card?.let { cardService.updateCardTimestamp(it, "mai2") } // TODO: why save regions on login?
}
res

View File

@@ -40,9 +40,6 @@ class Maimai2ServletController(
val db: Mai2Repos,
val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
@Autowired @Lazy lateinit var fedy: Fedy
companion object {
private val log = logger()
private val empty = listOf<Any>()
@@ -95,7 +92,6 @@ class Maimai2ServletController(
val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}")
if (api == "UpsertUserAllApi") { data["userId"]?.long?.let { fedy.onDataUpdated(it, "mai2", false) } }
}
}
} catch (e: Exception) {

View File

@@ -6,6 +6,7 @@ import ext.millis
import ext.parsing
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog
@@ -22,7 +23,8 @@ import kotlin.jvm.optionals.getOrNull
class UploadUserPlaylogHandler(
private val userDataRepository: Mai2UserDataRepo,
private val playlogRepo: Mai2UserPlaylogRepo,
private val mapper: BasicMapper
private val mapper: BasicMapper,
private val cardService: CardService
) : BaseHandler {
data class BacklogEntry(val time: Long, val playlog: Mai2UserPlaylog)
companion object {
@@ -60,7 +62,10 @@ class UploadUserPlaylogHandler(
// Save if the user is registered
val u = userDataRepository.findByCardExtId(uid).getOrNull()
if (u != null) playlogRepo.save(playlog.apply { user = u })
if (u != null) {
playlogRepo.save(playlog.apply { user = u })
// u.card?.let { cardService.updateCardTimestamp(it, "mai2") } // No need: always followed by an UpsertUserAll
}
// If the user hasn't registered (first play), save the playlog to a backlog
else {

View File

@@ -28,7 +28,6 @@ class UpsertUserAllHandler(
val cardService: CardService,
val repos: Mai2Repos
) : BaseHandler {
fun String.isValidUsername() = isNotBlank() && length <= 8
@Throws(JsonProcessingException::class)
@@ -172,6 +171,8 @@ class UpsertUserAllHandler(
})
}
u.card?.let { cardService.updateCardTimestamp(it, "mai2") }
return SUCCESS
}

View File

@@ -5,6 +5,7 @@ import ext.logger
import ext.long
import ext.parsing
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
@@ -18,6 +19,7 @@ import java.util.concurrent.ThreadLocalRandom
class UpsertUserPrintHandler(
val mapper: BasicMapper,
val db: Mai2Repos,
val cardService: CardService,
@param:Value("\${game.cardmaker.card.expiration:15}") val expirationTime: Long,
) : BaseHandler {
val log = logger()
@@ -43,6 +45,8 @@ class UpsertUserPrintHandler(
}
db.userPrintDetail.save(userPrint)
userData.card?.let { cardService.updateCardTimestamp(it, "mai2") }
return mapOf(
"returnCode" to 1,
"orderId" to 0,

View File

@@ -197,6 +197,8 @@ fun OngekiController.cmApiInit() {
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
@@ -250,6 +252,8 @@ fun OngekiController.cmApiInit() {
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
@@ -313,6 +317,8 @@ fun OngekiController.cmApiInit() {
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}

View File

@@ -7,6 +7,7 @@ import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
@@ -22,6 +23,7 @@ class OngekiController(
val gdb: OngekiGameRepos,
val us: AquaUserServices,
val pop: GameMusicPopularity,
val cardService: CardService,
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
val log = logger()

View File

@@ -209,6 +209,8 @@ fun OngekiController.initUpsertAll() {
id = db.kop.findByUserAndKopIdAndAreaId(u, kopId, areaId)()?.id ?: 0 }) }
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
}

View File

@@ -0,0 +1,11 @@
CREATE TABLE sega_card_timestamp
(
id BIGINT AUTO_INCREMENT NOT NULL,
created_at datetime(3) NOT NULL,
updated_at datetime(3) NOT NULL,
game VARCHAR(255) NOT NULL,
card_id BIGINT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_sega_card_timestamp_on_sega_card FOREIGN KEY (card_id) REFERENCES sega_card (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_sega_card_timestamp_on_game_card UNIQUE (game, card_id)
);