diff --git a/build.gradle b/build.gradle index cea70017..6afb7328 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-cache' //springdoc相关依赖没有被自动管理,必须保留版本号 - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' implementation 'com.github.therapi:therapi-runtime-javadoc:0.13.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0' diff --git a/src/main/java/plus/maa/backend/common/model/CopilotSetType.java b/src/main/java/plus/maa/backend/common/model/CopilotSetType.java new file mode 100644 index 00000000..70300cb6 --- /dev/null +++ b/src/main/java/plus/maa/backend/common/model/CopilotSetType.java @@ -0,0 +1,29 @@ +package plus.maa.backend.common.model; + +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.List; + +/** + * @author dragove + * create on 2024-01-01 + */ +public interface CopilotSetType { + + List getCopilotIds(); + + default List getDistinctIdsAndCheck() { + List copilotIds = getCopilotIds(); + if (copilotIds == null) { + return Collections.emptyList(); + } + if (copilotIds.isEmpty() || copilotIds.size() == 1) { + return getCopilotIds(); + } + copilotIds = copilotIds.stream().distinct().toList(); + Assert.state(copilotIds.size() <= 1000, "作业集总作业数量不能超过1000条"); + return copilotIds; + } + +} diff --git a/src/main/java/plus/maa/backend/common/utils/IdComponent.java b/src/main/java/plus/maa/backend/common/utils/IdComponent.java new file mode 100644 index 00000000..62a6ccec --- /dev/null +++ b/src/main/java/plus/maa/backend/common/utils/IdComponent.java @@ -0,0 +1,53 @@ +package plus.maa.backend.common.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Component; +import plus.maa.backend.repository.entity.CollectionMeta; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +@Slf4j +@Component +@RequiredArgsConstructor +public class IdComponent { + private final MongoTemplate mongoTemplate; + private final Map CURRENT_ID_MAP = new ConcurrentHashMap<>(); + + /** + * 获取id数据 + * @param meta 集合元数据 + * @return 新的id + */ + public long getId(CollectionMeta meta) { + Class cls = meta.entityClass(); + String collectionName = mongoTemplate.getCollectionName(cls); + AtomicLong v = CURRENT_ID_MAP.get(collectionName); + if (v == null) { + synchronized (cls) { + v = CURRENT_ID_MAP.get(collectionName); + if (v == null) { + v = new AtomicLong(getMax(cls, meta.idGetter(), meta.incIdField())); + log.info("初始化获取 collection: {} 的最大 id,id: {}", collectionName, v.get()); + CURRENT_ID_MAP.put(collectionName, v); + } + } + } + return v.incrementAndGet(); + } + + private Long getMax(Class entityClass, Function idGetter, String fieldName) { + return Optional.ofNullable(mongoTemplate.findOne( + new Query().with(Sort.by(fieldName).descending()).limit(1), + entityClass)) + .map(idGetter) + .orElse(20000L); + } +} diff --git a/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java b/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java index c5bcb239..8dd9f9f9 100644 --- a/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java +++ b/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java @@ -52,7 +52,7 @@ public interface CopilotConverter { @Mapping(target = "uploadTime", source = "now") @Mapping(target = "firstUploadTime", source = "now") @Mapping(target = "uploaderId", source = "userId") - Copilot toCopilot(CopilotDTO copilotDto, String userId, LocalDateTime now, Long copilotId, String content); + Copilot toCopilot(CopilotDTO copilotDto, Long copilotId, String userId, LocalDateTime now, String content); @Mapping(target = "ratingType", ignore = true) @Mapping(target = "ratingRatio", ignore = true) diff --git a/src/main/java/plus/maa/backend/common/utils/converter/CopilotSetConverter.java b/src/main/java/plus/maa/backend/common/utils/converter/CopilotSetConverter.java new file mode 100644 index 00000000..3057af84 --- /dev/null +++ b/src/main/java/plus/maa/backend/common/utils/converter/CopilotSetConverter.java @@ -0,0 +1,33 @@ +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.CopilotSetRes; +import plus.maa.backend.controller.response.user.CopilotSetListRes; +import plus.maa.backend.repository.entity.CopilotSet; + +import java.time.LocalDateTime; + +/** + * @author dragove + * create on 2024-01-01 + */ +@Mapper(componentModel = "spring", imports = { + LocalDateTime.class +}) +public interface CopilotSetConverter { + + @Mapping(target = "delete", ignore = true) + @Mapping(target = "deleteTime", ignore = true) + @Mapping(target = "copilotIds", expression = "java(createReq.getDistinctIdsAndCheck())") + @Mapping(target = "createTime", expression = "java(LocalDateTime.now())") + @Mapping(target = "updateTime", expression = "java(LocalDateTime.now())") + CopilotSet convert(CopilotSetCreateReq createReq, long id, String creatorId); + + @Mapping(target = "creator", ignore = true) + CopilotSetListRes convert(CopilotSet copilotSet, String creator); + + CopilotSetRes convertDetail(CopilotSet copilotSet, String creator); + +} diff --git a/src/main/java/plus/maa/backend/config/security/SecurityConfig.java b/src/main/java/plus/maa/backend/config/security/SecurityConfig.java index 0922176b..43466348 100644 --- a/src/main/java/plus/maa/backend/config/security/SecurityConfig.java +++ b/src/main/java/plus/maa/backend/config/security/SecurityConfig.java @@ -42,6 +42,8 @@ public class SecurityConfig { "/swagger-ui/**", "/arknights/level", "/copilot/query", + "/set/query", + "/set/get", "/copilot/get/**", "/copilot/rating", "/comments/query", diff --git a/src/main/java/plus/maa/backend/controller/CopilotSetController.java b/src/main/java/plus/maa/backend/controller/CopilotSetController.java new file mode 100644 index 00000000..015b7fdc --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/CopilotSetController.java @@ -0,0 +1,98 @@ +package plus.maa.backend.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import plus.maa.backend.config.SpringDocConfig; +import plus.maa.backend.config.security.AuthenticationHelper; +import plus.maa.backend.controller.request.CommonIdReq; +import plus.maa.backend.controller.request.CopilotSetQuery; +import plus.maa.backend.controller.request.CopilotSetUpdateReq; +import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq; +import plus.maa.backend.controller.request.copilotset.CopilotSetModCopilotsReq; +import plus.maa.backend.controller.response.CopilotSetPageRes; +import plus.maa.backend.controller.response.CopilotSetRes; +import plus.maa.backend.controller.response.MaaResult; +import plus.maa.backend.service.CopilotSetService; + +/** + * @author dragove + * create on 2024-01-01 + */ +@Tag(name = "CopilotSet", description = "作业集相关接口") +@RequestMapping("/set") +@RestController +@RequiredArgsConstructor +public class CopilotSetController { + + private final CopilotSetService service; + private final AuthenticationHelper helper; + + @Operation(summary = "查询作业集列表") + @ApiResponse(description = "作业集id") + @PostMapping("/query") + public MaaResult querySets( + @Parameter(description = "作业集列表查询请求") @Valid @RequestBody CopilotSetQuery req) { + return MaaResult.success(service.query(req)); + } + + @Operation(summary = "查询作业集列表") + @ApiResponse(description = "作业集id") + @GetMapping("/get") + public MaaResult getSet(@RequestParam @Parameter(description = "作业id") long id) { + return MaaResult.success(service.get(id)); + } + + + @Operation(summary = "创建作业集") + @ApiResponse(description = "作业集id") + @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) + @PostMapping("/create") + public MaaResult createSet( + @Parameter(description = "作业集新增请求") @Valid @RequestBody CopilotSetCreateReq req) { + return MaaResult.success(service.create(req, helper.getUserId())); + } + + @Operation(summary = "添加作业集作业列表") + @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) + @PostMapping("/add") + public MaaResult addCopilotIds( + @Parameter(description = "作业集中加入新作业请求") @Valid @RequestBody CopilotSetModCopilotsReq req) { + service.addCopilotIds(req, helper.getUserId()); + return MaaResult.success(); + } + + @Operation(summary = "添加作业集作业列表") + @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) + @PostMapping("/remove") + public MaaResult removeCopilotIds( + @Parameter(description = "作业集中删除作业请求") @Valid @RequestBody CopilotSetModCopilotsReq req) { + service.removeCopilotIds(req, helper.getUserId()); + return MaaResult.success(); + } + + @Operation(summary = "更新作业集信息") + @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) + @PostMapping("/update") + public MaaResult updateCopilotSet( + @Parameter(description = "更新作业集信息请求") @Valid @RequestBody CopilotSetUpdateReq req) { + service.update(req, helper.getUserId()); + return MaaResult.success(); + } + + @Operation(summary = "删除作业集") + @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) + @PostMapping("/delete") + public MaaResult deleteCopilotSet( + @Parameter(description = "删除作业集信息请求") @Valid @RequestBody CommonIdReq req) { + service.delete(req.getId(), helper.getUserId()); + return MaaResult.success(); + } + + +} diff --git a/src/main/java/plus/maa/backend/controller/request/CommonIdReq.java b/src/main/java/plus/maa/backend/controller/request/CommonIdReq.java new file mode 100644 index 00000000..c8a6abd5 --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/request/CommonIdReq.java @@ -0,0 +1,18 @@ +package plus.maa.backend.controller.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +/** + * @author dragove + * create on 2024-01-05 + */ +@Getter +@Setter +public class CommonIdReq { + + @NotNull(message = "id必填") + private T id; + +} diff --git a/src/main/java/plus/maa/backend/controller/request/CopilotSetQuery.java b/src/main/java/plus/maa/backend/controller/request/CopilotSetQuery.java new file mode 100644 index 00000000..bd13920e --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/request/CopilotSetQuery.java @@ -0,0 +1,31 @@ +package plus.maa.backend.controller.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Getter; +import lombok.Setter; + +/** + * @author dragove + * create on 2024-01-06 + */ +@Getter +@Setter +@Schema(title = "作业集列表查询接口参数") +public class CopilotSetQuery { + + @Positive(message = "页码必须为大于0的数字") + @Schema(title = "页码") + private int page = 1; + + @Schema(title = "单页数据量") + @PositiveOrZero(message = "单页数据量必须为大于等于0的数字") + @Max(value = 50, message = "单页大小不得超过50") + private int limit = 10; + + @Schema(title = "查询关键词") + private String keyword; + +} diff --git a/src/main/java/plus/maa/backend/controller/request/CopilotSetUpdateReq.java b/src/main/java/plus/maa/backend/controller/request/CopilotSetUpdateReq.java new file mode 100644 index 00000000..a503403e --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/request/CopilotSetUpdateReq.java @@ -0,0 +1,31 @@ +package plus.maa.backend.controller.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import plus.maa.backend.service.model.CopilotSetStatus; + +/** + * @author dragove + * create on 2024-01-02 + */ +@Getter +@Setter +@Schema(title = "作业集更新请求") +public class CopilotSetUpdateReq { + @NotNull(message = "作业集id不能为空") + private long id; + + @Schema(title = "作业集名称") + @NotBlank(message = "作业集名称不能为空") + private String name; + + @Schema(title = "作业集额外描述") + private String description; + + @NotNull(message = "作业集公开状态不能为null") + @Schema(title = "作业集公开状态", enumAsRef = true) + private CopilotSetStatus status; +} diff --git a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java index 81adcc70..1ec5133c 100644 --- a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java +++ b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java @@ -6,6 +6,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + /** * @author LoMu * Date 2022-12-26 2:48 @@ -25,6 +27,7 @@ public class CopilotQueriesRequest { private boolean desc = true; private String orderBy; private String language; + private List copilotIds; /* * 这里为了正确接收前端的下划线风格,手动写了三个 setter 用于起别名 @@ -33,17 +36,27 @@ public class CopilotQueriesRequest { * (吐槽一下,同样是Get请求,怎么CommentsQueries是驼峰命名,到了CopilotQueries就成了下划线命名) */ @JsonIgnore + @SuppressWarnings("unused") public void setLevel_keyword(String levelKeyword) { this.levelKeyword = levelKeyword; } @JsonIgnore + @SuppressWarnings("unused") public void setUploader_id(String uploaderId) { this.uploaderId = uploaderId; } @JsonIgnore + @SuppressWarnings("unused") public void setOrder_by(String orderBy) { this.orderBy = orderBy; } + + @JsonIgnore + @SuppressWarnings("unused") + public void setCopilot_ids(List copilotIds) { + this.copilotIds = copilotIds; + } + } diff --git a/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.java b/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.java new file mode 100644 index 00000000..696d1dc1 --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.java @@ -0,0 +1,39 @@ +package plus.maa.backend.controller.request.copilotset; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import plus.maa.backend.common.model.CopilotSetType; +import plus.maa.backend.service.model.CopilotSetStatus; + +import java.util.List; + +/** + * @author dragove + * create on 2024-01-01 + */ +@Getter +@Setter +@Schema(title = "作业集创建请求") +public class CopilotSetCreateReq implements CopilotSetType { + + @Schema(title = "作业集名称") + @NotBlank(message = "作业集名称不能为空") + private String name; + + @Schema(title = "作业集额外描述") + private String description; + + @NotNull(message = "作业id列表字段不能为null") + @Size(max = 1000, message = "作业集作业列表最大只能为1000") + @Schema(title = "初始关联作业列表") + private List copilotIds; + + @NotNull(message = "作业集公开状态不能为null") + @Schema(title = "作业集公开状态", enumAsRef = true) + private CopilotSetStatus status; + +} diff --git a/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.java b/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.java new file mode 100644 index 00000000..ce550f9c --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.java @@ -0,0 +1,28 @@ +package plus.maa.backend.controller.request.copilotset; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @author dragove + * create on 2024-01-02 + */ +@Getter +@Setter +@Schema(title = "作业集新增作业列表请求") +public class CopilotSetModCopilotsReq { + + @NotNull(message = "作业集id必填") + @Schema(title = "作业集id") + private long id; + + @NotEmpty(message = "添加/删除作业id列表不可为空") + @Schema(title = "添加/删除收藏的作业id列表") + private List copilotIds; + +} diff --git a/src/main/java/plus/maa/backend/controller/response/CopilotSetPageRes.java b/src/main/java/plus/maa/backend/controller/response/CopilotSetPageRes.java new file mode 100644 index 00000000..10e11603 --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/response/CopilotSetPageRes.java @@ -0,0 +1,30 @@ +package plus.maa.backend.controller.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import plus.maa.backend.controller.response.user.CopilotSetListRes; + +import java.util.List; + +/** + * @author dragove + * create on 2024-01-06 + */ +@Getter +@Setter +@Schema(title = "作业集分页返回数据") +@Accessors(chain = true) +public class CopilotSetPageRes { + + @Schema(title = "是否有下一页") + private boolean hasNext; + @Schema(title = "当前页码") + private int page; + @Schema(title = "总数据量") + private long total; + @Schema(title = "作业集列表") + private List data; + +} diff --git a/src/main/java/plus/maa/backend/controller/response/CopilotSetRes.java b/src/main/java/plus/maa/backend/controller/response/CopilotSetRes.java new file mode 100644 index 00000000..0349049b --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/response/CopilotSetRes.java @@ -0,0 +1,48 @@ +package plus.maa.backend.controller.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import plus.maa.backend.service.model.CopilotSetStatus; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author dragove + * create on 2024-01-06 + */ +@Getter +@Setter +@Schema(title = "作业集响应") +public class CopilotSetRes { + + @Schema(title = "作业集id") + private long id; + + @Schema(title = "作业集名称") + private String name; + + @Schema(title = "额外描述") + private String description; + + @Schema(title = "作业id列表") + private List copilotIds; + + @Schema(title = "上传者id") + private String creatorId; + + @Schema(title = "上传者昵称") + private String creator; + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "更新时间") + private LocalDateTime updateTime; + + @Schema(title = "作业状态", enumAsRef = true) + private CopilotSetStatus status; + + +} diff --git a/src/main/java/plus/maa/backend/controller/response/user/CopilotSetListRes.java b/src/main/java/plus/maa/backend/controller/response/user/CopilotSetListRes.java new file mode 100644 index 00000000..5b630bee --- /dev/null +++ b/src/main/java/plus/maa/backend/controller/response/user/CopilotSetListRes.java @@ -0,0 +1,39 @@ +package plus.maa.backend.controller.response.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * @author dragove + * create on 2024-01-06 + */ +@Getter +@Setter +@Schema(title = "作业集响应(列表)") +public class CopilotSetListRes { + + @Schema(title = "作业集id") + private long id; + + @Schema(title = "作业集名称") + private String name; + + @Schema(title = "额外描述") + private String description; + + @Schema(title = "上传者id") + private String creatorId; + + @Schema(title = "上传者昵称") + private String creator; + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java b/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java index 6f72e382..b4ea56bd 100644 --- a/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java +++ b/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java @@ -64,9 +64,9 @@ public MaaResult methodArgumentTypeMismatchException(MethodArgumentTypeM public MaaResult methodArgumentNotValidException(MethodArgumentNotValidException e) { FieldError fieldError = e.getBindingResult().getFieldError(); if (fieldError != null) { - return MaaResult.fail(400, String.format("参数校验错误:%s", fieldError.getDefaultMessage())); + return MaaResult.fail(400, String.format("参数校验错误: %s", fieldError.getDefaultMessage())); } - return MaaResult.fail(400, String.format("参数校验错误:%s", e.getMessage())); + return MaaResult.fail(400, String.format("参数校验错误: %s", e.getMessage())); } /** diff --git a/src/main/java/plus/maa/backend/repository/CopilotSetRepository.java b/src/main/java/plus/maa/backend/repository/CopilotSetRepository.java new file mode 100644 index 00000000..ab6814ad --- /dev/null +++ b/src/main/java/plus/maa/backend/repository/CopilotSetRepository.java @@ -0,0 +1,25 @@ +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 + */ +public interface CopilotSetRepository extends MongoRepository { + + @Query(""" + { + "$or": [ + {"name": {'$regex': ?0 ,'$options':'i'}}, + {"description": {'$regex': ?0,'$options':'i' }} + ] + } + """) + Page findByKeyword(String keyword, Pageable pageable); + +} diff --git a/src/main/java/plus/maa/backend/repository/UserRepository.java b/src/main/java/plus/maa/backend/repository/UserRepository.java index 35112d39..d9a4fde5 100644 --- a/src/main/java/plus/maa/backend/repository/UserRepository.java +++ b/src/main/java/plus/maa/backend/repository/UserRepository.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -20,6 +21,8 @@ public interface UserRepository extends MongoRepository { */ MaaUser findByEmail(String email); + Optional findByUserId(String userId); + default Map findByUsersId(List userId) { return findAllById(userId) .stream().collect(Collectors.toMap(MaaUser::getUserId, Function.identity())); diff --git a/src/main/java/plus/maa/backend/repository/entity/CollectionMeta.java b/src/main/java/plus/maa/backend/repository/entity/CollectionMeta.java new file mode 100644 index 00000000..68736d6a --- /dev/null +++ b/src/main/java/plus/maa/backend/repository/entity/CollectionMeta.java @@ -0,0 +1,16 @@ +package plus.maa.backend.repository.entity; + +import java.io.Serializable; +import java.util.function.Function; + +/** + * mongodb 集合元数据 + * + * @param 集合对应实体数据类型 + * @author dragove + * created on 2023-12-27 + */ +public record CollectionMeta(Function idGetter, String incIdField, Class entityClass) + implements Serializable { + +} diff --git a/src/main/java/plus/maa/backend/repository/entity/Copilot.java b/src/main/java/plus/maa/backend/repository/entity/Copilot.java index ce2705ca..aae48764 100644 --- a/src/main/java/plus/maa/backend/repository/entity/Copilot.java +++ b/src/main/java/plus/maa/backend/repository/entity/Copilot.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.Accessors; 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; @@ -26,6 +27,11 @@ @Accessors(chain = true) @Document("maa_copilot") public class Copilot implements Serializable { + + @Transient + public static final CollectionMeta META = new CollectionMeta<>(Copilot::getCopilotId, + "copilotId", Copilot.class); + @Id // 作业id private String id; diff --git a/src/main/java/plus/maa/backend/repository/entity/CopilotSet.java b/src/main/java/plus/maa/backend/repository/entity/CopilotSet.java new file mode 100644 index 00000000..c43db05d --- /dev/null +++ b/src/main/java/plus/maa/backend/repository/entity/CopilotSet.java @@ -0,0 +1,81 @@ +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 lombok.Data; +import lombok.experimental.Accessors; +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; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 作业集数据 + */ +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Accessors(chain = true) +@Document("maa_copilot_set") +public class CopilotSet implements Serializable, CopilotSetType { + + @Transient + public static final CollectionMeta META = new CollectionMeta<>(CopilotSet::getId, + "id", CopilotSet.class); + + /** + * 作业集id + */ + @Id + private long id; + + /** + * 作业集名称 + */ + private String name; + + /** + * 额外描述 + */ + private String description; + + /** + * 作业id列表 + * 使用 list 保证有序 + * 作业添加时应当保证唯一 + */ + private List copilotIds; + + /** + * 上传者id + */ + private String creatorId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + + /** + * 作业状态 + * {@link plus.maa.backend.service.model.CopilotSetStatus} + */ + private CopilotSetStatus status; + + @JsonIgnore + private boolean delete; + + @JsonIgnore + private LocalDateTime deleteTime; + +} diff --git a/src/main/java/plus/maa/backend/repository/entity/MaaUser.java b/src/main/java/plus/maa/backend/repository/entity/MaaUser.java index 06561db3..81849409 100644 --- a/src/main/java/plus/maa/backend/repository/entity/MaaUser.java +++ b/src/main/java/plus/maa/backend/repository/entity/MaaUser.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.Accessors; 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.controller.request.user.UserInfoUpdateDTO; @@ -25,6 +26,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @Document("maa_user") public class MaaUser implements Serializable { + @Transient + public static final MaaUser UNKNOWN = new MaaUser().setUserName("未知用户:("); @Id private String userId; private String userName; diff --git a/src/main/java/plus/maa/backend/service/CommentsAreaService.java b/src/main/java/plus/maa/backend/service/CommentsAreaService.java index 16ac8d5d..22084fd9 100644 --- a/src/main/java/plus/maa/backend/service/CommentsAreaService.java +++ b/src/main/java/plus/maa/backend/service/CommentsAreaService.java @@ -53,8 +53,6 @@ public class CommentsAreaService { private final MaaCopilotProperties maaCopilotProperties; - private final MaaUser UNKNOWN_USER = new MaaUser().setUserName("未知用户:("); - private final CommentConverter commentConverter; @@ -121,8 +119,8 @@ public void addComments(String userId, CommentsAddDTO commentsAddDTO) { CommentNotification commentNotification = new CommentNotification(); - String authorName = maaUserMap.getOrDefault(replyUserId, UNKNOWN_USER).getUserName(); - String reName = maaUserMap.getOrDefault(userId, UNKNOWN_USER).getUserName(); + String authorName = maaUserMap.getOrDefault(replyUserId, MaaUser.UNKNOWN).getUserName(); + String reName = maaUserMap.getOrDefault(userId, MaaUser.UNKNOWN).getUserName(); String title = isCopilotAuthor ? copilot.getDoc().getTitle() : commentsArea.getMessage(); @@ -342,7 +340,7 @@ public CommentsAreaInfo queriesCommentsArea(CommentsQueriesDTO request) { mainComment , maaUserMap.getOrDefault( mainComment.getUploaderId() - , UNKNOWN_USER + , MaaUser.UNKNOWN ) ); @@ -356,7 +354,7 @@ public CommentsAreaInfo queriesCommentsArea(CommentsQueriesDTO request) { //填充评论用户名 , maaUserMap.getOrDefault( subComment.getUploaderId(), - UNKNOWN_USER + MaaUser.UNKNOWN ) ) ).toList(); diff --git a/src/main/java/plus/maa/backend/service/CopilotService.java b/src/main/java/plus/maa/backend/service/CopilotService.java index ba5d8839..00dd697f 100644 --- a/src/main/java/plus/maa/backend/service/CopilotService.java +++ b/src/main/java/plus/maa/backend/service/CopilotService.java @@ -1,9 +1,9 @@ package plus.maa.backend.service; +import cn.hutool.core.collection.CollectionUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +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; @@ -58,12 +59,12 @@ public class CopilotService { private final ObjectMapper mapper; private final ArkLevelService levelService; private final RedisCache redisCache; + private final IdComponent idComponent; private final UserRepository userRepository; private final CommentsAreaRepository commentsAreaRepository; private final MaaCopilotProperties properties; private final CopilotConverter copilotConverter; - private final AtomicLong copilotIncrementId = new AtomicLong(20000); /* 首页分页查询缓存配置 @@ -76,17 +77,6 @@ public class CopilotService { "id", 300L ); - @PostConstruct - public void init() { - // 初始化copilotId, 从数据库中获取最大的copilotId - // 如果数据库中没有数据, 则从20000开始 - copilotRepository.findFirstByOrderByCopilotIdDesc() - .map(Copilot::getCopilotId) - .ifPresent(last -> copilotIncrementId.set(last + 1)); - - log.info("作业自增ID初始化完成: {}", copilotIncrementId.get()); - } - /** * 并修正前端的冗余部分 * @@ -154,10 +144,8 @@ private Pattern caseInsensitive(String s) { public Long upload(String loginUserId, String content) { CopilotDTO copilotDTO = correctCopilot(parseToCopilotDto(content)); // 将其转换为数据库存储对象 - Copilot copilot = copilotConverter.toCopilot( - copilotDTO, loginUserId, - LocalDateTime.now(), copilotIncrementId.getAndIncrement(), - content); + Copilot copilot = copilotConverter.toCopilot(copilotDTO, + idComponent.getId(Copilot.META), loginUserId, LocalDateTime.now(), content); copilotRepository.insert(copilot); return copilot.getCopilotId(); } @@ -214,7 +202,7 @@ public Optional getCopilotById(String userIdOrIpAddress, Long id) { // 新评分系统 RatingType ratingType = ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, - Long.toString(copilot.getCopilotId()), userIdOrIpAddress) + Long.toString(copilot.getCopilotId()), userIdOrIpAddress) .map(Rating::getRating) .orElse(null); // 用户点进作业会显示点赞信息 @@ -238,7 +226,8 @@ public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesReq AtomicReference setKey = new AtomicReference<>(); // 只缓存默认状态下热度和访问量排序的结果,并且最多只缓存前三页 if (request.getPage() <= 3 && request.getDocument() == null && request.getLevelKeyword() == null && - request.getUploaderId() == null && request.getOperator() == null) { + request.getUploaderId() == null && request.getOperator() == null && + CollectionUtil.isEmpty(request.getCopilotIds())) { Optional cacheOptional = Optional.ofNullable(request.getOrderBy()) .filter(StringUtils::isNotBlank) @@ -292,6 +281,11 @@ public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesReq } } + // 作业id列表 + if (CollectionUtil.isNotEmpty(request.getCopilotIds())) { + andQueries.add(Criteria.where("copilotId").in(request.getCopilotIds())); + } + //标题、描述、神秘代码 if (StringUtils.isNotBlank(request.getDocument())) { orQueries.add(Criteria.where("doc.title").regex(caseInsensitive(request.getDocument()))); @@ -354,9 +348,9 @@ public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesReq // 新版评分系统 // 反正目前首页和搜索不会直接展示当前用户有没有点赞,干脆直接不查,要用户点进作业才显示自己是否点赞 List infos = copilots.stream().map(copilot -> - formatCopilot(copilot, null, - maaUsers.get(copilot.getUploaderId()).getUserName(), - commentsCount.get(copilot.getCopilotId()))) + formatCopilot(copilot, null, + maaUsers.get(copilot.getUploaderId()).getUserName(), + commentsCount.get(copilot.getCopilotId()))) .toList(); // 计算页面 diff --git a/src/main/java/plus/maa/backend/service/CopilotSetService.java b/src/main/java/plus/maa/backend/service/CopilotSetService.java new file mode 100644 index 00000000..84cda80a --- /dev/null +++ b/src/main/java/plus/maa/backend/service/CopilotSetService.java @@ -0,0 +1,143 @@ +package plus.maa.backend.service; + +import cn.hutool.core.lang.Assert; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import plus.maa.backend.common.utils.IdComponent; +import plus.maa.backend.common.utils.converter.CopilotSetConverter; +import plus.maa.backend.controller.request.CopilotSetQuery; +import plus.maa.backend.controller.request.CopilotSetUpdateReq; +import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq; +import plus.maa.backend.controller.request.copilotset.CopilotSetModCopilotsReq; +import plus.maa.backend.controller.response.CopilotSetPageRes; +import plus.maa.backend.controller.response.CopilotSetRes; +import plus.maa.backend.repository.CopilotSetRepository; +import plus.maa.backend.repository.UserRepository; +import plus.maa.backend.repository.entity.CopilotSet; +import plus.maa.backend.repository.entity.MaaUser; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author dragove + * create on 2024-01-01 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CopilotSetService { + + private final IdComponent idComponent; + private final CopilotSetConverter converter; + private final CopilotSetRepository repository; + private final UserRepository userRepository; + private final Sort DEFAULT_SORT = Sort.by("id").descending(); + + /** + * 创建作业集 + * + * @param req 作业集创建请求 + * @param userId 创建者用户id + * @return 作业集id + */ + public long create(CopilotSetCreateReq req, String userId) { + long id = idComponent.getId(CopilotSet.META); + CopilotSet newCopilotSet = + converter.convert(req, id, userId); + repository.insert(newCopilotSet); + return id; + } + + /** + * 往作业集中加入作业id列表 + */ + public void addCopilotIds(CopilotSetModCopilotsReq req, String userId) { + CopilotSet copilotSet = repository.findById(req.getId()) + .orElseThrow(() -> new IllegalArgumentException("作业集不存在")); + Assert.state(copilotSet.getCreatorId().equals(userId), "您不是该作业集的创建者,无权修改该作业集"); + copilotSet.getCopilotIds().addAll(req.getCopilotIds()); + copilotSet.setCopilotIds(copilotSet.getDistinctIdsAndCheck()); + repository.save(copilotSet); + } + + /** + * 往作业集中删除作业id列表 + */ + public void removeCopilotIds(CopilotSetModCopilotsReq req, String userId) { + CopilotSet copilotSet = repository.findById(req.getId()) + .orElseThrow(() -> new IllegalArgumentException("作业集不存在")); + Assert.state(copilotSet.getCreatorId().equals(userId), "您不是该作业集的创建者,无权修改该作业集"); + Set removeIds = new HashSet<>(req.getCopilotIds()); + copilotSet.getCopilotIds().removeIf(removeIds::contains); + repository.save(copilotSet); + } + + /** + * 更新作业集信息 + */ + public void update(CopilotSetUpdateReq req, String userId) { + CopilotSet copilotSet = repository.findById(req.getId()) + .orElseThrow(() -> new IllegalArgumentException("作业集不存在")); + Assert.state(copilotSet.getCreatorId().equals(userId), "您不是该作业集的创建者,无权修改该作业集"); + copilotSet.setName(req.getName()); + copilotSet.setDescription(req.getDescription()); + copilotSet.setStatus(req.getStatus()); + repository.save(copilotSet); + } + + /** + * 删除作业集信息(逻辑删除,保留详情接口查询结果) + * + * @param id 作业集id + * @param userId 登陆用户id + */ + public void delete(long id, String userId) { + log.info("delete copilot set for id: {}, userId: {}", id, userId); + CopilotSet copilotSet = repository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("作业集不存在")); + Assert.state(copilotSet.getCreatorId().equals(userId), "您不是该作业集的创建者,无权删除该作业集"); + copilotSet.setDelete(true); + copilotSet.setDeleteTime(LocalDateTime.now()); + repository.save(copilotSet); + } + + public CopilotSetPageRes query(CopilotSetQuery req) { + PageRequest pageRequest = PageRequest.of(req.getPage() - 1, req.getLimit(), DEFAULT_SORT); + + String keyword = req.getKeyword(); + Page copilotSets; + if (StringUtils.isBlank(keyword)) { + copilotSets = repository.findAll(pageRequest); + } else { + copilotSets = repository.findByKeyword(keyword, pageRequest); + } + + List userIds = copilotSets.stream().map(CopilotSet::getCreatorId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map userById = userRepository.findByUsersId(userIds); + return new CopilotSetPageRes() + .setPage(copilotSets.getNumber() + 1) + .setTotal(copilotSets.getTotalElements()) + .setHasNext(copilotSets.getTotalPages() > req.getPage()) + .setData(copilotSets.stream().map(cs -> { + MaaUser user = userById.getOrDefault(cs.getCreatorId(), MaaUser.UNKNOWN); + return converter.convert(cs, user.getUserName()); + }).toList()); + + } + + public CopilotSetRes get(long id) { + return repository.findById(id).map($ -> { + String userName = userRepository.findByUserId($.getCreatorId()).orElse(MaaUser.UNKNOWN).getUserName(); + return converter.convertDetail($, userName); + }).orElseThrow(() -> new IllegalArgumentException("作业不存在")); + } +} diff --git a/src/main/java/plus/maa/backend/service/model/CopilotSetStatus.java b/src/main/java/plus/maa/backend/service/model/CopilotSetStatus.java new file mode 100644 index 00000000..cc1c7fd0 --- /dev/null +++ b/src/main/java/plus/maa/backend/service/model/CopilotSetStatus.java @@ -0,0 +1,23 @@ +package plus.maa.backend.service.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author dragove + * create on 2024-01-01 + */ +@Getter +@AllArgsConstructor +public enum CopilotSetStatus { + + /** + * 私有,仅查看自己的作业集的时候展示,其他列表页面不展示,但是通过详情接口可查询(无权限控制) + */ + PRIVATE, + /** + * 公开,可以被搜索 + */ + PUBLIC, + +}