From a714ec8cee1b73064489eddc59bb4d8ba10bd6ba Mon Sep 17 00:00:00 2001 From: zhonghua Date: Wed, 1 Apr 2026 10:48:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E7=AB=9E=E8=B5=9B?= =?UTF-8?q?=E4=B8=8E=E7=B3=BB=E7=BB=9F=E6=8E=A5=E5=8F=A3=E5=B9=B6=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=20UGC=20=E5=86=85=E5=AE=B9=E5=AE=A1=E6=A0=B8=E4=B8=8E?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 竞赛表/作品/评委/公告/评审规则等 Flyway 与路由、分页接口对齐 - 字典与配置 /page 路径、SysLog 统计去掉不存在的 deleted 条件 - 学校 GET 无数据时返回成功且 data 为 null - 新增 content-review、work_tags 等 UGC 表及服务(V29) - 前端作品管理列表支持 published+taken_down 等调整 Made-with: Cursor --- .../creation/controller/ConfigController.java | 10 +- .../controller/ContentReviewController.java | 128 +++++ .../controller/ContestController.java | 15 +- .../controller/ContestJudgeController.java | 18 +- .../controller/ContestNoticeController.java | 20 +- .../ContestPresetCommentController.java | 15 + .../ContestReviewRuleController.java | 18 +- .../controller/ContestWorkController.java | 14 + .../creation/controller/DictController.java | 10 +- .../creation/controller/SchoolController.java | 2 +- .../controller/WorkTagController.java | 65 +++ .../dto/contentreview/ApproveWorkBodyDTO.java | 11 + .../dto/contentreview/RejectWorkBodyDTO.java | 14 + .../contentreview/TakedownWorkBodyDTO.java | 13 + .../creation/dto/judge/JudgeQueryDTO.java | 31 ++ .../creation/dto/notice/NoticeQueryDTO.java | 31 ++ .../dto/reviewrule/ReviewRuleQueryDTO.java | 28 ++ .../creation/dto/tags/WorkTagCreateDTO.java | 14 + .../creation/dto/tags/WorkTagUpdateDTO.java | 13 + .../creation/dto/work/WorkQueryDTO.java | 10 + .../creation/entity/ContentReviewLog.java | 44 ++ .../lesingle/creation/entity/UserWork.java | 96 ++++ .../creation/entity/UserWorkPage.java | 32 ++ .../com/lesingle/creation/entity/WorkTag.java | 37 ++ .../creation/entity/WorkTagRelation.java | 24 + .../mapper/ContentReviewLogMapper.java | 9 + .../creation/mapper/UserWorkMapper.java | 9 + .../creation/mapper/UserWorkPageMapper.java | 9 + .../creation/mapper/WorkTagMapper.java | 9 + .../mapper/WorkTagRelationMapper.java | 9 + .../service/ContentReviewService.java | 32 ++ .../creation/service/ContestJudgeService.java | 6 + .../service/ContestNoticeService.java | 6 + .../service/ContestReviewRuleService.java | 6 + .../creation/service/ContestService.java | 5 +- .../creation/service/SchoolService.java | 2 +- .../creation/service/WorkTagService.java | 25 + .../impl/ContentReviewServiceImpl.java | 449 +++++++++++++++++ .../service/impl/ContestJudgeServiceImpl.java | 23 + .../impl/ContestNoticeServiceImpl.java | 42 ++ .../impl/ContestReviewRuleServiceImpl.java | 20 + .../service/impl/ContestServiceImpl.java | 7 +- .../service/impl/SchoolServiceImpl.java | 3 +- .../service/impl/WorkTagServiceImpl.java | 125 +++++ .../V23__align_t_biz_contest_tenant_audit.sql | 64 +++ ...25__add_valid_state_t_biz_contest_work.sql | 25 + .../V26__align_t_biz_contest_judge.sql | 36 ++ .../V27__align_t_biz_contest_notice.sql | 47 ++ ..._valid_state_t_biz_contest_review_rule.sql | 14 + .../V29__create_ugc_user_work_tags_review.sql | 88 ++++ .../main/resources/mapper/SysLogMapper.xml | 4 - java-frontend/src/api/public.ts | 470 ++++++++++-------- .../src/views/content/TagManagement.vue | 10 +- .../src/views/content/WorkManagement.vue | 13 +- .../src/views/content/WorkReview.vue | 10 +- .../src/views/system/public-users/Index.vue | 2 +- java-frontend/vite.config.ts | 1 + 57 files changed, 2025 insertions(+), 268 deletions(-) create mode 100644 java-backend/src/main/java/com/lesingle/creation/controller/ContentReviewController.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/controller/WorkTagController.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/contentreview/ApproveWorkBodyDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/contentreview/RejectWorkBodyDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/contentreview/TakedownWorkBodyDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/judge/JudgeQueryDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/notice/NoticeQueryDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/reviewrule/ReviewRuleQueryDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagCreateDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagUpdateDTO.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/ContentReviewLog.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/UserWork.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/UserWorkPage.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/WorkTag.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/WorkTagRelation.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/ContentReviewLogMapper.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkMapper.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkPageMapper.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagMapper.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagRelationMapper.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/service/ContentReviewService.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/service/WorkTagService.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/service/impl/ContentReviewServiceImpl.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/service/impl/WorkTagServiceImpl.java create mode 100644 java-backend/src/main/resources/db/migration/V23__align_t_biz_contest_tenant_audit.sql create mode 100644 java-backend/src/main/resources/db/migration/V25__add_valid_state_t_biz_contest_work.sql create mode 100644 java-backend/src/main/resources/db/migration/V26__align_t_biz_contest_judge.sql create mode 100644 java-backend/src/main/resources/db/migration/V27__align_t_biz_contest_notice.sql create mode 100644 java-backend/src/main/resources/db/migration/V28__add_valid_state_t_biz_contest_review_rule.sql create mode 100644 java-backend/src/main/resources/db/migration/V29__create_ugc_user_work_tags_review.sql diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ConfigController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ConfigController.java index e6839b3..a5f55c0 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ConfigController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ConfigController.java @@ -38,8 +38,8 @@ public class ConfigController { return Result.success(result); } - @GetMapping - @Operation(summary = "配置列表") + @GetMapping(value = {"", "/page"}) + @Operation(summary = "配置分页列表") @PreAuthorize("hasAuthority('config:read')") public Result> pageList( @AuthenticationPrincipal UserPrincipal userPrincipal, @@ -62,7 +62,7 @@ public class ConfigController { return Result.success(result); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "配置详情") @PreAuthorize("hasAuthority('config:read')") public Result detail( @@ -73,7 +73,7 @@ public class ConfigController { return Result.success(result); } - @PutMapping("/{id}") + @PutMapping("/{id:\\d+}") @Operation(summary = "更新配置") @PreAuthorize("hasAuthority('config:update')") public Result update( @@ -85,7 +85,7 @@ public class ConfigController { return Result.success(result); } - @DeleteMapping("/{id}") + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除配置") @PreAuthorize("hasAuthority('config:delete')") public Result delete( diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContentReviewController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContentReviewController.java new file mode 100644 index 0000000..642412f --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContentReviewController.java @@ -0,0 +1,128 @@ +package com.lesingle.creation.controller; + +import com.lesingle.creation.common.core.Result; +import com.lesingle.creation.common.security.UserPrincipal; +import com.lesingle.creation.dto.contentreview.ApproveWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.RejectWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.TakedownWorkBodyDTO; +import com.lesingle.creation.service.ContentReviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 内容审核 / UGC 作品管理(路径对齐原 Nest:/api/content-review) + */ +@Tag(name = "内容审核") +@RestController +@RequestMapping("/api/content-review") +@RequiredArgsConstructor +public class ContentReviewController { + + private final ContentReviewService contentReviewService; + + @GetMapping("/management/stats") + @Operation(summary = "作品管理统计") + public Result> managementStats(@AuthenticationPrincipal UserPrincipal principal) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + return Result.success(contentReviewService.getManagementStats(tenantId, superTenant)); + } + + @GetMapping("/works/stats") + @Operation(summary = "审核台统计") + public Result> workStats(@AuthenticationPrincipal UserPrincipal principal) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + return Result.success(contentReviewService.getWorkStats(tenantId, superTenant)); + } + + @GetMapping("/works") + @Operation(summary = "作品列表(审核队列/管理)") + public Result> workQueue( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String status, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + return Result.success(contentReviewService.getWorkQueue( + page, pageSize, status, keyword, startTime, endTime, tenantId, superTenant)); + } + + @GetMapping("/works/{id:\\d+}") + @Operation(summary = "作品详情") + public Result> workDetail( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + return Result.success(contentReviewService.getWorkDetail(id, tenantId, superTenant)); + } + + @PostMapping("/works/{id:\\d+}/approve") + @Operation(summary = "审核通过") + public Result approve( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody(required = false) ApproveWorkBodyDTO body) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + contentReviewService.approve(id, principal.getUserId(), tenantId, superTenant, body); + return Result.success(); + } + + @PostMapping("/works/{id:\\d+}/reject") + @Operation(summary = "审核拒绝") + public Result reject( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody @Validated RejectWorkBodyDTO body) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + contentReviewService.reject(id, principal.getUserId(), tenantId, superTenant, body); + return Result.success(); + } + + @PostMapping("/works/{id:\\d+}/takedown") + @Operation(summary = "下架") + public Result takedown( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody @Validated TakedownWorkBodyDTO body) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + contentReviewService.takedown(id, principal.getUserId(), tenantId, superTenant, body); + return Result.success(); + } + + @PostMapping("/works/{id:\\d+}/restore") + @Operation(summary = "恢复上架") + public Result restore( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + contentReviewService.restore(id, principal.getUserId(), tenantId, superTenant); + return Result.success(); + } + + @PostMapping("/works/{id:\\d+}/recommend") + @Operation(summary = "切换推荐") + public Result recommend( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + Long tenantId = principal.getTenantId(); + boolean superTenant = principal.isSuperTenant(); + contentReviewService.toggleRecommend(id, tenantId, superTenant); + return Result.success(); + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestController.java index d7847ca..7ab6054 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestController.java @@ -34,7 +34,8 @@ public class ContestController { @AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody @Validated CreateContestDTO dto) { Long tenantId = userPrincipal.getTenantId(); - ContestDetailVO result = contestService.create(dto, tenantId); + Long userId = userPrincipal.getUserId(); + ContestDetailVO result = contestService.create(dto, tenantId, userId); return Result.success(result); } @@ -68,7 +69,7 @@ public class ContestController { return Result.success(result); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "竞赛详情") @PreAuthorize("hasAuthority('contest:read')") public Result detail( @@ -79,7 +80,7 @@ public class ContestController { return Result.success(result); } - @PutMapping("/{id}") + @PutMapping("/{id:\\d+}") @Operation(summary = "更新竞赛") @PreAuthorize("hasAuthority('contest:update')") public Result update( @@ -91,7 +92,7 @@ public class ContestController { return Result.success(result); } - @PatchMapping("/{id}/publish") + @PatchMapping("/{id:\\d+}/publish") @Operation(summary = "发布/取消发布竞赛") @PreAuthorize("hasAuthority('contest:publish')") public Result publish( @@ -103,7 +104,7 @@ public class ContestController { return Result.success(result); } - @PatchMapping("/{id}/finish") + @PatchMapping("/{id:\\d+}/finish") @Operation(summary = "标记竞赛为完结") @PreAuthorize("hasAuthority('contest:update')") public Result finish( @@ -114,7 +115,7 @@ public class ContestController { return Result.success(result); } - @PatchMapping("/{id}/reopen") + @PatchMapping("/{id:\\d+}/reopen") @Operation(summary = "重新开启竞赛") @PreAuthorize("hasAuthority('contest:update')") public Result reopen( @@ -125,7 +126,7 @@ public class ContestController { return Result.success(result); } - @DeleteMapping("/{id}") + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除竞赛") @PreAuthorize("hasAuthority('contest:delete')") public Result delete(@PathVariable Long id) { diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestJudgeController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestJudgeController.java index a32d98f..d4f2c03 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestJudgeController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestJudgeController.java @@ -1,8 +1,10 @@ package com.lesingle.creation.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.common.core.Result; import com.lesingle.creation.common.security.UserPrincipal; import com.lesingle.creation.dto.judge.CreateJudgeDTO; +import com.lesingle.creation.dto.judge.JudgeQueryDTO; import com.lesingle.creation.dto.judge.UpdateJudgeDTO; import com.lesingle.creation.service.ContestJudgeService; import com.lesingle.creation.vo.judge.JudgeVO; @@ -40,7 +42,17 @@ public class ContestJudgeController { return Result.success(result); } - @PutMapping("/{id}") + @GetMapping + @Operation(summary = "分页查询评委") + @PreAuthorize("hasAuthority('contest:read')") + public Result> pageList( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @ModelAttribute JudgeQueryDTO queryDTO) { + Long tenantId = userPrincipal.getTenantId(); + return Result.success(judgeService.pageQuery(queryDTO, tenantId)); + } + + @PutMapping("/{id:\\d+}") @Operation(summary = "更新评委") @PreAuthorize("hasAuthority('contest:judge:update')") public Result update( @@ -50,7 +62,7 @@ public class ContestJudgeController { return Result.success(result); } - @DeleteMapping("/{id}") + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除评委") @PreAuthorize("hasAuthority('contest:judge:delete')") public Result delete(@PathVariable Long id) { @@ -58,7 +70,7 @@ public class ContestJudgeController { return Result.success(null); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "获取评委详情") @PreAuthorize("hasAuthority('contest:read')") public Result getDetail(@PathVariable Long id) { diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestNoticeController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestNoticeController.java index 3161f0e..c688701 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestNoticeController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestNoticeController.java @@ -1,8 +1,10 @@ package com.lesingle.creation.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.common.core.Result; import com.lesingle.creation.common.security.UserPrincipal; import com.lesingle.creation.dto.notice.CreateNoticeDTO; +import com.lesingle.creation.dto.notice.NoticeQueryDTO; import com.lesingle.creation.dto.notice.UpdateNoticeDTO; import com.lesingle.creation.service.ContestNoticeService; import com.lesingle.creation.vo.notice.NoticeVO; @@ -39,7 +41,17 @@ public class ContestNoticeController { return Result.success(result); } - @PutMapping("/{id}") + @GetMapping + @Operation(summary = "分页查询公告") + @PreAuthorize("hasAuthority('contest:read')") + public Result> pageList( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @ModelAttribute NoticeQueryDTO queryDTO) { + Long tenantId = userPrincipal.getTenantId(); + return Result.success(noticeService.pageQuery(queryDTO, tenantId)); + } + + @PutMapping("/{id:\\d+}") @Operation(summary = "更新公告") @PreAuthorize("hasAuthority('contest:notice:update')") public Result update( @@ -49,7 +61,7 @@ public class ContestNoticeController { return Result.success(result); } - @DeleteMapping("/{id}") + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除公告") @PreAuthorize("hasAuthority('contest:notice:delete')") public Result delete(@PathVariable Long id) { @@ -57,7 +69,7 @@ public class ContestNoticeController { return Result.success(null); } - @PostMapping("/{id}/publish") + @PostMapping("/{id:\\d+}/publish") @Operation(summary = "发布公告") @PreAuthorize("hasAuthority('contest:notice:publish')") public Result publish(@PathVariable Long id) { @@ -65,7 +77,7 @@ public class ContestNoticeController { return Result.success(result); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "获取公告详情") @PreAuthorize("hasAuthority('contest:read')") public Result getDetail(@PathVariable Long id) { diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestPresetCommentController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestPresetCommentController.java index 73a6a6b..95451db 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestPresetCommentController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestPresetCommentController.java @@ -4,7 +4,9 @@ import com.lesingle.creation.common.core.Result; import com.lesingle.creation.common.security.UserPrincipal; import com.lesingle.creation.dto.presetcomment.CreatePresetCommentDTO; import com.lesingle.creation.service.ContestPresetCommentService; +import com.lesingle.creation.service.ContestReviewService; import com.lesingle.creation.vo.presetcomment.PresetCommentVO; +import com.lesingle.creation.vo.review.JudgeContestVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -25,6 +27,7 @@ import java.util.List; public class ContestPresetCommentController { private final ContestPresetCommentService presetCommentService; + private final ContestReviewService reviewService; @PostMapping @Operation(summary = "创建预设评语") @@ -73,4 +76,16 @@ public class ContestPresetCommentController { List result = presetCommentService.listCommon(tenantId); return Result.success(result); } + + /** + * 与 {@code GET /api/contests/reviews/judge/contests} 一致,供前端在预设评语模块复用 + */ + @GetMapping("/judge/contests") + @Operation(summary = "评委参与的活动列表") + public Result> getJudgeContests( + @AuthenticationPrincipal UserPrincipal userPrincipal) { + Long judgeId = userPrincipal.getUserId(); + Long tenantId = userPrincipal.getTenantId(); + return Result.success(reviewService.getJudgeContests(judgeId, tenantId)); + } } diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestReviewRuleController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestReviewRuleController.java index 5b6eaeb..4c62564 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestReviewRuleController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestReviewRuleController.java @@ -1,9 +1,11 @@ package com.lesingle.creation.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.common.core.Result; import com.lesingle.creation.common.security.UserPrincipal; import com.lesingle.creation.dto.reviewrule.CreateReviewDimensionDTO; import com.lesingle.creation.dto.reviewrule.CreateReviewRuleDTO; +import com.lesingle.creation.dto.reviewrule.ReviewRuleQueryDTO; import com.lesingle.creation.service.ContestReviewRuleService; import com.lesingle.creation.vo.reviewrule.ReviewRuleVO; import io.swagger.v3.oas.annotations.Operation; @@ -39,7 +41,17 @@ public class ContestReviewRuleController { return Result.success(result); } - @DeleteMapping("/{id}") + @GetMapping + @Operation(summary = "分页查询评审规则") + @PreAuthorize("hasAuthority('contest:read')") + public Result> pageList( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @ModelAttribute ReviewRuleQueryDTO queryDTO) { + Long tenantId = userPrincipal.getTenantId(); + return Result.success(reviewRuleService.pageQuery(queryDTO, tenantId)); + } + + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除评审规则") @PreAuthorize("hasAuthority('contest:review-rule:delete')") public Result delete(@PathVariable Long id) { @@ -47,7 +59,7 @@ public class ContestReviewRuleController { return Result.success(null); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "获取评审规则详情") @PreAuthorize("hasAuthority('contest:read')") public Result getDetail(@PathVariable Long id) { @@ -82,7 +94,7 @@ public class ContestReviewRuleController { return Result.success(result); } - @DeleteMapping("/dimensions/{id}") + @DeleteMapping("/dimensions/{id:\\d+}") @Operation(summary = "删除评审维度") @PreAuthorize("hasAuthority('contest:review-rule:update')") public Result deleteDimension(@PathVariable Long id) { diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/ContestWorkController.java b/java-backend/src/main/java/com/lesingle/creation/controller/ContestWorkController.java index 05fd96b..7854a52 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/ContestWorkController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/ContestWorkController.java @@ -49,6 +49,20 @@ public class ContestWorkController { return Result.success(result); } + /** + * 与前端常用路径 GET /api/contests/works?page=&pageSize= 对齐(勿被竞赛详情 /{id} 抢占) + */ + @GetMapping + @Operation(summary = "分页查询作品列表") + @PreAuthorize("hasAuthority('contest:read')") + public Result> pageList( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @ModelAttribute WorkQueryDTO queryDTO) { + Long tenantId = userPrincipal.getTenantId(); + Page result = workService.pageQuery(queryDTO, tenantId); + return Result.success(result); + } + @GetMapping("/page") @Operation(summary = "分页查询作品列表") @PreAuthorize("hasAuthority('contest:read')") diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/DictController.java b/java-backend/src/main/java/com/lesingle/creation/controller/DictController.java index 5172a03..474cb84 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/DictController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/DictController.java @@ -40,8 +40,8 @@ public class DictController { return Result.success(result); } - @GetMapping - @Operation(summary = "字典列表") + @GetMapping(value = {"", "/page"}) + @Operation(summary = "字典分页列表") @PreAuthorize("hasAuthority('dict:read')") public Result> pageList( @AuthenticationPrincipal UserPrincipal userPrincipal, @@ -64,7 +64,7 @@ public class DictController { return Result.success(result); } - @GetMapping("/{id}") + @GetMapping("/{id:\\d+}") @Operation(summary = "字典详情") @PreAuthorize("hasAuthority('dict:read')") public Result detail( @@ -75,7 +75,7 @@ public class DictController { return Result.success(result); } - @PutMapping("/{id}") + @PutMapping("/{id:\\d+}") @Operation(summary = "更新字典") @PreAuthorize("hasAuthority('dict:update')") public Result update( @@ -87,7 +87,7 @@ public class DictController { return Result.success(result); } - @DeleteMapping("/{id}") + @DeleteMapping("/{id:\\d+}") @Operation(summary = "删除字典") @PreAuthorize("hasAuthority('dict:delete')") public Result delete( diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/SchoolController.java b/java-backend/src/main/java/com/lesingle/creation/controller/SchoolController.java index 64d9929..0b94d41 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/SchoolController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/SchoolController.java @@ -38,7 +38,7 @@ public class SchoolController { } @GetMapping - @Operation(summary = "获取学校信息") + @Operation(summary = "获取学校信息(未创建时 data 为 null)") @PreAuthorize("hasAuthority('school:read')") public Result get( @AuthenticationPrincipal UserPrincipal userPrincipal) { diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/WorkTagController.java b/java-backend/src/main/java/com/lesingle/creation/controller/WorkTagController.java new file mode 100644 index 0000000..e910d68 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/controller/WorkTagController.java @@ -0,0 +1,65 @@ +package com.lesingle.creation.controller; + +import com.lesingle.creation.common.core.Result; +import com.lesingle.creation.dto.tags.WorkTagCreateDTO; +import com.lesingle.creation.dto.tags.WorkTagUpdateDTO; +import com.lesingle.creation.entity.WorkTag; +import com.lesingle.creation.service.WorkTagService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 作品标签管理(路径对齐原 Nest:/api/tags) + */ +@Tag(name = "作品标签") +@RestController +@RequestMapping("/api/tags") +@RequiredArgsConstructor +public class WorkTagController { + + private final WorkTagService workTagService; + + @GetMapping + @Operation(summary = "标签列表(管理端)") + public Result> list() { + return Result.success(workTagService.listAll()); + } + + @GetMapping("/categories") + @Operation(summary = "标签分类列表") + public Result> categories() { + return Result.success(workTagService.listCategories()); + } + + @PostMapping + @Operation(summary = "创建标签") + public Result create(@RequestBody @Validated WorkTagCreateDTO dto) { + return Result.success(workTagService.create(dto)); + } + + @PutMapping("/{id:\\d+}") + @Operation(summary = "更新标签") + public Result update( + @PathVariable Long id, + @RequestBody WorkTagUpdateDTO dto) { + return Result.success(workTagService.update(id, dto)); + } + + @DeleteMapping("/{id:\\d+}") + @Operation(summary = "删除标签") + public Result remove(@PathVariable Long id) { + workTagService.remove(id); + return Result.success(); + } + + @PatchMapping("/{id:\\d+}/status") + @Operation(summary = "启用/禁用标签") + public Result toggleStatus(@PathVariable Long id) { + return Result.success(workTagService.toggleStatus(id)); + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/ApproveWorkBodyDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/ApproveWorkBodyDTO.java new file mode 100644 index 0000000..da561a9 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/ApproveWorkBodyDTO.java @@ -0,0 +1,11 @@ +package com.lesingle.creation.dto.contentreview; + +import lombok.Data; + +/** + * 审核通过请求体 + */ +@Data +public class ApproveWorkBodyDTO { + private String note; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/RejectWorkBodyDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/RejectWorkBodyDTO.java new file mode 100644 index 0000000..19c97d1 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/RejectWorkBodyDTO.java @@ -0,0 +1,14 @@ +package com.lesingle.creation.dto.contentreview; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 审核拒绝请求体 + */ +@Data +public class RejectWorkBodyDTO { + @NotBlank(message = "拒绝原因不能为空") + private String reason; + private String note; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/TakedownWorkBodyDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/TakedownWorkBodyDTO.java new file mode 100644 index 0000000..42055e8 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/contentreview/TakedownWorkBodyDTO.java @@ -0,0 +1,13 @@ +package com.lesingle.creation.dto.contentreview; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 下架请求体 + */ +@Data +public class TakedownWorkBodyDTO { + @NotBlank(message = "下架原因不能为空") + private String reason; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/judge/JudgeQueryDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/judge/JudgeQueryDTO.java new file mode 100644 index 0000000..cc76aa2 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/judge/JudgeQueryDTO.java @@ -0,0 +1,31 @@ +package com.lesingle.creation.dto.judge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 评委分页查询参数 + */ +@Data +@Schema(description = "评委分页查询") +public class JudgeQueryDTO { + + @Schema(description = "页码", example = "1") + private Integer pageNum = 1; + + @Schema(description = "每页数量", example = "10") + private Integer pageSize = 10; + + @Schema(description = "活动 ID") + private Long contestId; + + @Schema(description = "评委姓名关键字") + private String judgeName; + + @Schema(hidden = true) + public void setPage(Integer page) { + if (page != null && page > 0) { + this.pageNum = page; + } + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/notice/NoticeQueryDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/notice/NoticeQueryDTO.java new file mode 100644 index 0000000..61c6f22 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/notice/NoticeQueryDTO.java @@ -0,0 +1,31 @@ +package com.lesingle.creation.dto.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 公告分页查询参数 + */ +@Data +@Schema(description = "公告分页查询") +public class NoticeQueryDTO { + + @Schema(description = "页码", example = "1") + private Integer pageNum = 1; + + @Schema(description = "每页数量", example = "10") + private Integer pageSize = 10; + + @Schema(description = "活动 ID") + private Long contestId; + + @Schema(description = "标题关键字") + private String titleKeyword; + + @Schema(hidden = true) + public void setPage(Integer page) { + if (page != null && page > 0) { + this.pageNum = page; + } + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/reviewrule/ReviewRuleQueryDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/reviewrule/ReviewRuleQueryDTO.java new file mode 100644 index 0000000..8093bef --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/reviewrule/ReviewRuleQueryDTO.java @@ -0,0 +1,28 @@ +package com.lesingle.creation.dto.reviewrule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 评审规则分页查询参数 + */ +@Data +@Schema(description = "评审规则分页查询") +public class ReviewRuleQueryDTO { + + @Schema(description = "页码", example = "1") + private Integer pageNum = 1; + + @Schema(description = "每页数量", example = "10") + private Integer pageSize = 10; + + @Schema(description = "规则名称关键字") + private String ruleName; + + @Schema(hidden = true) + public void setPage(Integer page) { + if (page != null && page > 0) { + this.pageNum = page; + } + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagCreateDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagCreateDTO.java new file mode 100644 index 0000000..8ffdc42 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagCreateDTO.java @@ -0,0 +1,14 @@ +package com.lesingle.creation.dto.tags; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "创建标签") +public class WorkTagCreateDTO { + @NotBlank + private String name; + private String category; + private Integer sort; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagUpdateDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagUpdateDTO.java new file mode 100644 index 0000000..e1ebdd3 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/dto/tags/WorkTagUpdateDTO.java @@ -0,0 +1,13 @@ +package com.lesingle.creation.dto.tags; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "更新标签") +public class WorkTagUpdateDTO { + private String name; + private String category; + private Integer sort; + private String status; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/work/WorkQueryDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/work/WorkQueryDTO.java index 64b8f08..35e369b 100644 --- a/java-backend/src/main/java/com/lesingle/creation/dto/work/WorkQueryDTO.java +++ b/java-backend/src/main/java/com/lesingle/creation/dto/work/WorkQueryDTO.java @@ -16,6 +16,16 @@ public class WorkQueryDTO { @Schema(description = "每页数量", example = "10") private Integer pageSize = 10; + /** + * 兼容 query 参数 page(与 pageNum 同义) + */ + @Schema(hidden = true) + public void setPage(Integer page) { + if (page != null && page > 0) { + this.pageNum = page; + } + } + @Schema(description = "活动 ID") private Long contestId; diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/ContentReviewLog.java b/java-backend/src/main/java/com/lesingle/creation/entity/ContentReviewLog.java new file mode 100644 index 0000000..8727314 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/ContentReviewLog.java @@ -0,0 +1,44 @@ +package com.lesingle.creation.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 内容审核操作日志 + */ +@Data +@TableName("content_review_logs") +public class ContentReviewLog { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("tenant_id") + private Long tenantId; + + @TableField("target_type") + private String targetType; + + @TableField("target_id") + private Long targetId; + + @TableField("work_id") + private Long workId; + + private String action; + + private String reason; + + private String note; + + @TableField("operator_id") + private Long operatorId; + + @TableField("create_time") + private LocalDateTime createTime; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/UserWork.java b/java-backend/src/main/java/com/lesingle/creation/entity/UserWork.java new file mode 100644 index 0000000..9176f8c --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/UserWork.java @@ -0,0 +1,96 @@ +package com.lesingle.creation.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户 UGC 作品(绘本等) + */ +@Data +@TableName("user_works") +public class UserWork { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("tenant_id") + private Long tenantId; + + @TableField("user_id") + private Long userId; + + private String title; + + @TableField("cover_url") + private String coverUrl; + + private String description; + + private String visibility; + + private String status; + + @TableField("review_note") + private String reviewNote; + + @TableField("review_time") + private LocalDateTime reviewTime; + + @TableField("reviewer_id") + private Long reviewerId; + + @TableField("machine_review_result") + private String machineReviewResult; + + @TableField("machine_review_note") + private String machineReviewNote; + + @TableField("is_recommended") + private Boolean isRecommended; + + @TableField("view_count") + private Integer viewCount; + + @TableField("like_count") + private Integer likeCount; + + @TableField("favorite_count") + private Integer favoriteCount; + + @TableField("comment_count") + private Integer commentCount; + + @TableField("share_count") + private Integer shareCount; + + @TableField("original_image_url") + private String originalImageUrl; + + @TableField("voice_input_url") + private String voiceInputUrl; + + @TableField("text_input") + private String textInput; + + @TableField("ai_meta") + private String aiMeta; + + @TableField("publish_time") + private LocalDateTime publishTime; + + @TableLogic(value = "0", delval = "1") + @TableField("is_deleted") + private Integer isDeleted; + + @TableField("create_time") + private LocalDateTime createTime; + + @TableField("modify_time") + private LocalDateTime modifyTime; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/UserWorkPage.java b/java-backend/src/main/java/com/lesingle/creation/entity/UserWorkPage.java new file mode 100644 index 0000000..14d7f21 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/UserWorkPage.java @@ -0,0 +1,32 @@ +package com.lesingle.creation.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 用户作品分页(绘本页) + */ +@Data +@TableName("user_work_pages") +public class UserWorkPage { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("work_id") + private Long workId; + + @TableField("page_no") + private Integer pageNo; + + @TableField("image_url") + private String imageUrl; + + private String text; + + @TableField("audio_url") + private String audioUrl; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/WorkTag.java b/java-backend/src/main/java/com/lesingle/creation/entity/WorkTag.java new file mode 100644 index 0000000..55b6cdf --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/WorkTag.java @@ -0,0 +1,37 @@ +package com.lesingle.creation.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 作品标签(全局,与 Nest work_tags 一致) + */ +@Data +@TableName("work_tags") +public class WorkTag { + + @TableId(type = IdType.AUTO) + private Long id; + + private String name; + + private String category; + + private Integer sort; + + private String status; + + @TableField("usage_count") + private Integer usageCount; + + @TableField("create_time") + private LocalDateTime createTime; + + @TableField("modify_time") + private LocalDateTime modifyTime; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/WorkTagRelation.java b/java-backend/src/main/java/com/lesingle/creation/entity/WorkTagRelation.java new file mode 100644 index 0000000..d13470a --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/WorkTagRelation.java @@ -0,0 +1,24 @@ +package com.lesingle.creation.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 作品与标签关联 + */ +@Data +@TableName("work_tag_relations") +public class WorkTagRelation { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("work_id") + private Long workId; + + @TableField("tag_id") + private Long tagId; +} diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/ContentReviewLogMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/ContentReviewLogMapper.java new file mode 100644 index 0000000..f9ca819 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/ContentReviewLogMapper.java @@ -0,0 +1,9 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.ContentReviewLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ContentReviewLogMapper extends BaseMapper { +} diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkMapper.java new file mode 100644 index 0000000..ec03390 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkMapper.java @@ -0,0 +1,9 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.UserWork; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserWorkMapper extends BaseMapper { +} diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkPageMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkPageMapper.java new file mode 100644 index 0000000..96b5fef --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/UserWorkPageMapper.java @@ -0,0 +1,9 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.UserWorkPage; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserWorkPageMapper extends BaseMapper { +} diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagMapper.java new file mode 100644 index 0000000..3421093 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagMapper.java @@ -0,0 +1,9 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.WorkTag; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WorkTagMapper extends BaseMapper { +} diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagRelationMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagRelationMapper.java new file mode 100644 index 0000000..eb7b4af --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/WorkTagRelationMapper.java @@ -0,0 +1,9 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.WorkTagRelation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WorkTagRelationMapper extends BaseMapper { +} diff --git a/java-backend/src/main/java/com/lesingle/creation/service/ContentReviewService.java b/java-backend/src/main/java/com/lesingle/creation/service/ContentReviewService.java new file mode 100644 index 0000000..f869f39 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/service/ContentReviewService.java @@ -0,0 +1,32 @@ +package com.lesingle.creation.service; + +import com.lesingle.creation.dto.contentreview.ApproveWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.RejectWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.TakedownWorkBodyDTO; + +import java.util.Map; + +/** + * 内容审核 / UGC 作品管理(对齐原 Nest content-review) + */ +public interface ContentReviewService { + + Map getManagementStats(Long tenantId, boolean superTenant); + + Map getWorkStats(Long tenantId, boolean superTenant); + + Map getWorkQueue(int page, int pageSize, String status, String keyword, + String startTime, String endTime, Long tenantId, boolean superTenant); + + Map getWorkDetail(Long workId, Long tenantId, boolean superTenant); + + void approve(Long workId, Long operatorId, Long tenantId, boolean superTenant, ApproveWorkBodyDTO body); + + void reject(Long workId, Long operatorId, Long tenantId, boolean superTenant, RejectWorkBodyDTO body); + + void takedown(Long workId, Long operatorId, Long tenantId, boolean superTenant, TakedownWorkBodyDTO body); + + void restore(Long workId, Long operatorId, Long tenantId, boolean superTenant); + + void toggleRecommend(Long workId, Long tenantId, boolean superTenant); +} diff --git a/java-backend/src/main/java/com/lesingle/creation/service/ContestJudgeService.java b/java-backend/src/main/java/com/lesingle/creation/service/ContestJudgeService.java index 88df179..0b5c168 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/ContestJudgeService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/ContestJudgeService.java @@ -1,5 +1,6 @@ package com.lesingle.creation.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.judge.*; import com.lesingle.creation.vo.judge.JudgeVO; @@ -52,6 +53,11 @@ public interface ContestJudgeService { */ List listByContest(Long contestId); + /** + * 分页查询评委(当前租户) + */ + Page pageQuery(JudgeQueryDTO queryDTO, Long tenantId); + /** * 冻结评委 * @param id 评委 ID diff --git a/java-backend/src/main/java/com/lesingle/creation/service/ContestNoticeService.java b/java-backend/src/main/java/com/lesingle/creation/service/ContestNoticeService.java index 579792b..a3d693f 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/ContestNoticeService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/ContestNoticeService.java @@ -1,5 +1,6 @@ package com.lesingle.creation.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.notice.*; import com.lesingle.creation.vo.notice.NoticeVO; @@ -52,4 +53,9 @@ public interface ContestNoticeService { * @return 公告列表 */ List listByContest(Long contestId); + + /** + * 分页查询公告(按当前租户下活动范围) + */ + Page pageQuery(NoticeQueryDTO queryDTO, Long tenantId); } diff --git a/java-backend/src/main/java/com/lesingle/creation/service/ContestReviewRuleService.java b/java-backend/src/main/java/com/lesingle/creation/service/ContestReviewRuleService.java index 3bf5b64..d5bbdac 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/ContestReviewRuleService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/ContestReviewRuleService.java @@ -1,5 +1,6 @@ package com.lesingle.creation.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.reviewrule.*; import com.lesingle.creation.vo.reviewrule.ReviewRuleVO; @@ -39,6 +40,11 @@ public interface ContestReviewRuleService { */ List list(Long tenantId); + /** + * 分页查询评审规则 + */ + Page pageQuery(ReviewRuleQueryDTO queryDTO, Long tenantId); + /** * 查询可选的评审规则列表(仅返回有效规则) * @param tenantId 租户 ID diff --git a/java-backend/src/main/java/com/lesingle/creation/service/ContestService.java b/java-backend/src/main/java/com/lesingle/creation/service/ContestService.java index af6eb53..44d17e7 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/ContestService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/ContestService.java @@ -15,10 +15,11 @@ public interface ContestService extends IService { * 创建竞赛 * * @param dto 创建竞赛 DTO - * @param creatorId 创建人 ID + * @param tenantId 租户 ID + * @param creatorId 创建人用户 ID * @return 竞赛详情 VO */ - ContestDetailVO create(CreateContestDTO dto, Long creatorId); + ContestDetailVO create(CreateContestDTO dto, Long tenantId, Long creatorId); /** * 分页查询竞赛列表 diff --git a/java-backend/src/main/java/com/lesingle/creation/service/SchoolService.java b/java-backend/src/main/java/com/lesingle/creation/service/SchoolService.java index 56710e8..fd3dec4 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/SchoolService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/SchoolService.java @@ -25,7 +25,7 @@ public interface SchoolService extends IService { * 根据租户 ID 查询学校 * * @param tenantId 租户 ID - * @return 学校 VO + * @return 学校 VO;尚未创建时返回 {@code null} */ SchoolVO getByTenantId(Long tenantId); diff --git a/java-backend/src/main/java/com/lesingle/creation/service/WorkTagService.java b/java-backend/src/main/java/com/lesingle/creation/service/WorkTagService.java new file mode 100644 index 0000000..7ad378a --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/service/WorkTagService.java @@ -0,0 +1,25 @@ +package com.lesingle.creation.service; + +import com.lesingle.creation.dto.tags.WorkTagCreateDTO; +import com.lesingle.creation.dto.tags.WorkTagUpdateDTO; +import com.lesingle.creation.entity.WorkTag; + +import java.util.List; + +/** + * 作品标签管理(对齐原 Nest tags) + */ +public interface WorkTagService { + + List listAll(); + + WorkTag create(WorkTagCreateDTO dto); + + WorkTag update(Long id, WorkTagUpdateDTO dto); + + void remove(Long id); + + WorkTag toggleStatus(Long id); + + List listCategories(); +} diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContentReviewServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContentReviewServiceImpl.java new file mode 100644 index 0000000..b940ffe --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContentReviewServiceImpl.java @@ -0,0 +1,449 @@ +package com.lesingle.creation.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.lesingle.creation.common.exception.BusinessException; +import com.lesingle.creation.dto.contentreview.ApproveWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.RejectWorkBodyDTO; +import com.lesingle.creation.dto.contentreview.TakedownWorkBodyDTO; +import com.lesingle.creation.entity.*; +import com.lesingle.creation.mapper.*; +import com.lesingle.creation.service.ContentReviewService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 内容审核实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ContentReviewServiceImpl implements ContentReviewService { + + private final UserWorkMapper userWorkMapper; + private final UserWorkPageMapper userWorkPageMapper; + private final WorkTagRelationMapper workTagRelationMapper; + private final WorkTagMapper workTagMapper; + private final ContentReviewLogMapper contentReviewLogMapper; + private final UserMapper userMapper; + + @Override + public Map getManagementStats(Long tenantId, boolean superTenant) { + LocalDateTime start = LocalDate.now().atStartOfDay(); + + LambdaQueryWrapper pub = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant) + .eq(UserWork::getStatus, "published"); + long total = userWorkMapper.selectCount(pub); + + LambdaQueryWrapper todayNewQ = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant) + .eq(UserWork::getStatus, "published") + .ge(UserWork::getPublishTime, start); + long todayNew = userWorkMapper.selectCount(todayNewQ); + + QueryWrapper sumQ = new QueryWrapper<>(); + sumQ.select("COALESCE(SUM(view_count),0) AS totalViews"); + sumQ.eq("status", "published"); + if (!superTenant) { + sumQ.eq("tenant_id", tenantId); + } + List> sumRows = userWorkMapper.selectMaps(sumQ); + long totalViews = 0L; + if (!sumRows.isEmpty()) { + Object v = sumRows.get(0).get("totalViews"); + if (v == null) { + v = sumRows.get(0).values().stream().findFirst().orElse(0); + } + if (v instanceof Number) { + totalViews = ((Number) v).longValue(); + } + } + + LambdaQueryWrapper downQ = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant) + .eq(UserWork::getStatus, "taken_down"); + long takenDown = userWorkMapper.selectCount(downQ); + + Map m = new LinkedHashMap<>(); + m.put("total", total); + m.put("todayNew", todayNew); + m.put("totalViews", totalViews); + m.put("takenDown", takenDown); + return m; + } + + @Override + public Map getWorkStats(Long tenantId, boolean superTenant) { + LocalDateTime start = LocalDate.now().atStartOfDay(); + + LambdaQueryWrapper pend = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant) + .eq(UserWork::getStatus, "pending_review"); + long pending = userWorkMapper.selectCount(pend); + + LambdaQueryWrapper logBase = logTenantScope( + Wrappers.lambdaQuery(ContentReviewLog.class), tenantId, superTenant) + .eq(ContentReviewLog::getTargetType, "work") + .ge(ContentReviewLog::getCreateTime, start); + long todayReviewed = contentReviewLogMapper.selectCount(logBase); + + long todayApproved = contentReviewLogMapper.selectCount( + logTenantScope(Wrappers.lambdaQuery(ContentReviewLog.class), tenantId, superTenant) + .eq(ContentReviewLog::getTargetType, "work") + .eq(ContentReviewLog::getAction, "approve") + .ge(ContentReviewLog::getCreateTime, start)); + + long todayRejected = contentReviewLogMapper.selectCount( + logTenantScope(Wrappers.lambdaQuery(ContentReviewLog.class), tenantId, superTenant) + .eq(ContentReviewLog::getTargetType, "work") + .eq(ContentReviewLog::getAction, "reject") + .ge(ContentReviewLog::getCreateTime, start)); + + Map m = new LinkedHashMap<>(); + m.put("pending", pending); + m.put("todayReviewed", todayReviewed); + m.put("todayApproved", todayApproved); + m.put("todayRejected", todayRejected); + return m; + } + + @Override + public Map getWorkQueue(int page, int pageSize, String status, String keyword, + String startTime, String endTime, Long tenantId, boolean superTenant) { + LambdaQueryWrapper w = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant); + + if (StringUtils.hasText(status)) { + if (status.contains(",")) { + List sts = Arrays.stream(status.split(",")) + .map(String::trim) + .filter(StringUtils::hasText) + .collect(Collectors.toList()); + if (!sts.isEmpty()) { + w.in(UserWork::getStatus, sts); + } + } else { + w.eq(UserWork::getStatus, status.trim()); + } + } else { + w.in(UserWork::getStatus, "pending_review", "published", "rejected", "taken_down"); + } + + if (StringUtils.hasText(startTime)) { + try { + w.ge(UserWork::getCreateTime, LocalDate.parse(startTime.trim()).atStartOfDay()); + } catch (Exception e) { + log.warn("解析 startTime 失败: {}", startTime); + } + } + if (StringUtils.hasText(endTime)) { + try { + w.le(UserWork::getCreateTime, LocalDate.parse(endTime.trim()).atTime(23, 59, 59)); + } catch (Exception e) { + log.warn("解析 endTime 失败: {}", endTime); + } + } + + if (StringUtils.hasText(keyword)) { + String kw = keyword.trim(); + LambdaQueryWrapper uw = Wrappers.lambdaQuery(User.class).like(User::getNickname, kw); + if (!superTenant) { + uw.eq(User::getTenantId, tenantId); + } + List hitUsers = userMapper.selectList(uw); + List uids = hitUsers.stream().map(User::getId).collect(Collectors.toList()); + w.and(q -> { + q.like(UserWork::getTitle, kw); + if (!uids.isEmpty()) { + q.or().in(UserWork::getUserId, uids); + } + }); + } + + w.orderByDesc(UserWork::getCreateTime); + Page pg = new Page<>(page, pageSize); + Page result = userWorkMapper.selectPage(pg, w); + + List records = result.getRecords(); + Set userIds = records.stream().map(UserWork::getUserId).collect(Collectors.toSet()); + Map userMap = loadUsers(userIds); + Set workIds = records.stream().map(UserWork::getId).collect(Collectors.toSet()); + Map pageCounts = countPagesByWorkIds(workIds); + Map>> tagsByWork = loadTagsByWorkIds(workIds); + + List> list = new ArrayList<>(); + for (UserWork uw : records) { + list.add(toListItemMap(uw, userMap.get(uw.getUserId()), + pageCounts.getOrDefault(uw.getId(), 0), + tagsByWork.getOrDefault(uw.getId(), Collections.emptyList()))); + } + + Map body = new LinkedHashMap<>(); + body.put("list", list); + body.put("total", result.getTotal()); + body.put("page", result.getCurrent()); + body.put("pageSize", result.getSize()); + return body; + } + + @Override + public Map getWorkDetail(Long workId, Long tenantId, boolean superTenant) { + UserWork work = userWorkMapper.selectById(workId); + if (work == null) { + throw new BusinessException("作品不存在"); + } + if (!superTenant && !work.getTenantId().equals(tenantId)) { + throw new BusinessException("作品不存在"); + } + + User creator = userMapper.selectById(work.getUserId()); + List pages = userWorkPageMapper.selectList( + Wrappers.lambdaQuery(UserWorkPage.class) + .eq(UserWorkPage::getWorkId, workId) + .orderByAsc(UserWorkPage::getPageNo)); + + int pageCount = pages.size(); + List> tagList = loadTagsByWorkIds(Collections.singleton(workId)).getOrDefault(workId, Collections.emptyList()); + + Map detail = toDetailMap(work, creator, pages, tagList, pageCount); + return detail; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(Long workId, Long operatorId, Long tenantId, boolean superTenant, ApproveWorkBodyDTO body) { + UserWork work = requireWork(workId, tenantId, superTenant); + String note = body != null ? body.getNote() : null; + LocalDateTime now = LocalDateTime.now(); + UserWork upd = new UserWork(); + upd.setId(workId); + upd.setStatus("published"); + upd.setReviewTime(now); + upd.setReviewerId(operatorId); + upd.setReviewNote(note); + upd.setPublishTime(now); + userWorkMapper.updateById(upd); + insertLog(work.getTenantId(), workId, "approve", null, note, operatorId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void reject(Long workId, Long operatorId, Long tenantId, boolean superTenant, RejectWorkBodyDTO body) { + UserWork work = requireWork(workId, tenantId, superTenant); + LocalDateTime now = LocalDateTime.now(); + UserWork upd = new UserWork(); + upd.setId(workId); + upd.setStatus("rejected"); + upd.setReviewTime(now); + upd.setReviewerId(operatorId); + upd.setReviewNote(body.getReason()); + userWorkMapper.updateById(upd); + insertLog(work.getTenantId(), workId, "reject", body.getReason(), body.getNote(), operatorId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void takedown(Long workId, Long operatorId, Long tenantId, boolean superTenant, TakedownWorkBodyDTO body) { + UserWork work = requireWork(workId, tenantId, superTenant); + UserWork upd = new UserWork(); + upd.setId(workId); + upd.setStatus("taken_down"); + upd.setReviewNote(body.getReason()); + userWorkMapper.updateById(upd); + insertLog(work.getTenantId(), workId, "takedown", body.getReason(), null, operatorId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restore(Long workId, Long operatorId, Long tenantId, boolean superTenant) { + UserWork work = requireWork(workId, tenantId, superTenant); + UserWork upd = new UserWork(); + upd.setId(workId); + upd.setStatus("published"); + userWorkMapper.updateById(upd); + insertLog(work.getTenantId(), workId, "restore", null, null, operatorId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void toggleRecommend(Long workId, Long tenantId, boolean superTenant) { + UserWork work = requireWork(workId, tenantId, superTenant); + UserWork upd = new UserWork(); + upd.setId(workId); + upd.setIsRecommended(!Boolean.TRUE.equals(work.getIsRecommended())); + userWorkMapper.updateById(upd); + } + + private UserWork requireWork(Long workId, Long tenantId, boolean superTenant) { + UserWork work = userWorkMapper.selectById(workId); + if (work == null) { + throw new BusinessException("作品不存在"); + } + if (!superTenant && !work.getTenantId().equals(tenantId)) { + throw new BusinessException("作品不存在"); + } + return work; + } + + private void insertLog(Long tenantId, Long workId, String action, String reason, String note, Long operatorId) { + ContentReviewLog log = new ContentReviewLog(); + log.setTenantId(tenantId); + log.setTargetType("work"); + log.setTargetId(workId); + log.setWorkId(workId); + log.setAction(action); + log.setReason(reason); + log.setNote(note); + log.setOperatorId(operatorId); + log.setCreateTime(LocalDateTime.now()); + contentReviewLogMapper.insert(log); + } + + private static LambdaQueryWrapper tenantScope(LambdaQueryWrapper w, Long tenantId, boolean superTenant) { + if (!superTenant) { + w.eq(UserWork::getTenantId, tenantId); + } + return w; + } + + private static LambdaQueryWrapper logTenantScope(LambdaQueryWrapper w, + Long tenantId, boolean superTenant) { + if (!superTenant) { + w.eq(ContentReviewLog::getTenantId, tenantId); + } + return w; + } + + private Map loadUsers(Set ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyMap(); + } + List users = userMapper.selectList(Wrappers.lambdaQuery(User.class).in(User::getId, ids)); + return users.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a)); + } + + private Map countPagesByWorkIds(Set workIds) { + Map map = new HashMap<>(); + if (workIds == null || workIds.isEmpty()) { + return map; + } + for (Long wid : workIds) { + long c = userWorkPageMapper.selectCount( + Wrappers.lambdaQuery(UserWorkPage.class).eq(UserWorkPage::getWorkId, wid)); + map.put(wid, (int) c); + } + return map; + } + + private Map>> loadTagsByWorkIds(Set workIds) { + Map>> out = new HashMap<>(); + if (workIds == null || workIds.isEmpty()) { + return out; + } + List rels = workTagRelationMapper.selectList( + Wrappers.lambdaQuery(WorkTagRelation.class).in(WorkTagRelation::getWorkId, workIds)); + if (rels.isEmpty()) { + return out; + } + Set tagIds = rels.stream().map(WorkTagRelation::getTagId).collect(Collectors.toSet()); + List tags = workTagMapper.selectList(Wrappers.lambdaQuery(WorkTag.class).in(WorkTag::getId, tagIds)); + Map tagMap = tags.stream().collect(Collectors.toMap(WorkTag::getId, t -> t, (a, b) -> a)); + + for (WorkTagRelation rel : rels) { + WorkTag t = tagMap.get(rel.getTagId()); + if (t == null) { + continue; + } + Map tagMini = new LinkedHashMap<>(); + tagMini.put("id", t.getId()); + tagMini.put("name", t.getName()); + Map wrap = new LinkedHashMap<>(); + wrap.put("tag", tagMini); + out.computeIfAbsent(rel.getWorkId(), k -> new ArrayList<>()).add(wrap); + } + return out; + } + + private Map toListItemMap(UserWork uw, User creator, int pageCount, List> tags) { + Map m = workBaseMap(uw); + m.put("creator", creatorMini(creator)); + Map cnt = new LinkedHashMap<>(); + cnt.put("pages", pageCount); + m.put("_count", cnt); + m.put("tags", tags); + return m; + } + + private Map toDetailMap(UserWork uw, User creator, List pages, + List> tags, int pageCount) { + Map m = workBaseMap(uw); + m.put("creator", creatorMini(creator)); + List> pageList = new ArrayList<>(); + for (UserWorkPage p : pages) { + Map pm = new LinkedHashMap<>(); + pm.put("id", p.getId()); + pm.put("pageNo", p.getPageNo()); + pm.put("imageUrl", p.getImageUrl()); + pm.put("text", p.getText()); + pm.put("audioUrl", p.getAudioUrl()); + pageList.add(pm); + } + m.put("pages", pageList); + m.put("tags", tags); + Map cnt = new LinkedHashMap<>(); + cnt.put("pages", pageCount); + cnt.put("likes", uw.getLikeCount() != null ? uw.getLikeCount() : 0); + cnt.put("favorites", uw.getFavoriteCount() != null ? uw.getFavoriteCount() : 0); + cnt.put("comments", uw.getCommentCount() != null ? uw.getCommentCount() : 0); + m.put("_count", cnt); + return m; + } + + private Map workBaseMap(UserWork uw) { + Map m = new LinkedHashMap<>(); + m.put("id", uw.getId()); + m.put("userId", uw.getUserId()); + m.put("title", uw.getTitle()); + m.put("coverUrl", uw.getCoverUrl()); + m.put("description", uw.getDescription()); + m.put("visibility", uw.getVisibility()); + m.put("status", uw.getStatus()); + m.put("reviewNote", uw.getReviewNote()); + m.put("reviewTime", uw.getReviewTime()); + m.put("reviewerId", uw.getReviewerId()); + m.put("isRecommended", uw.getIsRecommended()); + m.put("viewCount", uw.getViewCount()); + m.put("likeCount", uw.getLikeCount()); + m.put("favoriteCount", uw.getFavoriteCount()); + m.put("commentCount", uw.getCommentCount()); + m.put("shareCount", uw.getShareCount()); + m.put("publishTime", uw.getPublishTime()); + m.put("createTime", uw.getCreateTime()); + m.put("modifyTime", uw.getModifyTime()); + return m; + } + + private Map creatorMini(User u) { + Map m = new LinkedHashMap<>(); + if (u == null) { + m.put("id", null); + m.put("nickname", null); + m.put("avatar", null); + m.put("username", null); + m.put("userType", null); + return m; + } + m.put("id", u.getId()); + m.put("nickname", u.getNickname()); + m.put("avatar", u.getAvatar()); + m.put("username", u.getUsername()); + m.put("userType", u.getUserType()); + return m; + } +} diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestJudgeServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestJudgeServiceImpl.java index c4bb32f..354ee9d 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestJudgeServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestJudgeServiceImpl.java @@ -1,7 +1,9 @@ package com.lesingle.creation.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.judge.CreateJudgeDTO; +import com.lesingle.creation.dto.judge.JudgeQueryDTO; import com.lesingle.creation.dto.judge.UpdateJudgeDTO; import com.lesingle.creation.entity.Contest; import com.lesingle.creation.entity.ContestJudge; @@ -14,6 +16,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.util.List; import java.util.stream.Collectors; @@ -172,6 +175,26 @@ public class ContestJudgeServiceImpl implements ContestJudgeService { .collect(Collectors.toList()); } + @Override + public Page pageQuery(JudgeQueryDTO queryDTO, Long tenantId) { + Page page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); + LambdaQueryWrapper w = new LambdaQueryWrapper() + .eq(ContestJudge::getTenantId, tenantId) + .eq(ContestJudge::getValidState, 1) + .eq(ContestJudge::getDeleted, 0); + if (queryDTO.getContestId() != null) { + w.eq(ContestJudge::getContestId, queryDTO.getContestId()); + } + if (StringUtils.hasText(queryDTO.getJudgeName())) { + w.like(ContestJudge::getJudgeName, queryDTO.getJudgeName()); + } + w.orderByDesc(ContestJudge::getCreateTime); + Page result = contestJudgeMapper.selectPage(page, w); + Page voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(result.getRecords().stream().map(this::convertToVO).collect(Collectors.toList())); + return voPage; + } + // ========== 转换方法 ========== private JudgeVO convertToVO(ContestJudge judge) { diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestNoticeServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestNoticeServiceImpl.java index 6472325..f53c32c 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestNoticeServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestNoticeServiceImpl.java @@ -1,7 +1,9 @@ package com.lesingle.creation.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.notice.CreateNoticeDTO; +import com.lesingle.creation.dto.notice.NoticeQueryDTO; import com.lesingle.creation.dto.notice.UpdateNoticeDTO; import com.lesingle.creation.entity.Contest; import com.lesingle.creation.entity.ContestNotice; @@ -16,9 +18,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.springframework.util.StringUtils; + /** * 竞赛公告服务实现类 */ @@ -127,6 +132,43 @@ public class ContestNoticeServiceImpl implements ContestNoticeService { .collect(Collectors.toList()); } + @Override + public Page pageQuery(NoticeQueryDTO queryDTO, Long tenantId) { + LambdaQueryWrapper w = new LambdaQueryWrapper() + .eq(ContestNotice::getValidState, 1) + .eq(ContestNotice::getDeleted, 0); + if (queryDTO.getContestId() != null) { + Contest contest = contestMapper.selectById(queryDTO.getContestId()); + if (contest == null || !contest.getTenantId().equals(tenantId)) { + throw new BusinessException("活动不存在或不属于当前租户"); + } + w.eq(ContestNotice::getContestId, queryDTO.getContestId()); + } else { + List contestIds = contestMapper.selectList( + new LambdaQueryWrapper() + .eq(Contest::getTenantId, tenantId) + .select(Contest::getId)) + .stream() + .map(Contest::getId) + .collect(Collectors.toList()); + if (contestIds.isEmpty()) { + Page empty = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize(), 0); + empty.setRecords(Collections.emptyList()); + return empty; + } + w.in(ContestNotice::getContestId, contestIds); + } + if (StringUtils.hasText(queryDTO.getTitleKeyword())) { + w.like(ContestNotice::getTitle, queryDTO.getTitleKeyword()); + } + w.orderByDesc(ContestNotice::getPriority).orderByDesc(ContestNotice::getPublishTime); + Page page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); + Page result = contestNoticeMapper.selectPage(page, w); + Page voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(result.getRecords().stream().map(this::convertToVO).collect(Collectors.toList())); + return voPage; + } + // ========== 转换方法 ========== private NoticeVO convertToVO(ContestNotice notice) { diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestReviewRuleServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestReviewRuleServiceImpl.java index 1bb5052..9d5d374 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestReviewRuleServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestReviewRuleServiceImpl.java @@ -1,8 +1,10 @@ package com.lesingle.creation.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.lesingle.creation.dto.reviewrule.CreateReviewDimensionDTO; import com.lesingle.creation.dto.reviewrule.CreateReviewRuleDTO; +import com.lesingle.creation.dto.reviewrule.ReviewRuleQueryDTO; import com.lesingle.creation.entity.ContestReviewDimension; import com.lesingle.creation.entity.ContestReviewRule; import com.lesingle.creation.common.exception.BusinessException; @@ -15,6 +17,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.util.List; import java.util.stream.Collectors; @@ -42,6 +45,7 @@ public class ContestReviewRuleServiceImpl implements ContestReviewRuleService { if (creatorId != null) { rule.setCreateBy(creatorId.toString()); } + rule.setValidState(1); reviewRuleMapper.insert(rule); log.info("创建评审规则成功,ID={}", rule.getId()); @@ -81,6 +85,22 @@ public class ContestReviewRuleServiceImpl implements ContestReviewRuleService { .collect(Collectors.toList()); } + @Override + public Page pageQuery(ReviewRuleQueryDTO queryDTO, Long tenantId) { + Page page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); + LambdaQueryWrapper w = new LambdaQueryWrapper() + .eq(ContestReviewRule::getTenantId, tenantId) + .eq(ContestReviewRule::getDeleted, 0); + if (StringUtils.hasText(queryDTO.getRuleName())) { + w.like(ContestReviewRule::getRuleName, queryDTO.getRuleName()); + } + w.orderByDesc(ContestReviewRule::getCreateTime); + Page result = reviewRuleMapper.selectPage(page, w); + Page voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(result.getRecords().stream().map(this::convertToVO).collect(Collectors.toList())); + return voPage; + } + @Override public List listForSelect(Long tenantId) { List rules = reviewRuleMapper.selectList(new LambdaQueryWrapper() diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestServiceImpl.java index 13b8108..7ae7649 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/ContestServiceImpl.java @@ -41,7 +41,7 @@ public class ContestServiceImpl extends ServiceImpl impl @Override @Transactional(rollbackFor = Exception.class) - public ContestDetailVO create(CreateContestDTO dto, Long creatorId) { + public ContestDetailVO create(CreateContestDTO dto, Long tenantId, Long creatorId) { log.info("开始创建竞赛,竞赛名称:{}", dto.getContestName()); // 检查竞赛名称是否已存在 @@ -85,7 +85,10 @@ public class ContestServiceImpl extends ServiceImpl impl contest.setResultState("unpublished"); contest.setRequireAudit(dto.getRequireAudit() != null ? dto.getRequireAudit() : true); contest.setSubmitRule(dto.getSubmitRule() != null ? dto.getSubmitRule() : "once"); - contest.setCreateBy(String.valueOf(creatorId)); + contest.setTenantId(tenantId); + if (creatorId != null) { + contest.setCreateBy(String.valueOf(creatorId)); + } contestMapper.insert(contest); log.info("竞赛创建成功,竞赛 ID: {}", contest.getId()); diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/SchoolServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/SchoolServiceImpl.java index e4ca6bf..d6c670e 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/SchoolServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/SchoolServiceImpl.java @@ -59,7 +59,8 @@ public class SchoolServiceImpl extends ServiceImpl impleme School school = schoolMapper.selectByTenantId(tenantId); if (school == null) { - throw new BusinessException("学校信息不存在"); + log.debug("当前租户尚未创建学校,租户 ID: {}", tenantId); + return null; } return convertToVO(school); diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/WorkTagServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/WorkTagServiceImpl.java new file mode 100644 index 0000000..7b4bef0 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/WorkTagServiceImpl.java @@ -0,0 +1,125 @@ +package com.lesingle.creation.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.lesingle.creation.common.exception.BusinessException; +import com.lesingle.creation.dto.tags.WorkTagCreateDTO; +import com.lesingle.creation.dto.tags.WorkTagUpdateDTO; +import com.lesingle.creation.entity.WorkTag; +import com.lesingle.creation.mapper.WorkTagMapper; +import com.lesingle.creation.service.WorkTagService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 作品标签服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WorkTagServiceImpl implements WorkTagService { + + private final WorkTagMapper workTagMapper; + + @Override + public List listAll() { + return workTagMapper.selectList( + Wrappers.lambdaQuery(WorkTag.class) + .orderByAsc(WorkTag::getCategory) + .orderByAsc(WorkTag::getSort)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public WorkTag create(WorkTagCreateDTO dto) { + Long dup = workTagMapper.selectCount( + Wrappers.lambdaQuery(WorkTag.class).eq(WorkTag::getName, dto.getName())); + if (dup != null && dup > 0) { + throw new BusinessException("标签名已存在"); + } + WorkTag tag = new WorkTag(); + tag.setName(dto.getName()); + tag.setCategory(dto.getCategory()); + tag.setSort(dto.getSort() != null ? dto.getSort() : 0); + tag.setStatus("enabled"); + tag.setUsageCount(0); + workTagMapper.insert(tag); + return workTagMapper.selectById(tag.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public WorkTag update(Long id, WorkTagUpdateDTO dto) { + WorkTag tag = workTagMapper.selectById(id); + if (tag == null) { + throw new BusinessException("标签不存在"); + } + if (StringUtils.hasText(dto.getName()) && !dto.getName().equals(tag.getName())) { + Long dup = workTagMapper.selectCount( + Wrappers.lambdaQuery(WorkTag.class) + .eq(WorkTag::getName, dto.getName()) + .ne(WorkTag::getId, id)); + if (dup != null && dup > 0) { + throw new BusinessException("标签名已存在"); + } + tag.setName(dto.getName()); + } + if (dto.getCategory() != null) { + tag.setCategory(dto.getCategory()); + } + if (dto.getSort() != null) { + tag.setSort(dto.getSort()); + } + if (StringUtils.hasText(dto.getStatus())) { + tag.setStatus(dto.getStatus()); + } + workTagMapper.updateById(tag); + return workTagMapper.selectById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void remove(Long id) { + WorkTag tag = workTagMapper.selectById(id); + if (tag == null) { + throw new BusinessException("标签不存在"); + } + if (tag.getUsageCount() != null && tag.getUsageCount() > 0) { + throw new BusinessException("该标签已被使用,无法删除,请先禁用"); + } + workTagMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public WorkTag toggleStatus(Long id) { + WorkTag tag = workTagMapper.selectById(id); + if (tag == null) { + throw new BusinessException("标签不存在"); + } + tag.setStatus("enabled".equals(tag.getStatus()) ? "disabled" : "enabled"); + workTagMapper.updateById(tag); + return workTagMapper.selectById(id); + } + + @Override + public List listCategories() { + List tags = workTagMapper.selectList( + Wrappers.lambdaQuery(WorkTag.class) + .isNotNull(WorkTag::getCategory) + .select(WorkTag::getCategory)); + return tags.stream() + .map(WorkTag::getCategory) + .filter(Objects::nonNull) + .map(String::trim) + .filter(StringUtils::hasText) + .distinct() + .collect(Collectors.toList()); + } +} diff --git a/java-backend/src/main/resources/db/migration/V23__align_t_biz_contest_tenant_audit.sql b/java-backend/src/main/resources/db/migration/V23__align_t_biz_contest_tenant_audit.sql new file mode 100644 index 0000000..176fbb4 --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V23__align_t_biz_contest_tenant_audit.sql @@ -0,0 +1,64 @@ +-- 对齐 t_biz_contest 与 BaseEntity / 多租户(幂等,可安全重复执行) +-- 若 flyway_schema_history 中本版本曾标记失败,请先执行 repair 或删除该失败记录后再迁移 + +SET @db := DATABASE(); + +-- tenant_id +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest' AND COLUMN_NAME = 'tenant_id' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest` ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT ''租户 ID'' AFTER `visibility`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +-- 索引 +SET @idx_exists := ( + SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest' AND INDEX_NAME = 'idx_tenant_id' +); +SET @stmt := IF(@idx_exists = 0, + 'ALTER TABLE `t_biz_contest` ADD KEY `idx_tenant_id` (`tenant_id`)', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +-- creator -> create_by +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest' AND COLUMN_NAME = 'creator' +); +SET @stmt := IF(@col_exists > 0, + 'ALTER TABLE `t_biz_contest` CHANGE COLUMN `creator` `create_by` VARCHAR(50) DEFAULT NULL COMMENT ''创建人账号''', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +-- modifier -> update_by +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest' AND COLUMN_NAME = 'modifier' +); +SET @stmt := IF(@col_exists > 0, + 'ALTER TABLE `t_biz_contest` CHANGE COLUMN `modifier` `update_by` VARCHAR(50) DEFAULT NULL COMMENT ''更新人账号''', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +-- modify_time -> update_time +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest' AND COLUMN_NAME = 'modify_time' +); +SET @stmt := IF(@col_exists > 0, + 'ALTER TABLE `t_biz_contest` CHANGE COLUMN `modify_time` `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ''更新时间''', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; diff --git a/java-backend/src/main/resources/db/migration/V25__add_valid_state_t_biz_contest_work.sql b/java-backend/src/main/resources/db/migration/V25__add_valid_state_t_biz_contest_work.sql new file mode 100644 index 0000000..ca6ce69 --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V25__add_valid_state_t_biz_contest_work.sql @@ -0,0 +1,25 @@ +-- 竞赛作品表补充 valid_state(V10 建表未包含,与实体 ContestWork 一致) + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_work' AND COLUMN_NAME = 'valid_state' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_work` ADD COLUMN `valid_state` TINYINT NOT NULL DEFAULT 1 COMMENT ''有效状态:1-有效,2-失效'' AFTER `certificate_url`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @idx_exists := ( + SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_work' AND INDEX_NAME = 'idx_valid_state' +); +SET @stmt := IF(@idx_exists = 0, + 'ALTER TABLE `t_biz_contest_work` ADD KEY `idx_valid_state` (`valid_state`)', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; diff --git a/java-backend/src/main/resources/db/migration/V26__align_t_biz_contest_judge.sql b/java-backend/src/main/resources/db/migration/V26__align_t_biz_contest_judge.sql new file mode 100644 index 0000000..85ff1ef --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V26__align_t_biz_contest_judge.sql @@ -0,0 +1,36 @@ +-- 评委表与实体对齐:补充 valid_state、weight(V10 建表未包含) + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_judge' AND COLUMN_NAME = 'valid_state' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_judge` ADD COLUMN `valid_state` TINYINT NOT NULL DEFAULT 1 COMMENT ''有效状态:1-有效,2-失效'' AFTER `status`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @idx_exists := ( + SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_judge' AND INDEX_NAME = 'idx_valid_state' +); +SET @stmt := IF(@idx_exists = 0, + 'ALTER TABLE `t_biz_contest_judge` ADD KEY `idx_valid_state` (`valid_state`)', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_judge' AND COLUMN_NAME = 'weight' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_judge` ADD COLUMN `weight` DECIMAL(10,2) DEFAULT NULL COMMENT ''权重'' AFTER `valid_state`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; diff --git a/java-backend/src/main/resources/db/migration/V27__align_t_biz_contest_notice.sql b/java-backend/src/main/resources/db/migration/V27__align_t_biz_contest_notice.sql new file mode 100644 index 0000000..20b0a9b --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V27__align_t_biz_contest_notice.sql @@ -0,0 +1,47 @@ +-- 公告表与实体对齐:补充 valid_state、notice_type、审计字段(V11 建表不完整) + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_notice' AND COLUMN_NAME = 'valid_state' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_notice` ADD COLUMN `valid_state` TINYINT NOT NULL DEFAULT 1 COMMENT ''有效状态:1-有效,2-失效'' AFTER `publish_time`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_notice' AND COLUMN_NAME = 'notice_type' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_notice` ADD COLUMN `notice_type` VARCHAR(50) DEFAULT NULL COMMENT ''公告类型'' AFTER `content`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_notice' AND COLUMN_NAME = 'update_by' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_notice` ADD COLUMN `update_by` VARCHAR(50) DEFAULT NULL COMMENT ''更新人'' AFTER `create_time`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_notice' AND COLUMN_NAME = 'update_time' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_notice` ADD COLUMN `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ''更新时间'' AFTER `update_by`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; diff --git a/java-backend/src/main/resources/db/migration/V28__add_valid_state_t_biz_contest_review_rule.sql b/java-backend/src/main/resources/db/migration/V28__add_valid_state_t_biz_contest_review_rule.sql new file mode 100644 index 0000000..1324c83 --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V28__add_valid_state_t_biz_contest_review_rule.sql @@ -0,0 +1,14 @@ +-- 评审规则表补充 valid_state(实体 ContestReviewRule、listForSelect 使用) + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_biz_contest_review_rule' AND COLUMN_NAME = 'valid_state' +); +SET @stmt := IF(@col_exists = 0, + 'ALTER TABLE `t_biz_contest_review_rule` ADD COLUMN `valid_state` TINYINT NOT NULL DEFAULT 1 COMMENT ''有效状态:1-有效,2-失效'' AFTER `description`', + 'SELECT 1'); +PREPARE ps FROM @stmt; +EXECUTE ps; +DEALLOCATE PREPARE ps; diff --git a/java-backend/src/main/resources/db/migration/V29__create_ugc_user_work_tags_review.sql b/java-backend/src/main/resources/db/migration/V29__create_ugc_user_work_tags_review.sql new file mode 100644 index 0000000..e209a8b --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V29__create_ugc_user_work_tags_review.sql @@ -0,0 +1,88 @@ +-- UGC 用户作品、标签、审核日志(对齐原 Nest/Prisma 能力,供 java-backend 使用) + +CREATE TABLE IF NOT EXISTS `user_works` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` BIGINT NOT NULL COMMENT '租户 ID(与创作者一致,便于隔离)', + `user_id` BIGINT NOT NULL COMMENT '创作者 t_sys_user.id', + `title` VARCHAR(200) NOT NULL COMMENT '作品名称', + `cover_url` TEXT DEFAULT NULL COMMENT '封面 URL', + `description` TEXT DEFAULT NULL COMMENT '简介', + `visibility` VARCHAR(50) NOT NULL DEFAULT 'private' COMMENT '可见性:public/private/friends', + `status` VARCHAR(50) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending_review/published/rejected/taken_down', + `review_note` TEXT DEFAULT NULL COMMENT '审核备注/拒绝原因', + `review_time` DATETIME DEFAULT NULL COMMENT '审核时间', + `reviewer_id` BIGINT DEFAULT NULL COMMENT '审核人 ID', + `machine_review_result` VARCHAR(50) DEFAULT NULL COMMENT '机器预审:safe/suspicious', + `machine_review_note` TEXT DEFAULT NULL, + `is_recommended` TINYINT NOT NULL DEFAULT 0 COMMENT '是否推荐', + `view_count` INT NOT NULL DEFAULT 0, + `like_count` INT NOT NULL DEFAULT 0, + `favorite_count` INT NOT NULL DEFAULT 0, + `comment_count` INT NOT NULL DEFAULT 0, + `share_count` INT NOT NULL DEFAULT 0, + `original_image_url` TEXT DEFAULT NULL, + `voice_input_url` TEXT DEFAULT NULL, + `text_input` TEXT DEFAULT NULL, + `ai_meta` JSON DEFAULT NULL COMMENT 'AI 元数据', + `publish_time` DATETIME DEFAULT NULL COMMENT '发布时间', + `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除 0/1', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_tenant_id` (`tenant_id`), + KEY `idx_user_status` (`user_id`, `status`), + KEY `idx_status_publish` (`status`, `publish_time`), + KEY `idx_rec_publish` (`is_recommended`, `publish_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 UGC 作品'; + +CREATE TABLE IF NOT EXISTS `user_work_pages` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `work_id` BIGINT NOT NULL, + `page_no` INT NOT NULL COMMENT '页码从 1 开始', + `image_url` TEXT DEFAULT NULL, + `text` TEXT DEFAULT NULL, + `audio_url` TEXT DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_work_page` (`work_id`, `page_no`), + KEY `idx_work_id` (`work_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='绘本分页'; + +CREATE TABLE IF NOT EXISTS `work_tags` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(50) NOT NULL COMMENT '标签名', + `category` VARCHAR(50) DEFAULT NULL, + `sort` INT NOT NULL DEFAULT 0, + `status` VARCHAR(50) NOT NULL DEFAULT 'enabled' COMMENT 'enabled/disabled', + `usage_count` INT NOT NULL DEFAULT 0, + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_work_tags_name` (`name`), + KEY `idx_category_sort` (`category`, `sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作品标签'; + +CREATE TABLE IF NOT EXISTS `work_tag_relations` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `work_id` BIGINT NOT NULL, + `tag_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_work_tag` (`work_id`, `tag_id`), + KEY `idx_tag_id` (`tag_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作品-标签关联'; + +CREATE TABLE IF NOT EXISTS `content_review_logs` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `tenant_id` BIGINT NOT NULL COMMENT '冗余租户,便于统计', + `target_type` VARCHAR(50) NOT NULL DEFAULT 'work' COMMENT 'work/comment', + `target_id` BIGINT NOT NULL, + `work_id` BIGINT DEFAULT NULL, + `action` VARCHAR(50) NOT NULL COMMENT 'approve/reject/takedown/restore', + `reason` TEXT DEFAULT NULL, + `note` TEXT DEFAULT NULL, + `operator_id` BIGINT NOT NULL, + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_tenant_time` (`tenant_id`, `create_time`), + KEY `idx_work_id` (`work_id`), + KEY `idx_target` (`target_type`, `target_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='内容审核日志'; diff --git a/java-backend/src/main/resources/mapper/SysLogMapper.xml b/java-backend/src/main/resources/mapper/SysLogMapper.xml index e834a48..36e8337 100644 --- a/java-backend/src/main/resources/mapper/SysLogMapper.xml +++ b/java-backend/src/main/resources/mapper/SysLogMapper.xml @@ -26,7 +26,6 @@ COUNT(*) as total_count, COUNT(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL ${days} DAY) THEN 1 END) as recent_count FROM t_sys_log - WHERE deleted = 0 OR deleted IS NULL @@ -36,7 +35,6 @@ COUNT(*) as count FROM t_sys_log WHERE create_time >= DATE_SUB(NOW(), INTERVAL ${days} DAY) - AND (deleted = 0 OR deleted IS NULL) GROUP BY action ORDER BY count DESC LIMIT 10 @@ -49,7 +47,6 @@ COUNT(*) as count FROM t_sys_log WHERE create_time >= DATE_SUB(NOW(), INTERVAL ${days} DAY) - AND (deleted = 0 OR deleted IS NULL) GROUP BY DATE(create_time) ORDER BY date DESC @@ -58,7 +55,6 @@ DELETE FROM t_sys_log WHERE create_time < #{cutoffDate} - AND (deleted = 0 OR deleted IS NULL) diff --git a/java-frontend/src/api/public.ts b/java-frontend/src/api/public.ts index 4e1af53..a555486 100644 --- a/java-frontend/src/api/public.ts +++ b/java-frontend/src/api/public.ts @@ -1,72 +1,72 @@ -import axios from "axios" +import axios from "axios"; // 公众端专用 axios 实例 const publicApi = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || "/api", timeout: 15000, -}) +}); // 请求拦截器 publicApi.interceptors.request.use((config) => { - const token = localStorage.getItem("public_token") + const token = localStorage.getItem("public_token"); if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } - return config -}) + return config; +}); // 响应拦截器 publicApi.interceptors.response.use( (response) => response.data?.data ?? response.data, (error) => { if (error.response?.status === 401) { - localStorage.removeItem("public_token") - localStorage.removeItem("public_user") + localStorage.removeItem("public_token"); + localStorage.removeItem("public_user"); // 如果在公众端页面,跳转到公众端登录 if (window.location.pathname.startsWith("/p/")) { - window.location.href = "/p/login" + window.location.href = "/p/login"; } } - return Promise.reject(error) + return Promise.reject(error); }, -) +); // ==================== 认证 ==================== export interface PublicRegisterParams { - username: string - password: string - nickname: string - phone?: string - city?: string + username: string; + password: string; + nickname: string; + phone?: string; + city?: string; } export interface PublicLoginParams { - username: string - password: string + username: string; + password: string; } export interface PublicUser { - id: number - username: string - nickname: string - phone: string | null - city: string | null - avatar: string | null - tenantId: number - tenantCode: string - userSource: string - userType: "adult" | "child" - parentUserId: number | null - roles: string[] - permissions: string[] - children?: any[] - childrenCount?: number + id: number; + username: string; + nickname: string; + phone: string | null; + city: string | null; + avatar: string | null; + tenantId: number; + tenantCode: string; + userSource: string; + userType: "adult" | "child"; + parentUserId: number | null; + roles: string[]; + permissions: string[]; + children?: any[]; + childrenCount?: number; } export interface LoginResponse { - token: string - user: PublicUser + token: string; + user: PublicUser; } export const publicAuthApi = { @@ -75,42 +75,43 @@ export const publicAuthApi = { login: (data: PublicLoginParams): Promise => publicApi.post("/api/public/auth/login", data), -} +}; // ==================== 个人信息 ==================== export const publicProfileApi = { - getProfile: (): Promise => publicApi.get("/api/public/mine/profile"), + getProfile: (): Promise => + publicApi.get("/api/public/mine/profile"), updateProfile: (data: { - nickname?: string - city?: string - avatar?: string - gender?: string + nickname?: string; + city?: string; + avatar?: string; + gender?: string; }) => publicApi.put("/api/public/mine/profile", data), -} +}; // ==================== 子女管理 ==================== export interface Child { - id: number - parentId: number - name: string - gender: string | null - birthday: string | null - grade: string | null - city: string | null - schoolName: string | null - avatar: string | null + id: number; + parentId: number; + name: string; + gender: string | null; + birthday: string | null; + grade: string | null; + city: string | null; + schoolName: string | null; + avatar: string | null; } export interface CreateChildParams { - name: string - gender?: string - birthday?: string - grade?: string - city?: string - schoolName?: string + name: string; + gender?: string; + birthday?: string; + grade?: string; + city?: string; + schoolName?: string; } export const publicChildrenApi = { @@ -120,40 +121,40 @@ export const publicChildrenApi = { publicApi.post("/api/public/mine/children", data), get: (id: number): Promise => - publicApi.get(`/public/mine/children/${id}`), + publicApi.get(`/api/public/mine/children/${id}`), update: (id: number, data: Partial): Promise => - publicApi.put(`/public/mine/children/${id}`, data), + publicApi.put(`/api/public/mine/children/${id}`, data), - delete: (id: number) => publicApi.delete(`/public/mine/children/${id}`), -} + delete: (id: number) => publicApi.delete(`/api/public/mine/children/${id}`), +}; // ==================== 子女独立账号管理 ==================== export interface CreateChildAccountParams { - username: string - password: string - nickname: string - gender?: string - birthday?: string - city?: string - avatar?: string - relationship?: string + username: string; + password: string; + nickname: string; + gender?: string; + birthday?: string; + city?: string; + avatar?: string; + relationship?: string; } export interface ChildAccount { - id: number - username: string - nickname: string - avatar: string | null - gender: string | null - birthday: string | null - city: string | null - status: string - userType: string - createTime: string - relationship: string | null - controlMode: string + id: number; + username: string; + nickname: string; + avatar: string | null; + gender: string | null; + birthday: string | null; + city: string | null; + status: string; + userType: string; + createTime: string; + relationship: string | null; + controlMode: string; } export const publicChildAccountApi = { @@ -170,78 +171,84 @@ export const publicChildAccountApi = { publicApi.post("/api/public/auth/switch-child", { childUserId }), // 更新子女账号信息 - update: (id: number, data: { - nickname?: string - password?: string - gender?: string - birthday?: string - city?: string - avatar?: string - controlMode?: string - }): Promise => - publicApi.put(`/public/children/accounts/${id}`, data), + update: ( + id: number, + data: { + nickname?: string; + password?: string; + gender?: string; + birthday?: string; + city?: string; + avatar?: string; + controlMode?: string; + }, + ): Promise => publicApi.put(`/api/public/children/accounts/${id}`, data), // 子女查看家长信息 getParentInfo: (): Promise<{ - parentId: number - nickname: string - avatar: string | null - relationship: string | null - } | null> => - publicApi.get("/api/public/mine/parent-info"), -} + parentId: number; + nickname: string; + avatar: string | null; + relationship: string | null; + } | null> => publicApi.get("/api/public/mine/parent-info"), +}; // ==================== 活动 ==================== export interface PublicActivity { - id: number - contestName: string - contestType: string - contestState: string - status: string - startTime: string - endTime: string - coverUrl: string | null - posterUrl: string | null - registerStartTime: string - registerEndTime: string - submitStartTime: string - submitEndTime: string - organizers: any - visibility: string + id: number; + contestName: string; + contestType: string; + contestState: string; + status: string; + startTime: string; + endTime: string; + coverUrl: string | null; + posterUrl: string | null; + registerStartTime: string; + registerEndTime: string; + submitStartTime: string; + submitEndTime: string; + organizers: any; + visibility: string; } export const publicActivitiesApi = { list: (params?: { - page?: number - pageSize?: number - keyword?: string - contestType?: string + page?: number; + pageSize?: number; + keyword?: string; + contestType?: string; }): Promise<{ list: PublicActivity[]; total: number }> => publicApi.get("/api/public/activities", { params }), - detail: (id: number) => publicApi.get(`/public/activities/${id}`), + detail: (id: number) => publicApi.get(`/api/public/activities/${id}`), register: ( id: number, data: { participantType: "self" | "child"; childId?: number }, - ) => publicApi.post(`/public/activities/${id}/register`, data), + ) => publicApi.post(`/api/public/activities/${id}/register`, data), getMyRegistration: (id: number) => - publicApi.get(`/public/activities/${id}/my-registration`), + publicApi.get(`/api/public/activities/${id}/my-registration`), submitWork: ( id: number, data: { - registrationId: number - title: string - description?: string - files?: string[] - previewUrl?: string - attachments?: { fileName: string; fileUrl: string; fileType?: string; size?: string }[] + registrationId: number; + title: string; + description?: string; + files?: string[]; + previewUrl?: string; + attachments?: { + fileName: string; + fileUrl: string; + fileType?: string; + size?: string; + }[]; }, - ) => publicApi.post(`/public/activities/${id}/submit-work`, data), -} + ) => publicApi.post(`/api/public/activities/${id}/submit-work`, data), +}; // ==================== 我的报名 & 作品 ==================== @@ -251,154 +258,191 @@ export const publicMineApi = { works: (params?: { page?: number; pageSize?: number }) => publicApi.get("/api/public/mine/works", { params }), -} +}; // ==================== 用户作品库 ==================== export interface UserWork { - id: number - userId: number - title: string - coverUrl: string | null - description: string | null - visibility: string - status: string - reviewNote: string | null - originalImageUrl: string | null - voiceInputUrl: string | null - textInput: string | null - aiMeta: any - viewCount: number - likeCount: number - favoriteCount: number - commentCount: number - shareCount: number - publishTime: string | null - createTime: string - updateTime: string - pages?: UserWorkPage[] - tags?: Array<{ tag: { id: number; name: string; category: string } }> - createBy?: { id: number; nickname: string; avatar: string | null; username: string } - _count?: { pages: number; likes: number; favorites: number; comments: number } + id: number; + userId: number; + title: string; + coverUrl: string | null; + description: string | null; + visibility: string; + status: string; + reviewNote: string | null; + originalImageUrl: string | null; + voiceInputUrl: string | null; + textInput: string | null; + aiMeta: any; + viewCount: number; + likeCount: number; + favoriteCount: number; + commentCount: number; + shareCount: number; + publishTime: string | null; + createTime: string; + updateTime: string; + pages?: UserWorkPage[]; + tags?: Array<{ tag: { id: number; name: string; category: string } }>; + createBy?: { + id: number; + nickname: string; + avatar: string | null; + username: string; + }; + _count?: { + pages: number; + likes: number; + favorites: number; + comments: number; + }; } export interface UserWorkPage { - id: number - workId: number - pageNo: number - imageUrl: string | null - text: string | null - audioUrl: string | null + id: number; + workId: number; + pageNo: number; + imageUrl: string | null; + text: string | null; + audioUrl: string | null; } export const publicUserWorksApi = { // 创建作品 create: (data: { - title: string - coverUrl?: string - description?: string - visibility?: string - originalImageUrl?: string - voiceInputUrl?: string - textInput?: string - aiMeta?: any - pages?: Array<{ pageNo: number; imageUrl?: string; text?: string; audioUrl?: string }> - tagIds?: number[] + title: string; + coverUrl?: string; + description?: string; + visibility?: string; + originalImageUrl?: string; + voiceInputUrl?: string; + textInput?: string; + aiMeta?: any; + pages?: Array<{ + pageNo: number; + imageUrl?: string; + text?: string; + audioUrl?: string; + }>; + tagIds?: number[]; }): Promise => publicApi.post("/api/public/works", data), // 我的作品列表 list: (params?: { - page?: number - pageSize?: number - status?: string - keyword?: string + page?: number; + pageSize?: number; + status?: string; + keyword?: string; }): Promise<{ list: UserWork[]; total: number }> => publicApi.get("/api/public/works", { params }), // 作品详情 detail: (id: number): Promise => - publicApi.get(`/public/works/${id}`), + publicApi.get(`/api/public/works/${id}`), // 更新作品 - update: (id: number, data: { - title?: string - description?: string - coverUrl?: string - visibility?: string - tagIds?: number[] - }): Promise => publicApi.put(`/public/works/${id}`, data), + update: ( + id: number, + data: { + title?: string; + description?: string; + coverUrl?: string; + visibility?: string; + tagIds?: number[]; + }, + ): Promise => publicApi.put(`/api/public/works/${id}`, data), // 删除作品 - delete: (id: number) => publicApi.delete(`/public/works/${id}`), + delete: (id: number) => publicApi.delete(`/api/public/works/${id}`), // 发布作品(进入审核) - publish: (id: number) => publicApi.post(`/public/works/${id}/publish`), + publish: (id: number) => publicApi.post(`/api/public/works/${id}/publish`), // 获取绘本分页 getPages: (id: number): Promise => - publicApi.get(`/public/works/${id}/pages`), + publicApi.get(`/api/public/works/${id}/pages`), // 保存绘本分页 - savePages: (id: number, pages: Array<{ pageNo: number; imageUrl?: string; text?: string; audioUrl?: string }>) => - publicApi.post(`/public/works/${id}/pages`, { pages }), -} + savePages: ( + id: number, + pages: Array<{ + pageNo: number; + imageUrl?: string; + text?: string; + audioUrl?: string; + }>, + ) => publicApi.post(`/api/public/works/${id}/pages`, { pages }), +}; // ==================== AI 创作流程 ==================== export const publicCreationApi = { // 提交创作请求 submit: (data: { - originalImageUrl: string - voiceInputUrl?: string - textInput?: string + originalImageUrl: string; + voiceInputUrl?: string; + textInput?: string; }): Promise<{ id: number; status: string; message: string }> => publicApi.post("/api/public/creation/submit", data), // 查询生成进度 - getStatus: (id: number): Promise<{ id: number; status: string; title: string; createdAt: string }> => - publicApi.get(`/public/creation/${id}/status`), + getStatus: ( + id: number, + ): Promise<{ + id: number; + status: string; + title: string; + createdAt: string; + }> => publicApi.get(`/api/public/creation/${id}/status`), // 获取生成结果 getResult: (id: number): Promise => - publicApi.get(`/public/creation/${id}/result`), + publicApi.get(`/api/public/creation/${id}/result`), // 创作历史 - history: (params?: { page?: number; pageSize?: number }): Promise<{ list: any[]; total: number }> => + history: (params?: { + page?: number; + pageSize?: number; + }): Promise<{ list: any[]; total: number }> => publicApi.get("/api/public/creation/history", { params }), -} +}; // ==================== 标签 ==================== export interface WorkTag { - id: number - name: string - category: string | null - usageCount: number + id: number; + name: string; + category: string | null; + usageCount: number; } export const publicTagsApi = { - list: (): Promise => publicApi.get("/api/public/tags"), - hot: (): Promise => publicApi.get("/api/public/tags/hot"), -} + list: (): Promise => publicApi.get("/api/public/api/tags"), + hot: (): Promise => publicApi.get("/api/public/api/tags/hot"), +}; // ==================== 作品广场 ==================== export const publicGalleryApi = { list: (params?: { - page?: number - pageSize?: number - tagId?: number - category?: string - sortBy?: string - keyword?: string + page?: number; + pageSize?: number; + tagId?: number; + category?: string; + sortBy?: string; + keyword?: string; }): Promise<{ list: UserWork[]; total: number }> => publicApi.get("/api/public/gallery", { params }), detail: (id: number): Promise => - publicApi.get(`/public/gallery/${id}`), + publicApi.get(`/api/public/gallery/${id}`), - userWorks: (userId: number, params?: { page?: number; pageSize?: number }): Promise<{ list: UserWork[]; total: number }> => - publicApi.get(`/public/users/${userId}/works`, { params }), -} + userWorks: ( + userId: number, + params?: { page?: number; pageSize?: number }, + ): Promise<{ list: UserWork[]; total: number }> => + publicApi.get(`/api/public/users/${userId}/works`, { params }), +}; -export default publicApi +export default publicApi; diff --git a/java-frontend/src/views/content/TagManagement.vue b/java-frontend/src/views/content/TagManagement.vue index e6607cd..78b29bf 100644 --- a/java-frontend/src/views/content/TagManagement.vue +++ b/java-frontend/src/views/content/TagManagement.vue @@ -83,7 +83,7 @@ const columns = [ const fetchTags = async () => { loading.value = true - try { tags.value = await request.get('/tags') as any } catch { message.error('获取失败') } + try { tags.value = await request.get('/api/tags') as any } catch { message.error('获取失败') } finally { loading.value = false } } @@ -99,10 +99,10 @@ const handleSubmit = async () => { submitting.value = true try { if (editingId.value) { - await request.put(`/tags/${editingId.value}`, form) + await request.put(`/api/tags/${editingId.value}`, form) message.success('编辑成功') } else { - await request.post('/tags', form) + await request.post('/api/tags', form) message.success('创建成功') } modalVisible.value = false @@ -113,14 +113,14 @@ const handleSubmit = async () => { const handleToggle = async (record: any) => { try { - await request.patch(`/tags/${record.id}/status`) + await request.patch(`/api/tags/${record.id}/status`) message.success(record.status === 'enabled' ? '已禁用' : '已启用') fetchTags() } catch { message.error('操作失败') } } const handleDelete = async (id: number) => { - try { await request.delete(`/tags/${id}`); message.success('删除成功'); fetchTags() } + try { await request.delete(`/api/tags/${id}`); message.success('删除成功'); fetchTags() } catch (e: any) { message.error(e?.response?.data?.message || '删除失败') } } diff --git a/java-frontend/src/views/content/WorkManagement.vue b/java-frontend/src/views/content/WorkManagement.vue index b789a48..557a3aa 100644 --- a/java-frontend/src/views/content/WorkManagement.vue +++ b/java-frontend/src/views/content/WorkManagement.vue @@ -99,7 +99,7 @@ const formatDate = (d: string) => d ? dayjs(d).format('YYYY-MM-DD HH:mm') : '-' const fetchStats = async () => { try { - const s: any = await request.get('/content-review/management/stats') + const s: any = await request.get('/api/content-review/management/stats') mgmtStats.value = [ { label: '总作品数', value: s.total }, { label: '今日新增', value: s.todayNew }, @@ -113,11 +113,12 @@ const fetchList = async () => { loading.value = true try { // 复用审核接口查已发布+已下架 - const res: any = await request.get('/content-review/works', { + const res: any = await request.get('/api/content-review/works', { params: { page: pagination.current, pageSize: pagination.pageSize, - status: 'published', // 作品管理只看已发布的 + // 已发布 + 已下架,便于管理「恢复」 + status: 'published,taken_down', keyword: keyword.value || undefined, }, }) @@ -132,7 +133,7 @@ const handleReset = () => { keyword.value = ''; sortBy.value = 'latest'; paginat const handleTableChange = (pag: any) => { pagination.current = pag.current; pagination.pageSize = pag.pageSize; fetchList() } const handleRecommend = async (record: any) => { - try { await request.post(`/content-review/works/${record.id}/recommend`); message.success(record.isRecommended ? '已取消推荐' : '已推荐'); fetchList() } + try { await request.post(`/api/content-review/works/${record.id}/recommend`); message.success(record.isRecommended ? '已取消推荐' : '已推荐'); fetchList() } catch { message.error('操作失败') } } @@ -142,14 +143,14 @@ const handleTakedown = (record: any) => { content: `下架后作品「${record.title}」将不再公开展示`, okType: 'danger', onOk: async () => { - try { await request.post(`/content-review/works/${record.id}/takedown`, { reason: '管理员下架' }); message.success('已下架'); fetchList(); fetchStats() } + try { await request.post(`/api/content-review/works/${record.id}/takedown`, { reason: '管理员下架' }); message.success('已下架'); fetchList(); fetchStats() } catch { message.error('操作失败') } }, }) } const handleRestore = async (record: any) => { - try { await request.post(`/content-review/works/${record.id}/restore`); message.success('已恢复'); fetchList(); fetchStats() } + try { await request.post(`/api/content-review/works/${record.id}/restore`); message.success('已恢复'); fetchList(); fetchStats() } catch { message.error('操作失败') } } diff --git a/java-frontend/src/views/content/WorkReview.vue b/java-frontend/src/views/content/WorkReview.vue index 0b6a479..8358725 100644 --- a/java-frontend/src/views/content/WorkReview.vue +++ b/java-frontend/src/views/content/WorkReview.vue @@ -165,13 +165,13 @@ const detailData = ref(null) const previewPage = ref(0) const fetchStats = async () => { - try { stats.value = await request.get('/content-review/works/stats') as any } catch { /* */ } + try { stats.value = await request.get('/api/content-review/works/stats') as any } catch { /* */ } } const fetchList = async () => { loading.value = true try { - const res: any = await request.get('/content-review/works', { + const res: any = await request.get('/api/content-review/works', { params: { page: pagination.current, pageSize: pagination.pageSize, status: searchStatus.value, keyword: searchKeyword.value || undefined }, }) dataSource.value = res.list @@ -193,7 +193,7 @@ const handleReset = () => { searchStatus.value = undefined; searchKeyword.value const handleTableChange = (pag: any) => { pagination.current = pag.current; pagination.pageSize = pag.pageSize; fetchList() } const quickApprove = async (id: number) => { - try { await request.post(`/content-review/works/${id}/approve`, {}); message.success('已通过'); fetchList(); fetchStats() } + try { await request.post(`/api/content-review/works/${id}/approve`, {}); message.success('已通过'); fetchList(); fetchStats() } catch { message.error('操作失败') } } @@ -202,14 +202,14 @@ const handleReject = async () => { const reason = rejectReason.value === 'other' ? rejectCustom.value : rejectReason.value if (!reason) { message.warning('请选择拒绝原因'); return } rejectLoading.value = true - try { await request.post(`/content-review/works/${rejectTargetId.value}/reject`, { reason }); message.success('已拒绝'); rejectVisible.value = false; fetchList(); fetchStats() } + try { await request.post(`/api/content-review/works/${rejectTargetId.value}/reject`, { reason }); message.success('已拒绝'); rejectVisible.value = false; fetchList(); fetchStats() } catch { message.error('操作失败') } finally { rejectLoading.value = false } } const showDetail = async (id: number) => { previewPage.value = 0 - try { detailData.value = await request.get(`/content-review/works/${id}`); detailVisible.value = true } + try { detailData.value = await request.get(`/api/content-review/works/${id}`); detailVisible.value = true } catch { message.error('获取详情失败') } } diff --git a/java-frontend/src/views/system/public-users/Index.vue b/java-frontend/src/views/system/public-users/Index.vue index 4492292..eb5fa6a 100644 --- a/java-frontend/src/views/system/public-users/Index.vue +++ b/java-frontend/src/views/system/public-users/Index.vue @@ -229,7 +229,7 @@ const showDetail = async (userId: number) => { detailVisible.value = true detailLoading.value = true try { - const res = await api.get(`/public/users/${userId}`) + const res = await api.get(`/api/public/users/${userId}`) detailData.value = res.data?.data || res.data } catch { message.error('获取用户详情失败') diff --git a/java-frontend/vite.config.ts b/java-frontend/vite.config.ts index ae3f233..e171ad0 100644 --- a/java-frontend/vite.config.ts +++ b/java-frontend/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => { }, server: { port: 3000, + host: "0.0.0.0", proxy: { "/api": { target: "http://localhost:8580",