forked from admin/innovation-platform
feat: refresh innovation platform demo
This commit is contained in:
5
backend/Dockerfile
Normal file
5
backend/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM eclipse-temurin:21-jdk-alpine
|
||||
WORKDIR /app
|
||||
COPY target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
92
backend/pom.xml
Normal file
92
backend/pom.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.innovation</groupId>
|
||||
<artifactId>innovation-platform</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>innovation-platform</name>
|
||||
<description>University innovation and entrepreneurship project management demo</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
<sa-token.version>1.37.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<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>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.25</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.innovation.platform;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 高校创新创业项目孵化平台启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.innovation.platform.mapper")
|
||||
public class InnovationPlatformApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(InnovationPlatformApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.innovation.platform.common;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页结果
|
||||
*/
|
||||
@Data
|
||||
public class PageResult<T> implements Serializable {
|
||||
private List<T> records;
|
||||
private Long total;
|
||||
private Long size;
|
||||
private Long current;
|
||||
private Long pages;
|
||||
|
||||
public static <T> PageResult<T> of(IPage<T> page) {
|
||||
PageResult<T> result = new PageResult<>();
|
||||
result.setRecords(page.getRecords());
|
||||
result.setTotal(page.getTotal());
|
||||
result.setSize(page.getSize());
|
||||
result.setCurrent(page.getCurrent());
|
||||
result.setPages(page.getPages());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.innovation.platform.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
private Long timestamp;
|
||||
|
||||
public Result() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(200);
|
||||
result.setMessage("Success");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
Result<T> result = success();
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
Result<T> result = success();
|
||||
result.setMessage(message);
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String message) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(500);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.innovation.platform.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.innovation.platform.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import com.innovation.platform.common.Result;
|
||||
import com.innovation.platform.dto.LoginRequest;
|
||||
import com.innovation.platform.dto.LoginResponse;
|
||||
import com.innovation.platform.dto.RegisterRequest;
|
||||
import com.innovation.platform.service.SysUserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "Authentication", description = "Login, registration, and logout endpoints")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
@SaIgnore
|
||||
@Operation(summary = "User login")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
return Result.success("Login succeeded.", sysUserService.login(request));
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@Operation(summary = "User registration")
|
||||
@PostMapping("/register")
|
||||
public Result<Void> register(@Valid @RequestBody RegisterRequest request) {
|
||||
sysUserService.register(request);
|
||||
return Result.success("Registration succeeded.", null);
|
||||
}
|
||||
|
||||
@Operation(summary = "User logout")
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout() {
|
||||
sysUserService.logout();
|
||||
return Result.success("Logout succeeded.", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.innovation.platform.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.innovation.platform.common.PageResult;
|
||||
import com.innovation.platform.common.Result;
|
||||
import com.innovation.platform.dto.ProjectQueryRequest;
|
||||
import com.innovation.platform.dto.ProjectRequest;
|
||||
import com.innovation.platform.dto.ProjectResponse;
|
||||
import com.innovation.platform.dto.ProjectStatsResponse;
|
||||
import com.innovation.platform.service.ProjectService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "Project management", description = "Project CRUD, filtering, and dashboard statistics")
|
||||
@RestController
|
||||
@RequestMapping("/api/projects")
|
||||
@RequiredArgsConstructor
|
||||
@SaCheckLogin
|
||||
public class ProjectController {
|
||||
|
||||
private final ProjectService projectService;
|
||||
|
||||
@Operation(summary = "List projects with pagination and keyword search")
|
||||
@GetMapping
|
||||
public Result<PageResult<ProjectResponse>> page(ProjectQueryRequest request) {
|
||||
IPage<ProjectResponse> page = projectService.page(request);
|
||||
return Result.success(PageResult.of(page));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get project dashboard statistics")
|
||||
@GetMapping("/stats")
|
||||
public Result<ProjectStatsResponse> stats() {
|
||||
return Result.success(projectService.stats());
|
||||
}
|
||||
|
||||
@Operation(summary = "Get project detail by id")
|
||||
@GetMapping("/{id}")
|
||||
public Result<ProjectResponse> getById(@Parameter(description = "Project id") @PathVariable Long id) {
|
||||
return Result.success(projectService.getById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "Create a project")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody ProjectRequest request) {
|
||||
return Result.success("Project created successfully.", projectService.create(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "Update a project")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(
|
||||
@Parameter(description = "Project id") @PathVariable Long id,
|
||||
@Valid @RequestBody ProjectRequest request
|
||||
) {
|
||||
projectService.update(id, request);
|
||||
return Result.success("Project updated successfully.", null);
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete a project")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@Parameter(description = "Project id") @PathVariable Long id) {
|
||||
projectService.delete(id);
|
||||
return Result.success("Project deleted successfully.", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
@NotBlank(message = "Username is required.")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Password is required.")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 登录响应 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String realName;
|
||||
private String avatar;
|
||||
private Integer roleType;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ProjectQueryRequest {
|
||||
private String keyword;
|
||||
private String projectName;
|
||||
private Integer projectType;
|
||||
private Integer projectLevel;
|
||||
private Integer status;
|
||||
private Long leaderId;
|
||||
private Long advisorId;
|
||||
private String college;
|
||||
private Integer current = 1;
|
||||
private Integer size = 10;
|
||||
|
||||
public Integer getCurrent() {
|
||||
return current == null || current < 1 ? 1 : current;
|
||||
}
|
||||
|
||||
public Integer getSize() {
|
||||
if (size == null || size < 1) {
|
||||
return 10;
|
||||
}
|
||||
return Math.min(size, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import com.innovation.platform.entity.Project;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class ProjectRequest {
|
||||
@NotBlank(message = "Project number is required.")
|
||||
@Size(max = 30, message = "Project number must stay within 30 characters.")
|
||||
private String projectNo;
|
||||
|
||||
@NotBlank(message = "Project name is required.")
|
||||
@Size(max = 200, message = "Project name must stay within 200 characters.")
|
||||
private String projectName;
|
||||
|
||||
@NotNull(message = "Project type is required.")
|
||||
private Integer projectType;
|
||||
|
||||
@NotNull(message = "Project level is required.")
|
||||
private Integer projectLevel;
|
||||
|
||||
@NotNull(message = "Leader id is required.")
|
||||
private Long leaderId;
|
||||
|
||||
@NotNull(message = "Advisor id is required.")
|
||||
private Long advisorId;
|
||||
|
||||
@Size(max = 4000, message = "Description must stay within 4000 characters.")
|
||||
private String description;
|
||||
|
||||
@Size(max = 4000, message = "Research plan must stay within 4000 characters.")
|
||||
private String researchPlan;
|
||||
|
||||
@Size(max = 4000, message = "Expected result must stay within 4000 characters.")
|
||||
private String expectedResult;
|
||||
|
||||
@Pattern(
|
||||
regexp = "^$|^\\d+(\\.\\d{1,2})?$",
|
||||
message = "Budget must be a number with up to two decimal places."
|
||||
)
|
||||
private String budget;
|
||||
|
||||
private Integer status;
|
||||
private LocalDate startTime;
|
||||
private LocalDate endTime;
|
||||
|
||||
@Size(max = 100, message = "College must stay within 100 characters.")
|
||||
private String college;
|
||||
|
||||
@AssertTrue(message = "End date must be on or after the start date.")
|
||||
public boolean isDateRangeValid() {
|
||||
if (startTime == null || endTime == null) {
|
||||
return true;
|
||||
}
|
||||
return !endTime.isBefore(startTime);
|
||||
}
|
||||
|
||||
public Project toEntity() {
|
||||
Project project = new Project();
|
||||
project.setProjectNo(clean(projectNo));
|
||||
project.setProjectName(clean(projectName));
|
||||
project.setProjectType(projectType);
|
||||
project.setProjectLevel(projectLevel);
|
||||
project.setLeaderId(leaderId);
|
||||
project.setAdvisorId(advisorId);
|
||||
project.setDescription(clean(description));
|
||||
project.setResearchPlan(clean(researchPlan));
|
||||
project.setExpectedResult(clean(expectedResult));
|
||||
if (budget != null && !budget.isBlank()) {
|
||||
project.setBudget(new BigDecimal(budget.trim()));
|
||||
}
|
||||
project.setStatus(status);
|
||||
project.setStartTime(startTime);
|
||||
project.setEndTime(endTime);
|
||||
project.setCollege(clean(college));
|
||||
return project;
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import com.innovation.platform.entity.Project;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ProjectResponse {
|
||||
private Long id;
|
||||
private String projectNo;
|
||||
private String projectName;
|
||||
private Integer projectType;
|
||||
private String projectTypeLabel;
|
||||
private Integer projectLevel;
|
||||
private String projectLevelLabel;
|
||||
private Long leaderId;
|
||||
private String leaderName;
|
||||
private Long advisorId;
|
||||
private String advisorName;
|
||||
private String description;
|
||||
private String researchPlan;
|
||||
private String expectedResult;
|
||||
private BigDecimal budget;
|
||||
private Integer status;
|
||||
private String statusLabel;
|
||||
private LocalDate startTime;
|
||||
private LocalDate endTime;
|
||||
private Long durationDays;
|
||||
private String college;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
public static ProjectResponse fromEntity(Project project) {
|
||||
return fromEntity(project, Map.of());
|
||||
}
|
||||
|
||||
public static ProjectResponse fromEntity(Project project, Map<Long, String> userNames) {
|
||||
return ProjectResponse.builder()
|
||||
.id(project.getId())
|
||||
.projectNo(project.getProjectNo())
|
||||
.projectName(project.getProjectName())
|
||||
.projectType(project.getProjectType())
|
||||
.projectTypeLabel(projectTypeLabel(project.getProjectType()))
|
||||
.projectLevel(project.getProjectLevel())
|
||||
.projectLevelLabel(projectLevelLabel(project.getProjectLevel()))
|
||||
.leaderId(project.getLeaderId())
|
||||
.leaderName(userNames.get(project.getLeaderId()))
|
||||
.advisorId(project.getAdvisorId())
|
||||
.advisorName(userNames.get(project.getAdvisorId()))
|
||||
.description(project.getDescription())
|
||||
.researchPlan(project.getResearchPlan())
|
||||
.expectedResult(project.getExpectedResult())
|
||||
.budget(project.getBudget())
|
||||
.status(project.getStatus())
|
||||
.statusLabel(statusLabel(project.getStatus()))
|
||||
.startTime(project.getStartTime())
|
||||
.endTime(project.getEndTime())
|
||||
.durationDays(durationDays(project.getStartTime(), project.getEndTime()))
|
||||
.college(project.getCollege())
|
||||
.createTime(project.getCreateTime())
|
||||
.updateTime(project.getUpdateTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String projectTypeLabel(Integer value) {
|
||||
if (value == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (value) {
|
||||
case 1 -> "Research";
|
||||
case 2 -> "Startup";
|
||||
case 3 -> "Competition";
|
||||
default -> "Other";
|
||||
};
|
||||
}
|
||||
|
||||
private static String projectLevelLabel(Integer value) {
|
||||
if (value == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (value) {
|
||||
case 1 -> "School";
|
||||
case 2 -> "Provincial";
|
||||
case 3 -> "National";
|
||||
default -> "Other";
|
||||
};
|
||||
}
|
||||
|
||||
private static String statusLabel(Integer value) {
|
||||
if (value == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (value) {
|
||||
case 0 -> "Draft";
|
||||
case 1 -> "Pending review";
|
||||
case 2 -> "In progress";
|
||||
case 3 -> "Completed";
|
||||
case 4 -> "Rejected";
|
||||
default -> "Other";
|
||||
};
|
||||
}
|
||||
|
||||
private static Long durationDays(LocalDate start, LocalDate end) {
|
||||
if (start == null || end == null || end.isBefore(start)) {
|
||||
return null;
|
||||
}
|
||||
return ChronoUnit.DAYS.between(start, end) + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProjectStatsResponse {
|
||||
private long totalProjects;
|
||||
private long draftProjects;
|
||||
private long pendingProjects;
|
||||
private long activeProjects;
|
||||
private long completedProjects;
|
||||
private long rejectedProjects;
|
||||
private long nationalProjects;
|
||||
private BigDecimal totalBudget;
|
||||
private BigDecimal averageBudget;
|
||||
private List<Bucket> statusBreakdown;
|
||||
private List<Bucket> levelBreakdown;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Bucket {
|
||||
private String key;
|
||||
private String label;
|
||||
private long value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.innovation.platform.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
@NotBlank(message = "Username is required.")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Password is required.")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "Real name is required.")
|
||||
private String realName;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "Phone number format is invalid.")
|
||||
private String phone;
|
||||
|
||||
@Pattern(
|
||||
regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
message = "Email format is invalid."
|
||||
)
|
||||
private String email;
|
||||
|
||||
private Integer gender;
|
||||
private Integer roleType;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 成果实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("achievement")
|
||||
public class Achievement extends BaseEntity {
|
||||
private Long projectId;
|
||||
private Integer achievementType;
|
||||
private String achievementName;
|
||||
private Integer achievementLevel;
|
||||
private String authorNames;
|
||||
private LocalDate publishTime;
|
||||
private String publishOrg;
|
||||
private String description;
|
||||
private BigDecimal credit;
|
||||
private Integer status;
|
||||
private Long auditorId;
|
||||
private LocalDateTime auditTime;
|
||||
private String auditOpinion;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 成果附件实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("achievement_attachment")
|
||||
public class AchievementAttachment implements Serializable {
|
||||
private Long id;
|
||||
private Long achievementId;
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
private Long fileSize;
|
||||
private String fileType;
|
||||
private LocalDateTime createTime;
|
||||
private Long createBy;
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体类
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 项目实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("project")
|
||||
public class Project extends BaseEntity {
|
||||
private String projectNo;
|
||||
private String projectName;
|
||||
private Integer projectType;
|
||||
private Integer projectLevel;
|
||||
private Long leaderId;
|
||||
private Long advisorId;
|
||||
private String description;
|
||||
private String researchPlan;
|
||||
private String expectedResult;
|
||||
private BigDecimal budget;
|
||||
private Integer status;
|
||||
private LocalDate startTime;
|
||||
private LocalDate endTime;
|
||||
private String college;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 项目附件实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("project_attachment")
|
||||
public class ProjectAttachment implements Serializable {
|
||||
private Long id;
|
||||
private Long projectId;
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
private Long fileSize;
|
||||
private String fileType;
|
||||
private Integer attachmentType;
|
||||
private LocalDateTime createTime;
|
||||
private Long createBy;
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 项目成员实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("project_member")
|
||||
public class ProjectMember implements Serializable {
|
||||
private Long id;
|
||||
private Long projectId;
|
||||
private Long userId;
|
||||
private Integer memberOrder;
|
||||
private Integer role;
|
||||
private LocalDateTime joinTime;
|
||||
private LocalDateTime createTime;
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 评审实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("review")
|
||||
public class Review extends BaseEntity {
|
||||
private Long projectId;
|
||||
private Long reviewerId;
|
||||
private Integer reviewType;
|
||||
private BigDecimal score;
|
||||
private String opinion;
|
||||
private Integer result;
|
||||
private Integer status;
|
||||
private LocalDateTime reviewTime;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 评审评分项实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("review_score_item")
|
||||
public class ReviewScoreItem implements Serializable {
|
||||
private Long id;
|
||||
private Long reviewId;
|
||||
private String itemName;
|
||||
private BigDecimal itemScore;
|
||||
private BigDecimal maxScore;
|
||||
private String itemComment;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 学生信息实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("stu_info")
|
||||
public class StuInfo extends BaseEntity {
|
||||
private Long userId;
|
||||
private String studentNo;
|
||||
private String college;
|
||||
private String major;
|
||||
private String grade;
|
||||
private String className;
|
||||
private Long advisorId;
|
||||
private BigDecimal totalCredit;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 系统配置实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_config")
|
||||
public class SysConfig extends BaseEntity {
|
||||
private String configKey;
|
||||
private String configValue;
|
||||
private String configName;
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_log")
|
||||
public class SysLog implements Serializable {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String operation;
|
||||
private String method;
|
||||
private String params;
|
||||
private String ip;
|
||||
private Long time;
|
||||
private Integer result;
|
||||
private String errorMsg;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user")
|
||||
public class SysUser extends BaseEntity {
|
||||
private String username;
|
||||
private String password;
|
||||
private String realName;
|
||||
private Integer gender;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String avatar;
|
||||
private Integer status;
|
||||
private Integer roleType;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.innovation.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 教师信息实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("teacher_info")
|
||||
public class TeacherInfo extends BaseEntity {
|
||||
private Long userId;
|
||||
private String teacherNo;
|
||||
private String college;
|
||||
private String title;
|
||||
private String researchField;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.innovation.platform.exception;
|
||||
|
||||
import com.innovation.platform.common.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public Result<Void> handleRuntimeException(RuntimeException e) {
|
||||
log.error("Business exception: {}", e.getMessage(), e);
|
||||
return Result.error(400, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("Validation failed: {}", message);
|
||||
return Result.error(400, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<Void> handleBindException(BindException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("Binding failed: {}", message);
|
||||
return Result.error(400, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("Unexpected system exception: {}", e.getMessage(), e);
|
||||
return Result.error(500, "Unexpected system error. Please try again later.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.innovation.platform.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.innovation.platform.entity.Project;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 项目 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProjectMapper extends BaseMapper<Project> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.innovation.platform.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.innovation.platform.entity.SysUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysUserMapper extends BaseMapper<SysUser> {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.innovation.platform.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.innovation.platform.dto.ProjectQueryRequest;
|
||||
import com.innovation.platform.dto.ProjectRequest;
|
||||
import com.innovation.platform.dto.ProjectResponse;
|
||||
import com.innovation.platform.dto.ProjectStatsResponse;
|
||||
import com.innovation.platform.entity.Project;
|
||||
|
||||
public interface ProjectService {
|
||||
|
||||
IPage<ProjectResponse> page(ProjectQueryRequest request);
|
||||
|
||||
ProjectResponse getById(Long id);
|
||||
|
||||
Long create(ProjectRequest request);
|
||||
|
||||
void update(Long id, ProjectRequest request);
|
||||
|
||||
void delete(Long id);
|
||||
|
||||
Project getEntityById(Long id);
|
||||
|
||||
ProjectStatsResponse stats();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.innovation.platform.service;
|
||||
|
||||
import com.innovation.platform.dto.LoginRequest;
|
||||
import com.innovation.platform.dto.LoginResponse;
|
||||
import com.innovation.platform.dto.RegisterRequest;
|
||||
import com.innovation.platform.entity.SysUser;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
*/
|
||||
public interface SysUserService {
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
LoginResponse login(LoginRequest request);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
void register(RegisterRequest request);
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
void logout();
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*/
|
||||
SysUser getByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*/
|
||||
SysUser getById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.innovation.platform.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.innovation.platform.dto.ProjectQueryRequest;
|
||||
import com.innovation.platform.dto.ProjectRequest;
|
||||
import com.innovation.platform.dto.ProjectResponse;
|
||||
import com.innovation.platform.dto.ProjectStatsResponse;
|
||||
import com.innovation.platform.entity.Project;
|
||||
import com.innovation.platform.entity.SysUser;
|
||||
import com.innovation.platform.mapper.ProjectMapper;
|
||||
import com.innovation.platform.mapper.SysUserMapper;
|
||||
import com.innovation.platform.service.ProjectService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
private final ProjectMapper projectMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
@Override
|
||||
public IPage<ProjectResponse> page(ProjectQueryRequest request) {
|
||||
Page<Project> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
|
||||
LambdaQueryWrapper<Project> wrapper = new LambdaQueryWrapper<>();
|
||||
String keyword = clean(request.getKeyword());
|
||||
wrapper.and(StringUtils.hasText(keyword), nested -> nested
|
||||
.like(Project::getProjectNo, keyword)
|
||||
.or()
|
||||
.like(Project::getProjectName, keyword)
|
||||
.or()
|
||||
.like(Project::getDescription, keyword)
|
||||
.or()
|
||||
.like(Project::getCollege, keyword))
|
||||
.like(StringUtils.hasText(request.getProjectName()), Project::getProjectName, clean(request.getProjectName()))
|
||||
.eq(request.getProjectType() != null, Project::getProjectType, request.getProjectType())
|
||||
.eq(request.getProjectLevel() != null, Project::getProjectLevel, request.getProjectLevel())
|
||||
.eq(request.getStatus() != null, Project::getStatus, request.getStatus())
|
||||
.eq(request.getLeaderId() != null, Project::getLeaderId, request.getLeaderId())
|
||||
.eq(request.getAdvisorId() != null, Project::getAdvisorId, request.getAdvisorId())
|
||||
.eq(StringUtils.hasText(request.getCollege()), Project::getCollege, clean(request.getCollege()))
|
||||
.orderByDesc(Project::getCreateTime);
|
||||
|
||||
IPage<Project> projectPage = projectMapper.selectPage(page, wrapper);
|
||||
Map<Long, String> userNames = loadUserNames(projectPage.getRecords());
|
||||
return projectPage.convert(project -> ProjectResponse.fromEntity(project, userNames));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectResponse getById(Long id) {
|
||||
Project project = projectMapper.selectById(id);
|
||||
if (project == null) {
|
||||
throw new RuntimeException("Project does not exist.");
|
||||
}
|
||||
return ProjectResponse.fromEntity(project, loadUserNames(List.of(project)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(ProjectRequest request) {
|
||||
Project project = request.toEntity();
|
||||
project.setStatus(project.getStatus() != null ? project.getStatus() : 0);
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
project.setCreateBy(userId);
|
||||
project.setUpdateBy(userId);
|
||||
|
||||
projectMapper.insert(project);
|
||||
return project.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, ProjectRequest request) {
|
||||
Project existingProject = projectMapper.selectById(id);
|
||||
if (existingProject == null) {
|
||||
throw new RuntimeException("Project does not exist.");
|
||||
}
|
||||
|
||||
Project project = request.toEntity();
|
||||
project.setId(id);
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
project.setUpdateBy(userId);
|
||||
project.setCreateBy(existingProject.getCreateBy());
|
||||
project.setCreateTime(existingProject.getCreateTime());
|
||||
|
||||
projectMapper.updateById(project);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Project project = projectMapper.selectById(id);
|
||||
if (project == null) {
|
||||
throw new RuntimeException("Project does not exist.");
|
||||
}
|
||||
projectMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Project getEntityById(Long id) {
|
||||
return projectMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectStatsResponse stats() {
|
||||
List<Project> projects = projectMapper.selectList(new LambdaQueryWrapper<Project>().orderByDesc(Project::getCreateTime));
|
||||
|
||||
long total = projects.size();
|
||||
long draft = projects.stream().filter(item -> statusEquals(item, 0)).count();
|
||||
long pending = projects.stream().filter(item -> statusEquals(item, 1)).count();
|
||||
long active = projects.stream().filter(item -> statusEquals(item, 2)).count();
|
||||
long completed = projects.stream().filter(item -> statusEquals(item, 3)).count();
|
||||
long rejected = projects.stream().filter(item -> statusEquals(item, 4)).count();
|
||||
long national = projects.stream().filter(item -> levelEquals(item, 3)).count();
|
||||
|
||||
BigDecimal totalBudget = projects.stream()
|
||||
.map(Project::getBudget)
|
||||
.filter(value -> value != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal averageBudget = total == 0
|
||||
? BigDecimal.ZERO
|
||||
: totalBudget.divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
|
||||
|
||||
return ProjectStatsResponse.builder()
|
||||
.totalProjects(total)
|
||||
.draftProjects(draft)
|
||||
.pendingProjects(pending)
|
||||
.activeProjects(active)
|
||||
.completedProjects(completed)
|
||||
.rejectedProjects(rejected)
|
||||
.nationalProjects(national)
|
||||
.totalBudget(totalBudget)
|
||||
.averageBudget(averageBudget)
|
||||
.statusBreakdown(List.of(
|
||||
ProjectStatsResponse.Bucket.builder().key("draft").label("Draft").value(draft).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("pending").label("Pending review").value(pending).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("active").label("In progress").value(active).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("completed").label("Completed").value(completed).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("rejected").label("Rejected").value(rejected).build()
|
||||
))
|
||||
.levelBreakdown(List.of(
|
||||
ProjectStatsResponse.Bucket.builder().key("school").label("School").value(projects.stream().filter(item -> levelEquals(item, 1)).count()).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("provincial").label("Provincial").value(projects.stream().filter(item -> levelEquals(item, 2)).count()).build(),
|
||||
ProjectStatsResponse.Bucket.builder().key("national").label("National").value(national).build()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean statusEquals(Project project, int status) {
|
||||
return project.getStatus() != null && project.getStatus() == status;
|
||||
}
|
||||
|
||||
private boolean levelEquals(Project project, int level) {
|
||||
return project.getProjectLevel() != null && project.getProjectLevel() == level;
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
private Map<Long, String> loadUserNames(List<Project> projects) {
|
||||
List<Long> userIds = projects.stream()
|
||||
.flatMap(project -> Stream.of(project.getLeaderId(), project.getAdvisorId()))
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (userIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return sysUserMapper.selectBatchIds(userIds).stream()
|
||||
.collect(Collectors.toMap(SysUser::getId, SysUser::getRealName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.innovation.platform.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.innovation.platform.dto.LoginRequest;
|
||||
import com.innovation.platform.dto.LoginResponse;
|
||||
import com.innovation.platform.dto.RegisterRequest;
|
||||
import com.innovation.platform.entity.SysUser;
|
||||
import com.innovation.platform.mapper.SysUserMapper;
|
||||
import com.innovation.platform.service.SysUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SysUserServiceImpl implements SysUserService {
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
@Override
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
SysUser user = getByUsername(request.getUsername());
|
||||
if (user == null) {
|
||||
throw new RuntimeException("Invalid username or password.");
|
||||
}
|
||||
|
||||
if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) {
|
||||
throw new RuntimeException("Invalid username or password.");
|
||||
}
|
||||
|
||||
if (user.getStatus() != null && user.getStatus() == 0) {
|
||||
throw new RuntimeException("This account is disabled.");
|
||||
}
|
||||
|
||||
StpUtil.login(user.getId());
|
||||
|
||||
return LoginResponse.builder()
|
||||
.token(StpUtil.getTokenValue())
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.realName(user.getRealName())
|
||||
.avatar(user.getAvatar())
|
||||
.roleType(user.getRoleType())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void register(RegisterRequest request) {
|
||||
if (getByUsername(request.getUsername()) != null) {
|
||||
throw new RuntimeException("Username already exists.");
|
||||
}
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(BCrypt.hashpw(request.getPassword()));
|
||||
user.setRealName(request.getRealName());
|
||||
user.setPhone(request.getPhone());
|
||||
user.setEmail(request.getEmail());
|
||||
user.setGender(request.getGender());
|
||||
user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 0);
|
||||
user.setStatus(1);
|
||||
|
||||
sysUserMapper.insert(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
StpUtil.logout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUser getByUsername(String username) {
|
||||
return sysUserMapper.selectOne(
|
||||
new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getUsername, username)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUser getById(Long id) {
|
||||
return sysUserMapper.selectById(id);
|
||||
}
|
||||
}
|
||||
28
backend/src/main/resources/application.yml
Normal file
28
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://mysql:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: innovation
|
||||
password: innovation123
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
schema-locations: classpath:schema.sql
|
||||
data-locations: classpath:data.sql
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
sa-token:
|
||||
token-name: satoken
|
||||
timeout: 86400
|
||||
active-timeout: -1
|
||||
is-concurrent: true
|
||||
is-share: true
|
||||
token-style: uuid
|
||||
is-log: false
|
||||
201
backend/src/main/resources/data.sql
Normal file
201
backend/src/main/resources/data.sql
Normal file
@@ -0,0 +1,201 @@
|
||||
-- Seed users
|
||||
INSERT INTO sys_user (id, username, password, real_name, gender, role_type, status) VALUES
|
||||
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'System Admin', 1, 3, 1),
|
||||
(2, 'teacher001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Alice Chen', 2, 2, 1),
|
||||
(3, 'teacher002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Michael Zhao', 1, 2, 1),
|
||||
(4, 'student001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Olivia Lin', 2, 1, 1),
|
||||
(5, 'student002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Ethan Wu', 1, 1, 1),
|
||||
(6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
password = VALUES(password),
|
||||
real_name = VALUES(real_name),
|
||||
gender = VALUES(gender),
|
||||
role_type = VALUES(role_type),
|
||||
status = VALUES(status);
|
||||
|
||||
-- Teacher profiles
|
||||
INSERT INTO teacher_info (id, user_id, teacher_no, college, title, research_field) VALUES
|
||||
(1, 2, 'T2024001', 'School of Computer Science', 'Associate Professor', 'Artificial Intelligence'),
|
||||
(2, 3, 'T2024002', 'School of Management', 'Professor', 'Digital Entrepreneurship')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
college = VALUES(college),
|
||||
title = VALUES(title),
|
||||
research_field = VALUES(research_field);
|
||||
|
||||
-- Student profiles
|
||||
INSERT INTO stu_info (id, user_id, student_no, college, major, grade, class_name, advisor_id) VALUES
|
||||
(1, 4, 'S2021001', 'School of Computer Science', 'Software Engineering', '2021', 'SE2101', 2),
|
||||
(2, 5, 'S2021002', 'School of Management', 'Business Analytics', '2021', 'BA2101', 3),
|
||||
(3, 6, 'S2022001', 'School of Information Engineering', 'Data Science', '2022', 'DS2201', 2)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
college = VALUES(college),
|
||||
major = VALUES(major),
|
||||
grade = VALUES(grade),
|
||||
class_name = VALUES(class_name),
|
||||
advisor_id = VALUES(advisor_id);
|
||||
|
||||
-- Demo projects
|
||||
INSERT INTO project (
|
||||
id,
|
||||
project_no,
|
||||
project_name,
|
||||
project_type,
|
||||
project_level,
|
||||
leader_id,
|
||||
advisor_id,
|
||||
description,
|
||||
research_plan,
|
||||
expected_result,
|
||||
budget,
|
||||
status,
|
||||
start_time,
|
||||
end_time,
|
||||
college,
|
||||
create_by,
|
||||
update_by
|
||||
) VALUES
|
||||
(
|
||||
1,
|
||||
'PRJ2024-001',
|
||||
'Smart Campus Assistant',
|
||||
1,
|
||||
2,
|
||||
4,
|
||||
2,
|
||||
'Build a multimodal assistant for campus services and policy guidance.',
|
||||
'Deliver a knowledge base, chat workflow, and pilot deployment in the service center.',
|
||||
'Working prototype, evaluation report, and student operation handbook.',
|
||||
18000.00,
|
||||
2,
|
||||
'2024-03-01',
|
||||
'2024-12-31',
|
||||
'School of Computer Science',
|
||||
4,
|
||||
4
|
||||
),
|
||||
(
|
||||
2,
|
||||
'PRJ2024-002',
|
||||
'Green Logistics Lab',
|
||||
2,
|
||||
1,
|
||||
5,
|
||||
3,
|
||||
'Design a campus scale reverse logistics service for reusable packaging.',
|
||||
'Run a semester pilot with collection points, route optimization, and merchant onboarding.',
|
||||
'Service blueprint, mini-program prototype, and impact dashboard.',
|
||||
9500.00,
|
||||
1,
|
||||
'2024-04-15',
|
||||
'2024-11-30',
|
||||
'School of Management',
|
||||
5,
|
||||
5
|
||||
),
|
||||
(
|
||||
3,
|
||||
'PRJ2024-003',
|
||||
'Medical Image Triage Toolkit',
|
||||
1,
|
||||
3,
|
||||
6,
|
||||
2,
|
||||
'Explore lightweight triage assistance for common medical imaging scenarios.',
|
||||
'Prepare datasets, benchmark candidate models, and validate interpretability outputs.',
|
||||
'Research paper draft, benchmark scripts, and explainability report.',
|
||||
32000.00,
|
||||
3,
|
||||
'2024-01-10',
|
||||
'2025-01-15',
|
||||
'School of Information Engineering',
|
||||
6,
|
||||
6
|
||||
),
|
||||
(
|
||||
4,
|
||||
'PRJ2024-004',
|
||||
'AR Heritage Guide',
|
||||
3,
|
||||
1,
|
||||
4,
|
||||
3,
|
||||
'Create an augmented reality campus heritage route for new students and visitors.',
|
||||
'Complete scene design, oral history collection, and route testing before launch.',
|
||||
'Interactive AR route, narration scripts, and event showcase materials.',
|
||||
12000.00,
|
||||
0,
|
||||
'2024-09-01',
|
||||
'2025-03-01',
|
||||
'School of Design',
|
||||
4,
|
||||
4
|
||||
),
|
||||
(
|
||||
5,
|
||||
'PRJ2024-005',
|
||||
'Agri IoT Monitoring Network',
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
2,
|
||||
'Prototype low-cost monitoring stations for greenhouse temperature and soil conditions.',
|
||||
'Build device kits, integrate dashboards, and verify long-run data stability.',
|
||||
'Sensor kit, field report, and maintenance SOP.',
|
||||
21000.00,
|
||||
4,
|
||||
'2024-02-20',
|
||||
'2024-10-20',
|
||||
'School of Agronomy',
|
||||
5,
|
||||
5
|
||||
),
|
||||
(
|
||||
6,
|
||||
'PRJ2024-006',
|
||||
'Inclusive Career Coach',
|
||||
2,
|
||||
3,
|
||||
6,
|
||||
3,
|
||||
'Build an inclusive career guidance service for first-generation college students.',
|
||||
'Interview student cohorts, refine service flows, and launch a recommendation prototype.',
|
||||
'Coaching toolkit, matching engine prototype, and adoption report.',
|
||||
28500.00,
|
||||
2,
|
||||
'2024-05-01',
|
||||
'2025-02-28',
|
||||
'School of Public Affairs',
|
||||
6,
|
||||
6
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
project_name = VALUES(project_name),
|
||||
project_type = VALUES(project_type),
|
||||
project_level = VALUES(project_level),
|
||||
leader_id = VALUES(leader_id),
|
||||
advisor_id = VALUES(advisor_id),
|
||||
description = VALUES(description),
|
||||
research_plan = VALUES(research_plan),
|
||||
expected_result = VALUES(expected_result),
|
||||
budget = VALUES(budget),
|
||||
status = VALUES(status),
|
||||
start_time = VALUES(start_time),
|
||||
end_time = VALUES(end_time),
|
||||
college = VALUES(college),
|
||||
update_by = VALUES(update_by);
|
||||
|
||||
-- Project members
|
||||
INSERT INTO project_member (id, project_id, user_id, member_order, role) VALUES
|
||||
(1, 1, 4, 1, 2),
|
||||
(2, 1, 5, 2, 1),
|
||||
(3, 2, 5, 1, 2),
|
||||
(4, 2, 6, 2, 1),
|
||||
(5, 3, 6, 1, 2),
|
||||
(6, 3, 4, 2, 1),
|
||||
(7, 4, 4, 1, 2),
|
||||
(8, 5, 5, 1, 2),
|
||||
(9, 6, 6, 1, 2),
|
||||
(10, 6, 5, 2, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
member_order = VALUES(member_order),
|
||||
role = VALUES(role);
|
||||
133
backend/src/main/resources/schema.sql
Normal file
133
backend/src/main/resources/schema.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
-- Core user table
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
real_name VARCHAR(50) NOT NULL,
|
||||
gender TINYINT DEFAULT 0,
|
||||
phone VARCHAR(20) DEFAULT NULL,
|
||||
email VARCHAR(100) DEFAULT NULL,
|
||||
avatar VARCHAR(255) DEFAULT NULL,
|
||||
status TINYINT DEFAULT 1,
|
||||
role_type TINYINT NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by BIGINT DEFAULT NULL,
|
||||
update_by BIGINT DEFAULT NULL,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_username (username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Student profile table
|
||||
CREATE TABLE IF NOT EXISTS stu_info (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
student_no VARCHAR(20) NOT NULL,
|
||||
college VARCHAR(100) NOT NULL,
|
||||
major VARCHAR(100) NOT NULL,
|
||||
grade VARCHAR(10) NOT NULL,
|
||||
class_name VARCHAR(50) DEFAULT NULL,
|
||||
advisor_id BIGINT DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_student_no (student_no),
|
||||
UNIQUE KEY uk_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Teacher profile table
|
||||
CREATE TABLE IF NOT EXISTS teacher_info (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
teacher_no VARCHAR(20) NOT NULL,
|
||||
college VARCHAR(100) NOT NULL,
|
||||
title VARCHAR(50) DEFAULT NULL,
|
||||
research_field VARCHAR(255) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_teacher_no (teacher_no),
|
||||
UNIQUE KEY uk_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Project table
|
||||
CREATE TABLE IF NOT EXISTS project (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
project_no VARCHAR(30) NOT NULL,
|
||||
project_name VARCHAR(200) NOT NULL,
|
||||
project_type TINYINT NOT NULL,
|
||||
project_level TINYINT NOT NULL,
|
||||
leader_id BIGINT NOT NULL,
|
||||
advisor_id BIGINT NOT NULL,
|
||||
description TEXT,
|
||||
research_plan TEXT,
|
||||
expected_result TEXT,
|
||||
budget DECIMAL(10,2) DEFAULT 0.00,
|
||||
status TINYINT DEFAULT 1,
|
||||
start_time DATE DEFAULT NULL,
|
||||
end_time DATE DEFAULT NULL,
|
||||
college VARCHAR(100) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by BIGINT DEFAULT NULL,
|
||||
update_by BIGINT DEFAULT NULL,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_project_no (project_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Project member table
|
||||
CREATE TABLE IF NOT EXISTS project_member (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
project_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
member_order INT DEFAULT 1,
|
||||
role TINYINT DEFAULT 1,
|
||||
join_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_project_user (project_id, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Achievement table
|
||||
CREATE TABLE IF NOT EXISTS achievement (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
project_id BIGINT NOT NULL,
|
||||
achievement_type TINYINT NOT NULL,
|
||||
achievement_name VARCHAR(200) NOT NULL,
|
||||
achievement_level TINYINT DEFAULT NULL,
|
||||
author_names VARCHAR(500) DEFAULT NULL,
|
||||
publish_time DATE DEFAULT NULL,
|
||||
publish_org VARCHAR(200) DEFAULT NULL,
|
||||
description TEXT,
|
||||
status TINYINT DEFAULT 1,
|
||||
auditor_id BIGINT DEFAULT NULL,
|
||||
audit_time DATETIME DEFAULT NULL,
|
||||
audit_opinion VARCHAR(500) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by BIGINT DEFAULT NULL,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Review table
|
||||
CREATE TABLE IF NOT EXISTS review (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
project_id BIGINT NOT NULL,
|
||||
reviewer_id BIGINT NOT NULL,
|
||||
review_type TINYINT NOT NULL,
|
||||
score DECIMAL(5,1) DEFAULT NULL,
|
||||
opinion TEXT,
|
||||
result TINYINT DEFAULT NULL,
|
||||
status TINYINT DEFAULT 1,
|
||||
review_time DATETIME DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
562
backend/src/main/resources/static/index.html
Normal file
562
backend/src/main/resources/static/index.html
Normal file
@@ -0,0 +1,562 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Innovation Platform Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f3f6fb;
|
||||
--panel: rgba(255,255,255,0.94);
|
||||
--line: #d9e2ec;
|
||||
--text: #102033;
|
||||
--muted: #5a6c81;
|
||||
--brand: #1768c8;
|
||||
--accent: #0f9d77;
|
||||
--warn: #f08c2b;
|
||||
--shadow: 0 18px 40px rgba(16, 32, 51, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(23, 104, 200, 0.14), transparent 28%),
|
||||
radial-gradient(circle at bottom left, rgba(15, 157, 119, 0.12), transparent 24%),
|
||||
var(--bg);
|
||||
}
|
||||
button, input, select { font: inherit; }
|
||||
.shell { max-width: 1460px; margin: 0 auto; padding: 24px; }
|
||||
.hero, .card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.hero { padding: 28px; margin-bottom: 18px; }
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
color: var(--brand);
|
||||
}
|
||||
h1, h2, h3 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: var(--muted); line-height: 1.85; }
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.hero-actions, .inline-actions, .chip-list {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn, .btn-soft {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 12px 18px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: #fff; }
|
||||
.btn-soft { background: #eaf3ff; color: var(--brand); }
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 360px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
|
||||
.card { padding: 22px; }
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
label { font-size: 13px; font-weight: 700; color: #21364d; }
|
||||
input, select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.stat {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 8px; }
|
||||
.stat strong { font-size: 26px; }
|
||||
.sub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
.list, .table-wrap {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.list-item:last-child { border-bottom: 0; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: top;
|
||||
}
|
||||
th { background: #f7fbff; color: #29425a; font-size: 13px; }
|
||||
td { color: var(--muted); }
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #edf5ff;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.notice {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.55);
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.status {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #d8e5f4;
|
||||
background: #f7fbff;
|
||||
color: #35506b;
|
||||
line-height: 1.8;
|
||||
min-height: 58px;
|
||||
}
|
||||
.status.error {
|
||||
background: #fff3f3;
|
||||
border-color: #f0d0d0;
|
||||
color: #b94242;
|
||||
}
|
||||
.status.success {
|
||||
background: #eefbf6;
|
||||
border-color: #d3f0e5;
|
||||
color: #177658;
|
||||
}
|
||||
@media (max-width: 1180px) {
|
||||
.layout, .hero-grid, .sub-grid { grid-template-columns: 1fr; }
|
||||
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.shell { padding: 14px; }
|
||||
.field-grid, .stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="hero">
|
||||
<div class="hero-grid">
|
||||
<div>
|
||||
<div class="eyebrow">Innovation Platform</div>
|
||||
<h1>Project dashboard for university innovation programs</h1>
|
||||
<p>This static page is served by Spring Boot and talks directly to the project APIs. It helps demo authentication, project search, and the new statistics endpoint without requiring a separate frontend build.</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="chip-list">
|
||||
<span class="pill">Use satoken header</span>
|
||||
<span class="pill">Supports quick login</span>
|
||||
<span class="pill">Reads stats and project pages</span>
|
||||
</div>
|
||||
<div class="notice">
|
||||
Seed accounts share the bundled demo password:
|
||||
<br />admin / admin123
|
||||
<br />teacher001 / admin123
|
||||
<br />student001 / admin123
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<section class="card">
|
||||
<div class="eyebrow">Connection</div>
|
||||
<h2>Login and token helper</h2>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="baseUrl">API base URL</label>
|
||||
<input id="baseUrl" value="" placeholder="http://localhost:8080" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="token">satoken header value</label>
|
||||
<input id="token" value="" placeholder="Paste token here" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="accountPreset">Demo account</label>
|
||||
<select id="accountPreset">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="teacher001">Teacher</option>
|
||||
<option value="student001">Student</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" value="admin" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" value="admin123" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-actions" style="margin-top: 14px;">
|
||||
<button class="btn" type="button" id="loginBtn">Request token</button>
|
||||
<button class="btn-soft" type="button" id="statsBtn">Load stats</button>
|
||||
</div>
|
||||
<div class="status" id="statusBox">Ready. Request a token or paste one manually, then load dashboard data.</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Filters</div>
|
||||
<h2>Project query</h2>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="keyword">Keyword</label>
|
||||
<input id="keyword" placeholder="AI, campus, PRJ2024..." />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="projectType">Project type</label>
|
||||
<select id="projectType">
|
||||
<option value="">All</option>
|
||||
<option value="1">Research</option>
|
||||
<option value="2">Startup</option>
|
||||
<option value="3">Competition</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="projectLevel">Project level</label>
|
||||
<select id="projectLevel">
|
||||
<option value="">All</option>
|
||||
<option value="1">School</option>
|
||||
<option value="2">Provincial</option>
|
||||
<option value="3">National</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="status">Status</label>
|
||||
<select id="status">
|
||||
<option value="">All</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Pending review</option>
|
||||
<option value="2">In progress</option>
|
||||
<option value="3">Completed</option>
|
||||
<option value="4">Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-actions" style="margin-top: 14px;">
|
||||
<button class="btn" type="button" id="projectsBtn">Load projects</button>
|
||||
<button class="btn-soft" type="button" id="resetBtn">Reset filters</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section class="card">
|
||||
<div class="eyebrow">Overview</div>
|
||||
<h2>Program health snapshot</h2>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Total projects</span><strong id="totalProjects">-</strong></div>
|
||||
<div class="stat"><span>Draft</span><strong id="draftProjects">-</strong></div>
|
||||
<div class="stat"><span>Pending review</span><strong id="pendingProjects">-</strong></div>
|
||||
<div class="stat"><span>In progress</span><strong id="activeProjects">-</strong></div>
|
||||
<div class="stat"><span>Completed</span><strong id="completedProjects">-</strong></div>
|
||||
<div class="stat"><span>Rejected</span><strong id="rejectedProjects">-</strong></div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>National level</span><strong id="nationalProjects">-</strong></div>
|
||||
<div class="stat"><span>Total budget</span><strong id="totalBudget">-</strong></div>
|
||||
<div class="stat"><span>Average budget</span><strong id="averageBudget">-</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Breakdown</div>
|
||||
<h2>Status and level distribution</h2>
|
||||
<div class="sub-grid">
|
||||
<div class="list" id="statusList"></div>
|
||||
<div class="list" id="levelList"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Projects</div>
|
||||
<h2>Recent project records</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Lead</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Level</th>
|
||||
<th>Timeline</th>
|
||||
<th>College</th>
|
||||
<th>Budget</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projectTable">
|
||||
<tr><td colspan="8">No data loaded yet.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const refs = {
|
||||
baseUrl: document.getElementById('baseUrl'),
|
||||
token: document.getElementById('token'),
|
||||
accountPreset: document.getElementById('accountPreset'),
|
||||
username: document.getElementById('username'),
|
||||
password: document.getElementById('password'),
|
||||
keyword: document.getElementById('keyword'),
|
||||
projectType: document.getElementById('projectType'),
|
||||
projectLevel: document.getElementById('projectLevel'),
|
||||
status: document.getElementById('status'),
|
||||
statusBox: document.getElementById('statusBox'),
|
||||
projectTable: document.getElementById('projectTable'),
|
||||
statusList: document.getElementById('statusList'),
|
||||
levelList: document.getElementById('levelList')
|
||||
};
|
||||
|
||||
const statRefs = {
|
||||
totalProjects: document.getElementById('totalProjects'),
|
||||
draftProjects: document.getElementById('draftProjects'),
|
||||
pendingProjects: document.getElementById('pendingProjects'),
|
||||
activeProjects: document.getElementById('activeProjects'),
|
||||
completedProjects: document.getElementById('completedProjects'),
|
||||
rejectedProjects: document.getElementById('rejectedProjects'),
|
||||
nationalProjects: document.getElementById('nationalProjects'),
|
||||
totalBudget: document.getElementById('totalBudget'),
|
||||
averageBudget: document.getElementById('averageBudget')
|
||||
};
|
||||
|
||||
const accountPresets = {
|
||||
admin: { username: 'admin', password: 'admin123' },
|
||||
teacher001: { username: 'teacher001', password: 'admin123' },
|
||||
student001: { username: 'student001', password: 'admin123' }
|
||||
};
|
||||
|
||||
refs.baseUrl.value = window.location.origin || 'http://localhost:8080';
|
||||
applyPresetAccount();
|
||||
|
||||
document.getElementById('loginBtn').addEventListener('click', login);
|
||||
document.getElementById('statsBtn').addEventListener('click', loadStats);
|
||||
document.getElementById('projectsBtn').addEventListener('click', loadProjects);
|
||||
refs.accountPreset.addEventListener('change', applyPresetAccount);
|
||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||
refs.keyword.value = '';
|
||||
refs.projectType.value = '';
|
||||
refs.projectLevel.value = '';
|
||||
refs.status.value = '';
|
||||
loadProjects();
|
||||
});
|
||||
|
||||
async function login() {
|
||||
setStatus('Requesting login token...');
|
||||
try {
|
||||
const response = await fetch(buildUrl('/api/auth/login'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: refs.username.value.trim(),
|
||||
password: refs.password.value
|
||||
})
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (payload.code !== 200) {
|
||||
throw new Error(payload.message || 'Login failed.');
|
||||
}
|
||||
refs.token.value = payload.data.token || '';
|
||||
setStatus('Login succeeded. Token stored in the satoken field.', 'success');
|
||||
await loadStats();
|
||||
await loadProjects();
|
||||
} catch (error) {
|
||||
setStatus(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
setStatus('Loading project statistics...');
|
||||
try {
|
||||
const payload = await apiFetch('/api/projects/stats');
|
||||
const stats = payload.data;
|
||||
statRefs.totalProjects.textContent = stats.totalProjects ?? '-';
|
||||
statRefs.draftProjects.textContent = stats.draftProjects ?? '-';
|
||||
statRefs.pendingProjects.textContent = stats.pendingProjects ?? '-';
|
||||
statRefs.activeProjects.textContent = stats.activeProjects ?? '-';
|
||||
statRefs.completedProjects.textContent = stats.completedProjects ?? '-';
|
||||
statRefs.rejectedProjects.textContent = stats.rejectedProjects ?? '-';
|
||||
statRefs.nationalProjects.textContent = stats.nationalProjects ?? '-';
|
||||
statRefs.totalBudget.textContent = formatMoney(stats.totalBudget);
|
||||
statRefs.averageBudget.textContent = formatMoney(stats.averageBudget);
|
||||
renderBucketList(refs.statusList, stats.statusBreakdown || []);
|
||||
renderBucketList(refs.levelList, stats.levelBreakdown || []);
|
||||
setStatus('Statistics loaded successfully.', 'success');
|
||||
} catch (error) {
|
||||
setStatus(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
setStatus('Loading project records...');
|
||||
const params = new URLSearchParams();
|
||||
if (refs.keyword.value.trim()) params.set('keyword', refs.keyword.value.trim());
|
||||
if (refs.projectType.value) params.set('projectType', refs.projectType.value);
|
||||
if (refs.projectLevel.value) params.set('projectLevel', refs.projectLevel.value);
|
||||
if (refs.status.value) params.set('status', refs.status.value);
|
||||
params.set('size', '8');
|
||||
|
||||
try {
|
||||
const payload = await apiFetch('/api/projects?' + params.toString());
|
||||
const page = payload.data || {};
|
||||
const records = page.records || [];
|
||||
if (!records.length) {
|
||||
refs.projectTable.innerHTML = '<tr><td colspan="8">No matching projects found.</td></tr>';
|
||||
} else {
|
||||
refs.projectTable.innerHTML = records.map((item) => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(item.projectName || 'Unnamed project')}</strong><br />
|
||||
<span>${escapeHtml(item.projectNo || '-')}</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>${escapeHtml(item.leaderName || 'Unassigned')}</strong><br />
|
||||
<span>Advisor: ${escapeHtml(item.advisorName || '-')}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(item.projectTypeLabel || '-')}</td>
|
||||
<td>${escapeHtml(item.statusLabel || '-')}</td>
|
||||
<td>${escapeHtml(item.projectLevelLabel || '-')}</td>
|
||||
<td>${formatDuration(item.startTime, item.endTime, item.durationDays)}</td>
|
||||
<td>${escapeHtml(item.college || '-')}</td>
|
||||
<td>${formatMoney(item.budget)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
setStatus(`Loaded ${records.length} project record(s).`, 'success');
|
||||
} catch (error) {
|
||||
setStatus(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetch(path) {
|
||||
const headers = {};
|
||||
const token = refs.token.value.trim();
|
||||
if (token) {
|
||||
headers.satoken = token;
|
||||
}
|
||||
const response = await fetch(buildUrl(path), { headers });
|
||||
const payload = await response.json();
|
||||
if (payload.code !== 200) {
|
||||
throw new Error(payload.message || 'Request failed.');
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderBucketList(target, items) {
|
||||
if (!items.length) {
|
||||
target.innerHTML = '<div class="list-item"><span>No data</span><strong>-</strong></div>';
|
||||
return;
|
||||
}
|
||||
target.innerHTML = items.map((item) => `
|
||||
<div class="list-item">
|
||||
<span>${escapeHtml(item.label || item.key || 'Item')}</span>
|
||||
<strong>${item.value ?? 0}</strong>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function buildUrl(path) {
|
||||
const base = refs.baseUrl.value.trim().replace(/\/$/, '');
|
||||
return (base || 'http://localhost:8080') + path;
|
||||
}
|
||||
|
||||
function formatMoney(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
const num = Number(value);
|
||||
if (Number.isNaN(num)) {
|
||||
return escapeHtml(String(value));
|
||||
}
|
||||
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function formatDuration(start, end, durationDays) {
|
||||
const parts = [];
|
||||
if (start || end) {
|
||||
parts.push(`${escapeHtml(start || '?')} to ${escapeHtml(end || '?')}`);
|
||||
}
|
||||
if (durationDays) {
|
||||
parts.push(`${durationDays} day(s)`);
|
||||
}
|
||||
return parts.length ? parts.join('<br />') : '-';
|
||||
}
|
||||
|
||||
function applyPresetAccount() {
|
||||
const preset = accountPresets[refs.accountPreset.value] || accountPresets.admin;
|
||||
refs.username.value = preset.username;
|
||||
refs.password.value = preset.password;
|
||||
}
|
||||
|
||||
function setStatus(message, tone) {
|
||||
refs.statusBox.textContent = message;
|
||||
refs.statusBox.className = 'status' + (tone ? ' ' + tone : '');
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user