feat: 修复竞赛与系统接口并迁移 UGC 内容审核与标签模块
- 竞赛表/作品/评委/公告/评审规则等 Flyway 与路由、分页接口对齐 - 字典与配置 /page 路径、SysLog 统计去掉不存在的 deleted 条件 - 学校 GET 无数据时返回成功且 data 为 null - 新增 content-review、work_tags 等 UGC 表及服务(V29) - 前端作品管理列表支持 published+taken_down 等调整 Made-with: Cursor
This commit is contained in:
parent
b20c00bea3
commit
a714ec8cee
@ -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<com.baomidou.mybatisplus.extension.plugins.pagination.Page<ConfigListVO>> 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<ConfigDetailVO> 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<ConfigDetailVO> 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<Void> delete(
|
||||
|
||||
@ -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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> recommend(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable Long id) {
|
||||
Long tenantId = principal.getTenantId();
|
||||
boolean superTenant = principal.isSuperTenant();
|
||||
contentReviewService.toggleRecommend(id, tenantId, superTenant);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -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<ContestDetailVO> 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<ContestDetailVO> 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<ContestDetailVO> 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<ContestDetailVO> 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<ContestDetailVO> 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<Void> delete(@PathVariable Long id) {
|
||||
|
||||
@ -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<Page<JudgeVO>> 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<JudgeVO> 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<Void> 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<JudgeVO> getDetail(@PathVariable Long id) {
|
||||
|
||||
@ -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<Page<NoticeVO>> 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<NoticeVO> 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<Void> 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<NoticeVO> 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<NoticeVO> getDetail(@PathVariable Long id) {
|
||||
|
||||
@ -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<PresetCommentVO> result = presetCommentService.listCommon(tenantId);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 {@code GET /api/contests/reviews/judge/contests} 一致,供前端在预设评语模块复用
|
||||
*/
|
||||
@GetMapping("/judge/contests")
|
||||
@Operation(summary = "评委参与的活动列表")
|
||||
public Result<List<JudgeContestVO>> getJudgeContests(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
Long judgeId = userPrincipal.getUserId();
|
||||
Long tenantId = userPrincipal.getTenantId();
|
||||
return Result.success(reviewService.getJudgeContests(judgeId, tenantId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Page<ReviewRuleVO>> 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<Void> 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<ReviewRuleVO> 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<Void> deleteDimension(@PathVariable Long id) {
|
||||
|
||||
@ -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<Page<WorkVO>> pageList(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal,
|
||||
@ModelAttribute WorkQueryDTO queryDTO) {
|
||||
Long tenantId = userPrincipal.getTenantId();
|
||||
Page<WorkVO> result = workService.pageQuery(queryDTO, tenantId);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询作品列表")
|
||||
@PreAuthorize("hasAuthority('contest:read')")
|
||||
|
||||
@ -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<com.baomidou.mybatisplus.extension.plugins.pagination.Page<DictListVO>> 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<DictDetailVO> 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<DictDetailVO> 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<Void> delete(
|
||||
|
||||
@ -38,7 +38,7 @@ public class SchoolController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取学校信息")
|
||||
@Operation(summary = "获取学校信息(未创建时 data 为 null)")
|
||||
@PreAuthorize("hasAuthority('school:read')")
|
||||
public Result<SchoolVO> get(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
@ -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<WorkTag>> list() {
|
||||
return Result.success(workTagService.listAll());
|
||||
}
|
||||
|
||||
@GetMapping("/categories")
|
||||
@Operation(summary = "标签分类列表")
|
||||
public Result<List<String>> categories() {
|
||||
return Result.success(workTagService.listCategories());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建标签")
|
||||
public Result<WorkTag> create(@RequestBody @Validated WorkTagCreateDTO dto) {
|
||||
return Result.success(workTagService.create(dto));
|
||||
}
|
||||
|
||||
@PutMapping("/{id:\\d+}")
|
||||
@Operation(summary = "更新标签")
|
||||
public Result<WorkTag> update(
|
||||
@PathVariable Long id,
|
||||
@RequestBody WorkTagUpdateDTO dto) {
|
||||
return Result.success(workTagService.update(id, dto));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id:\\d+}")
|
||||
@Operation(summary = "删除标签")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
workTagService.remove(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PatchMapping("/{id:\\d+}/status")
|
||||
@Operation(summary = "启用/禁用标签")
|
||||
public Result<WorkTag> toggleStatus(@PathVariable Long id) {
|
||||
return Result.success(workTagService.toggleStatus(id));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.lesingle.creation.dto.contentreview;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 审核通过请求体
|
||||
*/
|
||||
@Data
|
||||
public class ApproveWorkBodyDTO {
|
||||
private String note;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<ContentReviewLog> {
|
||||
}
|
||||
@ -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<UserWork> {
|
||||
}
|
||||
@ -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<UserWorkPage> {
|
||||
}
|
||||
@ -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<WorkTag> {
|
||||
}
|
||||
@ -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<WorkTagRelation> {
|
||||
}
|
||||
@ -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<String, Object> getManagementStats(Long tenantId, boolean superTenant);
|
||||
|
||||
Map<String, Object> getWorkStats(Long tenantId, boolean superTenant);
|
||||
|
||||
Map<String, Object> getWorkQueue(int page, int pageSize, String status, String keyword,
|
||||
String startTime, String endTime, Long tenantId, boolean superTenant);
|
||||
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
@ -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<JudgeVO> listByContest(Long contestId);
|
||||
|
||||
/**
|
||||
* 分页查询评委(当前租户)
|
||||
*/
|
||||
Page<JudgeVO> pageQuery(JudgeQueryDTO queryDTO, Long tenantId);
|
||||
|
||||
/**
|
||||
* 冻结评委
|
||||
* @param id 评委 ID
|
||||
|
||||
@ -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<NoticeVO> listByContest(Long contestId);
|
||||
|
||||
/**
|
||||
* 分页查询公告(按当前租户下活动范围)
|
||||
*/
|
||||
Page<NoticeVO> pageQuery(NoticeQueryDTO queryDTO, Long tenantId);
|
||||
}
|
||||
|
||||
@ -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<ReviewRuleVO> list(Long tenantId);
|
||||
|
||||
/**
|
||||
* 分页查询评审规则
|
||||
*/
|
||||
Page<ReviewRuleVO> pageQuery(ReviewRuleQueryDTO queryDTO, Long tenantId);
|
||||
|
||||
/**
|
||||
* 查询可选的评审规则列表(仅返回有效规则)
|
||||
* @param tenantId 租户 ID
|
||||
|
||||
@ -15,10 +15,11 @@ public interface ContestService extends IService<Contest> {
|
||||
* 创建竞赛
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* 分页查询竞赛列表
|
||||
|
||||
@ -25,7 +25,7 @@ public interface SchoolService extends IService<School> {
|
||||
* 根据租户 ID 查询学校
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @return 学校 VO
|
||||
* @return 学校 VO;尚未创建时返回 {@code null}
|
||||
*/
|
||||
SchoolVO getByTenantId(Long tenantId);
|
||||
|
||||
|
||||
@ -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<WorkTag> listAll();
|
||||
|
||||
WorkTag create(WorkTagCreateDTO dto);
|
||||
|
||||
WorkTag update(Long id, WorkTagUpdateDTO dto);
|
||||
|
||||
void remove(Long id);
|
||||
|
||||
WorkTag toggleStatus(Long id);
|
||||
|
||||
List<String> listCategories();
|
||||
}
|
||||
@ -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<String, Object> getManagementStats(Long tenantId, boolean superTenant) {
|
||||
LocalDateTime start = LocalDate.now().atStartOfDay();
|
||||
|
||||
LambdaQueryWrapper<UserWork> pub = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant)
|
||||
.eq(UserWork::getStatus, "published");
|
||||
long total = userWorkMapper.selectCount(pub);
|
||||
|
||||
LambdaQueryWrapper<UserWork> todayNewQ = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant)
|
||||
.eq(UserWork::getStatus, "published")
|
||||
.ge(UserWork::getPublishTime, start);
|
||||
long todayNew = userWorkMapper.selectCount(todayNewQ);
|
||||
|
||||
QueryWrapper<UserWork> sumQ = new QueryWrapper<>();
|
||||
sumQ.select("COALESCE(SUM(view_count),0) AS totalViews");
|
||||
sumQ.eq("status", "published");
|
||||
if (!superTenant) {
|
||||
sumQ.eq("tenant_id", tenantId);
|
||||
}
|
||||
List<Map<String, Object>> 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<UserWork> downQ = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant)
|
||||
.eq(UserWork::getStatus, "taken_down");
|
||||
long takenDown = userWorkMapper.selectCount(downQ);
|
||||
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("total", total);
|
||||
m.put("todayNew", todayNew);
|
||||
m.put("totalViews", totalViews);
|
||||
m.put("takenDown", takenDown);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getWorkStats(Long tenantId, boolean superTenant) {
|
||||
LocalDateTime start = LocalDate.now().atStartOfDay();
|
||||
|
||||
LambdaQueryWrapper<UserWork> pend = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant)
|
||||
.eq(UserWork::getStatus, "pending_review");
|
||||
long pending = userWorkMapper.selectCount(pend);
|
||||
|
||||
LambdaQueryWrapper<ContentReviewLog> 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<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("pending", pending);
|
||||
m.put("todayReviewed", todayReviewed);
|
||||
m.put("todayApproved", todayApproved);
|
||||
m.put("todayRejected", todayRejected);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getWorkQueue(int page, int pageSize, String status, String keyword,
|
||||
String startTime, String endTime, Long tenantId, boolean superTenant) {
|
||||
LambdaQueryWrapper<UserWork> w = tenantScope(Wrappers.lambdaQuery(UserWork.class), tenantId, superTenant);
|
||||
|
||||
if (StringUtils.hasText(status)) {
|
||||
if (status.contains(",")) {
|
||||
List<String> 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<User> uw = Wrappers.lambdaQuery(User.class).like(User::getNickname, kw);
|
||||
if (!superTenant) {
|
||||
uw.eq(User::getTenantId, tenantId);
|
||||
}
|
||||
List<User> hitUsers = userMapper.selectList(uw);
|
||||
List<Long> 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<UserWork> pg = new Page<>(page, pageSize);
|
||||
Page<UserWork> result = userWorkMapper.selectPage(pg, w);
|
||||
|
||||
List<UserWork> records = result.getRecords();
|
||||
Set<Long> userIds = records.stream().map(UserWork::getUserId).collect(Collectors.toSet());
|
||||
Map<Long, User> userMap = loadUsers(userIds);
|
||||
Set<Long> workIds = records.stream().map(UserWork::getId).collect(Collectors.toSet());
|
||||
Map<Long, Integer> pageCounts = countPagesByWorkIds(workIds);
|
||||
Map<Long, List<Map<String, Object>>> tagsByWork = loadTagsByWorkIds(workIds);
|
||||
|
||||
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<UserWorkPage> pages = userWorkPageMapper.selectList(
|
||||
Wrappers.lambdaQuery(UserWorkPage.class)
|
||||
.eq(UserWorkPage::getWorkId, workId)
|
||||
.orderByAsc(UserWorkPage::getPageNo));
|
||||
|
||||
int pageCount = pages.size();
|
||||
List<Map<String, Object>> tagList = loadTagsByWorkIds(Collections.singleton(workId)).getOrDefault(workId, Collections.emptyList());
|
||||
|
||||
Map<String, Object> 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<UserWork> tenantScope(LambdaQueryWrapper<UserWork> w, Long tenantId, boolean superTenant) {
|
||||
if (!superTenant) {
|
||||
w.eq(UserWork::getTenantId, tenantId);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
private static LambdaQueryWrapper<ContentReviewLog> logTenantScope(LambdaQueryWrapper<ContentReviewLog> w,
|
||||
Long tenantId, boolean superTenant) {
|
||||
if (!superTenant) {
|
||||
w.eq(ContentReviewLog::getTenantId, tenantId);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
private Map<Long, User> loadUsers(Set<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<User> 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<Long, Integer> countPagesByWorkIds(Set<Long> workIds) {
|
||||
Map<Long, Integer> 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<Long, List<Map<String, Object>>> loadTagsByWorkIds(Set<Long> workIds) {
|
||||
Map<Long, List<Map<String, Object>>> out = new HashMap<>();
|
||||
if (workIds == null || workIds.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
List<WorkTagRelation> rels = workTagRelationMapper.selectList(
|
||||
Wrappers.lambdaQuery(WorkTagRelation.class).in(WorkTagRelation::getWorkId, workIds));
|
||||
if (rels.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
Set<Long> tagIds = rels.stream().map(WorkTagRelation::getTagId).collect(Collectors.toSet());
|
||||
List<WorkTag> tags = workTagMapper.selectList(Wrappers.lambdaQuery(WorkTag.class).in(WorkTag::getId, tagIds));
|
||||
Map<Long, WorkTag> 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<String, Object> tagMini = new LinkedHashMap<>();
|
||||
tagMini.put("id", t.getId());
|
||||
tagMini.put("name", t.getName());
|
||||
Map<String, Object> wrap = new LinkedHashMap<>();
|
||||
wrap.put("tag", tagMini);
|
||||
out.computeIfAbsent(rel.getWorkId(), k -> new ArrayList<>()).add(wrap);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, Object> toListItemMap(UserWork uw, User creator, int pageCount, List<Map<String, Object>> tags) {
|
||||
Map<String, Object> m = workBaseMap(uw);
|
||||
m.put("creator", creatorMini(creator));
|
||||
Map<String, Object> cnt = new LinkedHashMap<>();
|
||||
cnt.put("pages", pageCount);
|
||||
m.put("_count", cnt);
|
||||
m.put("tags", tags);
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<String, Object> toDetailMap(UserWork uw, User creator, List<UserWorkPage> pages,
|
||||
List<Map<String, Object>> tags, int pageCount) {
|
||||
Map<String, Object> m = workBaseMap(uw);
|
||||
m.put("creator", creatorMini(creator));
|
||||
List<Map<String, Object>> pageList = new ArrayList<>();
|
||||
for (UserWorkPage p : pages) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> workBaseMap(UserWork uw) {
|
||||
Map<String, Object> 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<String, Object> creatorMini(User u) {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@ -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<JudgeVO> pageQuery(JudgeQueryDTO queryDTO, Long tenantId) {
|
||||
Page<ContestJudge> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
|
||||
LambdaQueryWrapper<ContestJudge> w = new LambdaQueryWrapper<ContestJudge>()
|
||||
.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<ContestJudge> result = contestJudgeMapper.selectPage(page, w);
|
||||
Page<JudgeVO> 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) {
|
||||
|
||||
@ -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<NoticeVO> pageQuery(NoticeQueryDTO queryDTO, Long tenantId) {
|
||||
LambdaQueryWrapper<ContestNotice> w = new LambdaQueryWrapper<ContestNotice>()
|
||||
.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<Long> contestIds = contestMapper.selectList(
|
||||
new LambdaQueryWrapper<Contest>()
|
||||
.eq(Contest::getTenantId, tenantId)
|
||||
.select(Contest::getId))
|
||||
.stream()
|
||||
.map(Contest::getId)
|
||||
.collect(Collectors.toList());
|
||||
if (contestIds.isEmpty()) {
|
||||
Page<NoticeVO> 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<ContestNotice> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
|
||||
Page<ContestNotice> result = contestNoticeMapper.selectPage(page, w);
|
||||
Page<NoticeVO> 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) {
|
||||
|
||||
@ -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<ReviewRuleVO> pageQuery(ReviewRuleQueryDTO queryDTO, Long tenantId) {
|
||||
Page<ContestReviewRule> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
|
||||
LambdaQueryWrapper<ContestReviewRule> w = new LambdaQueryWrapper<ContestReviewRule>()
|
||||
.eq(ContestReviewRule::getTenantId, tenantId)
|
||||
.eq(ContestReviewRule::getDeleted, 0);
|
||||
if (StringUtils.hasText(queryDTO.getRuleName())) {
|
||||
w.like(ContestReviewRule::getRuleName, queryDTO.getRuleName());
|
||||
}
|
||||
w.orderByDesc(ContestReviewRule::getCreateTime);
|
||||
Page<ContestReviewRule> result = reviewRuleMapper.selectPage(page, w);
|
||||
Page<ReviewRuleVO> 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<ReviewRuleVO> listForSelect(Long tenantId) {
|
||||
List<ContestReviewRule> rules = reviewRuleMapper.selectList(new LambdaQueryWrapper<ContestReviewRule>()
|
||||
|
||||
@ -41,7 +41,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> 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<ContestMapper, Contest> 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());
|
||||
|
||||
@ -59,7 +59,8 @@ public class SchoolServiceImpl extends ServiceImpl<SchoolMapper, School> impleme
|
||||
|
||||
School school = schoolMapper.selectByTenantId(tenantId);
|
||||
if (school == null) {
|
||||
throw new BusinessException("学校信息不存在");
|
||||
log.debug("当前租户尚未创建学校,租户 ID: {}", tenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToVO(school);
|
||||
|
||||
@ -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<WorkTag> 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<String> listCategories() {
|
||||
List<WorkTag> 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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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='内容审核日志';
|
||||
@ -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
|
||||
</select>
|
||||
|
||||
<!-- 获取按操作类型统计的日志 -->
|
||||
@ -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
|
||||
</select>
|
||||
@ -58,7 +55,6 @@
|
||||
<delete id="deleteOldLogs">
|
||||
DELETE FROM t_sys_log
|
||||
WHERE create_time < #{cutoffDate}
|
||||
AND (deleted = 0 OR deleted IS NULL)
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
@ -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<LoginResponse> =>
|
||||
publicApi.post("/api/public/auth/login", data),
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 个人信息 ====================
|
||||
|
||||
export const publicProfileApi = {
|
||||
getProfile: (): Promise<PublicUser> => publicApi.get("/api/public/mine/profile"),
|
||||
getProfile: (): Promise<PublicUser> =>
|
||||
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<Child> =>
|
||||
publicApi.get(`/public/mine/children/${id}`),
|
||||
publicApi.get(`/api/public/mine/children/${id}`),
|
||||
|
||||
update: (id: number, data: Partial<CreateChildParams>): Promise<Child> =>
|
||||
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<any> =>
|
||||
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<any> => 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<UserWork> => 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<UserWork> =>
|
||||
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<UserWork> => publicApi.put(`/public/works/${id}`, data),
|
||||
update: (
|
||||
id: number,
|
||||
data: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
coverUrl?: string;
|
||||
visibility?: string;
|
||||
tagIds?: number[];
|
||||
},
|
||||
): Promise<UserWork> => 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<UserWorkPage[]> =>
|
||||
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<UserWork> =>
|
||||
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<WorkTag[]> => publicApi.get("/api/public/tags"),
|
||||
hot: (): Promise<WorkTag[]> => publicApi.get("/api/public/tags/hot"),
|
||||
}
|
||||
list: (): Promise<WorkTag[]> => publicApi.get("/api/public/api/tags"),
|
||||
hot: (): Promise<WorkTag[]> => 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<UserWork> =>
|
||||
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;
|
||||
|
||||
@ -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 || '删除失败') }
|
||||
}
|
||||
|
||||
|
||||
@ -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('操作失败') }
|
||||
}
|
||||
|
||||
|
||||
@ -165,13 +165,13 @@ const detailData = ref<any>(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('获取详情失败') }
|
||||
}
|
||||
|
||||
|
||||
@ -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('获取用户详情失败')
|
||||
|
||||
@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8580",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user