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

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>