feat: 06 互动与内容治理落地(评论 / 点赞 / 举报 / 敏感词 / 审核留证)

为什么:
1) 产品侧要先把内容治理闭环跑通才能谈上线:评论可发布、可翻页、可删除,
   违规内容能在后台被审核与驳回,举报要有工单可追溯。本提交把这套补齐。
2) 审核不能只靠人肉:敏感词按 BLOCK/REPLACE/REVIEW 三档处置,
   机审通过即发布(可配置为 PRE_REVIEW 全部先审后发),人审与机审的结论
   都写进 content_audit_record,争议时可回答"当时凭什么放行/驳回"。
3) 举报处理必须与处置打通:结论成立会联动驳回被举报评论,并可选同时封禁被举报用户,
   避免"工单处理完了但违规内容还挂着"。
4) 顺带修复一个真实缺陷:user_favorite / content_like 上有唯一索引,而全局配置会把
   deleteById 改写成逻辑删除,导致取消收藏/点赞后旧的 deleted 行仍占位,
   再次收藏或点赞直接撞唯一索引报错。关系类数据没有留证价值,改为显式物理删除,
   并补了"取消后再来一次"的回归自测。
This commit is contained in:
Vicky committed 2026-10-15 12:03:32 +08:00
1 parent b3179fc836
commit af1dc48494
48 files changed
+1752 -14

No files matched your search

+4
View File
@@ -35,6 +35,10 @@
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-playback</artifactId>
</dependency>
<dependency>
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-interaction</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -0,0 +1,62 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.interaction.dal.entity.ContentAuditRecord;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.vo.CommentAuditReqVO;
import com.fruitshorts.interaction.vo.CommentQueryReqVO;
import com.fruitshorts.interaction.vo.CommentRespVO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** 后台评论审核。 */
@RestController
@RequestMapping("/admin/comments")
@RequiredArgsConstructor
public class CommentAdminController {
private final CommentService commentService;
/** 检索评论:默认按状态过滤,`status=pending` 即审核队列。 */
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('comment:list')")
public Result<PageResult<CommentRespVO>> page(CommentQueryReqVO req) {
return Result.ok(commentService.adminPage(req));
}
/** 审核留证:机审与人审的完整链路。 */
@GetMapping("/{id}/audit-records")
@PreAuthorize("@ss.hasPermission('comment:list')")
public Result<List<ContentAuditRecord>> auditRecords(@PathVariable Long id) {
return Result.ok(commentService.auditRecords(id));
}
@PutMapping("/{id}/audit")
@PreAuthorize("@ss.hasPermission('comment:audit')")
@OperationLog(module = "内容治理", action = "审核评论")
public Result<Void> audit(@PathVariable Long id, @Valid @RequestBody CommentAuditReqVO req) {
commentService.audit(id, req, SecurityUtils.getUserId());
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('comment:audit')")
@OperationLog(module = "内容治理", action = "删除评论")
public Result<Void> delete(@PathVariable Long id) {
commentService.adminDelete(id);
return Result.ok();
}
}
@@ -0,0 +1,42 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.interaction.dal.entity.ContentReport;
import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.ReportHandleReqVO;
import com.fruitshorts.interaction.vo.ReportQueryReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 后台举报工单。 */
@RestController
@RequestMapping("/admin/reports")
@RequiredArgsConstructor
public class ReportAdminController {
private final ReportService reportService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('report:list')")
public Result<PageResult<ContentReport>> page(ReportQueryReqVO req) {
return Result.ok(reportService.page(req));
}
/** 处理工单:结论成立会联动驳回被举报评论,可选择同时封禁被举报用户。 */
@PutMapping("/{id}/handle")
@PreAuthorize("@ss.hasPermission('report:handle')")
@OperationLog(module = "内容治理", action = "处理举报")
public Result<Void> handle(@PathVariable Long id, @RequestBody ReportHandleReqVO req) {
reportService.handle(id, req, SecurityUtils.getUserId());
return Result.ok();
}
}
@@ -0,0 +1,62 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.interaction.dal.entity.SensitiveWord;
import com.fruitshorts.interaction.service.SensitiveWordService;
import com.fruitshorts.interaction.vo.SensitiveWordReqVO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 后台敏感词库。改动即时生效(服务端词库缓存会失效重建)。 */
@RestController
@RequestMapping("/admin/sensitive-words")
@RequiredArgsConstructor
public class SensitiveWordAdminController {
private final SensitiveWordService sensitiveWordService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('sensitive-word:list')")
public Result<PageResult<SensitiveWord>> page(@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) String level,
@RequestParam(defaultValue = "1") Integer pageIndex,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.ok(sensitiveWordService.page(keyword, category, level, pageIndex, pageSize));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "新增敏感词")
public Result<Long> create(@Valid @RequestBody SensitiveWordReqVO req) {
return Result.ok(sensitiveWordService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "修改敏感词")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody SensitiveWordReqVO req) {
sensitiveWordService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "删除敏感词")
public Result<Void> delete(@PathVariable Long id) {
sensitiveWordService.delete(id);
return Result.ok();
}
}