diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1e0059d --- /dev/null +++ b/ARCHITECTURE.md @@ -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 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 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` + +**使用示例**: +```java +@RestController +@RequestMapping("/api/products") +public class ProductController { + + // 成功响应 - 返回数据 + @GetMapping("/{id}") + public ApiResponse getProduct(@PathVariable Long id) { + Product product = productService.findById(id); + return ApiResponse.success("查询成功", product); + } + + // 成功响应 - 无数据 + @DeleteMapping("/{id}") + public ApiResponse deleteProduct(@PathVariable Long id) { + productService.deleteById(id); + return ApiResponse.success("删除成功"); + } + + // 分页查询响应 + @GetMapping + public ApiResponse> getProducts( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + PageResult result = productService.findByPage(page, size); + return ApiResponse.success("查询成功", result); + } + + // 错误响应 + @PostMapping + public ApiResponse 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` + +**使用示例**: +```java +@Service +public class ProductService { + + @Autowired + private ProductMapper productMapper; + + public PageResult findByPage(int page, int size) { + // 使用MyBatis-Plus分页 + Page pageParam = new Page<>(page, size); + Page result = productMapper.selectPage(pageParam, null); + + return PageResult.builder() + .records(result.getRecords()) + .total(result.getTotal()) + .current(result.getCurrent()) + .size(result.getSize()) + .pages(result.getPages()) + .build(); + } + + // 带条件的分页查询 + public PageResult findByCondition(ProductQueryRequest request) { + Page pageParam = new Page<>(request.getPage(), request.getSize()); + + QueryWrapper 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 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 convertToDTOList(List 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 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 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 findExpensiveProducts() { + // 如果这个方法执行超过5秒,会自动记录警告日志 + return productMapper.selectExpensiveProducts(); + } + + public void batchUpdateProducts(List products) { + // 批量操作可能耗时较长,会被监控 + for (Product product : products) { + productMapper.updateById(product); + } + } +} + +@Repository +public interface ProductMapper extends BaseMapper { + + // 复杂查询可能耗时较长,会被监控 + @Select("SELECT * FROM products WHERE price > 1000 ORDER BY price DESC") + List 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`, `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 { + + // 自定义查询方法 + @Select("SELECT * FROM products WHERE category_id = #{categoryId} AND status = 1") + List 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 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 findByCondition(String name, BigDecimal minPrice, Integer status) { + QueryWrapper 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 findByPage(int page, int size, String keyword) { + Page pageParam = new Page<>(page, size); + + QueryWrapper 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 result = productMapper.selectPage(pageParam, queryWrapper); + return PageResult.fromPage(result); + } + + // 批量操作 + public void batchInsert(List 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 result = ApiResponse.success("登录成功", getUserInfo(authentication)); + response.getWriter().write(JSON.toJSONString(result)); + }; + } +} + +// 用户认证Controller +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @PostMapping("/login") + public ApiResponse 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 logout(HttpServletRequest request) { + SecurityContextHolder.clearContext(); + request.getSession().invalidate(); + return ApiResponse.success("退出成功"); + } + + @GetMapping("/current") + public ApiResponse 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 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 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> 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 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 { + // 自定义查询方法 +} +``` + +### 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 +``` + +这个架构模板提供了完整的企业级应用基础设施,您可以在此基础上快速开发各种类型的业务应用! \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -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-,maven-mvnd--}/ +[ -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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -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-,maven-mvnd--}/ +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" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..98f4870 --- /dev/null +++ b/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.6 + + + com.aida + lanecarford + 0.0.1-SNAPSHOT + lanecarford + Demo project for Spring Boot + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.5 + + + + + + + com.mysql + mysql-connector-j + 8.2.0 + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.security + spring-security-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + 21 + 21 + 21 + UTF-8 + + --enable-preview + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/aida/lanecarford/LanecarfordApplication.java b/src/main/java/com/aida/lanecarford/LanecarfordApplication.java new file mode 100644 index 0000000..ffa0845 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/LanecarfordApplication.java @@ -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."); + } + +} diff --git a/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java b/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java new file mode 100644 index 0000000..8c7e272 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java b/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java new file mode 100644 index 0000000..be3394d --- /dev/null +++ b/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/common/ApiResponse.java b/src/main/java/com/aida/lanecarford/common/ApiResponse.java new file mode 100644 index 0000000..491ce98 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/ApiResponse.java @@ -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 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 ApiResponse success() { + return new ApiResponse<>(true, "SUCCESS", "操作成功", null); + } + + /** + * 成功响应(带数据) + */ + public static ApiResponse success(T data) { + return new ApiResponse<>(true, "SUCCESS", "操作成功", data); + } + + /** + * 成功响应(自定义消息,无数据) + */ + public static ApiResponse success(String message) { + return new ApiResponse<>(true, "SUCCESS", message, null); + } + + /** + * 成功响应(自定义消息) + */ + public static ApiResponse success(String message, T data) { + return new ApiResponse<>(true, "SUCCESS", message, data); + } + + /** + * 失败响应 + */ + public static ApiResponse error(String code, String message) { + return new ApiResponse<>(false, code, message, null); + } + + /** + * 失败响应(默认错误码) + */ + public static ApiResponse error(String message) { + return new ApiResponse<>(false, "ERROR", message, null); + } + + /** + * 参数错误响应 + */ + public static ApiResponse paramError(String message) { + return new ApiResponse<>(false, "PARAM_ERROR", message, null); + } + + /** + * 数据不存在响应 + */ + public static ApiResponse notFound(String message) { + return new ApiResponse<>(false, "NOT_FOUND", message, null); + } + + /** + * 权限不足响应 + */ + public static ApiResponse forbidden(String message) { + return new ApiResponse<>(false, "FORBIDDEN", message, null); + } + + /** + * 服务器内部错误响应 + */ + public static ApiResponse serverError(String message) { + return new ApiResponse<>(false, "SERVER_ERROR", message, null); + } + + /** + * 业务异常响应 + */ + public static ApiResponse businessError(String code, String message) { + return new ApiResponse<>(false, code, message, null); + } + + /** + * 外部服务错误响应 + */ + public static ApiResponse externalServiceError(String message) { + return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null); + } + + /** + * 文件上传错误响应 + */ + public static ApiResponse fileUploadError(String message) { + return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null); + } + + /** + * 验证失败响应 + */ + public static ApiResponse validationError(String message) { + return new ApiResponse<>(false, "VALIDATION_ERROR", message, null); + } + + /** + * 重复数据响应 + */ + public static ApiResponse duplicateError(String message) { + return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null); + } + + /** + * 操作超时响应 + */ + public static ApiResponse timeoutError(String message) { + return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/common/PageResult.java b/src/main/java/com/aida/lanecarford/common/PageResult.java new file mode 100644 index 0000000..feb6a8b --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/PageResult.java @@ -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 implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 数据列表 + */ + private List records; + + /** + * 总记录数 + */ + private long total; + + /** + * 当前页码 + */ + private long current; + + /** + * 每页大小 + */ + private long size; + + /** + * 总页数 + */ + private long pages; + + /** + * 是否有上一页 + */ + private boolean hasPrevious; + + /** + * 是否有下一页 + */ + private boolean hasNext; + + public PageResult() { + } + + public PageResult(List 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 PageResult of(IPage page) { + return new PageResult<>( + page.getRecords(), + page.getTotal(), + page.getCurrent(), + page.getSize() + ); + } + + /** + * 创建空的分页结果 + */ + public static PageResult empty() { + return new PageResult<>(List.of(), 0, 1, 10); + } + + /** + * 创建空的分页结果(指定页码和大小) + */ + public static PageResult empty(long current, long size) { + return new PageResult<>(List.of(), 0, current, size); + } + + /** + * 创建单页结果 + */ + public static PageResult of(List 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/CacheConfig.java b/src/main/java/com/aida/lanecarford/config/CacheConfig.java new file mode 100644 index 0000000..2572e90 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/CacheConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/FileUploadConfig.java b/src/main/java/com/aida/lanecarford/config/FileUploadConfig.java new file mode 100644 index 0000000..bea3038 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/FileUploadConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/LoggingConfig.java b/src/main/java/com/aida/lanecarford/config/LoggingConfig.java new file mode 100644 index 0000000..9e34760 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/LoggingConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/MyBatisPlusConfig.java b/src/main/java/com/aida/lanecarford/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..68a3973 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/MyBatisPlusConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/PerformanceConfig.java b/src/main/java/com/aida/lanecarford/config/PerformanceConfig.java new file mode 100644 index 0000000..ad669f5 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/PerformanceConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/RestTemplateConfig.java b/src/main/java/com/aida/lanecarford/config/RestTemplateConfig.java new file mode 100644 index 0000000..8e1a889 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/RestTemplateConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/SecurityConfig.java b/src/main/java/com/aida/lanecarford/config/SecurityConfig.java new file mode 100644 index 0000000..9296291 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/SecurityConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java b/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java new file mode 100644 index 0000000..cc6acc0 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java @@ -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 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 createSecurityRequirements() { + return Arrays.asList( + new SecurityRequirement().addList("sessionAuth"), + new SecurityRequirement().addList("basicAuth") + ); + } + + /** + * 创建API标签分类 + */ + private List 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"); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/WebConfig.java b/src/main/java/com/aida/lanecarford/config/WebConfig.java new file mode 100644 index 0000000..5fe4125 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/WebConfig.java @@ -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/"); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/CustomerController.java b/src/main/java/com/aida/lanecarford/controller/CustomerController.java new file mode 100644 index 0000000..e2d2435 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/CustomerController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java b/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java new file mode 100644 index 0000000..a8bcf2e --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/ModelPhotoController.java b/src/main/java/com/aida/lanecarford/controller/ModelPhotoController.java new file mode 100644 index 0000000..c2461e8 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/ModelPhotoController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/SalesController.java b/src/main/java/com/aida/lanecarford/controller/SalesController.java new file mode 100644 index 0000000..94736cb --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/SalesController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/StyleController.java b/src/main/java/com/aida/lanecarford/controller/StyleController.java new file mode 100644 index 0000000..ec08b9c --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/StyleController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java new file mode 100644 index 0000000..128a9be --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java new file mode 100644 index 0000000..14be7d4 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/dto/BaseDTO.java b/src/main/java/com/aida/lanecarford/dto/BaseDTO.java new file mode 100644 index 0000000..c4175d7 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/BaseDTO.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/dto/BaseRequest.java b/src/main/java/com/aida/lanecarford/dto/BaseRequest.java new file mode 100644 index 0000000..6d8e6df --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/BaseRequest.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/BaseEntity.java b/src/main/java/com/aida/lanecarford/entity/BaseEntity.java new file mode 100644 index 0000000..04ca019 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/BaseEntity.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/Customer.java b/src/main/java/com/aida/lanecarford/entity/Customer.java new file mode 100644 index 0000000..851f381 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/Customer.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java new file mode 100644 index 0000000..e2a0799 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java b/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java new file mode 100644 index 0000000..92d39cc --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/Sales.java b/src/main/java/com/aida/lanecarford/entity/Sales.java new file mode 100644 index 0000000..cf61be6 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/Sales.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/Style.java b/src/main/java/com/aida/lanecarford/entity/Style.java new file mode 100644 index 0000000..00d5f8b --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/Style.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java new file mode 100644 index 0000000..5909738 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/VisitRecord.java b/src/main/java/com/aida/lanecarford/entity/VisitRecord.java new file mode 100644 index 0000000..dd087e2 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/VisitRecord.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/exception/BusinessException.java b/src/main/java/com/aida/lanecarford/exception/BusinessException.java new file mode 100644 index 0000000..e39b7f8 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/exception/BusinessException.java @@ -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", "外部服务调用失败"); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java b/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f403b64 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java @@ -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> 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> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) { + logger.warn("参数验证异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); + + Map 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> handleBindException(BindException e, HttpServletRequest request) { + logger.warn("绑定异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); + + Map 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> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) { + logger.warn("约束违反异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); + + Set> violations = e.getConstraintViolations(); + Map 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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 createErrorResponse(String code, String message, String path, Map errors) { + Map 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/CustomerMapper.java b/src/main/java/com/aida/lanecarford/mapper/CustomerMapper.java new file mode 100644 index 0000000..cd3f24d --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/CustomerMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/CustomerPhotoMapper.java b/src/main/java/com/aida/lanecarford/mapper/CustomerPhotoMapper.java new file mode 100644 index 0000000..28a1caa --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/CustomerPhotoMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/ModelPhotoMapper.java b/src/main/java/com/aida/lanecarford/mapper/ModelPhotoMapper.java new file mode 100644 index 0000000..94c8bc5 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/ModelPhotoMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/SalesMapper.java b/src/main/java/com/aida/lanecarford/mapper/SalesMapper.java new file mode 100644 index 0000000..956b3fe --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/SalesMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/StyleMapper.java b/src/main/java/com/aida/lanecarford/mapper/StyleMapper.java new file mode 100644 index 0000000..f27af4b --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/StyleMapper.java @@ -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