post_report

Change-Id: I191da6e9ea7371e66dadead873abd057f5d08c74
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index e5e4eab..1e0adf4 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -16,9 +16,12 @@
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.service.IReportService;
 
 import java.util.List;
 
+
 @RestController
 @RequestMapping("/post")
 @Validated
@@ -29,6 +32,8 @@
 
     @Autowired
     private PostViewMapper postViewMapper;
+    @Autowired
+    private IReportService reportService;
 
     @PostMapping("")
     public ResponseEntity<ApiResponse<Void>> createPost(@RequestBody PostCreateDTO postCreateDTO) {
@@ -191,4 +196,41 @@
         Long count = postRatingService.getRatingUserCount(postId);
         return ResponseEntity.ok(ApiResponse.success(count));
     }
+    @PostMapping("/{postId}/report")
+    public ResponseEntity<ApiResponse<String>> reportPost(
+            @PathVariable Long postId,
+            @RequestParam String reason) {
+        long userId = (long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        try {
+            reportService.submitReport(userId, postId, reason);
+            return ResponseEntity.ok(ApiResponse.message("举报已提交"));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(ApiResponse.error(400, e.getMessage()));
+        }
+    }
+
+
+    @GetMapping("/reports")
+    public ResponseEntity<ApiResponse<List<Report>>> getReports(
+            @RequestParam(required = false) String status) {
+        List<Report> reports = reportService.getReports(status);
+        return ResponseEntity.ok(ApiResponse.success(reports));
+    }
+
+    @PutMapping("/report/{reportId}")
+    public ResponseEntity<ApiResponse<String>> resolveReport(
+            @PathVariable Long reportId,
+            @RequestParam Long adminId,     // 实际部署时可从 token 解析或改为登录信息中获取
+            @RequestParam String status,
+            @RequestParam(required = false) String notes) {
+        try {
+            reportService.resolveReport(reportId, adminId, status, notes);
+            return ResponseEntity.ok(ApiResponse.message("举报处理完成"));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(ApiResponse.error(400, e.getMessage()));
+        }
+    }
+
+
+
 }
diff --git a/src/main/java/com/example/g8backend/entity/Report.java b/src/main/java/com/example/g8backend/entity/Report.java
new file mode 100644
index 0000000..9ddfa5b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Report.java
@@ -0,0 +1,23 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("reports")
+public class Report {
+    @TableId(type = IdType.AUTO)
+    private Long reportId;
+    private Long postId;
+    private Long userId;
+    private String reason;
+    private String status;
+    private LocalDateTime createdAt;
+    private Long resolvedBy;
+    private LocalDateTime resolvedAt;
+    private String resolutionNotes;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/mapper/ReportMapper.java b/src/main/java/com/example/g8backend/mapper/ReportMapper.java
new file mode 100644
index 0000000..01e5630
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/ReportMapper.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Report;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface ReportMapper extends BaseMapper<Report> {
+    // 自定义查询方法(如按状态查询)
+    @Select("SELECT * FROM reports WHERE status = #{status}")
+    List<Report> selectByStatus(@Param("status") String status);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IReportService.java b/src/main/java/com/example/g8backend/service/IReportService.java
new file mode 100644
index 0000000..c1cf775
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IReportService.java
@@ -0,0 +1,15 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Report;
+import java.util.List;
+
+public interface IReportService {
+    // 用户提交举报
+    boolean submitReport(Long userId, Long postId, String reason);
+
+    // 管理员处理举报
+    boolean resolveReport(Long reportId, Long adminUserId, String status, String notes);
+
+    // 获取举报列表(按状态过滤)
+    List<Report> getReports(String status);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java
new file mode 100644
index 0000000..f27de25
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/ReportServiceImpl.java
@@ -0,0 +1,64 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.mapper.ReportMapper;
+import com.example.g8backend.service.IReportService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ReportServiceImpl implements IReportService {
+    private final ReportMapper reportMapper;
+
+    @Override
+    @Transactional
+    public boolean submitReport(Long userId, Long postId, String reason) {
+        // 检查是否已存在未处理的举报
+        Long count = reportMapper.selectCount(
+                new QueryWrapper<Report>()
+                        .eq("user_id", userId)
+                        .eq("post_id", postId)
+                        .eq("status", "pending")
+        );
+        if (count > 0) {
+            throw new IllegalArgumentException("您已举报过该帖子,请勿重复提交");
+        }
+
+        // 创建举报记录
+        Report report = new Report();
+        report.setPostId(postId);
+        report.setUserId(userId);
+        report.setReason(reason);
+        report.setStatus("pending");
+        report.setCreatedAt(LocalDateTime.now());
+        return reportMapper.insert(report) > 0;
+    }
+
+    @Override
+    @Transactional
+    public boolean resolveReport(Long reportId, Long adminUserId, String status, String notes) {
+        Report report = reportMapper.selectById(reportId);
+        if (report == null) {
+            throw new IllegalArgumentException("举报记录不存在");
+        }
+        report.setStatus(status);
+        report.setResolvedBy(adminUserId);
+        report.setResolvedAt(LocalDateTime.now());
+        report.setResolutionNotes(notes);
+        return reportMapper.updateById(report) > 0;
+    }
+
+    @Override
+    public List<Report> getReports(String status) {
+        return reportMapper.selectList(
+                new QueryWrapper<Report>().eq("status", status)
+        );
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index f6d061b..1ee0c87 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -143,4 +143,19 @@
   FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
   FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
   INDEX idx_post_ratings_post_id ON post_ratings (post_id)
+);
+
+CREATE TABLE IF NOT EXISTS `reports` (
+    `report_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '举报ID',
+    `post_id` INT NOT NULL COMMENT '被举报的帖子ID',
+    `user_id` INT NOT NULL COMMENT '举报人ID',
+    `reason` TEXT NOT NULL COMMENT '举报原因',
+    `status` ENUM('pending', 'resolved', 'rejected') DEFAULT 'pending' COMMENT '处理状态(待处理/已解决/已驳回)',
+    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '举报时间',
+    `resolved_by` INT DEFAULT NULL COMMENT '处理人ID(管理员)',
+    `resolved_at` TIMESTAMP DEFAULT NULL COMMENT '处理时间',
+    `resolution_notes` TEXT DEFAULT NULL COMMENT '处理备注',
+    FOREIGN KEY (`post_id`) REFERENCES `posts`(`post_id`),
+    FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`),
+    FOREIGN KEY (`resolved_by`) REFERENCES `users`(`user_id`)
 );
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java b/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java
new file mode 100644
index 0000000..acd2730
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/ReportServiceImplTest.java
@@ -0,0 +1,113 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.g8backend.entity.Report;
+import com.example.g8backend.mapper.ReportMapper;
+import com.example.g8backend.service.impl.ReportServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class ReportServiceImplTest {
+
+    private ReportMapper reportMapper;
+    private ReportServiceImpl reportService;
+
+    @BeforeEach
+    void setUp() {
+        reportMapper = mock(ReportMapper.class);
+        reportService = new ReportServiceImpl(reportMapper);
+    }
+
+    @Test
+    void submitReport_shouldSucceedIfNoPendingExists() {
+        Long userId = 1L;
+        Long postId = 2L;
+        String reason = "Spam content";
+
+        // 模拟:没有已有 pending 状态的举报
+        when(reportMapper.selectCount(any(QueryWrapper.class))).thenReturn(0L);
+        when(reportMapper.insert(any(Report.class))).thenReturn(1);
+
+        boolean result = reportService.submitReport(userId, postId, reason);
+
+        assertTrue(result);
+        verify(reportMapper).insert(any(Report.class));
+    }
+
+    @Test
+    void submitReport_shouldThrowIfPendingExists() {
+        Long userId = 1L;
+        Long postId = 2L;
+        String reason = "重复举报";
+
+        when(reportMapper.selectCount(any(QueryWrapper.class))).thenReturn(1L);
+
+        IllegalArgumentException exception = assertThrows(
+                IllegalArgumentException.class,
+                () -> reportService.submitReport(userId, postId, reason)
+        );
+
+        assertEquals("您已举报过该帖子,请勿重复提交", exception.getMessage());
+        verify(reportMapper, never()).insert(any(Report.class));
+    }
+
+    @Test
+    void resolveReport_shouldUpdateExistingReport() {
+        Long reportId = 10L;
+        Long adminId = 100L;
+        String status = "resolved";
+        String notes = "已处理";
+
+        Report mockReport = new Report();
+        mockReport.setReportId(reportId);
+        mockReport.setStatus("pending");
+
+        when(reportMapper.selectById(reportId)).thenReturn(mockReport);
+        when(reportMapper.updateById(any(Report.class))).thenReturn(1);
+
+        boolean result = reportService.resolveReport(reportId, adminId, status, notes);
+
+        assertTrue(result);
+        verify(reportMapper).updateById(any(Report.class));
+    }
+
+    @Test
+    void resolveReport_shouldThrowIfNotFound() {
+        Long reportId = 999L;
+
+        when(reportMapper.selectById(reportId)).thenReturn(null);
+
+        IllegalArgumentException exception = assertThrows(
+                IllegalArgumentException.class,
+                () -> reportService.resolveReport(reportId, 1L, "resolved", "无效举报")
+        );
+
+        assertEquals("举报记录不存在", exception.getMessage());
+        verify(reportMapper, never()).updateById(any(Report.class));
+    }
+
+    @Test
+    void getReports_shouldReturnMatchingStatus() {
+        Report r1 = new Report();
+        r1.setStatus("pending");
+
+        Report r2 = new Report();
+        r2.setStatus("pending");
+
+        when(reportMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Arrays.asList(r1, r2));
+
+        List<Report> reports = reportService.getReports("pending");
+
+        assertEquals(2, reports.size());
+        assertEquals("pending", reports.get(0).getStatus());
+        verify(reportMapper).selectList(any(QueryWrapper.class));
+    }
+}