diff --git a/AquaNet/src/components/settings/UserSettings.svelte b/AquaNet/src/components/settings/UserSettings.svelte
index 29faee9f..1220b304 100644
--- a/AquaNet/src/components/settings/UserSettings.svelte
+++ b/AquaNet/src/components/settings/UserSettings.svelte
@@ -1,10 +1,10 @@
-
+
{#if !submitting && !error && me}
@@ -171,6 +209,16 @@
+
+
{t('settings.profile.delete-title')}
+
{t('settings.profile.delete-description')}
+
+
+
+
{/if}
@@ -234,6 +282,19 @@
object-fit: cover
aspect-ratio: 1
+ .danger-zone
+ margin-top: 1.5rem
+ padding-top: 1.5rem
+ border-top: 1px solid vars.$ov-lighter
+
+ .desc
+ opacity: 0.6
+
+ button
+ display: flex
+ align-items: center
+ gap: 0.5rem
+
.cropper-container
diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts
index 11b296fb..8e115ae9 100644
--- a/AquaNet/src/libs/i18n/en_ref.ts
+++ b/AquaNet/src/libs/i18n/en_ref.ts
@@ -274,6 +274,11 @@ export const EN_REF_SETTINGS = {
'settings.profile.unset': 'Unset',
'settings.profile.logout': 'Log out',
'settings.profile.unchanged': 'Unchanged',
+ 'settings.profile.delete-title': 'Delete account',
+ 'settings.profile.delete-description': 'Download a backup, then permanently delete your account and all associated game data.',
+ 'settings.profile.delete': 'Delete account',
+ 'settings.profile.delete-confirm-title': 'Permanently delete your account?',
+ 'settings.profile.delete-confirm-message': 'A backup of all game data will be downloaded first. After that, your account and all associated game data will be permanently deleted. This cannot be undone.',
'settings.export': 'Export Player Data',
'settings.batchManualExport': "Export in Batch Manual (for Tachi)",
'settings.cabNotice': "These settings only apply for your keychip. If you're playing on someone else's setup, please ask them to change these settings.",
diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts
index ca416d36..33b8555b 100644
--- a/AquaNet/src/libs/i18n/zh.ts
+++ b/AquaNet/src/libs/i18n/zh.ts
@@ -287,6 +287,11 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unset': '未设置',
'settings.profile.logout': '登出',
'settings.profile.unchanged': '未更改',
+ 'settings.profile.delete-title': '删除账户',
+ 'settings.profile.delete-description': '下载备份后,永久删除您的账户及所有关联游戏数据。',
+ 'settings.profile.delete': '删除账户',
+ 'settings.profile.delete-confirm-title': '永久删除您的账户?',
+ 'settings.profile.delete-confirm-message': '系统会先下载所有游戏数据的备份,然后永久删除您的账户及所有关联游戏数据。此操作无法撤销。',
'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts
index 7b1e41da..a6187a65 100644
--- a/AquaNet/src/libs/sdk.ts
+++ b/AquaNet/src/libs/sdk.ts
@@ -14,6 +14,8 @@ import type {
} from './generalTypes'
import type { GameName } from './scoring'
+export type ExportGameName = GameName | 'diva'
+
interface ExtReqInit extends RequestInit {
params?: { [index: string]: string }
json?: any
@@ -203,6 +205,8 @@ export const USER = {
ensureLoggedIn,
changeRegion: (regionId: number) =>
post('/api/v2/user/change-region', { regionId }),
+ deleteAccount: () =>
+ post('/api/v2/user/delete-account'),
}
export const USERBOX = {
@@ -236,7 +240,7 @@ export const GAME = {
post(`/api/v2/game/${game}/ranking`, typeof page === "number" ? { page } : {}),
changeName: (game: GameName, newName: string): Promise<{ newName: string }> =>
post(`/api/v2/game/${game}/change-name`, { newName }),
- export: (game: GameName): Promise> =>
+ export: (game: ExportGameName): Promise> =>
post(`/api/v2/game/${game}/export`),
import: (game: GameName, data: any): Promise> =>
post(`/api/v2/game/${game}/import`, {}, { json: data }),
diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt
index 038f9059..33ec414f 100644
--- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt
+++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt
@@ -2,25 +2,40 @@ package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.net.components.*
-import icu.samnyan.aqua.net.db.AquaNetUser
-import icu.samnyan.aqua.net.db.AquaNetUserRepo
-import icu.samnyan.aqua.net.db.AquaUserServices
-import icu.samnyan.aqua.net.db.EmailConfirmationRepo
-import icu.samnyan.aqua.net.db.ResetPasswordRepo
+import icu.samnyan.aqua.net.db.AquaNetUser
+import icu.samnyan.aqua.net.db.AquaNetUserRepo
+import icu.samnyan.aqua.net.db.AquaUserServices
+import icu.samnyan.aqua.net.db.EmailConfirmationRepo
+import icu.samnyan.aqua.net.db.ResetPasswordRepo
import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS
-import icu.samnyan.aqua.sega.allnet.UserKeychip
-import icu.samnyan.aqua.sega.allnet.UserKeychipRepo
-import icu.samnyan.aqua.sega.general.dao.CardRepository
-import icu.samnyan.aqua.sega.general.model.CardStatus
-import jakarta.servlet.http.HttpServletRequest
-import org.slf4j.LoggerFactory
-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 kotlin.io.path.writeBytes
+import icu.samnyan.aqua.sega.allnet.UserKeychip
+import icu.samnyan.aqua.sega.allnet.UserKeychipRepo
+import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
+import icu.samnyan.aqua.sega.diva.DivaRepos
+import icu.samnyan.aqua.sega.diva.model.db.userdata.PlayerProfile
+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.maimai2.model.Mai2Repos
+import icu.samnyan.aqua.sega.ongeki.OngekiUserRepos
+import icu.samnyan.aqua.sega.wacca.model.db.WaccaRepos
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.transaction.Transactional
+import org.slf4j.LoggerFactory
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.stereotype.Service
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.multipart.MultipartFile
+import java.nio.file.Files
+import java.nio.file.Path
+import java.time.Instant
+import kotlin.io.path.Path
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.exists
+import kotlin.io.path.name
+import kotlin.io.path.writeBytes
@RestController
@API("/api/v2/user")
@@ -39,7 +54,8 @@ class UserRegistrar(
val emailProps: EmailProperties,
val userKeychipRepo: UserKeychipRepo,
val aquaNetProps: AquaNetProps,
- final val paths: PathProps
+ final val paths: PathProps,
+ val accountDeletion: AccountDeletionService,
) {
val portraitPath = paths.aquaNetPortrait.path()
@@ -327,14 +343,105 @@ class UserRegistrar(
@API("/change-region")
@Doc("Change the region of the user.", "Success message")
- suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u ->
+ suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u ->
// Check if the region is valid (between 1 and 47)
val r = regionId.toIntOrNull() ?: (400 - "Invalid region")
if (r !in 1..47) 400 - "Invalid region"
async {
userRepo.save(u.apply { region = r.toString() })
}
-
- SUCCESS
- }
-}
+
+ SUCCESS
+ }
+
+ @API("/delete-account")
+ @Doc("Permanently delete the current user's account and all associated game data.", "Success message")
+ fun deleteAccount(@RP token: Str) = jwt.auth(token) { user ->
+ val cleanup = accountDeletion.deleteDatabaseData(user.auId)
+ accountDeletion.deleteUploadedFiles(cleanup)
+ SUCCESS
+ }
+}
+
+data class AccountFileCleanup(
+ val profilePicture: String?,
+ val gameUserId: Long,
+ val divaScreenshots: List,
+)
+
+@Service
+class AccountDeletionService(
+ val userRepo: AquaNetUserRepo,
+ val cardRepo: CardRepository,
+ val mai2: Mai2Repos,
+ val chu3: Chu3Repos,
+ val ongeki: OngekiUserRepos,
+ val wacca: WaccaRepos,
+ val diva: DivaRepos,
+ val paths: PathProps,
+) {
+ companion object {
+ val log = logger()
+ }
+
+ @Transactional
+ fun deleteDatabaseData(auId: Long): AccountFileCleanup {
+ val user = userRepo.findByAuId(auId) ?: (404 - "User not found")
+ val ghostCard = user.ghostCard
+ val divaScreenshots = deleteDiva(ghostCard)
+
+ mai2.userData.findByCard(ghostCard)?.let { mai2.userData.delete(it) }
+ chu3.userData.findByCard(ghostCard)?.let { chu3.userData.delete(it) }
+ ongeki.data.findByCard(ghostCard)?.let { ongeki.data.delete(it) }
+ wacca.user.findByCard(ghostCard)?.let { wacca.user.delete(it) }
+ chu3.userLoginBonus.deleteAll(chu3.userLoginBonus.findByUser(ghostCard.extId.toInt()))
+
+ // Keep Hibernate's managed state consistent with the database-level ON DELETE SET NULL.
+ // Hibernate 7 rejects the flush if a managed card still references the deleted user.
+ val linkedCards = cardRepo.findAllByAquaUserAuId(auId)
+ linkedCards.forEach { it.aquaUser = null }
+ cardRepo.saveAllAndFlush(linkedCards)
+
+ userRepo.delete(user)
+ userRepo.flush()
+ cardRepo.delete(ghostCard)
+ cardRepo.flush()
+
+ log.info("Deleted account and game data for user $auId")
+ return AccountFileCleanup(user.profilePicture, ghostCard.extId, divaScreenshots)
+ }
+
+ private fun deleteDiva(card: Card): List {
+ val profile: PlayerProfile = diva.profile.findByPdId(card.extId).orElse(null) ?: return emptyList()
+ val screenshots = diva.screenShot.findByPdId(profile)
+
+ diva.profile.delete(profile)
+
+ return screenshots.map { it.fileName }
+ }
+
+ fun deleteUploadedFiles(cleanup: AccountFileCleanup) {
+ try {
+ cleanup.profilePicture
+ ?.takeIf { it.isNotBlank() && Path(it).name == it }
+ ?.let { (Path(paths.aquaNetPortrait) / it).deleteIfExists() }
+
+ deleteFilesStartingWith(Path(paths.mai2Portrait), "${cleanup.gameUserId}-")
+ deleteFilesStartingWith(Path(paths.mai2Plays), "${cleanup.gameUserId}-")
+ deleteFilesStartingWith(Path("data/tmp"), "${cleanup.gameUserId}-")
+ cleanup.divaScreenshots
+ .filter { Path(it).name == it }
+ .forEach { (Path("data") / it).deleteIfExists() }
+ } catch (e: Exception) {
+ log.error("Failed to delete one or more uploaded files for deleted user ${cleanup.gameUserId}", e)
+ }
+ }
+
+ private fun deleteFilesStartingWith(directory: Path, prefix: String) {
+ if (!directory.exists()) return
+ Files.list(directory).use { files ->
+ files.filter { it.fileName.toString().startsWith(prefix) }
+ .forEach { it.deleteIfExists() }
+ }
+ }
+}
diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt
index 9a324647..ec347b8d 100644
--- a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt
+++ b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt
@@ -1,7 +1,9 @@
package icu.samnyan.aqua.net.db
import jakarta.persistence.*
-import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.data.jpa.repository.JpaRepository
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
@@ -20,9 +22,10 @@ class EmailConfirmation(
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
- // Linking to the AquaNetUser
- @ManyToOne
- @JoinColumn(name = "auId", referencedColumnName = "auId")
+ // Linking to the AquaNetUser
+ @ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@@ -30,4 +33,4 @@ class EmailConfirmation(
interface EmailConfirmationRepo : JpaRepository {
fun findByToken(token: String): EmailConfirmation?
fun findByAquaNetUserAuId(auId: Long): List
-}
\ No newline at end of file
+}
diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt
index 98a0281c..729c24aa 100644
--- a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt
+++ b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
@@ -22,6 +24,7 @@ class ResetPassword(
// Linking to the AquaNetUser
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@@ -30,4 +33,4 @@ class ResetPassword(
interface ResetPasswordRepo : JpaRepository {
fun findByToken(token: String): ResetPassword?
fun findByAquaNetUserAuId(auId: Long): List
-}
\ No newline at end of file
+}
diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
index f3f24d52..0b7220a7 100644
--- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
+++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
@@ -22,6 +24,7 @@ class SessionToken(
// Linking to the AquaNetUser
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt
index db2b2b5a..c80ba967 100644
--- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt
+++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt
@@ -69,23 +69,23 @@ class AquaNetUser(
// Email confirmation
var emailConfirmed: Boolean = false,
- @OneToOne(cascade = [CascadeType.ALL])
+ @OneToOne(cascade = [CascadeType.PERSIST, CascadeType.MERGE])
@JoinColumn(name = "ghostCard", unique = true, nullable = false)
var ghostCard: Card = Card(),
// One user can have multiple cards
- @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL])
+ @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.PERSIST, CascadeType.MERGE])
var cards: MutableList = mutableListOf(),
// Each user can have multiple keychips (if the user owns cabinets)
@JsonIgnore
- @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
- var keychips: MutableList = mutableListOf(),
+ @OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE])
+ var keychips: MutableList = mutableListOf(),
// Each user's keychip can have multiple sessions
@JsonIgnore
- @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
- var keychipSessions: MutableList = mutableListOf(),
+ @OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE])
+ var keychipSessions: MutableList = mutableListOf(),
@Column(nullable = false)
var canModifyKeychips: Boolean = false,
diff --git a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt
index d2fd3dba..56eb8b1b 100644
--- a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt
+++ b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt
@@ -3,6 +3,7 @@ package icu.samnyan.aqua.net.games.chu3
import ext.API
import ext.returns
import ext.vars
+import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.net.games.IExportClass
import icu.samnyan.aqua.net.games.ImportClass
import icu.samnyan.aqua.net.games.ImportController
@@ -12,6 +13,7 @@ import icu.samnyan.aqua.sega.chusan.model.userdata.*
import org.springframework.web.bind.annotation.RestController
import kotlin.reflect.full.declaredMembers
+@Suppress("UNCHECKED_CAST")
@RestController
@API("api/v2/game/chu3")
class Chu3Import(
@@ -22,7 +24,7 @@ class Chu3Import(
it.name.replace("List", "").lowercase()
},
exportRepos = Chu3DataExport::class.vars()
- .filter { f -> f.name !in setOf("gameId", "userData") }
+ .filter { f -> f.name !in setOf("gameId", "userData", "userLoginBonusList") }
.associateWith { Chu3Repos::class.declaredMembers
.filter { f -> f returns Chu3UserLinked::class }
.firstOrNull { f -> f.name == it.name || f.name == it.name.replace("List", "") }
@@ -44,7 +46,22 @@ class Chu3Import(
// "chuni_profile_emoney" to ImportClass(UserEmoney::class),
// "chuni_profile_overpower" to ImportClass(UserOverpower::class),
// "chuni_profile_recent_rating" to ImportClass(UserRecentRating::class),
- )
+ ),
+ customExporters = mapOf(
+ Chu3DataExport::userLoginBonusList to { user: Chu3UserData, _: ExportOptions ->
+ repos.userLoginBonus.findByUser(user.card!!.extId.toInt())
+ },
+ ) as Map, (Chu3UserData, ExportOptions) -> Any?>,
+ customImporters = mapOf(
+ Chu3DataExport::userLoginBonusList to { export: Chu3DataExport, user: Chu3UserData ->
+ val userId = user.card!!.extId.toInt()
+ repos.userLoginBonus.deleteAll(repos.userLoginBonus.findByUser(userId))
+ repos.userLoginBonus.saveAll(export.userLoginBonusList.map { it.apply {
+ id = 0
+ this.user = userId
+ } })
+ },
+ ) as Map, (Chu3DataExport, Chu3UserData) -> Unit>,
) {
override fun createEmpty() = Chu3DataExport()
override val userDataRepo = repos.userData
@@ -67,6 +84,16 @@ data class Chu3DataExport(
var userMateList: List = emptyList(),
var userGeneralDataList: List = emptyList(),
var userMiscList: Chu3UserMisc = Chu3UserMisc(),
+ var userCardPrintStateList: List = emptyList(),
+ var userGachaList: List = emptyList(),
+ var userRegionsList: List = emptyList(),
+ var userCMissionList: List = emptyList(),
+ var userCMissionProgressList: List = emptyList(),
+ var netBattleLogList: List = emptyList(),
+ var userChallengeList: List = emptyList(),
+ var userLinkedVerseList: List = emptyList(),
+ var userVoteList: List = emptyList(),
+ var userLoginBonusList: List = emptyList(),
): IExportClass {
constructor() : this("SDHD",
Chu3UserData(), UserGameOption(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), Chu3UserMisc())
diff --git a/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt b/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt
new file mode 100644
index 00000000..edce958e
--- /dev/null
+++ b/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt
@@ -0,0 +1,33 @@
+package icu.samnyan.aqua.net.games.diva
+
+import ext.*
+import icu.samnyan.aqua.net.db.AquaUserServices
+import icu.samnyan.aqua.sega.diva.DivaRepos
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@API("api/v2/game/diva")
+class DivaExport(
+ val us: AquaUserServices,
+ val repos: DivaRepos,
+) {
+ @API("export")
+ fun exportUserData(@RP token: Str) = us.jwt.auth(token) { account ->
+ val profile = repos.profile.findByPdId(account.ghostCard.extId).orElse(null)
+ ?: (404 - "User not found")
+
+ linkedMapOf(
+ "gameId" to "SBZV",
+ "userData" to profile,
+ "gameSession" to repos.gameSession.findByPdId(profile).orElse(null),
+ "playLogList" to repos.playLog.findByPdId(profile),
+ "playerContestList" to repos.contest.findByPdId(profile),
+ "playerCustomizeList" to repos.customize.findByPdId(profile),
+ "playerInventoryList" to repos.inventory.findByPdId(profile),
+ "playerModuleList" to repos.module.findByPdId(profile),
+ "playerPvCustomizeList" to repos.pvCustomize.findByPdId(profile),
+ "playerPvRecordList" to repos.pvRecord.findByPdId(profile),
+ "playerScreenShotList" to repos.screenShot.findByPdId(profile),
+ )
+ }
+}
diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt
index 850d0bb6..79c935a9 100644
--- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt
+++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt
@@ -68,7 +68,7 @@ class Mai2Import(
?.split(",")
?.mapIndexed { index, id -> Mai2UserFavoriteItem().apply { orderId = index; this.id = id.toInt() } }
?: emptyList()
- }
+ },
) as Map, (Mai2UserDetail, ExportOptions) -> Any?>,
customImporters = mapOf(
Maimai2DataExport::userPlaylogList to { export: Maimai2DataExport, user: Mai2UserDetail ->
@@ -85,7 +85,7 @@ class Mai2Import(
propertyValue = favoriteMusicList.sortedBy { it.orderId }.map { it.id }.joinToString(",")
})
}
- }
+ },
) as Map, (Maimai2DataExport, Mai2UserDetail) -> Unit>
) {
override fun createEmpty() = Maimai2DataExport()
@@ -113,6 +113,9 @@ data class Maimai2DataExport(
var userFavoriteMusicList: List = mutableListOf(),
var userKaleidxScopeList: List = mutableListOf(),
var userPlaylogList: List = mutableListOf(),
+ var userCardList: List = mutableListOf(),
+ var userPrintDetailList: List = mutableListOf(),
+ var userRegionsList: List = mutableListOf(),
// Not supported yet:
// var userWeeklyData
// var userMissionDataList
diff --git a/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt b/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt
index 8dad1db3..8c87c258 100644
--- a/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt
+++ b/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt
@@ -1,16 +1,10 @@
package icu.samnyan.aqua.net.games.wacca
-import ext.API
-import ext.RP
-import ext.isoDate
-import ext.utc
+import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.waccaScores
-import icu.samnyan.aqua.sega.wacca.model.db.WaccaUser
-import icu.samnyan.aqua.sega.wacca.model.db.WcUserBestScoreRepo
-import icu.samnyan.aqua.sega.wacca.model.db.WcUserPlayLogRepo
-import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
+import icu.samnyan.aqua.sega.wacca.model.db.*
import org.springframework.web.bind.annotation.RestController
@RestController
@@ -20,6 +14,7 @@ class Wacca(
override val playlogRepo: WcUserPlayLogRepo,
override val userDataRepo: WcUserRepo,
override val userMusicRepo: WcUserBestScoreRepo,
+ val repos: WaccaRepos,
): GameApiController("wacca", WaccaUser::class) {
override val settableFields: Map Unit> by lazy { mapOf(
"userName" to usernameCheck(true),
@@ -40,5 +35,38 @@ class Wacca(
genericUserSummary(card, mapOf(), null, data?.favoriteSongs)
}
+ @API("export")
+ fun exportUserData(@RP token: Str) = us.jwt.auth(token) { account ->
+ val user = repos.user.findByCard(account.ghostCard) ?: (404 - "User not found")
+ WaccaDataExport(
+ userData = user,
+ userOptionList = repos.option.findByUser(user),
+ userBingoList = repos.bingo.findByUser(user),
+ userFriendList = repos.friend.findByUser(user).map {
+ WaccaFriendExport(it.with.id, it.isAccepted)
+ },
+ userGateList = repos.gate.findByUser(user),
+ userItemList = repos.item.findByUser(user),
+ userBestScoreList = repos.bestScore.findByUser(user),
+ userPlaylogList = repos.playLog.findByUser(user),
+ userStageUpList = repos.stageUp.findByUser(user),
+ )
+ }
+
override val shownRanks: List> = waccaScores.filter { it.first > 85 * 10000 }
}
+
+data class WaccaFriendExport(val withUserId: Long, val isAccepted: Boolean)
+
+data class WaccaDataExport(
+ val gameId: String = "SDFE",
+ val userData: WaccaUser,
+ val userOptionList: List,
+ val userBingoList: List,
+ val userFriendList: List,
+ val userGateList: List,
+ val userItemList: List,
+ val userBestScoreList: List,
+ val userPlaylogList: List,
+ val userStageUpList: List,
+)
diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt
index 143e55d8..f53e014f 100644
--- a/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt
@@ -5,7 +5,9 @@ import icu.samnyan.aqua.net.db.AquaNetUser
import jakarta.persistence.*
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
-import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.data.jpa.repository.JpaRepository
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
@@ -18,9 +20,10 @@ import java.security.SecureRandom
@Table(name = "allnet_keychip_sessions", indexes = [
Index(name = "idx_last_use", columnList = "lastUse")
])
-class KeychipSession(
- @ManyToOne
- @JoinColumn(name = "au_id")
+class KeychipSession(
+ @ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @JoinColumn(name = "au_id")
var user: AquaNetUser? = null,
@Column(length = 4)
@@ -45,12 +48,12 @@ fun genUrlSafeToken(length: Int): String {
}
@Repository("KeychipSessionRepo")
-interface KeychipSessionRepo : JpaRepository {
- fun findByToken(token: String): KeychipSession?
-
- @Transactional
- fun deleteAllByLastUseBefore(expire: Long)
-}
+interface KeychipSessionRepo : JpaRepository {
+ fun findByToken(token: String): KeychipSession?
+
+ @Transactional
+ fun deleteAllByLastUseBefore(expire: Long)
+}
@Service
class KeychipSessionService(
diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt
index 49e70c80..5c73a69a 100644
--- a/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt
@@ -4,6 +4,8 @@ import icu.samnyan.aqua.net.db.AquaNetUser
import jakarta.persistence.*
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import org.springframework.stereotype.Repository
@Entity
@@ -13,6 +15,7 @@ class UserKeychip(
val id: Long = 0,
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "au_id", nullable = false)
var user: AquaNetUser,
diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt
index ff318f75..cb95b51e 100644
--- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt
@@ -27,8 +27,10 @@ interface Chu3UserLinked : IUserRepo {
// This repo cannot be generalized as UserLinked because the entity stores user as an int
// TODO: Find a way to generalize this
-interface Chu3UserLoginBonusRepo : JpaRepository {
- @Query(
+interface Chu3UserLoginBonusRepo : JpaRepository {
+ fun findByUser(userId: Int): List
+
+ @Query(
value = "select * from chusan_user_login_bonus where user = ?1 and version = ?2 and is_finished = ?3 order by last_update_date desc",
nativeQuery = true
)
diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt
index a45722f1..b02c739c 100644
--- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt
@@ -6,11 +6,14 @@ import icu.samnyan.aqua.net.games.IUserEntity
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.MappedSuperclass
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
@MappedSuperclass
class Chu3UserEntity : BaseEntity(), IUserEntity {
@JsonIgnore
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
override var user: Chu3UserData = Chu3UserData()
-}
\ No newline at end of file
+}
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt b/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt
index c1d19284..d22776a1 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt
@@ -67,6 +67,8 @@ interface PvEntryRepository : JpaRepository {
}
interface PlayerContestRepository : JpaRepository {
+ fun findByPdId(pdId: PlayerProfile): MutableList
+
fun findByPdIdAndContestId(pdId: PlayerProfile, contestId: Int): Optional
fun findTop4ByPdIdOrderByLastUpdateTimeDesc(pdId: PlayerProfile): MutableList
@@ -86,6 +88,8 @@ interface PlayerCustomizeRepository : JpaRepository {
@Repository
interface PlayerInventoryRepository : JpaRepository {
+ fun findByPdId(profile: PlayerProfile): MutableList
+
fun findByPdIdAndTypeAndValue(profile: PlayerProfile, type: String, value: String): Optional
}
@@ -96,6 +100,8 @@ interface PlayerProfileRepository : JpaRepository {
@Repository
interface PlayerPvCustomizeRepository : JpaRepository {
+ fun findByPdId(profile: PlayerProfile): MutableList
+
fun findByPdIdAndPvId(profile: PlayerProfile, pvId: Int): Optional
}
@@ -139,12 +145,16 @@ interface PlayerPvRecordRepository : JpaRepository {
): MutableList
}
-interface PlayerScreenShotRepository : JpaRepository
+interface PlayerScreenShotRepository : JpaRepository {
+ fun findByPdId(profile: PlayerProfile): MutableList
+}
@Repository
-interface PlayLogRepository : JpaRepository
+interface PlayLogRepository : JpaRepository {
+ fun findByPdId(profile: PlayerProfile): MutableList
+}
@Repository
interface PlayerModuleRepository : JpaRepository {
fun findByPdId(profile: PlayerProfile): MutableList
-}
\ No newline at end of file
+}
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt
index c3fed1e5..d4a91924 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata
import icu.samnyan.aqua.sega.diva.model.common.StartMode
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
import java.time.LocalDateTime
@@ -14,6 +16,7 @@ class GameSession : Serializable {
var acceptId = 0
@OneToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id", unique = true)
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt
index 0c79f6c6..65692034 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt
@@ -6,6 +6,8 @@ import icu.samnyan.aqua.sega.diva.model.common.ClearResult
import icu.samnyan.aqua.sega.diva.model.common.Difficulty
import icu.samnyan.aqua.sega.diva.model.common.Edition
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
import java.time.LocalDateTime
@@ -17,6 +19,7 @@ class PlayLog : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
@JsonIgnore
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt
index ca00f302..0be1ad04 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata
import icu.samnyan.aqua.sega.diva.model.common.ContestBorder
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
import java.time.LocalDateTime
@@ -13,6 +15,7 @@ class PlayerContest : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt
index a59ae4d8..513dfd2f 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt
@@ -1,6 +1,8 @@
package icu.samnyan.aqua.sega.diva.model.db.userdata
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
@Entity(name = "DivaPlayerCustomize")
@@ -11,6 +13,7 @@ class PlayerCustomize : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt
index 6d7e7901..13225bbf 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
@Entity(name = "DivaPlayerInventory")
@@ -12,6 +14,7 @@ class PlayerInventory : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
@JsonIgnore
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt
index 9cc691bd..a7a98ad5 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt
@@ -1,6 +1,8 @@
package icu.samnyan.aqua.sega.diva.model.db.userdata
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
@Entity(name = "DivaPlayerModule")
@@ -11,6 +13,7 @@ class PlayerModule : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt
index 3e5d276e..0f58cde0 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
@Entity(name = "DivaPlayerPvCustomize")
@@ -12,6 +14,7 @@ class PlayerPvCustomize : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
@JsonIgnore
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt
index 64168803..ee0b428b 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt
@@ -6,6 +6,8 @@ import icu.samnyan.aqua.sega.diva.model.common.ClearResult
import icu.samnyan.aqua.sega.diva.model.common.Difficulty
import icu.samnyan.aqua.sega.diva.model.common.Edition
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.io.Serializable
@Entity(name = "DivaPlayerPvRecord")
@@ -19,6 +21,7 @@ class PlayerPvRecord : Serializable {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
@JsonIgnore
var pdId: PlayerProfile = PlayerProfile()
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt
index 01d0caed..3235d3d5 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt
@@ -1,6 +1,8 @@
package icu.samnyan.aqua.sega.diva.model.db.userdata
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
@Entity(name = "DivaPlayerScreenShot")
@Table(name = "diva_player_screen_shot")
@@ -10,6 +12,7 @@ class PlayerScreenShot {
var id: Long = 0
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "pd_id")
var pdId: PlayerProfile = PlayerProfile()
var pvId: Long = 0
diff --git a/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt b/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt
index 538afe78..3307f061 100644
--- a/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt
@@ -11,4 +11,5 @@ import org.springframework.stereotype.Repository
interface CardRepository : JpaRepository {
fun findByExtId(extId: Long): Card?
fun findByLuid(luid: String): Card?
+ fun findAllByAquaUserAuId(auId: Long): List
}
diff --git a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt
index fcd0183f..9c50ee08 100644
--- a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt
@@ -3,6 +3,8 @@ package icu.samnyan.aqua.sega.general.model
import com.fasterxml.jackson.annotation.JsonIgnore
import icu.samnyan.aqua.net.db.AquaNetUser
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.time.LocalDateTime
enum class CardStatus {
@@ -59,6 +61,7 @@ class Card(
// Defines the AquaNet user that this card is bound to
@ManyToOne
+ @OnDelete(action = OnDeleteAction.SET_NULL)
@JoinColumn(name = "net_user_id")
@JsonIgnore
var aquaUser: AquaNetUser? = null,
diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
index 90f23f2c..326067a8 100644
--- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
@@ -107,7 +107,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo, Mai2UserLin
fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List
}
-interface Mai2UserPrintDetailRepo : JpaRepository
+interface Mai2UserPrintDetailRepo : Mai2UserLinked
interface Mai2UserUdemaeRepo : Mai2UserLinked
diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt
index 4d39264c..1183c2cb 100644
--- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt
@@ -3,6 +3,7 @@
package icu.samnyan.aqua.sega.maimai2.model.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
@@ -16,6 +17,8 @@ import icu.samnyan.aqua.net.games.IGenericUserMusic
import icu.samnyan.aqua.net.games.IUserEntity
import icu.samnyan.aqua.sega.general.IntegerListConverter
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@@ -24,6 +27,7 @@ import java.time.format.DateTimeFormatter
open class Mai2UserEntity : BaseEntity(), IUserEntity {
@JsonIgnore
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
override var user: Mai2UserDetail = Mai2UserDetail()
}
@@ -459,6 +463,7 @@ fun main(args: Array) {
@Table(name = "maimai2_user_print_detail")
@Entity
+@JsonIgnoreProperties(value = ["userCard"], allowSetters = true)
class Mai2UserPrintDetail : Mai2UserEntity() {
var orderId: Long = 0
var printNumber = 0
diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt
index 47aa146d..d200a5f7 100644
--- a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt
@@ -7,6 +7,8 @@ import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.util.AccessCodeSerializer
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.time.LocalDate
import java.time.LocalDateTime
@@ -14,6 +16,7 @@ import java.time.LocalDateTime
class OngekiUserEntity : BaseEntity(), IUserEntity {
@JsonIgnore
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
override var user: UserData = UserData()
}
@@ -541,4 +544,4 @@ class UserGacha : OngekiUserEntity() {
var fiveGachaCnt = 0
var elevenGachaCnt = 0
var dailyGachaDate: String = LocalDateTime.now().toString()
-}
\ No newline at end of file
+}
diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt
index 4dc7af80..e26e3339 100644
--- a/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt
@@ -9,6 +9,8 @@ import icu.samnyan.aqua.sega.general.IntegerListConverter
import icu.samnyan.aqua.sega.wacca.WaccaItemType
import icu.samnyan.aqua.sega.wacca.WaccaItemType.*
import jakarta.persistence.*
+import org.hibernate.annotations.OnDelete
+import org.hibernate.annotations.OnDeleteAction
import java.util.*
typealias UC = UniqueConstraint
@@ -20,6 +22,7 @@ typealias UC = UniqueConstraint
open class WaccaUserEntity : BaseEntity() {
@JsonIgnore
@ManyToOne
+ @OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
open var user: WaccaUser = WaccaUser()
}
diff --git a/src/main/resources/db/120/V1000_83__diva_user_cascade.sql b/src/main/resources/db/120/V1000_83__diva_user_cascade.sql
new file mode 100644
index 00000000..8ef1b894
--- /dev/null
+++ b/src/main/resources/db/120/V1000_83__diva_user_cascade.sql
@@ -0,0 +1,66 @@
+-- Diva's user foreign keys were removed in V1000.7 and never recreated.
+-- Remove any rows that can no longer be associated with a profile before restoring them.
+DELETE child FROM diva_game_session child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_play_log child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_contest child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_customize child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_inventory child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_module child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_pv_customize child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_pv_record child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+DELETE child FROM diva_player_screen_shot child
+ LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id
+ WHERE profile.id IS NULL;
+
+ALTER TABLE diva_game_session
+ ADD CONSTRAINT fku_diva_game_session_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_play_log
+ ADD CONSTRAINT fku_diva_play_log_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_contest
+ ADD CONSTRAINT fku_diva_player_contest_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_customize
+ ADD CONSTRAINT fku_diva_player_customize_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_inventory
+ ADD CONSTRAINT fku_diva_player_inventory_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_module
+ ADD CONSTRAINT fku_diva_player_module_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_pv_customize
+ ADD CONSTRAINT fku_diva_player_pv_customize_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_pv_record
+ ADD CONSTRAINT fku_diva_player_pv_record_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE diva_player_screen_shot
+ ADD CONSTRAINT fku_diva_player_screen_shot_profile
+ FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/src/main/resources/db/120/V1000_84__account_owned_cascade.sql b/src/main/resources/db/120/V1000_84__account_owned_cascade.sql
new file mode 100644
index 00000000..32add6df
--- /dev/null
+++ b/src/main/resources/db/120/V1000_84__account_owned_cascade.sql
@@ -0,0 +1,42 @@
+-- Account-owned rows disappear with the account. Physical cards are only unlinked.
+ALTER TABLE aqua_net_session
+ DROP FOREIGN KEY IF EXISTS FK_SESSION;
+ALTER TABLE aqua_net_session
+ ADD CONSTRAINT FK_SESSION
+ FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE aqua_net_email_confirmation
+ DROP FOREIGN KEY IF EXISTS FK_EMAIL_CONFIRMATION_ON_AQUA_USER;
+ALTER TABLE aqua_net_email_confirmation
+ ADD CONSTRAINT FK_EMAIL_CONFIRMATION_ON_AQUA_USER
+ FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE aqua_net_email_reset_password
+ DROP FOREIGN KEY IF EXISTS FK_EMAIL_RESET_PASSWORD_ON_AQUA_USER;
+ALTER TABLE aqua_net_email_reset_password
+ ADD CONSTRAINT FK_EMAIL_RESET_PASSWORD_ON_AQUA_USER
+ FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE allnet_keychip_sessions
+ DROP FOREIGN KEY IF EXISTS FK_ALLNET_KEYCHIP_SESSIONS_ON_AU;
+ALTER TABLE allnet_keychip_sessions
+ ADD CONSTRAINT FK_ALLNET_KEYCHIP_SESSIONS_ON_AU
+ FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE user_keychip
+ DROP FOREIGN KEY IF EXISTS fk_user_keychip_on_au;
+ALTER TABLE user_keychip
+ ADD CONSTRAINT fk_user_keychip_on_au
+ FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE sega_card
+ DROP FOREIGN KEY IF EXISTS FK_SEGA_CARD_ON_NET_USER;
+ALTER TABLE sega_card
+ ADD CONSTRAINT FK_SEGA_CARD_ON_NET_USER
+ FOREIGN KEY (net_user_id) REFERENCES aqua_net_user (au_id)
+ ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql b/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql
new file mode 100644
index 00000000..3d77ca56
--- /dev/null
+++ b/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql
@@ -0,0 +1,6 @@
+ALTER TABLE ongeki_user_gacha
+ DROP FOREIGN KEY IF EXISTS FK_ONGEKI_USER_GACHA_ON_USER;
+ALTER TABLE ongeki_user_gacha
+ ADD CONSTRAINT FK_ONGEKI_USER_GACHA_ON_USER
+ FOREIGN KEY (user_id) REFERENCES ongeki_user_data (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql b/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql
new file mode 100644
index 00000000..e89890f6
--- /dev/null
+++ b/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql
@@ -0,0 +1,11 @@
+-- V1000.7 accidentally replaced the user foreign key with a second user-card
+-- foreign key. Restore the direct ownership link so every print detail is
+-- removed with its Mai2 user, including legacy rows without a user card.
+DELETE detail FROM maimai2_user_print_detail detail
+ LEFT JOIN maimai2_user_detail user_data ON detail.user_id = user_data.id
+ WHERE user_data.id IS NULL;
+
+ALTER TABLE maimai2_user_print_detail
+ ADD CONSTRAINT fku_maimai2_user_print_detail_user
+ FOREIGN KEY (user_id) REFERENCES maimai2_user_detail (id)
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt b/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt
new file mode 100644
index 00000000..b08e258f
--- /dev/null
+++ b/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt
@@ -0,0 +1,23 @@
+package test
+
+import ext.JACKSON
+import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCard
+import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail
+import io.kotest.core.spec.style.StringSpec
+import io.kotest.matchers.shouldBe
+
+class Mai2UserPrintDetailJsonTest : StringSpec({
+ "userCard is accepted on input but omitted from output" {
+ val detail = Mai2UserPrintDetail().apply {
+ userCard = Mai2UserCard().apply { cardId = 42 }
+ }
+
+ JACKSON.writeValueAsString(detail).contains("\"userCard\"") shouldBe false
+
+ val imported = JACKSON.readValue(
+ """{"userCard":{"cardId":42}}""",
+ Mai2UserPrintDetail::class.java,
+ )
+ imported.userCard?.cardId shouldBe 42
+ }
+})