我是人,我提交了悬赏功能哦!悬赏功能是:1.人可以发布悬赏 2.人可以回复悬赏 3.只有回复人和悬赏发布者可以下载回复的附件

Change-Id: I269fb69c6ee4dd695a38fa0c91fa8fbe72fc5322
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index aa3f738..f147ecc 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -114,6 +114,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-system</artifactId>
+            <version>3.8.8</version>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
index 2869856..8a5fdfe 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
@@ -1,14 +1,26 @@
 package com.ruoyi;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
 
 /**
  * 启动程序
  * 
  * @author ruoyi
  */
+@ComponentScan(basePackages = {
+        "com.ruoyi",
+        "bounty"  // 新增您的包路径
+})
+// 关键添加:扩大MyBatis接口扫描范围
+@MapperScan(basePackages = {
+        "com.ruoyi.**.mapper",
+        "bounty.mapper"  // 如果bounty下有Mapper接口
+})
+
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 public class RuoYiApplication {
     public static void main(String[] args) {
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java
new file mode 100644
index 0000000..55d2e34
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java
@@ -0,0 +1,14 @@
+package com.ruoyi;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/uploads/**")
+                .addResourceLocations("file:uploads/");
+    }
+}
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index f80b3fd..2789eae 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -55,16 +55,10 @@
     basename: i18n/messages
   profiles:
     active: druid
-  # 静态资源映射
-  mvc:
-    static-path-pattern: /static/**
-  web:
-    resources:
-      static-locations: classpath:/static/
   # 文件上传
   servlet:
     multipart:
-      # 单个文件大小
+      enabled: true
       max-file-size: 10MB
       # 设置总上传的文件大小
       max-request-size: 20MB
@@ -96,6 +90,10 @@
           max-active: 8
           # #连接池最大阻塞等待时间(使用负值表示没有限制)
           max-wait: -1ms
+  mvc:
+    static-path-pattern: /**
+    resources:
+      static-locations: file:uploads/
 
 # token配置
 token:
@@ -110,7 +108,7 @@
 mybatis:
   # 搜索指定包别名
   typeAliasesPackage: com.ruoyi.**.domain
-  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  # 重点修改1:扩展XML扫描路径
   mapperLocations: classpath*:mapper/**/*Mapper.xml
   # 加载全局的配置文件
   configLocation: classpath:mybatis/mybatis-config.xml
@@ -119,8 +117,8 @@
 mybatis-plus:
   # 搜索指定包别名
   typeAliasesPackage: com.ruoyi.**.domain
-  # 配置mapper的扫描,找到所有的mapper.xml映射文件
-  mapperLocations: classpath:mapper/*.xml
+  # 重点修改2:覆盖原路径,支持多模块
+  mapper-locations: classpath*:mapper/**/*.xml
   configLocation: classpath:mybatis/mybatis-config.xml
   # 全局配置
   global-config:
@@ -132,7 +130,7 @@
     db-column-underline: true
     # 刷新mapper 调试神器
     refresh-mapper: true
-    # 配置
+    # 重点修改3:明确开启驼峰映射
     configuration:
       # 驼峰式命名
       map-underscore-to-camel-case: true
@@ -145,6 +143,9 @@
       # 允许 JDBC 支持自动生成主键
       use-generated-keys: true
 
+# 新增文件上传路径配置(放在末尾)
+file:
+  upload-dir: /var/www/uploads/
 
 # PageHelper分页插件
 pagehelper:
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountyControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountyControllerTest.java
new file mode 100644
index 0000000..121d59e
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountyControllerTest.java
@@ -0,0 +1,140 @@
+// BountyControllerTest.java
+package com.ruoyi.web.controller.bounty;
+
+import bounty.controller.BountyController;
+import bounty.domain.Bounty;
+import bounty.service.BountyService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@ExtendWith(MockitoExtension.class)
+class BountyControllerTest {
+
+    @Mock
+    private BountyService bountyService;
+
+    @InjectMocks
+    private BountyController bountyController;
+
+    private MockMvc mockMvc;
+    private ObjectMapper objectMapper;
+
+    @BeforeEach
+    void setUp() {
+        // 创建模拟的安全上下文
+        SysUser sysUser = new SysUser();
+        sysUser.setUserId(1L);
+        sysUser.setUserName("testUser");
+
+        LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+        UsernamePasswordAuthenticationToken authToken =
+                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
+        mockMvc = MockMvcBuilders.standaloneSetup(bountyController).build();
+        objectMapper = new ObjectMapper();
+    }
+
+    @Test
+    void testSaveBountySuccess() throws Exception {
+        Bounty bounty = new Bounty();
+        bounty.setTitle("测试悬赏");
+
+        when(bountyService.saveBounty(any(Bounty.class))).thenReturn(true);
+
+        mockMvc.perform(post("/bounties")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(bounty)))
+                .andExpect(status().isOk());
+
+        verify(bountyService, times(1)).saveBounty(any(Bounty.class));
+    }
+
+    @Test
+    void testSaveBountyFailure() throws Exception {
+        Bounty bounty = new Bounty();
+        bounty.setTitle("测试悬赏");
+
+        when(bountyService.saveBounty(any(Bounty.class))).thenReturn(false);
+
+        mockMvc.perform(post("/bounties")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(bounty)))
+                .andExpect(status().isInternalServerError());
+
+        verify(bountyService, times(1)).saveBounty(any(Bounty.class));
+    }
+
+    @Test
+    void testGetBounties() throws Exception {
+        List<Bounty> bounties = new ArrayList<>();
+        when(bountyService.getBounties()).thenReturn(bounties);
+
+        mockMvc.perform(get("/bounties"))
+                .andExpect(status().isOk());
+
+        verify(bountyService, times(1)).getBounties();
+    }
+
+    @Test
+    void testGetBountyById() throws Exception {
+        Bounty bounty = new Bounty();
+        bounty.setId(1L);
+        bounty.setTitle("测试悬赏");
+
+        when(bountyService.getBountyById(1L)).thenReturn(bounty);
+
+        mockMvc.perform(get("/bounties/1"))
+                .andExpect(status().isOk());
+
+        verify(bountyService, times(1)).getBountyById(1L);
+    }
+
+    @Test
+    void testGetNonExistentBounty() throws Exception {
+        when(bountyService.getBountyById(999L)).thenReturn(null);
+
+        mockMvc.perform(get("/bounties/999"))
+                .andExpect(status().isNotFound());
+
+        verify(bountyService, times(1)).getBountyById(999L);
+    }
+
+    @Test
+    void testPublishBountySuccess() throws Exception {
+        Bounty bounty = new Bounty();
+        bounty.setTitle("发布测试");
+
+        when(bountyService.publishBounty(any(Bounty.class))).thenReturn(true);
+
+        mockMvc.perform(post("/bounties/publish")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(bounty)))
+                .andExpect(status().isOk());
+
+        verify(bountyService, times(1)).publishBounty(any(Bounty.class));
+    }
+}
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountySubmissionControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountySubmissionControllerTest.java
new file mode 100644
index 0000000..b7f7f99
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/bounty/BountySubmissionControllerTest.java
@@ -0,0 +1,165 @@
+
+// BountySubmissionControllerTest.java
+package com.ruoyi.web.controller.bounty;
+
+import bounty.controller.BountySubmissionController;
+import bounty.domain.BountySubmission;
+import bounty.service.BountySubmissionService;
+import bounty.service.FileStorageService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@ExtendWith(MockitoExtension.class)
+class BountySubmissionControllerTest {
+
+    @Mock
+    private BountySubmissionService bountySubmissionService;
+
+    @Mock
+    private FileStorageService fileStorageService;
+
+    @InjectMocks
+    private BountySubmissionController bountySubmissionController;
+
+    private MockMvc mockMvc;
+    private ObjectMapper objectMapper;
+
+    @BeforeEach
+    void setUp() {
+        // 创建模拟的安全上下文
+        SysUser sysUser = new SysUser();
+        sysUser.setUserId(1L);
+        sysUser.setUserName("testUser");
+
+        LoginUser loginUser = new LoginUser(1L, 1L, sysUser, new HashSet<>());
+
+        UsernamePasswordAuthenticationToken authToken =
+                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
+        mockMvc = MockMvcBuilders.standaloneSetup(bountySubmissionController).build();
+        objectMapper = new ObjectMapper();
+    }
+
+    @Test
+    void testSaveBountySubmissionSuccess() throws Exception {
+        BountySubmission submission = new BountySubmission();
+        submission.setBountyId(1L);
+
+        String submissionJson = objectMapper.writeValueAsString(submission);
+
+        when(fileStorageService.saveFile(any(MultipartFile.class))).thenReturn("/uploads/test.txt");
+        when(bountySubmissionService.saveBountySubmission(any(BountySubmission.class))).thenReturn(true);
+
+        mockMvc.perform(MockMvcRequestBuilders.multipart("/bounty-submissions")
+                        .file(new MockMultipartFile("submission", "", MediaType.APPLICATION_JSON_VALUE, submissionJson.getBytes()))
+                        .file("file", "test content".getBytes())
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isOk());
+
+        verify(fileStorageService, times(1)).saveFile(any(MultipartFile.class));
+        verify(bountySubmissionService, times(1)).saveBountySubmission(any(BountySubmission.class));
+    }
+
+    @Test
+    void testGetBountySubmissions() throws Exception {
+        // 构造分页对象
+        Page<BountySubmission> page = new Page<>(1, 10);
+        page.setTotal(0);
+        page.setRecords(new ArrayList<>());
+
+        // 模拟 service 返回空分页
+        when(bountySubmissionService.getBountySubmissionsByPage(any(Page.class), eq(1L))).thenReturn(page);
+
+        // 发起 GET 请求
+        mockMvc.perform(get("/bounty-submissions")
+                        .param("current", "1")
+                        .param("size", "10")
+                        .param("bountyId", "1"))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void testAdoptSubmissionSuccess() throws Exception {
+        when(bountySubmissionService.adoptSubmission(1L)).thenReturn(true);
+
+        mockMvc.perform(put("/bounty-submissions/1/adopt"))
+                .andExpect(status().isOk());
+
+        verify(bountySubmissionService, times(1)).adoptSubmission(1L);
+    }
+
+    @Test
+    void testAdoptNonExistentSubmission() throws Exception {
+        when(bountySubmissionService.adoptSubmission(999L)).thenReturn(false);
+
+        mockMvc.perform(put("/bounty-submissions/999/adopt"))
+                .andExpect(status().isInternalServerError());
+
+        verify(bountySubmissionService, times(1)).adoptSubmission(999L);
+    }
+
+    @Test
+    void testUploadAttachment() throws Exception {
+        when(fileStorageService.saveFile(any(MultipartFile.class))).thenReturn("/uploads/test.jpg");
+
+        mockMvc.perform(multipart("/bounty-submissions/upload")
+                        .file("file", "test content".getBytes())
+                        .param("filename", "test.jpg"))
+                .andExpect(status().isOk());
+
+        verify(fileStorageService, times(1)).saveFile(any(MultipartFile.class));
+    }
+
+    @Test
+    void testDownloadAttachment() throws Exception {
+        Path uploadDir = Paths.get("uploads/");
+        if (!Files.exists(uploadDir)) {
+            Files.createDirectories(uploadDir);
+        }
+
+        Path testFilePath = uploadDir.resolve("test.jpg");
+        if (!Files.exists(testFilePath)) {
+            Files.write(testFilePath, "test content".getBytes());
+        }
+
+       mockMvc.perform(get("/bounty-submissions/download")
+                        .param("filename", "test.jpg"))
+                .andExpect(status().isOk())
+                .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
+                        Matchers.matchesPattern(".*filename=.*test\\.jpg.*")));
+    }
+}
\ No newline at end of file