diff --git a/build.gradle.kts b/build.gradle.kts index 7e617cc0..f345fa79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { id("org.hidetake.swagger.generator") version "2.19.2" id("com.gorylenko.gradle-git-properties") version "2.5.2" - val ktVersion = "2.2.0" + val ktVersion = "2.2.10" kotlin("jvm") version ktVersion kotlin("plugin.spring") version ktVersion kotlin("kapt") version ktVersion @@ -47,6 +47,7 @@ repositories { } dependencies { + val ktormVersion = "4.1.1" val hutoolVersion = "5.8.39" val mapstructVersion = "1.6.3" @@ -59,7 +60,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-cache") @@ -77,6 +77,12 @@ dependencies { // kotlin-logging implementation("io.github.oshai:kotlin-logging-jvm:7.0.7") + // ktorm connect with spring-jdbc + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.ktorm:ktorm-core:$ktormVersion") + implementation("org.ktorm:ktorm-jackson:$ktormVersion") + implementation("org.ktorm:ktorm-support-postgresql:$ktormVersion") + implementation("org.postgresql:postgresql:42.7.7") // hutool 的邮箱工具类依赖 implementation("com.sun.mail:javax.mail:1.6.2") implementation("cn.hutool:hutool-extra:$hutoolVersion") diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 377df313..0824078d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -15,6 +15,17 @@ services: ports: - "27017:27017" restart: always + db: + image: postgres:17-alpine + container_name: postgres + restart: always + ports: + - 5432:5432 + volumes: + - .././data/:/var/lib/postgresql/data/ + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + POSTGRES_PASSWORD: 1234 zootplusbackend: image: ghcr.io/zoot-plus/zootplusbackend:latest container_name: ZootPlusBackend diff --git a/docker/init.sql b/docker/init.sql new file mode 100644 index 00000000..d80956b4 --- /dev/null +++ b/docker/init.sql @@ -0,0 +1,273 @@ +create table if not exists "user" +( + user_id text not null + primary key, + user_name text not null, + email text not null, + password text not null, + status integer not null, + pwd_update_time timestamp(3) not null, + following_count integer not null, + fans_count integer not null +); + +create index if not exists idx_user_user_name + on "user" (user_name); + +create unique index if not exists idx_user_user_email + on "user" (email); + +create table if not exists copilot +( + copilot_id serial + primary key, + stage_name text not null, + uploader_id text not null, + views bigint not null, + rating_level integer not null, + rating_ratio double precision not null, + like_count bigint not null, + dislike_count bigint not null, + hot_score double precision not null, + title text not null, + details text, + first_upload_time timestamp(3), + upload_time timestamp(3), + content text not null, + status text default 'PUBLIC'::text not null, + comment_status text default 'ENABLED'::text not null, + delete boolean, + delete_time timestamp(3), + notification boolean +); + +comment on column copilot.copilot_id is '自增数字ID'; +comment on column copilot.stage_name is '关卡名'; +comment on column copilot.uploader_id is '上传者id'; +comment on column copilot.views is '查看次数'; +comment on column copilot.rating_level is '评级'; +comment on column copilot.rating_ratio is '评级比率 十分之一代表半星'; +comment on column copilot.hot_score is '热度'; +comment on column copilot.title is '指定干员@Cascade(["copilot_id"], ["copilot_id"])var opers: List?,文档字段,用于搜索,提取到Copilot类型上'; +comment on column copilot.first_upload_time is '首次上传时间'; +comment on column copilot.upload_time is '更新时间'; +comment on column copilot.content is '原始数据'; +comment on column copilot.delete is '作业状态,后端默认设置为公开以兼容历史逻辑[plus.maa.backend.service.model.CopilotSetStatus]'; + +create index if not exists idx_copilot_stage_name + on copilot (stage_name); + +create index if not exists idx_copilot_view + on copilot (views); + +create index if not exists idx_hot_score + on copilot (hot_score); + +create table if not exists copilot_operator +( + id serial + primary key, + copilot_id bigint not null, + name text not null +); + + +create index if not exists idx_operator_copilot_id + on copilot_operator (copilot_id); + +create index if not exists idx_operator_name + on copilot_operator (name); + +-- 评论区表 +create table if not exists comments_area +( + id text not null + primary key, + copilot_id bigint not null, + from_comment_id text, + uploader_id text not null, + message text not null, + like_count bigint default 0 not null, + dislike_count bigint default 0 not null, + upload_time timestamp(3) not null, + topping boolean default false not null, + delete boolean default false not null, + delete_time timestamp(3), + main_comment_id text, + notification boolean default false not null +); + +comment on table comments_area is '评论区表'; +comment on column comments_area.copilot_id is '关联的作业ID'; +comment on column comments_area.from_comment_id is '回复的评论ID'; +comment on column comments_area.uploader_id is '评论者用户ID'; +comment on column comments_area.message is '评论内容'; +comment on column comments_area.like_count is '点赞数'; +comment on column comments_area.dislike_count is '点踩数'; +comment on column comments_area.upload_time is '评论时间'; +comment on column comments_area.topping is '是否置顶'; +comment on column comments_area.delete is '是否删除'; +comment on column comments_area.delete_time is '删除时间'; +comment on column comments_area.main_comment_id is '主评论ID(如果自身为主评论则为null)'; +comment on column comments_area.notification is '邮件通知'; + +create index if not exists idx_comments_copilot_id + on comments_area (copilot_id); + +create index if not exists idx_comments_uploader_id + on comments_area (uploader_id); + +create index if not exists idx_comments_main_comment_id + on comments_area (main_comment_id); + +-- 评分表 +create table if not exists rating +( + id text not null + primary key, + type text not null, + key text not null, + user_id text not null, + rating text not null, + rate_time timestamp(3) not null +); + +comment on table rating is '评分表'; +comment on column rating.type is '评级类型 (COPILOT/COMMENT)'; +comment on column rating.key is '被评级对象的唯一标识'; +comment on column rating.user_id is '评级的用户ID'; +comment on column rating.rating is '评级 (Like/Dislike/None)'; +comment on column rating.rate_time is '评级时间'; + +-- 复合唯一索引,一个用户对一个对象只能有一种评级 +create unique index if not exists idx_rating_unique + on rating (type, key, user_id); + +create index if not exists idx_rating_user_id + on rating (user_id); + +-- 作业集表 +create table if not exists copilot_set +( + id bigint not null + primary key, + name text not null, + description text not null, + copilot_ids text not null, + creator_id text not null, + create_time timestamp(3) not null, + update_time timestamp(3) not null, + status text default 'PUBLIC'::text not null, + delete boolean default false not null +); + +comment on table copilot_set is '作业集表'; +comment on column copilot_set.name is '作业集名称'; +comment on column copilot_set.description is '额外描述'; +comment on column copilot_set.copilot_ids is 'JSON格式存储的作业ID列表'; +comment on column copilot_set.creator_id is '创建者用户ID'; +comment on column copilot_set.create_time is '创建时间'; +comment on column copilot_set.update_time is '更新时间'; +comment on column copilot_set.status is '状态'; +comment on column copilot_set.delete is '是否删除'; + +create index if not exists idx_copilot_set_creator_id + on copilot_set (creator_id); + +create index if not exists idx_copilot_set_status + on copilot_set (status); + +-- 用户关注表 +create table if not exists user_following +( + id text not null + primary key, + user_id text not null, + following_id text not null, + create_time timestamp(3) not null +); + +comment on table user_following is '用户关注表'; +comment on column user_following.user_id is '关注者用户ID'; +comment on column user_following.following_id is '被关注者用户ID'; +comment on column user_following.create_time is '关注时间'; + +-- 确保一个用户不能重复关注同一个人 +create unique index if not exists idx_user_following_unique + on user_following (user_id, following_id); + +create index if not exists idx_user_following_user_id + on user_following (user_id); + +create index if not exists idx_user_following_following_id + on user_following (following_id); + +-- 用户粉丝表 +create table if not exists user_fans +( + id text not null + primary key, + user_id text not null, + fans_id text not null, + create_time timestamp(3) not null +); + +comment on table user_fans is '用户粉丝表'; +comment on column user_fans.user_id is '被关注者用户ID'; +comment on column user_fans.fans_id is '粉丝用户ID'; +comment on column user_fans.create_time is '关注时间'; + +-- 确保一个用户不能重复成为同一个人的粉丝 +create unique index if not exists idx_user_fans_unique + on user_fans (user_id, fans_id); + +create index if not exists idx_user_fans_user_id + on user_fans (user_id); + +create index if not exists idx_user_fans_fans_id + on user_fans (fans_id); + +-- 关卡表 +create table if not exists ark_level +( + id text not null + primary key, + level_id text, + stage_id text, + sha text not null, + cat_one text, + cat_two text, + cat_three text, + name text, + width integer not null, + height integer not null, + is_open boolean, + close_time timestamp(3) +); + +comment on table ark_level is '明日方舟关卡表'; +comment on column ark_level.id is '关卡唯一标识'; +comment on column ark_level.level_id is '关卡ID'; +comment on column ark_level.stage_id is '阶段ID'; +comment on column ark_level.sha is 'SHA哈希值'; +comment on column ark_level.cat_one is '分类一'; +comment on column ark_level.cat_two is '分类二'; +comment on column ark_level.cat_three is '分类三'; +comment on column ark_level.name is '关卡名称'; +comment on column ark_level.width is '宽度'; +comment on column ark_level.height is '高度'; +comment on column ark_level.is_open is '是否开放'; +comment on column ark_level.close_time is '关闭时间'; + +create index if not exists idx_ark_level_level_id + on ark_level (level_id); + +create index if not exists idx_ark_level_stage_id + on ark_level (stage_id); + +create index if not exists idx_ark_level_name + on ark_level (name); + +create index if not exists idx_ark_level_is_open + on ark_level (is_open); + diff --git a/src/main/kotlin/plus/maa/backend/cache/InternalComposeCache.kt b/src/main/kotlin/plus/maa/backend/cache/InternalComposeCache.kt index 3ef023de..0d14a0dd 100644 --- a/src/main/kotlin/plus/maa/backend/cache/InternalComposeCache.kt +++ b/src/main/kotlin/plus/maa/backend/cache/InternalComposeCache.kt @@ -3,7 +3,7 @@ package plus.maa.backend.cache import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.stats.CacheStats import plus.maa.backend.cache.transfer.CopilotInnerCacheInfo -import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserEntity import java.time.Duration object InternalComposeCache { @@ -18,7 +18,7 @@ object InternalComposeCache { private val maaUserCache = Caffeine.newBuilder() .recordStats() .expireAfterAccess(Duration.ofDays(3)) - .build() + .build() // copilotId -> count private val commentCountCache = Caffeine.newBuilder() @@ -34,15 +34,11 @@ object InternalComposeCache { return copilotCache.get(cid, f) } - fun getCopilotCache(cid: Long): CopilotInnerCacheInfo? { - return copilotCache.getIfPresent(cid) - } - - fun getMaaUserCache(userId: String, f: (String) -> MaaUser): MaaUser { + fun getMaaUserCache(userId: String, f: (String) -> UserEntity): UserEntity { return maaUserCache.get(userId, f) } - fun getMaaUserCache(userId: String): MaaUser? { + fun getMaaUserCache(userId: String): UserEntity? { return maaUserCache.getIfPresent(userId) } @@ -50,7 +46,7 @@ object InternalComposeCache { return maaUserCache.stats() } - fun setMaaUserCache(userId: String, info: MaaUser) { + fun setUserCache(userId: String, info: UserEntity) { maaUserCache.put(userId, info) } diff --git a/src/main/kotlin/plus/maa/backend/cache/transfer/CopilotInnerCacheInfo.kt b/src/main/kotlin/plus/maa/backend/cache/transfer/CopilotInnerCacheInfo.kt index cb348dec..cf3e496e 100644 --- a/src/main/kotlin/plus/maa/backend/cache/transfer/CopilotInnerCacheInfo.kt +++ b/src/main/kotlin/plus/maa/backend/cache/transfer/CopilotInnerCacheInfo.kt @@ -1,9 +1,9 @@ package plus.maa.backend.cache.transfer -import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.CopilotEntity import java.util.concurrent.atomic.AtomicLong data class CopilotInnerCacheInfo( - val info: Copilot, + val info: CopilotEntity, val view: AtomicLong = AtomicLong(info.views), ) diff --git a/src/main/kotlin/plus/maa/backend/common/extensions/KtormExtensions.kt b/src/main/kotlin/plus/maa/backend/common/extensions/KtormExtensions.kt new file mode 100644 index 00000000..f82b3c94 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/extensions/KtormExtensions.kt @@ -0,0 +1,51 @@ +package plus.maa.backend.common.extensions + +import org.ktorm.entity.EntitySequence +import org.ktorm.entity.count +import org.ktorm.entity.drop +import org.ktorm.entity.take +import org.ktorm.entity.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserEntity + +data class PageResult( + val data: List, + val total: Long, + val page: Int, + val size: Int, + val hasNext: Boolean = (page * size) < total, +) + +fun EntitySequence.paginate(pageable: Pageable): Page { + val total = this.count() + val data = this.drop(pageable.offset.toInt()).take(pageable.pageSize).toList() + return PageImpl(data, pageable, total.toLong()) +} + +fun EntitySequence.paginate(page: Int, size: Int): PageResult { + val total = this.count() + val offset = (page - 1) * size + val data = this.drop(offset).take(size).toList() + return PageResult(data, total.toLong(), page, size) +} + +fun EntitySequence.limitAndOffset(limit: Int, offset: Int): List { + return this.drop(offset).take(limit).toList() +} + +// Entity转换扩展函数 +fun UserEntity.toMaaUser(): MaaUser { + return MaaUser( + userId = this.userId, + userName = this.userName, + email = this.email, + password = this.password, + status = this.status, + pwdUpdateTime = this.pwdUpdateTime, + followingCount = this.followingCount, + fansCount = this.fansCount, + ) +} diff --git a/src/main/kotlin/plus/maa/backend/common/extensions/MongoExtensions.kt b/src/main/kotlin/plus/maa/backend/common/extensions/MongoExtensions.kt deleted file mode 100644 index a13a793f..00000000 --- a/src/main/kotlin/plus/maa/backend/common/extensions/MongoExtensions.kt +++ /dev/null @@ -1,34 +0,0 @@ -package plus.maa.backend.common.extensions - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.core.MongoOperations -import org.springframework.data.mongodb.core.count -import org.springframework.data.mongodb.core.find -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.support.PageableExecutionUtils - -inline fun MongoOperations.findPage( - pageable: Pageable, - query: Query = Query(), - customizer: Query.() -> Unit = {}, -): Page { - val q = query.apply(customizer) - check(q.skip == 0L && !q.isLimited) { "query should not be paged" } - val content = find(q.with(pageable)) - val total = count(q.skip(0).limit(0)) - return PageableExecutionUtils.getPage(content, pageable) { total } -} - -fun Query.addAndCriteria(vararg criteria: Criteria) { - addCriteria(Criteria().andOperator(*criteria)) -} - -fun Query.addOrCriteria(vararg criteria: Criteria) { - addCriteria(Criteria().orOperator(*criteria)) -} - -fun Query.addNorCriteria(vararg criteria: Criteria) { - addCriteria(Criteria().norOperator(*criteria)) -} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt b/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt index 6cae83cf..46a645d8 100644 --- a/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt +++ b/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt @@ -1,11 +1,15 @@ package plus.maa.backend.common.utils import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.domain.Sort -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.query.Query +import org.ktorm.database.Database +import org.ktorm.dsl.from +import org.ktorm.dsl.map +import org.ktorm.dsl.max +import org.ktorm.dsl.select import org.springframework.stereotype.Component import plus.maa.backend.repository.entity.CollectionMeta +import plus.maa.backend.repository.entity.CopilotSets +import plus.maa.backend.repository.entity.Copilots import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong @@ -13,7 +17,7 @@ private val log = KotlinLogging.logger { } @Component class IdComponent( - private val mongoTemplate: MongoTemplate, + private val database: Database, ) { private val currentIdMap: MutableMap = ConcurrentHashMap() @@ -23,15 +27,15 @@ class IdComponent( * @return 新的id */ fun getId(meta: CollectionMeta): Long { - val cls = meta.entityClass - val collectionName = mongoTemplate.getCollectionName(cls) + val collectionName = meta.entityClass.simpleName val v = currentIdMap[collectionName] if (v == null) { - synchronized(cls) { + synchronized(meta.entityClass) { val rv = currentIdMap[collectionName] if (rv == null) { - val nv = AtomicLong(getMax(cls, meta.idGetter, meta.incIdField)) - log.info { "初始化获取 collection: $collectionName 的最大 id,id: ${nv.get()}" } + val maxId = getMaxId(meta.entityClass) + val nv = AtomicLong(maxId) + log.info { "初始化获取 $collectionName 的最大 id,id: ${nv.get()}" } currentIdMap[collectionName] = nv return nv.incrementAndGet() } @@ -41,10 +45,15 @@ class IdComponent( return v.incrementAndGet() } - private fun getMax(entityClass: Class, idGetter: (T) -> Long, fieldName: String) = mongoTemplate.findOne( - Query().with(Sort.by(fieldName).descending()).limit(1), - entityClass, - ) - ?.let(idGetter) - ?: 20000L + private fun getMaxId(entityClass: Class): Long { + return when (entityClass.simpleName) { + "Copilot" -> { + database.from(Copilots).select(max(Copilots.copilotId)).map { it.getLong(1) }.firstOrNull() ?: 20000L + } + "CopilotSet" -> { + database.from(CopilotSets).select(max(CopilotSets.id)).map { it.getLong(1) }.firstOrNull() ?: 20000L + } + else -> 20000L + } + } } diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt index ace69dd6..41d2e32e 100644 --- a/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt @@ -2,7 +2,7 @@ package plus.maa.backend.common.utils.converter import org.mapstruct.Mapper import plus.maa.backend.controller.response.copilot.ArkLevelInfo -import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.ArkLevelEntity /** * @author dragove @@ -10,7 +10,20 @@ import plus.maa.backend.repository.entity.ArkLevel */ @Mapper(componentModel = "spring") interface ArkLevelConverter { - fun convert(arkLevel: ArkLevel): ArkLevelInfo + fun convert(arkLevel: ArkLevelEntity): ArkLevelInfo { + return ArkLevelInfo( + levelId = arkLevel.levelId ?: "", + stageId = arkLevel.stageId ?: "", + catOne = arkLevel.catOne ?: "", + catTwo = arkLevel.catTwo ?: "", + catThree = arkLevel.catThree ?: "", + name = arkLevel.name ?: "", + width = arkLevel.width, + height = arkLevel.height, + ) + } - fun convert(arkLevel: List): List + fun convert(arkLevels: List): List { + return arkLevels.map { convert(it) } + } } diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelEntityConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelEntityConverter.kt new file mode 100644 index 00000000..45f2772f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelEntityConverter.kt @@ -0,0 +1,51 @@ +package plus.maa.backend.common.utils.converter + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.ArkLevelEntity + +@Component +class ArkLevelEntityConverter { + + fun convertToEntity(arkLevel: ArkLevel): ArkLevelEntity { + return ArkLevelEntity { + this.id = arkLevel.id ?: "" + this.levelId = arkLevel.levelId + this.stageId = arkLevel.stageId + this.sha = arkLevel.sha + this.catOne = arkLevel.catOne + this.catTwo = arkLevel.catTwo + this.catThree = arkLevel.catThree + this.name = arkLevel.name + this.width = arkLevel.width + this.height = arkLevel.height + this.isOpen = arkLevel.isOpen + this.closeTime = arkLevel.closeTime + } + } + + fun convertFromEntity(entity: ArkLevelEntity): ArkLevel { + return ArkLevel( + id = entity.id, + levelId = entity.levelId, + stageId = entity.stageId, + sha = entity.sha, + catOne = entity.catOne, + catTwo = entity.catTwo, + catThree = entity.catThree, + name = entity.name, + width = entity.width, + height = entity.height, + isOpen = entity.isOpen, + closeTime = entity.closeTime, + ) + } + + fun convertFromEntities(entities: List): List { + return entities.map { convertFromEntity(it) } + } + + fun convertToEntities(arkLevels: List): List { + return arkLevels.map { convertToEntity(it) } + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt index f86a52aa..d68d943f 100644 --- a/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt @@ -1,15 +1,6 @@ package plus.maa.backend.common.utils.converter -import org.mapstruct.BeanMapping import org.mapstruct.Mapper -import org.mapstruct.Mapping -import org.mapstruct.MappingTarget -import org.mapstruct.NullValuePropertyMappingStrategy -import plus.maa.backend.controller.request.copilot.CopilotDTO -import plus.maa.backend.controller.response.copilot.CopilotInfo -import plus.maa.backend.repository.entity.Copilot -import plus.maa.backend.service.model.CopilotSetStatus -import java.time.LocalDateTime /** * @author LoMu @@ -17,62 +8,5 @@ import java.time.LocalDateTime */ @Mapper(componentModel = "spring") interface CopilotConverter { - /** - * 实现增量更新 - * 将copilotDto 映射覆盖数据库中的 copilot - * 映射中跳过空值 - * - * @param copilotDTO 更新值 - * @param copilot 从数据库中查出的原始值 - */ - @Mapping(target = "deleteTime", ignore = true) - @Mapping(target = "delete", constant = "false") - @Mapping(target = "id", ignore = true) - @Mapping(target = "copilotId", ignore = true) - @Mapping(target = "views", ignore = true) - @Mapping(target = "hotScore", ignore = true) - @Mapping(target = "uploaderId", ignore = true) - @Mapping(target = "uploadTime", ignore = true) - @Mapping(target = "firstUploadTime", ignore = true) - @Mapping(target = "likeCount", ignore = true) - @Mapping(target = "dislikeCount", ignore = true) - @Mapping(target = "ratingRatio", ignore = true) - @Mapping(target = "ratingLevel", ignore = true) - @Mapping(target = "commentStatus", ignore = true) - @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) - fun updateCopilotFromDto(copilotDTO: CopilotDTO, content: String, @MappingTarget copilot: Copilot, status: CopilotSetStatus) - - @Mapping(target = "id", ignore = true) - @Mapping(target = "deleteTime", ignore = true) - @Mapping(target = "likeCount", ignore = true) - @Mapping(target = "dislikeCount", ignore = true) - @Mapping(target = "ratingRatio", ignore = true) - @Mapping(target = "ratingLevel", ignore = true) - @Mapping(target = "views", constant = "0L") - @Mapping(target = "hotScore", constant = "0") - @Mapping(target = "delete", constant = "false") - @Mapping(target = "uploadTime", source = "now") - @Mapping(target = "firstUploadTime", source = "now") - @Mapping(target = "uploaderId", source = "userId") - @Mapping(target = "commentStatus", ignore = true) - fun toCopilot( - copilotDto: CopilotDTO, - copilotId: Long, - userId: String, - now: LocalDateTime, - content: String, - status: CopilotSetStatus, - ): Copilot - - @Mapping(target = "ratingType", ignore = true) - @Mapping(target = "ratingRatio", ignore = true) - @Mapping(target = "ratingLevel", ignore = true) - @Mapping(target = "notEnoughRating", ignore = true) - @Mapping(target = "available", ignore = true) - @Mapping(target = "id", source = "copilotId") - @Mapping(target = "uploader", source = "userName") - @Mapping(target = "like", source = "copilot.likeCount") - @Mapping(target = "dislike", source = "copilot.dislikeCount") - @Mapping(target = "commentsCount", conditionExpression = "java(commentsCount != null)") - fun toCopilotInfo(copilot: Copilot, userName: String, copilotId: Long, commentsCount: Long?): CopilotInfo + // 所有方法已迁移到服务层中手动处理 } diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt index 5c3bf95f..78d8aadb 100644 --- a/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt @@ -1,11 +1,10 @@ package plus.maa.backend.common.utils.converter import org.mapstruct.Mapper -import org.mapstruct.Mapping -import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq import plus.maa.backend.controller.response.copilotset.CopilotSetListRes import plus.maa.backend.controller.response.copilotset.CopilotSetRes -import plus.maa.backend.repository.entity.CopilotSet +import plus.maa.backend.repository.entity.CopilotSetEntity +import plus.maa.backend.repository.entity.copilotIdsList import java.time.LocalDateTime /** @@ -17,14 +16,34 @@ import java.time.LocalDateTime imports = [LocalDateTime::class], ) interface CopilotSetConverter { - @Mapping(target = "delete", ignore = true) - @Mapping(target = "deleteTime", ignore = true) - @Mapping(target = "copilotIds", expression = "java(createReq.distinctIdsAndCheck())") - @Mapping(target = "createTime", expression = "java(LocalDateTime.now())") - @Mapping(target = "updateTime", expression = "java(LocalDateTime.now())") - fun convert(createReq: CopilotSetCreateReq, id: Long, creatorId: String): CopilotSet + // 旧的CopilotSet相关方法已废弃,转为手动处理 - fun convert(copilotSet: CopilotSet, creator: String): CopilotSetListRes + // 手动转换方法用于处理CopilotSetEntity + fun convert(copilotSetEntity: CopilotSetEntity, creator: String): CopilotSetListRes { + return CopilotSetListRes( + id = copilotSetEntity.id, + name = copilotSetEntity.name, + description = copilotSetEntity.description, + creatorId = copilotSetEntity.creatorId, + creator = creator, + status = copilotSetEntity.status, + createTime = copilotSetEntity.createTime, + updateTime = copilotSetEntity.updateTime, + copilotIds = copilotSetEntity.copilotIdsList, + ) + } - fun convertDetail(copilotSet: CopilotSet, creator: String): CopilotSetRes + fun convertDetail(copilotSetEntity: CopilotSetEntity, creator: String): CopilotSetRes { + return CopilotSetRes( + id = copilotSetEntity.id, + name = copilotSetEntity.name, + description = copilotSetEntity.description, + copilotIds = copilotSetEntity.copilotIdsList, + creatorId = copilotSetEntity.creatorId, + creator = creator, + createTime = copilotSetEntity.createTime, + updateTime = copilotSetEntity.updateTime, + status = copilotSetEntity.status, + ) + } } diff --git a/src/main/kotlin/plus/maa/backend/config/KtormConfig.kt b/src/main/kotlin/plus/maa/backend/config/KtormConfig.kt new file mode 100644 index 00000000..28f36eb0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/KtormConfig.kt @@ -0,0 +1,21 @@ +package plus.maa.backend.config + +import com.fasterxml.jackson.databind.Module +import org.ktorm.database.Database +import org.ktorm.jackson.KtormModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.sql.DataSource + +@Configuration +class KtormConfig(val dataSource: DataSource) { + @Bean + fun database(): Database { + return Database.connectWithSpringSupport(dataSource) + } + + @Bean + fun ktormModule(): Module { + return KtormModule() + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/validation/JsonSchemaMatchValidator.kt b/src/main/kotlin/plus/maa/backend/config/validation/JsonSchemaMatchValidator.kt index ddeab2c7..8b1905b7 100644 --- a/src/main/kotlin/plus/maa/backend/config/validation/JsonSchemaMatchValidator.kt +++ b/src/main/kotlin/plus/maa/backend/config/validation/JsonSchemaMatchValidator.kt @@ -35,7 +35,6 @@ class JsonSchemaMatchValidator : ConstraintValidator { loadSchema(COPILOT_SCHEMA_JSON), ) - @Suppress("SameParameterValue") private fun loadSchema(path: String): Pair { val jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012) diff --git a/src/main/kotlin/plus/maa/backend/controller/UserController.kt b/src/main/kotlin/plus/maa/backend/controller/UserController.kt index 9b7e7726..0cef3dd6 100644 --- a/src/main/kotlin/plus/maa/backend/controller/UserController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/UserController.kt @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import jakarta.validation.constraints.Max -import org.springframework.data.domain.PageRequest import org.springframework.http.HttpStatus import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping @@ -179,8 +178,7 @@ class UserController( @RequestParam page: Int = 1, @Max(50, message = "查询用户量不能超过50") @RequestParam size: Int = 10, ): MaaResult> { - val pageable = PageRequest.of(page - 1, size) - val resultPage = userService.search(userName, pageable) - return success(resultPage.content) + val result = userService.search(userName, (page - 1) * size, size) + return success(result.map(::MaaUserInfo)) } } diff --git a/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt b/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt index 91d0d743..c764684b 100644 --- a/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt @@ -1,115 +1,115 @@ package plus.maa.backend.controller.file - -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.Parameter -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import jakarta.servlet.http.HttpServletResponse -import jakarta.validation.Valid -import org.springframework.http.MediaType -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestPart -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.multipart.MultipartFile -import plus.maa.backend.config.accesslimit.AccessLimit -import plus.maa.backend.config.doc.RequireJwt -import plus.maa.backend.config.security.AuthenticationHelper -import plus.maa.backend.controller.response.MaaResult -import plus.maa.backend.controller.response.MaaResult.Companion.fail -import plus.maa.backend.controller.response.MaaResult.Companion.success -import plus.maa.backend.service.FileService - -/** - * @author LoMu - * Date 2023-03-31 16:41 - */ -@RestController -@RequestMapping("file") -class FileController( - private val fileService: FileService, - private val helper: AuthenticationHelper, -) { - /** - * 支持匿名 - * - * @param file file - * @return 上传成功, 数据已被接收 - */ - @AccessLimit - @PostMapping(value = ["/upload"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) - fun uploadFile( - @RequestPart file: MultipartFile, - @RequestPart type: String?, - @RequestPart version: String, - @RequestPart(required = false) classification: String?, - @RequestPart(required = false) label: String, - ): MaaResult { - fileService.uploadFile(file, type, version, classification, label, helper.obtainUserIdOrIpAddress()) - return success("上传成功,数据已被接收") - } - - @Operation(summary = "下载文件") - @ApiResponse( - responseCode = "200", - content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))], - ) - @RequireJwt - @AccessLimit - @GetMapping("/download") - fun downloadSpecifiedDateFile( - @Parameter(description = "日期 yyyy-MM-dd") date: String?, - @Parameter(description = "在日期之前或之后[before,after]") beLocated: String, - @Parameter(description = "对查询到的数据进行删除") delete: Boolean, - response: HttpServletResponse, - ) { - fileService.downloadDateFile(date, beLocated, delete, response) - } - - @Operation(summary = "下载文件") - @ApiResponse( - responseCode = "200", - content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))], - ) - @RequireJwt - @PostMapping("/download") - fun downloadFile(@RequestBody imageDownloadDTO: @Valid ImageDownloadDTO, response: HttpServletResponse) { - fileService.downloadFile(imageDownloadDTO, response) - } - - @Operation(summary = "设置上传文件功能状态") - @RequireJwt - @PostMapping("/upload_ability") - fun setUploadAbility(@RequestBody request: UploadAbility): MaaResult { - fileService.isUploadEnabled = request.enabled - return success() - } - - @GetMapping("/upload_ability") - @RequireJwt - @Operation(summary = "获取上传文件功能状态") - fun getUploadAbility(): MaaResult = success(UploadAbility(fileService.isUploadEnabled)) - - @Operation(summary = "关闭uploadfile接口") - @RequireJwt - @PostMapping("/disable") - fun disable(@RequestBody status: Boolean): MaaResult { - if (!status) { - return fail(403, "Forbidden") - } - return success(fileService.disable()) - } - - @Operation(summary = "开启uploadfile接口") - @RequireJwt - @PostMapping("/enable") - fun enable(@RequestBody status: Boolean): MaaResult { - if (!status) { - return fail(403, "Forbidden") - } - return success(fileService.enable()) - } -} +// TODO 摸一下 +//import io.swagger.v3.oas.annotations.Operation +//import io.swagger.v3.oas.annotations.Parameter +//import io.swagger.v3.oas.annotations.media.Content +//import io.swagger.v3.oas.annotations.media.Schema +//import io.swagger.v3.oas.annotations.responses.ApiResponse +//import jakarta.servlet.http.HttpServletResponse +//import jakarta.validation.Valid +//import org.springframework.http.MediaType +//import org.springframework.web.bind.annotation.GetMapping +//import org.springframework.web.bind.annotation.PostMapping +//import org.springframework.web.bind.annotation.RequestBody +//import org.springframework.web.bind.annotation.RequestMapping +//import org.springframework.web.bind.annotation.RequestPart +//import org.springframework.web.bind.annotation.RestController +//import org.springframework.web.multipart.MultipartFile +//import plus.maa.backend.config.accesslimit.AccessLimit +//import plus.maa.backend.config.doc.RequireJwt +//import plus.maa.backend.config.security.AuthenticationHelper +//import plus.maa.backend.controller.response.MaaResult +//import plus.maa.backend.controller.response.MaaResult.Companion.fail +//import plus.maa.backend.controller.response.MaaResult.Companion.success +//import plus.maa.backend.service.FileService +// +///** +// * @author LoMu +// * Date 2023-03-31 16:41 +// */ +//@RestController +//@RequestMapping("file") +//class FileController( +// private val fileService: FileService, +// private val helper: AuthenticationHelper, +//) { +// /** +// * 支持匿名 +// * +// * @param file file +// * @return 上传成功, 数据已被接收 +// */ +// @AccessLimit +// @PostMapping(value = ["/upload"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) +// fun uploadFile( +// @RequestPart file: MultipartFile, +// @RequestPart type: String?, +// @RequestPart version: String, +// @RequestPart(required = false) classification: String?, +// @RequestPart(required = false) label: String, +// ): MaaResult { +// fileService.uploadFile(file, type, version, classification, label, helper.obtainUserIdOrIpAddress()) +// return success("上传成功,数据已被接收") +// } +// +// @Operation(summary = "下载文件") +// @ApiResponse( +// responseCode = "200", +// content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))], +// ) +// @RequireJwt +// @AccessLimit +// @GetMapping("/download") +// fun downloadSpecifiedDateFile( +// @Parameter(description = "日期 yyyy-MM-dd") date: String?, +// @Parameter(description = "在日期之前或之后[before,after]") beLocated: String, +// @Parameter(description = "对查询到的数据进行删除") delete: Boolean, +// response: HttpServletResponse, +// ) { +// fileService.downloadDateFile(date, beLocated, delete, response) +// } +// +// @Operation(summary = "下载文件") +// @ApiResponse( +// responseCode = "200", +// content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))], +// ) +// @RequireJwt +// @PostMapping("/download") +// fun downloadFile(@RequestBody imageDownloadDTO: @Valid ImageDownloadDTO, response: HttpServletResponse) { +// fileService.downloadFile(imageDownloadDTO, response) +// } +// +// @Operation(summary = "设置上传文件功能状态") +// @RequireJwt +// @PostMapping("/upload_ability") +// fun setUploadAbility(@RequestBody request: UploadAbility): MaaResult { +// fileService.isUploadEnabled = request.enabled +// return success() +// } +// +// @GetMapping("/upload_ability") +// @RequireJwt +// @Operation(summary = "获取上传文件功能状态") +// fun getUploadAbility(): MaaResult = success(UploadAbility(fileService.isUploadEnabled)) +// +// @Operation(summary = "关闭uploadfile接口") +// @RequireJwt +// @PostMapping("/disable") +// fun disable(@RequestBody status: Boolean): MaaResult { +// if (!status) { +// return fail(403, "Forbidden") +// } +// return success(fileService.disable()) +// } +// +// @Operation(summary = "开启uploadfile接口") +// @RequireJwt +// @PostMapping("/enable") +// fun enable(@RequestBody status: Boolean): MaaResult { +// if (!status) { +// return fail(403, "Forbidden") +// } +// return success(fileService.enable()) +// } +//} diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt index fc670c9a..c006a327 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt @@ -10,7 +10,7 @@ import plus.maa.backend.repository.entity.Copilot data class CopilotDTO( // 关卡名 @field:NotBlank(message = "关卡名不能为空") - var stageName: String, + var stageName: String = "", // 难度 val difficulty: Int = 0, // 版本号(文档中说明:最低要求 maa 版本号,必选。保留字段) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt index 4ddeac43..c5d34724 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt @@ -10,17 +10,17 @@ import plus.maa.backend.service.model.CopilotSetStatus */ data class CopilotQueriesRequest( val page: Int = 0, - @Max(value = 50, message = "单页大小不得超过50") + @field:Max(value = 50, message = "单页大小不得超过50") val limit: Int = 10, - @BindParam("level_keyword") var levelKeyword: String? = null, + @field:BindParam("level_keyword") var levelKeyword: String? = null, val operator: String? = null, val content: String? = null, val document: String? = null, - @BindParam("uploader_id") var uploaderId: String? = null, + @field:BindParam("uploader_id") var uploaderId: String? = null, val desc: Boolean = true, - @BindParam("order_by") var orderBy: String? = null, + @field:BindParam("order_by") var orderBy: String? = null, val language: String? = null, - @BindParam("copilot_ids") var copilotIds: List? = null, + @field:BindParam("copilot_ids") var copilotIds: List? = null, val status: CopilotSetStatus? = null, val onlyFollowing: Boolean = false, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt index a8da705c..8d021515 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt @@ -8,9 +8,9 @@ import plus.maa.backend.config.validation.RatingType * Date 2023-01-20 16:25 */ data class CopilotRatingReq( - @NotBlank(message = "评分作业id不能为空") + @field:NotBlank(message = "评分作业id不能为空") val id: Long, - @NotBlank(message = "评分不能为空") + @field:NotBlank(message = "评分不能为空") @RatingType val rating: String, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt index 47ce9091..3a181817 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt @@ -13,19 +13,19 @@ import plus.maa.backend.service.model.CopilotSetStatus */ @Schema(title = "作业集创建请求") data class CopilotSetCreateReq( - @Schema(title = "作业集名称") + @field:Schema(title = "作业集名称") @field:NotBlank(message = "作业集名称不能为空") val name: String, - @Schema(title = "作业集额外描述") + @field:Schema(title = "作业集额外描述") val description: String = "", - @Schema(title = "初始关联作业列表") + @field:Schema(title = "初始关联作业列表") @field:NotNull(message = "作业id列表字段不能为null") - @Size( + @field:Size( max = 1000, message = "作业集作业列表最大只能为1000", ) override val copilotIds: MutableList, - @Schema(title = "作业集公开状态", enumAsRef = true) + @field:Schema(title = "作业集公开状态", enumAsRef = true) @field:NotNull(message = "作业集公开状态不能为null") val status: CopilotSetStatus, ) : CopilotSetType diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt index 72fb5123..453355b3 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt @@ -10,10 +10,10 @@ import jakarta.validation.constraints.NotNull */ @Schema(title = "作业集新增作业列表请求") data class CopilotSetModCopilotsReq( - @Schema(title = "作业集id") + @field:Schema(title = "作业集id") @field:NotNull(message = "作业集id必填") val id: Long, - @Schema(title = "添加/删除收藏的作业id列表") + @field:Schema(title = "添加/删除收藏的作业id列表") @field:NotEmpty(message = "添加/删除作业id列表不可为空") val copilotIds: MutableList, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt index dce7e1ab..4bf2320d 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt @@ -11,19 +11,19 @@ import jakarta.validation.constraints.PositiveOrZero */ @Schema(title = "作业集列表查询接口参数") data class CopilotSetQuery( - @Schema(title = "页码") - @Positive(message = "页码必须为大于0的数字") + @field:Schema(title = "页码") + @field:Positive(message = "页码必须为大于0的数字") val page: Int = 1, - @Schema(title = "单页数据量") - @PositiveOrZero(message = "单页数据量必须为大于等于0的数字") - @Max(value = 50, message = "单页大小不得超过50") + @field:Schema(title = "单页数据量") + @field:PositiveOrZero(message = "单页数据量必须为大于等于0的数字") + @field:Max(value = 50, message = "单页大小不得超过50") val limit: Int = 10, - @Schema(title = "查询关键词") + @field:Schema(title = "查询关键词") val keyword: String? = null, - @Schema(title = "创建者id") + @field:Schema(title = "创建者id") val creatorId: String? = null, - @Schema(title = "仅查询关注者的作业集") + @field:Schema(title = "仅查询关注者的作业集") var onlyFollowing: Boolean = false, - @Schema(title = "需要包含的作业id列表") + @field:Schema(title = "需要包含的作业id列表") val copilotIds: List? = null, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt index 54cdbdb9..4525f077 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt @@ -12,12 +12,12 @@ import plus.maa.backend.service.model.CopilotSetStatus data class CopilotSetUpdateReq( @field:NotNull(message = "作业集id不能为空") val id: Long, - @Schema(title = "作业集名称") + @field:Schema(title = "作业集名称") val name: String?, - @Schema(title = "作业集额外描述") + @field:Schema(title = "作业集额外描述") val description: String?, - @Schema(title = "作业集公开状态", enumAsRef = true) + @field:Schema(title = "作业集公开状态", enumAsRef = true) val status: CopilotSetStatus?, - @Schema(title = "作业集作业列表") + @field:Schema(title = "作业集作业列表") val copilotIds: MutableList?, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt index ceda835e..41801ea1 100644 --- a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt @@ -10,22 +10,22 @@ import java.time.LocalDateTime */ @Schema(title = "作业集响应(列表)") data class CopilotSetListRes( - @Schema(title = "作业集id") + @field:Schema(title = "作业集id") val id: Long, - @Schema(title = "作业集名称") + @field:Schema(title = "作业集名称") val name: String, - @Schema(title = "额外描述") + @field:Schema(title = "额外描述") val description: String, - @Schema(title = "上传者id") + @field:Schema(title = "上传者id") val creatorId: String, - @Schema(title = "上传者昵称") + @field:Schema(title = "上传者昵称") val creator: String, - @Schema(title = "作业状态", enumAsRef = true) + @field:Schema(title = "作业状态", enumAsRef = true) val status: CopilotSetStatus, - @Schema(title = "创建时间") + @field:Schema(title = "创建时间") val createTime: LocalDateTime, - @Schema(title = "更新时间") + @field:Schema(title = "更新时间") val updateTime: LocalDateTime, - @Schema(title = "作业id列表") + @field:Schema(title = "作业id列表") val copilotIds: List, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt index dea02857..c1bd24fc 100644 --- a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt @@ -10,22 +10,22 @@ import java.time.LocalDateTime */ @Schema(title = "作业集响应") data class CopilotSetRes( - @Schema(title = "作业集id") + @field:Schema(title = "作业集id") val id: Long, - @Schema(title = "作业集名称") + @field:Schema(title = "作业集名称") val name: String, - @Schema(title = "额外描述") + @field:Schema(title = "额外描述") val description: String, - @Schema(title = "作业id列表") + @field:Schema(title = "作业id列表") val copilotIds: List, - @Schema(title = "上传者id") + @field:Schema(title = "上传者id") val creatorId: String, - @Schema(title = "上传者昵称") + @field:Schema(title = "上传者昵称") val creator: String, - @Schema(title = "创建时间") + @field:Schema(title = "创建时间") val createTime: LocalDateTime, - @Schema(title = "更新时间") + @field:Schema(title = "更新时间") val updateTime: LocalDateTime, - @Schema(title = "作业状态", enumAsRef = true) + @field:Schema(title = "作业状态", enumAsRef = true) val status: CopilotSetStatus, ) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt index a272927a..efac5442 100644 --- a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt +++ b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt @@ -1,6 +1,7 @@ package plus.maa.backend.controller.response.user import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserEntity /** * 用户可对外公开的信息 @@ -21,4 +22,11 @@ data class MaaUserInfo( followingCount = user.followingCount, fansCount = user.fansCount, ) + constructor(user: UserEntity) : this( + id = user.userId, + userName = user.userName, + activated = user.status == 1, + followingCount = user.followingCount, + fansCount = user.fansCount, + ) } diff --git a/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt deleted file mode 100644 index e528320c..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt +++ /dev/null @@ -1,50 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.data.mongodb.repository.Query -import plus.maa.backend.repository.entity.ArkLevel -import plus.maa.backend.repository.entity.ArkLevelSha - -/** - * @author john180 - */ -interface ArkLevelRepository : MongoRepository { - fun findAllShaBy(): List - - fun findAllByCatOne(catOne: String, pageable: Pageable): Page - - @Query( - """ - { - "${'$'}or": [ - {"levelId": ?0}, - {"stageId": ?0}, - {"catThree": ?0} - ] - } - - """, - ) - fun findByLevelIdFuzzy(levelId: String): List - - /** - * 用于前端查询 关卡名、关卡类型、关卡编号 - */ - @Query( - """ - { - "${'$'}or": [ - {"stageId": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, - {"catThree": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, - {"catTwo": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, - {"catOne": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, - {"name": {'${'$'}regex': ?0,'${'$'}options':'i' }} - ] - } - - """, - ) - fun queryLevelByKeyword(keyword: String): List -} diff --git a/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt deleted file mode 100644 index fd9c3531..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt +++ /dev/null @@ -1,37 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.stereotype.Repository -import plus.maa.backend.repository.entity.CommentsArea - -/** - * @author LoMu - * Date 2023-02-17 15:06 - */ -@Repository -interface CommentsAreaRepository : MongoRepository { - fun findByMainCommentId(commentsId: String): List - - fun findByCopilotIdAndDeleteAndMainCommentIdExists( - copilotId: Long, - delete: Boolean, - exists: Boolean, - pageable: Pageable, - ): Page - - fun findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists( - copilotId: Long, - uploaderId: String, - delete: Boolean, - exists: Boolean, - pageable: Pageable, - ): Page - - fun findByCopilotIdInAndDelete(copilotIds: Collection, delete: Boolean): List - - fun findByMainCommentIdIn(ids: List): List - - fun countByCopilotIdAndDelete(copilotId: Long, delete: Boolean): Long -} diff --git a/src/main/kotlin/plus/maa/backend/repository/CopilotRepo.kt b/src/main/kotlin/plus/maa/backend/repository/CopilotRepo.kt new file mode 100644 index 00000000..5a3a2858 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/CopilotRepo.kt @@ -0,0 +1,18 @@ +package plus.maa.backend.repository + +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.copilots + +@Repository +class CopilotRepo( + val database: Database, +) { + + fun getById(id: Long) = database.copilots.filter { it.copilotId eq id }.firstOrNull() + + fun getNotDeletedQuery() = database.copilots.filter { it.delete eq false } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt deleted file mode 100644 index ab86b059..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import plus.maa.backend.repository.entity.Copilot -import java.time.LocalDateTime - -/** - * @author LoMu - * Date 2022-12-27 10:28 - */ -interface CopilotRepository : MongoRepository { - fun findAllByDeleteIsFalse(pageable: Pageable): Page - - fun findByCopilotIdAndDeleteIsFalse(copilotId: Long): Copilot? - - fun findByCopilotIdInAndDeleteIsFalse(copilotIds: Collection): List - - fun findByCopilotId(copilotId: Long): Copilot? - - fun existsCopilotsByCopilotId(copilotId: Long): Boolean - - fun findAllByUploadTimeAfterOrDeleteTimeAfter(d1: LocalDateTime, d2: LocalDateTime): List -} diff --git a/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt deleted file mode 100644 index 5fe4a4d3..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt +++ /dev/null @@ -1,26 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.data.mongodb.repository.Query -import plus.maa.backend.repository.entity.CopilotSet - -/** - * @author dragove - * create on 2024-01-01 - */ -interface CopilotSetRepository : MongoRepository { - @Query( - """ - { - "${'$'}or": [ - {"name": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, - {"description": {'${'$'}regex': ?0,'${'$'}options':'i' }} - ] - } - - """, - ) - fun findByKeyword(keyword: String, pageable: Pageable): Page -} diff --git a/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt b/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt deleted file mode 100644 index ba48f978..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.stereotype.Repository -import plus.maa.backend.repository.entity.Rating - -/** - * @author lixuhuilll - * Date 2023-08-20 12:06 - */ -@Repository -interface RatingRepository : MongoRepository { - fun findByTypeAndKeyAndUserId(type: Rating.KeyType, key: String, userId: String): Rating? -} diff --git a/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt b/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt index ae1850f3..a0210554 100644 --- a/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt +++ b/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt @@ -28,7 +28,7 @@ private val log = KotlinLogging.logger { } */ @Component class RedisCache( - @Value("\${maa-copilot.cache.default-expire}") private val expire: Int, + @param:Value($$"${maa-copilot.cache.default-expire}") private val expire: Int, private val redisTemplate: StringRedisTemplate, ) { // 添加 JSR310 模块,以便顺利序列化 LocalDateTime 等类型 diff --git a/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt deleted file mode 100644 index 3b59b111..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.mongodb.repository.MongoRepository -import plus.maa.backend.repository.entity.UserFans - -interface UserFansRepository : MongoRepository { - fun findByUserId(userId: String): UserFans? -} diff --git a/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt deleted file mode 100644 index 26d8ef02..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.mongodb.repository.MongoRepository -import plus.maa.backend.repository.entity.UserFollowing - -interface UserFollowingRepository : MongoRepository { - fun findByUserId(userId: String): UserFollowing? -} diff --git a/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt deleted file mode 100644 index 684b999c..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt +++ /dev/null @@ -1,28 +0,0 @@ -package plus.maa.backend.repository - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.data.mongodb.repository.Query -import plus.maa.backend.controller.response.user.MaaUserInfo -import plus.maa.backend.repository.entity.MaaUser - -/** - * @author AnselYuki - */ -interface UserRepository : MongoRepository { - /** - * 根据邮箱(用户唯一登录凭据)查询 - * - * @param email 邮箱字段 - * @return 查询用户 - */ - fun findByEmail(email: String): MaaUser? - - fun findByUserId(userId: String): MaaUser? - - @Query("{ 'userName': { '\$regex': ?0, '\$options': 'i' }, 'status': 1 }") - fun searchUsers(userName: String, pageable: Pageable): Page - - fun existsByUserName(userName: String): Boolean -} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt index 7c2f3544..419a7806 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt @@ -1,8 +1,5 @@ package plus.maa.backend.repository.entity -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.index.Indexed -import org.springframework.data.mongodb.core.mapping.Document import java.time.LocalDateTime /** @@ -10,12 +7,9 @@ import java.time.LocalDateTime * * @author john180 */ -@Document("maa_level") data class ArkLevel( - @Id val id: String? = null, val levelId: String? = null, - @Indexed val stageId: String? = null, // 文件版本, 用于判断是否需要更新 val sha: String = "", diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelEntity.kt new file mode 100644 index 00000000..754cf0a6 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelEntity.kt @@ -0,0 +1,52 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.boolean +import org.ktorm.schema.datetime +import org.ktorm.schema.int +import org.ktorm.schema.varchar +import java.time.LocalDateTime + +interface ArkLevelEntity : Entity { + var id: String + var levelId: String? + var stageId: String? + var sha: String + var catOne: String? + var catTwo: String? + var catThree: String? + var name: String? + var width: Int + var height: Int + var isOpen: Boolean? + var closeTime: LocalDateTime? + + companion object : Entity.Factory() { + val EMPTY: ArkLevelEntity get() = ArkLevelEntity { + this.id = "" + this.sha = "" + this.width = 0 + this.height = 0 + } + } +} + +object ArkLevels : Table("ark_level") { + val id = varchar("id").primaryKey().bindTo { it.id } + val levelId = varchar("level_id").bindTo { it.levelId } + val stageId = varchar("stage_id").bindTo { it.stageId } + val sha = varchar("sha").bindTo { it.sha } + val catOne = varchar("cat_one").bindTo { it.catOne } + val catTwo = varchar("cat_two").bindTo { it.catTwo } + val catThree = varchar("cat_three").bindTo { it.catThree } + val name = varchar("name").bindTo { it.name } + val width = int("width").bindTo { it.width } + val height = int("height").bindTo { it.height } + val isOpen = boolean("is_open").bindTo { it.isOpen } + val closeTime = datetime("close_time").bindTo { it.closeTime } +} + +val Database.arkLevels get() = sequenceOf(ArkLevels) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt deleted file mode 100644 index 81df48d8..00000000 --- a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.repository.entity - -/** - * @author john180 - */ -interface ArkLevelSha { - val sha: String -} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt index 2f8904fe..5e14f888 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt @@ -1,8 +1,5 @@ package plus.maa.backend.repository.entity -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.index.Indexed -import org.springframework.data.mongodb.core.mapping.Document import java.io.Serializable import java.time.LocalDateTime @@ -10,11 +7,8 @@ import java.time.LocalDateTime * @author LoMu * Date 2023-02-17 14:50 */ -@Document("maa_comments_area") class CommentsArea( - @Id var id: String? = null, - @Indexed val copilotId: Long, // 答复某个评论 val fromCommentId: String? = null, diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CommentsAreaEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsAreaEntity.kt new file mode 100644 index 00000000..26448a18 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsAreaEntity.kt @@ -0,0 +1,48 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.boolean +import org.ktorm.schema.datetime +import org.ktorm.schema.long +import org.ktorm.schema.text +import org.ktorm.schema.varchar +import java.time.LocalDateTime + +interface CommentsAreaEntity : Entity { + var id: String + var copilotId: Long + var fromCommentId: String? + var uploaderId: String + var message: String + var likeCount: Long + var dislikeCount: Long + var uploadTime: LocalDateTime + var topping: Boolean + var delete: Boolean + var deleteTime: LocalDateTime? + var mainCommentId: String? + var notification: Boolean + + companion object : Entity.Factory() +} + +object CommentsAreas : Table("comments_area") { + val id = varchar("id").primaryKey().bindTo { it.id } + val copilotId = long("copilot_id").bindTo { it.copilotId } + val fromCommentId = varchar("from_comment_id").bindTo { it.fromCommentId } + val uploaderId = varchar("uploader_id").bindTo { it.uploaderId } + val message = text("message").bindTo { it.message } + val likeCount = long("like_count").bindTo { it.likeCount } + val dislikeCount = long("dislike_count").bindTo { it.dislikeCount } + val uploadTime = datetime("upload_time").bindTo { it.uploadTime } + val topping = boolean("topping").bindTo { it.topping } + val delete = boolean("delete").bindTo { it.delete } + val deleteTime = datetime("delete_time").bindTo { it.deleteTime } + val mainCommentId = varchar("main_comment_id").bindTo { it.mainCommentId } + val notification = boolean("notification").bindTo { it.notification } +} + +val Database.commentsAreas get() = sequenceOf(CommentsAreas) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt b/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt index 6c78582f..ebb10330 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt @@ -3,10 +3,6 @@ package plus.maa.backend.repository.entity import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Transient -import org.springframework.data.mongodb.core.index.Indexed -import org.springframework.data.mongodb.core.mapping.Document import plus.maa.backend.service.model.CommentStatus import plus.maa.backend.service.model.CopilotSetStatus import java.io.Serializable @@ -17,20 +13,15 @@ import java.time.LocalDateTime * Date 2022-12-25 17:56 */ @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) -@Document("maa_copilot") class Copilot( - @Id var id: String? = null, // 自增数字ID - @Indexed(unique = true) var copilotId: Long? = null, // 关卡名 - @Indexed var stageName: String? = null, // 上传者id var uploaderId: String? = null, // 查看次数 - @Indexed var views: Long = 0L, // 评级 var ratingLevel: Int = 0, @@ -40,7 +31,6 @@ class Copilot( var dislikeCount: Long = 0, // 热度 - @Indexed var hotScore: Double = 0.0, // 难度 var difficulty: Int = 0, @@ -153,7 +143,6 @@ class Copilot( ) : Serializable companion object { - @Transient val META: CollectionMeta = CollectionMeta( { obj: Copilot -> obj.copilotId!! }, diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CopilotEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotEntity.kt new file mode 100644 index 00000000..e1cc6f2c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotEntity.kt @@ -0,0 +1,96 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.boolean +import org.ktorm.schema.datetime +import org.ktorm.schema.double +import org.ktorm.schema.enum +import org.ktorm.schema.int +import org.ktorm.schema.long +import org.ktorm.schema.text +import org.ktorm.schema.varchar +import plus.maa.backend.service.model.CommentStatus +import plus.maa.backend.service.model.CopilotSetStatus +import java.time.LocalDateTime + +interface CopilotEntity : Entity { + // 自增数字ID + var copilotId: Long + + // 关卡名 + var stageName: String + + // 上传者id + var uploaderId: String + + // 查看次数 + var views: Long + + // 评级 + var ratingLevel: Int + + // 评级比率 十分之一代表半星 + var ratingRatio: Double + var likeCount: Long + var dislikeCount: Long + + // 热度 + var hotScore: Double + + // 文档字段,用于搜索,提取到Copilot类型上 + var title: String + var details: String? + + // 首次上传时间 + var firstUploadTime: LocalDateTime + + // 更新时间 + var uploadTime: LocalDateTime + + // 原始数据 + var content: String + + /** + * 作业状态,后端默认设置为公开以兼容历史逻辑 + * [plus.maa.backend.service.model.CopilotSetStatus] + */ + var status: CopilotSetStatus + + /** + * 评论状态 + */ + var commentStatus: CommentStatus + + var delete: Boolean + var deleteTime: LocalDateTime? + var notification: Boolean + + companion object : Entity.Factory() +} + +object Copilots : Table("copilot") { + val copilotId = long("copilot_id").primaryKey().bindTo { it.copilotId } + val stageName = varchar("stage_name").bindTo { it.stageName } + val uploaderId = varchar("uploader_id").bindTo { it.uploaderId } + val views = long("views").bindTo { it.views } + val ratingLevel = int("rating_level").bindTo { it.ratingLevel } + val ratingRatio = double("rating_ratio").bindTo { it.ratingRatio } + val likeCount = long("like_count").bindTo { it.likeCount } + val dislikeCount = long("dislike_count").bindTo { it.dislikeCount } + val hotScore = double("hot_score").bindTo { it.hotScore } + val title = varchar("title").bindTo { it.title } + val details = text("details").bindTo { it.details } + val firstUploadTime = datetime("first_upload_time").bindTo { it.firstUploadTime } + val uploadTime = datetime("upload_time").bindTo { it.uploadTime } + val content = text("content").bindTo { it.content } + val status = enum("status").bindTo { it.status } + val commentStatus = enum("comment_status").bindTo { it.commentStatus } + val delete = boolean("delete").bindTo { it.delete } + val deleteTime = datetime("delete_time").bindTo { it.deleteTime } + val notification = boolean("notification").bindTo { it.notification } +} + +val Database.copilots get() = sequenceOf(Copilots) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt index dc1d49a4..2017dcc8 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt @@ -3,9 +3,6 @@ package plus.maa.backend.repository.entity import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Transient -import org.springframework.data.mongodb.core.mapping.Document import plus.maa.backend.common.model.CopilotSetType import plus.maa.backend.service.model.CopilotSetStatus import java.io.Serializable @@ -15,12 +12,11 @@ import java.time.LocalDateTime * 作业集数据 */ @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) -@Document("maa_copilot_set") data class CopilotSet( /** * 作业集id */ - @field:Id val id: Long = 0, + val id: Long = 0, /** * 作业集名称 */ @@ -56,7 +52,6 @@ data class CopilotSet( @field:JsonIgnore var deleteTime: LocalDateTime? = null, ) : Serializable, CopilotSetType { companion object { - @field:Transient val meta = CollectionMeta( { obj: CopilotSet -> obj.id }, "id", diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSetEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSetEntity.kt new file mode 100644 index 00000000..c2214d29 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSetEntity.kt @@ -0,0 +1,83 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.boolean +import org.ktorm.schema.datetime +import org.ktorm.schema.enum +import org.ktorm.schema.long +import org.ktorm.schema.text +import org.ktorm.schema.varchar +import org.springframework.util.Assert +import plus.maa.backend.service.model.CopilotSetStatus +import java.time.LocalDateTime + +interface CopilotSetEntity : Entity { + var id: Long + var name: String + var description: String + var copilotIds: String // JSON格式存储作业ID列表 + var creatorId: String + var createTime: LocalDateTime + var updateTime: LocalDateTime + var status: CopilotSetStatus + var delete: Boolean + + companion object : Entity.Factory() +} + +object CopilotSets : Table("copilot_set") { + val id = long("id").primaryKey().bindTo { it.id } + val name = varchar("name").bindTo { it.name } + val description = text("description").bindTo { it.description } + val copilotIds = text("copilot_ids").bindTo { it.copilotIds } + val creatorId = varchar("creator_id").bindTo { it.creatorId } + val createTime = datetime("create_time").bindTo { it.createTime } + val updateTime = datetime("update_time").bindTo { it.updateTime } + val status = enum("status").bindTo { it.status } + val delete = boolean("delete").bindTo { it.delete } +} + +val Database.copilotSets get() = sequenceOf(CopilotSets) + +// 扩展方法用于处理作业ID列表 +private val objectMapper = jacksonObjectMapper() + +/** + * 获取作业ID列表(从JSON字符串解析) + */ +val CopilotSetEntity.copilotIdsList: MutableList + get() = try { + if (copilotIds.isBlank()) { + mutableListOf() + } else { + objectMapper.readValue>(copilotIds) + } + } catch (e: Exception) { + mutableListOf() + } + +/** + * 设置作业ID列表(序列化为JSON字符串) + */ +fun CopilotSetEntity.setCopilotIdsList(ids: List) { + copilotIds = objectMapper.writeValueAsString(ids) +} + +/** + * 去重并检查作业ID列表,类似原有的distinctIdsAndCheck方法 + */ +fun CopilotSetEntity.distinctIdsAndCheck(): MutableList { + val currentIds = copilotIdsList + if (currentIds.isEmpty() || currentIds.size == 1) { + return currentIds + } + val distinctIds = currentIds.stream().distinct().toList() + Assert.state(distinctIds.size <= 1000, "作业集总作业数量不能超过1000条") + setCopilotIdsList(distinctIds) + return distinctIds.toMutableList() +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt index 1d8db785..144f7fc9 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt @@ -1,10 +1,6 @@ package plus.maa.backend.repository.entity import com.fasterxml.jackson.annotation.JsonInclude -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Transient -import org.springframework.data.mongodb.core.index.Indexed -import org.springframework.data.mongodb.core.mapping.Document import java.io.Serializable import java.time.Instant @@ -12,13 +8,9 @@ import java.time.Instant * @author AnselYuki */ @JsonInclude(JsonInclude.Include.NON_NULL) -@Document("maa_user") data class MaaUser( - @Id val userId: String? = null, - @Indexed var userName: String, - @Indexed(unique = true) val email: String, var password: String, var status: Int = 0, @@ -28,7 +20,6 @@ data class MaaUser( ) : Serializable { companion object { - @Transient val UNKNOWN: MaaUser = MaaUser( userId = "", userName = "未知用户:(", diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/OperatorEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/OperatorEntity.kt new file mode 100644 index 00000000..320fc454 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/OperatorEntity.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.long +import org.ktorm.schema.varchar + +interface OperatorEntity : Entity { + val id: Long + var copilot: CopilotEntity + var name: String + + companion object : Entity.Factory() +} + +object Operators : Table("copilot_operator") { + val id = long("id").primaryKey().bindTo { it.id } + val copilotId = long("copilot_id").references(Copilots) { it.copilot } + val name = varchar("name").bindTo { it.name } +} + +val Database.operators get() = this.sequenceOf(Operators) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt b/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt index c3a9fa6f..f6dc3cc7 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt @@ -1,9 +1,5 @@ package plus.maa.backend.repository.entity -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.index.CompoundIndex -import org.springframework.data.mongodb.core.index.CompoundIndexes -import org.springframework.data.mongodb.core.mapping.Document import plus.maa.backend.service.model.RatingType import java.time.LocalDateTime @@ -13,10 +9,7 @@ import java.time.LocalDateTime * Date 2023-08-20 11:20 * @author lixuhuilll */ -@Document(collection = "maa_rating") // 复合索引 -@CompoundIndexes(CompoundIndex(name = "idx_rating", def = "{'type': 1, 'key': 1, 'userId': 1}", unique = true)) data class Rating( - @Id val id: String? = null, /** * 评级的类型,如作业(copilot)、评论(comment) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/RatingEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/RatingEntity.kt new file mode 100644 index 00000000..dbff701f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/RatingEntity.kt @@ -0,0 +1,33 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.datetime +import org.ktorm.schema.enum +import org.ktorm.schema.varchar +import plus.maa.backend.service.model.RatingType +import java.time.LocalDateTime + +interface RatingEntity : Entity { + var id: String + var type: Rating.KeyType + var key: String + var userId: String + var rating: RatingType + var rateTime: LocalDateTime + + companion object : Entity.Factory() +} + +object Ratings : Table("rating") { + val id = varchar("id").primaryKey().bindTo { it.id } + val type = enum("type").bindTo { it.type } + val key = varchar("key").bindTo { it.key } + val userId = varchar("user_id").bindTo { it.userId } + val rating = enum("rating").bindTo { it.rating } + val rateTime = datetime("rate_time").bindTo { it.rateTime } +} + +val Database.ratings get() = sequenceOf(Ratings) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserEntity.kt new file mode 100644 index 00000000..0e3c36a9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserEntity.kt @@ -0,0 +1,43 @@ +package plus.maa.backend.repository.entity + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.int +import org.ktorm.schema.timestamp +import org.ktorm.schema.varchar +import java.time.Instant + +interface UserEntity : Entity { + var userId: String + var userName: String + var email: String + var password: String + var status: Int + var pwdUpdateTime: Instant + var followingCount: Int + var fansCount: Int + + companion object : Entity.Factory() { + val UNKNOWN = UserEntity { + userId = "" + userName = "未知用户" + email = "unknown@unkown.unkown" + password = "unknown" + } + } +} + +object Users : Table("user") { + val userId = varchar("user_id").primaryKey().bindTo { it.userId } + val userName = varchar("user_name").bindTo { it.userName } + val email = varchar("email").bindTo { it.email } + val password = varchar("password").bindTo { it.password } + val status = int("status").bindTo { it.status } + val pwdUpdateTime = timestamp("pwd_update_time").bindTo { it.pwdUpdateTime } + val followingCount = int("following_count").bindTo { it.followingCount } + val fansCount = int("fans_count").bindTo { it.fansCount } +} + +val Database.users get() = this.sequenceOf(Users) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt index deba11e4..6010e42e 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt @@ -1,12 +1,8 @@ package plus.maa.backend.repository.entity -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant -@Document("user_fans") data class UserFans( - @Id val id: String? = null, val userId: String, val fansList: MutableList = mutableListOf(), diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFansEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFansEntity.kt new file mode 100644 index 00000000..a62c05c1 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFansEntity.kt @@ -0,0 +1,54 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.datetime +import org.ktorm.schema.text +import org.ktorm.schema.varchar +import java.time.LocalDateTime + +interface UserFansEntity : Entity { + var id: String + var userId: String + var fansIds: String // JSON格式存储粉丝列表 + var updatedAt: LocalDateTime + + companion object : Entity.Factory() +} + +object UserFansTable : Table("user_fans") { + val id = varchar("id").primaryKey().bindTo { it.id } + val userId = varchar("user_id").bindTo { it.userId } + val fansIds = text("fans_ids").bindTo { it.fansIds } + val updatedAt = datetime("updated_at").bindTo { it.updatedAt } +} + +val Database.userFans get() = sequenceOf(UserFansTable) + +// 扩展方法用于处理粉丝列表 +private val objectMapper = jacksonObjectMapper() + +/** + * 获取粉丝列表(从JSON字符串解析) + */ +val UserFansEntity.fansList: MutableList + get() = try { + if (fansIds.isBlank()) { + mutableListOf() + } else { + objectMapper.readValue>(fansIds) + } + } catch (e: Exception) { + mutableListOf() + } + +/** + * 设置粉丝列表(序列化为JSON字符串) + */ +fun UserFansEntity.setFansList(list: List) { + fansIds = objectMapper.writeValueAsString(list) +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt index 3b32f202..5d56155d 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt @@ -1,12 +1,8 @@ package plus.maa.backend.repository.entity -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant -@Document("user_following") data class UserFollowing( - @Id val id: String? = null, val userId: String, val followList: MutableList = mutableListOf(), diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowingEntity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowingEntity.kt new file mode 100644 index 00000000..4f5e1d07 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowingEntity.kt @@ -0,0 +1,54 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.datetime +import org.ktorm.schema.text +import org.ktorm.schema.varchar +import java.time.LocalDateTime + +interface UserFollowingEntity : Entity { + var id: String + var userId: String + var followingIds: String // JSON格式存储关注列表 + var updatedAt: LocalDateTime + + companion object : Entity.Factory() +} + +object UserFollowings : Table("user_following") { + val id = varchar("id").primaryKey().bindTo { it.id } + val userId = varchar("user_id").bindTo { it.userId } + val followingIds = text("following_ids").bindTo { it.followingIds } + val updatedAt = datetime("updated_at").bindTo { it.updatedAt } +} + +val Database.userFollowings get() = sequenceOf(UserFollowings) + +// 扩展方法用于处理关注列表 +private val objectMapper = jacksonObjectMapper() + +/** + * 获取关注列表(从JSON字符串解析) + */ +val UserFollowingEntity.followList: MutableList + get() = try { + if (followingIds.isBlank()) { + mutableListOf() + } else { + objectMapper.readValue>(followingIds) + } + } catch (e: Exception) { + mutableListOf() + } + +/** + * 设置关注列表(序列化为JSON字符串) + */ +fun UserFollowingEntity.setFollowList(list: List) { + followingIds = objectMapper.writeValueAsString(list) +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/ArkLevelKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/ArkLevelKtormRepository.kt new file mode 100644 index 00000000..f38db747 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/ArkLevelKtormRepository.kt @@ -0,0 +1,116 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.dsl.from +import org.ktorm.dsl.inList +import org.ktorm.dsl.like +import org.ktorm.dsl.map +import org.ktorm.dsl.or +import org.ktorm.dsl.select +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.count +import org.ktorm.entity.drop +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.ktorm.entity.take +import org.ktorm.entity.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.ArkLevelEntity +import plus.maa.backend.repository.entity.ArkLevels + +@Repository +class ArkLevelKtormRepository( + database: Database, +) : KtormRepository(database, ArkLevels) { + + fun findByStageId(stageId: String): ArkLevelEntity? { + return entities.firstOrNull { it.stageId eq stageId } + } + + fun findAllByStageIds(stageIds: List): List { + return entities.filter { it.stageId inList stageIds }.toList() + } + + fun findByLevelId(levelId: String): ArkLevelEntity? { + return entities.firstOrNull { it.levelId eq levelId } + } + + fun findAllOpenLevels(): List { + return entities.filter { it.isOpen eq true }.toList() + } + + fun insertEntity(entity: ArkLevelEntity): ArkLevelEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: ArkLevelEntity): ArkLevelEntity { + entity.flushChanges() + return entity + } + + override fun findById(id: Any): ArkLevelEntity? { + return entities.firstOrNull { it.id eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString() } + } + + override fun getIdColumn(entity: ArkLevelEntity): Any = entity.id + + override fun isNewEntity(entity: ArkLevelEntity): Boolean { + return entity.id.isBlank() || !existsById(entity.id) + } + + override fun save(entity: ArkLevelEntity): ArkLevelEntity { + return if (isNewEntity(entity)) { + insertEntity(entity) + } else { + updateEntity(entity) + } + } + + fun findByLevelIdFuzzy(levelId: String): List { + return entities.filter { it.levelId like "%$levelId%" }.toList() + } + + fun queryLevelByKeyword(keyword: String): List { + return entities.filter { + it.name like "%$keyword%" or + (it.levelId like "%$keyword%") or + (it.stageId like "%$keyword%") + }.toList() + } + + fun findAllShaBy(): List { + return database.from(ArkLevels) + .select(ArkLevels.sha) + .map { row -> ShaProjection(row[ArkLevels.sha]!!) } + } + + fun findAllByCatOne(catOne: String, pageable: Pageable): Page { + val total = entities.filter { it.catOne eq catOne }.count() + val items = entities.filter { it.catOne eq catOne } + .drop(pageable.offset.toInt()) + .take(pageable.pageSize) + .toList() + return PageImpl(items, pageable, total.toLong()) + } + + fun saveAll(entities: List) { + entities.forEach { save(it) } + } + + data class ShaProjection(val sha: String) +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/CommentsAreaKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/CommentsAreaKtormRepository.kt new file mode 100644 index 00000000..136287e0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/CommentsAreaKtormRepository.kt @@ -0,0 +1,106 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.eq +import org.ktorm.dsl.inList +import org.ktorm.dsl.isNotNull +import org.ktorm.dsl.isNull +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.count +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.ktorm.entity.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import plus.maa.backend.common.extensions.paginate +import plus.maa.backend.repository.entity.CommentsAreaEntity +import plus.maa.backend.repository.entity.CommentsAreas + +@Repository +class CommentsAreaKtormRepository( + database: Database, +) : KtormRepository(database, CommentsAreas) { + + fun findByMainCommentId(commentsId: String): List { + return entities.filter { it.mainCommentId eq commentsId }.toList() + } + + fun findByCopilotIdAndDeleteAndMainCommentIdExists( + copilotId: Long, + delete: Boolean, + exists: Boolean, + pageable: Pageable, + ): Page { + val sequence = entities.filter { + it.copilotId eq copilotId and + (it.delete eq delete) and + if (exists) it.mainCommentId.isNotNull() else it.mainCommentId.isNull() + } + + return sequence.paginate(pageable) + } + + fun findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists( + copilotId: Long, + uploaderId: String, + delete: Boolean, + exists: Boolean, + ): Page { + val sequence = entities.filter { + it.copilotId eq copilotId and + (it.uploaderId eq uploaderId) and + (it.delete eq delete) and + if (exists) it.mainCommentId.isNotNull() else it.mainCommentId.isNull() + } + + return sequence.paginate(Pageable.unpaged()) + } + + fun findByCopilotIdInAndDelete(copilotIds: Collection, delete: Boolean): List { + return entities.filter { + it.copilotId inList copilotIds and (it.delete eq delete) + }.toList() + } + + fun findByMainCommentIdIn(ids: List): List { + return entities.filter { it.mainCommentId inList ids }.toList() + } + + fun countByCopilotIdAndDelete(copilotId: Long, delete: Boolean): Long { + return entities.filter { + it.copilotId eq copilotId and (it.delete eq delete) + }.count().toLong() + } + + override fun findById(id: Any): CommentsAreaEntity? { + return entities.firstOrNull { it.id eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString() } + } + + override fun getIdColumn(entity: CommentsAreaEntity): Any = entity.id + + override fun isNewEntity(entity: CommentsAreaEntity): Boolean { + return entity.id.isBlank() || !existsById(entity.id) + } + + fun insertEntity(entity: CommentsAreaEntity): CommentsAreaEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: CommentsAreaEntity): CommentsAreaEntity { + entity.flushChanges() + return entity + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotKtormRepository.kt new file mode 100644 index 00000000..f63bd2b3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotKtormRepository.kt @@ -0,0 +1,104 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.eq +import org.ktorm.dsl.gte +import org.ktorm.dsl.inList +import org.ktorm.dsl.notEq +import org.ktorm.dsl.or +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.ktorm.entity.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import plus.maa.backend.common.extensions.paginate +import plus.maa.backend.repository.entity.CopilotEntity +import plus.maa.backend.repository.entity.Copilots +import java.time.LocalDateTime +import java.util.stream.Stream + +@Repository +class CopilotKtormRepository( + database: Database, +) : KtormRepository(database, Copilots) { + + fun findAllByDeleteIsFalse(pageable: Pageable): Page { + val sequence = entities.filter { it.delete eq false } + return sequence.paginate(pageable) + } + + fun findByCopilotIdAndDeleteIsFalse(copilotId: Long): CopilotEntity? { + return entities.firstOrNull { + it.copilotId eq copilotId and (it.delete eq false) + } + } + + fun findByCopilotIdInAndDeleteIsFalse(copilotIds: Collection): List { + return entities.filter { + it.copilotId inList copilotIds and (it.delete eq false) + }.toList() + } + + fun findByCopilotId(copilotId: Long): CopilotEntity? { + return entities.firstOrNull { it.copilotId eq copilotId } + } + + fun existsCopilotsByCopilotId(copilotId: Long): Boolean { + return entities.any { it.copilotId eq copilotId } + } + + fun existsByCopilotId(copilotId: Long): Boolean { + return entities.any { it.copilotId eq copilotId } + } + + fun findByContentIsNotNull(): Stream { + return entities.filter { + it.content notEq "" + }.toList().stream() + } + + fun insert(copilot: CopilotEntity): CopilotEntity { + entities.add(copilot) + return copilot + } + + fun insertEntity(copilot: CopilotEntity): CopilotEntity { + entities.add(copilot) + return copilot + } + + fun updateEntity(copilot: CopilotEntity): CopilotEntity { + copilot.flushChanges() + return copilot + } + + override fun findById(id: Any): CopilotEntity? { + return entities.firstOrNull { it.copilotId eq id.toString().toLong() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.copilotId eq id.toString().toLong() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.copilotId eq id.toString().toLong() } + } + + override fun getIdColumn(entity: CopilotEntity): Any = entity.copilotId + + override fun isNewEntity(entity: CopilotEntity): Boolean { + // 如果copilotId为0或在数据库中不存在,则认为是新实体 + return entity.copilotId == 0L || !existsById(entity.copilotId) + } + + fun findAllByUploadTimeAfterOrDeleteTimeAfter(uploadTimeAfter: LocalDateTime, deleteTimeAfter: LocalDateTime): List { + return entities.filter { + (it.uploadTime gte uploadTimeAfter) or (it.deleteTime gte deleteTimeAfter) + }.toList() + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotSetKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotSetKtormRepository.kt new file mode 100644 index 00000000..3afe8181 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/CopilotSetKtormRepository.kt @@ -0,0 +1,110 @@ +package plus.maa.backend.repository.ktorm + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.eq +import org.ktorm.dsl.like +import org.ktorm.dsl.or +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import plus.maa.backend.common.extensions.paginate +import plus.maa.backend.repository.entity.CopilotSetEntity +import plus.maa.backend.repository.entity.CopilotSets + +@Repository +class CopilotSetKtormRepository( + database: Database, + private val objectMapper: ObjectMapper, +) : KtormRepository(database, CopilotSets) { + + fun findByKeyword(keyword: String, pageable: Pageable): Page { + val sequence = entities.filter { + (it.name like "%$keyword%") or (it.description like "%$keyword%") + } + return sequence.paginate(pageable) + } + + fun findByIdAndDeleteIsFalse(id: Long): CopilotSetEntity? { + return entities.firstOrNull { + (it.id eq id) and (it.delete eq false) + } + } + + fun findAllByDeleteIsFalse(pageable: Pageable): Page { + val sequence = entities.filter { it.delete eq false } + return sequence.paginate(pageable) + } + + fun findByCreatorIdAndDeleteIsFalse(creatorId: String, pageable: Pageable): Page { + val sequence = entities.filter { + (it.creatorId eq creatorId) and (it.delete eq false) + } + return sequence.paginate(pageable) + } + + /** + * 将JSON格式的copilotIds转换为List + */ + fun getCopilotIdsList(entity: CopilotSetEntity): MutableList { + return try { + objectMapper.readValue>(entity.copilotIds) + } catch (e: Exception) { + mutableListOf() + } + } + + /** + * 将List转换为JSON格式保存 + */ + fun setCopilotIdsList(entity: CopilotSetEntity, copilotIds: List) { + entity.copilotIds = objectMapper.writeValueAsString(copilotIds) + } + + override fun findById(id: Any): CopilotSetEntity? { + return entities.firstOrNull { it.id eq id.toString().toLong() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString().toLong() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString().toLong() } + } + + override fun getIdColumn(entity: CopilotSetEntity): Any = entity.id + + override fun isNewEntity(entity: CopilotSetEntity): Boolean { + return entity.id == 0L || !existsById(entity.id) + } + + fun insertEntity(entity: CopilotSetEntity): CopilotSetEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: CopilotSetEntity): CopilotSetEntity { + entity.flushChanges() + return entity + } + + fun findByIdAsOptional(id: Long): java.util.Optional { + return findById(id)?.let { java.util.Optional.of(it) } ?: java.util.Optional.empty() + } + + override fun save(entity: CopilotSetEntity): CopilotSetEntity { + return if (isNewEntity(entity)) { + insertEntity(entity) + } else { + updateEntity(entity) + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/KtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/KtormRepository.kt new file mode 100644 index 00000000..ea6a3bee --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/KtormRepository.kt @@ -0,0 +1,44 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.add +import org.ktorm.entity.count +import org.ktorm.entity.sequenceOf +import org.ktorm.entity.toList +import org.ktorm.entity.update +import org.ktorm.schema.Table + +abstract class KtormRepository, T : Table>( + protected val database: Database, + protected val table: T, +) { + protected val entities get() = database.sequenceOf(table) + + abstract fun findById(id: Any): E? + + open fun findAll(): List { + return entities.toList() + } + + open fun save(entity: E): E { + return if (isNewEntity(entity)) { + entities.add(entity) + entity + } else { + entities.update(entity) + entity + } + } + + abstract fun deleteById(id: Any): Boolean + + open fun count(): Long { + return entities.count().toLong() + } + + abstract fun existsById(id: Any): Boolean + + protected abstract fun getIdColumn(entity: E): Any + protected abstract fun isNewEntity(entity: E): Boolean +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/RatingKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/RatingKtormRepository.kt new file mode 100644 index 00000000..8dced3bd --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/RatingKtormRepository.kt @@ -0,0 +1,95 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.count +import org.ktorm.dsl.eq +import org.ktorm.dsl.from +import org.ktorm.dsl.greater +import org.ktorm.dsl.groupBy +import org.ktorm.dsl.map +import org.ktorm.dsl.select +import org.ktorm.dsl.where +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.repository.entity.RatingEntity +import plus.maa.backend.repository.entity.Ratings +import plus.maa.backend.service.model.RatingCount +import java.time.LocalDateTime + +@Repository +class RatingKtormRepository( + database: Database, +) : KtormRepository(database, Ratings) { + + fun findByTypeAndKeyAndUserId(type: Rating.KeyType, key: String, userId: String): RatingEntity? { + return entities.firstOrNull { + (it.type eq type) and (it.key eq key) and (it.userId eq userId) + } + } + + /** + * 获取指定时间后的评分统计 + */ + fun getRatingCountAfter(after: LocalDateTime): List { + return database + .from(Ratings) + .select(Ratings.key, count(Ratings.id)) + .where { Ratings.rateTime greater after } + .groupBy(Ratings.key) + .map { row -> + RatingCount( + key = row[Ratings.key]!!, + count = row.getLong(2), // second column + ) + } + } + + /** + * 获取所有评分统计 + */ + fun getAllRatingCount(): List { + return database + .from(Ratings) + .select(Ratings.key, count(Ratings.id)) + .groupBy(Ratings.key) + .map { row -> + RatingCount( + key = row[Ratings.key]!!, + count = row.getLong(2), // second column + ) + } + } + + override fun findById(id: Any): RatingEntity? { + return entities.firstOrNull { it.id eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString() } + } + + override fun getIdColumn(entity: RatingEntity): Any = entity.id + + override fun isNewEntity(entity: RatingEntity): Boolean { + return entity.id.isBlank() || !existsById(entity.id) + } + + fun insertEntity(entity: RatingEntity): RatingEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: RatingEntity): RatingEntity { + entity.flushChanges() + return entity + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFansKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFansKtormRepository.kt new file mode 100644 index 00000000..ca8a31e9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFansKtormRepository.kt @@ -0,0 +1,60 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.UserFansEntity +import plus.maa.backend.repository.entity.UserFansTable + +@Repository +class UserFansKtormRepository( + database: Database, +) : KtormRepository(database, UserFansTable) { + + /** + * 获取用户粉丝列表实体 + */ + fun findByUserId(userId: String): UserFansEntity? { + return entities.firstOrNull { it.userId eq userId } + } + + fun insertEntity(entity: UserFansEntity): UserFansEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: UserFansEntity): UserFansEntity { + entity.flushChanges() + return entity + } + + override fun findById(id: Any): UserFansEntity? { + return entities.firstOrNull { it.id eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString() } + } + + override fun getIdColumn(entity: UserFansEntity): Any = entity.id + + override fun isNewEntity(entity: UserFansEntity): Boolean { + return entity.id.isBlank() || !existsById(entity.id) + } + + override fun save(entity: UserFansEntity): UserFansEntity { + return if (isNewEntity(entity)) { + insertEntity(entity) + } else { + updateEntity(entity) + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFollowingKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFollowingKtormRepository.kt new file mode 100644 index 00000000..ffbd45e3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserFollowingKtormRepository.kt @@ -0,0 +1,68 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.UserFollowingEntity +import plus.maa.backend.repository.entity.UserFollowings +import plus.maa.backend.repository.entity.followList + +@Repository +class UserFollowingKtormRepository( + database: Database, +) : KtormRepository(database, UserFollowings) { + + /** + * 获取用户关注列表实体 + */ + fun findByUserId(userId: String): UserFollowingEntity? { + return entities.firstOrNull { it.userId eq userId } + } + + /** + * 获取用户关注的ID列表 + */ + fun getFollowingIds(userId: String): List { + return findByUserId(userId)?.followList ?: emptyList() + } + + fun insertEntity(entity: UserFollowingEntity): UserFollowingEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: UserFollowingEntity): UserFollowingEntity { + entity.flushChanges() + return entity + } + + override fun findById(id: Any): UserFollowingEntity? { + return entities.firstOrNull { it.id eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.id eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.id eq id.toString() } + } + + override fun getIdColumn(entity: UserFollowingEntity): Any = entity.id + + override fun isNewEntity(entity: UserFollowingEntity): Boolean { + return entity.id.isBlank() || !existsById(entity.id) + } + + override fun save(entity: UserFollowingEntity): UserFollowingEntity { + return if (isNewEntity(entity)) { + insertEntity(entity) + } else { + updateEntity(entity) + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ktorm/UserKtormRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserKtormRepository.kt new file mode 100644 index 00000000..fc3f01ac --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ktorm/UserKtormRepository.kt @@ -0,0 +1,124 @@ +package plus.maa.backend.repository.ktorm + +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.eq +import org.ktorm.dsl.inList +import org.ktorm.dsl.like +import org.ktorm.entity.add +import org.ktorm.entity.any +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.removeIf +import org.ktorm.entity.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import plus.maa.backend.common.extensions.paginate +import plus.maa.backend.controller.response.user.MaaUserInfo +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserEntity +import plus.maa.backend.repository.entity.Users +import java.util.stream.Stream + +@Repository +class UserKtormRepository( + database: Database, +) : KtormRepository(database, Users) { + + fun findByEmail(email: String): UserEntity? { + return entities.firstOrNull { it.email eq email } + } + + fun findByUserId(userId: String): UserEntity? { + return entities.firstOrNull { it.userId eq userId } + } + + fun searchUsers(userName: String, pageable: Pageable): Page { + val sequence = entities.filter { + it.userName like "%$userName%" and (it.status eq 1) + } + + val page = sequence.paginate(pageable) + + // Convert UserEntity to MaaUserInfo + val userInfos = page.content.map { user -> + MaaUserInfo( + id = user.userId, + userName = user.userName, + ) + } + + return PageImpl(userInfos, pageable, page.totalElements) + } + + fun existsByUserName(userName: String): Boolean { + return entities.any { it.userName eq userName } + } + + fun findAllBy(): Stream { + return entities.toList().stream() + } + + fun findAllById(ids: Iterable): List { + val idList = ids.toList() + if (idList.isEmpty()) { + return mutableListOf() + } + return entities.filter { it.userId inList idList }.toList() + } + + override fun getIdColumn(entity: UserEntity): Any = entity.userId + + override fun findById(id: Any): UserEntity? { + return entities.firstOrNull { it.userId eq id.toString() } + } + + override fun deleteById(id: Any): Boolean { + return entities.removeIf { it.userId eq id.toString() } > 0 + } + + override fun existsById(id: Any): Boolean { + return entities.any { it.userId eq id.toString() } + } + + override fun isNewEntity(entity: UserEntity): Boolean { + // 如果userId为空或在数据库中不存在,则认为是新实体 + return entity.userId.isBlank() || !existsById(entity.userId) + } + + /** + * 从MaaUser创建UserEntity + */ + fun createFromMaaUser(maaUser: MaaUser): UserEntity { + return UserEntity { + this.userId = maaUser.userId ?: "" + this.userName = maaUser.userName + this.email = maaUser.email + this.password = maaUser.password + this.status = maaUser.status + this.pwdUpdateTime = maaUser.pwdUpdateTime + this.followingCount = maaUser.followingCount + this.fansCount = maaUser.fansCount + } + } + + fun insertEntity(entity: UserEntity): UserEntity { + entities.add(entity) + return entity + } + + fun updateEntity(entity: UserEntity): UserEntity { + entity.flushChanges() + return entity + } + + override fun save(entity: UserEntity): UserEntity { + return if (isNewEntity(entity)) { + insertEntity(entity) + } else { + updateEntity(entity) + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt index c6c3fd44..9cf6b1b7 100644 --- a/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt @@ -2,14 +2,7 @@ package plus.maa.backend.service import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable -import org.springframework.data.domain.Sort -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.query.exists -import org.springframework.data.mongodb.core.query.isEqualTo -import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service -import plus.maa.backend.common.extensions.addAndCriteria -import plus.maa.backend.common.extensions.findPage import plus.maa.backend.common.extensions.requireNotNull import plus.maa.backend.controller.request.comments.CommentsAddDTO import plus.maa.backend.controller.request.comments.CommentsQueriesDTO @@ -19,11 +12,11 @@ import plus.maa.backend.controller.response.MaaResultException import plus.maa.backend.controller.response.comments.CommentsAreaInfo import plus.maa.backend.controller.response.comments.CommentsInfo import plus.maa.backend.controller.response.comments.SubCommentsInfo -import plus.maa.backend.repository.CommentsAreaRepository -import plus.maa.backend.repository.CopilotRepository -import plus.maa.backend.repository.entity.CommentsArea -import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.CommentsAreaEntity +import plus.maa.backend.repository.entity.CopilotEntity import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.ktorm.CommentsAreaKtormRepository +import plus.maa.backend.repository.ktorm.CopilotKtormRepository import plus.maa.backend.service.model.CommentStatus import plus.maa.backend.service.model.RatingType import plus.maa.backend.service.sensitiveword.SensitiveWordService @@ -36,13 +29,12 @@ import plus.maa.backend.cache.InternalComposeCache as Cache */ @Service class CommentsAreaService( - private val commentsAreaRepository: CommentsAreaRepository, + private val commentsAreaKtormRepository: CommentsAreaKtormRepository, private val ratingService: RatingService, - private val copilotRepository: CopilotRepository, + private val copilotKtormRepository: CopilotKtormRepository, private val userService: UserService, private val emailService: EmailService, private val sensitiveWordService: SensitiveWordService, - private val mongoTemplate: MongoTemplate, ) { /** * 评论 @@ -54,7 +46,7 @@ class CommentsAreaService( fun addComments(userId: String, commentsAddDTO: CommentsAddDTO) { sensitiveWordService.validate(commentsAddDTO.message) val copilotId = commentsAddDTO.copilotId - val copilot = copilotRepository.findByCopilotId(copilotId).requireNotNull { "作业不存在" } + val copilot = copilotKtormRepository.findByCopilotId(copilotId).requireNotNull { "作业不存在" } if (copilot.commentStatus == CommentStatus.DISABLED && userId != copilot.uploaderId) { throw MaaResultException("评论区已被禁用") @@ -70,28 +62,35 @@ class CommentsAreaService( notifyRelatedUser(userId, commentsAddDTO.message, copilot, parentComment) - val comment = CommentsArea( - copilotId = copilotId, - uploaderId = userId, - fromCommentId = parentComment?.id, - mainCommentId = parentComment?.run { mainCommentId ?: id }, - message = commentsAddDTO.message, - notification = commentsAddDTO.notification, - ) - commentsAreaRepository.insert(comment) + val comment = CommentsAreaEntity { + this.id = "" + this.copilotId = copilotId + this.uploaderId = userId + this.fromCommentId = parentComment?.id + this.mainCommentId = parentComment?.run { mainCommentId ?: id } + this.message = commentsAddDTO.message + this.notification = commentsAddDTO.notification + this.uploadTime = LocalDateTime.now() + this.likeCount = 0L + this.dislikeCount = 0L + this.topping = false + this.delete = false + this.deleteTime = null + } + commentsAreaKtormRepository.insertEntity(comment) Cache.invalidateCommentCountById(copilotId) } - private fun notifyRelatedUser(replierId: String, message: String, copilot: Copilot, parentComment: CommentsArea?) { + private fun notifyRelatedUser(replierId: String, message: String, copilot: CopilotEntity, parentComment: CommentsAreaEntity?) { if (parentComment?.notification == false) return val receiverId = parentComment?.uploaderId ?: copilot.uploaderId - if (receiverId == null || receiverId == replierId) return + if (receiverId == replierId) return val userMap = userService.findByUsersId(listOf(receiverId, replierId)) val receiver = userMap[receiverId] ?: return val replier = userMap.getOrDefault(replierId) - val targetMsg = parentComment?.message ?: copilot.doc?.title ?: "" + val targetMsg = parentComment?.message ?: copilot.title emailService.sendCommentNotification( receiver.email, receiver.userName, @@ -105,21 +104,22 @@ class CommentsAreaService( fun deleteComments(userId: String, commentsId: String) { val commentsArea = requireCommentsAreaById(commentsId) // 允许作者删除评论 - val copilot = copilotRepository.findByCopilotId(commentsArea.copilotId) + val copilot = copilotKtormRepository.findByCopilotId(commentsArea.copilotId) require(userId == copilot?.uploaderId || userId == commentsArea.uploaderId) { "您无法删除不属于您的评论" } val now = LocalDateTime.now() commentsArea.delete = true commentsArea.deleteTime = now - val comments = mutableListOf(commentsArea) // 删除所有回复 if (commentsArea.mainCommentId.isNullOrBlank()) { - comments += commentsAreaRepository.findByMainCommentId(commentsId).onEach { ca -> + val subComments = commentsAreaKtormRepository.findByMainCommentId(commentsId) + subComments.forEach { ca -> ca.deleteTime = now ca.delete = true + commentsAreaKtormRepository.updateEntity(ca) } } - commentsAreaRepository.saveAll(comments) + commentsAreaKtormRepository.updateEntity(commentsArea) Cache.invalidateCommentCountById(commentsArea.copilotId) } @@ -145,7 +145,7 @@ class CommentsAreaService( commentsArea.likeCount = (commentsArea.likeCount + likeCountChange).coerceAtLeast(0) commentsArea.dislikeCount = (commentsArea.dislikeCount + dislikeCountChange).coerceAtLeast(0) - commentsAreaRepository.save(commentsArea) + commentsAreaKtormRepository.updateEntity(commentsArea) } /** @@ -157,11 +157,11 @@ class CommentsAreaService( fun topping(userId: String, commentsToppingDTO: CommentsToppingDTO) { val commentsArea = requireCommentsAreaById(commentsToppingDTO.commentId) // 只允许作者置顶评论 - val copilot = copilotRepository.findByCopilotId(commentsArea.copilotId) + val copilot = copilotKtormRepository.findByCopilotId(commentsArea.copilotId) require(userId == copilot?.uploaderId) { "只有作者才能置顶评论" } commentsArea.topping = commentsToppingDTO.topping - commentsAreaRepository.save(commentsArea) + commentsAreaKtormRepository.updateEntity(commentsArea) } /** @@ -171,45 +171,46 @@ class CommentsAreaService( * @return CommentsAreaInfo */ fun queriesCommentsArea(request: CommentsQueriesDTO): CommentsAreaInfo { - val toppingOrder = Sort.Order.desc("topping") - val sortOrder = Sort.Order( - if (request.desc) Sort.Direction.DESC else Sort.Direction.ASC, - when (request.orderBy) { - "hot" -> "likeCount" - "id" -> "uploadTime" - else -> request.orderBy ?: "likeCount" - }, - ) val page = (request.page - 1).coerceAtLeast(0) val limit = if (request.limit > 0) request.limit else 10 - val pageable: Pageable = PageRequest.of(page, limit, Sort.by(toppingOrder, sortOrder)) + val pageable: Pageable = PageRequest.of(page, limit) - // 主评论 - val mainCommentsPage = mongoTemplate.findPage(pageable) { - if (!request.justSeeId.isNullOrBlank()) { - addCriteria(CommentsArea::id isEqualTo request.justSeeId) + // 主评论 - 使用Ktorm查询 + val mainCommentsPage = if (!request.justSeeId.isNullOrBlank()) { + // 如果指定了评论ID,直接查询该评论 + val comment = commentsAreaKtormRepository.findById(request.justSeeId) + if (comment != null && !comment.delete && comment.copilotId == request.copilotId && comment.mainCommentId.isNullOrBlank()) { + org.springframework.data.domain.PageImpl(listOf(comment), pageable, 1) + } else { + org.springframework.data.domain.PageImpl(emptyList(), pageable, 0) } - addAndCriteria( - CommentsArea::copilotId isEqualTo request.copilotId, - CommentsArea::delete isEqualTo false, - CommentsArea::mainCommentId exists false, + } else { + commentsAreaKtormRepository.findByCopilotIdAndDeleteAndMainCommentIdExists( + request.copilotId, + delete = false, + exists = false, + pageable = pageable, ) } - val mainCommentIds = mainCommentsPage.map(CommentsArea::id).filterNotNull() + val mainCommentIds = mainCommentsPage.content.mapNotNull { it.id } // 获取子评论 - val subCommentsList = commentsAreaRepository.findByMainCommentIdIn(mainCommentIds).onEach { - // 将已删除评论内容替换为空 - if (it.delete) it.message = "" + val subCommentsList = if (mainCommentIds.isNotEmpty()) { + commentsAreaKtormRepository.findByMainCommentIdIn(mainCommentIds).onEach { + // 将已删除评论内容替换为空 + if (it.delete) it.message = "" + } + } else { + emptyList() } // 获取所有评论用户 - val allUserIds = (mainCommentsPage + subCommentsList).map(CommentsArea::uploaderId).distinct() + val allUserIds = (mainCommentsPage.content + subCommentsList).map { it.uploaderId }.distinct() val users = userService.findByUsersId(allUserIds) - val subCommentGroups = subCommentsList.groupBy(CommentsArea::mainCommentId) + val subCommentGroups = subCommentsList.groupBy { it.mainCommentId } // 转换主评论数据并填充用户名 - val commentsInfos = mainCommentsPage.toList().map { mainComment -> + val commentsInfos = mainCommentsPage.content.map { mainComment -> val subCommentsInfos = (subCommentGroups[mainComment.id] ?: emptyList()).map { c -> buildSubCommentsInfo(c, users.getOrDefault(c.uploaderId)) } @@ -227,8 +228,8 @@ class CommentsAreaService( /** * 转换子评论数据并填充用户名 */ - private fun buildSubCommentsInfo(c: CommentsArea, user: MaaUser) = SubCommentsInfo( - commentId = c.id!!, + private fun buildSubCommentsInfo(c: CommentsAreaEntity, user: MaaUser) = SubCommentsInfo( + commentId = c.id, uploader = user.userName, uploaderId = c.uploaderId, message = c.message, @@ -240,8 +241,8 @@ class CommentsAreaService( deleted = c.delete, ) - private fun buildMainCommentsInfo(c: CommentsArea, user: MaaUser, subList: List) = CommentsInfo( - commentId = c.id!!, + private fun buildMainCommentsInfo(c: CommentsAreaEntity, user: MaaUser, subList: List) = CommentsInfo( + commentId = c.id, uploader = user.userName, uploaderId = c.uploaderId, message = c.message, @@ -256,9 +257,9 @@ class CommentsAreaService( val commentsArea = requireCommentsAreaById(id) require(userId == commentsArea.uploaderId) { "您没有权限修改" } commentsArea.notification = status - commentsAreaRepository.save(commentsArea) + commentsAreaKtormRepository.updateEntity(commentsArea) } - private fun requireCommentsAreaById(commentsId: String, lazyMessage: () -> Any = { "评论不存在" }): CommentsArea = - commentsAreaRepository.findByIdOrNull(commentsId)?.takeIf { !it.delete }.requireNotNull(lazyMessage) + private fun requireCommentsAreaById(commentsId: String, lazyMessage: () -> Any = { "评论不存在" }): CommentsAreaEntity = + commentsAreaKtormRepository.findById(commentsId)?.takeIf { !it.delete }.requireNotNull(lazyMessage) } diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt index 5fbe8e50..8ca2a5b2 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt @@ -2,39 +2,55 @@ package plus.maa.backend.service import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Pageable -import org.springframework.data.domain.Sort -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.mongodb.core.query.Update -import org.springframework.data.mongodb.core.query.inValues +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.asc +import org.ktorm.dsl.desc +import org.ktorm.dsl.eq +import org.ktorm.dsl.from +import org.ktorm.dsl.inList +import org.ktorm.dsl.like +import org.ktorm.dsl.notInList +import org.ktorm.dsl.select +import org.ktorm.dsl.where +import org.ktorm.entity.drop +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.forEach +import org.ktorm.entity.sortedBy +import org.ktorm.entity.take +import org.ktorm.entity.toList +import org.ktorm.expression.ArgumentExpression +import org.ktorm.schema.BooleanSqlType +import org.ktorm.schema.ColumnDeclaring import org.springframework.stereotype.Service import plus.maa.backend.cache.transfer.CopilotInnerCacheInfo import plus.maa.backend.common.extensions.blankAsNull import plus.maa.backend.common.extensions.removeQuotes import plus.maa.backend.common.extensions.requireNotNull import plus.maa.backend.common.utils.IdComponent -import plus.maa.backend.common.utils.converter.CopilotConverter import plus.maa.backend.config.external.MaaCopilotProperties import plus.maa.backend.controller.request.copilot.CopilotCUDRequest import plus.maa.backend.controller.request.copilot.CopilotDTO import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest import plus.maa.backend.controller.request.copilot.CopilotRatingReq import plus.maa.backend.controller.response.MaaResultException -import plus.maa.backend.controller.response.copilot.ArkLevelInfo import plus.maa.backend.controller.response.copilot.CopilotInfo import plus.maa.backend.controller.response.copilot.CopilotPageInfo -import plus.maa.backend.repository.CommentsAreaRepository -import plus.maa.backend.repository.CopilotRepository import plus.maa.backend.repository.RedisCache -import plus.maa.backend.repository.UserFollowingRepository import plus.maa.backend.repository.entity.Copilot import plus.maa.backend.repository.entity.Copilot.OperationGroup -import plus.maa.backend.repository.entity.MaaUser -import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.repository.entity.CopilotEntity +import plus.maa.backend.repository.entity.Operators +import plus.maa.backend.repository.entity.RatingEntity +import plus.maa.backend.repository.entity.UserEntity +import plus.maa.backend.repository.entity.copilots +import plus.maa.backend.repository.entity.users +import plus.maa.backend.repository.ktorm.CommentsAreaKtormRepository +import plus.maa.backend.repository.ktorm.CopilotKtormRepository +import plus.maa.backend.repository.ktorm.UserFollowingKtormRepository import plus.maa.backend.service.level.ArkLevelService import plus.maa.backend.service.model.CommentStatus import plus.maa.backend.service.model.CopilotSetStatus @@ -47,7 +63,6 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference -import java.util.regex.Pattern import kotlin.math.ln import kotlin.math.max import plus.maa.backend.cache.InternalComposeCache as Cache @@ -58,20 +73,20 @@ import plus.maa.backend.cache.InternalComposeCache as Cache */ @Service class CopilotService( - private val copilotRepository: CopilotRepository, + private val database: Database, + private val copilotKtormRepository: CopilotKtormRepository, private val ratingService: RatingService, - private val mongoTemplate: MongoTemplate, private val mapper: ObjectMapper, private val levelService: ArkLevelService, private val redisCache: RedisCache, private val idComponent: IdComponent, private val userRepository: UserService, - private val commentsAreaRepository: CommentsAreaRepository, + private val commentsAreaKtormRepository: CommentsAreaKtormRepository, private val properties: MaaCopilotProperties, - private val copilotConverter: CopilotConverter, private val sensitiveWordService: SensitiveWordService, private val segmentService: SegmentService, - private val userFollowingRepository: UserFollowingRepository, + private val userFollowingKtormRepository: UserFollowingKtormRepository, + private val objectMapper: ObjectMapper, ) { private val log = KotlinLogging.logger { } @@ -107,17 +122,36 @@ class CopilotService( /** * 上传新的作业 */ - fun upload(loginUserId: String, request: CopilotCUDRequest): Long = copilotConverter.toCopilot( - request.content.parseToCopilotDto(), - idComponent.getId(Copilot.META), - loginUserId, - LocalDateTime.now(), - request.content, - request.status, - ).run { - copilotRepository.insert(this).copilotId!!.also { - segmentService.updateIndex(it, doc?.title, doc?.details) + fun upload(loginUserId: String, request: CopilotCUDRequest): Long { + val dto = request.content.parseToCopilotDto() + val copilotId = idComponent.getId(Copilot.META) + val now = LocalDateTime.now() + + val entity = CopilotEntity { + this.copilotId = copilotId + this.stageName = dto.stageName + this.uploaderId = loginUserId + this.views = 0L + this.ratingLevel = 0 + this.ratingRatio = 0.0 + this.likeCount = 0L + this.dislikeCount = 0L + this.hotScore = 0.0 + this.title = dto.doc?.title ?: "" + this.details = dto.doc?.details + this.firstUploadTime = now + this.uploadTime = now + this.content = request.content + this.status = request.status + this.commentStatus = CommentStatus.ENABLED + this.delete = false + this.deleteTime = null + this.notification = false } + + copilotKtormRepository.insertEntity(entity) + segmentService.updateIndex(copilotId, entity.title, entity.details) + return copilotId } /** @@ -129,7 +163,7 @@ class CopilotService( }.apply { // 删除作业时,如果被删除的项在 Redis 首页缓存中存在,则清空对应的首页缓存 // 新增作业就不必,因为新作业显然不会那么快就登上热度榜和浏览量榜 - deleteCacheWhenMatchCopilotId(copilotId!!) + deleteCacheWhenMatchCopilotId(copilotId) Cache.invalidateCopilotInfoByCid(copilotId) } @@ -138,15 +172,17 @@ class CopilotService( */ fun getCopilotById(userIdOrIpAddress: String, id: Long): CopilotInfo? { val result = Cache.getCopilotCache(id) { - copilotRepository.findByCopilotIdAndDeleteIsFalse(it)?.run { - CopilotInnerCacheInfo(this) + database.copilots.filter { copilot -> + (copilot.copilotId eq id) and (copilot.delete eq false) + }.firstOrNull()?.run { + CopilotInnerCacheInfo(this.copy()) } - }?.let { it -> + }?.let { val copilot = it.info - val maaUser = userRepository.findByUserIdOrDefaultInCache(copilot.uploaderId!!) + val maaUser = userRepository.findByUserIdOrDefaultInCache(copilot.uploaderId) - val commentsCount = Cache.getCommentCountCache(copilot.copilotId!!) { cid -> - commentsAreaRepository.countByCopilotIdAndDelete(cid, false) + val commentsCount = Cache.getCommentCountCache(copilot.copilotId) { cid -> + commentsAreaKtormRepository.countByCopilotIdAndDelete(cid, false) } copilot.format( ratingService.findPersonalRatingOfCopilot(userIdOrIpAddress, id), @@ -169,11 +205,10 @@ class CopilotService( second.incrementAndGet() // 丢到调度队列中, 一致性要求不高 Thread.startVirtualThread { - val query = Query.query(Criteria.where("copilotId").`is`(id)) - val update = Update().apply { - inc("views") + copilotKtormRepository.findByCopilotIdAndDeleteIsFalse(id)?.let { copilot -> + copilot.views += 1 + copilotKtormRepository.updateEntity(copilot) } - mongoTemplate.updateFirst(query, update, Copilot::class.java) } } }?.run { @@ -182,12 +217,7 @@ class CopilotService( } /** - * 分页查询。传入 userId 不为空时限制为用户所有的数据 - * 会缓存默认状态下热度和访问量排序的结果 - * - * @param userId 获取已登录用户自己的作业数据 - * @param request 模糊查询 - * @return CopilotPageInfo + * 使用 postgresql 查询作业 */ fun queriesCopilot(userId: String?, request: CopilotQueriesRequest): CopilotPageInfo { val cacheTimeout = AtomicLong() @@ -213,95 +243,27 @@ class CopilotService( }?.let { return it } } - val sortOrder = Sort.Order( - if (request.desc) Sort.Direction.DESC else Sort.Direction.ASC, - request.orderBy?.blankAsNull().let { ob -> - when (ob) { - "hot" -> "hotScore" - "id" -> "copilotId" - else -> request.orderBy - } - } ?: "copilotId", - ) // 判断是否有值 无值则为默认 val page = if (request.page > 0) request.page else 1 val limit = if (request.limit > 0) request.limit else 10 + val levelKeyword = request.levelKeyword - val pageable: Pageable = PageRequest.of(page - 1, limit, Sort.by(sortOrder)) - - val criteriaObj = Criteria() - - val andQueries: MutableSet = HashSet() - val norQueries: MutableSet = HashSet() - val orQueries: MutableSet = HashSet() - - andQueries.add(Criteria.where("delete").`is`(false)) - + var inUserIds: List? = null if (request.onlyFollowing && userId != null) { - val userFollowing = userFollowingRepository.findByUserId(userId) - val followingIds = userFollowing?.followList ?: emptyList() - + val followingIds = userFollowingKtormRepository.getFollowingIds(userId) if (followingIds.isEmpty()) { return CopilotPageInfo(false, 0, 0, emptyList()) } // 添加查询范围为关注者 - andQueries.add(Criteria.where("uploaderId").`in`(followingIds)) - } - // 仅查询自己的作业时才展示所有数据,否则只查询公开作业 - if (request.uploaderId == "me" && userId != null) { - if (request.status != null) { - andQueries.add(Criteria.where("status").`is`(request.status)) - } - } else { - andQueries.add(Criteria.where("status").`is`(CopilotSetStatus.PUBLIC)) - } - - // 关卡名、关卡类型、关卡编号 - request.levelKeyword?.blankAsNull()?.let { keyword -> - val levelInfo = levelService.queryLevelInfosByKeyword(keyword) - val c = if (levelInfo.isEmpty()) { - Criteria.where("stageName").regex(keyword.toPattern(Pattern.CASE_INSENSITIVE)) - } else { - Criteria.where("stageName").`in`(levelInfo.map(ArkLevelInfo::stageId)) - } - andQueries.add(c) - } - - // 作业id列表 - request.copilotIds?.ifEmpty { null }?.let { ids -> - andQueries.add(Criteria.where("copilotId").`in`(ids)) - } - - // 包含或排除干员 - request.operator?.removeQuotes()?.split(",")?.filterNot(String::isBlank)?.forEach { oper -> - if (oper.startsWith("~")) { - // 排除查询指定干员 - norQueries.add(Criteria.where("opers.name").`is`(oper.substring(1))) - } else { - // 模糊匹配查询指定干员 - andQueries.add(Criteria.where("opers.name").`is`(oper)) - } + inUserIds = followingIds } val uploaderId = if (request.uploaderId == "me") userId else request.uploaderId uploaderId?.blankAsNull()?.let { - andQueries.add(Criteria.where("uploaderId").`is`(it)) + inUserIds = listOf(it) } - // 封装查询 - if (andQueries.isNotEmpty()) { - criteriaObj.andOperator(andQueries) - } - if (norQueries.isNotEmpty()) { - criteriaObj.norOperator(norQueries) - } - if (orQueries.isNotEmpty()) { - criteriaObj.orOperator(orQueries) - } - - // 标题、描述、神秘代码 - val queryObj = Query().addCriteria(criteriaObj) - + var inCopilotIds: List? = request.copilotIds if (!(keyword?.length == 1 && keyword[0].isLetterOrDigit())) { segmentService.getSegment(keyword) .takeIf { @@ -310,7 +272,7 @@ class CopilotService( ?.let { words -> val idList = words.mapNotNull { val result = segmentService.fetchIndexInfo(it) - if (it.lowercase() == keyword?.lowercase() && result.isEmpty()) { + if (it.equals(keyword, ignoreCase = true) && result.isEmpty()) { null } else { result @@ -332,21 +294,111 @@ class CopilotService( if (intersection.isEmpty()) { return CopilotPageInfo(false, 1, 0, emptyList()) } - queryObj.addCriteria(Copilot::copilotId inValues intersection) + inCopilotIds = inCopilotIds?.intersect(intersection)?.toList() ?: intersection.toList() } } - // 去除large fields - queryObj.fields().exclude("content", "actions") + val requestStatus = if (request.uploaderId == "me" && userId != null) { + request.status + } else { + CopilotSetStatus.PUBLIC + } + var stageNameKeyword: String? = null + var stageNames: List? = null + if (levelKeyword != null) { + val levelList = levelService.queryLevelInfosByKeyword(levelKeyword) + if (levelList.isEmpty()) { + stageNameKeyword = keyword + } else { + stageNames = levelList.map { level -> level.stageId } + } + } + + val ops = request.operator?.removeQuotes()?.split(",")?.filterNot(String::isBlank) + var includeOps: List? = null + var notIncludeOps: List? = null + if (ops != null) { + val g = ops.groupBy { it.startsWith('~') } + if (!g[true].isNullOrEmpty()) { + notIncludeOps = g[true]?.map { it.substring(1) } + } + if (!g[false].isNullOrEmpty()) { + includeOps = g[false] + } + } + + val copilotsSeq = database.copilots.filter { + val conditions = ArrayList>() + conditions += ArgumentExpression(true, BooleanSqlType) + conditions += it.delete eq false + if (requestStatus != null) { + conditions += it.status eq requestStatus + } + if (stageNameKeyword != null) { + conditions += it.stageName like stageNameKeyword + } + if (stageNames != null) { + conditions += it.stageName inList stageNames + } + if (inUserIds != null) { + conditions += it.uploaderId inList inUserIds + } + if (inCopilotIds != null) { + conditions += it.copilotId inList inCopilotIds + } + if (includeOps != null) { + conditions += it.copilotId inList ( + database.from(Operators) + .select(Operators.copilotId) + .where { Operators.name inList includeOps } + ) + } + if (notIncludeOps != null) { + conditions += it.copilotId notInList ( + database.from(Operators) + .select(Operators.copilotId) + .where { Operators.name inList notIncludeOps } + ) + } + conditions.reduce { a, b -> a and b } + }.sortedBy { + val ord = when (request.orderBy ?: "id") { + "hot" -> it.hotScore + "id" -> it.copilotId + "views" -> it.views + else -> it.copilotId + } + if (request.desc) { + ord.desc() + } else { + ord.asc() + } + }.drop((page - 1) * limit).take(limit) + + val resultAgg = if (keyword.isNullOrEmpty() && + request.levelKeyword.isNullOrBlank() && + request.uploaderId != null && + request.uploaderId != "me" && + request.operator.isNullOrBlank() && + request.copilotIds.isNullOrEmpty() + ) { + val r = copilotsSeq.toList() + val count = r.count() + val hasNext = count > (page * limit) + (r to count) to hasNext + } else { + val r = copilotsSeq.toList() + (r to 0) to (r.size >= limit) + } - val countQueryObj = Query.of(queryObj) - // 分页排序查询 - val copilots = mongoTemplate.find(queryObj.with(pageable), Copilot::class.java) + val count = resultAgg.first.second + val copilots: List = resultAgg.first.first + val hasNext = resultAgg.second - val userIds = copilots.mapNotNull { it.uploaderId } + val userIds = copilots.map { it.uploaderId } // 填充前端所需信息 - val maaUsers = hashMapOf() + val maaUsers = hashMapOf() val remainingUserIds = userIds.filter { userId -> val info = Cache.getMaaUserCache(userId)?.also { maaUsers[userId] = it @@ -354,13 +406,14 @@ class CopilotService( info == null }.toList() if (remainingUserIds.isNotEmpty()) { - userRepository.findByUsersId(remainingUserIds).entries().forEach { - maaUsers.put(it.key, it.value) - Cache.setMaaUserCache(it.key, it.value) + val users = database.users.filter { it.userId inList remainingUserIds } + users.forEach { + maaUsers[it.userId] = it + Cache.setUserCache(it.userId, it) } } - val copilotIds = copilots.mapNotNull { it.copilotId } + val copilotIds = copilots.map { it.copilotId } val commentsCount = hashMapOf() val remainingCopilotIds = copilotIds.filter { copilotId -> val c = Cache.getCommentCountCache(copilotId)?.also { @@ -370,7 +423,7 @@ class CopilotService( }.toList() if (remainingCopilotIds.isNotEmpty()) { - val existedCount = commentsAreaRepository.findByCopilotIdInAndDelete(remainingCopilotIds, false) + val existedCount = commentsAreaKtormRepository.findByCopilotIdInAndDelete(remainingCopilotIds, false) .groupBy { it.copilotId } .mapValues { it.value.size.toLong() } copilotIds.forEach { copilotId -> @@ -383,39 +436,21 @@ class CopilotService( // 新版评分系统 // 反正目前首页和搜索不会直接展示当前用户有没有点赞,干脆直接不查,要用户点进作业才显示自己是否点赞 val infos = copilots.map { copilot -> - copilot.content = mapOf( - "stageName" to copilot.stageName, - "doc" to copilot.doc, - "opers" to copilot.opers, - "groups" to copilot.groups, - "minimumRequired" to copilot.minimumRequired, - "difficulty" to copilot.difficulty, - ).run(mapper::writeValueAsString) + val contentObj = objectMapper.readTree(copilot.content) as ObjectNode + contentObj.remove("actions") + contentObj.remove("minimum_required") + contentObj.remove("stage_name") + copilot.content = contentObj.toString() + copilot.format( null, - maaUsers.getOrDefault(copilot.uploaderId!!, MaaUser.UNKNOWN).userName, + maaUsers.getOrDefault(copilot.uploaderId, UserEntity.UNKNOWN).userName, commentsCount[copilot.copilotId] ?: 0, ) } - // 作者页需要返回作业数目 - val (count, hasNext) = if (keyword.isNullOrEmpty() && - request.levelKeyword.isNullOrBlank() && - request.uploaderId != null && - request.uploaderId != "me" && - request.operator.isNullOrBlank() && - request.copilotIds.isNullOrEmpty() - ) { - // 查询总数 - val count = mongoTemplate.count(countQueryObj, Copilot::class.java) - val hasNext = count.toInt() > (page * limit) - count to hasNext - } else { - 0L to (infos.size >= limit) - } - // 封装数据 - val data = CopilotPageInfo(hasNext, page, count, infos) + val data = CopilotPageInfo(hasNext, page, count.toLong(), infos) // 决定是否缓存 if (cacheKey.get() != null) { @@ -434,20 +469,21 @@ class CopilotService( var cIdToDeleteCache: Long? = null userEditCopilot(loginUserId, request.id) { - segmentService.removeIndex(copilotId!!, doc?.title, doc?.details) + segmentService.removeIndex(copilotId, title, details) // 从公开改为隐藏时,如果数据存在缓存中则需要清除缓存 if (status == CopilotSetStatus.PUBLIC && request.status == CopilotSetStatus.PRIVATE) cIdToDeleteCache = copilotId - copilotConverter.updateCopilotFromDto( - request.content.parseToCopilotDto(), - request.content, - this, - request.status, - ) + + val dto = request.content.parseToCopilotDto() + stageName = dto.stageName + title = dto.doc?.title ?: title + details = dto.doc?.details ?: details + content = request.content + status = request.status uploadTime = LocalDateTime.now() }.apply { Cache.invalidateCopilotInfoByCid(copilotId) - segmentService.updateIndex(copilotId!!, doc?.title, doc?.details) + segmentService.updateIndex(copilotId, title, details) } cIdToDeleteCache?.let { @@ -462,7 +498,7 @@ class CopilotService( * @param userIdOrIpAddress 用于已登录用户作出评分 */ fun rates(userIdOrIpAddress: String, request: CopilotRatingReq) { - requireNotNull(copilotRepository.existsCopilotsByCopilotId(request.id)) { "作业id不存在" } + requireNotNull(copilotKtormRepository.existsByCopilotId(request.id)) { "作业id不存在" } val ratingChange = ratingService.rateCopilot( request.id, @@ -471,13 +507,8 @@ class CopilotService( ) val (likeCountChange, dislikeCountChange) = ratingService.calcLikeChange(ratingChange) - // 获取只包含评分的作业 - var query = Query.query( - Criteria.where("copilotId").`is`(request.id).and("delete").`is`(false), - ) - // 排除 _id,防止误 save 该不完整作业后原有数据丢失 - query.fields().include("likeCount", "dislikeCount").exclude("_id") - val copilot = mongoTemplate.findOne(query, Copilot::class.java) + // 获取作业 + val copilot = copilotKtormRepository.findByCopilotIdAndDeleteIsFalse(request.id) checkNotNull(copilot) { "作业不存在" } // 计算评分相关 @@ -488,15 +519,11 @@ class CopilotService( // 只取一位小数点 val ratingLevel = rawRatingLevel.toBigDecimal().setScale(1, RoundingMode.HALF_UP).toDouble() // 更新数据 - query = Query.query( - Criteria.where("copilotId").`is`(request.id).and("delete").`is`(false), - ) - val update = Update() - update["likeCount"] = likeCount - update["dislikeCount"] = ratingCount - likeCount - update["ratingLevel"] = (ratingLevel * 10).toInt() - update["ratingRatio"] = ratingLevel - mongoTemplate.updateFirst(query, update, Copilot::class.java) + copilot.likeCount = likeCount + copilot.dislikeCount = ratingCount - likeCount + copilot.ratingLevel = (ratingLevel * 10).toInt() + copilot.ratingRatio = ratingLevel + copilotKtormRepository.updateEntity(copilot) // 记录近期评分变化量前 100 的作业 id redisCache.incZSet( @@ -508,24 +535,21 @@ class CopilotService( ) } - /** - * 将数据库内容转换为前端所需格式 - */ - private fun Copilot.format(rating: Rating?, userName: String, commentsCount: Long) = CopilotInfo( - id = copilotId!!, - uploadTime = uploadTime!!, - uploaderId = uploaderId!!, + private fun CopilotEntity.format(rating: RatingEntity?, userName: String, commentsCount: Long) = CopilotInfo( + id = copilotId, + uploadTime = uploadTime, + uploaderId = uploaderId, uploader = userName, views = views, hotScore = hotScore, available = true, ratingLevel = ratingLevel, - notEnoughRating = likeCount + dislikeCount <= properties.copilot.minValueShowNotEnoughRating, + notEnoughRating = likeCount + dislikeCount <= this@CopilotService.properties.copilot.minValueShowNotEnoughRating, ratingRatio = ratingRatio, ratingType = (rating?.rating ?: RatingType.NONE).display, commentsCount = commentsCount, - commentStatus = commentStatus ?: CommentStatus.ENABLED, - content = content ?: "", + commentStatus = commentStatus, + content = content, like = likeCount, dislike = dislikeCount, status = status, @@ -539,11 +563,13 @@ class CopilotService( commentStatus = status } - fun userEditCopilot(userId: String?, copilotId: Long?, edit: Copilot.() -> Unit): Copilot { + fun userEditCopilot(userId: String?, copilotId: Long?, edit: CopilotEntity.() -> Unit): CopilotEntity { val cId = copilotId.requireNotNull { "copilotId 不能为空" } - val copilot = copilotRepository.findByCopilotIdAndDeleteIsFalse(cId).requireNotNull { "copilot 不存在" } + val copilot = copilotKtormRepository.findByCopilotIdAndDeleteIsFalse(cId).requireNotNull { "copilot 不存在" } require(copilot.uploaderId == userId) { "您没有权限修改" } - return copilot.apply(edit).run(copilotRepository::save) + copilot.apply(edit) + copilotKtormRepository.updateEntity(copilot) + return copilot } /** @@ -575,7 +601,7 @@ class CopilotService( ) @JvmStatic - fun getHotScore(copilot: Copilot, lastWeekLike: Long, lastWeekDislike: Long): Double { + fun getHotScore(copilot: CopilotEntity, lastWeekLike: Long, lastWeekDislike: Long): Double { val now = LocalDateTime.now() val uploadTime = copilot.uploadTime // 基于时间的基础分 diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt index f50b43d5..2f53bb49 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt @@ -2,12 +2,17 @@ package plus.maa.backend.service import cn.hutool.core.lang.Assert import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.support.PageableExecutionUtils +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.dsl.inList +import org.ktorm.dsl.like +import org.ktorm.dsl.or +import org.ktorm.entity.count +import org.ktorm.entity.drop +import org.ktorm.entity.filter +import org.ktorm.entity.sortedBy +import org.ktorm.entity.take +import org.ktorm.entity.toList import org.springframework.stereotype.Service import plus.maa.backend.common.controller.PagedDTO import plus.maa.backend.common.utils.IdComponent @@ -18,12 +23,16 @@ import plus.maa.backend.controller.request.copilotset.CopilotSetQuery import plus.maa.backend.controller.request.copilotset.CopilotSetUpdateReq import plus.maa.backend.controller.response.copilotset.CopilotSetListRes import plus.maa.backend.controller.response.copilotset.CopilotSetRes -import plus.maa.backend.repository.CopilotSetRepository -import plus.maa.backend.repository.UserFollowingRepository import plus.maa.backend.repository.entity.CopilotSet +import plus.maa.backend.repository.entity.CopilotSetEntity +import plus.maa.backend.repository.entity.copilotIdsList +import plus.maa.backend.repository.entity.copilotSets +import plus.maa.backend.repository.entity.distinctIdsAndCheck +import plus.maa.backend.repository.entity.setCopilotIdsList +import plus.maa.backend.repository.ktorm.CopilotSetKtormRepository +import plus.maa.backend.repository.ktorm.UserFollowingKtormRepository import plus.maa.backend.service.model.CopilotSetStatus import java.time.LocalDateTime -import java.util.regex.Pattern /** * @author dragove @@ -31,15 +40,14 @@ import java.util.regex.Pattern */ @Service class CopilotSetService( + private val database: Database, private val idComponent: IdComponent, private val converter: CopilotSetConverter, - private val userFollowingRepository: UserFollowingRepository, - private val repository: CopilotSetRepository, + private val userFollowingKtormRepository: UserFollowingKtormRepository, + private val copilotSetKtormRepository: CopilotSetKtormRepository, private val userService: UserService, - private val mongoTemplate: MongoTemplate, ) { private val log = KotlinLogging.logger { } - private val defaultSort: Sort = Sort.by("id").descending() /** * 创建作业集 @@ -50,8 +58,21 @@ class CopilotSetService( */ fun create(req: CopilotSetCreateReq, userId: String?): Long { val id = idComponent.getId(CopilotSet.meta) - val newCopilotSet = converter.convert(req, id, userId!!) - repository.insert(newCopilotSet) + val now = LocalDateTime.now() + + val entity = CopilotSetEntity { + this.id = id + this.name = req.name + this.description = req.description + this.creatorId = userId!! + this.createTime = now + this.updateTime = now + this.status = req.status + this.delete = false + } + entity.setCopilotIdsList(req.copilotIds) + + copilotSetKtormRepository.insertEntity(entity) return id } @@ -59,29 +80,32 @@ class CopilotSetService( * 往作业集中加入作业id列表 */ fun addCopilotIds(req: CopilotSetModCopilotsReq, userId: String) { - val copilotSet = repository.findById(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } + val copilotSet = copilotSetKtormRepository.findByIdAsOptional(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") - copilotSet.copilotIds.addAll(req.copilotIds) - copilotSet.copilotIds = copilotSet.distinctIdsAndCheck() - repository.save(copilotSet) + val currentIds = copilotSet.copilotIdsList + currentIds.addAll(req.copilotIds) + copilotSet.setCopilotIdsList(copilotSet.distinctIdsAndCheck()) + copilotSetKtormRepository.updateEntity(copilotSet) } /** * 往作业集中删除作业id列表 */ fun removeCopilotIds(req: CopilotSetModCopilotsReq, userId: String) { - val copilotSet = repository.findById(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } + val copilotSet = copilotSetKtormRepository.findByIdAsOptional(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") val removeIds: Set = HashSet(req.copilotIds) - copilotSet.copilotIds.removeIf { o: Long -> removeIds.contains(o) } - repository.save(copilotSet) + val currentIds = copilotSet.copilotIdsList + currentIds.removeIf { o: Long -> removeIds.contains(o) } + copilotSet.setCopilotIdsList(currentIds) + copilotSetKtormRepository.updateEntity(copilotSet) } /** * 更新作业集信息 */ fun update(req: CopilotSetUpdateReq, userId: String) { - val copilotSet = repository.findById(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } + val copilotSet = copilotSetKtormRepository.findByIdAsOptional(req.id).orElseThrow { IllegalArgumentException("作业集不存在") } Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") if (!req.name.isNullOrBlank()) { copilotSet.name = req.name @@ -93,10 +117,11 @@ class CopilotSetService( copilotSet.status = req.status } if (req.copilotIds != null) { - copilotSet.copilotIds = req.copilotIds + copilotSet.setCopilotIdsList(req.copilotIds) copilotSet.distinctIdsAndCheck() } - repository.save(copilotSet) + copilotSet.updateTime = LocalDateTime.now() + copilotSetKtormRepository.updateEntity(copilotSet) } /** @@ -107,77 +132,80 @@ class CopilotSetService( */ fun delete(id: Long, userId: String) { log.info { "delete copilot set for id: $id, userId: $userId" } - val copilotSet = repository.findById(id).orElseThrow { IllegalArgumentException("作业集不存在") } + val copilotSet = copilotSetKtormRepository.findByIdAsOptional(id).orElseThrow { IllegalArgumentException("作业集不存在") } Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权删除该作业集") copilotSet.delete = true - copilotSet.deleteTime = LocalDateTime.now() - repository.save(copilotSet) + copilotSetKtormRepository.updateEntity(copilotSet) } fun query(req: CopilotSetQuery, userId: String?): PagedDTO { - val pageRequest = PageRequest.of(req.page - 1, req.limit, defaultSort) + val page = req.page - 1 + val limit = req.limit + val offset = page * limit - val andList = ArrayList() - val publicCriteria = Criteria.where("status").`is`(CopilotSetStatus.PUBLIC) - val permissionCriterion = if (userId.isNullOrBlank()) { - publicCriteria + var copilotSetsSeq = database.copilotSets.filter { it.delete eq false } + + // 权限过滤 + copilotSetsSeq = if (userId.isNullOrBlank()) { + copilotSetsSeq.filter { it.status eq CopilotSetStatus.PUBLIC } } else { - Criteria().orOperator(publicCriteria, Criteria.where("creatorId").`is`(userId)) + copilotSetsSeq.filter { + (it.status eq CopilotSetStatus.PUBLIC) or (it.creatorId eq userId) + } } - andList.add(permissionCriterion) - andList.add(Criteria.where("delete").`is`(false)) + // 只关注的用户 if (req.onlyFollowing && userId != null) { - val userFollowing = userFollowingRepository.findByUserId(userId) - val followingIds = userFollowing?.followList ?: emptyList() + val followingIds = userFollowingKtormRepository.getFollowingIds(userId) if (followingIds.isEmpty()) { return PagedDTO(false, 0, 0, emptyList()) } - - andList.add(Criteria.where("creatorId").`in`(followingIds)) + copilotSetsSeq = copilotSetsSeq.filter { it.creatorId inList followingIds } } - if (!req.copilotIds.isNullOrEmpty()) { - andList.add(Criteria.where("copilotIds").all(req.copilotIds)) - } + // 创建者过滤 if (!req.creatorId.isNullOrBlank()) { - if (req.creatorId == "me" && userId != null) { - andList.add(Criteria.where("creatorId").`is`(userId)) - } else { - andList.add(Criteria.where("creatorId").`is`(req.creatorId)) - } + val targetCreatorId = if (req.creatorId == "me" && userId != null) userId else req.creatorId + copilotSetsSeq = copilotSetsSeq.filter { it.creatorId eq targetCreatorId } } + + // 关键词搜索 if (!req.keyword.isNullOrBlank()) { - val pattern = Pattern.compile(req.keyword, Pattern.CASE_INSENSITIVE) - andList.add( - Criteria().orOperator( - Criteria.where("name").regex(pattern), - Criteria.where("description").regex(pattern), - ), - ) + val keyword = "%${req.keyword}%" + copilotSetsSeq = copilotSetsSeq.filter { + (it.name like keyword) or (it.description like keyword) + } } - val query = Query.query(Criteria().andOperator(andList)).with(pageRequest) - val copilotSets = PageableExecutionUtils.getPage(mongoTemplate.find(query, CopilotSet::class.java), pageRequest) { - mongoTemplate.count( - query.limit(-1).skip(-1), - CopilotSet::class.java, - ) + + // 作业ID过滤(这个需要特殊处理,因为copilotIds是JSON字符串) + var copilotSets = copilotSetsSeq.sortedBy { it.id }.drop(offset).take(limit).toList() + + // 如果有copilotIds过滤条件,需要在内存中过滤 + if (!req.copilotIds.isNullOrEmpty()) { + copilotSets = copilotSets.filter { entity -> + val entityCopilotIds = entity.copilotIdsList + req.copilotIds.all { entityCopilotIds.contains(it) } + } } - val userIds = copilotSets.map { obj: CopilotSet -> obj.creatorId }.distinct().toList() + + val totalCount = copilotSetsSeq.count().toLong() + val hasNext = (offset + limit) < totalCount + val totalPages = ((totalCount + limit - 1) / limit).toInt() + + val userIds = copilotSets.map { entity -> entity.creatorId }.distinct() val userById = userService.findByUsersId(userIds) - return PagedDTO( - copilotSets.hasNext(), - copilotSets.totalPages, - copilotSets.totalElements, - copilotSets.map { cs: CopilotSet -> - val user = userById.getOrDefault(cs.creatorId) - converter.convert(cs, user.userName) - }.toList(), - ) + + val results = copilotSets.map { cs -> + val user = userById.getOrDefault(cs.creatorId) + converter.convert(cs, user.userName) + } + + return PagedDTO(hasNext, totalPages, totalCount, results) } - fun get(id: Long): CopilotSetRes = repository.findById(id).map { copilotSet: CopilotSet -> + fun get(id: Long): CopilotSetRes { + val copilotSet = copilotSetKtormRepository.findByIdAsOptional(id).orElseThrow { IllegalArgumentException("作业不存在") } val userName = userService.findByUserIdOrDefaultInCache(copilotSet.creatorId).userName - converter.convertDetail(copilotSet, userName) - }.orElseThrow { IllegalArgumentException("作业不存在") } + return converter.convertDetail(copilotSet, userName) + } } diff --git a/src/main/kotlin/plus/maa/backend/service/FileService.kt b/src/main/kotlin/plus/maa/backend/service/FileService.kt index f035c64c..0f36eda4 100644 --- a/src/main/kotlin/plus/maa/backend/service/FileService.kt +++ b/src/main/kotlin/plus/maa/backend/service/FileService.kt @@ -1,213 +1,213 @@ package plus.maa.backend.service - -import com.mongodb.client.gridfs.GridFSFindIterable -import jakarta.servlet.http.HttpServletResponse -import org.apache.commons.lang3.StringUtils -import org.bson.Document -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.mongodb.gridfs.GridFsCriteria -import org.springframework.data.mongodb.gridfs.GridFsOperations -import org.springframework.stereotype.Service -import org.springframework.util.Assert -import org.springframework.web.multipart.MultipartException -import org.springframework.web.multipart.MultipartFile -import plus.maa.backend.controller.file.ImageDownloadDTO -import plus.maa.backend.controller.response.MaaResultException -import plus.maa.backend.repository.RedisCache -import java.io.IOException -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.UUID -import java.util.concurrent.TimeUnit -import java.util.regex.Pattern -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -/** - * @author LoMu - * Date 2023-04-16 23:21 - */ -@Service -class FileService( - private val gridFsOperations: GridFsOperations, - private val redisCache: RedisCache, -) { - fun uploadFile(file: MultipartFile, type: String?, version: String, classification: String?, label: String?, ip: String?) { - // redis持久化 - - var realVersion = version - if (redisCache.getCache("NotEnable:UploadFile", String::class.java) != null) { - throw MaaResultException(403, "closed uploadfile") - } - - // 文件小于1024Bytes不接收 - if (file.size < 1024) { - throw MultipartException("Minimum upload size exceeded") - } - Assert.notNull(file.originalFilename, "文件名不可为空") - - var antecedentVersion: String? = null - if (realVersion.contains("-")) { - val split = realVersion.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - realVersion = split[0] - antecedentVersion = split[1] - } - - val document = Document() - document["version"] = realVersion - document["antecedentVersion"] = antecedentVersion - document["label"] = label - document["classification"] = classification - document["type"] = type - document["ip"] = ip - - val index = file.originalFilename!!.lastIndexOf(".") - var fileType = "" - if (index != -1) { - fileType = file.originalFilename!!.substring(index) - } - - val fileName = "Maa-" + UUID.randomUUID().toString().replace("-".toRegex(), "") + fileType - - try { - gridFsOperations.store(file.inputStream, fileName, document) - } catch (e: IOException) { - throw RuntimeException(e) - } - } - - fun downloadDateFile(date: String?, beLocated: String, delete: Boolean, response: HttpServletResponse) { - val formatter = SimpleDateFormat("yyyy-MM-dd") - val query: Query - - val d = if (date.isNullOrBlank()) { - Date(System.currentTimeMillis()) - } else { - try { - formatter.parse(date) - } catch (e: ParseException) { - throw RuntimeException(e) - } - } - - query = if (StringUtils.isBlank(beLocated) || "after" == beLocated.lowercase(Locale.getDefault())) { - Query(Criteria.where("metadata").gte(d)) - } else { - Query(Criteria.where("uploadDate").lte(d)) - } - val files = gridFsOperations.find(query) - - response.addHeader("Content-Disposition", "attachment;filename=" + System.currentTimeMillis() + ".zip") - - gzip(response, files) - - if (delete) { - gridFsOperations.delete(query) - } - } - - fun downloadFile(imageDownloadDTO: ImageDownloadDTO, response: HttpServletResponse) { - val query = Query() - val criteriaSet: MutableSet = HashSet() - - // 图片类型 - criteriaSet.add( - GridFsCriteria.whereMetaData("type").regex(Pattern.compile(imageDownloadDTO.type, Pattern.CASE_INSENSITIVE)), - ) - - // 指定下载某个类型的图片 - if (!imageDownloadDTO.classification.isNullOrBlank()) { - criteriaSet.add( - GridFsCriteria.whereMetaData("classification") - .regex(Pattern.compile(imageDownloadDTO.classification, Pattern.CASE_INSENSITIVE)), - ) - } - - // 指定版本或指定范围版本 - if (!imageDownloadDTO.version.isNullOrEmpty()) { - val version = imageDownloadDTO.version - - if (version.size == 1) { - var antecedentVersion: String? = null - if (version[0].contains("-")) { - val split = version[0].split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - antecedentVersion = split[1] - } - criteriaSet.add( - GridFsCriteria.whereMetaData("version").`is`(version[0]).and("antecedentVersion").`is`(antecedentVersion), - ) - } else if (version.size == 2) { - criteriaSet.add(GridFsCriteria.whereMetaData("version").gte(version[0]).lte(version[1])) - } - } - - if (!imageDownloadDTO.label.isNullOrBlank()) { - criteriaSet.add( - GridFsCriteria.whereMetaData("label").regex(Pattern.compile(imageDownloadDTO.label, Pattern.CASE_INSENSITIVE)), - ) - } - - val criteria = Criteria().andOperator(criteriaSet) - query.addCriteria(criteria) - - val gridFSFiles = gridFsOperations.find(query) - - response.addHeader("Content-Disposition", "attachment;filename=" + "Maa-" + imageDownloadDTO.type + ".zip") - - gzip(response, gridFSFiles) - - if (imageDownloadDTO.delete) { - gridFsOperations.delete(query) - } - } - - fun disable(): String { - isUploadEnabled = false - return "已关闭" - } - - fun enable(): String { - isUploadEnabled = true - return "已启用" - } - - /** - * 上传功能状态 - */ - var isUploadEnabled: Boolean - get() = redisCache.getCache("NotEnable:UploadFile", String::class.java) == null - set(enabled) { - // Fixme: redis recovery solution should be added, or change to another storage - if (enabled) { - redisCache.removeCache("NotEnable:UploadFile") - } else { - redisCache.setCache("NotEnable:UploadFile", "1", 0, TimeUnit.DAYS) - } - } - - private fun gzip(response: HttpServletResponse, files: GridFSFindIterable) { - try { - ZipOutputStream(response.outputStream).use { zipOutputStream -> - for (file in files) { - val zipEntry = ZipEntry(file.filename) - gridFsOperations.getResource(file).inputStream.use { inputStream -> - // 添加压缩文件 - zipOutputStream.putNextEntry(zipEntry) - - val bytes = ByteArray(1024) - var len: Int - while ((inputStream.read(bytes).also { len = it }) != -1) { - zipOutputStream.write(bytes, 0, len) - zipOutputStream.flush() - } - } - } - } - } catch (e: IOException) { - throw RuntimeException(e) - } - } -} +// TODO 摸一下 +//import com.mongodb.client.gridfs.GridFSFindIterable +//import jakarta.servlet.http.HttpServletResponse +//import org.apache.commons.lang3.StringUtils +//import org.bson.Document +//import org.springframework.data.mongodb.core.query.Criteria +//import org.springframework.data.mongodb.core.query.Query +//import org.springframework.data.mongodb.gridfs.GridFsCriteria +//import org.springframework.data.mongodb.gridfs.GridFsOperations +//import org.springframework.stereotype.Service +//import org.springframework.util.Assert +//import org.springframework.web.multipart.MultipartException +//import org.springframework.web.multipart.MultipartFile +//import plus.maa.backend.controller.file.ImageDownloadDTO +//import plus.maa.backend.controller.response.MaaResultException +//import plus.maa.backend.repository.RedisCache +//import java.io.IOException +//import java.text.ParseException +//import java.text.SimpleDateFormat +//import java.util.Date +//import java.util.Locale +//import java.util.UUID +//import java.util.concurrent.TimeUnit +//import java.util.regex.Pattern +//import java.util.zip.ZipEntry +//import java.util.zip.ZipOutputStream +// +///** +// * @author LoMu +// * Date 2023-04-16 23:21 +// */ +//@Service +//class FileService( +// private val gridFsOperations: GridFsOperations, +// private val redisCache: RedisCache, +//) { +// fun uploadFile(file: MultipartFile, type: String?, version: String, classification: String?, label: String?, ip: String?) { +// // redis持久化 +// +// var realVersion = version +// if (redisCache.getCache("NotEnable:UploadFile", String::class.java) != null) { +// throw MaaResultException(403, "closed uploadfile") +// } +// +// // 文件小于1024Bytes不接收 +// if (file.size < 1024) { +// throw MultipartException("Minimum upload size exceeded") +// } +// Assert.notNull(file.originalFilename, "文件名不可为空") +// +// var antecedentVersion: String? = null +// if (realVersion.contains("-")) { +// val split = realVersion.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() +// realVersion = split[0] +// antecedentVersion = split[1] +// } +// +// val document = Document() +// document["version"] = realVersion +// document["antecedentVersion"] = antecedentVersion +// document["label"] = label +// document["classification"] = classification +// document["type"] = type +// document["ip"] = ip +// +// val index = file.originalFilename!!.lastIndexOf(".") +// var fileType = "" +// if (index != -1) { +// fileType = file.originalFilename!!.substring(index) +// } +// +// val fileName = "Maa-" + UUID.randomUUID().toString().replace("-".toRegex(), "") + fileType +// +// try { +// gridFsOperations.store(file.inputStream, fileName, document) +// } catch (e: IOException) { +// throw RuntimeException(e) +// } +// } +// +// fun downloadDateFile(date: String?, beLocated: String, delete: Boolean, response: HttpServletResponse) { +// val formatter = SimpleDateFormat("yyyy-MM-dd") +// val query: Query +// +// val d = if (date.isNullOrBlank()) { +// Date(System.currentTimeMillis()) +// } else { +// try { +// formatter.parse(date) +// } catch (e: ParseException) { +// throw RuntimeException(e) +// } +// } +// +// query = if (StringUtils.isBlank(beLocated) || "after" == beLocated.lowercase(Locale.getDefault())) { +// Query(Criteria.where("metadata").gte(d)) +// } else { +// Query(Criteria.where("uploadDate").lte(d)) +// } +// val files = gridFsOperations.find(query) +// +// response.addHeader("Content-Disposition", "attachment;filename=" + System.currentTimeMillis() + ".zip") +// +// gzip(response, files) +// +// if (delete) { +// gridFsOperations.delete(query) +// } +// } +// +// fun downloadFile(imageDownloadDTO: ImageDownloadDTO, response: HttpServletResponse) { +// val query = Query() +// val criteriaSet: MutableSet = HashSet() +// +// // 图片类型 +// criteriaSet.add( +// GridFsCriteria.whereMetaData("type").regex(Pattern.compile(imageDownloadDTO.type, Pattern.CASE_INSENSITIVE)), +// ) +// +// // 指定下载某个类型的图片 +// if (!imageDownloadDTO.classification.isNullOrBlank()) { +// criteriaSet.add( +// GridFsCriteria.whereMetaData("classification") +// .regex(Pattern.compile(imageDownloadDTO.classification, Pattern.CASE_INSENSITIVE)), +// ) +// } +// +// // 指定版本或指定范围版本 +// if (!imageDownloadDTO.version.isNullOrEmpty()) { +// val version = imageDownloadDTO.version +// +// if (version.size == 1) { +// var antecedentVersion: String? = null +// if (version[0].contains("-")) { +// val split = version[0].split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() +// antecedentVersion = split[1] +// } +// criteriaSet.add( +// GridFsCriteria.whereMetaData("version").`is`(version[0]).and("antecedentVersion").`is`(antecedentVersion), +// ) +// } else if (version.size == 2) { +// criteriaSet.add(GridFsCriteria.whereMetaData("version").gte(version[0]).lte(version[1])) +// } +// } +// +// if (!imageDownloadDTO.label.isNullOrBlank()) { +// criteriaSet.add( +// GridFsCriteria.whereMetaData("label").regex(Pattern.compile(imageDownloadDTO.label, Pattern.CASE_INSENSITIVE)), +// ) +// } +// +// val criteria = Criteria().andOperator(criteriaSet) +// query.addCriteria(criteria) +// +// val gridFSFiles = gridFsOperations.find(query) +// +// response.addHeader("Content-Disposition", "attachment;filename=" + "Maa-" + imageDownloadDTO.type + ".zip") +// +// gzip(response, gridFSFiles) +// +// if (imageDownloadDTO.delete) { +// gridFsOperations.delete(query) +// } +// } +// +// fun disable(): String { +// isUploadEnabled = false +// return "已关闭" +// } +// +// fun enable(): String { +// isUploadEnabled = true +// return "已启用" +// } +// +// /** +// * 上传功能状态 +// */ +// var isUploadEnabled: Boolean +// get() = redisCache.getCache("NotEnable:UploadFile", String::class.java) == null +// set(enabled) { +// // Fixme: redis recovery solution should be added, or change to another storage +// if (enabled) { +// redisCache.removeCache("NotEnable:UploadFile") +// } else { +// redisCache.setCache("NotEnable:UploadFile", "1", 0, TimeUnit.DAYS) +// } +// } +// +// private fun gzip(response: HttpServletResponse, files: GridFSFindIterable) { +// try { +// ZipOutputStream(response.outputStream).use { zipOutputStream -> +// for (file in files) { +// val zipEntry = ZipEntry(file.filename) +// gridFsOperations.getResource(file).inputStream.use { inputStream -> +// // 添加压缩文件 +// zipOutputStream.putNextEntry(zipEntry) +// +// val bytes = ByteArray(1024) +// var len: Int +// while ((inputStream.read(bytes).also { len = it }) != -1) { +// zipOutputStream.write(bytes, 0, len) +// zipOutputStream.flush() +// } +// } +// } +// } +// } catch (e: IOException) { +// throw RuntimeException(e) +// } +// } +//} diff --git a/src/main/kotlin/plus/maa/backend/service/RatingService.kt b/src/main/kotlin/plus/maa/backend/service/RatingService.kt index 6b7bb5d5..31c96268 100644 --- a/src/main/kotlin/plus/maa/backend/service/RatingService.kt +++ b/src/main/kotlin/plus/maa/backend/service/RatingService.kt @@ -1,13 +1,14 @@ package plus.maa.backend.service import org.springframework.stereotype.Service -import plus.maa.backend.repository.RatingRepository import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.repository.entity.RatingEntity +import plus.maa.backend.repository.ktorm.RatingKtormRepository import plus.maa.backend.service.model.RatingType import java.time.LocalDateTime @Service -class RatingService(private val ratingRepository: RatingRepository) { +class RatingService(private val ratingKtormRepository: RatingKtormRepository) { /** * Update rating of target object * @@ -17,27 +18,42 @@ class RatingService(private val ratingRepository: RatingRepository) { * @param ratingType Target rating type * @return A pair, previous one and the target one. */ - fun rate(keyType: Rating.KeyType, key: String, raterId: String, ratingType: RatingType): Pair { - val rating = ratingRepository.findByTypeAndKeyAndUserId( + fun rate(keyType: Rating.KeyType, key: String, raterId: String, ratingType: RatingType): Pair { + val rating = ratingKtormRepository.findByTypeAndKeyAndUserId( keyType, key, raterId, - ) ?: Rating( - null, - keyType, - key, - raterId, - RatingType.NONE, - LocalDateTime.now(), - ) + ) ?: run { + val newRating = RatingEntity { + this.id = "" + this.type = keyType + this.key = key + this.userId = raterId + this.rating = RatingType.NONE + this.rateTime = LocalDateTime.now() + } + ratingKtormRepository.insertEntity(newRating) + newRating + } if (ratingType == rating.rating) return rating to rating - val prev = rating.copy() + val prevRating = rating.rating rating.rating = ratingType rating.rateTime = LocalDateTime.now() - ratingRepository.save(rating) - return prev to rating + ratingKtormRepository.updateEntity(rating) + + // 创建一个表示之前状态的对象 + val prevEntity = RatingEntity { + this.id = rating.id + this.type = rating.type + this.key = rating.key + this.userId = rating.userId + this.rating = prevRating + this.rateTime = rating.rateTime + } + + return prevEntity to rating } /** @@ -45,19 +61,19 @@ class RatingService(private val ratingRepository: RatingRepository) { * @param ratingChange Pair of previous rating and current rating * @return Pair of like count change and dislike count change */ - fun calcLikeChange(ratingChange: Pair): Pair { + fun calcLikeChange(ratingChange: Pair): Pair { val (prev, next) = ratingChange val likeCountChange = next.rating.countLike() - prev.rating.countLike() val dislikeCountChange = next.rating.countDislike() - prev.rating.countDislike() return likeCountChange to dislikeCountChange } - fun rateComment(commentId: String, raterId: String, ratingType: RatingType): Pair = + fun rateComment(commentId: String, raterId: String, ratingType: RatingType): Pair = rate(Rating.KeyType.COMMENT, commentId, raterId, ratingType) - fun rateCopilot(copilotId: Long, raterId: String, ratingType: RatingType): Pair = + fun rateCopilot(copilotId: Long, raterId: String, ratingType: RatingType): Pair = rate(Rating.KeyType.COPILOT, copilotId.toString(), raterId, ratingType) - fun findPersonalRatingOfCopilot(raterId: String, copilotId: Long) = - ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, copilotId.toString(), raterId) + fun findPersonalRatingOfCopilot(raterId: String, copilotId: Long): RatingEntity? = + ratingKtormRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, copilotId.toString(), raterId) } diff --git a/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt b/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt index aa407e2d..4d9cb461 100644 --- a/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt +++ b/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt @@ -6,8 +6,9 @@ import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service -import plus.maa.backend.repository.UserRepository +import plus.maa.backend.common.extensions.toMaaUser import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.ktorm.UserKtormRepository import plus.maa.backend.service.model.LoginUser /** @@ -15,7 +16,7 @@ import plus.maa.backend.service.model.LoginUser */ @Service class UserDetailServiceImpl( - private val userRepository: UserRepository, + private val userRepository: UserKtormRepository, ) : UserDetailsService { /** * 查询用户信息 @@ -26,7 +27,8 @@ class UserDetailServiceImpl( */ @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(email: String): UserDetails { - val user = userRepository.findByEmail(email) ?: throw UsernameNotFoundException("用户不存在") + val userEntity = userRepository.findByEmail(email) ?: throw UsernameNotFoundException("用户不存在") + val user = userEntity.toMaaUser() val permissions = collectAuthoritiesFor(user) // 数据封装成UserDetails返回 diff --git a/src/main/kotlin/plus/maa/backend/service/UserService.kt b/src/main/kotlin/plus/maa/backend/service/UserService.kt index 719fdd82..f154e4a6 100644 --- a/src/main/kotlin/plus/maa/backend/service/UserService.kt +++ b/src/main/kotlin/plus/maa/backend/service/UserService.kt @@ -1,13 +1,21 @@ package plus.maa.backend.service +import org.ktorm.database.Database +import org.ktorm.dsl.desc +import org.ktorm.dsl.eq +import org.ktorm.dsl.like +import org.ktorm.entity.drop +import org.ktorm.entity.filter +import org.ktorm.entity.firstOrNull +import org.ktorm.entity.sortedBy +import org.ktorm.entity.take +import org.ktorm.entity.toList import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.dao.DuplicateKeyException -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.repository.findByIdOrNull import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import plus.maa.backend.common.MaaStatusCode +import plus.maa.backend.common.extensions.toMaaUser import plus.maa.backend.controller.request.user.LoginDTO import plus.maa.backend.controller.request.user.PasswordResetDTO import plus.maa.backend.controller.request.user.RegisterDTO @@ -16,8 +24,10 @@ import plus.maa.backend.controller.request.user.UserInfoUpdateDTO import plus.maa.backend.controller.response.MaaResultException import plus.maa.backend.controller.response.user.MaaLoginRsp import plus.maa.backend.controller.response.user.MaaUserInfo -import plus.maa.backend.repository.UserRepository import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserEntity +import plus.maa.backend.repository.entity.users +import plus.maa.backend.repository.ktorm.UserKtormRepository import plus.maa.backend.service.jwt.JwtExpiredException import plus.maa.backend.service.jwt.JwtInvalidException import plus.maa.backend.service.jwt.JwtService @@ -30,7 +40,8 @@ import plus.maa.backend.cache.InternalComposeCache as Cache */ @Service class UserService( - private val userRepository: UserRepository, + private val database: Database, + private val userKtormRepository: UserKtormRepository, private val emailService: EmailService, private val passwordEncoder: PasswordEncoder, private val userDetailService: UserDetailServiceImpl, @@ -44,7 +55,7 @@ class UserService( * @return 携带了token的封装类 */ fun login(loginDTO: LoginDTO): MaaLoginRsp { - val user = userRepository.findByEmail(loginDTO.email) + val user = userKtormRepository.findByEmail(loginDTO.email) if (user == null || !passwordEncoder.matches(loginDTO.password, user.password)) { throw MaaResultException(401, "用户不存在或者密码错误") } @@ -53,8 +64,9 @@ class UserService( throw MaaResultException(MaaStatusCode.MAA_USER_NOT_ENABLED) } - val authorities = userDetailService.collectAuthoritiesFor(user) - val authToken = jwtService.issueAuthToken(user.userId!!, null, authorities) + val maaUser = user.toMaaUser() + val authorities = userDetailService.collectAuthoritiesFor(maaUser) + val authToken = jwtService.issueAuthToken(user.userId, null, authorities) val refreshToken = jwtService.issueRefreshToken(user.userId, null) return MaaLoginRsp( @@ -64,7 +76,7 @@ class UserService( refreshToken.value, refreshToken.expiresAt, refreshToken.notBefore, - MaaUserInfo(user), + MaaUserInfo(maaUser), ) } @@ -75,7 +87,8 @@ class UserService( * @param rawPassword 新密码 */ fun modifyPassword(userId: String, rawPassword: String, originPassword: String? = null, verifyOriginPassword: Boolean = true) { - val maaUser = userRepository.findByIdOrNull(userId) ?: return + val userEntity = userKtormRepository.findById(userId) ?: return + val maaUser = userEntity.toMaaUser() if (verifyOriginPassword) { check(!originPassword.isNullOrEmpty()) { "请输入原密码" @@ -89,14 +102,14 @@ class UserService( } } // 修改密码的逻辑,应当使用与 authentication provider 一致的编码器 - maaUser.password = passwordEncoder.encode(rawPassword) + userEntity.password = passwordEncoder.encode(rawPassword) // 更新密码时,如果用户未启用则自动启用 - if (maaUser.status == 0) { - maaUser.status = 1 + if (userEntity.status == 0) { + userEntity.status = 1 } - maaUser.pwdUpdateTime = Instant.now() - userRepository.save(maaUser) - Cache.invalidateMaaUserById(maaUser.userId) + userEntity.pwdUpdateTime = Instant.now() + userKtormRepository.save(userEntity) + Cache.invalidateMaaUserById(userId) } /** @@ -108,7 +121,7 @@ class UserService( fun register(registerDTO: RegisterDTO): MaaUserInfo { val userName = registerDTO.userName.trim() check(userName.length >= 4) { "用户名长度应在4-24位之间" } - check(!userRepository.existsByUserName(userName)) { + check(!userKtormRepository.existsByUserName(userName)) { "用户名已存在,请重新取个名字吧" } @@ -125,7 +138,9 @@ class UserService( pwdUpdateTime = Instant.now(), ) return try { - userRepository.save(user).run(::MaaUserInfo).also { + val userEntity = userKtormRepository.createFromMaaUser(user) + userKtormRepository.save(userEntity) + MaaUserInfo(user).also { Cache.invalidateMaaUserById(it.id) } } catch (_: DuplicateKeyException) { @@ -140,19 +155,19 @@ class UserService( * @param updateDTO 更新参数 */ fun updateUserInfo(userId: String, updateDTO: UserInfoUpdateDTO) { - val maaUser = userRepository.findByIdOrNull(userId) ?: return + val userEntity = userKtormRepository.findById(userId) ?: return val newName = updateDTO.userName.trim() check(newName.length >= 4) { "用户名长度应在4-24位之间" } - if (newName == maaUser.userName) { + if (newName == userEntity.userName) { // 暂时只支持修改用户名,如果有其他字段修改需要同步修改该逻辑 return } // 用户名需要trim - check(!userRepository.existsByUserName(newName)) { + check(!userKtormRepository.existsByUserName(newName)) { "用户名已存在,请重新取个名字吧" } - maaUser.userName = newName - userRepository.save(maaUser) + userEntity.userName = newName + userKtormRepository.save(userEntity) Cache.invalidateMaaUserById(userId) } @@ -166,7 +181,8 @@ class UserService( val old = jwtService.verifyAndParseRefreshToken(token) val userId = old.subject - val user = userRepository.findById(userId).orElseThrow() + val userEntity = userKtormRepository.findById(userId) ?: throw NoSuchElementException() + val user = userEntity.toMaaUser() if (old.issuedAt.isBefore(user.pwdUpdateTime)) { throw MaaResultException(401, "invalid token") } @@ -205,8 +221,8 @@ class UserService( */ fun modifyPasswordByActiveCode(passwordResetDTO: PasswordResetDTO) { emailService.verifyVCode(passwordResetDTO.email, passwordResetDTO.activeCode) - val maaUser = userRepository.findByEmail(passwordResetDTO.email) - modifyPassword(maaUser!!.userId!!, passwordResetDTO.password, verifyOriginPassword = false) + val userEntity = userKtormRepository.findByEmail(passwordResetDTO.email) + modifyPassword(userEntity!!.userId, passwordResetDTO.password, verifyOriginPassword = false) } /** @@ -215,7 +231,7 @@ class UserService( * @param email 用户邮箱 */ fun checkUserExistByEmail(email: String) { - if (null == userRepository.findByEmail(email)) { + if (null == userKtormRepository.findByEmail(email)) { throw MaaResultException(MaaStatusCode.MAA_USER_NOT_FOUND) } } @@ -225,8 +241,8 @@ class UserService( */ fun sendRegistrationToken(regDTO: SendRegistrationTokenDTO) { // 判断用户是否存在 - val maaUser = userRepository.findByEmail(regDTO.email) - if (maaUser != null) { + val userEntity = userKtormRepository.findByEmail(regDTO.email) + if (userEntity != null) { // 用户已存在 log.info { "send registration token: user exists for email: ${regDTO.email}" } throw MaaResultException(MaaStatusCode.MAA_USER_EXISTS) @@ -235,14 +251,14 @@ class UserService( emailService.sendVCode(regDTO.email) } - fun findByUserIdOrDefault(id: String) = userRepository.findByUserId(id) ?: MaaUser.UNKNOWN + fun findByUserIdOrDefault(id: String) = database.users.filter { it.userId eq id }.firstOrNull() ?: UserEntity.UNKNOWN - fun findByUserIdOrDefaultInCache(id: String): MaaUser { + fun findByUserIdOrDefaultInCache(id: String): UserEntity { return Cache.getMaaUserCache(id, ::findByUserIdOrDefault) } fun findByUsersId(ids: Iterable): UserDict { - return userRepository.findAllById(ids).let(::UserDict) + return userKtormRepository.findAllById(ids).map { it.toMaaUser() }.let { UserDict(it) } } class UserDict(users: List) { @@ -252,13 +268,17 @@ class UserService( fun getOrDefault(id: String) = get(id) ?: MaaUser.UNKNOWN } - fun get(userId: String): MaaUserInfo? = userRepository.findByUserId(userId)?.run(::MaaUserInfo) + fun get(userId: String): MaaUserInfo? = database.users.filter { it.userId eq userId }.firstOrNull()?.run(::MaaUserInfo) /** * 用户模糊搜索 */ - fun search(userName: String, pageable: Pageable): Page { - return userRepository.searchUsers(userName, pageable) + fun search(userName: String, offset: Int, limit: Int): List { + return database.users.filter { it.userName like "%$userName%" } + .sortedBy { it.fansCount.desc() } + .drop(offset) + .take(limit) + .toList() } @Suppress("unused") diff --git a/src/main/kotlin/plus/maa/backend/service/follow/UserFollowService.kt b/src/main/kotlin/plus/maa/backend/service/follow/UserFollowService.kt index 1fe0c251..2778d245 100644 --- a/src/main/kotlin/plus/maa/backend/service/follow/UserFollowService.kt +++ b/src/main/kotlin/plus/maa/backend/service/follow/UserFollowService.kt @@ -1,26 +1,34 @@ package plus.maa.backend.service.follow +import org.ktorm.database.Database +import org.ktorm.dsl.inList +import org.ktorm.entity.filter +import org.ktorm.entity.toList import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.aggregation.Aggregation -import org.springframework.data.mongodb.core.aggregation.ArrayOperators -import org.springframework.data.mongodb.core.aggregation.ConvertOperators -import org.springframework.data.mongodb.core.aggregation.VariableOperators -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.mongodb.core.query.Update import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import plus.maa.backend.controller.response.user.MaaUserInfo -import plus.maa.backend.repository.entity.MaaUser -import plus.maa.backend.repository.entity.UserFans -import plus.maa.backend.repository.entity.UserFollowing -import java.time.Instant -import kotlin.reflect.KClass +import plus.maa.backend.repository.entity.UserEntity +import plus.maa.backend.repository.entity.UserFansEntity +import plus.maa.backend.repository.entity.UserFollowingEntity +import plus.maa.backend.repository.entity.fansList +import plus.maa.backend.repository.entity.followList +import plus.maa.backend.repository.entity.setFansList +import plus.maa.backend.repository.entity.setFollowList +import plus.maa.backend.repository.entity.users +import plus.maa.backend.repository.ktorm.UserFansKtormRepository +import plus.maa.backend.repository.ktorm.UserFollowingKtormRepository +import plus.maa.backend.repository.ktorm.UserKtormRepository +import java.time.LocalDateTime @Service -class UserFollowService(private val mongoTemplate: MongoTemplate) { +class UserFollowService( + private val database: Database, + private val userKtormRepository: UserKtormRepository, + private val userFollowingKtormRepository: UserFollowingKtormRepository, + private val userFansKtormRepository: UserFansKtormRepository, +) { @Transactional fun follow(userId: String, followUserId: String) = updateFollowingRel(userId, followUserId, true) @@ -31,64 +39,108 @@ class UserFollowService(private val mongoTemplate: MongoTemplate) { private fun updateFollowingRel(followerId: String, followeeId: String, add: Boolean) { val opStr = if (add) "关注" else "取关" require(followerId != followeeId) { "不能${opStr}自己" } - if (!mongoTemplate.exists(Query.query(Criteria.where("userId").`is`(followeeId)), MaaUser::class.java)) { + if (!userKtormRepository.existsById(followeeId)) { throw IllegalArgumentException("${opStr}对象不存在") } - updateUserListAndCount(followerId, "followingCount", UserFollowing::class, "followList", add, followeeId) - updateUserListAndCount(followeeId, "fansCount", UserFans::class, "fansList", add, followerId) + // 更新关注列表 + updateFollowingList(followerId, followeeId, add) + // 更新粉丝列表 + updateFansList(followeeId, followerId, add) } - private fun updateUserListAndCount( - ownerId: String, - ownerCountField: String, - srcClazz: KClass, - srcListField: String, - add: Boolean, - userId: String, - ) { - val userIdMatch = Criteria.where("userId").`is`(ownerId) - val update = Update().apply { - (if (add) ::addToSet else ::pull).invoke(srcListField, userId) - set("updatedAt", Instant.now()) + private fun updateFollowingList(followerId: String, followeeId: String, add: Boolean) { + val following = userFollowingKtormRepository.findByUserId(followerId) ?: run { + val newFollowing = UserFollowingEntity { + this.id = "following_$followerId" + this.userId = followerId + this.updatedAt = LocalDateTime.now() + } + newFollowing.setFollowList(mutableListOf()) + userFollowingKtormRepository.insertEntity(newFollowing) + newFollowing } - mongoTemplate.upsert(Query.query(userIdMatch), update, srcClazz.java) - - val cR = mongoTemplate.aggregate( - Aggregation.newAggregation( - Aggregation.match(userIdMatch), - Aggregation.project().and(ArrayOperators.arrayOf(srcListField).length()).`as`("total"), - ), - srcClazz.java, - CountResult::class.java, - ).uniqueMappedResult ?: return - mongoTemplate.updateFirst( - Query.query(userIdMatch), - Update().set(ownerCountField, cR.total), - MaaUser::class.java, - ) + + val currentList = following.followList.toMutableList() + if (add) { + if (!currentList.contains(followeeId)) { + currentList.add(followeeId) + } + } else { + currentList.remove(followeeId) + } + + following.setFollowList(currentList) + following.updatedAt = LocalDateTime.now() + userFollowingKtormRepository.updateEntity(following) + + // 更新用户关注数量 + updateFansCount(followerId, currentList) + } + + private fun updateFansList(userId: String, fanId: String, add: Boolean) { + val fans = userFansKtormRepository.findByUserId(userId) ?: run { + val newFans = UserFansEntity { + this.id = "fans_$userId" + this.userId = userId + this.updatedAt = LocalDateTime.now() + } + newFans.setFansList(mutableListOf()) + userFansKtormRepository.insertEntity(newFans) + newFans + } + + val currentList = fans.fansList.toMutableList() + if (add) { + if (!currentList.contains(fanId)) { + currentList.add(fanId) + } + } else { + currentList.remove(fanId) + } + + fans.setFansList(currentList) + fans.updatedAt = LocalDateTime.now() + userFansKtormRepository.updateEntity(fans) + + // 更新用户粉丝数量 + updateFansCount(userId, currentList) } - fun getFollowingList(userId: String, pageable: Pageable) = getReferredUserPage(userId, UserFollowing::class, "followList", pageable) + private fun updateFansCount(userId: String, currentList: MutableList) { + val user: UserEntity? = userKtormRepository.findById(userId) + user?.let { userEntity: UserEntity -> + userEntity.fansCount = currentList.size + userKtormRepository.updateEntity(userEntity) + } + } - fun getFansList(userId: String, pageable: Pageable) = getReferredUserPage(userId, UserFans::class, "fansList", pageable) + fun getFollowingList(userId: String, pageable: Pageable): PageImpl { + val following = userFollowingKtormRepository.findByUserId(userId) + val followingIds = following?.followList ?: emptyList() + return getUserPageFromIds(followingIds, pageable) + } - private fun getReferredUserPage(ownerId: String, clazz: KClass, field: String, pageable: Pageable): PageImpl { - val match = Aggregation.match(Criteria.where("userId").`is`(ownerId)) + fun getFansList(userId: String, pageable: Pageable): PageImpl { + val fans = userFansKtormRepository.findByUserId(userId) + val fansIds = fans?.fansList ?: emptyList() + return getUserPageFromIds(fansIds, pageable) + } - val slice = ArrayOperators.arrayOf(field).slice().offset(pageable.pageNumber * pageable.pageSize).itemCount(pageable.pageSize) - val slicedIds = VariableOperators.mapItemsOf(slice).`as`("id").andApply(ConvertOperators.valueOf("id").convertToObjectId()) - val extractCountAndIds = Aggregation.project().and(ArrayOperators.arrayOf(field).length()).`as`("total").and(slicedIds).`as`("ids") + private fun getUserPageFromIds(userIds: List, pageable: Pageable): PageImpl { + val totalCount = userIds.size.toLong() + val offset = pageable.pageNumber * pageable.pageSize + val limit = pageable.pageSize - val lookupUsers = Aggregation.lookup("maa_user", "ids", "_id", "paged") + val pagedIds = userIds.drop(offset).take(limit) - val result = mongoTemplate.aggregate( - Aggregation.newAggregation(match, extractCountAndIds, lookupUsers), - clazz.java, - PagedUserListResult::class.java, - ).uniqueMappedResult + val users = if (pagedIds.isEmpty()) { + emptyList() + } else { + database.users.filter { it.userId inList pagedIds }.toList() + } - val userInfos = result?.paged.orEmpty().map(::MaaUserInfo) - return PageImpl(userInfos, pageable, result?.total ?: 0L) + val userInfos = users.map { MaaUserInfo(it) } + return PageImpl(userInfos, pageable, totalCount) } } diff --git a/src/main/kotlin/plus/maa/backend/service/level/ArkLevelService.kt b/src/main/kotlin/plus/maa/backend/service/level/ArkLevelService.kt index a29034fe..2fa8bdcc 100644 --- a/src/main/kotlin/plus/maa/backend/service/level/ArkLevelService.kt +++ b/src/main/kotlin/plus/maa/backend/service/level/ArkLevelService.kt @@ -20,9 +20,9 @@ import plus.maa.backend.common.extensions.lazySuspend import plus.maa.backend.common.extensions.meetAll import plus.maa.backend.common.extensions.traceRun import plus.maa.backend.common.utils.converter.ArkLevelConverter +import plus.maa.backend.common.utils.converter.ArkLevelEntityConverter import plus.maa.backend.config.external.MaaCopilotProperties import plus.maa.backend.controller.response.copilot.ArkLevelInfo -import plus.maa.backend.repository.ArkLevelRepository import plus.maa.backend.repository.GithubRepository import plus.maa.backend.repository.RedisCache import plus.maa.backend.repository.entity.ArkLevel @@ -30,6 +30,7 @@ import plus.maa.backend.repository.entity.gamedata.ArkTilePos import plus.maa.backend.repository.entity.gamedata.MaaArkStage import plus.maa.backend.repository.entity.github.GithubCommit import plus.maa.backend.repository.entity.github.GithubTree +import plus.maa.backend.repository.ktorm.ArkLevelKtormRepository import reactor.netty.http.client.HttpClient import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -46,9 +47,10 @@ class ArkLevelService( properties: MaaCopilotProperties, private val githubRepo: GithubRepository, private val redisCache: RedisCache, - private val arkLevelRepo: ArkLevelRepository, + private val arkLevelKtormRepo: ArkLevelKtormRepository, private val mapper: ObjectMapper, private val arkLevelConverter: ArkLevelConverter, + private val arkLevelEntityConverter: ArkLevelEntityConverter, webClientBuilder: WebClient.Builder, ) { private val log = KotlinLogging.logger { } @@ -62,14 +64,20 @@ class ArkLevelService( @get:Cacheable("arkLevelInfos") val arkLevelInfos: List - get() = arkLevelRepo.findAll().map { arkLevel -> arkLevelConverter.convert(arkLevel) } + get() { + val entities = arkLevelKtormRepo.findAll() + return arkLevelConverter.convert(entities) + } @Cacheable("arkLevel") - fun findByLevelIdFuzzy(levelId: String): ArkLevel? = arkLevelRepo.findByLevelIdFuzzy(levelId).firstOrNull() + fun findByLevelIdFuzzy(levelId: String): ArkLevel? { + val entities = arkLevelKtormRepo.findByLevelIdFuzzy(levelId) + return entities.firstOrNull()?.let { arkLevelEntityConverter.convertFromEntity(it) } + } fun queryLevelInfosByKeyword(keyword: String): List { - val levels = arkLevelRepo.queryLevelByKeyword(keyword) - return arkLevelConverter.convert(levels) + val entities = arkLevelKtormRepo.queryLevelByKeyword(keyword) + return arkLevelConverter.convert(entities) } /** @@ -87,7 +95,7 @@ class ArkLevelService( logI { "已发现 ${trees.size} 份地图数据" } // 根据 sha 筛选无需更新的地图 - val shaSet = withContext(Dispatchers.IO) { arkLevelRepo.findAllShaBy() }.map { it.sha }.toSet() + val shaSet = withContext(Dispatchers.IO) { arkLevelKtormRepo.findAllShaBy() }.map { it.sha }.toSet() val filtered = trees.filter { !shaSet.contains(it.sha) } val parser = fetchLevelParser() @@ -146,7 +154,8 @@ class ArkLevelService( pass.incrementAndGet() logI { entryInfo(tree.path, "未知类型,跳过") } } else { - withContext(Dispatchers.IO) { arkLevelRepo.save(level) } + val entity = arkLevelEntityConverter.convertToEntity(level) + withContext(Dispatchers.IO) { arkLevelKtormRepo.save(entity) } success.incrementAndGet() logI { entryInfo(tree.path, "成功") } } @@ -177,9 +186,9 @@ class ArkLevelService( val now = LocalDateTime.now() logI { "下载完成,开始更新" } - updateLevelsOfTypeInBatch(ArkLevelType.ACTIVITIES) { level -> - level.isOpen = ArkLevelUtil.getKeyInfoById(level.stageId) in openStageKeys - level.closeTime = if (level.isOpen ?: false) null else level.closeTime ?: now + updateLevelsOfTypeInBatch(ArkLevelType.ACTIVITIES) { entity -> + entity.isOpen = ArkLevelUtil.getKeyInfoById(entity.stageId) in openStageKeys + entity.closeTime = if (entity.isOpen ?: false) null else entity.closeTime ?: now } logI { "更新完成" } } @@ -197,20 +206,24 @@ class ArkLevelService( val holder = ArkGameDataHolder.updateCrisisV2Info(fetchDataHolder(), webClient) val nowTime = LocalDateTime.now() - updateLevelsOfTypeInBatch(ArkLevelType.RUNE) { level -> - val info = holder.findCrisisV2InfoById(level.stageId) ?: return@updateLevelsOfTypeInBatch - level.closeTime = LocalDateTime.ofEpochSecond(info.endTs, 0, ZoneOffset.UTC) - level.isOpen = level.closeTime?.isAfter(nowTime) + updateLevelsOfTypeInBatch(ArkLevelType.RUNE) { entity -> + val info = holder.findCrisisV2InfoById(entity.stageId) ?: return@updateLevelsOfTypeInBatch + entity.closeTime = LocalDateTime.ofEpochSecond(info.endTs, 0, ZoneOffset.UTC) + entity.isOpen = entity.closeTime?.isAfter(nowTime) } logI { "开放状态更新完毕" } } - suspend fun updateLevelsOfTypeInBatch(catOne: ArkLevelType, batchSize: Int = 1000, block: (ArkLevel) -> Unit) { + suspend fun updateLevelsOfTypeInBatch( + catOne: ArkLevelType, + batchSize: Int = 1000, + block: (plus.maa.backend.repository.entity.ArkLevelEntity) -> Unit, + ) { var pageable = Pageable.ofSize(batchSize) do { - val page = withContext(Dispatchers.IO) { arkLevelRepo.findAllByCatOne(catOne.display, pageable) } + val page = withContext(Dispatchers.IO) { arkLevelKtormRepo.findAllByCatOne(catOne.display, pageable) } page.forEach(block) - withContext(Dispatchers.IO) { arkLevelRepo.saveAll(page) } + withContext(Dispatchers.IO) { arkLevelKtormRepo.saveAll(page.content) } pageable = page.nextPageable() } while (page.hasNext()) } diff --git a/src/main/kotlin/plus/maa/backend/service/segment/SegmentService.kt b/src/main/kotlin/plus/maa/backend/service/segment/SegmentService.kt index 0aa75e63..e6f725be 100644 --- a/src/main/kotlin/plus/maa/backend/service/segment/SegmentService.kt +++ b/src/main/kotlin/plus/maa/backend/service/segment/SegmentService.kt @@ -1,28 +1,28 @@ package plus.maa.backend.service.segment import io.github.oshai.kotlinlogging.KotlinLogging +import org.ktorm.database.Database +import org.ktorm.dsl.eq +import org.ktorm.entity.filter +import org.ktorm.entity.forEach import org.springframework.beans.factory.InitializingBean import org.springframework.context.ApplicationContext -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.find -import org.springframework.data.mongodb.core.query.Query -import org.springframework.data.mongodb.core.query.isEqualTo import org.springframework.stereotype.Service import org.wltea.analyzer.cfg.Configuration import org.wltea.analyzer.cfg.DefaultConfig import org.wltea.analyzer.core.IKSegmenter import org.wltea.analyzer.dic.Dictionary import plus.maa.backend.config.external.MaaCopilotProperties -import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.copilots import java.io.StringReader import java.time.Instant import java.util.concurrent.ConcurrentHashMap @Service class SegmentService( + private val database: Database, private val ctx: ApplicationContext, private val properties: MaaCopilotProperties, - private val mongoTemplate: MongoTemplate, ) : InitializingBean { private val log = KotlinLogging.logger { } @@ -75,15 +75,11 @@ class SegmentService( val segUpdateAt = Instant.now() log.info { "Segments updating start at: $segUpdateAt" } - val query = Query().apply { - fields().include("id", "doc", "copilotId") - addCriteria(Copilot::delete isEqualTo false) - } // small data, fetch all info - val fetched = mongoTemplate.find(query) - - fetched.forEach { - updateIndex(it.copilotId!!, it.doc?.title, it.doc?.details) + database.copilots.filter { + it.delete eq false + }.forEach { + updateIndex(it.copilotId, it.title, it.details) } log.info { "Segments updated: ${INDEX.size}" } diff --git a/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt b/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt index e70c9942..d915234d 100644 --- a/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt +++ b/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt @@ -23,7 +23,7 @@ class ArkLevelSyncTask( * 地图数据同步定时任务,每10分钟执行一次 * 应用启动时自动同步一次 */ - @Scheduled(cron = "\${maa-copilot.task-cron.ark-level:-}", zone = "Asia/Shanghai") + @Scheduled(cron = $$"${maa-copilot.task-cron.ark-level:-}", zone = "Asia/Shanghai") fun syncArkLevels() = atomRun(levelSyncing) { arkLevelService.syncLevelData() } diff --git a/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt b/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt index ddaa8a3f..368d8e42 100644 --- a/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt +++ b/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt @@ -13,8 +13,8 @@ import org.eclipse.jgit.util.FS import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import plus.maa.backend.config.external.MaaCopilotProperties -import plus.maa.backend.repository.CopilotRepository -import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.CopilotEntity +import plus.maa.backend.repository.ktorm.CopilotKtormRepository import plus.maa.backend.service.level.ArkLevelService import plus.maa.backend.service.model.CopilotSetStatus import java.io.File @@ -32,7 +32,7 @@ private val log = KotlinLogging.logger { } @Component class CopilotBackupTask( private val config: MaaCopilotProperties, - private val copilotRepository: CopilotRepository, + private val copilotKtormRepository: CopilotKtormRepository, private val levelService: ArkLevelService, ) { private lateinit var git: Git @@ -75,7 +75,7 @@ class CopilotBackupTask( /** * copilot数据同步定时任务,每天执行一次 */ - @Scheduled(cron = "\${maa-copilot.task-cron.copilot-update:-}", zone = "Asia/Shanghai") + @Scheduled(cron = $$"${maa-copilot.task-cron.copilot-update:-}", zone = "Asia/Shanghai") fun backupCopilots() { if (config.backup.disabled || Objects.isNull(git)) { return @@ -88,9 +88,9 @@ class CopilotBackupTask( val monthAgo = LocalDateTime.now().minusDays(60L) val baseDirectory = git.repository.workTree - val copilots = copilotRepository.findAllByUploadTimeAfterOrDeleteTimeAfter(monthAgo, monthAgo) - copilots.forEach { copilot: Copilot -> - val level = levelService.findByLevelIdFuzzy(copilot.stageName!!) ?: return@forEach + val copilots = copilotKtormRepository.findAllByUploadTimeAfterOrDeleteTimeAfter(monthAgo, monthAgo) + copilots.forEach { copilot: CopilotEntity -> + val level = levelService.findByLevelIdFuzzy(copilot.stageName) ?: return@forEach // 暂时使用 copilotId 作为文件名 val filePath = File( java.lang.String.join( @@ -102,7 +102,7 @@ class CopilotBackupTask( copilot.copilotId.toString() + ".json", ), ) - val content = copilot.content ?: return@forEach + val content = copilot.content if (copilot.delete || copilot.status == CopilotSetStatus.PRIVATE) { // 删除文件 deleteCopilot(filePath) diff --git a/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt b/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt index d6b2363c..a5fd5373 100644 --- a/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt +++ b/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt @@ -1,15 +1,31 @@ package plus.maa.backend.task -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.aggregation.Aggregation -import org.springframework.data.mongodb.core.query.Criteria +import org.ktorm.database.Database +import org.ktorm.dsl.and +import org.ktorm.dsl.batchUpdate +import org.ktorm.dsl.count +import org.ktorm.dsl.eq +import org.ktorm.dsl.from +import org.ktorm.dsl.groupBy +import org.ktorm.dsl.gte +import org.ktorm.dsl.inList +import org.ktorm.dsl.map +import org.ktorm.dsl.select +import org.ktorm.dsl.where +import org.ktorm.entity.count +import org.ktorm.entity.drop +import org.ktorm.entity.filter +import org.ktorm.entity.take +import org.ktorm.entity.toList import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component -import plus.maa.backend.repository.CopilotRepository +import plus.maa.backend.repository.CopilotRepo import plus.maa.backend.repository.RedisCache -import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.CopilotEntity +import plus.maa.backend.repository.entity.Copilots import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.repository.entity.Ratings +import plus.maa.backend.repository.entity.copilots import plus.maa.backend.service.CopilotService.Companion.getHotScore import plus.maa.backend.service.level.ArkLevelService import plus.maa.backend.service.model.RatingCount @@ -24,10 +40,10 @@ import java.time.LocalDateTime */ @Component class CopilotScoreRefreshTask( + private val copilotRepo: CopilotRepo, + private val database: Database, private val arkLevelService: ArkLevelService, private val redisCache: RedisCache, - private val copilotRepository: CopilotRepository, - private val mongoTemplate: MongoTemplate, ) { /** * 热度值刷入任务,每日四点三十执行(实际可能会更晚,因为需要等待之前启动的定时任务完成) @@ -35,22 +51,28 @@ class CopilotScoreRefreshTask( @Scheduled(cron = "0 30 4 * * ?", zone = "Asia/Shanghai") fun refreshHotScores() { // 分页获取所有未删除的作业 - var pageable = Pageable.ofSize(1000) - var copilots = copilotRepository.findAllByDeleteIsFalse(pageable) +// var pageable = Pageable.ofSize(1000) +// var copilots = copilotRepository.findAllByDeleteIsFalse(pageable) + var offset = 0 + val pageSize = 1000 + val query = copilotRepo.getNotDeletedQuery() + val count = query.count() + var copilots = query.take(pageSize).drop(offset).toList() // 循环读取直到没有未删除的作业为止 - while (copilots.hasContent()) { + while (copilots.isNotEmpty()) { val copilotIdSTRs = copilots.map { copilot -> copilot.copilotId.toString() }.toList() refresh(copilotIdSTRs, copilots) // 获取下一页 - if (!copilots.hasNext()) { + offset += pageSize + if (offset >= count) { // 没有下一页了,跳出循环 break } - pageable = copilots.nextPageable() - copilots = copilotRepository.findAllByDeleteIsFalse(pageable) + offset += pageSize + copilots = query.take(pageSize).drop(offset).toList() } // 移除首页热度缓存 @@ -67,9 +89,10 @@ class CopilotScoreRefreshTask( return } - val copilots = copilotRepository.findByCopilotIdInAndDeleteIsFalse( - copilotIdSTRs.map { s: String? -> s!!.toLong() }, - ) + val copilots = database.copilots.filter { + it.copilotId inList copilotIdSTRs.map { s: String? -> s!!.toLong() } and + it.delete eq false + }.toList() if (copilots.isEmpty()) { return } @@ -82,7 +105,7 @@ class CopilotScoreRefreshTask( redisCache.syncRemoveCacheByPattern("home:hot:*") } - private fun refresh(copilotIdSTRs: Collection, copilots: Iterable) { + private fun refresh(copilotIdSTRs: Collection, copilots: Iterable) { // 批量获取最近七天的点赞和点踩数量 val now = LocalDateTime.now() val likeCounts = counts(copilotIdSTRs, RatingType.LIKE, now.minusDays(7)) @@ -95,12 +118,11 @@ class CopilotScoreRefreshTask( val dislikeCount = dislikeCountMap.getOrDefault(copilot.copilotId.toString(), 0L) var hotScore = getHotScore(copilot, likeCount, dislikeCount) // 判断关卡是否开放 - val level = arkLevelService.findByLevelIdFuzzy(copilot.stageName!!) + val level = arkLevelService.findByLevelIdFuzzy(copilot.stageName) // 关卡已关闭,且作业在关闭前上传 if (level?.closeTime != null && - copilot.firstUploadTime != null && false == level.isOpen && - copilot.firstUploadTime!!.isBefore(level.closeTime) + copilot.firstUploadTime.isBefore(level.closeTime) ) { // 非开放关卡打入冷宫 @@ -109,21 +131,33 @@ class CopilotScoreRefreshTask( copilot.hotScore = hotScore } // 批量更新热度值 - copilotRepository.saveAll(copilots) + database.batchUpdate(Copilots) { + for (copilot in copilots) { + item { + set(it.hotScore, copilot.hotScore) + where { it.copilotId eq copilot.copilotId } + } + } + } +// copilotRepository.saveAll(copilots) } private fun counts(keys: Collection, rating: RatingType, startTime: LocalDateTime): List { - val aggregation = Aggregation.newAggregation( - Aggregation.match( - Criteria - .where("type").`is`(Rating.KeyType.COPILOT) - .and("key").`in`(keys) - .and("rating").`is`(rating) - .and("rateTime").gte(startTime), - ), - Aggregation.group("key").count().`as`("count").first("key").`as`("key"), - Aggregation.project("key", "count"), - ).withOptions(Aggregation.newAggregationOptions().allowDiskUse(true).build()) // 放弃内存优化,使用磁盘优化,免得内存炸了 - return mongoTemplate.aggregate(aggregation, Rating::class.java, RatingCount::class.java).mappedResults + // 使用Ktorm DSL进行GROUP BY查询,等价于MongoDB的聚合操作 + return database.from(Ratings) + .select(Ratings.key, count(Ratings.id)) + .where { + (Ratings.type eq Rating.KeyType.COPILOT) and + (Ratings.key inList keys.filterNotNull()) and + (Ratings.rating eq rating) and + (Ratings.rateTime gte startTime) + } + .groupBy(Ratings.key) + .map { row -> + RatingCount( + key = row[Ratings.key]!!, + count = row.getLong(2), + ) + } } } diff --git a/src/main/resources/application-template.yml b/src/main/resources/application-template.yml index 7e94dd26..356ec65e 100644 --- a/src/main/resources/application-template.yml +++ b/src/main/resources/application-template.yml @@ -1,15 +1,14 @@ spring: # 不是本地部署的Redis或者修改端口的记得修改Redis配置 data: - mongodb: - # 如果没有密码 - # uri: mongodb://localhost:27017/MaaBackend - # 有密码 - # uri: mongodb://用户名:密码@localhost:27017/MaaBackend - uri: mongodb://127.0.0.1/MaaBackend redis: port: 6379 host: 127.0.0.1 + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/zoot + username: root + password: admin maa-copilot: backup: @@ -81,3 +80,8 @@ springdoc: enabled: true # 配置需要生成接口文档的扫描包 packages-to-scan: plus.maa.backend + +# 如果需要打印SQL,则启用下面日志 +#logging: +# level: +# org.ktorm: debug diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cae91d83..3ff6fcb0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,11 @@ spring: + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://100.84.158.57:5432/zoot + username: root + password: admin profiles: - active: dev + active: template servlet: multipart: #最大文件阈值 @@ -13,10 +18,6 @@ spring: time-zone: UTC deserialization: FAIL_ON_UNKNOWN_PROPERTIES: false - data: - mongodb: - # 打开自动索引生成(否则数据库内唯一索引不生效 - auto-index-creation: true freemarker: check-template-location: false # 缓存配置,使用caffeine缓存框架,缓存时长为5分钟,最大缓存数量500 diff --git a/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt b/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt deleted file mode 100644 index 1d7d6be8..00000000 --- a/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package plus.maa.backend.service - -import org.junit.jupiter.api.Test -import plus.maa.backend.repository.entity.Copilot -import plus.maa.backend.service.CopilotService.Companion.getHotScore -import java.time.LocalDateTime - -class CopilotServiceTest { - @Test - fun testHotScores() { - val now = LocalDateTime.now() - val beforeWeek = now.minusDays(8L) - val copilots = arrayOfNulls(5) - val lastWeekLikeCounts = LongArray(5) - val lastWeekDislikeCounts = LongArray(5) - // 一月前的作业,评分高,但是只有一条近期好评,浏览量高 - val oldGreat = Copilot(doc = Copilot.Doc(title = "test")) - oldGreat.uploadTime = beforeWeek.minusDays(14) - oldGreat.views = 20000L - copilots[0] = oldGreat - lastWeekLikeCounts[0] = 1L - lastWeekDislikeCounts[0] = 0L - - // 近期作业,含有差评,但是均为近期评分 - val newGreat = Copilot(doc = Copilot.Doc(title = "test")) - newGreat.uploadTime = now - newGreat.views = 1000L - copilots[1] = newGreat - lastWeekLikeCounts[1] = 6L - lastWeekDislikeCounts[1] = 1L - - // 近期作业,差评较多,均为近期评分 - val newBad = Copilot(doc = Copilot.Doc(title = "test")) - newBad.uploadTime = now - newBad.views = 500L - copilots[2] = newBad - lastWeekLikeCounts[2] = 2L - lastWeekDislikeCounts[2] = 4L - - // 一月前的作业,评分高,但是只有一条近期好评,浏览量尚可 - val oldNormal = Copilot(doc = Copilot.Doc(title = "test")) - oldNormal.uploadTime = beforeWeek.minusDays(21L) - oldNormal.views = 4000L - copilots[3] = oldNormal - lastWeekLikeCounts[3] = 1L - lastWeekDislikeCounts[3] = 0L - - // 新增作业,暂无评分 - val newEmpty = Copilot(doc = Copilot.Doc(title = "test")) - newEmpty.uploadTime = now - newEmpty.views = 100L - copilots[4] = newEmpty - lastWeekLikeCounts[4] = 0L - lastWeekDislikeCounts[4] = 0L - - for (i in 0..4) { - copilots[i]!!.hotScore = getHotScore(copilots[i]!!, lastWeekLikeCounts[i], lastWeekDislikeCounts[i]) - } - - // 近期好评 > 远古好评 > 近期新增 > 近期差评 > 远古一般 - check(newGreat.hotScore > oldGreat.hotScore) - check(newEmpty.hotScore > oldGreat.hotScore) - check(oldGreat.hotScore > newBad.hotScore) - check(oldNormal.hotScore > newBad.hotScore) - } -} diff --git a/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt b/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt deleted file mode 100644 index b905e85e..00000000 --- a/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package plus.maa.backend.task - -import io.mockk.every -import io.mockk.mockk -import org.bson.Document -import org.junit.jupiter.api.Test -import org.springframework.data.domain.PageImpl -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.aggregation.AggregationResults -import plus.maa.backend.repository.CopilotRepository -import plus.maa.backend.repository.RedisCache -import plus.maa.backend.repository.entity.ArkLevel -import plus.maa.backend.repository.entity.Copilot -import plus.maa.backend.repository.entity.Rating -import plus.maa.backend.service.level.ArkLevelService -import plus.maa.backend.service.model.RatingCount -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit - -class CopilotScoreRefreshTaskTest { - private val copilotRepository = mockk() - private val mongoTemplate = mockk() - private val redisCache = mockk() - private val arkLevelService = mockk() - private val refreshTask: CopilotScoreRefreshTask = CopilotScoreRefreshTask( - arkLevelService, - redisCache, - copilotRepository, - mongoTemplate, - ) - - @Test - fun testRefreshScores() { - val now = LocalDateTime.now() - val copilot1 = Copilot(doc = Copilot.Doc(title = "test")) - copilot1.copilotId = 1L - copilot1.views = 100L - copilot1.uploadTime = now - copilot1.stageName = "stage1" - val copilot2 = Copilot(doc = Copilot.Doc(title = "test")) - copilot2.copilotId = 2L - copilot2.views = 200L - copilot2.uploadTime = now - copilot2.stageName = "stage2" - val copilot3 = Copilot(doc = Copilot.Doc(title = "test")) - copilot3.copilotId = 3L - copilot3.views = 200L - copilot3.uploadTime = now - copilot3.stageName = "stage3" - val allCopilots = listOf(copilot1, copilot2, copilot3) - - // 配置copilotRepository - every { - copilotRepository.findAllByDeleteIsFalse(any()) - } returns PageImpl(allCopilots) - - // 配置mongoTemplate - every { - mongoTemplate.aggregate(any(), Rating::class.java, RatingCount::class.java) - } returns AggregationResults( - listOf( - RatingCount("1", 1L), - RatingCount("2", 0L), - RatingCount("3", 0L), - ), - Document(), - ) - - val arkLevel = ArkLevel() - arkLevel.isOpen = true - arkLevel.closeTime = LocalDateTime.now().plus(1, ChronoUnit.DAYS) - every { arkLevelService.findByLevelIdFuzzy(any()) } returns arkLevel - every { copilotRepository.saveAll(any>()) } returns allCopilots - every { redisCache.syncRemoveCacheByPattern(any()) } returns Unit - refreshTask.refreshHotScores() - - check(copilot1.hotScore > 0) - check(copilot2.hotScore > 0) - } - - @Test - fun testRefreshTop100HotScores() { - val now = LocalDateTime.now() - val copilot1 = Copilot(doc = Copilot.Doc(title = "test")) - copilot1.copilotId = 1L - copilot1.views = 100L - copilot1.uploadTime = now - copilot1.stageName = "stage1" - val copilot2 = Copilot(doc = Copilot.Doc(title = "test")) - copilot2.copilotId = 2L - copilot2.views = 200L - copilot2.uploadTime = now - copilot2.stageName = "stage2" - val copilot3 = Copilot(doc = Copilot.Doc(title = "test")) - copilot3.copilotId = 3L - copilot3.views = 200L - copilot3.uploadTime = now - copilot3.stageName = "stage3" - val allCopilots = listOf(copilot1, copilot2, copilot3) - - // 配置 RedisCache - every { redisCache.getZSetReverse("rate:hot:copilotIds", 0, 99) } returns setOf("1", "2", "3") - - // 配置copilotRepository - every { - copilotRepository.findByCopilotIdInAndDeleteIsFalse(any()) - } returns allCopilots - - // 配置mongoTemplate - every { - mongoTemplate.aggregate(any(), Rating::class.java, RatingCount::class.java) - } returns AggregationResults( - listOf( - RatingCount("1", 1L), - RatingCount("2", 0L), - RatingCount("3", 0L), - ), - Document(), - ) - val arkLevel = ArkLevel() - arkLevel.isOpen = true - arkLevel.closeTime = LocalDateTime.now().plus(1, ChronoUnit.DAYS) - every { arkLevelService.findByLevelIdFuzzy(any()) } returns arkLevel - every { copilotRepository.saveAll(any>()) } returns allCopilots - every { redisCache.removeCache(any()) } returns Unit - every { redisCache.syncRemoveCacheByPattern(any()) } returns Unit - refreshTask.refreshTop100HotScores() - - check(copilot1.hotScore > 0) - check(copilot2.hotScore > 0) - } -}