This commit is contained in:
王子琦
2026-01-14 15:11:25 +08:00
parent cd5a2a3255
commit f006ed4c89
73 changed files with 4632 additions and 0 deletions

76
backend/pom.xml Normal file
View File

@@ -0,0 +1,76 @@
<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>
<groupId>com.car</groupId>
<artifactId>car-rental</artifactId>
<version>1.0.0</version>
<name>car-rental</name>
<description>Car rental platform</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<mybatis.version>3.0.3</mybatis.version>
<sa.token.version>1.38.0</sa.token.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa.token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${sa.token.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

85
backend/sql/init.sql Normal file
View File

@@ -0,0 +1,85 @@
create database if not exists car_rental default character set utf8mb4 collate utf8mb4_general_ci;
use car_rental;
create table if not exists users (
id bigint primary key auto_increment,
username varchar(50) not null unique,
password varchar(255) not null,
phone varchar(50),
email varchar(100),
role varchar(20) not null,
status varchar(20) not null,
balance decimal(10,2) default 0,
real_name_status varchar(20) default 'NONE',
real_name varchar(50),
id_number varchar(30),
id_front varchar(255),
id_back varchar(255),
created_at datetime,
updated_at datetime
);
create table if not exists cars (
id bigint primary key auto_increment,
brand varchar(50),
model varchar(50),
plate_no varchar(50),
price_per_day decimal(10,2),
deposit decimal(10,2),
status varchar(20),
is_special tinyint(1) default 0,
images text,
description text,
seats int,
transmission varchar(20),
fuel_type varchar(20),
mileage int,
created_at datetime,
updated_at datetime
);
create table if not exists favorites (
id bigint primary key auto_increment,
user_id bigint not null,
car_id bigint not null,
created_at datetime,
unique key uk_user_car(user_id, car_id)
);
create table if not exists orders (
id bigint primary key auto_increment,
order_no varchar(64) not null unique,
user_id bigint not null,
car_id bigint not null,
start_date date,
end_date date,
days int,
price_per_day decimal(10,2),
deposit decimal(10,2),
total_amount decimal(10,2),
status varchar(20),
pay_type varchar(20),
paid_at datetime,
created_at datetime,
updated_at datetime
);
create table if not exists payments (
id bigint primary key auto_increment,
order_id bigint not null,
user_id bigint not null,
amount decimal(10,2),
type varchar(20),
created_at datetime
);
insert into users(username, password, phone, email, role, status, balance, real_name_status, created_at, updated_at)
values ('admin', '$2a$10$7hErhcmS8xj6QcGbe0yE0eDBn5OQWw4tGxqVylxYxe3CxbJc88x76', '13800000000', 'admin@example.com', 'ADMIN', 'ACTIVE', 0, 'NONE', now(), now())
on duplicate key update username = username;
insert into cars(brand, model, plate_no, price_per_day, deposit, status, is_special, seats, transmission, fuel_type, mileage, description, created_at, updated_at)
values
('丰田', '卡罗拉', '粤A12345', 200, 1000, 'AVAILABLE', 0, 5, 'AT', '汽油', 32000, '经济实用,适合通勤', now(), now()),
('特斯拉', 'Model 3', '粤B54321', 500, 2000, 'AVAILABLE', 1, 5, 'AT', '电动', 18000, '电动轿跑,舒适安静', now(), now()),
('本田', 'CR-V', '粤C67890', 350, 1500, 'AVAILABLE', 0, 5, 'AT', '汽油', 40000, '空间大,适合家庭出行', now(), now())
on duplicate key update plate_no = plate_no;

View File

@@ -0,0 +1,11 @@
package com.car;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CarRentalApplication {
public static void main(String[] args) {
SpringApplication.run(CarRentalApplication.class, args);
}
}

View File

@@ -0,0 +1,13 @@
package com.car.common;
import lombok.Getter;
@Getter
public class ApiException extends RuntimeException {
private final int code;
public ApiException(int code, String message) {
super(message);
this.code = code;
}
}

View File

@@ -0,0 +1,26 @@
package com.car.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(0, "OK", data);
}
public static ApiResponse<Void> ok() {
return new ApiResponse<>(0, "OK", null);
}
public static ApiResponse<Void> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}

View File

@@ -0,0 +1,10 @@
package com.car.common;
public class ErrorCode {
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int CONFLICT = 409;
public static final int SERVER_ERROR = 500;
}

View File

@@ -0,0 +1,39 @@
package com.car.common;
import cn.dev33.satoken.exception.NotLoginException;
import jakarta.validation.ConstraintViolationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ApiException.class)
public ApiResponse<Void> handleApiException(ApiException ex) {
return ApiResponse.error(ex.getCode(), ex.getMessage());
}
@ExceptionHandler(NotLoginException.class)
public ApiResponse<Void> handleNotLogin(NotLoginException ex) {
return ApiResponse.error(ErrorCode.UNAUTHORIZED, "请先登录");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValid(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getAllErrors().isEmpty()
? "参数错误"
: ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ApiResponse.error(ErrorCode.BAD_REQUEST, msg);
}
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraint(ConstraintViolationException ex) {
return ApiResponse.error(ErrorCode.BAD_REQUEST, ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleGeneric(Exception ex) {
return ApiResponse.error(ErrorCode.SERVER_ERROR, ex.getMessage());
}
}

View File

@@ -0,0 +1,13 @@
package com.car.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,32 @@
package com.car.config;
import cn.dev33.satoken.stp.StpInterface;
import com.car.entity.User;
import com.car.mapper.UserMapper;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
@Component
public class StpInterfaceImpl implements StpInterface {
private final UserMapper userMapper;
public StpInterfaceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return Collections.emptyList();
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
User user = userMapper.findById(Long.valueOf(loginId.toString()));
if (user == null || user.getRole() == null) {
return Collections.emptyList();
}
return Collections.singletonList(user.getRole());
}
}

View File

@@ -0,0 +1,17 @@
package com.car.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}

View File

@@ -0,0 +1,107 @@
package com.car.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.car.common.ApiResponse;
import com.car.common.ApiException;
import com.car.common.ErrorCode;
import com.car.entity.Car;
import com.car.entity.Order;
import com.car.entity.User;
import com.car.service.CarService;
import com.car.service.OrderService;
import com.car.service.StatsService;
import com.car.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
private final UserService userService;
private final CarService carService;
private final OrderService orderService;
private final StatsService statsService;
public AdminController(UserService userService, CarService carService, OrderService orderService, StatsService statsService) {
this.userService = userService;
this.carService = carService;
this.orderService = orderService;
this.statsService = statsService;
}
private void checkAdmin() {
if (!StpUtil.hasRole("ADMIN")) {
throw new ApiException(ErrorCode.FORBIDDEN, "需要管理员权限");
}
}
@GetMapping("/users")
public ApiResponse<List<User>> listUsers(@RequestParam(required = false) String keyword) {
checkAdmin();
return ApiResponse.ok(userService.listUsers(keyword));
}
@PostMapping("/users/{userId}/status")
public ApiResponse<User> changeStatus(@PathVariable Long userId, @RequestParam String status) {
checkAdmin();
return ApiResponse.ok(userService.changeStatus(userId, status));
}
@PostMapping("/real-name/{userId}/approve")
public ApiResponse<User> approve(@PathVariable Long userId) {
checkAdmin();
return ApiResponse.ok(userService.reviewRealName(userId, true));
}
@PostMapping("/real-name/{userId}/reject")
public ApiResponse<User> reject(@PathVariable Long userId) {
checkAdmin();
return ApiResponse.ok(userService.reviewRealName(userId, false));
}
@GetMapping("/cars")
public ApiResponse<List<Car>> listCars(@RequestParam(required = false) String keyword,
@RequestParam(required = false) Boolean isSpecial) {
checkAdmin();
return ApiResponse.ok(carService.list(keyword, isSpecial));
}
@PostMapping("/cars")
public ApiResponse<Car> createCar(@RequestBody Car car) {
checkAdmin();
return ApiResponse.ok(carService.create(car));
}
@PutMapping("/cars")
public ApiResponse<Car> updateCar(@RequestBody Car car) {
checkAdmin();
return ApiResponse.ok(carService.update(car));
}
@DeleteMapping("/cars/{id}")
public ApiResponse<Void> deleteCar(@PathVariable Long id) {
checkAdmin();
carService.delete(id);
return ApiResponse.ok();
}
@GetMapping("/orders")
public ApiResponse<List<Order>> listOrders(@RequestParam(required = false) String status) {
checkAdmin();
return ApiResponse.ok(orderService.listAll(status));
}
@GetMapping("/stats/orders")
public ApiResponse<Map<String, Object>> orderStats() {
checkAdmin();
return ApiResponse.ok(statsService.orderStats());
}
@GetMapping("/stats/finance")
public ApiResponse<Map<String, Object>> financeStats() {
checkAdmin();
return ApiResponse.ok(statsService.financeStats());
}
}

View File

@@ -0,0 +1,44 @@
package com.car.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.car.common.ApiResponse;
import com.car.dto.LoginRequest;
import com.car.dto.RegisterRequest;
import com.car.entity.User;
import com.car.service.UserService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public ApiResponse<User> register(@Valid @RequestBody RegisterRequest request) {
return ApiResponse.ok(userService.register(request));
}
@PostMapping("/login")
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest request) {
User user = userService.login(request);
StpUtil.login(user.getId());
Map<String, Object> data = new HashMap<>();
data.put("token", StpUtil.getTokenValue());
data.put("role", user.getRole());
return ApiResponse.ok(data);
}
@PostMapping("/logout")
public ApiResponse<Void> logout() {
StpUtil.logout();
return ApiResponse.ok();
}
}

View File

@@ -0,0 +1,29 @@
package com.car.controller;
import com.car.common.ApiResponse;
import com.car.entity.Car;
import com.car.service.CarService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/cars")
public class CarController {
private final CarService carService;
public CarController(CarService carService) {
this.carService = carService;
}
@GetMapping
public ApiResponse<List<Car>> list(@RequestParam(required = false) String keyword,
@RequestParam(required = false) Boolean isSpecial) {
return ApiResponse.ok(carService.list(keyword, isSpecial));
}
@GetMapping("/{id}")
public ApiResponse<Car> detail(@PathVariable Long id) {
return ApiResponse.ok(carService.get(id));
}
}

View File

@@ -0,0 +1,109 @@
package com.car.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.car.common.ApiResponse;
import com.car.dto.BalanceRequest;
import com.car.dto.OrderCreateRequest;
import com.car.dto.PayRequest;
import com.car.dto.RealNameRequest;
import com.car.entity.Favorite;
import com.car.entity.Order;
import com.car.entity.User;
import com.car.service.FavoriteService;
import com.car.service.OrderService;
import com.car.service.UserService;
import com.car.util.AuthUtil;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
private final FavoriteService favoriteService;
private final OrderService orderService;
public UserController(UserService userService, FavoriteService favoriteService, OrderService orderService) {
this.userService = userService;
this.favoriteService = favoriteService;
this.orderService = orderService;
}
@GetMapping("/me")
public ApiResponse<User> me() {
Long userId = AuthUtil.getUserId();
User user = userService.getById(userId);
if (user != null) {
user.setPassword(null);
}
return ApiResponse.ok(user);
}
@PutMapping("/profile")
public ApiResponse<User> updateProfile(@RequestBody User user) {
Long userId = AuthUtil.getUserId();
user.setId(userId);
return ApiResponse.ok(userService.updateProfile(user));
}
@PostMapping("/real-name")
public ApiResponse<User> realName(@Valid @RequestBody RealNameRequest request) {
Long userId = AuthUtil.getUserId();
return ApiResponse.ok(userService.submitRealName(userId, request));
}
@PostMapping("/balance")
public ApiResponse<User> addBalance(@Valid @RequestBody BalanceRequest request) {
Long userId = AuthUtil.getUserId();
return ApiResponse.ok(userService.addBalance(userId, request));
}
@PostMapping("/favorite/{carId}")
public ApiResponse<Void> addFavorite(@PathVariable Long carId) {
favoriteService.add(AuthUtil.getUserId(), carId);
return ApiResponse.ok();
}
@DeleteMapping("/favorite/{carId}")
public ApiResponse<Void> removeFavorite(@PathVariable Long carId) {
favoriteService.remove(AuthUtil.getUserId(), carId);
return ApiResponse.ok();
}
@GetMapping("/favorite")
public ApiResponse<List<Favorite>> listFavorites() {
return ApiResponse.ok(favoriteService.list(AuthUtil.getUserId()));
}
@PostMapping("/order")
public ApiResponse<Order> createOrder(@Valid @RequestBody OrderCreateRequest request) {
return ApiResponse.ok(orderService.create(AuthUtil.getUserId(), request));
}
@PostMapping("/order/pay")
public ApiResponse<Order> pay(@Valid @RequestBody PayRequest request) {
return ApiResponse.ok(orderService.pay(AuthUtil.getUserId(), request));
}
@PostMapping("/order/{orderId}/cancel")
public ApiResponse<Order> cancel(@PathVariable Long orderId) {
return ApiResponse.ok(orderService.cancel(AuthUtil.getUserId(), orderId));
}
@PostMapping("/order/{orderId}/return")
public ApiResponse<Order> returnCar(@PathVariable Long orderId) {
return ApiResponse.ok(orderService.returnCar(AuthUtil.getUserId(), orderId));
}
@GetMapping("/order")
public ApiResponse<List<Order>> myOrders(@RequestParam(required = false) String status) {
return ApiResponse.ok(orderService.listByUser(AuthUtil.getUserId(), status));
}
@GetMapping("/check-login")
public ApiResponse<Boolean> checkLogin() {
return ApiResponse.ok(StpUtil.isLogin());
}
}

View File

@@ -0,0 +1,10 @@
package com.car.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class BalanceRequest {
@NotNull(message = "金额不能为空")
private Double amount;
}

View File

@@ -0,0 +1,12 @@
package com.car.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,16 @@
package com.car.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
@Data
public class OrderCreateRequest {
@NotNull(message = "车辆不能为空")
private Long carId;
@NotNull(message = "开始日期不能为空")
private LocalDate startDate;
@NotNull(message = "结束日期不能为空")
private LocalDate endDate;
}

View File

@@ -0,0 +1,13 @@
package com.car.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class PayRequest {
@NotNull(message = "订单不能为空")
private Long orderId;
@NotBlank(message = "支付方式不能为空")
private String payType;
}

View File

@@ -0,0 +1,14 @@
package com.car.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RealNameRequest {
@NotBlank(message = "真实姓名不能为空")
private String realName;
@NotBlank(message = "身份证号不能为空")
private String idNumber;
private String idFront;
private String idBack;
}

View File

@@ -0,0 +1,14 @@
package com.car.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
private String phone;
private String email;
}

View File

@@ -0,0 +1,25 @@
package com.car.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Car {
private Long id;
private String brand;
private String model;
private String plateNo;
private Double pricePerDay;
private Double deposit;
private String status;
private Boolean isSpecial;
private String images;
private String description;
private Integer seats;
private String transmission;
private String fuelType;
private Integer mileage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,13 @@
package com.car.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Favorite {
private Long id;
private Long userId;
private Long carId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,25 @@
package com.car.entity;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class Order {
private Long id;
private String orderNo;
private Long userId;
private Long carId;
private LocalDate startDate;
private LocalDate endDate;
private Integer days;
private Double pricePerDay;
private Double deposit;
private Double totalAmount;
private String status;
private String payType;
private LocalDateTime paidAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,15 @@
package com.car.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Payment {
private Long id;
private Long orderId;
private Long userId;
private Double amount;
private String type;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,24 @@
package com.car.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String username;
private String password;
private String phone;
private String email;
private String role;
private String status;
private Double balance;
private String realNameStatus;
private String realName;
private String idNumber;
private String idFront;
private String idBack;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,20 @@
package com.car.mapper;
import com.car.entity.Car;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CarMapper {
Car findById(@Param("id") Long id);
List<Car> findAll(@Param("keyword") String keyword, @Param("isSpecial") Boolean isSpecial);
int insert(Car car);
int update(Car car);
int delete(@Param("id") Long id);
}

View File

@@ -0,0 +1,18 @@
package com.car.mapper;
import com.car.entity.Favorite;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface FavoriteMapper {
Favorite findByUserAndCar(@Param("userId") Long userId, @Param("carId") Long carId);
int insert(Favorite favorite);
int delete(@Param("id") Long id);
List<Favorite> findByUserId(@Param("userId") Long userId);
}

View File

@@ -0,0 +1,27 @@
package com.car.mapper;
import com.car.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
@Mapper
public interface OrderMapper {
Order findById(@Param("id") Long id);
Order findByOrderNo(@Param("orderNo") String orderNo);
int insert(Order order);
int update(Order order);
List<Order> findByUserId(@Param("userId") Long userId, @Param("status") String status);
List<Order> findAll(@Param("status") String status);
int countOverlap(@Param("carId") Long carId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
}

View File

@@ -0,0 +1,14 @@
package com.car.mapper;
import com.car.entity.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PaymentMapper {
int insert(Payment payment);
List<Payment> findByUserId(@Param("userId") Long userId);
}

View File

@@ -0,0 +1,13 @@
package com.car.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Map;
@Mapper
public interface StatsMapper {
Map<String, Object> orderStats(@Param("status") String status);
Map<String, Object> financeStats();
}

View File

@@ -0,0 +1,20 @@
package com.car.mapper;
import com.car.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
User findByUsername(@Param("username") String username);
User findById(@Param("id") Long id);
int insert(User user);
int update(User user);
List<User> findAll(@Param("keyword") String keyword);
}

View File

@@ -0,0 +1,54 @@
package com.car.service;
import com.car.common.ApiException;
import com.car.common.ErrorCode;
import com.car.entity.Car;
import com.car.mapper.CarMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CarService {
private final CarMapper carMapper;
public CarService(CarMapper carMapper) {
this.carMapper = carMapper;
}
public List<Car> list(String keyword, Boolean isSpecial) {
return carMapper.findAll(keyword, isSpecial);
}
public Car get(Long id) {
Car car = carMapper.findById(id);
if (car == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "车辆不存在");
}
return car;
}
public Car create(Car car) {
if (car.getStatus() == null) {
car.setStatus("AVAILABLE");
}
if (car.getIsSpecial() == null) {
car.setIsSpecial(false);
}
carMapper.insert(car);
return car;
}
public Car update(Car car) {
Car db = carMapper.findById(car.getId());
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "车辆不存在");
}
carMapper.update(car);
return carMapper.findById(car.getId());
}
public void delete(Long id) {
carMapper.delete(id);
}
}

View File

@@ -0,0 +1,41 @@
package com.car.service;
import com.car.common.ApiException;
import com.car.common.ErrorCode;
import com.car.entity.Favorite;
import com.car.mapper.FavoriteMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class FavoriteService {
private final FavoriteMapper favoriteMapper;
public FavoriteService(FavoriteMapper favoriteMapper) {
this.favoriteMapper = favoriteMapper;
}
public void add(Long userId, Long carId) {
Favorite existing = favoriteMapper.findByUserAndCar(userId, carId);
if (existing != null) {
throw new ApiException(ErrorCode.CONFLICT, "已收藏");
}
Favorite favorite = new Favorite();
favorite.setUserId(userId);
favorite.setCarId(carId);
favoriteMapper.insert(favorite);
}
public void remove(Long userId, Long carId) {
Favorite existing = favoriteMapper.findByUserAndCar(userId, carId);
if (existing == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "收藏不存在");
}
favoriteMapper.delete(existing.getId());
}
public List<Favorite> list(Long userId) {
return favoriteMapper.findByUserId(userId);
}
}

View File

@@ -0,0 +1,151 @@
package com.car.service;
import com.car.common.ApiException;
import com.car.common.ErrorCode;
import com.car.dto.OrderCreateRequest;
import com.car.dto.PayRequest;
import com.car.entity.Car;
import com.car.entity.Order;
import com.car.entity.Payment;
import com.car.entity.User;
import com.car.mapper.CarMapper;
import com.car.mapper.OrderMapper;
import com.car.mapper.PaymentMapper;
import com.car.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final CarMapper carMapper;
private final UserMapper userMapper;
private final PaymentMapper paymentMapper;
public OrderService(OrderMapper orderMapper, CarMapper carMapper, UserMapper userMapper, PaymentMapper paymentMapper) {
this.orderMapper = orderMapper;
this.carMapper = carMapper;
this.userMapper = userMapper;
this.paymentMapper = paymentMapper;
}
public Order create(Long userId, OrderCreateRequest request) {
Car car = carMapper.findById(request.getCarId());
if (car == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "车辆不存在");
}
if (!"AVAILABLE".equals(car.getStatus())) {
throw new ApiException(ErrorCode.CONFLICT, "车辆不可租");
}
User user = userMapper.findById(userId);
if (user == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
if (!"APPROVED".equals(user.getRealNameStatus())) {
throw new ApiException(ErrorCode.FORBIDDEN, "请先完成实名认证审核");
}
LocalDate start = request.getStartDate();
LocalDate end = request.getEndDate();
if (end.isBefore(start)) {
throw new ApiException(ErrorCode.BAD_REQUEST, "结束日期不能早于开始日期");
}
int overlap = orderMapper.countOverlap(request.getCarId(), start, end);
if (overlap > 0) {
throw new ApiException(ErrorCode.CONFLICT, "该日期已被预订");
}
long days = ChronoUnit.DAYS.between(start, end) + 1;
Order order = new Order();
order.setOrderNo("ORD" + UUID.randomUUID().toString().replace("-", ""));
order.setUserId(userId);
order.setCarId(request.getCarId());
order.setStartDate(start);
order.setEndDate(end);
order.setDays((int) days);
order.setPricePerDay(car.getPricePerDay());
order.setDeposit(car.getDeposit());
order.setTotalAmount(car.getPricePerDay() * days + car.getDeposit());
order.setStatus("PENDING_PAY");
orderMapper.insert(order);
return orderMapper.findById(order.getId());
}
public Order pay(Long userId, PayRequest request) {
Order order = orderMapper.findById(request.getOrderId());
if (order == null || !order.getUserId().equals(userId)) {
throw new ApiException(ErrorCode.NOT_FOUND, "订单不存在");
}
if (!"PENDING_PAY".equals(order.getStatus())) {
throw new ApiException(ErrorCode.CONFLICT, "订单无法支付");
}
if (!"BALANCE".equalsIgnoreCase(request.getPayType())) {
throw new ApiException(ErrorCode.BAD_REQUEST, "仅支持余额支付");
}
User user = userMapper.findById(userId);
if (user.getBalance() < order.getTotalAmount()) {
throw new ApiException(ErrorCode.CONFLICT, "余额不足");
}
user.setBalance(user.getBalance() - order.getTotalAmount());
userMapper.update(user);
Car car = carMapper.findById(order.getCarId());
car.setStatus("RENTED");
carMapper.update(car);
order.setStatus("RENTING");
order.setPayType("BALANCE");
order.setPaidAt(java.time.LocalDateTime.now());
orderMapper.update(order);
Payment payment = new Payment();
payment.setOrderId(order.getId());
payment.setUserId(userId);
payment.setAmount(order.getTotalAmount());
payment.setType("BALANCE");
paymentMapper.insert(payment);
return orderMapper.findById(order.getId());
}
public Order cancel(Long userId, Long orderId) {
Order order = orderMapper.findById(orderId);
if (order == null || !order.getUserId().equals(userId)) {
throw new ApiException(ErrorCode.NOT_FOUND, "订单不存在");
}
if (!"PENDING_PAY".equals(order.getStatus())) {
throw new ApiException(ErrorCode.CONFLICT, "订单无法取消");
}
order.setStatus("CANCELLED");
orderMapper.update(order);
return orderMapper.findById(orderId);
}
public Order returnCar(Long userId, Long orderId) {
Order order = orderMapper.findById(orderId);
if (order == null || !order.getUserId().equals(userId)) {
throw new ApiException(ErrorCode.NOT_FOUND, "订单不存在");
}
if (!"RENTING".equals(order.getStatus())) {
throw new ApiException(ErrorCode.CONFLICT, "订单无法归还");
}
order.setStatus("RETURNED");
orderMapper.update(order);
Car car = carMapper.findById(order.getCarId());
car.setStatus("AVAILABLE");
carMapper.update(car);
return orderMapper.findById(orderId);
}
public List<Order> listByUser(Long userId, String status) {
return orderMapper.findByUserId(userId, status);
}
public List<Order> listAll(String status) {
return orderMapper.findAll(status);
}
}

View File

@@ -0,0 +1,23 @@
package com.car.service;
import com.car.mapper.StatsMapper;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class StatsService {
private final StatsMapper statsMapper;
public StatsService(StatsMapper statsMapper) {
this.statsMapper = statsMapper;
}
public Map<String, Object> orderStats() {
return statsMapper.orderStats(null);
}
public Map<String, Object> financeStats() {
return statsMapper.financeStats();
}
}

View File

@@ -0,0 +1,126 @@
package com.car.service;
import com.car.common.ApiException;
import com.car.common.ErrorCode;
import com.car.dto.BalanceRequest;
import com.car.dto.LoginRequest;
import com.car.dto.RealNameRequest;
import com.car.dto.RegisterRequest;
import com.car.entity.User;
import com.car.mapper.UserMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserMapper userMapper;
private final BCryptPasswordEncoder encoder;
public UserService(UserMapper userMapper, BCryptPasswordEncoder encoder) {
this.userMapper = userMapper;
this.encoder = encoder;
}
public User register(RegisterRequest request) {
User existing = userMapper.findByUsername(request.getUsername());
if (existing != null) {
throw new ApiException(ErrorCode.CONFLICT, "用户名已存在");
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(encoder.encode(request.getPassword()));
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setRole("USER");
user.setStatus("ACTIVE");
user.setBalance(0.0);
user.setRealNameStatus("NONE");
userMapper.insert(user);
user.setPassword(null);
return user;
}
public User login(LoginRequest request) {
User user = userMapper.findByUsername(request.getUsername());
if (user == null || !encoder.matches(request.getPassword(), user.getPassword())) {
throw new ApiException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
}
if (!"ACTIVE".equals(user.getStatus())) {
throw new ApiException(ErrorCode.FORBIDDEN, "账号已禁用");
}
return user;
}
public User getById(Long id) {
return userMapper.findById(id);
}
public User updateProfile(User input) {
User db = userMapper.findById(input.getId());
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
db.setPhone(input.getPhone());
db.setEmail(input.getEmail());
userMapper.update(db);
db.setPassword(null);
return db;
}
public User submitRealName(Long userId, RealNameRequest request) {
User db = userMapper.findById(userId);
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
db.setRealName(request.getRealName());
db.setIdNumber(request.getIdNumber());
db.setIdFront(request.getIdFront());
db.setIdBack(request.getIdBack());
db.setRealNameStatus("PENDING");
userMapper.update(db);
db.setPassword(null);
return db;
}
public User reviewRealName(Long userId, boolean approved) {
User db = userMapper.findById(userId);
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
db.setRealNameStatus(approved ? "APPROVED" : "REJECTED");
userMapper.update(db);
db.setPassword(null);
return db;
}
public User changeStatus(Long userId, String status) {
User db = userMapper.findById(userId);
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
db.setStatus(status);
userMapper.update(db);
db.setPassword(null);
return db;
}
public User addBalance(Long userId, BalanceRequest request) {
if (request.getAmount() <= 0) {
throw new ApiException(ErrorCode.BAD_REQUEST, "金额必须大于0");
}
User db = userMapper.findById(userId);
if (db == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
}
db.setBalance(db.getBalance() + request.getAmount());
userMapper.update(db);
db.setPassword(null);
return db;
}
public List<User> listUsers(String keyword) {
return userMapper.findAll(keyword);
}
}

View File

@@ -0,0 +1,9 @@
package com.car.util;
import cn.dev33.satoken.stp.StpUtil;
public class AuthUtil {
public static Long getUserId() {
return Long.valueOf(StpUtil.getLoginId().toString());
}
}

View File

@@ -0,0 +1,26 @@
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/car_rental?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
mybatis:
mapper-locations: classpath:mappers/*.xml
configuration:
map-underscore-to-camel-case: true
sa-token:
token-name: Authorization
token-style: jwt
timeout: 86400
active-timeout: -1
is-concurrent: true
is-share: true
token-prefix: Bearer
is-log: false

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.CarMapper">
<resultMap id="CarMap" type="com.car.entity.Car">
<id column="id" property="id"/>
<result column="brand" property="brand"/>
<result column="model" property="model"/>
<result column="plate_no" property="plateNo"/>
<result column="price_per_day" property="pricePerDay"/>
<result column="deposit" property="deposit"/>
<result column="status" property="status"/>
<result column="is_special" property="isSpecial"/>
<result column="images" property="images"/>
<result column="description" property="description"/>
<result column="seats" property="seats"/>
<result column="transmission" property="transmission"/>
<result column="fuel_type" property="fuelType"/>
<result column="mileage" property="mileage"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="findById" resultMap="CarMap">
select * from cars where id = #{id}
</select>
<select id="findAll" resultMap="CarMap">
select * from cars
<where>
<if test="keyword != null and keyword != ''">
(brand like concat('%', #{keyword}, '%')
or model like concat('%', #{keyword}, '%')
or plate_no like concat('%', #{keyword}, '%'))
</if>
<if test="isSpecial != null">
and is_special = #{isSpecial}
</if>
</where>
order by id desc
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into cars(brand, model, plate_no, price_per_day, deposit, status, is_special, images, description, seats, transmission, fuel_type, mileage, created_at, updated_at)
values (#{brand}, #{model}, #{plateNo}, #{pricePerDay}, #{deposit}, #{status}, #{isSpecial}, #{images}, #{description}, #{seats}, #{transmission}, #{fuelType}, #{mileage}, now(), now())
</insert>
<update id="update">
update cars
set brand = #{brand},
model = #{model},
plate_no = #{plateNo},
price_per_day = #{pricePerDay},
deposit = #{deposit},
status = #{status},
is_special = #{isSpecial},
images = #{images},
description = #{description},
seats = #{seats},
transmission = #{transmission},
fuel_type = #{fuelType},
mileage = #{mileage},
updated_at = now()
where id = #{id}
</update>
<delete id="delete">
delete from cars where id = #{id}
</delete>
</mapper>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.FavoriteMapper">
<resultMap id="FavoriteMap" type="com.car.entity.Favorite">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="car_id" property="carId"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<select id="findByUserAndCar" resultMap="FavoriteMap">
select * from favorites where user_id = #{userId} and car_id = #{carId} limit 1
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into favorites(user_id, car_id, created_at)
values (#{userId}, #{carId}, now())
</insert>
<delete id="delete">
delete from favorites where id = #{id}
</delete>
<select id="findByUserId" resultMap="FavoriteMap">
select * from favorites where user_id = #{userId} order by id desc
</select>
</mapper>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.OrderMapper">
<resultMap id="OrderMap" type="com.car.entity.Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="car_id" property="carId"/>
<result column="start_date" property="startDate"/>
<result column="end_date" property="endDate"/>
<result column="days" property="days"/>
<result column="price_per_day" property="pricePerDay"/>
<result column="deposit" property="deposit"/>
<result column="total_amount" property="totalAmount"/>
<result column="status" property="status"/>
<result column="pay_type" property="payType"/>
<result column="paid_at" property="paidAt"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="findById" resultMap="OrderMap">
select * from orders where id = #{id}
</select>
<select id="findByOrderNo" resultMap="OrderMap">
select * from orders where order_no = #{orderNo}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into orders(order_no, user_id, car_id, start_date, end_date, days,
price_per_day, deposit, total_amount, status, pay_type, paid_at, created_at, updated_at)
values (#{orderNo}, #{userId}, #{carId}, #{startDate}, #{endDate}, #{days},
#{pricePerDay}, #{deposit}, #{totalAmount}, #{status}, #{payType}, #{paidAt}, now(), now())
</insert>
<update id="update">
update orders
set start_date = #{startDate},
end_date = #{endDate},
days = #{days},
price_per_day = #{pricePerDay},
deposit = #{deposit},
total_amount = #{totalAmount},
status = #{status},
pay_type = #{payType},
paid_at = #{paidAt},
updated_at = now()
where id = #{id}
</update>
<select id="findByUserId" resultMap="OrderMap">
select * from orders
<where>
user_id = #{userId}
<if test="status != null and status != ''">
and status = #{status}
</if>
</where>
order by id desc
</select>
<select id="findAll" resultMap="OrderMap">
select * from orders
<where>
<if test="status != null and status != ''">
status = #{status}
</if>
</where>
order by id desc
</select>
<select id="countOverlap" resultType="int">
select count(1) from orders
where car_id = #{carId}
and status in ('PAID', 'RENTING')
and not (end_date &lt; #{startDate} or start_date &gt; #{endDate})
</select>
</mapper>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.PaymentMapper">
<resultMap id="PaymentMap" type="com.car.entity.Payment">
<id column="id" property="id"/>
<result column="order_id" property="orderId"/>
<result column="user_id" property="userId"/>
<result column="amount" property="amount"/>
<result column="type" property="type"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into payments(order_id, user_id, amount, type, created_at)
values (#{orderId}, #{userId}, #{amount}, #{type}, now())
</insert>
<select id="findByUserId" resultMap="PaymentMap">
select * from payments where user_id = #{userId} order by id desc
</select>
</mapper>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.StatsMapper">
<select id="orderStats" resultType="map">
select count(1) as total,
sum(case when status = 'RENTING' then 1 else 0 end) as renting,
sum(case when status = 'RETURNED' then 1 else 0 end) as returned,
sum(case when status = 'PENDING_PAY' then 1 else 0 end) as pendingPay
from orders
</select>
<select id="financeStats" resultType="map">
select ifnull(sum(total_amount), 0) as income,
ifnull(sum(deposit), 0) as deposit,
count(1) as orderCount
from orders
where status in ('PAID', 'RENTING', 'RETURNED')
</select>
</mapper>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.car.mapper.UserMapper">
<resultMap id="UserMap" type="com.car.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="role" property="role"/>
<result column="status" property="status"/>
<result column="balance" property="balance"/>
<result column="real_name_status" property="realNameStatus"/>
<result column="real_name" property="realName"/>
<result column="id_number" property="idNumber"/>
<result column="id_front" property="idFront"/>
<result column="id_back" property="idBack"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="findByUsername" resultMap="UserMap">
select * from users where username = #{username} limit 1
</select>
<select id="findById" resultMap="UserMap">
select * from users where id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into users(username, password, phone, email, role, status, balance, real_name_status, created_at, updated_at)
values (#{username}, #{password}, #{phone}, #{email}, #{role}, #{status}, #{balance}, #{realNameStatus}, now(), now())
</insert>
<update id="update">
update users
set password = #{password},
phone = #{phone},
email = #{email},
role = #{role},
status = #{status},
balance = #{balance},
real_name_status = #{realNameStatus},
real_name = #{realName},
id_number = #{idNumber},
id_front = #{idFront},
id_back = #{idBack},
updated_at = now()
where id = #{id}
</update>
<select id="findAll" resultMap="UserMap">
select * from users
<where>
<if test="keyword != null and keyword != ''">
(username like concat('%', #{keyword}, '%')
or phone like concat('%', #{keyword}, '%')
or email like concat('%', #{keyword}, '%'))
</if>
</where>
order by id desc
</select>
</mapper>