user module API

POST /resource
POST /resource/purchase
POST /resource/like
POST /resource/collection

Change-Id: I3d9f342a34321ae10b31976ee583188b5386dccb
diff --git a/src/main/java/com/g9/g9backend/controller/ResourceController.java b/src/main/java/com/g9/g9backend/controller/ResourceController.java
index b01898d..b6726cf 100644
--- a/src/main/java/com/g9/g9backend/controller/ResourceController.java
+++ b/src/main/java/com/g9/g9backend/controller/ResourceController.java
@@ -1,9 +1,18 @@
 package com.g9.g9backend.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.g9.g9backend.mapper.UserPurchaseMapper;
+import com.g9.g9backend.pojo.*;
+import com.g9.g9backend.pojo.DTO.*;
+import com.g9.g9backend.service.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
+
 
 /**
  * ResourceController 资源控制器类,处理与资源相关的请求
@@ -14,5 +23,204 @@
 @RequestMapping("/resource")
 public class ResourceController {
 
+    private final ResourceService resourceService;
+
+    private final GameplayService gameplayService;
+
+    private final RewardService rewardService;
+
+    private final UserUploadService userUploadService;
+
+    private final CommunityService communityService;
+
+    private final UserService userService;
+
+    private final UserPurchaseService userPurchaseService;
+
+    private final UserLikeService userLikeService;
+
+    private final UserCollectionService userCollectionService;
+
+    private final NotificationService notificationService;
+
+    public ResourceController(ResourceService resourceService, GameplayService gameplayService, RewardService rewardService, UserUploadService userUploadService, CommunityService communityService, UserService userService, UserPurchaseMapper userPurchaseMapper, UserPurchaseService userPurchaseService, UserLikeService userLikeService, UserCollectionService userCollectionService, NotificationService notificationService) {
+        this.resourceService = resourceService;
+        this.gameplayService = gameplayService;
+        this.rewardService = rewardService;
+        this.userUploadService = userUploadService;
+        this.communityService = communityService;
+        this.userService = userService;
+        this.userPurchaseService = userPurchaseService;
+        this.userLikeService = userLikeService;
+        this.userCollectionService = userCollectionService;
+        this.notificationService = notificationService;
+    }
+
     private final Logger logger = LoggerFactory.getLogger(ResourceController.class);
-}
+
+    /**
+     * 上传资源
+     *
+     * @param postResourceDTO 上传资源信息
+     * @return 上传资源结果
+     */
+    @PostMapping
+    public ResponseEntity<String> uploadResource(@RequestBody PostResourceDTO postResourceDTO) {
+        // 存资源
+        Resource resource = postResourceDTO.getResource();
+        resourceService.save(resource);
+        // 存玩法列表
+        String[] gameplayList = postResourceDTO.getGameplayList();
+        for (String gameplayName : gameplayList) {
+            Gameplay gameplay = new Gameplay();
+            gameplay.setGameplayName(gameplayName);
+            gameplay.setResourceId(postResourceDTO.getResource().getResourceId());
+            gameplayService.save(gameplay);
+        }
+        // 完成对应悬赏
+        if (postResourceDTO.getCompleteRewardId() != 0) {
+            UpdateWrapper<Reward> rewardUpdate = new UpdateWrapper<>();
+            rewardUpdate.eq("reward_id", postResourceDTO.getCompleteRewardId()).set("completed_by", postResourceDTO.getUserId()).set("completed_at", postResourceDTO.getResource().getUploadTime()).set("resource_id", postResourceDTO.getResource().getResourceId());
+            rewardService.update(rewardUpdate);
+        }
+        // 存用户上传表
+        UserUpload userUpload = new UserUpload();
+        userUpload.setUserId(postResourceDTO.getUserId());
+        userUpload.setResourceId(postResourceDTO.getResource().getResourceId());
+        userUploadService.save(userUpload);
+        // 创建资源社区
+        Community community = new Community();
+        community.setCommunityName(postResourceDTO.getResource().getResourceName());
+        community.setType(postResourceDTO.getResource().getClassify());
+        community.setResourceId(postResourceDTO.getResource().getResourceId());
+        communityService.save(community);
+        return ResponseEntity.ok("");
+    }
+
+    /**
+     * 购买资源
+     *
+     * @param userResourceDTO 购买资源信息
+     * @return 购买资源结果
+     */
+    @PostMapping("purchase")
+    public ResponseEntity<String> purchaseResource(@RequestBody UserResourceDTO userResourceDTO) {
+
+        QueryWrapper<User> userQuery = new QueryWrapper<>();
+        userQuery.eq("user_id", userResourceDTO.getUserId());
+        User user = userService.getOne(userQuery);
+
+        QueryWrapper<Resource> ResourceQuery = new QueryWrapper<>();
+        ResourceQuery.eq("resource_id", userResourceDTO.getResourceId());
+        Resource resource = resourceService.getOne(ResourceQuery);
+
+        if (user.getCredits() < resource.getPrice()) {
+            // 积分余额不足
+            logger.info("The balance of points is insufficient to cover the price of this resource: {}", resource.getPrice());
+            return ResponseEntity.status(412).body("");
+        } else {
+            // 扣除用户积分
+            UpdateWrapper<User> userUpdate = new UpdateWrapper<>();
+            userUpdate.eq("user_id", user.getUserId()).set("credits", user.getCredits() - resource.getPrice());
+            userService.update(userUpdate);
+            // 添加购买资源记录
+            UserPurchase userPurchase = new UserPurchase();
+            userPurchase.setUserId(user.getUserId());
+            userPurchase.setResourceId(resource.getResourceId());
+            userPurchaseService.save(userPurchase);
+            // 给上传该资源的用户发送通知
+            Notification notification = new Notification();
+            QueryWrapper<UserUpload> userUploadQuery = new QueryWrapper<>();
+            userUploadQuery.eq("resource_id", userResourceDTO.getResourceId());
+            UserUpload userUpload = userUploadService.getOne(userUploadQuery);
+            notification.setUserId(userUpload.getUserId());
+            notification.setTitle("资源被购买");
+            notification.setContent("你的资源:" + resource.getResourceName() + " 被: " + user.getUsername() + " 购买了!");
+            notification.setCreateAt(new Date());
+            notification.setRead(false);
+            notification.setTriggeredBy(userResourceDTO.getUserId());
+            notification.setRelatedId(userResourceDTO.getResourceId());
+            notificationService.save(notification);
+            return ResponseEntity.ok("");
+        }
+    }
+
+    /**
+     * 点赞资源
+     *
+     * @param userResourceDTO 点赞资源信息
+     * @return 点赞资源结果
+     */
+    @PostMapping("like")
+    public ResponseEntity<String> likeResource(@RequestBody UserResourceDTO userResourceDTO) {
+
+        QueryWrapper<User> userQuery = new QueryWrapper<>();
+        userQuery.eq("user_id", userResourceDTO.getUserId());
+        User user = userService.getOne(userQuery);
+
+        QueryWrapper<Resource> ResourceQuery = new QueryWrapper<>();
+        ResourceQuery.eq("resource_id", userResourceDTO.getResourceId());
+        Resource resource = resourceService.getOne(ResourceQuery);
+
+        UserLike userLike = new UserLike();
+        userLike.setUserId(userResourceDTO.getUserId());
+        userLike.setResourceId(userResourceDTO.getResourceId());
+        userLikeService.save(userLike);
+
+        // 给上传该资源的用户发送通知
+        Notification notification = new Notification();
+        QueryWrapper<UserUpload> userUploadQuery = new QueryWrapper<>();
+        userUploadQuery.eq("resource_id", userResourceDTO.getResourceId());
+        UserUpload userUpload = userUploadService.getOne(userUploadQuery);
+        notification.setUserId(userUpload.getUserId());
+        notification.setTitle("资源被点赞");
+        notification.setContent("你的资源:" + resource.getResourceName() + " 被: " + user.getUsername() + " 点赞了!");
+        notification.setCreateAt(new Date());
+        notification.setRead(false);
+        notification.setTriggeredBy(userResourceDTO.getUserId());
+        notification.setRelatedId(userResourceDTO.getResourceId());
+        notificationService.save(notification);
+
+        return ResponseEntity.ok("");
+    }
+
+    /**
+     * 收藏资源
+     *
+     * @param userResourceDTO 收藏资源信息
+     * @return 收藏资源结果
+     */
+    @PostMapping("collection")
+    public ResponseEntity<String> collectResource(@RequestBody UserResourceDTO userResourceDTO) {
+
+        QueryWrapper<User> userQuery = new QueryWrapper<>();
+        userQuery.eq("user_id", userResourceDTO.getUserId());
+        User user = userService.getOne(userQuery);
+
+        QueryWrapper<Resource> ResourceQuery = new QueryWrapper<>();
+        ResourceQuery.eq("resource_id", userResourceDTO.getResourceId());
+        Resource resource = resourceService.getOne(ResourceQuery);
+
+        UserCollection userCollection = new UserCollection();
+        userCollection.setUserId(userResourceDTO.getUserId());
+        userCollection.setResourceId(userResourceDTO.getResourceId());
+        userCollectionService.save(userCollection);
+
+
+        // 给上传该资源的用户发送通知
+        Notification notification = new Notification();
+        QueryWrapper<UserUpload> userUploadQuery = new QueryWrapper<>();
+        userUploadQuery.eq("resource_id", userResourceDTO.getResourceId());
+        UserUpload userUpload = userUploadService.getOne(userUploadQuery);
+        notification.setUserId(userUpload.getUserId());
+        notification.setTitle("资源被收藏");
+        notification.setContent("你的资源:" + resource.getResourceName() + " 被: " + user.getUsername() + " 收藏了!");
+        notification.setCreateAt(new Date());
+        notification.setRead(false);
+        notification.setTriggeredBy(userResourceDTO.getUserId());
+        notification.setRelatedId(userResourceDTO.getResourceId());
+        notificationService.save(notification);
+
+        return ResponseEntity.ok("");
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/g9/g9backend/controller/ResourceControllerTest.java b/src/test/java/com/g9/g9backend/controller/ResourceControllerTest.java
index c02be49..9d0600b 100644
--- a/src/test/java/com/g9/g9backend/controller/ResourceControllerTest.java
+++ b/src/test/java/com/g9/g9backend/controller/ResourceControllerTest.java
@@ -1,5 +1,204 @@
 package com.g9.g9backend.controller;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.g9.g9backend.pojo.*;
+import com.g9.g9backend.pojo.DTO.*;
+import com.g9.g9backend.service.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Date;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/*
+    @SpringBootTest 启动一个完整的 Spring 应用上下文,用来测试整个应用的行为
+    @AutoConfigureMockMvc 自动配置 MockMvc 对象,MockMvc 是 Spring 提供的工具,用来模拟 HTTP 请求,从而不需要启动真正的服务器来发送HTTP请求就能进行测试
+    注意@SpringBootTest 已经自动配置了 Mockito 或 Spring 的 Mock 环境,再在setup中手动调用 openMocks(this) 可能导致 Mock 对象被重复初始化
+    出现状态丢失问题(如 when().thenReturn() 规则失效),所以改为使用 JUnit 5 默认运行器,不需要在这里写注解
+*/
 public class ResourceControllerTest {
 
+    // MockMvc对象
+    private MockMvc mockMvc;
+
+    // @InjectMocks告诉测试框架(Mockito)创建 ResourceController 的实例,并自动将标注了 @Mock 的依赖注入到这个实例中
+    @InjectMocks
+    private ResourceController resourceController;
+
+    // @Mock告诉 Mockito 创建这些依赖的“模拟对象”,用来模拟服务行为,和controller层使用的service依赖一一对应即可
+    @Mock
+    private ResourceService resourceService;
+
+    @Mock
+    private GameplayService gameplayService;
+
+    @Mock
+    private RewardService rewardService;
+
+    @Mock
+    private UserUploadService userUploadService;
+
+    @Mock
+    private CommunityService communityService;
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private UserPurchaseService userPurchaseService;
+
+    @Mock
+    private UserLikeService userLikeService;
+
+    @Mock
+    private UserCollectionService userCollectionService;
+
+    @Mock
+    private NotificationService notificationService;
+
+    // ObjectMapper对象用于将 Java 对象和 JSON 字符串互相转换 ,因为在测试中,需要把请求参数或返回响应转换成 JSON 字符串形式来传输
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 这个方法会在每个测试方法执行之前运行。用来设置测试的初始化逻辑
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        mockMvc = MockMvcBuilders.standaloneSetup(resourceController).build();
+    }
+
+    // 以下是对各接口的测试,注意测试方法是要对每个接口的每个响应测试(如果一个接口有三种响应,就写三个测试方法)
+    // 上传资源
+    @Test
+    public void testUploadResource() throws Exception {
+
+        // 创建测试用的请求数据
+        PostResourceDTO postResourceDTO = new PostResourceDTO();
+        Resource resource = new Resource();
+        resource.setResourceId(1);
+        resource.setResourceName("Test Resource");
+        resource.setUploadTime(new Date());
+        resource.setLastUpdateTime(new Date());
+        resource.setPrice(1);
+        resource.setClassify("map");
+        postResourceDTO.setResource(resource);
+        postResourceDTO.setGameplayList(new String[]{"Gameplay1", "Gameplay2"});
+        postResourceDTO.setUserId(1);
+
+        /*
+            模拟服务层的行为,when(...).thenReturn(...)表示当某个服务方法被调用时,返回一个指定的结果(注意mybatis-plus的这些方法一般返回是boolean类型,所以写true不能写null)
+            同样是测试方法,测试粒度有区别
+            有简单写法:when(gameplayService.save(any())).thenReturn(true);只验证方法是否被调用(间接验证逻辑是否执行到该服务方法),不关心方法调用的次数和传递的参数
+            也有复杂写法:额外用 verify 验证调用次数和 ArgumentCaptor 验证参数是否传递正确和InOrder 来验证方法的调用顺序是否正确...
+        */
+
+        // 测试主是要测试传参是否能正确接收,接口调用过程是否使用过这些方法(所以使用到的方法一般来说应该按顺序写在这,不能缺不能多也不能乱序,但是这里实际因为测试粒度不够细,没有对方法调用过程的验证,所以即是方法缺少或调用过程有问题,也不会报错),返回响应是否发送正确
+        when(resourceService.save(any())).thenReturn(true);
+        when(gameplayService.save(any())).thenReturn(true);
+        when(userUploadService.save(any())).thenReturn(true);
+        when(communityService.save(any())).thenReturn(true);
+        when(rewardService.update(any())).thenReturn(true);
+
+        // 模拟 HTTP 请求
+        mockMvc.perform(post("/resource") // 使用 MockMvc 模拟用户提交 POST 请求到 /resource 接口
+                        .contentType(MediaType.APPLICATION_JSON) // 设置请求体是 JSON 格式
+                        .content(objectMapper.writeValueAsString(postResourceDTO))) //把请求参数对象:postResourceDTO 转成 JSON 字符串,作为请求体内容发送
+                .andExpect(status().isOk()); // 期望接口返回 HTTP 状态码 200(成功)
+    }
+
+    // 购买资源
+    @Test
+    public void testPurchaseResource_success() throws Exception {
+
+        // 用户积分足够
+        User user = new User();
+        user.setUserId(1);
+        user.setCredits(100);
+        Resource resource = new Resource();
+        resource.setResourceId(1);
+        resource.setPrice(50);
+
+        when(userService.getOne(any())).thenReturn(user);
+        when(resourceService.getOne(any())).thenReturn(resource);
+        when(userService.update(any())).thenReturn(true);
+        when(userPurchaseService.save(any())).thenReturn(true);
+        when(userUploadService.getOne(any())).thenReturn(new UserUpload(2, 1));
+        when(notificationService.save(any())).thenReturn(true);
+
+        mockMvc.perform(post("/resource/purchase")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(new UserResourceDTO(1, 1))))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testPurchaseResource_purchaseFailed() throws Exception {
+
+        //用户积分不足
+        User user = new User();
+        user.setUserId(1);
+        user.setCredits(10);
+        Resource resource = new Resource();
+        resource.setResourceId(1);
+        resource.setPrice(50);
+
+        when(userService.getOne(any())).thenReturn(user);
+        when(resourceService.getOne(any())).thenReturn(resource);
+
+        mockMvc.perform(post("/resource/purchase")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(new UserResourceDTO(1, 1))))
+                .andExpect(status().is(412));
+    }
+
+    // 点赞资源
+    @Test
+    public void testLikeResource() throws Exception {
+
+        User user = new User();
+        user.setUserId(1);
+        Resource resource = new Resource();
+        resource.setResourceId(1);
+
+        when(userService.getOne(any())).thenReturn(user);
+        when(resourceService.getOne(any())).thenReturn(resource);
+        when(userLikeService.save(any())).thenReturn(true);
+        when(userUploadService.getOne(any())).thenReturn(new UserUpload(2, 1));
+        when(notificationService.save(any())).thenReturn(true);
+
+        mockMvc.perform(post("/resource/like")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(new UserResourceDTO(1, 1))))
+                .andExpect(status().isOk());
+    }
+
+    // 收藏资源
+    @Test
+    public void testCollectResource() throws Exception {
+
+        User user = new User();
+        user.setUserId(1);
+        Resource resource = new Resource();
+        resource.setResourceId(1);
+
+        when(userService.getOne(any())).thenReturn(user);
+        when(resourceService.getOne(any())).thenReturn(resource);
+        when(userCollectionService.save(any())).thenReturn(true);
+        when(userUploadService.getOne(any())).thenReturn(new UserUpload(2, 1));
+        when(notificationService.save(any())).thenReturn(true);
+
+        mockMvc.perform(post("/resource/collection")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(new UserResourceDTO(1, 1))))
+                .andExpect(status().isOk());
+    }
 }
\ No newline at end of file