From 275ca899624535a549e95a03fc24fb313aefc733 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Sat, 21 Jun 2025 11:22:01 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BB=8EIdea=20local=20history?= =?UTF-8?q?=E4=B8=AD=E6=81=A2=E5=A4=8D=E6=96=87=E4=BB=B6=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E8=87=B3=E7=94=A8=E6=88=B7=E9=A2=84=E7=BA=A6=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 78 +++++++++++ HELP.md | 33 +++++ db/equip_reserve.sql | 100 ++++++++++++++ pom.xml | 113 +++++++++++++++ .../EquipReserveBackendApplication.java | 15 ++ .../config/MybatisPlusConfig.java | 19 +++ .../config/SecurityConfig.java | 41 ++++++ .../equipreservebackend/config/WebConfig.java | 15 ++ .../constant/DeviceReservationState.java | 17 +++ .../constant/DeviceStatus.java | 12 ++ .../constant/ReservationStatus.java | 18 +++ .../controller/DeviceController.java | 61 ++++++++ .../controller/ReservationController.java | 36 +++++ .../controller/UserController.java | 28 ++++ .../equipreservebackend/entity/Approval.java | 19 +++ .../equipreservebackend/entity/Device.java | 22 +++ .../entity/Reservation.java | 25 ++++ .../equipreservebackend/entity/Role.java | 14 ++ .../equipreservebackend/entity/Team.java | 17 +++ .../equipreservebackend/entity/User.java | 22 +++ .../exception/ApiException.java | 19 +++ .../exception/GlobalExceptionHandler.java | 31 +++++ .../mapper/DeviceMapper.java | 9 ++ .../mapper/ReservationMapper.java | 9 ++ .../mapper/RoleMapper.java | 22 +++ .../mapper/UserMapper.java | 9 ++ .../response/ResponseCode.java | 26 ++++ .../response/ResponseResult.java | 35 +++++ .../security/JwtAuthFilter.java | 55 ++++++++ .../security/SecurityUser.java | 40 ++++++ .../service/DeviceService.java | 20 +++ .../service/ReservationService.java | 16 +++ .../service/UserService.java | 11 ++ .../service/impl/DeviceServiceImpl.java | 130 ++++++++++++++++++ .../service/impl/ReservationServiceImpl.java | 97 +++++++++++++ .../service/impl/UserServiceImpl.java | 48 +++++++ .../equipreservebackend/utils/FileUtil.java | 26 ++++ .../equipreservebackend/utils/JwtUtil.java | 69 ++++++++++ .../equipreservebackend/utils/PageUtil.java | 12 ++ .../utils/PasswordUtil.java | 18 +++ .../equipreservebackend/vo/DeviceUserVO.java | 30 ++++ .../equipreservebackend/vo/LoginResponse.java | 14 ++ .../vo/UserReservationVO.java | 44 ++++++ src/main/resources/application-template.yaml | 8 ++ .../EquipReserveBackendApplicationTests.java | 13 ++ .../equipreservebackend/MybatisInitTest.java | 30 ++++ 46 files changed, 1546 insertions(+) create mode 100644 .gitignore create mode 100644 HELP.md create mode 100644 db/equip_reserve.sql create mode 100644 pom.xml create mode 100644 src/main/java/github/benjamin/equipreservebackend/EquipReserveBackendApplication.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/config/MybatisPlusConfig.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/config/SecurityConfig.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/config/WebConfig.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/constant/DeviceReservationState.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/constant/DeviceStatus.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/constant/ReservationStatus.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/controller/DeviceController.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/controller/ReservationController.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/controller/UserController.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/Approval.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/Device.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/Reservation.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/Role.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/Team.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/entity/User.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/exception/ApiException.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/mapper/DeviceMapper.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/mapper/ReservationMapper.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/mapper/RoleMapper.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/mapper/UserMapper.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/response/ResponseCode.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/response/ResponseResult.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/security/JwtAuthFilter.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/security/SecurityUser.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/DeviceService.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/ReservationService.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/UserService.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/impl/DeviceServiceImpl.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/impl/ReservationServiceImpl.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/service/impl/UserServiceImpl.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/utils/FileUtil.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/utils/JwtUtil.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/utils/PageUtil.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/utils/PasswordUtil.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/vo/DeviceUserVO.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/vo/LoginResponse.java create mode 100644 src/main/java/github/benjamin/equipreservebackend/vo/UserReservationVO.java create mode 100644 src/main/resources/application-template.yaml create mode 100644 src/test/java/github/benjamin/equipreservebackend/EquipReserveBackendApplicationTests.java create mode 100644 src/test/java/github/benjamin/equipreservebackend/MybatisInitTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c759098 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..eda0c22 --- /dev/null +++ b/HELP.md @@ -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 `` and `` 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. + diff --git a/db/equip_reserve.sql b/db/equip_reserve.sql new file mode 100644 index 0000000..79349ae --- /dev/null +++ b/db/equip_reserve.sql @@ -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', '普通用户'); diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..646c86a --- /dev/null +++ b/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.6 + + + github.benjamin + equip-reserve-backend + 0.0.1-SNAPSHOT + equip-reserve-backend + equip-reserve-backend + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.5 + + + com.mysql + mysql-connector-j + 8.3.0 + + + + org.projectlombok + lombok + true + + + + io.jsonwebtoken + jjwt + 0.12.6 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.baomidou + mybatis-plus-spring-boot3-starter-test + 3.5.12 + test + + + org.springframework.security + spring-security-test + test + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/github/benjamin/equipreservebackend/EquipReserveBackendApplication.java b/src/main/java/github/benjamin/equipreservebackend/EquipReserveBackendApplication.java new file mode 100644 index 0000000..97131da --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/EquipReserveBackendApplication.java @@ -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); + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/config/MybatisPlusConfig.java b/src/main/java/github/benjamin/equipreservebackend/config/MybatisPlusConfig.java new file mode 100644 index 0000000..34407a4 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/config/MybatisPlusConfig.java @@ -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; + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/config/SecurityConfig.java b/src/main/java/github/benjamin/equipreservebackend/config/SecurityConfig.java new file mode 100644 index 0000000..0483177 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/config/SecurityConfig.java @@ -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); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/config/WebConfig.java b/src/main/java/github/benjamin/equipreservebackend/config/WebConfig.java new file mode 100644 index 0000000..1493a1c --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/config/WebConfig.java @@ -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/"); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/constant/DeviceReservationState.java b/src/main/java/github/benjamin/equipreservebackend/constant/DeviceReservationState.java new file mode 100644 index 0000000..a92aeb2 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/constant/DeviceReservationState.java @@ -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; + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/constant/DeviceStatus.java b/src/main/java/github/benjamin/equipreservebackend/constant/DeviceStatus.java new file mode 100644 index 0000000..a10fbae --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/constant/DeviceStatus.java @@ -0,0 +1,12 @@ +package github.benjamin.equipreservebackend.constant; + +/** + * 设备内部状态 + */ +public enum DeviceStatus { + + AVAILABLE, + MAINTENANCE, + DISABLED + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/constant/ReservationStatus.java b/src/main/java/github/benjamin/equipreservebackend/constant/ReservationStatus.java new file mode 100644 index 0000000..f6f10b9 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/constant/ReservationStatus.java @@ -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; + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/controller/DeviceController.java b/src/main/java/github/benjamin/equipreservebackend/controller/DeviceController.java new file mode 100644 index 0000000..20a4cc7 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/controller/DeviceController.java @@ -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> getDevices(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + Page pageRequest = new Page<>(page, size); + Page 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); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/controller/ReservationController.java b/src/main/java/github/benjamin/equipreservebackend/controller/ReservationController.java new file mode 100644 index 0000000..8089dfa --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/controller/ReservationController.java @@ -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> getUserReservation(@PathVariable("userId") Long userId, + @RequestParam(defaultValue = "10") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + Page pageRequest = new Page<>(page, size); + Page res = reservationService.getUserReservationVO(userId, pageRequest); + return ResponseResult.success(res); + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/controller/UserController.java b/src/main/java/github/benjamin/equipreservebackend/controller/UserController.java new file mode 100644 index 0000000..26cc3aa --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/controller/UserController.java @@ -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 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)); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/Approval.java b/src/main/java/github/benjamin/equipreservebackend/entity/Approval.java new file mode 100644 index 0000000..ea54908 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/Approval.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/Device.java b/src/main/java/github/benjamin/equipreservebackend/entity/Device.java new file mode 100644 index 0000000..0578eb9 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/Device.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/Reservation.java b/src/main/java/github/benjamin/equipreservebackend/entity/Reservation.java new file mode 100644 index 0000000..3fb9e64 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/Reservation.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/Role.java b/src/main/java/github/benjamin/equipreservebackend/entity/Role.java new file mode 100644 index 0000000..d809e31 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/Role.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/Team.java b/src/main/java/github/benjamin/equipreservebackend/entity/Team.java new file mode 100644 index 0000000..94360b2 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/Team.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/entity/User.java b/src/main/java/github/benjamin/equipreservebackend/entity/User.java new file mode 100644 index 0000000..6b4fe9e --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/entity/User.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/exception/ApiException.java b/src/main/java/github/benjamin/equipreservebackend/exception/ApiException.java new file mode 100644 index 0000000..da5350a --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/exception/ApiException.java @@ -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; + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/exception/GlobalExceptionHandler.java b/src/main/java/github/benjamin/equipreservebackend/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b4bc036 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/exception/GlobalExceptionHandler.java @@ -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,"服务器内部异常,请联系开发人员"); + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/mapper/DeviceMapper.java b/src/main/java/github/benjamin/equipreservebackend/mapper/DeviceMapper.java new file mode 100644 index 0000000..a2bfa17 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/mapper/DeviceMapper.java @@ -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 { +} diff --git a/src/main/java/github/benjamin/equipreservebackend/mapper/ReservationMapper.java b/src/main/java/github/benjamin/equipreservebackend/mapper/ReservationMapper.java new file mode 100644 index 0000000..4851185 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/mapper/ReservationMapper.java @@ -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 { +} diff --git a/src/main/java/github/benjamin/equipreservebackend/mapper/RoleMapper.java b/src/main/java/github/benjamin/equipreservebackend/mapper/RoleMapper.java new file mode 100644 index 0000000..4904163 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/mapper/RoleMapper.java @@ -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 { + + @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 selectRoleByUserId(@Param("userId") Long userId); + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/mapper/UserMapper.java b/src/main/java/github/benjamin/equipreservebackend/mapper/UserMapper.java new file mode 100644 index 0000000..7358f94 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/mapper/UserMapper.java @@ -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 { +} diff --git a/src/main/java/github/benjamin/equipreservebackend/response/ResponseCode.java b/src/main/java/github/benjamin/equipreservebackend/response/ResponseCode.java new file mode 100644 index 0000000..ae9ce45 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/response/ResponseCode.java @@ -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() {} // 不允许实例化 + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/response/ResponseResult.java b/src/main/java/github/benjamin/equipreservebackend/response/ResponseResult.java new file mode 100644 index 0000000..cb1b1dc --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/response/ResponseResult.java @@ -0,0 +1,35 @@ +package github.benjamin.equipreservebackend.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ResponseResult { + + private int code; + private String message; + private T data; + + public static ResponseResult success() { + return ResponseResult.success("success", null); + } + + public static ResponseResult success(T data) { + return ResponseResult.success("success", data); + } + + public static ResponseResult success(String message, T data) { + return new ResponseResult<>(ResponseCode.SUCCESS, message, data); + } + + public static ResponseResult fail(Integer code, String message) { + return new ResponseResult<>(code, message, null); + } + + public static ResponseResult of(Integer code, String message, T data) { + return new ResponseResult<>(code, message, data); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/security/JwtAuthFilter.java b/src/main/java/github/benjamin/equipreservebackend/security/JwtAuthFilter.java new file mode 100644 index 0000000..3164721 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/security/JwtAuthFilter.java @@ -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; + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/security/SecurityUser.java b/src/main/java/github/benjamin/equipreservebackend/security/SecurityUser.java new file mode 100644 index 0000000..42496b6 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/security/SecurityUser.java @@ -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 roles; + + @Override + public Collection 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(); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/DeviceService.java b/src/main/java/github/benjamin/equipreservebackend/service/DeviceService.java new file mode 100644 index 0000000..d32b351 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/DeviceService.java @@ -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 getDeviceVO(Page pageRequest); + + void addDevice(Device device); + + void deleteDevice(Long id); + + Device updateDevice(Device device); + + String saveImage(Long id, MultipartFile image) throws IOException; +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/ReservationService.java b/src/main/java/github/benjamin/equipreservebackend/service/ReservationService.java new file mode 100644 index 0000000..77770c4 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/ReservationService.java @@ -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 getApprovedReservationsByDeviceIds(List devicesIds); + + void addReservation(Reservation reservation); + + Page getUserReservationVO(Long userId, Page pageRequest); +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/UserService.java b/src/main/java/github/benjamin/equipreservebackend/service/UserService.java new file mode 100644 index 0000000..ca9dd3f --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/UserService.java @@ -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); +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/impl/DeviceServiceImpl.java b/src/main/java/github/benjamin/equipreservebackend/service/impl/DeviceServiceImpl.java new file mode 100644 index 0000000..ca1313a --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/impl/DeviceServiceImpl.java @@ -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 getDeviceVO(Page pageRequest) { + Page devices = deviceMapper.selectPage(pageRequest, new LambdaQueryWrapper() + .eq(Device::getStatus, DeviceStatus.AVAILABLE) + .orderByAsc(Device::getName)); + List deviceIds = devices.getRecords().stream() + .map(Device::getId) + .toList(); + List reservations = reservationService.getApprovedReservationsByDeviceIds(deviceIds); + List deviceUserVOS = devices.getRecords().stream() + .map(device -> buildDeviceVO(device, reservations)) + .toList(); + Page 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() + .eq(Device::getId, id)); + } + + @Override + public Device updateDevice(Device device) { + LambdaUpdateWrapper 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() + .eq(Device::getId, device.getId())); + } + + @Override + public String saveImage(Long id, MultipartFile image) throws IOException { + Device device = deviceMapper.selectOne(new LambdaQueryWrapper().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() + .eq(Device::getId, id) + .set(Device::getImagePath, device.getImagePath())); + + return imagePath; + } + + private DeviceUserVO buildDeviceVO(Device device, List 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; + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/impl/ReservationServiceImpl.java b/src/main/java/github/benjamin/equipreservebackend/service/impl/ReservationServiceImpl.java new file mode 100644 index 0000000..e331cc6 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/impl/ReservationServiceImpl.java @@ -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 getApprovedReservationsByDeviceIds(List devicesIds) { + LocalDateTime endTime = LocalDateTime.now().plusDays(days); + return reservationMapper.selectList(new LambdaQueryWrapper() + .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 getUserReservationVO(Long userId, Page pageRequest) { + Page reservations = reservationMapper.selectPage(pageRequest, new LambdaQueryWrapper() + .eq(Reservation::getUserId, userId) + .orderByDesc(Reservation::getCreatedTime)); + // 获取设备名称 + List deviceIds = reservations.getRecords().stream() + .map(Reservation::getDeviceId) + .distinct() + .toList(); + Map deviceNameMap = deviceMapper.selectList(new LambdaQueryWrapper() + .in(Device::getId, deviceIds)) + .stream() + .collect(Collectors.toMap(Device::getId, Device::getName)); + + // 获取设备管理员信息 + List deviceAdminIDs = reservations.getRecords().stream() + .map(Reservation::getDeviceAdminId) + .filter(Objects::nonNull) + .distinct() + .toList(); + + Page res = PageUtil.copyPage(reservations); + List vos; + if (deviceAdminIDs.isEmpty()) { + vos = reservations.getRecords().stream() + .map(reservation -> new UserReservationVO(reservation, deviceNameMap)) + .toList(); + } else { + Map deviceAdminMap = userMapper.selectList(new LambdaQueryWrapper() + .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; + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/service/impl/UserServiceImpl.java b/src/main/java/github/benjamin/equipreservebackend/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..c56ff95 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/service/impl/UserServiceImpl.java @@ -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().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().eq(User::getId, userId)); + List roles = roleMapper.selectRoleByUserId(userId); + return new SecurityUser(user, roles); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/utils/FileUtil.java b/src/main/java/github/benjamin/equipreservebackend/utils/FileUtil.java new file mode 100644 index 0000000..d408faa --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/utils/FileUtil.java @@ -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(){} + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/utils/JwtUtil.java b/src/main/java/github/benjamin/equipreservebackend/utils/JwtUtil.java new file mode 100644 index 0000000..c84f260 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/utils/JwtUtil.java @@ -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)); + } +} diff --git a/src/main/java/github/benjamin/equipreservebackend/utils/PageUtil.java b/src/main/java/github/benjamin/equipreservebackend/utils/PageUtil.java new file mode 100644 index 0000000..e24be83 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/utils/PageUtil.java @@ -0,0 +1,12 @@ +package github.benjamin.equipreservebackend.utils; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +public class PageUtil { + + public static Page copyPage(Page page) { + return new Page(page.getCurrent(), page.getSize(), page.getTotal(), page.searchCount()); + } + + private PageUtil() {} +} diff --git a/src/main/java/github/benjamin/equipreservebackend/utils/PasswordUtil.java b/src/main/java/github/benjamin/equipreservebackend/utils/PasswordUtil.java new file mode 100644 index 0000000..e6dae75 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/utils/PasswordUtil.java @@ -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(){} +} diff --git a/src/main/java/github/benjamin/equipreservebackend/vo/DeviceUserVO.java b/src/main/java/github/benjamin/equipreservebackend/vo/DeviceUserVO.java new file mode 100644 index 0000000..ede3f59 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/vo/DeviceUserVO.java @@ -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(); + } + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/vo/LoginResponse.java b/src/main/java/github/benjamin/equipreservebackend/vo/LoginResponse.java new file mode 100644 index 0000000..2a10c4b --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/vo/LoginResponse.java @@ -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; + +} diff --git a/src/main/java/github/benjamin/equipreservebackend/vo/UserReservationVO.java b/src/main/java/github/benjamin/equipreservebackend/vo/UserReservationVO.java new file mode 100644 index 0000000..f8e6c15 --- /dev/null +++ b/src/main/java/github/benjamin/equipreservebackend/vo/UserReservationVO.java @@ -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 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 deviceNameMap, Map 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(); + } + } +} diff --git a/src/main/resources/application-template.yaml b/src/main/resources/application-template.yaml new file mode 100644 index 0000000..50d091b --- /dev/null +++ b/src/main/resources/application-template.yaml @@ -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 diff --git a/src/test/java/github/benjamin/equipreservebackend/EquipReserveBackendApplicationTests.java b/src/test/java/github/benjamin/equipreservebackend/EquipReserveBackendApplicationTests.java new file mode 100644 index 0000000..d7a4cb9 --- /dev/null +++ b/src/test/java/github/benjamin/equipreservebackend/EquipReserveBackendApplicationTests.java @@ -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() { + } + +} diff --git a/src/test/java/github/benjamin/equipreservebackend/MybatisInitTest.java b/src/test/java/github/benjamin/equipreservebackend/MybatisInitTest.java new file mode 100644 index 0000000..e297d61 --- /dev/null +++ b/src/test/java/github/benjamin/equipreservebackend/MybatisInitTest.java @@ -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()); + } + +}