Compare commits

..

2 Commits

18 changed files with 172 additions and 46 deletions

10
pom.xml
View File

@ -39,6 +39,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -71,6 +75,7 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
@ -117,6 +122,11 @@
<artifactId>poi-ooxml</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
<build>

View File

@ -2,6 +2,7 @@ package cn.edu.hfut.rmdjzz.projectmanagement.advice;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.BadRequestException;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.ForbiddenException;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.TooManyRequestException;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.UnauthorizedException;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseMap;
import lombok.extern.slf4j.Slf4j;
@ -51,4 +52,10 @@ public class ExceptionHandlerAdvice {
.collect(Collectors.joining(""))
);
}
@ExceptionHandler(TooManyRequestException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ResponseMap handleTooManyRequestException(TooManyRequestException e) {
return ResponseMap.of(HttpStatus.TOO_MANY_REQUESTS.value(), e.getMessage());
}
}

View File

@ -0,0 +1,29 @@
package cn.edu.hfut.rmdjzz.projectmanagement.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author
* @since 2022/7/11 16:57
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.METHOD})
public @interface RateLimit {
/**
*
*/
String key() default "";
int permitsPerSecond();
long timeout() default 0;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
long maxBurstSeconds() default 1;
String msg() default "系统繁忙,请稍后再试";
}

View File

@ -0,0 +1,53 @@
package cn.edu.hfut.rmdjzz.projectmanagement.aop;
import cn.edu.hfut.rmdjzz.projectmanagement.annotation.RateLimit;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.TooManyRequestException;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author
* @since 2022/7/11 17:23
*/
@SuppressWarnings("UnstableApiUsage")
@Aspect
@Component
public class RateLimitAOP {
private final Map<String, RateLimiter> rateLimitMap = Maps.newConcurrentMap();
@Around("@annotation(cn.edu.hfut.rmdjzz.projectmanagement.annotation.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimit limit = method.getAnnotation(RateLimit.class);
if (limit == null) {
return joinPoint.proceed();
}
String key = limit.key();
RateLimiter limiter = rateLimitMap.get(key);
if (limiter == null) {
limiter = RateLimiter.create(limit.permitsPerSecond());
Class<? extends RateLimiter> clazz = limiter.getClass();
//TODO: DEBUG TEST
Field burstSecondsField = clazz.getDeclaredField("maxBurstSeconds");
burstSecondsField.setAccessible(true);
burstSecondsField.set(limiter, limit.maxBurstSeconds());
rateLimitMap.put(key, limiter);
}
if (!limiter.tryAcquire(limit.timeout(), limit.timeUnit())) {
throw new TooManyRequestException(limit.msg());
}
return joinPoint.proceed();
}
}

View File

@ -29,7 +29,7 @@ public class AnnouncementController {
@SneakyThrows
@GetMapping
public ResponseList<AnnouncementDTO> getAnnouncementList(@RequestHeader("Token") String token, @PathVariable Integer projectId) {
public ResponseList<AnnouncementDTO> getAnnouncementList(@RequestHeader(TokenUtils.HEADER_TOKEN) String token, @PathVariable Integer projectId) {
if (projectGroupService.getProjectAccessLevel(token, projectId) == 0) {
throw new ForbiddenException(IProjectGroupService.UNABLE_TO_ACCESS_PROJECT);
}
@ -39,7 +39,7 @@ public class AnnouncementController {
@SneakyThrows
@GetMapping("/{announcementId}")
public ResponseMap getAnnouncementById(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Long announcementId
) {
@ -52,7 +52,7 @@ public class AnnouncementController {
@SneakyThrows
@PostMapping
public ResponseMap createAnnouncement(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@RequestBody Announcement announcement
) {
@ -73,7 +73,7 @@ public class AnnouncementController {
/*@SneakyThrows
@PutMapping("/{announcementId}")
public ResponseMap modifyAnnouncement(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Long announcementId,
@RequestBody Announcement announcement
@ -88,7 +88,7 @@ public class AnnouncementController {
@SneakyThrows
@DeleteMapping("/{announcementId}")
public ResponseMap deleteAnnouncement(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Long announcementId
) {

View File

@ -5,6 +5,7 @@ import cn.edu.hfut.rmdjzz.projectmanagement.entity.dto.ProjectDTO;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.BadRequestException;
import cn.edu.hfut.rmdjzz.projectmanagement.service.IProjectGroupService;
import cn.edu.hfut.rmdjzz.projectmanagement.service.IProjectService;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.TokenUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.RequestPage;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseList;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseMap;
@ -35,7 +36,7 @@ public class ProjectController {
@SneakyThrows
@GetMapping
public ResponseList<ProjectDTO> getProjectListOfStaff(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@Valid RequestPage page,
@Parameter(description = "参数列表见Project实体类时间可以用xxxxStart与xxxxEnd来确定区间"
, required = true) @RequestParam("paramMap") Map<String, Object> paramMap
@ -47,7 +48,7 @@ public class ProjectController {
@SneakyThrows
@GetMapping("/{projectId}")
public ResponseMap getOneProjectBasicInfo(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId
) {
if (projectGroupService.getProjectAccessLevel(token, projectId) == 0) {
@ -59,7 +60,7 @@ public class ProjectController {
@Operation(description = "根据Token获取该员工的Project数")
@SneakyThrows
@GetMapping("/count")
public ResponseMap getProjectNumOfStaff(@RequestHeader("Token") String token) {
public ResponseMap getProjectNumOfStaff(@RequestHeader(TokenUtils.HEADER_TOKEN) String token) {
return ResponseMap.ofSuccess()
.put("totalNum", projectService.countMyProjects(token));
}
@ -67,7 +68,7 @@ public class ProjectController {
@SneakyThrows
@PostMapping("/complete")
public ResponseMap completeProject(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@Parameter(description = "只需要传projectId即可{\"projectId\": 1}")
@RequestBody Map<String, Object> map
) {
@ -78,7 +79,7 @@ public class ProjectController {
@SneakyThrows
@PostMapping
public ResponseMap createProject(@RequestHeader("Token") String token, @RequestBody Project project) {
public ResponseMap createProject(@RequestHeader(TokenUtils.HEADER_TOKEN) String token, @RequestBody Project project) {
projectService.createProject(token, project);
return ResponseMap.ofSuccess();
}
@ -86,7 +87,7 @@ public class ProjectController {
@SneakyThrows
@PutMapping("/{projectId}")
public ResponseMap updateProject(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@RequestBody Project project
) {
@ -100,7 +101,7 @@ public class ProjectController {
@SneakyThrows
@GetMapping("/{projectId}/stats")
public ResponseMap getProjectProcess(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId
) {
return ResponseMap.ofSuccess(projectService.getProjectProcess(token, projectId));

View File

@ -43,7 +43,7 @@ public class ProjectGroupController {
@GetMapping
public ResponseList<ProjectGroupDTO> getGroupMembers(
@PathVariable Integer projectId,
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
RequestPage page
) {
if (projectGroupService.getProjectAccessLevel(token, projectId) == 0) {
@ -59,7 +59,7 @@ public class ProjectGroupController {
@SneakyThrows
@GetMapping("/{staffId}")
public ResponseMap getDesignatedStaffPosition(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Integer staffId
) {
@ -77,7 +77,7 @@ public class ProjectGroupController {
@SneakyThrows
@PostMapping
public ResponseMap addGroupMember(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@Parameter(description = "只传staffUsername和projectStaffPosition") @RequestBody GroupPositionVO groupPosition
) {
@ -91,7 +91,7 @@ public class ProjectGroupController {
@SneakyThrows
@PutMapping("/{staffId}")
public ResponseMap modifyDesignatedStaffPosition(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Integer staffId,
@Parameter(description = "在body中只传projectStaffPosition") @RequestBody GroupPositionVO groupPosition
@ -105,7 +105,7 @@ public class ProjectGroupController {
@SneakyThrows
@GetMapping("/stats")
public ResponseMap getGroupPositionsStatistics(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId
) {
return ResponseMap.ofSuccess(projectGroupService.collectStatsForGroupPositions(token, projectId));
@ -114,7 +114,7 @@ public class ProjectGroupController {
@SneakyThrows
@GetMapping("/{staffId}/stats")
public ResponseList<StaffProcessDTO> getProjectProcessOfStaff(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Integer staffId
) {
@ -129,7 +129,7 @@ public class ProjectGroupController {
@SneakyThrows
@PutMapping("/{staffId}/transfer")
public ResponseMap transferStaffTasks(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable Integer staffId,
@RequestBody Map<Long, Integer> transferMap

View File

@ -39,7 +39,7 @@ public class StaffController {
@SneakyThrows
@PostMapping("/logout")
public ResponseMap logout(@RequestHeader("Token") String token) {
public ResponseMap logout(@RequestHeader(TokenUtils.HEADER_TOKEN) String token) {
if (staffService.logout(token)) {
return ResponseMap.ofSuccess("登出成功");
}
@ -49,8 +49,8 @@ public class StaffController {
@SneakyThrows
@PostMapping(value = "/import")
public ResponseMap importStaffs(
@RequestHeader("Token") String token,
@RequestHeader("File-Digest") String digest,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@RequestHeader(FileUtils.HEADER_FILE_DIGEST) String digest,
@RequestParam("uploadFile") MultipartFile uploadFile
) {
if (null == uploadFile) {
@ -69,15 +69,15 @@ public class StaffController {
@SneakyThrows
@GetMapping("/import/template")
public ResponseMap downloadTemplate(
@RequestHeader("Token") String token,
public void downloadTemplate(
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
HttpServletResponse response
) {
if (TokenUtils.getStaffGlobalLevel(token) > 2) {
throw new ForbiddenException(ForbiddenException.UNABLE_TO_OPERATE);
}
if (FileUtils.downloadResource("static/账户导入模板.xlsx", response)) {
return ResponseMap.ofSuccess();
return;
}
throw new BadRequestException(BadRequestException.OPERATE_FAILED);
}

View File

@ -5,6 +5,7 @@ import cn.edu.hfut.rmdjzz.projectmanagement.entity.dto.TaskDTO;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.BadRequestException;
import cn.edu.hfut.rmdjzz.projectmanagement.service.IProjectService;
import cn.edu.hfut.rmdjzz.projectmanagement.service.ITaskService;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.TokenUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseList;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseMap;
import lombok.SneakyThrows;
@ -27,7 +28,7 @@ public class TaskController {
@SneakyThrows
@GetMapping("/{fatherId}/subtask")
public ResponseList<TaskDTO> getSubTaskList(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId,
@PathVariable("fatherId") Long fatherId
) {
@ -37,7 +38,7 @@ public class TaskController {
@SneakyThrows
@GetMapping("/mine")
public ResponseList<Task> getMyTasks(@RequestHeader("Token") String token, @PathVariable("projectId") Integer projectId) {
public ResponseList<Task> getMyTasks(@RequestHeader(TokenUtils.HEADER_TOKEN) String token, @PathVariable("projectId") Integer projectId) {
List<Task> result = taskService.listMyTasks(token, projectId);
return ResponseList.ofSuccess(result);
}
@ -45,7 +46,7 @@ public class TaskController {
@SneakyThrows
@GetMapping("/subtask/exist")
public ResponseMap existSubTask(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId,
@RequestParam("taskId") Long taskId
) {
@ -56,7 +57,7 @@ public class TaskController {
@SneakyThrows
@PostMapping
public ResponseMap createTask(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId,
@RequestBody Task task
) {
@ -70,7 +71,7 @@ public class TaskController {
@SneakyThrows
@PutMapping("/{taskId}")
public ResponseMap modifyTask(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId,
@PathVariable("taskId") Long taskId,
@RequestBody Task task
@ -86,7 +87,7 @@ public class TaskController {
@SneakyThrows
@DeleteMapping("/{taskId}")
public ResponseMap deleteTaskAndSubTask(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable("projectId") Integer projectId,
@PathVariable("taskId") Long taskId
) {
@ -99,7 +100,7 @@ public class TaskController {
@SneakyThrows
@GetMapping("/stats")
public ResponseMap getTaskTrend(
@RequestHeader("Token") String token,
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId
) {
if(!projectService.checkOpenStatus(projectId)) {

View File

@ -57,6 +57,7 @@ public class Task {
"demandSource:需求来源 (String), estimatedManHours:预估工时 (Integer), severity:严重程度 (String), recurrenceProbability:复现概率 (String)")
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> attachedInfo;
private Integer childrenCount;
@TableField("is_deleted")
@TableLogic
private Boolean deleted;

View File

@ -0,0 +1,11 @@
package cn.edu.hfut.rmdjzz.projectmanagement.exception;
/**
* @author
* @since 2022/7/11 17:35
*/
public class TooManyRequestException extends Exception {
public TooManyRequestException(String message) {
super(message);
}
}

View File

@ -27,7 +27,7 @@ public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws TokenException {
System.out.println(httpServletRequest.getRequestURL() + " " + httpServletRequest.getMethod());
String token = httpServletRequest.getHeader("Token");
String token = httpServletRequest.getHeader(TokenUtils.HEADER_TOKEN);
if (null == token || "".equals(token.trim())) {
throw new TokenException("缺少Token");
}
@ -50,7 +50,7 @@ public class TokenInterceptor implements HandlerInterceptor {
Objects.requireNonNull(TokenUtils.getDuration(token)), TimeUnit.SECONDS
);
}
httpServletResponse.setHeader("Token", newToken);
httpServletResponse.setHeader(TokenUtils.HEADER_TOKEN, newToken);
return true;
}
}

View File

@ -45,7 +45,7 @@ public interface ITaskService extends IService<Task> {
Task modifyTask(String token, Task task) throws BadRequestException, ForbiddenException;
Map<String, List<TaskTrendDTO>> getProjectTaskTrend(String token, Integer projectId) throws BadRequestException, ForbiddenException;
Map<String, List<TaskTrendDTO>> getProjectTaskTrend(String token, Integer projectId) throws ForbiddenException;
Boolean transferStaffTasks(String token, Integer projectId, Integer transferredStaffId, Map<Long, Integer> transferMap) throws ForbiddenException, BadRequestException;

View File

@ -62,7 +62,7 @@ public class StaffServiceImpl extends ServiceImpl<StaffMapper, Staff> implements
Objects.requireNonNull(TokenUtils.getDuration(token)), TimeUnit.SECONDS
);
return new MapBuilder()
.put("Token", token)
.put(TokenUtils.HEADER_TOKEN, token)
.putAll(BeanUtils.beanToMap(staff, false))
.build();
}

View File

@ -268,13 +268,14 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements IT
throw new BadRequestException("还有子工作尚未完成");
}
try {
boolean closed = false;
if (typeChangeValue != 0) {
task.setTaskClosedTime(LocalDateTime.now());
}
if (typeChangeValue == 2) {
closeTaskAndSubTask(token, task.getTaskProjectId(), task.getTaskId());
closed = closeTaskAndSubTask(token, task.getTaskProjectId(), task.getTaskId());
}
if (baseMapper.update(task, Wrappers.<Task>lambdaQuery().eq(Task::getTaskId, task.getTaskId())) == 0) {
if (!closed && baseMapper.update(task, Wrappers.<Task>lambdaQuery().eq(Task::getTaskId, task.getTaskId())) == 0) {
throw new BadRequestException(BadRequestException.OPERATE_FAILED);
}
} catch (Exception e) {
@ -285,7 +286,7 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements IT
}
@Override
public Map<String, List<TaskTrendDTO>> getProjectTaskTrend(String token, Integer projectId) throws BadRequestException, ForbiddenException {
public Map<String, List<TaskTrendDTO>> getProjectTaskTrend(String token, Integer projectId) throws ForbiddenException {
if (projectGroupService.getProjectAccessLevel(token, projectId) == 0) {
throw new ForbiddenException(ForbiddenException.UNABLE_TO_OPERATE);
}

View File

@ -6,25 +6,36 @@ import org.springframework.util.MimeTypeUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author
* @since 2022/7/11 9:32
*/
public class FileUtils {
public static final String HEADER_FILE_DIGEST = "File-Digest";
/**
* File-Digestmd5
* <p>
* Controllervoid
*/
public static Boolean downloadResource(String resourceName, HttpServletResponse response) throws IOException {
@Cleanup InputStream is = FileUtils.class.getResourceAsStream(resourceName);
if (is == null) {
throw new FileNotFoundException("该文件不存在");
}
@Cleanup BufferedInputStream bis = new BufferedInputStream(is);
@Cleanup InputStream is = FileUtils.class.getClassLoader().getResourceAsStream(resourceName);
BufferedInputStream bis = new BufferedInputStream(is);
bis.mark(bis.available() + 1);
response.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE);
response.setCharacterEncoding("UTF-8");
ServletOutputStream out = response.getOutputStream();
response.addHeader("File-Digest", DigestUtils.md5DigestAsHex(bis));
response.addHeader(HEADER_FILE_DIGEST, DigestUtils.md5DigestAsHex(bis));
bis.reset();
@Cleanup BufferedOutputStream bos = new BufferedOutputStream(out);
bis.transferTo(bos);
bis.close();
bos.flush();
return true;
}

View File

@ -19,6 +19,7 @@ import java.util.Date;
public final class TokenUtils {
private final static String PV_KEY = "SignedByRMDJZZ";
public final static String HEADER_TOKEN = "Token";
private final static String STAFF_USERNAME = "staffUsername";
private final static String STAFF_ID = "staffId";
private final static String STAFF_GLOBAL_LEVEL = "staffGlobalLevel";

Binary file not shown.