first commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||||
708
ARCHITECTURE.md
Normal file
708
ARCHITECTURE.md
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
# Lane Carford 基础架构功能说明
|
||||||
|
|
||||||
|
## 🏗️ 架构概述
|
||||||
|
|
||||||
|
这是一个基于 **Spring Boot 3.1.6** 和 **Java 21** 的纯净架构模板,提供了完整的企业级应用基础设施,可以快速开发各种类型的业务应用。
|
||||||
|
|
||||||
|
## 📋 核心功能清单
|
||||||
|
|
||||||
|
### 1. 🛡️ 全局异常处理
|
||||||
|
**功能描述**: 统一处理应用程序中的各种异常,提供一致的错误响应格式
|
||||||
|
|
||||||
|
**实现类**: `GlobalExceptionHandler`
|
||||||
|
|
||||||
|
**支持的异常类型**:
|
||||||
|
- 业务异常 (`BusinessException`)
|
||||||
|
- 参数验证异常 (`MethodArgumentNotValidException`)
|
||||||
|
- 文件上传大小超限异常 (`MaxUploadSizeExceededException`)
|
||||||
|
- 数据库操作异常 (`DataAccessException`, `SQLException`)
|
||||||
|
- HTTP方法不支持异常 (`HttpRequestMethodNotSupportedException`)
|
||||||
|
- 404异常 (`NoHandlerFoundException`)
|
||||||
|
- 运行时异常 (`RuntimeException`)
|
||||||
|
- 其他通用异常
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 1. 创建自定义业务异常
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<User> createUser(@Valid @RequestBody CreateUserRequest request) {
|
||||||
|
// 业务逻辑验证
|
||||||
|
if (userService.existsByEmail(request.getEmail())) {
|
||||||
|
throw new BusinessException("USER_EXISTS", "用户邮箱已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userService.createUser(request);
|
||||||
|
return ApiResponse.success("用户创建成功", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<User> getUser(@PathVariable Long id) {
|
||||||
|
User user = userService.findById(id);
|
||||||
|
if (user == null) {
|
||||||
|
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
|
||||||
|
}
|
||||||
|
return ApiResponse.success(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 参数验证异常自动处理
|
||||||
|
public class CreateUserRequest {
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "邮箱不能为空")
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotNull(message = "年龄不能为空")
|
||||||
|
@Min(value = 18, message = "年龄不能小于18岁")
|
||||||
|
@Max(value = 100, message = "年龄不能大于100岁")
|
||||||
|
private Integer age;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异常响应格式示例:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "参数验证失败",
|
||||||
|
"timestamp": 1640995200000,
|
||||||
|
"path": "/api/users",
|
||||||
|
"errors": {
|
||||||
|
"username": "用户名不能为空",
|
||||||
|
"email": "邮箱格式不正确"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 📤 统一API响应格式
|
||||||
|
**功能描述**: 提供标准化的API响应格式,确保前后端接口的一致性
|
||||||
|
|
||||||
|
**实现类**: `ApiResponse<T>`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/products")
|
||||||
|
public class ProductController {
|
||||||
|
|
||||||
|
// 成功响应 - 返回数据
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<Product> getProduct(@PathVariable Long id) {
|
||||||
|
Product product = productService.findById(id);
|
||||||
|
return ApiResponse.success("查询成功", product);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应 - 无数据
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> deleteProduct(@PathVariable Long id) {
|
||||||
|
productService.deleteById(id);
|
||||||
|
return ApiResponse.success("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询响应
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<Product>> getProducts(
|
||||||
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size) {
|
||||||
|
PageResult<Product> result = productService.findByPage(page, size);
|
||||||
|
return ApiResponse.success("查询成功", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Product> createProduct(@Valid @RequestBody CreateProductRequest request) {
|
||||||
|
if (productService.existsByName(request.getName())) {
|
||||||
|
return ApiResponse.businessError("PRODUCT_EXISTS", "产品名称已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Product product = productService.create(request);
|
||||||
|
return ApiResponse.success("创建成功", product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应格式示例:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"code": "SUCCESS",
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "产品名称",
|
||||||
|
"price": 99.99
|
||||||
|
},
|
||||||
|
"timestamp": 1640995200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 📊 分页查询支持
|
||||||
|
**功能描述**: 提供标准化的分页查询结果封装
|
||||||
|
|
||||||
|
**实现类**: `PageResult<T>`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
public PageResult<Product> findByPage(int page, int size) {
|
||||||
|
// 使用MyBatis-Plus分页
|
||||||
|
Page<Product> pageParam = new Page<>(page, size);
|
||||||
|
Page<Product> result = productMapper.selectPage(pageParam, null);
|
||||||
|
|
||||||
|
return PageResult.<Product>builder()
|
||||||
|
.records(result.getRecords())
|
||||||
|
.total(result.getTotal())
|
||||||
|
.current(result.getCurrent())
|
||||||
|
.size(result.getSize())
|
||||||
|
.pages(result.getPages())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带条件的分页查询
|
||||||
|
public PageResult<Product> findByCondition(ProductQueryRequest request) {
|
||||||
|
Page<Product> pageParam = new Page<>(request.getPage(), request.getSize());
|
||||||
|
|
||||||
|
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
|
||||||
|
if (StringUtils.hasText(request.getName())) {
|
||||||
|
queryWrapper.like("name", request.getName());
|
||||||
|
}
|
||||||
|
if (request.getMinPrice() != null) {
|
||||||
|
queryWrapper.ge("price", request.getMinPrice());
|
||||||
|
}
|
||||||
|
if (request.getMaxPrice() != null) {
|
||||||
|
queryWrapper.le("price", request.getMaxPrice());
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<Product> result = productMapper.selectPage(pageParam, queryWrapper);
|
||||||
|
return PageResult.fromPage(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 🔄 Bean转换工具
|
||||||
|
**功能描述**: 提供对象之间的转换功能,简化DTO和Entity之间的转换
|
||||||
|
|
||||||
|
**实现类**: `BeanUtil`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
// 单个对象转换
|
||||||
|
public UserDTO convertToDTO(User user) {
|
||||||
|
return BeanUtil.convert(user, UserDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表转换
|
||||||
|
public List<UserDTO> convertToDTOList(List<User> users) {
|
||||||
|
return BeanUtil.convertList(users, UserDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Supplier转换(适用于复杂对象)
|
||||||
|
public UserDetailDTO convertToDetailDTO(User user) {
|
||||||
|
return BeanUtil.convert(user, () -> {
|
||||||
|
UserDetailDTO dto = new UserDetailDTO();
|
||||||
|
// 可以在这里设置默认值
|
||||||
|
dto.setStatus("ACTIVE");
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 忽略null值的属性复制
|
||||||
|
public void updateUser(Long id, UpdateUserRequest request) {
|
||||||
|
User existingUser = findById(id);
|
||||||
|
// 只复制非null的属性,避免覆盖现有数据
|
||||||
|
BeanUtil.copyPropertiesIgnoreNull(request, existingUser);
|
||||||
|
userMapper.updateById(existingUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实体类示例
|
||||||
|
@Data
|
||||||
|
@TableName("users")
|
||||||
|
public class User extends BaseEntity {
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String phone;
|
||||||
|
private Integer age;
|
||||||
|
private String avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO示例
|
||||||
|
@Data
|
||||||
|
public class UserDTO {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String phone;
|
||||||
|
private Integer age;
|
||||||
|
private String avatar;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 📝 日志切面
|
||||||
|
**功能描述**: 自动记录Controller方法的调用日志,包括请求参数和响应结果
|
||||||
|
|
||||||
|
**实现类**: `LoggingAspect`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 无需额外配置,自动对所有Controller方法生效
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/orders")
|
||||||
|
public class OrderController {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Order> createOrder(@RequestBody CreateOrderRequest request) {
|
||||||
|
// 自动记录日志:
|
||||||
|
// [INFO] 开始执行方法: OrderController.createOrder
|
||||||
|
// [INFO] 请求参数: {"productId":1,"quantity":2,"address":"北京市朝阳区"}
|
||||||
|
|
||||||
|
Order order = orderService.createOrder(request);
|
||||||
|
|
||||||
|
// [INFO] 方法执行完成: OrderController.createOrder, 耗时: 150ms
|
||||||
|
// [INFO] 响应结果: {"success":true,"data":{"id":1,"orderNo":"ORD20240101001"}}
|
||||||
|
|
||||||
|
return ApiResponse.success("订单创建成功", order);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<Order> getOrder(@PathVariable Long id) {
|
||||||
|
// 自动记录日志,包括路径参数
|
||||||
|
Order order = orderService.findById(id);
|
||||||
|
return ApiResponse.success(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志输出示例:
|
||||||
|
// 2024-01-01 10:30:15.123 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 开始执行方法: OrderController.createOrder
|
||||||
|
// 2024-01-01 10:30:15.124 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 请求参数: [{"productId":1,"quantity":2}]
|
||||||
|
// 2024-01-01 10:30:15.275 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 方法执行完成: OrderController.createOrder, 耗时: 151ms
|
||||||
|
// 2024-01-01 10:30:15.276 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 响应结果: {"success":true,"code":"SUCCESS"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. ⚡ 性能监控切面
|
||||||
|
**功能描述**: 监控Controller、Service、Mapper层方法的执行性能,超过阈值时发出警告
|
||||||
|
|
||||||
|
**实现类**: `PerformanceAspect`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 无需额外配置,自动监控所有层的方法性能
|
||||||
|
@Service
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
public List<Product> findExpensiveProducts() {
|
||||||
|
// 如果这个方法执行超过5秒,会自动记录警告日志
|
||||||
|
return productMapper.selectExpensiveProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void batchUpdateProducts(List<Product> products) {
|
||||||
|
// 批量操作可能耗时较长,会被监控
|
||||||
|
for (Product product : products) {
|
||||||
|
productMapper.updateById(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ProductMapper extends BaseMapper<Product> {
|
||||||
|
|
||||||
|
// 复杂查询可能耗时较长,会被监控
|
||||||
|
@Select("SELECT * FROM products WHERE price > 1000 ORDER BY price DESC")
|
||||||
|
List<Product> selectExpensiveProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能监控日志示例:
|
||||||
|
// 2024-01-01 10:30:15.123 [http-nio-8080-exec-1] WARN c.a.l.aspect.PerformanceAspect - [性能警告] Service层方法执行缓慢: ProductService.findExpensiveProducts, 耗时: 6543ms (阈值: 5000ms)
|
||||||
|
// 2024-01-01 10:30:15.124 [http-nio-8080-exec-1] INFO c.a.l.aspect.PerformanceAspect - [性能监控] Mapper层方法执行: ProductMapper.selectExpensiveProducts, 耗时: 234ms
|
||||||
|
|
||||||
|
// 配置性能阈值 (application.properties):
|
||||||
|
app.performance.warning.controller=5000 # Controller层警告阈值5秒
|
||||||
|
app.performance.warning.service=3000 # Service层警告阈值3秒
|
||||||
|
app.performance.warning.mapper=500 # Mapper层警告阈值500毫秒
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 🗄️ MyBatis-Plus集成
|
||||||
|
**功能描述**: 提供强大的ORM功能,包括基础CRUD、分页查询、条件构造器等
|
||||||
|
|
||||||
|
**实现类**: `BaseMapper<T>`, `BaseEntity`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 1. 实体类继承BaseEntity
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("products")
|
||||||
|
public class Product extends BaseEntity {
|
||||||
|
|
||||||
|
@TableField("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@TableField("description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@TableField("price")
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
@TableField("category_id")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
@TableField("stock")
|
||||||
|
private Integer stock;
|
||||||
|
|
||||||
|
@TableField("status")
|
||||||
|
private Integer status; // 0-下架, 1-上架
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Mapper接口继承BaseMapper
|
||||||
|
@Repository
|
||||||
|
public interface ProductMapper extends BaseMapper<Product> {
|
||||||
|
|
||||||
|
// 自定义查询方法
|
||||||
|
@Select("SELECT * FROM products WHERE category_id = #{categoryId} AND status = 1")
|
||||||
|
List<Product> findByCategoryAndActive(@Param("categoryId") Long categoryId);
|
||||||
|
|
||||||
|
// 复杂查询
|
||||||
|
@Select("SELECT p.*, c.name as category_name FROM products p " +
|
||||||
|
"LEFT JOIN categories c ON p.category_id = c.id " +
|
||||||
|
"WHERE p.price BETWEEN #{minPrice} AND #{maxPrice}")
|
||||||
|
List<ProductVO> findByPriceRange(@Param("minPrice") BigDecimal minPrice,
|
||||||
|
@Param("maxPrice") BigDecimal maxPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Service层使用示例
|
||||||
|
@Service
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
// 基础CRUD操作
|
||||||
|
public Product save(Product product) {
|
||||||
|
productMapper.insert(product); // 自动填充创建时间
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product findById(Long id) {
|
||||||
|
return productMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteById(Long id) {
|
||||||
|
productMapper.deleteById(id); // 逻辑删除
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product update(Product product) {
|
||||||
|
productMapper.updateById(product); // 自动更新修改时间
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件查询
|
||||||
|
public List<Product> findByCondition(String name, BigDecimal minPrice, Integer status) {
|
||||||
|
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
|
||||||
|
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
queryWrapper.like("name", name);
|
||||||
|
}
|
||||||
|
if (minPrice != null) {
|
||||||
|
queryWrapper.ge("price", minPrice);
|
||||||
|
}
|
||||||
|
if (status != null) {
|
||||||
|
queryWrapper.eq("status", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return productMapper.selectList(queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
public PageResult<Product> findByPage(int page, int size, String keyword) {
|
||||||
|
Page<Product> pageParam = new Page<>(page, size);
|
||||||
|
|
||||||
|
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
|
||||||
|
if (StringUtils.hasText(keyword)) {
|
||||||
|
queryWrapper.and(wrapper -> wrapper
|
||||||
|
.like("name", keyword)
|
||||||
|
.or()
|
||||||
|
.like("description", keyword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
queryWrapper.eq("status", 1); // 只查询上架商品
|
||||||
|
queryWrapper.orderByDesc("created_time");
|
||||||
|
|
||||||
|
Page<Product> result = productMapper.selectPage(pageParam, queryWrapper);
|
||||||
|
return PageResult.fromPage(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作
|
||||||
|
public void batchInsert(List<Product> products) {
|
||||||
|
// MyBatis-Plus会自动优化批量插入
|
||||||
|
for (Product product : products) {
|
||||||
|
productMapper.insert(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 9. 🔒 Spring Security集成
|
||||||
|
**功能描述**: 提供基础的安全配置,支持认证和授权
|
||||||
|
|
||||||
|
**配置类**: `SecurityConfig`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 当前配置允许所有请求访问,可以根据需要自定义安全规则
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class CustomSecurityConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.authorizeHttpRequests(authz -> authz
|
||||||
|
// 公开接口
|
||||||
|
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
|
||||||
|
.requestMatchers("/actuator/**", "/api-docs/**", "/swagger-ui/**").permitAll()
|
||||||
|
|
||||||
|
// 需要认证的接口
|
||||||
|
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
|
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(form -> form
|
||||||
|
.loginPage("/login")
|
||||||
|
.loginProcessingUrl("/api/auth/login")
|
||||||
|
.successHandler(authenticationSuccessHandler())
|
||||||
|
.failureHandler(authenticationFailureHandler())
|
||||||
|
)
|
||||||
|
.logout(logout -> logout
|
||||||
|
.logoutUrl("/api/auth/logout")
|
||||||
|
.logoutSuccessHandler(logoutSuccessHandler())
|
||||||
|
)
|
||||||
|
.sessionManagement(session -> session
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
|
.maximumSessions(1)
|
||||||
|
.maxSessionsPreventsLogin(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证成功处理器
|
||||||
|
@Bean
|
||||||
|
public AuthenticationSuccessHandler authenticationSuccessHandler() {
|
||||||
|
return (request, response, authentication) -> {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
ApiResponse<UserInfo> result = ApiResponse.success("登录成功", getUserInfo(authentication));
|
||||||
|
response.getWriter().write(JSON.toJSONString(result));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户认证Controller
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ApiResponse<UserInfo> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) {
|
||||||
|
// Spring Security会自动处理认证
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
UserInfo userInfo = getUserInfo(authentication);
|
||||||
|
return ApiResponse.success("登录成功", userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ApiResponse<Void> logout(HttpServletRequest request) {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
request.getSession().invalidate();
|
||||||
|
return ApiResponse.success("退出成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/current")
|
||||||
|
public ApiResponse<UserInfo> getCurrentUser() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return ApiResponse.error("UNAUTHORIZED", "未登录");
|
||||||
|
}
|
||||||
|
UserInfo userInfo = getUserInfo(authentication);
|
||||||
|
return ApiResponse.success(userInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. 📚 Swagger API文档
|
||||||
|
**功能描述**: 自动生成API文档,支持在线测试
|
||||||
|
|
||||||
|
**配置类**: `SwaggerConfig`
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```java
|
||||||
|
// 在Controller中添加Swagger注解
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/products")
|
||||||
|
@Tag(name = "商品管理", description = "商品相关的API接口")
|
||||||
|
public class ProductController {
|
||||||
|
|
||||||
|
@Operation(summary = "创建商品", description = "创建一个新的商品")
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "创建成功"),
|
||||||
|
@ApiResponse(responseCode = "400", description = "参数错误"),
|
||||||
|
@ApiResponse(responseCode = "500", description = "服务器错误")
|
||||||
|
})
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Product> createProduct(
|
||||||
|
@Parameter(description = "商品信息", required = true)
|
||||||
|
@Valid @RequestBody CreateProductRequest request) {
|
||||||
|
|
||||||
|
Product product = productService.createProduct(request);
|
||||||
|
return ApiResponse.success("创建成功", product);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取商品详情", description = "根据商品ID获取商品详细信息")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<Product> getProduct(
|
||||||
|
@Parameter(description = "商品ID", required = true, example = "1")
|
||||||
|
@PathVariable Long id) {
|
||||||
|
|
||||||
|
Product product = productService.findById(id);
|
||||||
|
return ApiResponse.success(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "商品列表查询", description = "分页查询商品列表,支持关键词搜索")
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<Product>> getProducts(
|
||||||
|
@Parameter(description = "页码", example = "1")
|
||||||
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
|
||||||
|
@Parameter(description = "每页大小", example = "10")
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
|
||||||
|
@Parameter(description = "搜索关键词")
|
||||||
|
@RequestParam(required = false) String keyword) {
|
||||||
|
|
||||||
|
PageResult<Product> result = productService.findByPage(page, size, keyword);
|
||||||
|
return ApiResponse.success("查询成功", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO类添加Swagger注解
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建商品请求")
|
||||||
|
public class CreateProductRequest {
|
||||||
|
|
||||||
|
@Schema(description = "商品名称", example = "iPhone 15 Pro", required = true)
|
||||||
|
@NotBlank(message = "商品名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "商品描述", example = "最新款iPhone手机")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "商品价格", example = "8999.00", required = true)
|
||||||
|
@NotNull(message = "商品价格不能为空")
|
||||||
|
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
@Schema(description = "商品分类ID", example = "1", required = true)
|
||||||
|
@NotNull(message = "商品分类不能为空")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
@Schema(description = "库存数量", example = "100", required = true)
|
||||||
|
@NotNull(message = "库存数量不能为空")
|
||||||
|
@Min(value = 0, message = "库存数量不能为负数")
|
||||||
|
private Integer stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 访问Swagger UI: http://localhost:8080/swagger-ui.html
|
||||||
|
// 访问API文档JSON: http://localhost:8080/api-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 创建实体类
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("your_table")
|
||||||
|
public class YourEntity extends BaseEntity {
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
// 其他字段...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建Mapper接口
|
||||||
|
```java
|
||||||
|
@Repository
|
||||||
|
public interface YourEntityMapper extends BaseMapper<YourEntity> {
|
||||||
|
// 自定义查询方法
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 创建Service类
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class YourEntityService {
|
||||||
|
@Autowired
|
||||||
|
private YourEntityMapper mapper;
|
||||||
|
|
||||||
|
// 业务逻辑方法
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 创建Controller类
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/your-entity")
|
||||||
|
public class YourEntityController {
|
||||||
|
@Autowired
|
||||||
|
private YourEntityService service;
|
||||||
|
|
||||||
|
// API接口方法
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 配置说明
|
||||||
|
|
||||||
|
### 数据库配置
|
||||||
|
```properties
|
||||||
|
# MySQL数据库连接
|
||||||
|
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
|
||||||
|
spring.datasource.username=your_username
|
||||||
|
spring.datasource.password=your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件上传配置
|
||||||
|
```properties
|
||||||
|
# 文件上传大小限制
|
||||||
|
spring.servlet.multipart.max-file-size=10MB
|
||||||
|
spring.servlet.multipart.max-request-size=50MB
|
||||||
|
app.upload.dir=./uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能监控配置
|
||||||
|
```properties
|
||||||
|
# 性能警告阈值(毫秒)
|
||||||
|
app.performance.warning.controller=5000
|
||||||
|
app.performance.warning.service=3000
|
||||||
|
app.performance.warning.mapper=500
|
||||||
|
```
|
||||||
|
|
||||||
|
这个架构模板提供了完整的企业级应用基础设施,您可以在此基础上快速开发各种类型的业务应用!
|
||||||
295
mvnw
vendored
Normal file
295
mvnw
vendored
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
189
mvnw.cmd
vendored
Normal file
189
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
154
pom.xml
Normal file
154
pom.xml
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<?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.6</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.aida</groupId>
|
||||||
|
<artifactId>lanecarford</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>lanecarford</name>
|
||||||
|
<description>Demo project for Spring Boot</description>
|
||||||
|
<url/>
|
||||||
|
<licenses>
|
||||||
|
<license/>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer/>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection/>
|
||||||
|
<developerConnection/>
|
||||||
|
<tag/>
|
||||||
|
<url/>
|
||||||
|
</scm>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web Starter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Security -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MyBatis-Plus -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
|
<version>3.5.5</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MySQL Driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<version>8.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Validation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Actuator -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON Processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Swagger for API Documentation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Spring Boot AOP -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Test Dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.12.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
<release>21</release>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>--enable-preview</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@MapperScan("com.aida.lanecarford.mapper")
|
||||||
|
public class LanecarfordApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(LanecarfordApplication.class, args);
|
||||||
|
System.out.println("LaneCarfordApplication started successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
127
src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java
Normal file
127
src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package com.aida.lanecarford.aspect;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志切面
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义切点:所有Controller方法
|
||||||
|
*/
|
||||||
|
@Pointcut("execution(* com.aida.lanecarford.controller..*(..))")
|
||||||
|
public void controllerMethods() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义切点:所有Service方法
|
||||||
|
*/
|
||||||
|
@Pointcut("execution(* com.aida.lanecarford.service..*(..))")
|
||||||
|
public void serviceMethods() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller方法执行前记录日志
|
||||||
|
*/
|
||||||
|
@Before("controllerMethods()")
|
||||||
|
public void logControllerBefore(JoinPoint joinPoint) {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
|
||||||
|
logger.info("=== 请求开始 ===");
|
||||||
|
logger.info("请求URL: {}", request.getRequestURL().toString());
|
||||||
|
logger.info("请求方法: {}", request.getMethod());
|
||||||
|
logger.info("请求IP: {}", getClientIpAddress(request));
|
||||||
|
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
|
||||||
|
logger.info("请求参数: {}", Arrays.toString(joinPoint.getArgs()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller方法执行后记录日志
|
||||||
|
*/
|
||||||
|
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
|
||||||
|
public void logControllerAfterReturning(JoinPoint joinPoint, Object result) {
|
||||||
|
logger.info("方法执行成功: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
|
||||||
|
logger.info("返回结果: {}", result);
|
||||||
|
logger.info("=== 请求结束 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller方法抛出异常时记录日志
|
||||||
|
*/
|
||||||
|
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
|
||||||
|
public void logControllerAfterThrowing(JoinPoint joinPoint, Throwable exception) {
|
||||||
|
logger.error("方法执行异常: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
|
||||||
|
logger.error("异常信息: ", exception);
|
||||||
|
logger.info("=== 请求异常结束 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service方法环绕通知,记录执行时间
|
||||||
|
*/
|
||||||
|
@Around("serviceMethods()")
|
||||||
|
public Object logServiceAround(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("Service方法开始执行: {}", methodName);
|
||||||
|
Object result = joinPoint.proceed();
|
||||||
|
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
logger.debug("Service方法执行成功: {}, 耗时: {}ms", methodName, (endTime - startTime));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
logger.error("Service方法执行异常: {}, 耗时: {}ms", methodName, (endTime - startTime));
|
||||||
|
logger.error("异常详情: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端真实IP地址
|
||||||
|
*/
|
||||||
|
private String getClientIpAddress(HttpServletRequest request) {
|
||||||
|
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||||
|
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
|
||||||
|
return xForwardedFor.split(",")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
String xRealIp = request.getHeader("X-Real-IP");
|
||||||
|
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||||
|
return xRealIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
String proxyClientIp = request.getHeader("Proxy-Client-IP");
|
||||||
|
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
|
||||||
|
return proxyClientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
|
||||||
|
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
|
||||||
|
return wlProxyClientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java
Normal file
134
src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package com.aida.lanecarford.aspect;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能监控切面
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class PerformanceAspect {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监控Controller方法性能
|
||||||
|
*/
|
||||||
|
@Around("execution(* com.aida.lanecarford.controller..*(..))")
|
||||||
|
public Object monitorControllerPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
return monitorMethodPerformance(joinPoint, "Controller");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监控Service方法性能
|
||||||
|
*/
|
||||||
|
@Around("execution(* com.aida.lanecarford.service..*(..))")
|
||||||
|
public Object monitorServicePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
return monitorMethodPerformance(joinPoint, "Service");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监控数据库操作性能
|
||||||
|
*/
|
||||||
|
@Around("execution(* com.aida.lanecarford.mapper..*(..))")
|
||||||
|
public Object monitorMapperPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
return monitorMethodPerformance(joinPoint, "Mapper");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用性能监控方法
|
||||||
|
*/
|
||||||
|
private Object monitorMethodPerformance(ProceedingJoinPoint joinPoint, String layer) throws Throwable {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object result = joinPoint.proceed();
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long executionTime = endTime - startTime;
|
||||||
|
|
||||||
|
// 记录性能日志
|
||||||
|
logPerformance(layer, methodName, executionTime, true);
|
||||||
|
|
||||||
|
// 如果执行时间过长,记录警告
|
||||||
|
if (executionTime > getWarningThreshold(layer)) {
|
||||||
|
logger.warn("{}方法执行时间过长: {} - {}ms", layer, methodName, executionTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long executionTime = endTime - startTime;
|
||||||
|
|
||||||
|
// 记录异常性能日志
|
||||||
|
logPerformance(layer, methodName, executionTime, false);
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录性能日志
|
||||||
|
*/
|
||||||
|
private void logPerformance(String layer, String methodName, long executionTime, boolean success) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("性能监控 - {}: {} - {}ms - {}",
|
||||||
|
layer, methodName, executionTime, success ? "成功" : "失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以在这里添加性能数据收集逻辑,比如发送到监控系统
|
||||||
|
collectPerformanceMetrics(layer, methodName, executionTime, success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取警告阈值(毫秒)
|
||||||
|
*/
|
||||||
|
private long getWarningThreshold(String layer) {
|
||||||
|
switch (layer) {
|
||||||
|
case "Controller":
|
||||||
|
return 5000; // 5秒
|
||||||
|
case "Service":
|
||||||
|
return 3000; // 3秒
|
||||||
|
case "Mapper":
|
||||||
|
return 1000; // 1秒
|
||||||
|
default:
|
||||||
|
return 2000; // 2秒
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集性能指标
|
||||||
|
* TODO: 可以集成到监控系统(如Micrometer、Prometheus等)
|
||||||
|
*/
|
||||||
|
private void collectPerformanceMetrics(String layer, String methodName, long executionTime, boolean success) {
|
||||||
|
// 这里可以添加性能指标收集逻辑
|
||||||
|
// 例如:
|
||||||
|
// - 发送到时序数据库
|
||||||
|
// - 更新内存中的统计信息
|
||||||
|
// - 发送到监控系统
|
||||||
|
|
||||||
|
// 示例:简单的内存统计
|
||||||
|
PerformanceMetrics.recordExecution(layer, methodName, executionTime, success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的性能指标收集器
|
||||||
|
*/
|
||||||
|
private static class PerformanceMetrics {
|
||||||
|
|
||||||
|
public static void recordExecution(String layer, String methodName, long executionTime, boolean success) {
|
||||||
|
// 简单的日志记录,实际项目中可以替换为更复杂的指标收集
|
||||||
|
if (executionTime > 1000) { // 超过1秒的操作
|
||||||
|
logger.info("慢操作记录 - {}.{}: {}ms, 成功: {}", layer, methodName, executionTime, success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/main/java/com/aida/lanecarford/common/ApiResponse.java
Normal file
168
src/main/java/com/aida/lanecarford/common/ApiResponse.java
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package com.aida.lanecarford.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一API响应格式
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ApiResponse<T> implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应状态码
|
||||||
|
*/
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应消息
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应数据
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否成功
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间戳
|
||||||
|
*/
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public ApiResponse() {
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean success, String code, String message, T data) {
|
||||||
|
this();
|
||||||
|
this.success = success;
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(无数据)
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success() {
|
||||||
|
return new ApiResponse<>(true, "SUCCESS", "操作成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(带数据)
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return new ApiResponse<>(true, "SUCCESS", "操作成功", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(自定义消息,无数据)
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success(String message) {
|
||||||
|
return new ApiResponse<>(true, "SUCCESS", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(自定义消息)
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success(String message, T data) {
|
||||||
|
return new ApiResponse<>(true, "SUCCESS", message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> error(String code, String message) {
|
||||||
|
return new ApiResponse<>(false, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败响应(默认错误码)
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> error(String message) {
|
||||||
|
return new ApiResponse<>(false, "ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数错误响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> paramError(String message) {
|
||||||
|
return new ApiResponse<>(false, "PARAM_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据不存在响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> notFound(String message) {
|
||||||
|
return new ApiResponse<>(false, "NOT_FOUND", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限不足响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> forbidden(String message) {
|
||||||
|
return new ApiResponse<>(false, "FORBIDDEN", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器内部错误响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> serverError(String message) {
|
||||||
|
return new ApiResponse<>(false, "SERVER_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> businessError(String code, String message) {
|
||||||
|
return new ApiResponse<>(false, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部服务错误响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> externalServiceError(String message) {
|
||||||
|
return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传错误响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> fileUploadError(String message) {
|
||||||
|
return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证失败响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> validationError(String message) {
|
||||||
|
return new ApiResponse<>(false, "VALIDATION_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重复数据响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> duplicateError(String message) {
|
||||||
|
return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作超时响应
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> timeoutError(String message) {
|
||||||
|
return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/main/java/com/aida/lanecarford/common/PageResult.java
Normal file
136
src/main/java/com/aida/lanecarford/common/PageResult.java
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package com.aida.lanecarford.common;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页结果类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PageResult<T> implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据列表
|
||||||
|
*/
|
||||||
|
private List<T> records;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总记录数
|
||||||
|
*/
|
||||||
|
private long total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前页码
|
||||||
|
*/
|
||||||
|
private long current;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页大小
|
||||||
|
*/
|
||||||
|
private long size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总页数
|
||||||
|
*/
|
||||||
|
private long pages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有上一页
|
||||||
|
*/
|
||||||
|
private boolean hasPrevious;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有下一页
|
||||||
|
*/
|
||||||
|
private boolean hasNext;
|
||||||
|
|
||||||
|
public PageResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PageResult(List<T> records, long total, long current, long size) {
|
||||||
|
this.records = records;
|
||||||
|
this.total = total;
|
||||||
|
this.current = current;
|
||||||
|
this.size = size;
|
||||||
|
this.pages = (total + size - 1) / size;
|
||||||
|
this.hasPrevious = current > 1;
|
||||||
|
this.hasNext = current < pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从MyBatis-Plus的IPage转换
|
||||||
|
*/
|
||||||
|
public static <T> PageResult<T> of(IPage<T> page) {
|
||||||
|
return new PageResult<>(
|
||||||
|
page.getRecords(),
|
||||||
|
page.getTotal(),
|
||||||
|
page.getCurrent(),
|
||||||
|
page.getSize()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空的分页结果
|
||||||
|
*/
|
||||||
|
public static <T> PageResult<T> empty() {
|
||||||
|
return new PageResult<>(List.of(), 0, 1, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空的分页结果(指定页码和大小)
|
||||||
|
*/
|
||||||
|
public static <T> PageResult<T> empty(long current, long size) {
|
||||||
|
return new PageResult<>(List.of(), 0, current, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单页结果
|
||||||
|
*/
|
||||||
|
public static <T> PageResult<T> of(List<T> records) {
|
||||||
|
return new PageResult<>(records, records.size(), 1, records.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开始记录索引(从1开始)
|
||||||
|
*/
|
||||||
|
public long getStartIndex() {
|
||||||
|
return (current - 1) * size + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取结束记录索引
|
||||||
|
*/
|
||||||
|
public long getEndIndex() {
|
||||||
|
long end = current * size;
|
||||||
|
return Math.min(end, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为空结果
|
||||||
|
*/
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return records == null || records.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为第一页
|
||||||
|
*/
|
||||||
|
public boolean isFirst() {
|
||||||
|
return current == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为最后一页
|
||||||
|
*/
|
||||||
|
public boolean isLast() {
|
||||||
|
return current == pages;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/com/aida/lanecarford/config/CacheConfig.java
Normal file
53
src/main/java/com/aida/lanecarford/config/CacheConfig.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class CacheConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存名称常量
|
||||||
|
*/
|
||||||
|
public static final String STYLE_OUTFITS_CACHE = "styleOutfits";
|
||||||
|
public static final String MODEL_PHOTOS_CACHE = "modelPhotos";
|
||||||
|
public static final String VIRTUAL_TRYONS_CACHE = "virtualTryOns";
|
||||||
|
public static final String FAVORITE_OUTFITS_CACHE = "favoriteOutfits";
|
||||||
|
public static final String CUSTOMER_CACHE = "customers";
|
||||||
|
public static final String SALES_ADVISOR_CACHE = "salesAdvisors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认缓存管理器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public CacheManager cacheManager() {
|
||||||
|
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
|
||||||
|
|
||||||
|
// 预创建缓存
|
||||||
|
cacheManager.setCacheNames(java.util.Arrays.asList(
|
||||||
|
STYLE_OUTFITS_CACHE,
|
||||||
|
MODEL_PHOTOS_CACHE,
|
||||||
|
VIRTUAL_TRYONS_CACHE,
|
||||||
|
FAVORITE_OUTFITS_CACHE,
|
||||||
|
CUSTOMER_CACHE,
|
||||||
|
SALES_ADVISOR_CACHE
|
||||||
|
));
|
||||||
|
|
||||||
|
// 允许空值缓存
|
||||||
|
cacheManager.setAllowNullValues(true);
|
||||||
|
|
||||||
|
return cacheManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.util.unit.DataSize;
|
||||||
|
|
||||||
|
import jakarta.servlet.MultipartConfigElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class FileUploadConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置文件上传参数
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public MultipartConfigElement multipartConfigElement() {
|
||||||
|
MultipartConfigFactory factory = new MultipartConfigFactory();
|
||||||
|
|
||||||
|
// 设置单个文件最大大小(10MB)
|
||||||
|
factory.setMaxFileSize(DataSize.ofMegabytes(10));
|
||||||
|
|
||||||
|
// 设置总上传数据最大大小(50MB)
|
||||||
|
factory.setMaxRequestSize(DataSize.ofMegabytes(50));
|
||||||
|
|
||||||
|
// 设置内存临界值(1MB)
|
||||||
|
factory.setFileSizeThreshold(DataSize.ofMegabytes(1));
|
||||||
|
|
||||||
|
return factory.createMultipartConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/java/com/aida/lanecarford/config/LoggingConfig.java
Normal file
36
src/main/java/com/aida/lanecarford/config/LoggingConfig.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class LoggingConfig {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoggingConfig.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置请求日志过滤器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public CommonsRequestLoggingFilter requestLoggingFilter() {
|
||||||
|
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
|
||||||
|
filter.setIncludeClientInfo(true);
|
||||||
|
filter.setIncludeQueryString(true);
|
||||||
|
filter.setIncludePayload(true);
|
||||||
|
filter.setIncludeHeaders(false);
|
||||||
|
filter.setMaxPayloadLength(1000);
|
||||||
|
filter.setAfterMessagePrefix("REQUEST DATA : ");
|
||||||
|
|
||||||
|
logger.info("请求日志过滤器已配置");
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyBatis-Plus配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class MyBatisPlusConfig implements MetaObjectHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入时自动填充
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
|
||||||
|
this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时自动填充
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页插件配置
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||||
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||||
|
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||||
|
return interceptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class PerformanceConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置异步任务执行器
|
||||||
|
*/
|
||||||
|
@Bean(name = "taskExecutor")
|
||||||
|
public Executor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|
||||||
|
// 核心线程数
|
||||||
|
executor.setCorePoolSize(5);
|
||||||
|
|
||||||
|
// 最大线程数
|
||||||
|
executor.setMaxPoolSize(20);
|
||||||
|
|
||||||
|
// 队列容量
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
|
||||||
|
// 线程名前缀
|
||||||
|
executor.setThreadNamePrefix("LaneCarford-Async-");
|
||||||
|
|
||||||
|
// 线程空闲时间(秒)
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
|
||||||
|
// 拒绝策略:由调用线程处理
|
||||||
|
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
|
||||||
|
// 等待所有任务结束后再关闭线程池
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
|
||||||
|
// 等待时间(秒)
|
||||||
|
executor.setAwaitTerminationSeconds(60);
|
||||||
|
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置文件处理专用线程池
|
||||||
|
*/
|
||||||
|
@Bean(name = "fileTaskExecutor")
|
||||||
|
public Executor fileTaskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|
||||||
|
// 文件处理通常是IO密集型,可以设置更多线程
|
||||||
|
executor.setCorePoolSize(3);
|
||||||
|
executor.setMaxPoolSize(10);
|
||||||
|
executor.setQueueCapacity(50);
|
||||||
|
executor.setThreadNamePrefix("LaneCarford-File-");
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
executor.setAwaitTerminationSeconds(60);
|
||||||
|
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置AI服务调用专用线程池
|
||||||
|
*/
|
||||||
|
@Bean(name = "aiServiceExecutor")
|
||||||
|
public Executor aiServiceExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|
||||||
|
// AI服务调用可能耗时较长,设置较少的线程数
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(5);
|
||||||
|
executor.setQueueCapacity(20);
|
||||||
|
executor.setThreadNamePrefix("LaneCarford-AI-");
|
||||||
|
executor.setKeepAliveSeconds(120);
|
||||||
|
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
executor.setAwaitTerminationSeconds(120);
|
||||||
|
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestTemplate配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建RestTemplate Bean
|
||||||
|
*
|
||||||
|
* @param builder RestTemplate构建器
|
||||||
|
* @return RestTemplate实例
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||||
|
return builder
|
||||||
|
.setConnectTimeout(Duration.ofSeconds(30))
|
||||||
|
.setReadTimeout(Duration.ofSeconds(60))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security配置类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码编码器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证管理器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||||
|
return config.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全过滤器链配置
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
// 禁用CSRF保护
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
|
||||||
|
// 配置授权规则 - 允许所有请求访问(基础架构模式)
|
||||||
|
.authorizeHttpRequests(authz -> authz
|
||||||
|
.requestMatchers("/actuator/**", "/api-docs/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/main/java/com/aida/lanecarford/config/SwaggerConfig.java
Normal file
172
src/main/java/com/aida/lanecarford/config/SwaggerConfig.java
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.Components;
|
||||||
|
import io.swagger.v3.oas.models.ExternalDocumentation;
|
||||||
|
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.info.License;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
import io.swagger.v3.oas.models.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swagger配置类
|
||||||
|
* 提供完整的API文档配置,包括安全认证、服务器信息和标签分类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Value("${server.port:8080}")
|
||||||
|
private String serverPort;
|
||||||
|
|
||||||
|
@Value("${spring.application.name:lanecarford}")
|
||||||
|
private String applicationName;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI customOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(createApiInfo())
|
||||||
|
.servers(createServers())
|
||||||
|
.components(createComponents())
|
||||||
|
.security(createSecurityRequirements())
|
||||||
|
.tags(createTags())
|
||||||
|
.externalDocs(createExternalDocumentation());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建API信息
|
||||||
|
*/
|
||||||
|
private Info createApiInfo() {
|
||||||
|
return new Info()
|
||||||
|
.title("Lane Carford 虚拟试衣系统 API文档")
|
||||||
|
.description("""
|
||||||
|
## Lane Carford 虚拟试衣系统后端接口文档
|
||||||
|
|
||||||
|
### 系统功能
|
||||||
|
- **客户管理**: 客户信息的创建、查询和管理
|
||||||
|
- **风格分析**: 基于AI的服装风格分析和推荐
|
||||||
|
- **虚拟试衣**: 虚拟试衣效果展示和管理
|
||||||
|
- **收藏管理**: 客户喜欢的搭配收藏和管理
|
||||||
|
- **模特照片**: 模特照片的上传和管理
|
||||||
|
- **导购系统**: 导购员登录和客户服务管理
|
||||||
|
|
||||||
|
### 认证说明
|
||||||
|
系统使用Session认证,需要先通过登录接口获取认证状态。
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
所有API响应都遵循统一格式:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"code": "SUCCESS",
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": {},
|
||||||
|
"timestamp": 1640995200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
系统提供统一的错误处理机制,详细的错误信息会在响应中返回。
|
||||||
|
""")
|
||||||
|
.version("1.0.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Lane Carford 开发团队")
|
||||||
|
.email("dev@lanecarford.com")
|
||||||
|
.url("https://www.lanecarford.com"))
|
||||||
|
.license(new License()
|
||||||
|
.name("Apache 2.0")
|
||||||
|
.url("http://www.apache.org/licenses/LICENSE-2.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建服务器信息
|
||||||
|
*/
|
||||||
|
private List<Server> createServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Server()
|
||||||
|
.url("http://localhost:" + serverPort)
|
||||||
|
.description("本地开发环境"),
|
||||||
|
new Server()
|
||||||
|
.url("https://api.lanecarford.com")
|
||||||
|
.description("生产环境"),
|
||||||
|
new Server()
|
||||||
|
.url("https://test-api.lanecarford.com")
|
||||||
|
.description("测试环境")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建组件配置(安全方案等)
|
||||||
|
*/
|
||||||
|
private Components createComponents() {
|
||||||
|
return new Components()
|
||||||
|
.addSecuritySchemes("sessionAuth", new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
.in(SecurityScheme.In.COOKIE)
|
||||||
|
.name("JSESSIONID")
|
||||||
|
.description("Session认证,通过登录接口获取"))
|
||||||
|
.addSecuritySchemes("basicAuth", new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("basic")
|
||||||
|
.description("基础认证(仅用于开发测试)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建安全要求
|
||||||
|
*/
|
||||||
|
private List<SecurityRequirement> createSecurityRequirements() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new SecurityRequirement().addList("sessionAuth"),
|
||||||
|
new SecurityRequirement().addList("basicAuth")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建API标签分类
|
||||||
|
*/
|
||||||
|
private List<Tag> createTags() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Tag()
|
||||||
|
.name("认证管理")
|
||||||
|
.description("用户登录、登出和认证相关接口"),
|
||||||
|
new Tag()
|
||||||
|
.name("客户管理")
|
||||||
|
.description("客户信息的创建、查询、更新和删除"),
|
||||||
|
new Tag()
|
||||||
|
.name("风格分析")
|
||||||
|
.description("基于AI的服装风格分析和推荐系统"),
|
||||||
|
new Tag()
|
||||||
|
.name("虚拟试衣")
|
||||||
|
.description("虚拟试衣效果的生成和管理"),
|
||||||
|
new Tag()
|
||||||
|
.name("收藏管理")
|
||||||
|
.description("客户喜欢的搭配收藏和个人化管理"),
|
||||||
|
new Tag()
|
||||||
|
.name("模特照片")
|
||||||
|
.description("模特照片的上传、管理和展示"),
|
||||||
|
new Tag()
|
||||||
|
.name("系统监控")
|
||||||
|
.description("系统健康检查和状态监控接口")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建外部文档链接
|
||||||
|
*/
|
||||||
|
private ExternalDocumentation createExternalDocumentation() {
|
||||||
|
return new ExternalDocumentation()
|
||||||
|
.description("Lane Carford 系统文档")
|
||||||
|
.url("https://docs.lanecarford.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/java/com/aida/lanecarford/config/WebConfig.java
Normal file
46
src/main/java/com/aida/lanecarford/config/WebConfig.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.aida.lanecarford.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web配置类 - 纯API后端服务
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置跨域 - 支持前后端分离
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/api/**")
|
||||||
|
.allowedOriginPatterns("*")
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.allowCredentials(true)
|
||||||
|
.maxAge(3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置资源处理 - 仅保留API文档和文件上传
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
// 配置上传文件的访问路径
|
||||||
|
registry.addResourceHandler("/uploads/**")
|
||||||
|
.addResourceLocations("file:uploads/");
|
||||||
|
|
||||||
|
// 配置Swagger UI资源
|
||||||
|
registry.addResourceHandler("/swagger-ui/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
|
||||||
|
|
||||||
|
registry.addResourceHandler("/v3/api-docs/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.CustomerService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/customers")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerController {
|
||||||
|
|
||||||
|
private final CustomerService customerService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.CustomerPhotoService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/customer-photos")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerPhotoController {
|
||||||
|
|
||||||
|
private final CustomerPhotoService customerPhotoService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.ModelPhotoService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/model-photos")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ModelPhotoController {
|
||||||
|
|
||||||
|
private final ModelPhotoService modelPhotoService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.SalesService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/sales")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SalesController {
|
||||||
|
|
||||||
|
private final SalesService salesService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.StyleService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/styles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StyleController {
|
||||||
|
|
||||||
|
private final StyleService styleService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.TryOnEffectService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/try-on-effects")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TryOnEffectController {
|
||||||
|
|
||||||
|
private final TryOnEffectService tryOnEffectService;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aida.lanecarford.controller;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.service.VisitRecordService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录控制器
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/visit-records")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VisitRecordController {
|
||||||
|
|
||||||
|
private final VisitRecordService visitRecordService;
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/java/com/aida/lanecarford/dto/BaseDTO.java
Normal file
44
src/main/java/com/aida/lanecarford/dto/BaseDTO.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package com.aida.lanecarford.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据传输对象基类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "数据传输对象基类")
|
||||||
|
public abstract class BaseDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "记录ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
@Schema(description = "版本号(用于乐观锁)")
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除(0-未删除,1-已删除)")
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
98
src/main/java/com/aida/lanecarford/dto/BaseRequest.java
Normal file
98
src/main/java/com/aida/lanecarford/dto/BaseRequest.java
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package com.aida.lanecarford.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求参数基类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "请求参数基类")
|
||||||
|
public abstract class BaseRequest implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "当前页码", example = "1")
|
||||||
|
@Min(value = 1, message = "页码必须大于0")
|
||||||
|
private Integer current = 1;
|
||||||
|
|
||||||
|
@Schema(description = "每页大小", example = "10")
|
||||||
|
@Min(value = 1, message = "每页大小必须大于0")
|
||||||
|
@Max(value = 100, message = "每页大小不能超过100")
|
||||||
|
private Integer size = 10;
|
||||||
|
|
||||||
|
@Schema(description = "排序字段", example = "createTime")
|
||||||
|
private String sortField;
|
||||||
|
|
||||||
|
@Schema(description = "排序方向(ASC-升序,DESC-降序)", example = "DESC")
|
||||||
|
private String sortOrder = "DESC";
|
||||||
|
|
||||||
|
@Schema(description = "搜索关键词")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "开始时间(格式:yyyy-MM-dd HH:mm:ss)")
|
||||||
|
private String startTime;
|
||||||
|
|
||||||
|
@Schema(description = "结束时间(格式:yyyy-MM-dd HH:mm:ss)")
|
||||||
|
private String endTime;
|
||||||
|
|
||||||
|
@Schema(description = "状态筛选")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取偏移量
|
||||||
|
*/
|
||||||
|
public long getOffset() {
|
||||||
|
return (long) (current - 1) * size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取限制数量
|
||||||
|
*/
|
||||||
|
public long getLimit() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有搜索关键词
|
||||||
|
*/
|
||||||
|
public boolean hasKeyword() {
|
||||||
|
return keyword != null && !keyword.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有时间范围筛选
|
||||||
|
*/
|
||||||
|
public boolean hasTimeRange() {
|
||||||
|
return startTime != null && !startTime.trim().isEmpty()
|
||||||
|
&& endTime != null && !endTime.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有状态筛选
|
||||||
|
*/
|
||||||
|
public boolean hasStatus() {
|
||||||
|
return status != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要排序
|
||||||
|
*/
|
||||||
|
public boolean needSort() {
|
||||||
|
return sortField != null && !sortField.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否降序排序
|
||||||
|
*/
|
||||||
|
public boolean isDesc() {
|
||||||
|
return "DESC".equalsIgnoreCase(sortOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/com/aida/lanecarford/entity/BaseEntity.java
Normal file
42
src/main/java/com/aida/lanecarford/entity/BaseEntity.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础实体类
|
||||||
|
* 包含通用字段,其他实体类可以继承此类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除标志:0-未删除,1-已删除
|
||||||
|
*/
|
||||||
|
@TableLogic
|
||||||
|
@TableField(value = "deleted")
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
73
src/main/java/com/aida/lanecarford/entity/Customer.java
Normal file
73
src/main/java/com/aida/lanecarford/entity/Customer.java
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("customers")
|
||||||
|
public class Customer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客姓名
|
||||||
|
*/
|
||||||
|
@TableField("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客邮箱
|
||||||
|
*/
|
||||||
|
@TableField("email")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号
|
||||||
|
*/
|
||||||
|
@TableField("phone")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别
|
||||||
|
*/
|
||||||
|
@TableField("gender")
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年龄段
|
||||||
|
*/
|
||||||
|
@TableField("age_range")
|
||||||
|
private String ageRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
67
src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java
Normal file
67
src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("customer_photos")
|
||||||
|
public class CustomerPhoto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客ID
|
||||||
|
*/
|
||||||
|
@TableField("customer_id")
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录ID
|
||||||
|
*/
|
||||||
|
@TableField("visit_record_id")
|
||||||
|
private Long visitRecordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 照片URL
|
||||||
|
*/
|
||||||
|
@TableField("photo_url")
|
||||||
|
private String photoUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为主照片(0-否,1-是)
|
||||||
|
*/
|
||||||
|
@TableField("is_primary")
|
||||||
|
private Integer isPrimary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传时间
|
||||||
|
*/
|
||||||
|
@TableField("upload_time")
|
||||||
|
private LocalDateTime uploadTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
}
|
||||||
73
src/main/java/com/aida/lanecarford/entity/ModelPhoto.java
Normal file
73
src/main/java/com/aida/lanecarford/entity/ModelPhoto.java
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("model_photos")
|
||||||
|
public class ModelPhoto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片URL
|
||||||
|
*/
|
||||||
|
@TableField("photo_url")
|
||||||
|
private String photoUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 照片名称
|
||||||
|
*/
|
||||||
|
@TableField("photo_name")
|
||||||
|
private String photoName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别
|
||||||
|
*/
|
||||||
|
@TableField("gender")
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用(0-禁用,1-启用)
|
||||||
|
*/
|
||||||
|
@TableField("is_active")
|
||||||
|
private Integer isActive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序权重
|
||||||
|
*/
|
||||||
|
@TableField("sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
97
src/main/java/com/aida/lanecarford/entity/Sales.java
Normal file
97
src/main/java/com/aida/lanecarford/entity/Sales.java
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("sales")
|
||||||
|
public class Sales {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
@TableField("username")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码(加密后)
|
||||||
|
*/
|
||||||
|
@TableField("password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真实姓名
|
||||||
|
*/
|
||||||
|
@TableField("real_name")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 员工编号
|
||||||
|
*/
|
||||||
|
@TableField("employee_id")
|
||||||
|
private String employeeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店ID
|
||||||
|
*/
|
||||||
|
@TableField("store_id")
|
||||||
|
private String storeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店名称
|
||||||
|
*/
|
||||||
|
@TableField("store_name")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号
|
||||||
|
*/
|
||||||
|
@TableField("phone")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
@TableField("email")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用(0-禁用,1-启用)
|
||||||
|
*/
|
||||||
|
@TableField("is_active")
|
||||||
|
private Integer isActive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
85
src/main/java/com/aida/lanecarford/entity/Style.java
Normal file
85
src/main/java/com/aida/lanecarford/entity/Style.java
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("styles")
|
||||||
|
public class Style {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客ID
|
||||||
|
*/
|
||||||
|
@TableField("customer_id")
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录ID
|
||||||
|
*/
|
||||||
|
@TableField("visit_record_id")
|
||||||
|
private Long visitRecordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否选中(0-未选中,1-已选中)
|
||||||
|
*/
|
||||||
|
@TableField("is_selected")
|
||||||
|
private Integer isSelected;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格图片URL
|
||||||
|
*/
|
||||||
|
@TableField("style_image_url")
|
||||||
|
private String styleImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python请求ID
|
||||||
|
*/
|
||||||
|
@TableField("python_request_id")
|
||||||
|
private String pythonRequestId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成状态(0-处理中,1-已完成,2-失败)
|
||||||
|
*/
|
||||||
|
@TableField("generation_status")
|
||||||
|
private Integer generationStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
@TableField("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
109
src/main/java/com/aida/lanecarford/entity/TryOnEffect.java
Normal file
109
src/main/java/com/aida/lanecarford/entity/TryOnEffect.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("try_on_effects")
|
||||||
|
public class TryOnEffect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客ID
|
||||||
|
*/
|
||||||
|
@TableField("customer_id")
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录ID
|
||||||
|
*/
|
||||||
|
@TableField("visit_record_id")
|
||||||
|
private Long visitRecordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片ID
|
||||||
|
*/
|
||||||
|
@TableField("customer_photo_id")
|
||||||
|
private Long customerPhotoId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片ID
|
||||||
|
*/
|
||||||
|
@TableField("model_photo_id")
|
||||||
|
private Long modelPhotoId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果图URL
|
||||||
|
*/
|
||||||
|
@TableField("try_on_image_url")
|
||||||
|
private String tryOnImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示词
|
||||||
|
*/
|
||||||
|
@TableField("prompt")
|
||||||
|
private String prompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python请求ID
|
||||||
|
*/
|
||||||
|
@TableField("python_request_id")
|
||||||
|
private String pythonRequestId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成状态(0-处理中,1-已完成,2-失败)
|
||||||
|
*/
|
||||||
|
@TableField("generation_status")
|
||||||
|
private Integer generationStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
@TableField("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否收藏(0-否,1-是)
|
||||||
|
*/
|
||||||
|
@TableField("is_favorite")
|
||||||
|
private Integer isFavorite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始试穿效果ID(用于重新生成)
|
||||||
|
*/
|
||||||
|
@TableField("original_try_on_id")
|
||||||
|
private Long originalTryOnId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
86
src/main/java/com/aida/lanecarford/entity/VisitRecord.java
Normal file
86
src/main/java/com/aida/lanecarford/entity/VisitRecord.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package com.aida.lanecarford.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录实体类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("visit_records")
|
||||||
|
public class VisitRecord {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客ID
|
||||||
|
*/
|
||||||
|
@TableField("customer_id")
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购ID
|
||||||
|
*/
|
||||||
|
@TableField("sales_id")
|
||||||
|
private Long salesId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店日期
|
||||||
|
*/
|
||||||
|
@TableField("visit_date")
|
||||||
|
private LocalDate visitDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店时间
|
||||||
|
*/
|
||||||
|
@TableField("visit_time")
|
||||||
|
private LocalDateTime visitTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
@TableField("session_id")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(0-已结束,1-进行中)
|
||||||
|
*/
|
||||||
|
@TableField("status")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
@TableField("notes")
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.aida.lanecarford.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public BusinessException(String code, String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = "BUSINESS_ERROR";
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(String code, String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常用的业务异常静态方法
|
||||||
|
public static BusinessException customerNotFound() {
|
||||||
|
return new BusinessException("CUSTOMER_NOT_FOUND", "客户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException styleOutfitNotFound() {
|
||||||
|
return new BusinessException("STYLE_OUTFIT_NOT_FOUND", "风格搭配不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException modelPhotoNotFound() {
|
||||||
|
return new BusinessException("MODEL_PHOTO_NOT_FOUND", "模特照片不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException virtualTryOnNotFound() {
|
||||||
|
return new BusinessException("VIRTUAL_TRYON_NOT_FOUND", "虚拟试穿记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException favoriteNotFound() {
|
||||||
|
return new BusinessException("FAVORITE_NOT_FOUND", "收藏记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException fileUploadFailed() {
|
||||||
|
return new BusinessException("FILE_UPLOAD_FAILED", "文件上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException invalidFileFormat() {
|
||||||
|
return new BusinessException("INVALID_FILE_FORMAT", "文件格式不支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException fileSizeExceeded() {
|
||||||
|
return new BusinessException("FILE_SIZE_EXCEEDED", "文件大小超过限制");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException emailAlreadyExists() {
|
||||||
|
return new BusinessException("EMAIL_ALREADY_EXISTS", "邮箱已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException phoneAlreadyExists() {
|
||||||
|
return new BusinessException("PHONE_ALREADY_EXISTS", "手机号已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException invalidCredentials() {
|
||||||
|
return new BusinessException("INVALID_CREDENTIALS", "用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException accessDenied() {
|
||||||
|
return new BusinessException("ACCESS_DENIED", "访问被拒绝");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException operationFailed() {
|
||||||
|
return new BusinessException("OPERATION_FAILED", "操作失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException dataNotFound() {
|
||||||
|
return new BusinessException("DATA_NOT_FOUND", "数据不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException duplicateData() {
|
||||||
|
return new BusinessException("DUPLICATE_DATA", "数据重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException invalidParameter(String paramName) {
|
||||||
|
return new BusinessException("INVALID_PARAMETER", "参数 " + paramName + " 无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException serviceUnavailable() {
|
||||||
|
return new BusinessException("SERVICE_UNAVAILABLE", "服务暂时不可用");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessException externalServiceError() {
|
||||||
|
return new BusinessException("EXTERNAL_SERVICE_ERROR", "外部服务调用失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.aida.lanecarford.exception;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局异常处理器
|
||||||
|
* 统一处理应用程序中的各种异常,提供一致的错误响应格式
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
// 错误代码常量
|
||||||
|
private static final String BUSINESS_ERROR = "BUSINESS_ERROR";
|
||||||
|
private static final String VALIDATION_ERROR = "VALIDATION_ERROR";
|
||||||
|
private static final String BIND_ERROR = "BIND_ERROR";
|
||||||
|
private static final String FILE_SIZE_EXCEEDED = "FILE_SIZE_EXCEEDED";
|
||||||
|
private static final String ILLEGAL_ARGUMENT = "ILLEGAL_ARGUMENT";
|
||||||
|
private static final String NULL_POINTER = "NULL_POINTER";
|
||||||
|
private static final String RUNTIME_ERROR = "RUNTIME_ERROR";
|
||||||
|
private static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
||||||
|
private static final String DATABASE_ERROR = "DATABASE_ERROR";
|
||||||
|
private static final String METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
||||||
|
private static final String NOT_FOUND = "NOT_FOUND";
|
||||||
|
private static final String MEDIA_TYPE_NOT_SUPPORTED = "MEDIA_TYPE_NOT_SUPPORTED";
|
||||||
|
private static final String MESSAGE_NOT_READABLE = "MESSAGE_NOT_READABLE";
|
||||||
|
private static final String MISSING_PARAMETER = "MISSING_PARAMETER";
|
||||||
|
private static final String TYPE_MISMATCH = "TYPE_MISMATCH";
|
||||||
|
private static final String CONSTRAINT_VIOLATION = "CONSTRAINT_VIOLATION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理业务异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||||
|
logger.warn("业务异常 [{}]: {} - 请求路径: {}", e.getCode(), e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(BUSINESS_ERROR, e.getMessage(), request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理参数验证异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||||
|
logger.warn("参数验证异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
error -> ((FieldError) error).getField(),
|
||||||
|
error -> error.getDefaultMessage(),
|
||||||
|
(existing, replacement) -> existing
|
||||||
|
));
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(VALIDATION_ERROR, "参数验证失败", request.getRequestURI(), errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理绑定异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BindException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBindException(BindException e, HttpServletRequest request) {
|
||||||
|
logger.warn("绑定异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
error -> ((FieldError) error).getField(),
|
||||||
|
error -> error.getDefaultMessage(),
|
||||||
|
(existing, replacement) -> existing
|
||||||
|
));
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(BIND_ERROR, "数据绑定失败", request.getRequestURI(), errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理约束违反异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||||
|
logger.warn("约束违反异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
|
||||||
|
Map<String, String> errors = violations.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
violation -> violation.getPropertyPath().toString(),
|
||||||
|
ConstraintViolation::getMessage,
|
||||||
|
(existing, replacement) -> existing
|
||||||
|
));
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(CONSTRAINT_VIOLATION, "约束验证失败", request.getRequestURI(), errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件上传大小超限异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
|
||||||
|
logger.warn("文件上传大小超限: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(FILE_SIZE_EXCEEDED, "上传文件大小超过限制", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理非法参数异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
|
||||||
|
logger.warn("非法参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(ILLEGAL_ARGUMENT, "参数错误:" + e.getMessage(), request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理缺少请求参数异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
|
||||||
|
logger.warn("缺少请求参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
String message = String.format("缺少必需的请求参数: %s", e.getParameterName());
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(MISSING_PARAMETER, message, request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理参数类型不匹配异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||||
|
logger.warn("参数类型不匹配异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
String message = String.format("参数 %s 的值 %s 类型不正确", e.getName(), e.getValue());
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(TYPE_MISMATCH, message, request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理HTTP请求方法不支持异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
|
||||||
|
logger.warn("HTTP请求方法不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
String message = String.format("不支持的请求方法: %s", e.getMethod());
|
||||||
|
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||||
|
.body(createErrorResponse(METHOD_NOT_ALLOWED, message, request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理媒体类型不支持异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e, HttpServletRequest request) {
|
||||||
|
logger.warn("媒体类型不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||||
|
.body(createErrorResponse(MEDIA_TYPE_NOT_SUPPORTED, "不支持的媒体类型", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理HTTP消息不可读异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||||
|
logger.warn("HTTP消息不可读异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(createErrorResponse(MESSAGE_NOT_READABLE, "请求体格式错误", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理404异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(NoHandlerFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||||
|
logger.warn("404异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body(createErrorResponse(NOT_FOUND, "请求的资源不存在", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理数据库相关异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler({DataAccessException.class, SQLException.class, DataIntegrityViolationException.class})
|
||||||
|
public ResponseEntity<Map<String, Object>> handleDatabaseException(Exception e, HttpServletRequest request) {
|
||||||
|
logger.error("数据库异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI(), e);
|
||||||
|
|
||||||
|
String message = "数据库操作失败";
|
||||||
|
if (e instanceof DataIntegrityViolationException) {
|
||||||
|
message = "数据完整性约束违反";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(createErrorResponse(DATABASE_ERROR, message, request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理空指针异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(NullPointerException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
|
||||||
|
logger.error("空指针异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(createErrorResponse(NULL_POINTER, "系统内部错误", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理运行时异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||||
|
logger.error("运行时异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(createErrorResponse(RUNTIME_ERROR, "系统运行时错误", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理所有其他异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleException(Exception e, HttpServletRequest request) {
|
||||||
|
logger.error("未知异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(createErrorResponse(UNKNOWN_ERROR, "系统内部错误", request.getRequestURI(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建统一的错误响应格式
|
||||||
|
*/
|
||||||
|
private Map<String, Object> createErrorResponse(String code, String message, String path, Map<String, String> errors) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("code", code);
|
||||||
|
response.put("message", message);
|
||||||
|
response.put("timestamp", System.currentTimeMillis());
|
||||||
|
response.put("path", path);
|
||||||
|
|
||||||
|
if (errors != null && !errors.isEmpty()) {
|
||||||
|
response.put("errors", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Customer;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface CustomerMapper extends BaseMapper<Customer> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.CustomerPhoto;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface CustomerPhotoMapper extends BaseMapper<CustomerPhoto> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.ModelPhoto;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ModelPhotoMapper extends BaseMapper<ModelPhoto> {
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/java/com/aida/lanecarford/mapper/SalesMapper.java
Normal file
16
src/main/java/com/aida/lanecarford/mapper/SalesMapper.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Sales;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface SalesMapper extends BaseMapper<Sales> {
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/java/com/aida/lanecarford/mapper/StyleMapper.java
Normal file
16
src/main/java/com/aida/lanecarford/mapper/StyleMapper.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Style;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface StyleMapper extends BaseMapper<Style> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.TryOnEffect;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TryOnEffectMapper extends BaseMapper<TryOnEffect> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aida.lanecarford.mapper;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.VisitRecord;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录Mapper接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface VisitRecordMapper extends BaseMapper<VisitRecord> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.CustomerPhoto;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface CustomerPhotoService extends IService<CustomerPhoto> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Customer;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface CustomerService extends IService<Customer> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.ModelPhoto;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface ModelPhotoService extends IService<ModelPhoto> {
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/com/aida/lanecarford/service/SalesService.java
Normal file
14
src/main/java/com/aida/lanecarford/service/SalesService.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Sales;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface SalesService extends IService<Sales> {
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/com/aida/lanecarford/service/StyleService.java
Normal file
14
src/main/java/com/aida/lanecarford/service/StyleService.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Style;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface StyleService extends IService<Style> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.TryOnEffect;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface TryOnEffectService extends IService<TryOnEffect> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aida.lanecarford.service;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.VisitRecord;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录服务接口
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public interface VisitRecordService extends IService<VisitRecord> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.CustomerPhoto;
|
||||||
|
import com.aida.lanecarford.mapper.CustomerPhotoMapper;
|
||||||
|
import com.aida.lanecarford.service.CustomerPhotoService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客照片服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerPhotoServiceImpl extends ServiceImpl<CustomerPhotoMapper, CustomerPhoto> implements CustomerPhotoService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Customer;
|
||||||
|
import com.aida.lanecarford.mapper.CustomerMapper;
|
||||||
|
import com.aida.lanecarford.service.CustomerService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顾客服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.ModelPhoto;
|
||||||
|
import com.aida.lanecarford.mapper.ModelPhotoMapper;
|
||||||
|
import com.aida.lanecarford.service.ModelPhotoService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模特照片服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ModelPhotoServiceImpl extends ServiceImpl<ModelPhotoMapper, ModelPhoto> implements ModelPhotoService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Sales;
|
||||||
|
import com.aida.lanecarford.mapper.SalesMapper;
|
||||||
|
import com.aida.lanecarford.service.SalesService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导购服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SalesServiceImpl extends ServiceImpl<SalesMapper, Sales> implements SalesService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.Style;
|
||||||
|
import com.aida.lanecarford.mapper.StyleMapper;
|
||||||
|
import com.aida.lanecarford.service.StyleService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风格配置服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements StyleService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.TryOnEffect;
|
||||||
|
import com.aida.lanecarford.mapper.TryOnEffectMapper;
|
||||||
|
import com.aida.lanecarford.service.TryOnEffectService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试穿效果服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.aida.lanecarford.service.impl;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.entity.VisitRecord;
|
||||||
|
import com.aida.lanecarford.mapper.VisitRecordMapper;
|
||||||
|
import com.aida.lanecarford.service.VisitRecordService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进店记录服务实现类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, VisitRecord> implements VisitRecordService {
|
||||||
|
|
||||||
|
}
|
||||||
194
src/main/java/com/aida/lanecarford/util/BeanUtil.java
Normal file
194
src/main/java/com/aida/lanecarford/util/BeanUtil.java
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package com.aida.lanecarford.util;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bean转换工具类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public class BeanUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个对象转换
|
||||||
|
*
|
||||||
|
* @param source 源对象
|
||||||
|
* @param targetClass 目标类型
|
||||||
|
* @param <T> 目标类型
|
||||||
|
* @return 转换后的对象
|
||||||
|
*/
|
||||||
|
public static <T> T convert(Object source, Class<T> targetClass) {
|
||||||
|
if (source == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
T target = targetClass.getDeclaredConstructor().newInstance();
|
||||||
|
BeanUtils.copyProperties(source, target);
|
||||||
|
return target;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("对象转换失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个对象转换(使用Supplier)
|
||||||
|
*
|
||||||
|
* @param source 源对象
|
||||||
|
* @param targetSupplier 目标对象供应商
|
||||||
|
* @param <T> 目标类型
|
||||||
|
* @return 转换后的对象
|
||||||
|
*/
|
||||||
|
public static <T> T convert(Object source, Supplier<T> targetSupplier) {
|
||||||
|
if (source == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
T target = targetSupplier.get();
|
||||||
|
BeanUtils.copyProperties(source, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表对象转换
|
||||||
|
*
|
||||||
|
* @param sourceList 源列表
|
||||||
|
* @param targetClass 目标类型
|
||||||
|
* @param <T> 目标类型
|
||||||
|
* @return 转换后的列表
|
||||||
|
*/
|
||||||
|
public static <T> List<T> convertList(List<?> sourceList, Class<T> targetClass) {
|
||||||
|
if (CollectionUtils.isEmpty(sourceList)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> targetList = new ArrayList<>(sourceList.size());
|
||||||
|
for (Object source : sourceList) {
|
||||||
|
T target = convert(source, targetClass);
|
||||||
|
if (target != null) {
|
||||||
|
targetList.add(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表对象转换(使用Supplier)
|
||||||
|
*
|
||||||
|
* @param sourceList 源列表
|
||||||
|
* @param targetSupplier 目标对象供应商
|
||||||
|
* @param <T> 目标类型
|
||||||
|
* @return 转换后的列表
|
||||||
|
*/
|
||||||
|
public static <T> List<T> convertList(List<?> sourceList, Supplier<T> targetSupplier) {
|
||||||
|
if (CollectionUtils.isEmpty(sourceList)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> targetList = new ArrayList<>(sourceList.size());
|
||||||
|
for (Object source : sourceList) {
|
||||||
|
T target = convert(source, targetSupplier);
|
||||||
|
if (target != null) {
|
||||||
|
targetList.add(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制属性(忽略null值)
|
||||||
|
*
|
||||||
|
* @param source 源对象
|
||||||
|
* @param target 目标对象
|
||||||
|
*/
|
||||||
|
public static void copyPropertiesIgnoreNull(Object source, Object target) {
|
||||||
|
if (source == null || target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对象中为null的属性名
|
||||||
|
*
|
||||||
|
* @param source 源对象
|
||||||
|
* @return null属性名数组
|
||||||
|
*/
|
||||||
|
private static String[] getNullPropertyNames(Object source) {
|
||||||
|
final java.beans.BeanInfo beanInfo;
|
||||||
|
try {
|
||||||
|
beanInfo = java.beans.Introspector.getBeanInfo(source.getClass());
|
||||||
|
} catch (java.beans.IntrospectionException e) {
|
||||||
|
throw new RuntimeException("获取Bean信息失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final java.beans.PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
|
||||||
|
final java.util.Set<String> emptyNames = new java.util.HashSet<>();
|
||||||
|
|
||||||
|
for (java.beans.PropertyDescriptor pd : propertyDescriptors) {
|
||||||
|
final java.lang.reflect.Method readMethod = pd.getReadMethod();
|
||||||
|
if (readMethod != null) {
|
||||||
|
try {
|
||||||
|
final Object value = readMethod.invoke(source);
|
||||||
|
if (value == null) {
|
||||||
|
emptyNames.add(pd.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyNames.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断对象是否为空
|
||||||
|
*
|
||||||
|
* @param obj 对象
|
||||||
|
* @return 是否为空
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof String) {
|
||||||
|
return ((String) obj).trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof java.util.Collection) {
|
||||||
|
return ((java.util.Collection<?>) obj).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof java.util.Map) {
|
||||||
|
return ((java.util.Map<?, ?>) obj).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.getClass().isArray()) {
|
||||||
|
return java.lang.reflect.Array.getLength(obj) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断对象是否不为空
|
||||||
|
*
|
||||||
|
* @param obj 对象
|
||||||
|
* @return 是否不为空
|
||||||
|
*/
|
||||||
|
public static boolean isNotEmpty(Object obj) {
|
||||||
|
return !isEmpty(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
262
src/main/java/com/aida/lanecarford/util/FileUtil.java
Normal file
262
src/main/java/com/aida/lanecarford/util/FileUtil.java
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
package com.aida.lanecarford.util;
|
||||||
|
|
||||||
|
import com.aida.lanecarford.exception.BusinessException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件工具类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public class FileUtil {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的图片格式
|
||||||
|
*/
|
||||||
|
private static final List<String> SUPPORTED_IMAGE_FORMATS = Arrays.asList(
|
||||||
|
"jpg", "jpeg", "png", "gif", "bmp", "webp"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大文件大小(10MB)
|
||||||
|
*/
|
||||||
|
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传根目录
|
||||||
|
*/
|
||||||
|
private static final String UPLOAD_ROOT_DIR = "uploads";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件是否为图片
|
||||||
|
*/
|
||||||
|
public static boolean isImageFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = getFileExtension(originalFilename).toLowerCase();
|
||||||
|
return SUPPORTED_IMAGE_FORMATS.contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件大小
|
||||||
|
*/
|
||||||
|
public static boolean isValidFileSize(MultipartFile file) {
|
||||||
|
return file != null && file.getSize() <= MAX_FILE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
*/
|
||||||
|
public static String getFileExtension(String filename) {
|
||||||
|
if (filename == null || filename.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename.substring(lastDotIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一文件名
|
||||||
|
*/
|
||||||
|
public static String generateUniqueFileName(String originalFilename) {
|
||||||
|
String extension = getFileExtension(originalFilename);
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||||
|
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
||||||
|
return timestamp + "_" + uuid + "." + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建目录结构
|
||||||
|
*/
|
||||||
|
public static String createDirectoryStructure(String category) {
|
||||||
|
String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||||
|
String fullPath = UPLOAD_ROOT_DIR + File.separator + category + File.separator + dateDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path path = Paths.get(fullPath);
|
||||||
|
Files.createDirectories(path);
|
||||||
|
return fullPath;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("创建目录失败: {}", fullPath, e);
|
||||||
|
throw BusinessException.fileUploadFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文件
|
||||||
|
*/
|
||||||
|
public static String saveFile(MultipartFile file, String category) {
|
||||||
|
// 验证文件
|
||||||
|
validateFile(file);
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
String directoryPath = createDirectoryStructure(category);
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
String filename = generateUniqueFileName(file.getOriginalFilename());
|
||||||
|
|
||||||
|
// 完整文件路径
|
||||||
|
String fullPath = directoryPath + File.separator + filename;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 保存文件
|
||||||
|
Path filePath = Paths.get(fullPath);
|
||||||
|
Files.write(filePath, file.getBytes());
|
||||||
|
|
||||||
|
logger.info("文件保存成功: {}", fullPath);
|
||||||
|
|
||||||
|
// 返回相对路径(用于数据库存储和访问)
|
||||||
|
return fullPath.replace(File.separator, "/");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("文件保存失败: {}", fullPath, e);
|
||||||
|
throw BusinessException.fileUploadFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
*/
|
||||||
|
public static boolean deleteFile(String filePath) {
|
||||||
|
if (filePath == null || filePath.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||||
|
boolean deleted = Files.deleteIfExists(path);
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
logger.info("文件删除成功: {}", filePath);
|
||||||
|
} else {
|
||||||
|
logger.warn("文件不存在或删除失败: {}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("文件删除失败: {}", filePath, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
*/
|
||||||
|
public static boolean fileExists(String filePath) {
|
||||||
|
if (filePath == null || filePath.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||||
|
return Files.exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件大小(字节)
|
||||||
|
*/
|
||||||
|
public static long getFileSize(String filePath) {
|
||||||
|
if (!fileExists(filePath)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||||
|
return Files.size(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("获取文件大小失败: {}", filePath, e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
*/
|
||||||
|
public static String formatFileSize(long size) {
|
||||||
|
if (size < 1024) {
|
||||||
|
return size + " B";
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return String.format("%.1f KB", size / 1024.0);
|
||||||
|
} else if (size < 1024 * 1024 * 1024) {
|
||||||
|
return String.format("%.1f MB", size / (1024.0 * 1024.0));
|
||||||
|
} else {
|
||||||
|
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件
|
||||||
|
*/
|
||||||
|
private static void validateFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw BusinessException.invalidParameter("file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isImageFile(file)) {
|
||||||
|
throw BusinessException.invalidFileFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidFileSize(file)) {
|
||||||
|
throw BusinessException.fileSizeExceeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的MIME类型
|
||||||
|
*/
|
||||||
|
public static String getContentType(String filename) {
|
||||||
|
String extension = getFileExtension(filename).toLowerCase();
|
||||||
|
|
||||||
|
switch (extension) {
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
return "image/jpeg";
|
||||||
|
case "png":
|
||||||
|
return "image/png";
|
||||||
|
case "gif":
|
||||||
|
return "image/gif";
|
||||||
|
case "bmp":
|
||||||
|
return "image/bmp";
|
||||||
|
case "webp":
|
||||||
|
return "image/webp";
|
||||||
|
default:
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期文件(可用于定时任务)
|
||||||
|
*/
|
||||||
|
public static void cleanupExpiredFiles(String directoryPath, int daysToKeep) {
|
||||||
|
// TODO: 实现文件清理逻辑
|
||||||
|
logger.info("清理过期文件: {} 天前的文件", daysToKeep);
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/main/java/com/aida/lanecarford/util/ValidationUtil.java
Normal file
246
src/main/java/com/aida/lanecarford/util/ValidationUtil.java
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package com.aida.lanecarford.util;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证工具类
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public class ValidationUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱正则表达式
|
||||||
|
*/
|
||||||
|
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||||
|
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号正则表达式(中国大陆)
|
||||||
|
*/
|
||||||
|
private static final Pattern PHONE_PATTERN = Pattern.compile(
|
||||||
|
"^1[3-9]\\d{9}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码正则表达式(至少8位,包含字母和数字)
|
||||||
|
*/
|
||||||
|
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
|
||||||
|
"^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名正则表达式(4-20位字母、数字、下划线)
|
||||||
|
*/
|
||||||
|
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||||
|
"^[a-zA-Z0-9_]{4,20}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱格式
|
||||||
|
*/
|
||||||
|
public static boolean isValidEmail(String email) {
|
||||||
|
return email != null && EMAIL_PATTERN.matcher(email).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证手机号格式
|
||||||
|
*/
|
||||||
|
public static boolean isValidPhone(String phone) {
|
||||||
|
return phone != null && PHONE_PATTERN.matcher(phone).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码强度
|
||||||
|
*/
|
||||||
|
public static boolean isValidPassword(String password) {
|
||||||
|
return password != null && PASSWORD_PATTERN.matcher(password).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户名格式
|
||||||
|
*/
|
||||||
|
public static boolean isValidUsername(String username) {
|
||||||
|
return username != null && USERNAME_PATTERN.matcher(username).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证字符串是否为空或null
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(String str) {
|
||||||
|
return str == null || str.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证字符串是否不为空
|
||||||
|
*/
|
||||||
|
public static boolean isNotEmpty(String str) {
|
||||||
|
return !isEmpty(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证字符串长度是否在指定范围内
|
||||||
|
*/
|
||||||
|
public static boolean isValidLength(String str, int minLength, int maxLength) {
|
||||||
|
if (str == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int length = str.length();
|
||||||
|
return length >= minLength && length <= maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证数字是否在指定范围内
|
||||||
|
*/
|
||||||
|
public static boolean isInRange(Number number, Number min, Number max) {
|
||||||
|
if (number == null || min == null || max == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double value = number.doubleValue();
|
||||||
|
double minValue = min.doubleValue();
|
||||||
|
double maxValue = max.doubleValue();
|
||||||
|
return value >= minValue && value <= maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证是否为正整数
|
||||||
|
*/
|
||||||
|
public static boolean isPositiveInteger(Number number) {
|
||||||
|
if (number == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return number.longValue() > 0 && number.doubleValue() == number.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证是否为非负整数
|
||||||
|
*/
|
||||||
|
public static boolean isNonNegativeInteger(Number number) {
|
||||||
|
if (number == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return number.longValue() >= 0 && number.doubleValue() == number.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证身份证号格式(简单验证)
|
||||||
|
*/
|
||||||
|
public static boolean isValidIdCard(String idCard) {
|
||||||
|
if (isEmpty(idCard)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 18位身份证号
|
||||||
|
if (idCard.length() == 18) {
|
||||||
|
return Pattern.matches("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$", idCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15位身份证号
|
||||||
|
if (idCard.length() == 15) {
|
||||||
|
return Pattern.matches("^[1-9]\\d{5}\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}$", idCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证URL格式
|
||||||
|
*/
|
||||||
|
public static boolean isValidUrl(String url) {
|
||||||
|
if (isEmpty(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^(https?|ftp)://[^\\s/$.?#].[^\\s]*$", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证IP地址格式
|
||||||
|
*/
|
||||||
|
public static boolean isValidIpAddress(String ip) {
|
||||||
|
if (isEmpty(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$", ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证颜色代码格式(十六进制)
|
||||||
|
*/
|
||||||
|
public static boolean isValidColorCode(String colorCode) {
|
||||||
|
if (isEmpty(colorCode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", colorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证日期格式(yyyy-MM-dd)
|
||||||
|
*/
|
||||||
|
public static boolean isValidDateFormat(String date) {
|
||||||
|
if (isEmpty(date)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^\\d{4}-\\d{2}-\\d{2}$", date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证时间格式(HH:mm:ss)
|
||||||
|
*/
|
||||||
|
public static boolean isValidTimeFormat(String time) {
|
||||||
|
if (isEmpty(time)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^([01]?\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证数字字符串
|
||||||
|
*/
|
||||||
|
public static boolean isNumeric(String str) {
|
||||||
|
if (isEmpty(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^-?\\d+(\\.\\d+)?$", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证只包含字母
|
||||||
|
*/
|
||||||
|
public static boolean isAlpha(String str) {
|
||||||
|
if (isEmpty(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^[a-zA-Z]+$", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证只包含字母和数字
|
||||||
|
*/
|
||||||
|
public static boolean isAlphanumeric(String str) {
|
||||||
|
if (isEmpty(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^[a-zA-Z0-9]+$", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证中文字符
|
||||||
|
*/
|
||||||
|
public static boolean isChinese(String str) {
|
||||||
|
if (isEmpty(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches("^[\\u4e00-\\u9fa5]+$", str);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/resources/application.yml
Normal file
50
src/main/resources/application.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Spring Boot 应用配置
|
||||||
|
spring:
|
||||||
|
# 应用基本信息
|
||||||
|
application:
|
||||||
|
name: lanecarford-ai-styling-assistant
|
||||||
|
|
||||||
|
# 数据源配置 - MySQL
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
# HikariCP 连接池配置
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 20
|
||||||
|
minimum-idle: 5
|
||||||
|
idle-timeout: 300000
|
||||||
|
max-lifetime: 1200000
|
||||||
|
connection-timeout: 20000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# MyBatis-Plus 配置
|
||||||
|
mybatis-plus:
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
logic-delete-field: deleted
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
|
mapper-locations: classpath*:/mapper/**/*.xml
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
# Swagger 文档配置
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.aida.lanecarford: DEBUG
|
||||||
146
src/main/resources/sql/schema.sql
Normal file
146
src/main/resources/sql/schema.sql
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
-- Lane Carford AI系统基础架构数据库表结构
|
||||||
|
-- 创建数据库
|
||||||
|
CREATE DATABASE IF NOT EXISTS lanecarford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE lanecarford;
|
||||||
|
|
||||||
|
-- 1. 导购表
|
||||||
|
CREATE TABLE sales (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '导购ID',
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||||
|
password VARCHAR(255) NOT NULL COMMENT '密码(加密后)',
|
||||||
|
real_name VARCHAR(100) NOT NULL COMMENT '真实姓名',
|
||||||
|
employee_id VARCHAR(50) UNIQUE COMMENT '员工编号',
|
||||||
|
store_id VARCHAR(50) COMMENT '门店ID',
|
||||||
|
store_name VARCHAR(100) COMMENT '门店名称',
|
||||||
|
phone VARCHAR(20) COMMENT '手机号',
|
||||||
|
email VARCHAR(100) COMMENT '邮箱',
|
||||||
|
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_employee_id (employee_id),
|
||||||
|
INDEX idx_store_id (store_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导购表';
|
||||||
|
|
||||||
|
-- 2. 顾客表
|
||||||
|
CREATE TABLE customers (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客ID',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '顾客姓名',
|
||||||
|
email VARCHAR(100) NOT NULL COMMENT '顾客邮箱',
|
||||||
|
phone VARCHAR(20) COMMENT '手机号',
|
||||||
|
gender VARCHAR(10) COMMENT '性别',
|
||||||
|
age_range VARCHAR(20) COMMENT '年龄段',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_phone (phone),
|
||||||
|
INDEX idx_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客表';
|
||||||
|
|
||||||
|
-- 3. 进店记录表
|
||||||
|
CREATE TABLE visit_records (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '进店记录ID',
|
||||||
|
customer_id BIGINT NOT NULL COMMENT '顾客ID',
|
||||||
|
sales_id BIGINT NOT NULL COMMENT '导购ID',
|
||||||
|
visit_date DATE NOT NULL COMMENT '进店日期',
|
||||||
|
visit_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '进店时间',
|
||||||
|
session_id VARCHAR(100) COMMENT '会话ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态(0-已结束,1-进行中)',
|
||||||
|
notes TEXT COMMENT '备注',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (sales_id) REFERENCES sales(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_customer_id (customer_id),
|
||||||
|
INDEX idx_sales_id (sales_id),
|
||||||
|
INDEX idx_visit_date (visit_date),
|
||||||
|
INDEX idx_session_id (session_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='进店记录表';
|
||||||
|
|
||||||
|
-- 4. 风格配置表
|
||||||
|
CREATE TABLE styles (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '风格配置ID',
|
||||||
|
customer_id BIGINT NOT NULL COMMENT '顾客ID',
|
||||||
|
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
|
||||||
|
is_selected TINYINT DEFAULT 0 COMMENT '是否选中(0-未选中,1-已选中)',
|
||||||
|
style_image_url VARCHAR(500) COMMENT '风格图片URL',
|
||||||
|
python_request_id VARCHAR(100) COMMENT 'Python请求ID',
|
||||||
|
generation_status TINYINT DEFAULT 0 COMMENT '生成状态(0-处理中,1-已完成,2-失败)',
|
||||||
|
error_message TEXT COMMENT '错误信息',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_customer_id (customer_id),
|
||||||
|
INDEX idx_visit_record_id (visit_record_id),
|
||||||
|
INDEX idx_python_request_id (python_request_id),
|
||||||
|
INDEX idx_is_selected (is_selected),
|
||||||
|
INDEX idx_generation_status (generation_status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='风格配置表';
|
||||||
|
|
||||||
|
-- 5. 模特照片表
|
||||||
|
CREATE TABLE model_photos (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '模特照片ID',
|
||||||
|
photo_url VARCHAR(500) NOT NULL COMMENT '模特照片URL',
|
||||||
|
photo_name VARCHAR(200) COMMENT '照片名称',
|
||||||
|
gender VARCHAR(10) NOT NULL COMMENT '性别',
|
||||||
|
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序权重',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_gender (gender),
|
||||||
|
INDEX idx_is_active (is_active),
|
||||||
|
INDEX idx_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模特照片表';
|
||||||
|
|
||||||
|
-- 6. 顾客照片表
|
||||||
|
CREATE TABLE customer_photos (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客照片ID',
|
||||||
|
customer_id BIGINT NOT NULL COMMENT '顾客ID',
|
||||||
|
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
|
||||||
|
photo_url VARCHAR(500) NOT NULL COMMENT '照片URL',
|
||||||
|
is_primary TINYINT DEFAULT 0 COMMENT '是否为主照片(0-否,1-是)',
|
||||||
|
upload_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_customer_id (customer_id),
|
||||||
|
INDEX idx_visit_record_id (visit_record_id),
|
||||||
|
INDEX idx_is_primary (is_primary)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客照片表';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 8. 试穿效果表
|
||||||
|
CREATE TABLE try_on_effects (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '试穿效果ID',
|
||||||
|
customer_id BIGINT NOT NULL COMMENT '顾客ID',
|
||||||
|
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
|
||||||
|
styles_id BIGINT NOT NULL COMMENT '风格ID',
|
||||||
|
model_photo_id BIGINT COMMENT '模特照片ID',
|
||||||
|
customer_photo_id BIGINT COMMENT '顾客照片ID',
|
||||||
|
prompt VARCHAR(500) COMMENT '提示词',
|
||||||
|
original_try_on_id BIGINT COMMENT '原试穿效果ID,当is_regenerated为1时才会有值',
|
||||||
|
is_regenerated TINYINT DEFAULT 0 COMMENT '是否由生成结果重新生成(0-否,1-是)',
|
||||||
|
result_image_url VARCHAR(500) COMMENT '试穿结果图片URL',
|
||||||
|
request_id VARCHAR(100) COMMENT '请求ID',
|
||||||
|
generation_status VARCHAR(20) DEFAULT 'pending' COMMENT '生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)',
|
||||||
|
error_message TEXT COMMENT '错误信息',
|
||||||
|
is_favorite TINYINT DEFAULT 0 COMMENT '是否喜欢的最终造型(0-否,1-是)',
|
||||||
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (styles_id) REFERENCES styles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (model_photo_id) REFERENCES model_photos(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (customer_photo_id) REFERENCES customer_photos(id) ON DELETE SET NULL,
|
||||||
|
INDEX idx_customer_id (customer_id),
|
||||||
|
INDEX idx_visit_record_id (visit_record_id),
|
||||||
|
INDEX idx_styles_id (styles_id),
|
||||||
|
INDEX idx_request_id (request_id),
|
||||||
|
INDEX idx_generation_status (generation_status),
|
||||||
|
INDEX idx_is_favorite (is_favorite)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='试穿效果表';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.aida.lanecarford;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class LanecarfordApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user