不好意思发晚了
```text
shopping-mall-backend/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── mall/
│ │ │ ├── MallApplication.java
│ │ │ ├── common/
│ │ │ │ ├── config/
│ │ │ │ │ ├── MybatisPlusConfig.java
│ │ │ │ │ ├── RedisConfig.java
│ │ │ │ │ ├── SwaggerConfig.java
│ │ │ │ │ └── WebConfig.java
│ │ │ │ ├── exception/
│ │ │ │ │ ├── BusinessException.java
│ │ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ │ └── ResultCode.java
│ │ │ │ ├── interceptor/
│ │ │ │ │ └── JwtInterceptor.java
│ │ │ │ ├── utils/
│ │ │ │ │ ├── JwtUtil.java
│ │ │ │ │ └── RedisUtil.java
│ │ │ │ └── vo/
│ │ │ │ └── Result.java
│ │ │ ├── module/
│ │ │ │ ├── user/
│ │ │ │ │ ├── controller/
│ │ │ │ │ │ └── UserController.java
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ └── User.java
│ │ │ │ │ ├── mapper/
│ │ │ │ │ │ └── UserMapper.java
│ │ │ │ │ ├── service/
│ │ │ │ │ │ ├── UserService.java
│ │ │ │ │ │ └── impl/
│ │ │ │ │ │ └── UserServiceImpl.java
│ │ │ │ │ └── dto/
│ │ │ │ │ ├── LoginDTO.java
│ │ │ │ │ └── UserDTO.java
│ │ │ │ ├── order/
│ │ │ │ │ ├── controller/
│ │ │ │ │ │ └── OrderController.java
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ └── Order.java
│ │ │ │ │ ├── mapper/
│ │ │ │ │ │ └── OrderMapper.java
│ │ │ │ │ ├── service/
│ │ │ │ │ │ ├── OrderService.java
│ │ │ │ │ │ └── impl/
│ │ │ │ │ │ └── OrderServiceImpl.java
│ │ │ │ │ └── dto/
│ │ │ │ │ └── OrderDTO.java
│ │ │ │ └── inventory/
│ │ │ │ ├── controller/
│ │ │ │ │ └── InventoryController.java
│ │ │ │ ├── entity/
│ │ │ │ │ └── Inventory.java
│ │ │ │ ├── mapper/
│ │ │ │ │ └── InventoryMapper.java
│ │ │ │ ├── service/
│ │ │ │ │ ├── InventoryService.java
│ │ │ │ │ └── impl/
│ │ │ │ │ └── InventoryServiceImpl.java
│ │ │ │ └── dto/
│ │ │ │ └── InventoryDTO.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ └── mapper/
│ │ ├── UserMapper.xml
│ │ ├── OrderMapper.xml
│ │ └── InventoryMapper.xml
│ └── test/
│ └── java/
│ └── com/
│ └── mall/
│ └── MallApplicationTests.java
└── .gitignore
```
核心代码文件
- pom.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<groupId>com.mall</groupId>
<artifactId>shopping-mall-backend</artifactId>
<version>1.0.0</version>
<name>shopping-mall-backend</name>
<description>Shopping Mall Backend with Spring Boot 3</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<jwt.version>0.11.5</jwt.version>
<swagger.version>2.4.0</swagger.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- Utils -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
```
- application.yml
```yaml
spring:
profiles:
active: dev
application:
name: shopping-mall-backend
server:
port: 8080
servlet:
context-path: /api
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secret: your-256-bit-secret-key-for-jwt-signing
expiration: 86400000 # 24 hours in milliseconds
swagger:
enabled: true
title: Shopping Mall API
description: Shopping Mall Backend API Documentation
version: 1.0.0
```
- application-dev.yml
```yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mall_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
data:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
logging:
level:
com.mall: debug
```
- MallApplication.java
```java
package com.mall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MallApplication {
public static void main(String\[\] args) {
SpringApplication.run(MallApplication.class, args);
}
}
```
- common/config/MybatisPlusConfig.java
```java
package com.mall.common.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;
}
}
```
- common/config/RedisConfig.java
```java
package com.mall.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key serializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value serializer
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
```
- common/config/SwaggerConfig.java
```java
package com.mall.common.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Value("${swagger.enabled:true}")
private boolean enabled;
@Value("${swagger.title:Shopping Mall API}")
private String title;
@Value("${swagger.description:Shopping Mall Backend API}")
private String description;
@Value("${swagger.version:1.0.0}")
private String version;
@Bean
public OpenAPI customOpenAPI() {
final String securitySchemeName = "BearerAuth";
return new OpenAPI()
.info(new Info()
.title(title)
.description(description)
.version(version)
.contact(new Contact()
.name("Mall Team")
.email("support@mall.com")))
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.schemaRequirement(securitySchemeName, new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));
}
}
```
- common/config/WebConfig.java
```java
package com.mall.common.config;
import com.mall.common.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/user/login",
"/user/register",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html"
);
}
}
```
- common/utils/JwtUtil.java
```java
package com.mall.common.utils;
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.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
}
```
- common/interceptor/JwtInterceptor.java
```java
package com.mall.common.interceptor;
import com.mall.common.utils.JwtUtil;
import com.mall.common.exception.BusinessException;
import com.mall.common.exception.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// OPTIONS requests pass through
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new BusinessException(ResultCode.UNAUTHORIZED, "Token is required");
}
// Remove "Bearer " prefix if present
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
if (!jwtUtil.validateToken(token)) {
throw new BusinessException(ResultCode.UNAUTHORIZED, "Invalid token");
}
// Set user info in request attributes
request.setAttribute("userId", jwtUtil.getUserIdFromToken(token));
request.setAttribute("username", jwtUtil.getUsernameFromToken(token));
return true;
}
}
```
- common/vo/Result.java
```java
package com.mall.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
return new Result<>(200, "Success", data, System.currentTimeMillis());
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(200, message, data, System.currentTimeMillis());
}
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null, System.currentTimeMillis());
}
}
```
- common/exception/ResultCode.java
```java
package com.mall.common.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResultCode {
SUCCESS(200, "Success"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
CONFLICT(409, "Conflict"),
INTERNAL_ERROR(500, "Internal Server Error"),
// Business Errors
USER_NOT_FOUND(1001, "User not found"),
USER_EXISTS(1002, "User already exists"),
INVALID_PASSWORD(1003, "Invalid password"),
INSUFFICIENT_STOCK(2001, "Insufficient stock"),
ORDER_NOT_FOUND(2002, "Order not found"),
INVENTORY_NOT_FOUND(3001, "Inventory not found");
private final Integer code;
private final String message;
}
```
- common/exception/BusinessException.java
```java
package com.mall.common.exception;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
private final String message;
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
this.message = message;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
```
- common/exception/GlobalExceptionHandler.java
```java
package com.mall.common.exception;
import com.mall.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.error("Business exception: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.error("Validation exception: {}", message);
return Result.error(ResultCode.BAD_REQUEST.getCode(), message);
}
@ExceptionHandler(BindException.class)
public Result<?> handleBindException(BindException e) {
String message = e.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.error("Bind exception: {}", message);
return Result.error(ResultCode.BAD_REQUEST.getCode(), message);
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("Internal error: ", e);
return Result.error(ResultCode.INTERNAL_ERROR.getCode(), "System error, please try again later");
}
}
```
- module/user/entity/User.java
```java
package com.mall.module.user.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String email;
private String phone;
private String fullName;
private Integer status; // 0: disabled, 1: enabled
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
```
- module/user/dto/LoginDTO.java
```java
package com.mall.module.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "Login Request")
public class LoginDTO {
@NotBlank(message = "Username is required")
@Schema(description = "Username", required = true)
private String username;
@NotBlank(message = "Password is required")
@Schema(description = "Password", required = true)
private String password;
}
```
- module/user/dto/UserDTO.java
```java
package com.mall.module.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "User Request")
public class UserDTO {
@NotBlank(message = "Username is required")
@Schema(description = "Username", required = true)
private String username;
@NotBlank(message = "Password is required")
@Schema(description = "Password", required = true)
private String password;
@Email(message = "Invalid email format")
@Schema(description = "Email")
private String email;
@Schema(description = "Phone number")
private String phone;
@Schema(description = "Full name")
private String fullName;
}
```
- module/user/mapper/UserMapper.java
```java
package com.mall.module.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mall.module.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE username = #{username}")
User findByUsername(String username);
}
```
- module/user/service/UserService.java
```java
package com.mall.module.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mall.module.user.entity.User;
import com.mall.module.user.dto.LoginDTO;
import com.mall.module.user.dto.UserDTO;
import java.util.Map;
public interface UserService extends IService<User> {
Map<String, String> login(LoginDTO loginDTO);
boolean register(UserDTO userDTO);
User getByUsername(String username);
User getById(Long id);
}
```
- module/user/service/impl/UserServiceImpl.java
```java
package com.mall.module.user.service.impl;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mall.common.exception.BusinessException;
import com.mall.common.exception.ResultCode;
import com.mall.common.utils.JwtUtil;
import com.mall.module.user.dto.LoginDTO;
import com.mall.module.user.dto.UserDTO;
import com.mall.module.user.entity.User;
import com.mall.module.user.mapper.UserMapper;
import com.mall.module.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private JwtUtil jwtUtil;
@Override
public Map<String, String> login(LoginDTO loginDTO) {
User user = baseMapper.findByUsername(loginDTO.getUsername());
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
if (!BCrypt.checkpw(loginDTO.getPassword(), user.getPassword())) {
throw new BusinessException(ResultCode.INVALID_PASSWORD);
}
if (user.getStatus() == 0) {
throw new BusinessException(ResultCode.FORBIDDEN, "User account is disabled");
}
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
Map<String, String> result = new HashMap<>();
result.put("token", token);
result.put("username", user.getUsername());
return result;
}
@Override
public boolean register(UserDTO userDTO) {
User existing = baseMapper.findByUsername(userDTO.getUsername());
if (existing != null) {
throw new BusinessException(ResultCode.USER_EXISTS);
}
User user = User.builder()
.username(userDTO.getUsername())
.password(BCrypt.hashpw(userDTO.getPassword()))
.email(userDTO.getEmail())
.phone(userDTO.getPhone())
.fullName(userDTO.getFullName())
.status(1)
.build();
return save(user);
}
@Override
public User getByUsername(String username) {
return baseMapper.findByUsername(username);
}
@Override
public User getById(Long id) {
User user = super.getById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
return user;
}
}
```
- module/user/controller/UserController.java
```java
package com.mall.module.user.controller;
import com.mall.common.vo.Result;
import com.mall.module.user.dto.LoginDTO;
import com.mall.module.user.dto.UserDTO;
import com.mall.module.user.entity.User;
import com.mall.module.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Tag(name = "User Management", description = "User registration, login and management APIs")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
@Operation(summary = "User login", description = "Authenticate user and return JWT token")
public Result<Map<String, String>> login(@Valid @RequestBody LoginDTO loginDTO) {
return Result.success(userService.login(loginDTO));
}
@PostMapping("/register")
@Operation(summary = "User registration", description = "Register a new user")
public Result<Boolean> register(@Valid @RequestBody UserDTO userDTO) {
return Result.success("Registration successful", userService.register(userDTO));
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID", description = "Retrieve user information by user ID")
public Result<User> getById(@Parameter(description = "User ID") @PathVariable Long id) {
return Result.success(userService.getById(id));
}
@GetMapping("/profile")
@Operation(summary = "Get current user profile", description = "Retrieve current authenticated user's profile")
public Result<User> getProfile(@RequestAttribute("userId") Long userId) {
return Result.success(userService.getById(userId));
}
}
```
- module/order/entity/Order.java
```java
package com.mall.module.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("`order`")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private Long productId;
private Integer quantity;
private BigDecimal amount;
private Integer status; // 0: pending, 1: paid, 2: shipped, 3: completed, 4: cancelled
private String address;
private String remark;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
```
- module/order/dto/OrderDTO.java
```java
package com.mall.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "Order Request")
public class OrderDTO {
@NotNull(message = "Product ID is required")
@Schema(description = "Product ID", required = true)
private Long productId;
@NotNull(message = "Quantity is required")
@Min(value = 1, message = "Quantity must be at least 1")
@Schema(description = "Quantity", required = true, minimum = "1")
private Integer quantity;
@Schema(description = "Shipping address")
private String address;
@Schema(description = "Order remark")
private String remark;
}
```
- module/order/mapper/OrderMapper.java
```java
package com.mall.module.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mall.module.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
@Select("SELECT * FROM `order` WHERE user_id = #{userId} ORDER BY created_at DESC")
List<Order> findByUserId(Long userId);
@Select("SELECT * FROM `order` WHERE order_no = #{orderNo}")
Order findByOrderNo(String orderNo);
}
```
- module/order/service/OrderService.java
```java
package com.mall.module.order.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mall.module.order.entity.Order;
import com.mall.module.order.dto.OrderDTO;
import java.util.List;
public interface OrderService extends IService<Order> {
Order createOrder(Long userId, OrderDTO orderDTO);
Order getByOrderNo(String orderNo);
List<Order> getByUserId(Long userId);
boolean cancelOrder(Long orderId, Long userId);
boolean updateOrderStatus(Long orderId, Integer status);
}
```
- module/order/service/impl/OrderServiceImpl.java
```java
package com.mall.module.order.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mall.common.exception.BusinessException;
import com.mall.common.exception.ResultCode;
import com.mall.module.inventory.entity.Inventory;
import com.mall.module.inventory.service.InventoryService;
import com.mall.module.order.dto.OrderDTO;
import com.mall.module.order.entity.Order;
import com.mall.module.order.mapper.OrderMapper;
import com.mall.module.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private InventoryService inventoryService;
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, OrderDTO orderDTO) {
// Check inventory
Inventory inventory = inventoryService.getByProductId(orderDTO.getProductId());
if (inventory == null || inventory.getStock() < orderDTO.getQuantity()) {
throw new BusinessException(ResultCode.INSUFFICIENT_STOCK);
}
// Deduct stock
inventoryService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
// Create order
Order order = Order.builder()
.orderNo(IdUtil.getSnowflakeNextIdStr())
.userId(userId)
.productId(orderDTO.getProductId())
.quantity(orderDTO.getQuantity())
.amount(BigDecimal.valueOf(inventory.getPrice() * orderDTO.getQuantity()))
.address(orderDTO.getAddress())
.remark(orderDTO.getRemark())
.status(0) // pending
.build();
save(order);
return order;
}
@Override
public Order getByOrderNo(String orderNo) {
Order order = baseMapper.findByOrderNo(orderNo);
if (order == null) {
throw new BusinessException(ResultCode.ORDER_NOT_FOUND);
}
return order;
}
@Override
public List<Order> getByUserId(Long userId) {
return baseMapper.findByUserId(userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(Long orderId, Long userId) {
Order order = getById(orderId);
if (order == null) {
throw new BusinessException(ResultCode.ORDER_NOT_FOUND);
}
if (!order.getUserId().equals(userId)) {
throw new BusinessException(ResultCode.FORBIDDEN, "You are not authorized to cancel this order");
}
if (order.getStatus() >= 1) {
throw new BusinessException(ResultCode.CONFLICT, "Order cannot be cancelled in its current status");
}
// Restore stock
inventoryService.addStock(order.getProductId(), order.getQuantity());
order.setStatus(4); // cancelled
return updateById(order);
}
@Override
public boolean updateOrderStatus(Long orderId, Integer status) {
Order order = getById(orderId);
if (order == null) {
throw new BusinessException(ResultCode.ORDER_NOT_FOUND);
}
order.setStatus(status);
return updateById(order);
}
}
```
- module/order/controller/OrderController.java
```java
package com.mall.module.order.controller;
import com.mall.common.vo.Result;
import com.mall.module.order.dto.OrderDTO;
import com.mall.module.order.entity.Order;
import com.mall.module.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/order")
@Tag(name = "Order Management", description = "Order creation, query and management APIs")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
@Operation(summary = "Create order", description = "Create a new order and deduct inventory")
public Result<Order> createOrder(
@RequestAttribute("userId") Long userId,
@Valid @RequestBody OrderDTO orderDTO) {
return Result.success("Order created successfully", orderService.createOrder(userId, orderDTO));
}
@GetMapping("/my")
@Operation(summary = "Get user orders", description = "Retrieve all orders for current user")
public Result<List<Order>> getMyOrders(@RequestAttribute("userId") Long userId) {
return Result.success(orderService.getByUserId(userId));
}
@GetMapping("/{id}")
@Operation(summary = "Get order by ID", description = "Retrieve order details by order ID")
public Result<Order> getById(@Parameter(description = "Order ID") @PathVariable Long id) {
return Result.success(orderService.getById(id));
}
@GetMapping("/order-no/{orderNo}")
@Operation(summary = "Get order by order number", description = "Retrieve order details by order number")
public Result<Order> getByOrderNo(@Parameter(description = "Order Number") @PathVariable String orderNo) {
return Result.success(orderService.getByOrderNo(orderNo));
}
@PutMapping("/cancel/{id}")
@Operation(summary = "Cancel order", description = "Cancel an order and restore inventory")
public Result<Boolean> cancelOrder(
@Parameter(description = "Order ID") @PathVariable Long id,
@RequestAttribute("userId") Long userId) {
return Result.success("Order cancelled successfully", orderService.cancelOrder(id, userId));
}
}
```
- module/inventory/entity/Inventory.java
```java
package com.mall.module.inventory.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("inventory")
public class Inventory {
@TableId(type = IdType.AUTO)
private Long id;
private Long productId;
private String productName;
private Integer stock;
private Double price;
private Integer reservedStock;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
```
- module/inventory/dto/InventoryDTO.java
```java
package com.mall.module.inventory.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "Inventory Request")
public class InventoryDTO {
@NotNull(message = "Product ID is required")
@Schema(description = "Product ID", required = true)
private Long productId;
@NotBlank(message = "Product name is required")
@Schema(description = "Product name", required = true)
private String productName;
@NotNull(message = "Stock is required")
@Min(value = 0, message = "Stock must be at least 0")
@Schema(description = "Stock quantity", required = true, minimum = "0")
private Integer stock;
@NotNull(message = "Price is required")
@Min(value = 0, message = "Price must be at least 0")
@Schema(description = "Product price", required = true, minimum = "0")
private Double price;
}
```
- module/inventory/mapper/InventoryMapper.java
```java
package com.mall.module.inventory.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mall.module.inventory.entity.Inventory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface InventoryMapper extends BaseMapper<Inventory> {
@Select("SELECT * FROM inventory WHERE product_id = #{productId}")
Inventory findByProductId(Long productId);
@Update("UPDATE inventory SET stock = stock - #{quantity}, reserved_stock = reserved_stock + #{quantity} " +
"WHERE product_id = #{productId} AND stock >= #{quantity}")
int deductStock(Long productId, Integer quantity);
@Update("UPDATE inventory SET stock = stock + #{quantity}, reserved_stock = reserved_stock - #{quantity} " +
"WHERE product_id = #{productId}")
int addStock(Long productId, Integer quantity);
}
```
- module/inventory/service/InventoryService.java
```java
package com.mall.module.inventory.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mall.module.inventory.entity.Inventory;
import com.mall.module.inventory.dto.InventoryDTO;
public interface InventoryService extends IService<Inventory> {
Inventory getByProductId(Long productId);
boolean deductStock(Long productId, Integer quantity);
boolean addStock(Long productId, Integer quantity);
Inventory createInventory(InventoryDTO inventoryDTO);
boolean updateInventory(Long id, InventoryDTO inventoryDTO);
}
```
- module/inventory/service/impl/InventoryServiceImpl.java
```java
package com.mall.module.inventory.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mall.common.exception.BusinessException;
import com.mall.common.exception.ResultCode;
import com.mall.module.inventory.dto.InventoryDTO;
import com.mall.module.inventory.entity.Inventory;
import com.mall.module.inventory.mapper.InventoryMapper;
import com.mall.module.inventory.service.InventoryService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory> implements InventoryService {
@Override
@Cacheable(value = "inventory", key = "#productId")
public Inventory getByProductId(Long productId) {
Inventory inventory = baseMapper.findByProductId(productId);
if (inventory == null) {
throw new BusinessException(ResultCode.INVENTORY_NOT_FOUND);
}
return inventory;
}
@Override
public boolean deductStock(Long productId, Integer quantity) {
int result = baseMapper.deductStock(productId, quantity);
if (result <= 0) {
throw new BusinessException(ResultCode.INSUFFICIENT_STOCK);
}
return true;
}
@Override
public boolean addStock(Long productId, Integer quantity) {
int result = baseMapper.addStock(productId, quantity);
if (result <= 0) {
throw new BusinessException(ResultCode.INVENTORY_NOT_FOUND);
}
return true;
}
@Override
public Inventory createInventory(InventoryDTO inventoryDTO) {
Inventory existing = baseMapper.findByProductId(inventoryDTO.getProductId());
if (existing != null) {
throw new BusinessException(ResultCode.CONFLICT, "Inventory already exists for this product");
}
Inventory inventory = Inventory.builder()
.productId(inventoryDTO.getProductId())
.productName(inventoryDTO.getProductName())
.stock(inventoryDTO.getStock())
.price(inventoryDTO.getPrice())
.reservedStock(0)
.build();
save(inventory);
return inventory;
}
@Override
@CachePut(value = "inventory", key = "#result.productId")
public boolean updateInventory(Long id, InventoryDTO inventoryDTO) {
Inventory inventory = getById(id);
if (inventory == null) {
throw new BusinessException(ResultCode.INVENTORY_NOT_FOUND);
}
inventory.setProductName(inventoryDTO.getProductName());
inventory.setStock(inventoryDTO.getStock());
inventory.setPrice(inventoryDTO.getPrice());
return updateById(inventory);
}
}
```
- module/inventory/controller/InventoryController.java
```java
package com.mall.module.inventory.controller;
import com.mall.common.vo.Result;
import com.mall.module.inventory.dto.InventoryDTO;
import com.mall.module.inventory.entity.Inventory;
import com.mall.module.inventory.service.InventoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/inventory")
@Tag(name = "Inventory Management", description = "Inventory query and management APIs")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@GetMapping("/product/{productId}")
@Operation(summary = "Get inventory by product ID", description = "Retrieve inventory information by product ID")
public Result<Inventory> getByProductId(@Parameter(description = "Product ID") @PathVariable Long productId) {
return Result.success(inventoryService.getByProductId(productId));
}
@GetMapping("/{id}")
@Operation(summary = "Get inventory by ID", description = "Retrieve inventory information by inventory ID")
public Result<Inventory> getById(@Parameter(description = "Inventory ID") @PathVariable Long id) {
return Result.success(inventoryService.getById(id));
}
@PostMapping
@Operation(summary = "Create inventory", description = "Create new inventory record for a product")
public Result<Inventory> createInventory(@Valid @RequestBody InventoryDTO inventoryDTO) {
return Result.success("Inventory created successfully", inventoryService.createInventory(inventoryDTO));
}
@PutMapping("/{id}")
@Operation(summary = "Update inventory", description = "Update existing inventory information")
public Result<Boolean> updateInventory(
@Parameter(description = "Inventory ID") @PathVariable Long id,
@Valid @RequestBody InventoryDTO inventoryDTO) {
return Result.success("Inventory updated successfully", inventoryService.updateInventory(id, inventoryDTO));
}
@PostMapping("/deduct")
@Operation(summary = "Deduct stock", description = "Deduct stock from inventory")
public Result<Boolean> deductStock(
@Parameter(description = "Product ID") @RequestParam Long productId,
@Parameter(description = "Quantity") @RequestParam Integer quantity) {
return Result.success("Stock deducted successfully", inventoryService.deductStock(productId, quantity));
}
@PostMapping("/add")
@Operation(summary = "Add stock", description = "Add stock to inventory")
public Result<Boolean> addStock(
@Parameter(description = "Product ID") @RequestParam Long productId,
@Parameter(description = "Quantity") @RequestParam Integer quantity) {
return Result.success("Stock added successfully", inventoryService.addStock(productId, quantity));
}
}
```
数据库初始化SQL
```sql
-- Create database
CREATE DATABASE IF NOT EXISTS mall_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mall_db;
-- User table
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT 'Username',
`password` VARCHAR(255) NOT NULL COMMENT 'Password (BCrypt)',
`email` VARCHAR(100) COMMENT 'Email',
`phone` VARCHAR(20) COMMENT 'Phone number',
`full_name` VARCHAR(100) COMMENT 'Full name',
`status` TINYINT DEFAULT 1 COMMENT '0: disabled, 1: enabled',
`deleted` TINYINT DEFAULT 0 COMMENT 'Logic delete flag',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX idx_username (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User table';
-- Order table
CREATE TABLE IF NOT EXISTS `order` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT 'Order number',
`user_id` BIGINT NOT NULL COMMENT 'User ID',
`product_id` BIGINT NOT NULL COMMENT 'Product ID',
`quantity` INT NOT NULL COMMENT 'Quantity',
`amount` DECIMAL(10,2) NOT NULL COMMENT 'Total amount',
`status` TINYINT DEFAULT 0 COMMENT '0: pending, 1: paid, 2: shipped, 3: completed, 4: cancelled',
`address` VARCHAR(255) COMMENT 'Shipping address',
`remark` VARCHAR(255) COMMENT 'Order remark',
`deleted` TINYINT DEFAULT 0 COMMENT 'Logic delete flag',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX idx_user_id (`user_id`),
INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Order table';
-- Inventory table
CREATE TABLE IF NOT EXISTS `inventory` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_id` BIGINT NOT NULL UNIQUE COMMENT 'Product ID',
`product_name` VARCHAR(100) NOT NULL COMMENT 'Product name',
`stock` INT NOT NULL DEFAULT 0 COMMENT 'Stock quantity',
`price` DECIMAL(10,2) NOT NULL COMMENT 'Product price',
`reserved_stock` INT NOT NULL DEFAULT 0 COMMENT 'Reserved stock',
`deleted` TINYINT DEFAULT 0 COMMENT 'Logic delete flag',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX idx_product_id (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Inventory table';
```
说明
-
技术栈: Spring Boot 3.1.5, Java 17, MyBatis-Plus 3.5.3.1
-
JWT鉴权: 使用jjwt 0.11.5,拦截器验证Token
-
Redis缓存: 集成Lettuce连接池,用于缓存Inventory查询
-
Swagger文档: 使用springdoc-openapi 2.4.0,自动生成API文档
-
异常处理: 全局异常处理,统一返回格式
-
密码加密: BCrypt加密存储
-
事务管理: 订单创建时包含库存扣减的事务操作
API文档访问