chore: 从Idea local history中恢复文件,完成至用户预约列表查询接口

This commit is contained in:
BenjaminNH 2025-06-21 11:22:01 +08:00
commit 275ca89962
46 changed files with 1546 additions and 0 deletions

78
.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
# -----------------------------
# Java & Spring Boot 项目通用
# -----------------------------
# 编译输出
/out/
/target/
/build/
# 日志
*.log
# 临时文件
*.tmp
*.bak
*.swp
# 操作系统垃圾文件
.DS_Store
Thumbs.db
# -----------------------------
# Maven
# -----------------------------
/.mvn/
/**/target/
!src/**/target/keepme.txt # 可选:防止误删有用文件夹
# Maven Wrapper
mvnw
mvnw.cmd
.mvn/
# -----------------------------
# Gradle如果用的是 Gradle
# -----------------------------
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
# Gradle Wrapper
gradlew
gradlew.bat
# -----------------------------
# IntelliJ IDEA
# -----------------------------
.idea/
*.iml
*.iws
*.ipr
# -----------------------------
# VSCode
# -----------------------------
.vscode/
# -----------------------------
# 数据库 & 配置
# -----------------------------
# 忽略本地配置文件(避免提交数据库密码等)
application-local.yaml
application-dev.yaml
application.properties
application.yaml
# -----------------------------
# 图片 / 文件上传目录(如有)
# -----------------------------
/device_image/
/uploads/
/images/
# -----------------------------
# Git 忽略规则文件自身
# -----------------------------
!.gitignore

33
HELP.md Normal file
View File

@ -0,0 +1,33 @@
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.6/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.6/maven-plugin/build-image.html)
* [MyBatis Framework](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/)
* [Spring Security](https://docs.spring.io/spring-boot/3.4.6/reference/web/spring-security.html)
* [Spring Web](https://docs.spring.io/spring-boot/3.4.6/reference/web/servlet.html)
### Guides
The following guides illustrate how to use some features concretely:
* [MyBatis Quick Start](https://github.com/mybatis/spring-boot-starter/wiki/Quick-Start)
* [Securing a Web Application](https://spring.io/guides/gs/securing-web/)
* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)
* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/)
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
### Maven Parent overrides
Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the
parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.

100
db/equip_reserve.sql Normal file
View File

@ -0,0 +1,100 @@
-- 创建数据库
CREATE
DATABASE IF NOT EXISTS equip_reserve DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE
equip_reserve;
-- 用户表
CREATE TABLE users
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
name VARCHAR(50),
phone VARCHAR(20),
team_id BIGINT,
enabled BOOLEAN DEFAULT TRUE,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE roles
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(30) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL
);
-- 用户-角色关联表
CREATE TABLE user_roles
(
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 团队表
CREATE TABLE teams
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
leader_id BIGINT,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 团队成员表
CREATE TABLE team_members
(
team_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
PRIMARY KEY (team_id, user_id)
);
-- 设备表
CREATE TABLE devices
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
usage_requirement TEXT,
location VARCHAR(255),
image_path VARCHAR(255),
status VARCHAR(20) NOT NULL default 'AVAILABLE',
team_id BIGINT NOT NULL,
device_admin_id BIGINT,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 预约记录表
CREATE TABLE reservations
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
device_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
start_time DATE NOT NULL,
end_time DATE NOT NULL,
applicant_name VARCHAR(50) NOT NULL,
applicant_team VARCHAR(50),
applicant_contact VARCHAR(50),
status VARCHAR(30) NOT NULL,
device_admin_id BIGINT,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 审批记录表
CREATE TABLE approvals
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
reservation_id BIGINT NOT NULL,
step TINYINT NOT NULL,
approver_id BIGINT NOT NULL,
decision TINYINT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 初始角色数据
INSERT INTO roles (code, name)
VALUES ('ADMIN', '系统管理员'),
('LEADER', '团队负责人'),
('DEVICE_ADMIN', '设备管理员'),
('USER', '普通用户');

113
pom.xml Normal file
View File

@ -0,0 +1,113 @@
<?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.4.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>github.benjamin</groupId>
<artifactId>equip-reserve-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>equip-reserve-backend</name>
<description>equip-reserve-backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter-test</artifactId>
<version>3.5.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-compiler-plugin</artifactId>-->
<!-- <version>3.13.0</version>-->
<!-- <configuration>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- </path>-->
<!-- </annotationProcessorPaths>-->
<!-- </configuration>-->
<!-- </plugin>-->
<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>

View File

@ -0,0 +1,15 @@
package github.benjamin.equipreservebackend;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("github.benjamin.equipreservebackend.mapper")
public class EquipReserveBackendApplication {
public static void main(String[] args) {
SpringApplication.run(EquipReserveBackendApplication.class, args);
}
}

View File

@ -0,0 +1,19 @@
package github.benjamin.equipreservebackend.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@ -0,0 +1,41 @@
package github.benjamin.equipreservebackend.config;
import github.benjamin.equipreservebackend.security.JwtAuthFilter;
import github.benjamin.equipreservebackend.service.UserService;
import github.benjamin.equipreservebackend.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.requestMatchers("/device_image/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, userService);
}
}

View File

@ -0,0 +1,15 @@
package github.benjamin.equipreservebackend.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/device_image/**")
.addResourceLocations("file:" + System.getProperty("user.dir") + "/device_image/");
}
}

View File

@ -0,0 +1,17 @@
package github.benjamin.equipreservebackend.constant;
import lombok.Getter;
@Getter
public enum DeviceReservationState {
FREE("可预约"),
RESERVED("有预约");
private final String label;
DeviceReservationState(String label) {
this.label = label;
}
}

View File

@ -0,0 +1,12 @@
package github.benjamin.equipreservebackend.constant;
/**
* 设备内部状态
*/
public enum DeviceStatus {
AVAILABLE,
MAINTENANCE,
DISABLED
}

View File

@ -0,0 +1,18 @@
package github.benjamin.equipreservebackend.constant;
import lombok.Getter;
@Getter
public enum ReservationStatus {
PENDING_LEADER("团队负责人审批中"),
PENDING_DEVICE_ADMIN("设备负责人审批中"),
APPROVED("通过"),
APPROVED_ASSIST("需要协助实验"),
REJECTED("审批不通过");
final String label;
ReservationStatus(String label) {
this.label = label;
}
}

View File

@ -0,0 +1,61 @@
package github.benjamin.equipreservebackend.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.entity.Device;
import github.benjamin.equipreservebackend.response.ResponseResult;
import github.benjamin.equipreservebackend.service.DeviceService;
import github.benjamin.equipreservebackend.vo.DeviceUserVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
@RequiredArgsConstructor
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
@PreAuthorize("hasRole('USER')")
@GetMapping
public ResponseResult<Page<DeviceUserVO>> getDevices(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
Page<Device> pageRequest = new Page<>(page, size);
Page<DeviceUserVO> res = deviceService.getDeviceVO(pageRequest);
return ResponseResult.success(res);
}
@PreAuthorize("hasRole('DEVICE_ADMIN')")
@PostMapping
public ResponseResult<?> addDevice(@RequestBody Device device){
deviceService.addDevice(device);
return ResponseResult.success(device);
}
@PreAuthorize("hasRole('DEVICE_ADMIN')")
@DeleteMapping("/{id}")
public ResponseResult<?> deleteDevice(@PathVariable("id") Long id) {
deviceService.deleteDevice(id);
return ResponseResult.success();
}
@PreAuthorize("hasRole('DEVICE_ADMIN')")
@PutMapping("/{id}")
public ResponseResult<?> updateDevice(@PathVariable("id") Long id,
@RequestBody Device device) {
device.setId(id);
Device updatedDevice = deviceService.updateDevice(device);
return ResponseResult.success(updatedDevice);
}
@PreAuthorize("hasRole('DEVICE_ADMIN')")
@PostMapping("/{id}/image")
public ResponseResult<?> uploadImage(@PathVariable("id") Long id,
@RequestParam("image") MultipartFile image) throws IOException {
String imagePath = deviceService.saveImage(id, image);
return ResponseResult.success(imagePath);
}
}

View File

@ -0,0 +1,36 @@
package github.benjamin.equipreservebackend.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.entity.Reservation;
import github.benjamin.equipreservebackend.response.ResponseResult;
import github.benjamin.equipreservebackend.service.ReservationService;
import github.benjamin.equipreservebackend.vo.UserReservationVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/reservation")
@RequiredArgsConstructor
public class ReservationController {
private final ReservationService reservationService;
@PreAuthorize("hasRole('USER')")
@PostMapping
public ResponseResult<?> addReservation(@RequestBody Reservation reservation) {
reservationService.addReservation(reservation);
return ResponseResult.success();
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/{userId}")
public ResponseResult<Page<UserReservationVO>> getUserReservation(@PathVariable("userId") Long userId,
@RequestParam(defaultValue = "10") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
Page<Reservation> pageRequest = new Page<>(page, size);
Page<UserReservationVO> res = reservationService.getUserReservationVO(userId, pageRequest);
return ResponseResult.success(res);
}
}

View File

@ -0,0 +1,28 @@
package github.benjamin.equipreservebackend.controller;
import github.benjamin.equipreservebackend.vo.LoginResponse;
import github.benjamin.equipreservebackend.response.ResponseResult;
import github.benjamin.equipreservebackend.entity.User;
import github.benjamin.equipreservebackend.security.SecurityUser;
import github.benjamin.equipreservebackend.service.UserService;
import github.benjamin.equipreservebackend.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseResult<LoginResponse> login(String username, String password) {
User user = userService.login(username, password);
SecurityUser securityUser = userService.loadSecurityUserById(user.getId());
String token = jwtUtil.generateToken(securityUser);
return ResponseResult.success(new LoginResponse(user.getId(), user.getName(), token));
}
}

View File

@ -0,0 +1,19 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("approvals")
public class Approval {
private Long id;
private Long reservationId;
private Integer step;
private Long approverId;
private Integer decision;
private LocalDateTime timeStamp;
}

View File

@ -0,0 +1,22 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("devices")
public class Device {
private Long id;
private String name;
private String usageRequirement;
private String location;
private String imagePath;
private String status;
private Long teamId;
private Long deviceAdminId;
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,25 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("reservations")
public class Reservation {
private Long id;
private Long deviceId;
private Long userId;
private LocalDate startTime;
private LocalDate endTime;
private String applicantName;
private String applicantTeam;
private String applicantContact;
private String status;
private Long deviceAdminId;
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,14 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("roles")
public class Role {
private Long id;
private String code;
private String name;
}

View File

@ -0,0 +1,17 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("teams")
public class Team {
private Long id;
private String name;
private Long leaderId;
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,22 @@
package github.benjamin.equipreservebackend.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("users")
public class User {
private Long id;
private String username;
private String password;
private String name;
private String phone;
private Long teamId;
private Boolean enabled;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
}

View File

@ -0,0 +1,19 @@
package github.benjamin.equipreservebackend.exception;
import lombok.Getter;
@Getter
public class ApiException extends RuntimeException{
private final int code;
public ApiException(String message) {
super(message);
this.code = -1;
}
public ApiException(int code, String message) {
super(message);
this.code = code;
}
}

View File

@ -0,0 +1,31 @@
package github.benjamin.equipreservebackend.exception;
import github.benjamin.equipreservebackend.response.ResponseCode;
import github.benjamin.equipreservebackend.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ApiException.class)
public ResponseResult<?> handleApiException(ApiException e) {
return ResponseResult.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(AuthorizationDeniedException.class)
public ResponseResult<?> handleAuthorizationDeniedException(AuthorizationDeniedException e) {
return ResponseResult.fail(ResponseCode.UNAUTHORIZED, "该用户无权访问此功能");
}
@ExceptionHandler(Exception.class)
public ResponseResult<?> handleException(Exception e) {
// TODO 日志
e.printStackTrace();
return ResponseResult.fail(ResponseCode.UNKNOWN_ERROR,"服务器内部异常,请联系开发人员");
}
}

View File

@ -0,0 +1,9 @@
package github.benjamin.equipreservebackend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import github.benjamin.equipreservebackend.entity.Device;
import org.springframework.stereotype.Repository;
@Repository
public interface DeviceMapper extends BaseMapper<Device> {
}

View File

@ -0,0 +1,9 @@
package github.benjamin.equipreservebackend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import github.benjamin.equipreservebackend.entity.Reservation;
import org.springframework.stereotype.Repository;
@Repository
public interface ReservationMapper extends BaseMapper<Reservation> {
}

View File

@ -0,0 +1,22 @@
package github.benjamin.equipreservebackend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import github.benjamin.equipreservebackend.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RoleMapper extends BaseMapper<Role> {
@Select("""
select r.id, r.code, r.name
from roles r
join user_roles ur on r.id = ur.role_id
where ur.user_id = #{userId}
""")
List<Role> selectRoleByUserId(@Param("userId") Long userId);
}

View File

@ -0,0 +1,9 @@
package github.benjamin.equipreservebackend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import github.benjamin.equipreservebackend.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}

View File

@ -0,0 +1,26 @@
package github.benjamin.equipreservebackend.response;
public class ResponseCode {
// 通用
public static final int SUCCESS = 0;
public static final int UNKNOWN_ERROR = -1;
// 登录相关
public static final int PASSWORD_ERROR = 4001;
public static final int USER_NOT_EXIST = 4002;
public static final int UNAUTHORIZED = 4010;
public static final int FORBIDDEN = 4030;
public static final int NOT_FOUND = 4040;
// 业务相关错误码
public static final int DUPLICATE_USERNAME = 10001;
public static final int TEAM_NOT_EXIST = 10002;
public static final int DEVICE_ALREADY_BOOKED = 10003;
private ResponseCode() {} // 不允许实例化
}

View File

@ -0,0 +1,35 @@
package github.benjamin.equipreservebackend.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
private int code;
private String message;
private T data;
public static <T> ResponseResult<T> success() {
return ResponseResult.success("success", null);
}
public static <T> ResponseResult<T> success(T data) {
return ResponseResult.success("success", data);
}
public static <T> ResponseResult<T> success(String message, T data) {
return new ResponseResult<>(ResponseCode.SUCCESS, message, data);
}
public static <T> ResponseResult<T> fail(Integer code, String message) {
return new ResponseResult<>(code, message, null);
}
public static <T> ResponseResult<T> of(Integer code, String message, T data) {
return new ResponseResult<>(code, message, data);
}
}

View File

@ -0,0 +1,55 @@
package github.benjamin.equipreservebackend.security;
import github.benjamin.equipreservebackend.service.UserService;
import github.benjamin.equipreservebackend.utils.JwtUtil;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromHeader(request);
if (token != null) {
try {
Long userId = jwtUtil.getUserId(token);
SecurityUser userDetails = userService.loadSecurityUserById(userId);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
// 续期
if (jwtUtil.shouldRefresh(token)) {
String newToken = jwtUtil.generateToken(userDetails);
response.setHeader("Authorization", "Bearer " + newToken);
}
} catch (JwtException e) {
// token 失效或非法忽略即可
}
}
filterChain.doFilter(request, response);
}
private String getTokenFromHeader(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
}

View File

@ -0,0 +1,40 @@
package github.benjamin.equipreservebackend.security;
import github.benjamin.equipreservebackend.entity.Role;
import github.benjamin.equipreservebackend.entity.User;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Getter
public class SecurityUser implements UserDetails {
private final User user;
private final List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getCode()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
}

View File

@ -0,0 +1,20 @@
package github.benjamin.equipreservebackend.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.entity.Device;
import github.benjamin.equipreservebackend.vo.DeviceUserVO;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
public interface DeviceService {
Page<DeviceUserVO> getDeviceVO(Page<Device> pageRequest);
void addDevice(Device device);
void deleteDevice(Long id);
Device updateDevice(Device device);
String saveImage(Long id, MultipartFile image) throws IOException;
}

View File

@ -0,0 +1,16 @@
package github.benjamin.equipreservebackend.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.entity.Reservation;
import github.benjamin.equipreservebackend.vo.UserReservationVO;
import java.util.List;
public interface ReservationService {
List<Reservation> getApprovedReservationsByDeviceIds(List<Long> devicesIds);
void addReservation(Reservation reservation);
Page<UserReservationVO> getUserReservationVO(Long userId, Page<Reservation> pageRequest);
}

View File

@ -0,0 +1,11 @@
package github.benjamin.equipreservebackend.service;
import github.benjamin.equipreservebackend.entity.User;
import github.benjamin.equipreservebackend.security.SecurityUser;
public interface UserService {
User login(String username, String password);
SecurityUser loadSecurityUserById(Long userId);
}

View File

@ -0,0 +1,130 @@
package github.benjamin.equipreservebackend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.constant.DeviceReservationState;
import github.benjamin.equipreservebackend.constant.DeviceStatus;
import github.benjamin.equipreservebackend.entity.Device;
import github.benjamin.equipreservebackend.entity.Reservation;
import github.benjamin.equipreservebackend.exception.ApiException;
import github.benjamin.equipreservebackend.mapper.DeviceMapper;
import github.benjamin.equipreservebackend.response.ResponseCode;
import github.benjamin.equipreservebackend.service.DeviceService;
import github.benjamin.equipreservebackend.service.ReservationService;
import github.benjamin.equipreservebackend.utils.FileUtil;
import github.benjamin.equipreservebackend.utils.PageUtil;
import github.benjamin.equipreservebackend.vo.DeviceUserVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
@Service
@RequiredArgsConstructor
public class DeviceServiceImpl implements DeviceService {
private final DeviceMapper deviceMapper;
private final ReservationService reservationService;
@Override
public Page<DeviceUserVO> getDeviceVO(Page<Device> pageRequest) {
Page<Device> devices = deviceMapper.selectPage(pageRequest, new LambdaQueryWrapper<Device>()
.eq(Device::getStatus, DeviceStatus.AVAILABLE)
.orderByAsc(Device::getName));
List<Long> deviceIds = devices.getRecords().stream()
.map(Device::getId)
.toList();
List<Reservation> reservations = reservationService.getApprovedReservationsByDeviceIds(deviceIds);
List<DeviceUserVO> deviceUserVOS = devices.getRecords().stream()
.map(device -> buildDeviceVO(device, reservations))
.toList();
Page<DeviceUserVO> res = PageUtil.copyPage(devices);
res.setRecords(deviceUserVOS);
return res;
}
@Override
public void addDevice(Device device) {
deviceMapper.insert(device);
}
@Override
public void deleteDevice(Long id) {
deviceMapper.delete(new LambdaQueryWrapper<Device>()
.eq(Device::getId, id));
}
@Override
public Device updateDevice(Device device) {
LambdaUpdateWrapper<Device> wrapper =new LambdaUpdateWrapper<>();
wrapper.eq(Device::getId, device.getId());
wrapper.set(Objects.nonNull(device.getName()), Device::getName, device.getName());
wrapper.set(Objects.nonNull(device.getUsageRequirement()), Device::getUsageRequirement, device.getUsageRequirement());
wrapper.set(Objects.nonNull(device.getLocation()), Device::getLocation, device.getLocation());
wrapper.set(Objects.nonNull(device.getStatus()), Device::getStatus, device.getStatus());
wrapper.set(Objects.nonNull(device.getTeamId()), Device::getTeamId, device.getTeamId());
if (deviceMapper.update(wrapper) <= 0) {
throw new ApiException(ResponseCode.UNKNOWN_ERROR, "更新设备失败");
}
return deviceMapper.selectOne(new LambdaQueryWrapper<Device>()
.eq(Device::getId, device.getId()));
}
@Override
public String saveImage(Long id, MultipartFile image) throws IOException {
Device device = deviceMapper.selectOne(new LambdaQueryWrapper<Device>().eq(Device::getId, id));
String basePath = System.getProperty("user.dir");
if (StringUtils.hasText(device.getImagePath())) {
Path fullPath = Paths.get(basePath, device.getImagePath()).normalize();
File file = fullPath.toFile();
if (file.exists() && file.isFile()) {
file.delete();
}
}
File dir = new File(basePath, "device_image");
if (!dir.exists()) {
dir.mkdirs();
}
String newFilename = FileUtil.getUniqueFileName(device.getName(), image);
// 保存图片
Path savePath = Paths.get(basePath, "device_image", newFilename);
Files.copy(image.getInputStream(), savePath, StandardCopyOption.REPLACE_EXISTING);
String imagePath = "device_image/" + newFilename;
device.setImagePath(imagePath);
deviceMapper.update(new LambdaUpdateWrapper<Device>()
.eq(Device::getId, id)
.set(Device::getImagePath, device.getImagePath()));
return imagePath;
}
private DeviceUserVO buildDeviceVO(Device device, List<Reservation> reservations) {
DeviceUserVO vo = new DeviceUserVO(device);
// TODO 显示设备状态逻辑根据客户需求修改
if (reservations.stream().anyMatch(r -> Objects.equals(r.getDeviceId(), device.getId()))) {
vo.setState(DeviceReservationState.RESERVED.getLabel());
} else {
vo.setState(DeviceReservationState.FREE.getLabel());
}
return vo;
}
}

View File

@ -0,0 +1,97 @@
package github.benjamin.equipreservebackend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.constant.ReservationStatus;
import github.benjamin.equipreservebackend.entity.Device;
import github.benjamin.equipreservebackend.entity.Reservation;
import github.benjamin.equipreservebackend.entity.User;
import github.benjamin.equipreservebackend.mapper.DeviceMapper;
import github.benjamin.equipreservebackend.mapper.ReservationMapper;
import github.benjamin.equipreservebackend.mapper.UserMapper;
import github.benjamin.equipreservebackend.service.ReservationService;
import github.benjamin.equipreservebackend.utils.PageUtil;
import github.benjamin.equipreservebackend.vo.UserReservationVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ReservationServiceImpl implements ReservationService {
private final ReservationMapper reservationMapper;
private final DeviceMapper deviceMapper;
private final UserMapper userMapper;
/**
* 未来days天内有预约的设备显示为有预约
*/
@Value("${equip-reserve.device-days}")
private Integer days;
@Override
public List<Reservation> getApprovedReservationsByDeviceIds(List<Long> devicesIds) {
LocalDateTime endTime = LocalDateTime.now().plusDays(days);
return reservationMapper.selectList(new LambdaQueryWrapper<Reservation>()
.in(Reservation::getDeviceId, devicesIds)
.eq(Reservation::getStatus, ReservationStatus.APPROVED)
.lt(Reservation::getEndTime, endTime));
}
@Override
public void addReservation(Reservation reservation) {
reservation.setStatus(String.valueOf(ReservationStatus.PENDING_LEADER));
reservationMapper.insert(reservation);
}
@Override
public Page<UserReservationVO> getUserReservationVO(Long userId, Page<Reservation> pageRequest) {
Page<Reservation> reservations = reservationMapper.selectPage(pageRequest, new LambdaQueryWrapper<Reservation>()
.eq(Reservation::getUserId, userId)
.orderByDesc(Reservation::getCreatedTime));
// 获取设备名称
List<Long> deviceIds = reservations.getRecords().stream()
.map(Reservation::getDeviceId)
.distinct()
.toList();
Map<Long, String> deviceNameMap = deviceMapper.selectList(new LambdaQueryWrapper<Device>()
.in(Device::getId, deviceIds))
.stream()
.collect(Collectors.toMap(Device::getId, Device::getName));
// 获取设备管理员信息
List<Long> deviceAdminIDs = reservations.getRecords().stream()
.map(Reservation::getDeviceAdminId)
.filter(Objects::nonNull)
.distinct()
.toList();
Page<UserReservationVO> res = PageUtil.copyPage(reservations);
List<UserReservationVO> vos;
if (deviceAdminIDs.isEmpty()) {
vos = reservations.getRecords().stream()
.map(reservation -> new UserReservationVO(reservation, deviceNameMap))
.toList();
} else {
Map<Long, User> deviceAdminMap = userMapper.selectList(new LambdaQueryWrapper<User>()
.in(User::getId, deviceAdminIDs))
.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
vos = reservations.getRecords().stream()
.map(reservation -> new UserReservationVO(reservation, deviceNameMap, deviceAdminMap))
.toList();
}
res.setRecords(vos);
return res;
}
}

View File

@ -0,0 +1,48 @@
package github.benjamin.equipreservebackend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import github.benjamin.equipreservebackend.entity.Role;
import github.benjamin.equipreservebackend.entity.User;
import github.benjamin.equipreservebackend.exception.ApiException;
import github.benjamin.equipreservebackend.mapper.RoleMapper;
import github.benjamin.equipreservebackend.mapper.UserMapper;
import github.benjamin.equipreservebackend.response.ResponseCode;
import github.benjamin.equipreservebackend.security.SecurityUser;
import github.benjamin.equipreservebackend.service.UserService;
import github.benjamin.equipreservebackend.utils.JwtUtil;
import github.benjamin.equipreservebackend.utils.PasswordUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final JwtUtil jwtUtil;
@Override
public User login(String username, String password) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (Objects.isNull(user)) {
throw new ApiException(ResponseCode.USER_NOT_EXIST, "用户不存在");
}
if (!PasswordUtil.matches(password, user.getPassword())) {
throw new ApiException(ResponseCode.PASSWORD_ERROR, "密码错误");
}
return user;
}
@Override
public SecurityUser loadSecurityUserById(Long userId) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId));
List<Role> roles = roleMapper.selectRoleByUserId(userId);
return new SecurityUser(user, roles);
}
}

View File

@ -0,0 +1,26 @@
package github.benjamin.equipreservebackend.utils;
import org.springframework.web.multipart.MultipartFile;
import java.util.UUID;
public class FileUtil {
public static String getFileExtension(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
return originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf('.'))
: "";
}
public static String getUniqueFileName(String name, MultipartFile file) {
String extension = FileUtil.getFileExtension(file);
// 构建新文件名设备名称 + UUID + 扩展名
String safeName = name.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); // 防止非法字符
return safeName + "_" + UUID.randomUUID() + extension;
}
private FileUtil(){}
}

View File

@ -0,0 +1,69 @@
package github.benjamin.equipreservebackend.utils;
import github.benjamin.equipreservebackend.security.SecurityUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String SECRET_KEY;
public String generateToken(SecurityUser securityUser) {
return Jwts.builder()
.subject(securityUser.getUser().getId().toString())
.claim("role", securityUser.getRoles())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3600_000))
.signWith(getKey(), Jwts.SIG.HS256)
.compact();
}
public Long getUserId(String token) {
return Long.valueOf(parseToken(token).getSubject());
}
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 如果token在十分钟内会过期则需要刷新
* @param token
* @return 是否需要刷新token
*/
public Boolean shouldRefresh(String token) {
Date expiration = getExpiration(token);
Long currentTime = System.currentTimeMillis();
Long expireTime = expiration.getTime();
return expireTime - currentTime < TimeUnit.MINUTES.toMillis(10);
}
public Date getExpiration(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
}
private SecretKey getKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -0,0 +1,12 @@
package github.benjamin.equipreservebackend.utils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
public class PageUtil {
public static <T> Page<T> copyPage(Page<?> page) {
return new Page<T>(page.getCurrent(), page.getSize(), page.getTotal(), page.searchCount());
}
private PageUtil() {}
}

View File

@ -0,0 +1,18 @@
package github.benjamin.equipreservebackend.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordUtil {
private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder();
public static String encode(String rawPassword) {
return ENCODER.encode(rawPassword);
}
public static Boolean matches(String rawPassword, String encodedPassword) {
return ENCODER.matches(rawPassword, encodedPassword);
}
private PasswordUtil(){}
}

View File

@ -0,0 +1,30 @@
package github.benjamin.equipreservebackend.vo;
import github.benjamin.equipreservebackend.entity.Device;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class DeviceUserVO {
private Long deviceId;
private String name;
private String usageRequirement;
private String location;
private String imagePath;
/**
* 直接给出前端要显示的内容
* 可预约有预约
*/
private String state;
public DeviceUserVO(Device device) {
this.deviceId = device.getId();
this.name = device.getName();
this.usageRequirement = device.getUsageRequirement();
this.location = device.getLocation();
this.imagePath = device.getImagePath();
}
}

View File

@ -0,0 +1,14 @@
package github.benjamin.equipreservebackend.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class LoginResponse {
private Long userId;
private String name;
private String token;
}

View File

@ -0,0 +1,44 @@
package github.benjamin.equipreservebackend.vo;
import github.benjamin.equipreservebackend.constant.ReservationStatus;
import github.benjamin.equipreservebackend.entity.Reservation;
import github.benjamin.equipreservebackend.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserReservationVO {
private String deviceName;
private LocalDate startTime;
private LocalDate endTime;
private String statusLabel;
private String deviceAdminName;
private String deviceAdminContact;
private LocalDateTime createdTime;
public UserReservationVO(Reservation r, Map<Long, String> deviceNameMap) {
this.deviceName = deviceNameMap.get(r.getDeviceId());
this.startTime = r.getStartTime();
this.endTime = r.getEndTime();
this.statusLabel = ReservationStatus.valueOf(r.getStatus()).getLabel();
this.createdTime = r.getCreatedTime();
}
public UserReservationVO(Reservation r, Map<Long, String> deviceNameMap, Map<Long, User> deviceAdminMap) {
this(r, deviceNameMap);
ReservationStatus status = ReservationStatus.valueOf(r.getStatus());
if (status == ReservationStatus.APPROVED_ASSIST) {
User deviceAdmin = deviceAdminMap.get(r.getDeviceAdminId());
this.deviceAdminName = deviceAdmin.getName();
this.deviceAdminContact = deviceAdmin.getPhone();
}
}
}

View File

@ -0,0 +1,8 @@
spring:
application:
name: equip-reserve-backend
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: your-username
password: your-password
url: jdbc:mysql://127.0.0.1:3306/equip_reserve?serverTimeZone=UTC

View File

@ -0,0 +1,13 @@
package github.benjamin.equipreservebackend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class EquipReserveBackendApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -0,0 +1,30 @@
package github.benjamin.equipreservebackend;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import github.benjamin.equipreservebackend.constant.DeviceReservationState;
import github.benjamin.equipreservebackend.controller.DeviceController;
import github.benjamin.equipreservebackend.response.ResponseResult;
import github.benjamin.equipreservebackend.vo.DeviceUserVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class MybatisInitTest {
@Test
public void testSelect(){
}
@Autowired
DeviceController deviceController;
@Test
public void testGetDevice() {
log.info(DeviceReservationState.RESERVED.getLabel());
log.info(DeviceReservationState.RESERVED.name());
log.info(DeviceReservationState.valueOf("RESERVED").getLabel());
}
}