mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-09-22 22:38:14 +03:00
[+] Delete account (#234)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import type { AquaNetUser } from "../../libs/generalTypes";
|
||||
import { CARD, USER } from "../../libs/sdk";
|
||||
import type { AquaNetUser, ConfirmProps } from "../../libs/generalTypes";
|
||||
import { CARD, GAME, USER, type ExportGameName } from "../../libs/sdk";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { pfp } from "../../libs/ui";
|
||||
import { download, pfp } from "../../libs/ui";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import Cropper from "svelte-easy-crop";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
let error: string;
|
||||
let submitting = ""
|
||||
let loading = false;
|
||||
let confirm: ConfirmProps | null = null;
|
||||
|
||||
const profileFields = [
|
||||
[ 'displayName', t('settings.profile.name') ],
|
||||
@@ -97,11 +98,48 @@
|
||||
location.href = "/";
|
||||
}
|
||||
|
||||
function requestAccountDeletion() {
|
||||
if (submitting) return;
|
||||
confirm = {
|
||||
title: t('settings.profile.delete-confirm-title'),
|
||||
message: t('settings.profile.delete-confirm-message'),
|
||||
dangerous: true,
|
||||
cancel: () => confirm = null,
|
||||
confirm: deleteAccount,
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
if (submitting) return;
|
||||
submitting = "deleteAccount";
|
||||
|
||||
try {
|
||||
const gameSummary = await CARD.userGames(me.username);
|
||||
const games = (['mai2', 'chu3', 'ongeki', 'wacca', 'diva'] as ExportGameName[])
|
||||
.filter(game => gameSummary[game] !== null);
|
||||
const exports = await Promise.all(games.map(async game => ({
|
||||
game,
|
||||
data: await GAME.export(game),
|
||||
})));
|
||||
|
||||
exports.forEach(({game, data}) => download(
|
||||
JSON.stringify(data, null, 2),
|
||||
`AquaDX_${game}_export_${me.username}.json`,
|
||||
));
|
||||
await USER.deleteAccount();
|
||||
localStorage.removeItem("token");
|
||||
location.href = "/";
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
submitting = "";
|
||||
}
|
||||
}
|
||||
|
||||
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
|
||||
if (whether) node.type = 'password'
|
||||
}
|
||||
</script>
|
||||
<StatusOverlays {error} loading={!!submitting || loading} />
|
||||
<StatusOverlays bind:confirm={confirm} {error} loading={!!submitting || loading} />
|
||||
{#if !submitting && !error && me}
|
||||
<div>
|
||||
<div class="fields">
|
||||
@@ -171,6 +209,16 @@
|
||||
<button on:click={logOut}>{ts(`settings.profile.logout`)}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field danger-zone m-t">
|
||||
<span class="name">{t('settings.profile.delete-title')}</span>
|
||||
<span class="desc">{t('settings.profile.delete-description')}</span>
|
||||
<div>
|
||||
<button class="error" on:click={requestAccountDeletion}>
|
||||
<Icon icon="tabler:trash-x-filled" />
|
||||
{t('settings.profile.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
|
||||
|
||||
@@ -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<Record<string, any>> =>
|
||||
export: (game: ExportGameName): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/export`),
|
||||
import: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import`, {}, { json: data }),
|
||||
|
||||
@@ -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<String>,
|
||||
)
|
||||
|
||||
@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<String> {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EmailConfirmation, Long> {
|
||||
fun findByToken(token: String): EmailConfirmation?
|
||||
fun findByAquaNetUserAuId(auId: Long): List<EmailConfirmation>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ResetPassword, Long> {
|
||||
fun findByToken(token: String): ResetPassword?
|
||||
fun findByAquaNetUserAuId(auId: Long): List<ResetPassword>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Card> = mutableListOf(),
|
||||
|
||||
// Each user can have multiple keychips (if the user owns cabinets)
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
|
||||
var keychips: MutableList<UserKeychip> = mutableListOf(),
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE])
|
||||
var keychips: MutableList<UserKeychip> = mutableListOf(),
|
||||
|
||||
// Each user's keychip can have multiple sessions
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
|
||||
var keychipSessions: MutableList<KeychipSession> = mutableListOf(),
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE])
|
||||
var keychipSessions: MutableList<KeychipSession> = mutableListOf(),
|
||||
|
||||
@Column(nullable = false)
|
||||
var canModifyKeychips: Boolean = false,
|
||||
|
||||
@@ -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<kotlin.reflect.KMutableProperty1<Chu3DataExport, Any>, (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<kotlin.reflect.KMutableProperty1<Chu3DataExport, Any>, (Chu3DataExport, Chu3UserData) -> Unit>,
|
||||
) {
|
||||
override fun createEmpty() = Chu3DataExport()
|
||||
override val userDataRepo = repos.userData
|
||||
@@ -67,6 +84,16 @@ data class Chu3DataExport(
|
||||
var userMateList: List<UserMate> = emptyList(),
|
||||
var userGeneralDataList: List<UserGeneralData> = emptyList(),
|
||||
var userMiscList: Chu3UserMisc = Chu3UserMisc(),
|
||||
var userCardPrintStateList: List<UserCardPrintState> = emptyList(),
|
||||
var userGachaList: List<UserGacha> = emptyList(),
|
||||
var userRegionsList: List<UserRegions> = emptyList(),
|
||||
var userCMissionList: List<UserCMission> = emptyList(),
|
||||
var userCMissionProgressList: List<UserCMissionProgress> = emptyList(),
|
||||
var netBattleLogList: List<Chu3NetBattleLog> = emptyList(),
|
||||
var userChallengeList: List<Chu3UserChallenge> = emptyList(),
|
||||
var userLinkedVerseList: List<Chu3UserLinkedVerse> = emptyList(),
|
||||
var userVoteList: List<UserVote> = emptyList(),
|
||||
var userLoginBonusList: List<UserLoginBonus> = emptyList(),
|
||||
): IExportClass<Chu3UserData> {
|
||||
constructor() : this("SDHD",
|
||||
Chu3UserData(), UserGameOption(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), Chu3UserMisc())
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class Mai2Import(
|
||||
?.split(",")
|
||||
?.mapIndexed { index, id -> Mai2UserFavoriteItem().apply { orderId = index; this.id = id.toInt() } }
|
||||
?: emptyList()
|
||||
}
|
||||
},
|
||||
) as Map<kotlin.reflect.KMutableProperty1<Maimai2DataExport, Any>, (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<kotlin.reflect.KMutableProperty1<Maimai2DataExport, Any>, (Maimai2DataExport, Mai2UserDetail) -> Unit>
|
||||
) {
|
||||
override fun createEmpty() = Maimai2DataExport()
|
||||
@@ -113,6 +113,9 @@ data class Maimai2DataExport(
|
||||
var userFavoriteMusicList: List<Mai2UserFavoriteItem> = mutableListOf(),
|
||||
var userKaleidxScopeList: List<Mai2UserKaleidx> = mutableListOf(),
|
||||
var userPlaylogList: List<Mai2UserPlaylog> = mutableListOf(),
|
||||
var userCardList: List<Mai2UserCard> = mutableListOf(),
|
||||
var userPrintDetailList: List<Mai2UserPrintDetail> = mutableListOf(),
|
||||
var userRegionsList: List<UserRegions> = mutableListOf(),
|
||||
// Not supported yet:
|
||||
// var userWeeklyData
|
||||
// var userMissionDataList
|
||||
|
||||
@@ -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<WaccaUser>("wacca", WaccaUser::class) {
|
||||
override val settableFields: Map<String, (WaccaUser, String) -> 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<Pair<Int, String>> = 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<WcUserOption>,
|
||||
val userBingoList: List<WcUserBingo>,
|
||||
val userFriendList: List<WaccaFriendExport>,
|
||||
val userGateList: List<WcUserGate>,
|
||||
val userItemList: List<WcUserItem>,
|
||||
val userBestScoreList: List<WcUserScore>,
|
||||
val userPlaylogList: List<WcUserPlayLog>,
|
||||
val userStageUpList: List<WcUserStageUp>,
|
||||
)
|
||||
|
||||
@@ -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<KeychipSession, String> {
|
||||
fun findByToken(token: String): KeychipSession?
|
||||
|
||||
@Transactional
|
||||
fun deleteAllByLastUseBefore(expire: Long)
|
||||
}
|
||||
interface KeychipSessionRepo : JpaRepository<KeychipSession, String> {
|
||||
fun findByToken(token: String): KeychipSession?
|
||||
|
||||
@Transactional
|
||||
fun deleteAllByLastUseBefore(expire: Long)
|
||||
}
|
||||
|
||||
@Service
|
||||
class KeychipSessionService(
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -27,8 +27,10 @@ interface Chu3UserLinked<T : Any> : IUserRepo<Chu3UserData, T> {
|
||||
|
||||
// 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<UserLoginBonus, Long> {
|
||||
@Query(
|
||||
interface Chu3UserLoginBonusRepo : JpaRepository<UserLoginBonus, Long> {
|
||||
fun findByUser(userId: Int): List<UserLoginBonus>
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
@@ -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<Chu3UserData> {
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
@JoinColumn(name = "user_id")
|
||||
override var user: Chu3UserData = Chu3UserData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ interface PvEntryRepository : JpaRepository<PvEntry, Int> {
|
||||
}
|
||||
|
||||
interface PlayerContestRepository : JpaRepository<PlayerContest, Long> {
|
||||
fun findByPdId(pdId: PlayerProfile): MutableList<PlayerContest>
|
||||
|
||||
fun findByPdIdAndContestId(pdId: PlayerProfile, contestId: Int): Optional<PlayerContest>
|
||||
|
||||
fun findTop4ByPdIdOrderByLastUpdateTimeDesc(pdId: PlayerProfile): MutableList<PlayerContest>
|
||||
@@ -86,6 +88,8 @@ interface PlayerCustomizeRepository : JpaRepository<PlayerCustomize, Long> {
|
||||
|
||||
@Repository
|
||||
interface PlayerInventoryRepository : JpaRepository<PlayerInventory, Long> {
|
||||
fun findByPdId(profile: PlayerProfile): MutableList<PlayerInventory>
|
||||
|
||||
fun findByPdIdAndTypeAndValue(profile: PlayerProfile, type: String, value: String): Optional<PlayerInventory>
|
||||
}
|
||||
|
||||
@@ -96,6 +100,8 @@ interface PlayerProfileRepository : JpaRepository<PlayerProfile, Long> {
|
||||
|
||||
@Repository
|
||||
interface PlayerPvCustomizeRepository : JpaRepository<PlayerPvCustomize, Long> {
|
||||
fun findByPdId(profile: PlayerProfile): MutableList<PlayerPvCustomize>
|
||||
|
||||
fun findByPdIdAndPvId(profile: PlayerProfile, pvId: Int): Optional<PlayerPvCustomize>
|
||||
}
|
||||
|
||||
@@ -139,12 +145,16 @@ interface PlayerPvRecordRepository : JpaRepository<PlayerPvRecord, Long> {
|
||||
): MutableList<PlayerPvRecord>
|
||||
}
|
||||
|
||||
interface PlayerScreenShotRepository : JpaRepository<PlayerScreenShot, Long>
|
||||
interface PlayerScreenShotRepository : JpaRepository<PlayerScreenShot, Long> {
|
||||
fun findByPdId(profile: PlayerProfile): MutableList<PlayerScreenShot>
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface PlayLogRepository : JpaRepository<PlayLog, Long>
|
||||
interface PlayLogRepository : JpaRepository<PlayLog, Long> {
|
||||
fun findByPdId(profile: PlayerProfile): MutableList<PlayLog>
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface PlayerModuleRepository : JpaRepository<PlayerModule, Long> {
|
||||
fun findByPdId(profile: PlayerProfile): MutableList<PlayerModule>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,4 +11,5 @@ import org.springframework.stereotype.Repository
|
||||
interface CardRepository : JpaRepository<Card, Long> {
|
||||
fun findByExtId(extId: Long): Card?
|
||||
fun findByLuid(luid: String): Card?
|
||||
fun findAllByAquaUserAuId(auId: Long): List<Card>
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -107,7 +107,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo<Mai2UserPlaylog>, Mai2UserLin
|
||||
fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List<Mai2UserPlaylog>
|
||||
}
|
||||
|
||||
interface Mai2UserPrintDetailRepo : JpaRepository<Mai2UserPrintDetail, Long>
|
||||
interface Mai2UserPrintDetailRepo : Mai2UserLinked<Mai2UserPrintDetail>
|
||||
|
||||
interface Mai2UserUdemaeRepo : Mai2UserLinked<Mai2UserUdemae>
|
||||
|
||||
|
||||
@@ -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<Mai2UserDetail> {
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
@JoinColumn(name = "user_id")
|
||||
override var user: Mai2UserDetail = Mai2UserDetail()
|
||||
}
|
||||
@@ -459,6 +463,7 @@ fun main(args: Array<String>) {
|
||||
|
||||
@Table(name = "maimai2_user_print_detail")
|
||||
@Entity
|
||||
@JsonIgnoreProperties(value = ["userCard"], allowSetters = true)
|
||||
class Mai2UserPrintDetail : Mai2UserEntity() {
|
||||
var orderId: Long = 0
|
||||
var printNumber = 0
|
||||
|
||||
@@ -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<UserData> {
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user