Compare commits

..

14 Commits

13 changed files with 174 additions and 27 deletions
+4
View File
@@ -3,6 +3,10 @@ FROM gradle:9.5.1-jdk25 AS builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
ENV GRADLE_OPTS="-Xmx1536m -XX:MaxMetaspaceSize=512m"
ENV NODE_OPTIONS="--max-old-space-size=1536"
RUN gradle clean shadowJar --no-daemon RUN gradle clean shadowJar --no-daemon
# 運行階段 # 運行階段
@@ -30,6 +30,7 @@ data class AttackDetailDto(
@SerialName("boss_index") val bossIndex: Int, // 1~5 號王 @SerialName("boss_index") val bossIndex: Int, // 1~5 號王
val damage: Long, // 傷害 val damage: Long, // 傷害
@SerialName("is_compensation") val isCompensation: Boolean = false, // true: 補償刀, false: 正刀 @SerialName("is_compensation") val isCompensation: Boolean = false, // true: 補償刀, false: 正刀
@SerialName("is_dropped") val isDropped: Boolean = false, // true: 掉刀, false: 沒掉刀
@SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(), @SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(),
@SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli() @SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli()
) { ) {
@@ -8,11 +8,11 @@ import java.time.Instant
@Serializable @Serializable
data class MemberDto( data class MemberDto(
var uid: String, @SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String, @SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String, @SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String, @SerialName("discord_id") val discordID: String,
var leave: Boolean = false, @SerialName("leave") var leave: Boolean,
@SerialName("created_at") var createAt: Long? = null, @SerialName("created_at") var createAt: Long? = null,
@SerialName("updated_at") var updatedAt: Long? = null @SerialName("updated_at") var updatedAt: Long? = null
) { ) {
@@ -1,6 +1,9 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.database.service package club.sakunadaisuki.pcrediveclanrecordbackend.database.service
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto
import club.sakunadaisuki.pcrediveclanrecordbackend.mapper.toEmptyBattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecord
import com.mongodb.client.MongoCollection import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters import com.mongodb.client.model.Filters
@@ -8,28 +11,34 @@ import com.mongodb.client.model.Filters.and
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.bson.Document import org.bson.Document
import java.time.LocalDate
import java.time.YearMonth
class BattleRecordService(private val database: MongoDatabase) { class BattleRecordService(private val database: MongoDatabase) {
var collection: MongoCollection<Document> var collection: MongoCollection<Document>
var memberCollection: MongoCollection<Document>
init { init {
database.createCollection("battle_records") database.createCollection("battle_records")
collection = database.getCollection("battle_records") collection = database.getCollection("battle_records")
database.createCollection("member")
memberCollection = database.getCollection("member")
} }
// Create new car // Create new battle record
suspend fun create(data: BattleRecordDto): BattleRecordDto = withContext(Dispatchers.IO) { suspend fun create(data: BattleRecordDto): BattleRecordDto = withContext(Dispatchers.IO) {
val doc = data.toDocument() val doc = data.toDocument()
collection.insertOne(doc) collection.insertOne(doc)
doc.let(BattleRecordDto::fromDocument) doc.let(BattleRecordDto::fromDocument)
} }
// Read a car // Read a battle record
suspend fun read(date: String): List<BattleRecordDto> = withContext(Dispatchers.IO) { suspend fun read(date: String): List<BattleRecordDto> = withContext(Dispatchers.IO) {
collection.find(Filters.eq("date", date)).toList().map(BattleRecordDto::fromDocument) collection.find(Filters.eq("date", date)).toList().map(BattleRecordDto::fromDocument)
} }
// Read a car // Read a battle record
suspend fun read(date: String, uid: String): BattleRecordDto? = withContext(Dispatchers.IO) { suspend fun read(date: String, uid: String): BattleRecordDto? = withContext(Dispatchers.IO) {
val filter = and( val filter = and(
Filters.eq("date", date), Filters.eq("date", date),
@@ -38,7 +47,7 @@ class BattleRecordService(private val database: MongoDatabase) {
collection.find(filter).first()?.let(BattleRecordDto::fromDocument) collection.find(filter).first()?.let(BattleRecordDto::fromDocument)
} }
// Update a car // Update a battle record
suspend fun update(date: String, data: BattleRecordDto): BattleRecordDto? = withContext(Dispatchers.IO) { suspend fun update(date: String, data: BattleRecordDto): BattleRecordDto? = withContext(Dispatchers.IO) {
val filter = and( val filter = and(
Filters.eq("date", date), Filters.eq("date", date),
@@ -48,7 +57,7 @@ class BattleRecordService(private val database: MongoDatabase) {
collection.find(filter).first()?.let(BattleRecordDto::fromDocument) collection.find(filter).first()?.let(BattleRecordDto::fromDocument)
} }
// Delete a car // Delete a battle record
suspend fun delete(date: String, uid: String): Document? = withContext(Dispatchers.IO) { suspend fun delete(date: String, uid: String): Document? = withContext(Dispatchers.IO) {
val filter = and( val filter = and(
Filters.eq("date", date), Filters.eq("date", date),
@@ -56,4 +65,36 @@ class BattleRecordService(private val database: MongoDatabase) {
) )
collection.findOneAndDelete(filter) collection.findOneAndDelete(filter)
} }
suspend fun createTable(date: String): List<BattleRecordDto> = withContext(Dispatchers.IO) {
val memberList = memberCollection.find(Filters.eq("leave", false)).toList().map(MemberDto::fromDocument)
val emptyBattleRecordList = memberList.map { it.toEmptyBattleRecordDto(date) }
collection.insertMany(emptyBattleRecordList.map { it.toDocument() })
collection.find(Filters.eq("date", date)).toList().map(BattleRecordDto::fromDocument)
}
suspend fun createTableMonth(date: String): List<List<BattleRecordDto>> = withContext(Dispatchers.IO) {
val memberList = memberCollection.find(Filters.eq("leave", false)).toList().map(MemberDto::fromDocument)
// 1. 指定年份與月份(例如:2026年8月)
val dateSplit = date.split("-").map { it.toInt() }
val yearMonth = YearMonth.of(dateSplit[0], dateSplit[1])
// 2. 取得該月最後一天的 LocalDate 物件
val lastDayOfMonth = yearMonth.atEndOfMonth()
// 3. 取得最後 5 天的日期列表(由舊到新排序)
val lastFiveDays: List<LocalDate> = (4 downTo 0).map { lastDayOfMonth.minusDays(it.toLong()) }
// 4. Temp list
val tables = mutableListOf<List<BattleRecordDto>>()
// 5. 寫入資料
lastFiveDays.forEach { date ->
val emptyBattleRecordList = memberList.map { it.toEmptyBattleRecordDto(date.toString()) }
tables.add(emptyBattleRecordList)
collection.insertMany(emptyBattleRecordList.map { it.toDocument() })
}
tables
}
} }
@@ -17,13 +17,13 @@ class MemberService(private val database: MongoDatabase) {
} }
// Create new member // Create new member
suspend fun create(data: MemberDto): String = withContext(Dispatchers.IO) { suspend fun create(data: MemberDto): MemberDto = withContext(Dispatchers.IO) {
data.createAt = System.currentTimeMillis() data.createAt = System.currentTimeMillis()
data.updatedAt = System.currentTimeMillis() data.updatedAt = System.currentTimeMillis()
data.leave = false data.leave = false
val doc = data.toDocument() val doc = data.toDocument()
collection.insertOne(doc) collection.insertOne(doc)
doc["uid"].toString() doc.let(MemberDto::fromDocument)
} }
// Read a member // Read a member
@@ -37,8 +37,17 @@ class MemberService(private val database: MongoDatabase) {
} }
// Update a member // Update a member
suspend fun update(uid: String, data: MemberDto): Document? = withContext(Dispatchers.IO) { suspend fun update(uid: String, data: MemberDto): MemberDto? = withContext(Dispatchers.IO) {
collection.findOneAndReplace(Filters.eq("uid", uid), data.toDocument()) collection.find(Filters.eq("uid", uid)).first()?.let(MemberDto::fromDocument)?.copy(
playerName = data.playerName,
discordName = data.discordName,
discordID = data.discordID,
leave = data.leave,
updatedAt = System.currentTimeMillis(),
)?.let {
collection.findOneAndReplace(Filters.eq("uid", uid), it.toDocument())
}
collection.find(Filters.eq("uid", uid)).first()?.let(MemberDto::fromDocument)
} }
// Delete a member // Delete a member
@@ -19,6 +19,7 @@ fun AttackDetail.toDto(): AttackDetailDto = AttackDetailDto(
bossIndex = bossIndex, bossIndex = bossIndex,
damage = damage, damage = damage,
isCompensation = isCompensation, isCompensation = isCompensation,
isDropped = isDropped,
createdAt = createdAt, createdAt = createdAt,
updatedAt = updatedAt, updatedAt = updatedAt,
) )
@@ -1,8 +1,10 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.mapper package club.sakunadaisuki.pcrediveclanrecordbackend.mapper
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.Member import club.sakunadaisuki.pcrediveclanrecordbackend.model.Member
import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest
import club.sakunadaisuki.pcrediveclanrecordbackend.model.UpdateMemberRequest
fun Member.toDto(): MemberDto { fun Member.toDto(): MemberDto {
return MemberDto( return MemberDto(
@@ -24,4 +26,25 @@ fun MemberRequest.toDto(): MemberDto {
discordID = discordID, discordID = discordID,
leave = leave, leave = leave,
) )
}
fun UpdateMemberRequest.toDto(uid: String): MemberDto {
return MemberDto(
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
leave = leave,
)
}
fun MemberDto.toEmptyBattleRecordDto(date: String): BattleRecordDto {
return BattleRecordDto(
date = date,
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
attackDetailList = emptyList(),
)
} }
@@ -1,23 +1,26 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model package club.sakunadaisuki.pcrediveclanrecordbackend.model
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.AttackDetailDto
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant import java.time.Instant
@Serializable
data class BattleRecord( data class BattleRecord(
val date: String, // yyyy-MM-dd @SerialName("date") val date: String, // yyyy-MM-dd
var uid: String, @SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String, @SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String, @SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String, @SerialName("discord_id") val discordID: String,
@SerialName("attack_detail_list") val attackDetailList: List<AttackDetail> @SerialName("attack_detail_list") val attackDetailList: List<AttackDetail>
) )
@Serializable
data class AttackDetail( data class AttackDetail(
@SerialName("boss_round") val bossRound: Int, // 周目 @SerialName("boss_round") val bossRound: Int, // 周目
@SerialName("boss_index") val bossIndex: Int, // 1~5 號王 @SerialName("boss_index") val bossIndex: Int, // 1~5 號王
val damage: Long, // 傷害 @SerialName("damage") val damage: Long, // 傷害
@SerialName("is_compensation") val isCompensation: Boolean = false, // true: 補償刀, false: 正刀 @SerialName("is_compensation") val isCompensation: Boolean, // true: 補償刀, false: 正刀
@SerialName("is_dropped") val isDropped: Boolean, // true: 掉刀, false: 沒掉刀
@SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(), @SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(),
@SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli() @SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli()
) )
@@ -14,4 +14,5 @@ data class BattleRecordRequest(
val boss: Int, val boss: Int,
val damage: Long, val damage: Long,
@SerialName("is_compensation") val isCompensation: Boolean = false, @SerialName("is_compensation") val isCompensation: Boolean = false,
@SerialName("is_dropped") val isDropped: Boolean = false,
) )
@@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Member( data class Member(
var uid: String, @SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String, @SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String, @SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String, @SerialName("discord_id") val discordID: String,
var leave: Boolean = false, @SerialName("leave") var leave: Boolean,
@SerialName("created_at") var createAt: Long? = null, @SerialName("created_at") var createAt: Long? = null,
@SerialName("updated_at") var updatedAt: Long? = null @SerialName("updated_at") var updatedAt: Long? = null
) )
@@ -0,0 +1,12 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UpdateMemberRequest(
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("leave") var leave: Boolean,
)
@@ -8,13 +8,21 @@ import io.ktor.server.routing.*
fun Application.configureHttp() { fun Application.configureHttp() {
install(CORS) { install(CORS) {
allowHost("localhost:8080")
allowHost("table.sakunadaisuki.club")
allowHost("192.168.50.128:10002")
allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put) allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete) allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch) allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization) allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType) // 處理 JSON 必備
allowHeader(HttpHeaders.Authorization) // 傳遞 JWT / 身份驗證權限必備
allowHeader("MyCustomHeader") allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. //anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
} }
routing { routing {
swaggerUI(path = "openapi") { swaggerUI(path = "openapi") {
@@ -2,16 +2,18 @@ package club.sakunadaisuki.pcrediveclanrecordbackend.plugins
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.BattleRecordService import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.BattleRecordService
import club.sakunadaisuki.pcrediveclanrecordbackend.model.Member
import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.MemberService import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.MemberService
import club.sakunadaisuki.pcrediveclanrecordbackend.mapper.toDto import club.sakunadaisuki.pcrediveclanrecordbackend.mapper.toDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.AttackDetail import club.sakunadaisuki.pcrediveclanrecordbackend.model.AttackDetail
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecord
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecordRequest import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecordRequest
import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest
import com.mongodb.client.* import club.sakunadaisuki.pcrediveclanrecordbackend.model.UpdateMemberRequest
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoDatabase
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.config.tryGetString import io.ktor.server.config.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@@ -34,6 +36,7 @@ fun Application.configureMongo() {
call.respond(HttpStatusCode.OK, member) call.respond(HttpStatusCode.OK, member)
} }
} }
// Read member // Read member
get("/member/{uid}") { get("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found") val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
@@ -41,20 +44,28 @@ fun Application.configureMongo() {
call.respond(HttpStatusCode.OK, member) call.respond(HttpStatusCode.OK, member)
} ?: call.respond(HttpStatusCode.NotFound) } ?: call.respond(HttpStatusCode.NotFound)
} }
// Create member // Create member
post("/member") { post("/member") {
val member = call.receive<MemberRequest>() val member = call.receive<MemberRequest>()
val id = memberService.create(member.toDto()) val existMember = memberService.read(member.uid)
call.respond(HttpStatusCode.Created, id) if (existMember == null) {
val id = memberService.create(member.toDto())
call.respond(HttpStatusCode.Created, id)
} else {
call.respond(HttpStatusCode.Conflict)
}
} }
// Update member // Update member
put("/member/{uid}") { put("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found") val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
val member = call.receive<Member>() val member = call.receive<UpdateMemberRequest>()
memberService.update(uid, member.toDto())?.let { memberService.update(uid, member.toDto(uid))?.let { member ->
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK, member)
} ?: call.respond(HttpStatusCode.NotFound) } ?: call.respond(HttpStatusCode.NotFound)
} }
// Delete member // Delete member
delete("/member/{uid}") { delete("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found") val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
@@ -71,6 +82,21 @@ fun Application.configureMongo() {
} }
} }
get("/battle_record/{date}/{uid}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
battleRecordService.read(date, uid)?.let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
} ?: call.respond(HttpStatusCode.NotFound)
}
put("/battle_record") {
val request = call.receive<BattleRecord>()
battleRecordService.update(request.date, request.toDto())?.let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
} ?: call.respond(HttpStatusCode.NotFound)
}
post("/battle_record") { post("/battle_record") {
val request = call.receive<BattleRecordRequest>() val request = call.receive<BattleRecordRequest>()
val newAttackDetail = AttackDetail( val newAttackDetail = AttackDetail(
@@ -78,6 +104,7 @@ fun Application.configureMongo() {
bossIndex = request.boss, bossIndex = request.boss,
damage = request.damage, damage = request.damage,
isCompensation = request.isCompensation, isCompensation = request.isCompensation,
isDropped = request.isDropped,
) )
val existData = battleRecordService.read(request.date, request.uid) val existData = battleRecordService.read(request.date, request.uid)
if (existData != null) { if (existData != null) {
@@ -103,7 +130,24 @@ fun Application.configureMongo() {
} }
} }
post("/battle_record/create_table_day/{date}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
val exist = battleRecordService.read(date)
if (exist.isNotEmpty()) {
call.respond(HttpStatusCode.Conflict)
return@post
}
battleRecordService.createTable(date).let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
}
}
post("/battle_record/create_table_month/{date}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
battleRecordService.createTableMonth(date).let { battleRecordList ->
call.respond(HttpStatusCode.OK, battleRecordList)
}
}
} }
} }
} }