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:
zhonghua 2026-04-01 10:48:49 +08:00
parent b20c00bea3
commit a714ec8cee
57 changed files with 2025 additions and 268 deletions

View File

@ -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(

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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));
}
}

View File

@ -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) {

View File

@ -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')")

View File

@ -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(

View File

@ -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) {

View File

@ -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));
}
}

View File

@ -0,0 +1,11 @@
package com.lesingle.creation.dto.contentreview;
import lombok.Data;
/**
* 审核通过请求体
*/
@Data
public class ApproveWorkBodyDTO {
private String note;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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);
/**
* 分页查询竞赛列表

View File

@ -25,7 +25,7 @@ public interface SchoolService extends IService<School> {
* 根据租户 ID 查询学校
*
* @param tenantId 租户 ID
* @return 学校 VO
* @return 学校 VO尚未创建时返回 {@code null}
*/
SchoolVO getByTenantId(Long tenantId);

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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>()

View File

@ -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());

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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;

View File

@ -0,0 +1,25 @@
-- 竞赛作品表补充 valid_stateV10 建表未包含,与实体 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;

View File

@ -0,0 +1,36 @@
-- 评委表与实体对齐:补充 valid_state、weightV10 建表未包含)
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;

View File

@ -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;

View File

@ -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;

View File

@ -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='内容审核日志';

View File

@ -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 &lt; #{cutoffDate}
AND (deleted = 0 OR deleted IS NULL)
</delete>
</mapper>

View File

@ -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;

View File

@ -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 || '删除失败') }
}

View File

@ -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('操作失败') }
}

View File

@ -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('获取详情失败') }
}

View File

@ -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('获取用户详情失败')

View File

@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => {
},
server: {
port: 3000,
host: "0.0.0.0",
proxy: {
"/api": {
target: "http://localhost:8580",