From a28742630c0b35658b817667b9ddd0ef87cd63b2 Mon Sep 17 00:00:00 2001 From: nullaqua Date: Mon, 16 Sep 2024 01:17:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E5=B1=80=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/subit/database/Likes.kt | 3 + src/main/kotlin/subit/database/Posts.kt | 4 + src/main/kotlin/subit/database/Stars.kt | 3 + .../subit/database/memoryImpl/LikesImpl.kt | 39 ++++----- .../subit/database/memoryImpl/PostsImpl.kt | 16 ++++ .../subit/database/memoryImpl/StarsImpl.kt | 9 +++ .../subit/database/sqlImpl/LikesImpl.kt | 9 +++ .../subit/database/sqlImpl/PostsImpl.kt | 20 +++++ .../subit/database/sqlImpl/StarsImpl.kt | 9 +++ src/main/kotlin/subit/router/Admin.kt | 81 ++++++++++++++++++- src/main/kotlin/subit/utils/FileUtils.kt | 1 - 11 files changed, 173 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/subit/database/Likes.kt b/src/main/kotlin/subit/database/Likes.kt index 3fecfc9..ae9bab9 100644 --- a/src/main/kotlin/subit/database/Likes.kt +++ b/src/main/kotlin/subit/database/Likes.kt @@ -1,6 +1,7 @@ package subit.database import subit.dataClasses.* +import kotlin.time.Duration /** * @author nullaqua @@ -17,4 +18,6 @@ interface Likes begin: Long = 1, limit: Int = Int.MAX_VALUE, ): Slice + + suspend fun totalLikesCount(duration: Duration?): Long } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/Posts.kt b/src/main/kotlin/subit/database/Posts.kt index 2bcda14..a2aa99c 100644 --- a/src/main/kotlin/subit/database/Posts.kt +++ b/src/main/kotlin/subit/database/Posts.kt @@ -3,6 +3,7 @@ package subit.database import kotlinx.serialization.Serializable import subit.dataClasses.* import subit.router.home.AdvancedSearchData +import kotlin.time.Duration interface Posts { @@ -117,4 +118,7 @@ interface Posts * 获得***最近一个月***点赞数最多的帖子 */ suspend fun monthly(loginUser: DatabaseUser?, begin: Long, count: Int): Slice + + suspend fun totalPostCount(comment: Boolean, duration: Duration?): Map + suspend fun totalReadCount(): Long } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/Stars.kt b/src/main/kotlin/subit/database/Stars.kt index 8eabd17..9ba8021 100644 --- a/src/main/kotlin/subit/database/Stars.kt +++ b/src/main/kotlin/subit/database/Stars.kt @@ -4,6 +4,7 @@ import subit.dataClasses.PostId import subit.dataClasses.Slice import subit.dataClasses.Star import subit.dataClasses.UserId +import kotlin.time.Duration interface Stars { @@ -17,4 +18,6 @@ interface Stars begin: Long = 1, limit: Int = Int.MAX_VALUE, ): Slice + + suspend fun totalStarsCount(duration: Duration?): Long } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/memoryImpl/LikesImpl.kt b/src/main/kotlin/subit/database/memoryImpl/LikesImpl.kt index c1e9500..ad2a959 100644 --- a/src/main/kotlin/subit/database/memoryImpl/LikesImpl.kt +++ b/src/main/kotlin/subit/database/memoryImpl/LikesImpl.kt @@ -1,6 +1,5 @@ package subit.database.memoryImpl -import kotlinx.datetime.Clock import kotlinx.datetime.Instant import subit.dataClasses.Like import subit.dataClasses.PostId @@ -9,35 +8,37 @@ import subit.dataClasses.Slice.Companion.asSlice import subit.dataClasses.UserId import subit.database.Likes import java.util.* +import kotlin.time.Duration class LikesImpl: Likes { - private val map = Collections.synchronizedMap(hashMapOf,Instant>()) - + private val set = Collections.synchronizedSet(hashSetOf()) override suspend fun addLike(uid: UserId, pid: PostId) { - map[uid to pid] = Clock.System.now() + set.add(Like(uid, pid, System.currentTimeMillis())) } + override suspend fun removeLike(uid: UserId, pid: PostId) { - map.remove(uid to pid) - } - override suspend fun getLike(uid: UserId, pid: PostId): Boolean = map[uid to pid] != null - override suspend fun getLikesCount(pid: PostId): Long - { - val likes = map.entries.filter { it.key.second == pid } - return likes.size.toLong() + set.removeIf { it.user == uid && it.post == pid } } - override suspend fun getLikes(user: UserId?, post: PostId?, begin: Long, limit: Int): Slice - { - val likes = map.entries.filter { (user == null || it.key.first == user) && (post == null || it.key.second == post) } - return likes.asSequence().map { Like(it.key.first, it.key.second, it.value.toEpochMilliseconds()) }.asSlice(begin, limit) - } + override suspend fun getLike(uid: UserId, pid: PostId): Boolean = + set.count { it.user == uid && it.post == pid } > 0 + + override suspend fun getLikesCount(pid: PostId): Long = + set.count { it.post == pid }.toLong() - fun getLikesAfter(post: PostId, time: Instant): Long + override suspend fun getLikes(user: UserId?, post: PostId?, begin: Long, limit: Int): Slice = + set.filter { (user == null || it.user == user) && (post == null || it.post == post) } + .sortedByDescending(Like::time).asSequence().asSlice(begin, limit) + + override suspend fun totalLikesCount(duration: Duration?): Long { - val likes = map.entries.filter { it.key.second == post && it.value > time } - return likes.size.toLong() + val time = duration?.let { System.currentTimeMillis() - it.inWholeMilliseconds } ?: 0 + return set.count { it.time > time }.toLong() } + + fun getLikesAfter(pid: PostId, time: Instant): Long = + set.count { it.post == pid && it.time > time.toEpochMilliseconds() }.toLong() } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/memoryImpl/PostsImpl.kt b/src/main/kotlin/subit/database/memoryImpl/PostsImpl.kt index c6ba116..24e89a8 100644 --- a/src/main/kotlin/subit/database/memoryImpl/PostsImpl.kt +++ b/src/main/kotlin/subit/database/memoryImpl/PostsImpl.kt @@ -11,8 +11,10 @@ import subit.dataClasses.PostId.Companion.toPostId import subit.dataClasses.Slice.Companion.asSlice import subit.database.* import subit.router.home.AdvancedSearchData +import subit.utils.toInstant import java.util.* import kotlin.math.pow +import kotlin.time.Duration import kotlin.time.Duration.Companion.days class PostsImpl: Posts, KoinComponent @@ -246,4 +248,18 @@ class PostsImpl: Posts, KoinComponent .asSlice(begin, count) .map { it.toPostFullBasicInfo() } } + + override suspend fun totalPostCount(comment: Boolean, duration: Duration?): Map + { + val time = duration?.let { Clock.System.now() - it } ?: 0L.toInstant() + val res = map.values + .filter { (it.first.parent != null) == comment } + .map { getPostFull(it.first.id)!! } + .filter { it.create != null && it.create >= time.toEpochMilliseconds() } + .groupBy { it.state } + .mapValues { it.value.size.toLong() } + return State.entries.associateWith { res[it] ?: 0 } + } + + override suspend fun totalReadCount(): Long = map.values.sumOf { it.first.view } } diff --git a/src/main/kotlin/subit/database/memoryImpl/StarsImpl.kt b/src/main/kotlin/subit/database/memoryImpl/StarsImpl.kt index fcecb1c..9ddf67f 100644 --- a/src/main/kotlin/subit/database/memoryImpl/StarsImpl.kt +++ b/src/main/kotlin/subit/database/memoryImpl/StarsImpl.kt @@ -1,12 +1,15 @@ package subit.database.memoryImpl +import kotlinx.datetime.Clock import subit.dataClasses.PostId import subit.dataClasses.Slice import subit.dataClasses.Slice.Companion.asSlice import subit.dataClasses.Star import subit.dataClasses.UserId import subit.database.Stars +import subit.utils.toInstant import java.util.* +import kotlin.time.Duration class StarsImpl: Stars { @@ -30,4 +33,10 @@ class StarsImpl: Stars override suspend fun getStars(user: UserId?, post: PostId?, begin: Long, limit: Int): Slice = set.filter { (user == null || it.user == user) && (post == null || it.post == post) } .sortedByDescending(Star::time).asSequence().asSlice(begin, limit) + + override suspend fun totalStarsCount(duration: Duration?): Long + { + val time = duration?.let { Clock.System.now() - it } ?: 0L.toInstant() + return set.count { it.time > time.toEpochMilliseconds() }.toLong() + } } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/sqlImpl/LikesImpl.kt b/src/main/kotlin/subit/database/sqlImpl/LikesImpl.kt index e78bd30..0c1de0b 100644 --- a/src/main/kotlin/subit/database/sqlImpl/LikesImpl.kt +++ b/src/main/kotlin/subit/database/sqlImpl/LikesImpl.kt @@ -1,5 +1,6 @@ package subit.database.sqlImpl +import kotlinx.datetime.Clock import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp @@ -11,6 +12,8 @@ import subit.dataClasses.Slice import subit.dataClasses.UserId import subit.database.Likes import subit.database.sqlImpl.utils.asSlice +import subit.utils.toInstant +import kotlin.time.Duration class LikesImpl: DaoSqlImpl(LikesTable), Likes, KoinComponent { @@ -60,4 +63,10 @@ class LikesImpl: DaoSqlImpl(LikesTable), Likes, KoinCompon post?.let { query.andWhere { LikesTable.post eq it } } query.asSlice(begin, limit).map { deserialize(it) } } + + override suspend fun totalLikesCount(duration: Duration?): Long = query() + { + val time = duration?.let { Clock.System.now() - it } ?: 0L.toInstant() + table.selectAll().where { table.time greaterEq time }.count() + } } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/sqlImpl/PostsImpl.kt b/src/main/kotlin/subit/database/sqlImpl/PostsImpl.kt index a16eaaa..ad776bd 100644 --- a/src/main/kotlin/subit/database/sqlImpl/PostsImpl.kt +++ b/src/main/kotlin/subit/database/sqlImpl/PostsImpl.kt @@ -26,12 +26,14 @@ import subit.database.Posts.PostListSort.* import subit.database.sqlImpl.PostVersionsImpl.PostVersionsTable import subit.database.sqlImpl.PostsImpl.PostsTable.view import subit.database.sqlImpl.utils.asSlice +import subit.database.sqlImpl.utils.single import subit.database.sqlImpl.utils.singleOrNull import subit.router.home.AdvancedSearchData import subit.utils.toInstant import subit.utils.toTimestamp import java.sql.ResultSet import kotlin.reflect.typeOf +import kotlin.time.Duration import kotlin.time.Duration.Companion.days /** @@ -620,4 +622,22 @@ class PostsImpl: DaoSqlImpl(PostsTable), Posts, KoinCompon .asSlice(begin, count) .map { deserializePost(it) } } + + override suspend fun totalPostCount(comment: Boolean, duration: Duration?): Map = query() + { + val time = duration?.let { Clock.System.now() - it } ?: 0L.toInstant() + val res = table + .joinPostFull(true) + .select(state, id.count()) + .andWhere { if (comment) parent.isNotNull() else parent.isNull() } + .andWhere { lastModified.aliasOnlyExpression() greaterEq timestampParam(time) } + .groupBy(state) + .associate { it[state] to it[id.count()] } + State.entries.associateWith { (res[it] ?: 0) } + } + + override suspend fun totalReadCount(): Long = query() + { + table.select(view.sum()).single()[view.sum()] ?: 0 + } } \ No newline at end of file diff --git a/src/main/kotlin/subit/database/sqlImpl/StarsImpl.kt b/src/main/kotlin/subit/database/sqlImpl/StarsImpl.kt index 186f886..6eb1c68 100644 --- a/src/main/kotlin/subit/database/sqlImpl/StarsImpl.kt +++ b/src/main/kotlin/subit/database/sqlImpl/StarsImpl.kt @@ -1,5 +1,6 @@ package subit.database.sqlImpl +import kotlinx.datetime.Clock import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp @@ -10,6 +11,8 @@ import subit.dataClasses.Star import subit.dataClasses.UserId import subit.database.Stars import subit.database.sqlImpl.utils.asSlice +import subit.utils.toInstant +import kotlin.time.Duration /** * 收藏数据库交互类 @@ -68,4 +71,10 @@ class StarsImpl: DaoSqlImpl(StarsTable), Stars q.asSlice(begin, limit).map(::deserialize) } + + override suspend fun totalStarsCount(duration: Duration?): Long = query() + { + val time = duration?.let { Clock.System.now() - it } ?: 0L.toInstant() + table.selectAll().where { table.time greaterEq time }.count() + } } \ No newline at end of file diff --git a/src/main/kotlin/subit/router/Admin.kt b/src/main/kotlin/subit/router/Admin.kt index 6e9d7e3..17146c9 100644 --- a/src/main/kotlin/subit/router/Admin.kt +++ b/src/main/kotlin/subit/router/Admin.kt @@ -15,9 +15,10 @@ import subit.utils.HttpStatus import subit.utils.SSO import subit.utils.respond import subit.utils.statuses +import kotlin.time.Duration.Companion.days fun Route.admin() = route("/admin", { - tags = listOf("用户管理") + tags = listOf("管理") response { statuses(HttpStatus.Unauthorized, HttpStatus.Forbidden) } @@ -65,6 +66,13 @@ fun Route.admin() = route("/admin", { statuses(HttpStatus.OK) } }) { changePermission() } + + get("/globalInfo", { + description = "获取全局信息, 需要当前用户的user权限大于ADMIN" + response { + statuses(HttpStatus.OK, example = GlobalInfo.example) + } + }) { globalInfo() } } @Serializable @@ -119,3 +127,74 @@ private suspend fun Context.changePermission() ) call.respond(HttpStatus.OK) } + +@Serializable +private data class Data( + val day: Long, + val month: Long, + val year: Long, + val total: Long +) +{ + companion object + { + val example = Data(1, 1, 1, 1) + } +} + +@Serializable +private data class GlobalInfo( + val post: Map, + val comment: Data, + val like: Data, + val star: Data, + val read: Long, +) +{ + companion object + { + val example = GlobalInfo( + State.entries.associateWith { Data.example }, + Data.example, + Data.example, + Data.example, + 1 + ) + } +} + +private suspend fun Context.globalInfo() +{ + withPermission { checkHasGlobalAdmin() } + val posts = get() + val dayPost = posts.totalPostCount(false, 1.days) + val monthPost = posts.totalPostCount(false, 30.days) + val yearPost = posts.totalPostCount(false, 365.days) + val totalPost = posts.totalPostCount(false, null) + val post = State.entries.associateWith { Data(dayPost[it]!!, monthPost[it]!!, yearPost[it]!!, totalPost[it]!!) } + + val dayComment = posts.totalPostCount(true, 1.days).values.sum() + val monthComment = posts.totalPostCount(true, 30.days).values.sum() + val yearComment = posts.totalPostCount(true, 365.days).values.sum() + val totalComment = posts.totalPostCount(true, null).values.sum() + val comment = Data(dayComment, monthComment, yearComment, totalComment) + + val read = posts.totalReadCount() + + val likes = get() + val dayLike = likes.totalLikesCount(1.days) + val monthLike = likes.totalLikesCount(30.days) + val yearLike = likes.totalLikesCount(365.days) + val totalLike = likes.totalLikesCount(null) + val like = Data(dayLike, monthLike, yearLike, totalLike) + + val stars = get() + val dayStar = stars.totalStarsCount(1.days) + val monthStar = stars.totalStarsCount(30.days) + val yearStar = stars.totalStarsCount(365.days) + val totalStar = stars.totalStarsCount(null) + val star = Data(dayStar, monthStar, yearStar, totalStar) + + val globalInfo = GlobalInfo(post, comment, like, star, read) + call.respond(HttpStatus.OK, globalInfo) +} \ No newline at end of file diff --git a/src/main/kotlin/subit/utils/FileUtils.kt b/src/main/kotlin/subit/utils/FileUtils.kt index f5d27e1..9b24a6d 100644 --- a/src/main/kotlin/subit/utils/FileUtils.kt +++ b/src/main/kotlin/subit/utils/FileUtils.kt @@ -10,7 +10,6 @@ import subit.dataClasses.PermissionLevel import subit.dataClasses.UserFull import subit.dataClasses.UserId import subit.dataDir -import subit.workDir import java.awt.image.BufferedImage import java.io.File import java.io.FileInputStream