hushuyu 4 bulan lalu
induk
melakukan
7baa507593

+ 31 - 0
web/src/main/java/cn/seecoder/web/controller/fork/ForkController.java

@@ -1,21 +1,52 @@
 package cn.seecoder.web.controller.fork;
 
+import cn.seecoder.web.model.po.user.UserPO;
 import cn.seecoder.web.model.vo.fork.ForkVO;
 import cn.seecoder.web.service.fork.ForkService;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.concurrent.ConcurrentHashMap;
 
 @RestController
 @RequestMapping("/fork")
 @RequiredArgsConstructor
 public class ForkController {
+    private static final long LIMIT_WINDOW_MS = 200L;
     private final ForkService forkService;
+    private final ConcurrentHashMap<String, Long> lastRequestTimeMap = new ConcurrentHashMap<>();
 
     @PostMapping
     public ForkVO forkProject(@RequestBody ForkVO forkVO) {
+        enforceRateLimit();
         return forkService.forkProject(forkVO);
     }
+
+    private void enforceRateLimit() {
+        String key = resolveRateLimitKey();
+        long now = System.currentTimeMillis();
+        lastRequestTimeMap.compute(key, (k, lastTime) -> {
+            if (lastTime != null && now - lastTime < LIMIT_WINDOW_MS) {
+                throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "请求过于频繁,请 1 秒后重试");
+            }
+            return now;
+        });
+    }
+
+    private String resolveRateLimitKey() {
+        if (SecurityContextHolder.getContext().getAuthentication() == null) {
+            return "anonymous";
+        }
+        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        if (principal instanceof UserPO) {
+            return "uid:" + ((UserPO) principal).getId();
+        }
+        return String.valueOf(principal);
+    }
 }

+ 60 - 21
web/src/main/java/cn/seecoder/web/service/fork/impl/ForkServiceImpl.java

@@ -12,62 +12,84 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+import java.util.Set;
 
 @RequiredArgsConstructor
 @Service
 @Slf4j
 public class ForkServiceImpl implements ForkService {
+    private static final String ALPHABETS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    private static final int FORK_NAME_SUFFIX_LEN = 6;
+    private static final int MAX_RETRY = 10;
     private final SeecoderGitlabApi gitlabApi;
     private final ProjectMapper projectMapper;
     private final ApplicationProperties properties;
-    private final String ALPHABETS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
     private final Random random = new Random();
 
     @Override
     public ForkVO forkProject(ForkVO forkVO) {
+        if (forkVO == null || forkVO.getProjectId() == null) {
+            log.error("fork 参数非法, forkVO={}", forkVO);
+            return null;
+        }
+        if (SecurityContextHolder.getContext().getAuthentication() == null
+                || !(SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof UserPO)) {
+            log.error("未登录或登录态异常, projectId={}", forkVO.getProjectId());
+            return null;
+        }
         UserPO principal = (UserPO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
         Integer userId = principal.getId();
         try {
             ProjectPO oldProject = projectMapper.getProjectById(forkVO.getProjectId());
             if (oldProject == null) {
-                log.error("不存在想要 Fork 的项目 id: " + forkVO.getProjectId());
+                log.error("不存在想要 Fork 的项目 id={}", forkVO.getProjectId());
                 return null;
             }
-            StringBuilder sb = new StringBuilder(oldProject.getName() + "-");
-            for (int i = 0; i < 6; ++i) {
-                sb.append(ALPHABETS.charAt(random.nextInt(ALPHABETS.length())));
-            }
-            String newName = sb.toString();
+
             List<com.nju.edu.gitlab.vo.ProjectVO> allProjects = gitlabApi.getAllProjectsByUserId(userId);
-            // 检测是否已经有重名项目
-            for (com.nju.edu.gitlab.vo.ProjectVO project: allProjects) {
-                if (Objects.equals(project.getProjectName(), newName)) {
-                    log.error("存在重名项目,项目名:" + newName);
-                    return null;
+            if (allProjects == null) {
+                log.error("获取用户项目列表失败, userId={}", userId);
+                return null;
+            }
+
+            Set<String> existingNames = new HashSet<>();
+            for (com.nju.edu.gitlab.vo.ProjectVO project : allProjects) {
+                if (project != null && project.getProjectName() != null) {
+                    existingNames.add(project.getProjectName());
                 }
             }
+            String newName = generateAvailableForkName(oldProject.getName(), existingNames);
+            if (newName == null) {
+                log.error("生成 Fork 项目名失败, baseName={}", oldProject.getName());
+                return null;
+            }
+
             // TODO 软工二没有userName与examId,这里先传null
             Integer newProjectId = gitlabApi.forkProject(forkVO.getProjectId(), userId, null, null);
+            if (newProjectId == null) {
+                log.error("Fork API 未返回项目 id, sourceProjectId={}, userId={}", forkVO.getProjectId(), userId);
+                return null;
+            }
             allProjects = gitlabApi.getAllProjectsByUserId(userId);
             // 拿 Fork 出来的项目的对象,主要是要拿到它的 Web URL
-            com.nju.edu.gitlab.vo.ProjectVO newProject = null;
-            for (com.nju.edu.gitlab.vo.ProjectVO project: allProjects) {
-                if (Objects.equals(project.getProjectId(), newProjectId)) {
-                    newProject = project;
-                    break;
-                }
-            }
+            com.nju.edu.gitlab.vo.ProjectVO newProject = allProjects.stream()
+                    .filter(Objects::nonNull)
+                    .filter(project -> Objects.equals(project.getProjectId(), newProjectId))
+                    .findFirst()
+                    .orElse(null);
             if (newProject == null) {
-                log.error("未知错误,貌似没有成功创建 Fork 出来的项目,新项目不存在,新 id: " + newProjectId);
+                log.error("未知错误,貌似没有成功创建 Fork 出来的项目,新项目不存在,新 id={}", newProjectId);
                 return null;
             }
+
             // 向 Devcloud 数据库中插入新项目的 PO
             // 添加 Gitlab Web Hook 回调
             ProjectPO projectPO = new ProjectPO(newProjectId,
-                    newName,
+                    newProject.getProjectName() == null ? newName : newProject.getProjectName(),
                     newProject.getWebUrl(),
                     oldProject.getDescription(),
                     false);
@@ -84,4 +106,21 @@ public class ForkServiceImpl implements ForkService {
             return null;
         }
     }
+
+    private String generateAvailableForkName(String baseName, Set<String> existingNames) {
+        if (baseName == null || baseName.isEmpty()) {
+            return null;
+        }
+        for (int i = 0; i < MAX_RETRY; i++) {
+            StringBuilder sb = new StringBuilder(baseName).append('-');
+            for (int j = 0; j < FORK_NAME_SUFFIX_LEN; ++j) {
+                sb.append(ALPHABETS.charAt(random.nextInt(ALPHABETS.length())));
+            }
+            String candidate = sb.toString();
+            if (!existingNames.contains(candidate)) {
+                return candidate;
+            }
+        }
+        return null;
+    }
 }