first commit

This commit is contained in:
zchengrong
2025-10-20 16:13:39 +08:00
parent 8cf82c52c1
commit af8fbd90e3
67 changed files with 5127 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

3
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip

708
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,708 @@
# Lane Carford 基础架构功能说明
## 🏗️ 架构概述
这是一个基于 **Spring Boot 3.1.6****Java 21** 的纯净架构模板,提供了完整的企业级应用基础设施,可以快速开发各种类型的业务应用。
## 📋 核心功能清单
### 1. 🛡️ 全局异常处理
**功能描述**: 统一处理应用程序中的各种异常,提供一致的错误响应格式
**实现类**: `GlobalExceptionHandler`
**支持的异常类型**:
- 业务异常 (`BusinessException`)
- 参数验证异常 (`MethodArgumentNotValidException`)
- 文件上传大小超限异常 (`MaxUploadSizeExceededException`)
- 数据库操作异常 (`DataAccessException`, `SQLException`)
- HTTP方法不支持异常 (`HttpRequestMethodNotSupportedException`)
- 404异常 (`NoHandlerFoundException`)
- 运行时异常 (`RuntimeException`)
- 其他通用异常
**使用示例**:
```java
// 1. 创建自定义业务异常
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ApiResponse<User> createUser(@Valid @RequestBody CreateUserRequest request) {
// 业务逻辑验证
if (userService.existsByEmail(request.getEmail())) {
throw new BusinessException("USER_EXISTS", "用户邮箱已存在");
}
User user = userService.createUser(request);
return ApiResponse.success("用户创建成功", user);
}
@GetMapping("/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
}
return ApiResponse.success(user);
}
}
// 2. 参数验证异常自动处理
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 100, message = "年龄不能大于100岁")
private Integer age;
}
// 异常响应格式示例:
{
"success": false,
"code": "VALIDATION_ERROR",
"message": "参数验证失败",
"timestamp": 1640995200000,
"path": "/api/users",
"errors": {
"username": "用户名不能为空",
"email": "邮箱格式不正确"
}
}
```
### 2. 📤 统一API响应格式
**功能描述**: 提供标准化的API响应格式确保前后端接口的一致性
**实现类**: `ApiResponse<T>`
**使用示例**:
```java
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 成功响应 - 返回数据
@GetMapping("/{id}")
public ApiResponse<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ApiResponse.success("查询成功", product);
}
// 成功响应 - 无数据
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteProduct(@PathVariable Long id) {
productService.deleteById(id);
return ApiResponse.success("删除成功");
}
// 分页查询响应
@GetMapping
public ApiResponse<PageResult<Product>> getProducts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
PageResult<Product> result = productService.findByPage(page, size);
return ApiResponse.success("查询成功", result);
}
// 错误响应
@PostMapping
public ApiResponse<Product> createProduct(@Valid @RequestBody CreateProductRequest request) {
if (productService.existsByName(request.getName())) {
return ApiResponse.businessError("PRODUCT_EXISTS", "产品名称已存在");
}
Product product = productService.create(request);
return ApiResponse.success("创建成功", product);
}
}
// 响应格式示例:
{
"success": true,
"code": "SUCCESS",
"message": "查询成功",
"data": {
"id": 1,
"name": "产品名称",
"price": 99.99
},
"timestamp": 1640995200000
}
```
### 3. 📊 分页查询支持
**功能描述**: 提供标准化的分页查询结果封装
**实现类**: `PageResult<T>`
**使用示例**:
```java
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public PageResult<Product> findByPage(int page, int size) {
// 使用MyBatis-Plus分页
Page<Product> pageParam = new Page<>(page, size);
Page<Product> result = productMapper.selectPage(pageParam, null);
return PageResult.<Product>builder()
.records(result.getRecords())
.total(result.getTotal())
.current(result.getCurrent())
.size(result.getSize())
.pages(result.getPages())
.build();
}
// 带条件的分页查询
public PageResult<Product> findByCondition(ProductQueryRequest request) {
Page<Product> pageParam = new Page<>(request.getPage(), request.getSize());
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(request.getName())) {
queryWrapper.like("name", request.getName());
}
if (request.getMinPrice() != null) {
queryWrapper.ge("price", request.getMinPrice());
}
if (request.getMaxPrice() != null) {
queryWrapper.le("price", request.getMaxPrice());
}
Page<Product> result = productMapper.selectPage(pageParam, queryWrapper);
return PageResult.fromPage(result);
}
}
```
### 4. 🔄 Bean转换工具
**功能描述**: 提供对象之间的转换功能简化DTO和Entity之间的转换
**实现类**: `BeanUtil`
**使用示例**:
```java
@Service
public class UserService {
// 单个对象转换
public UserDTO convertToDTO(User user) {
return BeanUtil.convert(user, UserDTO.class);
}
// 列表转换
public List<UserDTO> convertToDTOList(List<User> users) {
return BeanUtil.convertList(users, UserDTO.class);
}
// 使用Supplier转换适用于复杂对象
public UserDetailDTO convertToDetailDTO(User user) {
return BeanUtil.convert(user, () -> {
UserDetailDTO dto = new UserDetailDTO();
// 可以在这里设置默认值
dto.setStatus("ACTIVE");
return dto;
});
}
// 忽略null值的属性复制
public void updateUser(Long id, UpdateUserRequest request) {
User existingUser = findById(id);
// 只复制非null的属性避免覆盖现有数据
BeanUtil.copyPropertiesIgnoreNull(request, existingUser);
userMapper.updateById(existingUser);
}
}
// 实体类示例
@Data
@TableName("users")
public class User extends BaseEntity {
private String username;
private String email;
private String phone;
private Integer age;
private String avatar;
}
// DTO示例
@Data
public class UserDTO {
private Long id;
private String username;
private String email;
private String phone;
private Integer age;
private String avatar;
private LocalDateTime createdTime;
}
```
### 5. 📝 日志切面
**功能描述**: 自动记录Controller方法的调用日志包括请求参数和响应结果
**实现类**: `LoggingAspect`
**使用示例**:
```java
// 无需额外配置自动对所有Controller方法生效
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ApiResponse<Order> createOrder(@RequestBody CreateOrderRequest request) {
// 自动记录日志:
// [INFO] 开始执行方法: OrderController.createOrder
// [INFO] 请求参数: {"productId":1,"quantity":2,"address":"北京市朝阳区"}
Order order = orderService.createOrder(request);
// [INFO] 方法执行完成: OrderController.createOrder, 耗时: 150ms
// [INFO] 响应结果: {"success":true,"data":{"id":1,"orderNo":"ORD20240101001"}}
return ApiResponse.success("订单创建成功", order);
}
@GetMapping("/{id}")
public ApiResponse<Order> getOrder(@PathVariable Long id) {
// 自动记录日志,包括路径参数
Order order = orderService.findById(id);
return ApiResponse.success(order);
}
}
// 日志输出示例:
// 2024-01-01 10:30:15.123 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 开始执行方法: OrderController.createOrder
// 2024-01-01 10:30:15.124 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 请求参数: [{"productId":1,"quantity":2}]
// 2024-01-01 10:30:15.275 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 方法执行完成: OrderController.createOrder, 耗时: 151ms
// 2024-01-01 10:30:15.276 [http-nio-8080-exec-1] INFO c.a.l.aspect.LoggingAspect - 响应结果: {"success":true,"code":"SUCCESS"}
```
### 6. ⚡ 性能监控切面
**功能描述**: 监控Controller、Service、Mapper层方法的执行性能超过阈值时发出警告
**实现类**: `PerformanceAspect`
**使用示例**:
```java
// 无需额外配置,自动监控所有层的方法性能
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public List<Product> findExpensiveProducts() {
// 如果这个方法执行超过5秒会自动记录警告日志
return productMapper.selectExpensiveProducts();
}
public void batchUpdateProducts(List<Product> products) {
// 批量操作可能耗时较长,会被监控
for (Product product : products) {
productMapper.updateById(product);
}
}
}
@Repository
public interface ProductMapper extends BaseMapper<Product> {
// 复杂查询可能耗时较长,会被监控
@Select("SELECT * FROM products WHERE price > 1000 ORDER BY price DESC")
List<Product> selectExpensiveProducts();
}
// 性能监控日志示例:
// 2024-01-01 10:30:15.123 [http-nio-8080-exec-1] WARN c.a.l.aspect.PerformanceAspect - [性能警告] Service层方法执行缓慢: ProductService.findExpensiveProducts, 耗时: 6543ms (阈值: 5000ms)
// 2024-01-01 10:30:15.124 [http-nio-8080-exec-1] INFO c.a.l.aspect.PerformanceAspect - [性能监控] Mapper层方法执行: ProductMapper.selectExpensiveProducts, 耗时: 234ms
// 配置性能阈值 (application.properties):
app.performance.warning.controller=5000 # Controller层警告阈值5秒
app.performance.warning.service=3000 # Service层警告阈值3秒
app.performance.warning.mapper=500 # Mapper层警告阈值500毫秒
```
### 7. 🗄️ MyBatis-Plus集成
**功能描述**: 提供强大的ORM功能包括基础CRUD、分页查询、条件构造器等
**实现类**: `BaseMapper<T>`, `BaseEntity`
**使用示例**:
```java
// 1. 实体类继承BaseEntity
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("products")
public class Product extends BaseEntity {
@TableField("name")
private String name;
@TableField("description")
private String description;
@TableField("price")
private BigDecimal price;
@TableField("category_id")
private Long categoryId;
@TableField("stock")
private Integer stock;
@TableField("status")
private Integer status; // 0-下架, 1-上架
}
// 2. Mapper接口继承BaseMapper
@Repository
public interface ProductMapper extends BaseMapper<Product> {
// 自定义查询方法
@Select("SELECT * FROM products WHERE category_id = #{categoryId} AND status = 1")
List<Product> findByCategoryAndActive(@Param("categoryId") Long categoryId);
// 复杂查询
@Select("SELECT p.*, c.name as category_name FROM products p " +
"LEFT JOIN categories c ON p.category_id = c.id " +
"WHERE p.price BETWEEN #{minPrice} AND #{maxPrice}")
List<ProductVO> findByPriceRange(@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice);
}
// 3. Service层使用示例
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 基础CRUD操作
public Product save(Product product) {
productMapper.insert(product); // 自动填充创建时间
return product;
}
public Product findById(Long id) {
return productMapper.selectById(id);
}
public void deleteById(Long id) {
productMapper.deleteById(id); // 逻辑删除
}
public Product update(Product product) {
productMapper.updateById(product); // 自动更新修改时间
return product;
}
// 条件查询
public List<Product> findByCondition(String name, BigDecimal minPrice, Integer status) {
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(name)) {
queryWrapper.like("name", name);
}
if (minPrice != null) {
queryWrapper.ge("price", minPrice);
}
if (status != null) {
queryWrapper.eq("status", status);
}
return productMapper.selectList(queryWrapper);
}
// 分页查询
public PageResult<Product> findByPage(int page, int size, String keyword) {
Page<Product> pageParam = new Page<>(page, size);
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(keyword)) {
queryWrapper.and(wrapper -> wrapper
.like("name", keyword)
.or()
.like("description", keyword)
);
}
queryWrapper.eq("status", 1); // 只查询上架商品
queryWrapper.orderByDesc("created_time");
Page<Product> result = productMapper.selectPage(pageParam, queryWrapper);
return PageResult.fromPage(result);
}
// 批量操作
public void batchInsert(List<Product> products) {
// MyBatis-Plus会自动优化批量插入
for (Product product : products) {
productMapper.insert(product);
}
}
}
```
### 9. 🔒 Spring Security集成
**功能描述**: 提供基础的安全配置,支持认证和授权
**配置类**: `SecurityConfig`
**使用示例**:
```java
// 当前配置允许所有请求访问,可以根据需要自定义安全规则
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
// 公开接口
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
.requestMatchers("/actuator/**", "/api-docs/**", "/swagger-ui/**").permitAll()
// 需要认证的接口
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/api/auth/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.logoutSuccessHandler(logoutSuccessHandler())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
);
return http.build();
}
// 认证成功处理器
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return (request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
ApiResponse<UserInfo> result = ApiResponse.success("登录成功", getUserInfo(authentication));
response.getWriter().write(JSON.toJSONString(result));
};
}
}
// 用户认证Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public ApiResponse<UserInfo> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) {
// Spring Security会自动处理认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserInfo userInfo = getUserInfo(authentication);
return ApiResponse.success("登录成功", userInfo);
}
@PostMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request) {
SecurityContextHolder.clearContext();
request.getSession().invalidate();
return ApiResponse.success("退出成功");
}
@GetMapping("/current")
public ApiResponse<UserInfo> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return ApiResponse.error("UNAUTHORIZED", "未登录");
}
UserInfo userInfo = getUserInfo(authentication);
return ApiResponse.success(userInfo);
}
}
```
### 10. 📚 Swagger API文档
**功能描述**: 自动生成API文档支持在线测试
**配置类**: `SwaggerConfig`
**使用示例**:
```java
// 在Controller中添加Swagger注解
@RestController
@RequestMapping("/api/products")
@Tag(name = "商品管理", description = "商品相关的API接口")
public class ProductController {
@Operation(summary = "创建商品", description = "创建一个新的商品")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "400", description = "参数错误"),
@ApiResponse(responseCode = "500", description = "服务器错误")
})
@PostMapping
public ApiResponse<Product> createProduct(
@Parameter(description = "商品信息", required = true)
@Valid @RequestBody CreateProductRequest request) {
Product product = productService.createProduct(request);
return ApiResponse.success("创建成功", product);
}
@Operation(summary = "获取商品详情", description = "根据商品ID获取商品详细信息")
@GetMapping("/{id}")
public ApiResponse<Product> getProduct(
@Parameter(description = "商品ID", required = true, example = "1")
@PathVariable Long id) {
Product product = productService.findById(id);
return ApiResponse.success(product);
}
@Operation(summary = "商品列表查询", description = "分页查询商品列表,支持关键词搜索")
@GetMapping
public ApiResponse<PageResult<Product>> getProducts(
@Parameter(description = "页码", example = "1")
@RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小", example = "10")
@RequestParam(defaultValue = "10") int size,
@Parameter(description = "搜索关键词")
@RequestParam(required = false) String keyword) {
PageResult<Product> result = productService.findByPage(page, size, keyword);
return ApiResponse.success("查询成功", result);
}
}
// DTO类添加Swagger注解
@Data
@Schema(description = "创建商品请求")
public class CreateProductRequest {
@Schema(description = "商品名称", example = "iPhone 15 Pro", required = true)
@NotBlank(message = "商品名称不能为空")
private String name;
@Schema(description = "商品描述", example = "最新款iPhone手机")
private String description;
@Schema(description = "商品价格", example = "8999.00", required = true)
@NotNull(message = "商品价格不能为空")
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
private BigDecimal price;
@Schema(description = "商品分类ID", example = "1", required = true)
@NotNull(message = "商品分类不能为空")
private Long categoryId;
@Schema(description = "库存数量", example = "100", required = true)
@NotNull(message = "库存数量不能为空")
@Min(value = 0, message = "库存数量不能为负数")
private Integer stock;
}
// 访问Swagger UI: http://localhost:8080/swagger-ui.html
// 访问API文档JSON: http://localhost:8080/api-docs
```
## 🚀 快速开始
### 1. 创建实体类
```java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("your_table")
public class YourEntity extends BaseEntity {
private String name;
private String description;
// 其他字段...
}
```
### 2. 创建Mapper接口
```java
@Repository
public interface YourEntityMapper extends BaseMapper<YourEntity> {
// 自定义查询方法
}
```
### 3. 创建Service类
```java
@Service
public class YourEntityService {
@Autowired
private YourEntityMapper mapper;
// 业务逻辑方法
}
```
### 4. 创建Controller类
```java
@RestController
@RequestMapping("/api/your-entity")
public class YourEntityController {
@Autowired
private YourEntityService service;
// API接口方法
}
```
## 📝 配置说明
### 数据库配置
```properties
# MySQL数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password
```
### 文件上传配置
```properties
# 文件上传大小限制
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
app.upload.dir=./uploads/
```
### 性能监控配置
```properties
# 性能警告阈值(毫秒)
app.performance.warning.controller=5000
app.performance.warning.service=3000
app.performance.warning.mapper=500
```
这个架构模板提供了完整的企业级应用基础设施,您可以在此基础上快速开发各种类型的业务应用!

295
mvnw vendored Normal file
View File

@@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
mvnw.cmd vendored Normal file
View File

@@ -0,0 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

154
pom.xml Normal file
View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aida</groupId>
<artifactId>lanecarford</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lanecarford</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Swagger for API Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<release>21</release>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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.");
}
}

View File

@@ -0,0 +1,127 @@
package com.aida.lanecarford.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* 日志切面
*
* @author AI Assistant
* @since 2024-01-01
*/
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
/**
* 定义切点所有Controller方法
*/
@Pointcut("execution(* com.aida.lanecarford.controller..*(..))")
public void controllerMethods() {}
/**
* 定义切点所有Service方法
*/
@Pointcut("execution(* com.aida.lanecarford.service..*(..))")
public void serviceMethods() {}
/**
* Controller方法执行前记录日志
*/
@Before("controllerMethods()")
public void logControllerBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
logger.info("=== 请求开始 ===");
logger.info("请求URL: {}", request.getRequestURL().toString());
logger.info("请求方法: {}", request.getMethod());
logger.info("请求IP: {}", getClientIpAddress(request));
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("请求参数: {}", Arrays.toString(joinPoint.getArgs()));
}
}
/**
* Controller方法执行后记录日志
*/
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
public void logControllerAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("方法执行成功: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("返回结果: {}", result);
logger.info("=== 请求结束 ===");
}
/**
* Controller方法抛出异常时记录日志
*/
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
public void logControllerAfterThrowing(JoinPoint joinPoint, Throwable exception) {
logger.error("方法执行异常: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.error("异常信息: ", exception);
logger.info("=== 请求异常结束 ===");
}
/**
* Service方法环绕通知记录执行时间
*/
@Around("serviceMethods()")
public Object logServiceAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
try {
logger.debug("Service方法开始执行: {}", methodName);
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.debug("Service方法执行成功: {}, 耗时: {}ms", methodName, (endTime - startTime));
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
logger.error("Service方法执行异常: {}, 耗时: {}ms", methodName, (endTime - startTime));
logger.error("异常详情: ", e);
throw e;
}
}
/**
* 获取客户端真实IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0];
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
String proxyClientIp = request.getHeader("Proxy-Client-IP");
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
return proxyClientIp;
}
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
return wlProxyClientIp;
}
return request.getRemoteAddr();
}
}

View File

@@ -0,0 +1,134 @@
package com.aida.lanecarford.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 性能监控切面
*
* @author AI Assistant
* @since 2024-01-01
*/
@Aspect
@Component
public class PerformanceAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);
/**
* 监控Controller方法性能
*/
@Around("execution(* com.aida.lanecarford.controller..*(..))")
public Object monitorControllerPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Controller");
}
/**
* 监控Service方法性能
*/
@Around("execution(* com.aida.lanecarford.service..*(..))")
public Object monitorServicePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Service");
}
/**
* 监控数据库操作性能
*/
@Around("execution(* com.aida.lanecarford.mapper..*(..))")
public Object monitorMapperPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Mapper");
}
/**
* 通用性能监控方法
*/
private Object monitorMethodPerformance(ProceedingJoinPoint joinPoint, String layer) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录性能日志
logPerformance(layer, methodName, executionTime, true);
// 如果执行时间过长,记录警告
if (executionTime > getWarningThreshold(layer)) {
logger.warn("{}方法执行时间过长: {} - {}ms", layer, methodName, executionTime);
}
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录异常性能日志
logPerformance(layer, methodName, executionTime, false);
throw e;
}
}
/**
* 记录性能日志
*/
private void logPerformance(String layer, String methodName, long executionTime, boolean success) {
if (logger.isDebugEnabled()) {
logger.debug("性能监控 - {}: {} - {}ms - {}",
layer, methodName, executionTime, success ? "成功" : "失败");
}
// 可以在这里添加性能数据收集逻辑,比如发送到监控系统
collectPerformanceMetrics(layer, methodName, executionTime, success);
}
/**
* 获取警告阈值(毫秒)
*/
private long getWarningThreshold(String layer) {
switch (layer) {
case "Controller":
return 5000; // 5秒
case "Service":
return 3000; // 3秒
case "Mapper":
return 1000; // 1秒
default:
return 2000; // 2秒
}
}
/**
* 收集性能指标
* TODO: 可以集成到监控系统如Micrometer、Prometheus等
*/
private void collectPerformanceMetrics(String layer, String methodName, long executionTime, boolean success) {
// 这里可以添加性能指标收集逻辑
// 例如:
// - 发送到时序数据库
// - 更新内存中的统计信息
// - 发送到监控系统
// 示例:简单的内存统计
PerformanceMetrics.recordExecution(layer, methodName, executionTime, success);
}
/**
* 简单的性能指标收集器
*/
private static class PerformanceMetrics {
public static void recordExecution(String layer, String methodName, long executionTime, boolean success) {
// 简单的日志记录,实际项目中可以替换为更复杂的指标收集
if (executionTime > 1000) { // 超过1秒的操作
logger.info("慢操作记录 - {}.{}: {}ms, 成功: {}", layer, methodName, executionTime, success);
}
}
}
}

View File

@@ -0,0 +1,168 @@
package com.aida.lanecarford.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
/**
* 统一API响应格式
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应状态码
*/
private String code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 是否成功
*/
private boolean success;
/**
* 时间戳
*/
private long timestamp;
public ApiResponse() {
this.timestamp = System.currentTimeMillis();
}
public ApiResponse(boolean success, String code, String message, T data) {
this();
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功响应(无数据)
*/
public static <T> ApiResponse<T> success() {
return new ApiResponse<>(true, "SUCCESS", "操作成功", null);
}
/**
* 成功响应(带数据)
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "SUCCESS", "操作成功", data);
}
/**
* 成功响应(自定义消息,无数据)
*/
public static <T> ApiResponse<T> success(String message) {
return new ApiResponse<>(true, "SUCCESS", message, null);
}
/**
* 成功响应(自定义消息)
*/
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, "SUCCESS", message, data);
}
/**
* 失败响应
*/
public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(false, code, message, null);
}
/**
* 失败响应(默认错误码)
*/
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, "ERROR", message, null);
}
/**
* 参数错误响应
*/
public static <T> ApiResponse<T> paramError(String message) {
return new ApiResponse<>(false, "PARAM_ERROR", message, null);
}
/**
* 数据不存在响应
*/
public static <T> ApiResponse<T> notFound(String message) {
return new ApiResponse<>(false, "NOT_FOUND", message, null);
}
/**
* 权限不足响应
*/
public static <T> ApiResponse<T> forbidden(String message) {
return new ApiResponse<>(false, "FORBIDDEN", message, null);
}
/**
* 服务器内部错误响应
*/
public static <T> ApiResponse<T> serverError(String message) {
return new ApiResponse<>(false, "SERVER_ERROR", message, null);
}
/**
* 业务异常响应
*/
public static <T> ApiResponse<T> businessError(String code, String message) {
return new ApiResponse<>(false, code, message, null);
}
/**
* 外部服务错误响应
*/
public static <T> ApiResponse<T> externalServiceError(String message) {
return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null);
}
/**
* 文件上传错误响应
*/
public static <T> ApiResponse<T> fileUploadError(String message) {
return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null);
}
/**
* 验证失败响应
*/
public static <T> ApiResponse<T> validationError(String message) {
return new ApiResponse<>(false, "VALIDATION_ERROR", message, null);
}
/**
* 重复数据响应
*/
public static <T> ApiResponse<T> duplicateError(String message) {
return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null);
}
/**
* 操作超时响应
*/
public static <T> ApiResponse<T> timeoutError(String message) {
return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null);
}
}

View File

@@ -0,0 +1,136 @@
package com.aida.lanecarford.common;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 数据列表
*/
private List<T> records;
/**
* 总记录数
*/
private long total;
/**
* 当前页码
*/
private long current;
/**
* 每页大小
*/
private long size;
/**
* 总页数
*/
private long pages;
/**
* 是否有上一页
*/
private boolean hasPrevious;
/**
* 是否有下一页
*/
private boolean hasNext;
public PageResult() {
}
public PageResult(List<T> records, long total, long current, long size) {
this.records = records;
this.total = total;
this.current = current;
this.size = size;
this.pages = (total + size - 1) / size;
this.hasPrevious = current > 1;
this.hasNext = current < pages;
}
/**
* 从MyBatis-Plus的IPage转换
*/
public static <T> PageResult<T> of(IPage<T> page) {
return new PageResult<>(
page.getRecords(),
page.getTotal(),
page.getCurrent(),
page.getSize()
);
}
/**
* 创建空的分页结果
*/
public static <T> PageResult<T> empty() {
return new PageResult<>(List.of(), 0, 1, 10);
}
/**
* 创建空的分页结果(指定页码和大小)
*/
public static <T> PageResult<T> empty(long current, long size) {
return new PageResult<>(List.of(), 0, current, size);
}
/**
* 创建单页结果
*/
public static <T> PageResult<T> of(List<T> records) {
return new PageResult<>(records, records.size(), 1, records.size());
}
/**
* 获取开始记录索引从1开始
*/
public long getStartIndex() {
return (current - 1) * size + 1;
}
/**
* 获取结束记录索引
*/
public long getEndIndex() {
long end = current * size;
return Math.min(end, total);
}
/**
* 是否为空结果
*/
public boolean isEmpty() {
return records == null || records.isEmpty();
}
/**
* 是否为第一页
*/
public boolean isFirst() {
return current == 1;
}
/**
* 是否为最后一页
*/
public boolean isLast() {
return current == pages;
}
}

View File

@@ -0,0 +1,53 @@
package com.aida.lanecarford.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* 缓存配置类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 缓存名称常量
*/
public static final String STYLE_OUTFITS_CACHE = "styleOutfits";
public static final String MODEL_PHOTOS_CACHE = "modelPhotos";
public static final String VIRTUAL_TRYONS_CACHE = "virtualTryOns";
public static final String FAVORITE_OUTFITS_CACHE = "favoriteOutfits";
public static final String CUSTOMER_CACHE = "customers";
public static final String SALES_ADVISOR_CACHE = "salesAdvisors";
/**
* 默认缓存管理器
*/
@Bean
@Primary
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
// 预创建缓存
cacheManager.setCacheNames(java.util.Arrays.asList(
STYLE_OUTFITS_CACHE,
MODEL_PHOTOS_CACHE,
VIRTUAL_TRYONS_CACHE,
FAVORITE_OUTFITS_CACHE,
CUSTOMER_CACHE,
SALES_ADVISOR_CACHE
));
// 允许空值缓存
cacheManager.setAllowNullValues(true);
return cacheManager;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,36 @@
package com.aida.lanecarford.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
* 日志配置类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Configuration
public class LoggingConfig {
private static final Logger logger = LoggerFactory.getLogger(LoggingConfig.class);
/**
* 配置请求日志过滤器
*/
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeClientInfo(true);
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setIncludeHeaders(false);
filter.setMaxPayloadLength(1000);
filter.setAfterMessagePrefix("REQUEST DATA : ");
logger.info("请求日志过滤器已配置");
return filter;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,172 @@
package com.aida.lanecarford.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* Swagger配置类
* 提供完整的API文档配置包括安全认证、服务器信息和标签分类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Configuration
public class SwaggerConfig {
@Value("${server.port:8080}")
private String serverPort;
@Value("${spring.application.name:lanecarford}")
private String applicationName;
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(createApiInfo())
.servers(createServers())
.components(createComponents())
.security(createSecurityRequirements())
.tags(createTags())
.externalDocs(createExternalDocumentation());
}
/**
* 创建API信息
*/
private Info createApiInfo() {
return new Info()
.title("Lane Carford 虚拟试衣系统 API文档")
.description("""
## Lane Carford 虚拟试衣系统后端接口文档
### 系统功能
- **客户管理**: 客户信息的创建、查询和管理
- **风格分析**: 基于AI的服装风格分析和推荐
- **虚拟试衣**: 虚拟试衣效果展示和管理
- **收藏管理**: 客户喜欢的搭配收藏和管理
- **模特照片**: 模特照片的上传和管理
- **导购系统**: 导购员登录和客户服务管理
### 认证说明
系统使用Session认证需要先通过登录接口获取认证状态。
### 响应格式
所有API响应都遵循统一格式
```json
{
"success": true,
"code": "SUCCESS",
"message": "操作成功",
"data": {},
"timestamp": 1640995200000
}
```
### 错误处理
系统提供统一的错误处理机制,详细的错误信息会在响应中返回。
""")
.version("1.0.0")
.contact(new Contact()
.name("Lane Carford 开发团队")
.email("dev@lanecarford.com")
.url("https://www.lanecarford.com"))
.license(new License()
.name("Apache 2.0")
.url("http://www.apache.org/licenses/LICENSE-2.0"));
}
/**
* 创建服务器信息
*/
private List<Server> createServers() {
return Arrays.asList(
new Server()
.url("http://localhost:" + serverPort)
.description("本地开发环境"),
new Server()
.url("https://api.lanecarford.com")
.description("生产环境"),
new Server()
.url("https://test-api.lanecarford.com")
.description("测试环境")
);
}
/**
* 创建组件配置(安全方案等)
*/
private Components createComponents() {
return new Components()
.addSecuritySchemes("sessionAuth", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.COOKIE)
.name("JSESSIONID")
.description("Session认证通过登录接口获取"))
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
.description("基础认证(仅用于开发测试)"));
}
/**
* 创建安全要求
*/
private List<SecurityRequirement> createSecurityRequirements() {
return Arrays.asList(
new SecurityRequirement().addList("sessionAuth"),
new SecurityRequirement().addList("basicAuth")
);
}
/**
* 创建API标签分类
*/
private List<Tag> createTags() {
return Arrays.asList(
new Tag()
.name("认证管理")
.description("用户登录、登出和认证相关接口"),
new Tag()
.name("客户管理")
.description("客户信息的创建、查询、更新和删除"),
new Tag()
.name("风格分析")
.description("基于AI的服装风格分析和推荐系统"),
new Tag()
.name("虚拟试衣")
.description("虚拟试衣效果的生成和管理"),
new Tag()
.name("收藏管理")
.description("客户喜欢的搭配收藏和个人化管理"),
new Tag()
.name("模特照片")
.description("模特照片的上传、管理和展示"),
new Tag()
.name("系统监控")
.description("系统健康检查和状态监控接口")
);
}
/**
* 创建外部文档链接
*/
private ExternalDocumentation createExternalDocumentation() {
return new ExternalDocumentation()
.description("Lane Carford 系统文档")
.url("https://docs.lanecarford.com");
}
}

View File

@@ -0,0 +1,46 @@
package com.aida.lanecarford.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类 - 纯API后端服务
*
* @author AI Assistant
* @since 2024-01-01
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置跨域 - 支持前后端分离
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
/**
* 配置资源处理 - 仅保留API文档和文件上传
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置上传文件的访问路径
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/");
// 配置Swagger UI资源
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
registry.addResourceHandler("/v3/api-docs/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,44 @@
package com.aida.lanecarford.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 数据传输对象基类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@Schema(description = "数据传输对象基类")
public abstract class BaseDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "记录ID")
private Long id;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "创建者")
private String createBy;
@Schema(description = "更新者")
private String updateBy;
@Schema(description = "版本号(用于乐观锁)")
private Integer version;
@Schema(description = "是否删除0-未删除1-已删除)")
private Integer deleted;
}

View File

@@ -0,0 +1,98 @@
package com.aida.lanecarford.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.io.Serializable;
/**
* 请求参数基类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@Schema(description = "请求参数基类")
public abstract class BaseRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "当前页码", example = "1")
@Min(value = 1, message = "页码必须大于0")
private Integer current = 1;
@Schema(description = "每页大小", example = "10")
@Min(value = 1, message = "每页大小必须大于0")
@Max(value = 100, message = "每页大小不能超过100")
private Integer size = 10;
@Schema(description = "排序字段", example = "createTime")
private String sortField;
@Schema(description = "排序方向ASC-升序DESC-降序)", example = "DESC")
private String sortOrder = "DESC";
@Schema(description = "搜索关键词")
private String keyword;
@Schema(description = "开始时间格式yyyy-MM-dd HH:mm:ss")
private String startTime;
@Schema(description = "结束时间格式yyyy-MM-dd HH:mm:ss")
private String endTime;
@Schema(description = "状态筛选")
private Integer status;
/**
* 获取偏移量
*/
public long getOffset() {
return (long) (current - 1) * size;
}
/**
* 获取限制数量
*/
public long getLimit() {
return size;
}
/**
* 是否有搜索关键词
*/
public boolean hasKeyword() {
return keyword != null && !keyword.trim().isEmpty();
}
/**
* 是否有时间范围筛选
*/
public boolean hasTimeRange() {
return startTime != null && !startTime.trim().isEmpty()
&& endTime != null && !endTime.trim().isEmpty();
}
/**
* 是否有状态筛选
*/
public boolean hasStatus() {
return status != null;
}
/**
* 是否需要排序
*/
public boolean needSort() {
return sortField != null && !sortField.trim().isEmpty();
}
/**
* 是否降序排序
*/
public boolean isDesc() {
return "DESC".equalsIgnoreCase(sortOrder);
}
}

View File

@@ -0,0 +1,42 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 基础实体类
* 包含通用字段,其他实体类可以继承此类
*
* @author AI Assistant
* @since 1.0.0
*/
@Data
public abstract class BaseEntity {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
/**
* 逻辑删除标志0-未删除1-已删除
*/
@TableLogic
@TableField(value = "deleted")
private Integer deleted;
}

View File

@@ -0,0 +1,73 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 顾客实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("customers")
public class Customer {
/**
* 顾客ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 顾客姓名
*/
@TableField("name")
private String name;
/**
* 顾客邮箱
*/
@TableField("email")
private String email;
/**
* 手机号
*/
@TableField("phone")
private String phone;
/**
* 性别
*/
@TableField("gender")
private String gender;
/**
* 年龄段
*/
@TableField("age_range")
private String ageRange;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -0,0 +1,67 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 顾客照片实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("customer_photos")
public class CustomerPhoto {
/**
* 顾客照片ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 顾客ID
*/
@TableField("customer_id")
private Long customerId;
/**
* 进店记录ID
*/
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 照片URL
*/
@TableField("photo_url")
private String photoUrl;
/**
* 是否为主照片(0-否,1-是)
*/
@TableField("is_primary")
private Integer isPrimary;
/**
* 上传时间
*/
@TableField("upload_time")
private LocalDateTime uploadTime;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
}

View File

@@ -0,0 +1,73 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 模特照片实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("model_photos")
public class ModelPhoto {
/**
* 模特照片ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 模特照片URL
*/
@TableField("photo_url")
private String photoUrl;
/**
* 照片名称
*/
@TableField("photo_name")
private String photoName;
/**
* 性别
*/
@TableField("gender")
private String gender;
/**
* 是否启用(0-禁用,1-启用)
*/
@TableField("is_active")
private Integer isActive;
/**
* 排序权重
*/
@TableField("sort_order")
private Integer sortOrder;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -0,0 +1,97 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 导购实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("sales")
public class Sales {
/**
* 导购ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码(加密后)
*/
@TableField("password")
private String password;
/**
* 真实姓名
*/
@TableField("real_name")
private String realName;
/**
* 员工编号
*/
@TableField("employee_id")
private String employeeId;
/**
* 门店ID
*/
@TableField("store_id")
private String storeId;
/**
* 门店名称
*/
@TableField("store_name")
private String storeName;
/**
* 手机号
*/
@TableField("phone")
private String phone;
/**
* 邮箱
*/
@TableField("email")
private String email;
/**
* 是否启用(0-禁用,1-启用)
*/
@TableField("is_active")
private Integer isActive;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -0,0 +1,85 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 风格配置实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("styles")
public class Style {
/**
* 风格配置ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 顾客ID
*/
@TableField("customer_id")
private Long customerId;
/**
* 进店记录ID
*/
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 是否选中(0-未选中,1-已选中)
*/
@TableField("is_selected")
private Integer isSelected;
/**
* 风格图片URL
*/
@TableField("style_image_url")
private String styleImageUrl;
/**
* Python请求ID
*/
@TableField("python_request_id")
private String pythonRequestId;
/**
* 生成状态(0-处理中,1-已完成,2-失败)
*/
@TableField("generation_status")
private Integer generationStatus;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -0,0 +1,109 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 试穿效果实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("try_on_effects")
public class TryOnEffect {
/**
* 试穿效果ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 顾客ID
*/
@TableField("customer_id")
private Long customerId;
/**
* 进店记录ID
*/
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 顾客照片ID
*/
@TableField("customer_photo_id")
private Long customerPhotoId;
/**
* 模特照片ID
*/
@TableField("model_photo_id")
private Long modelPhotoId;
/**
* 试穿效果图URL
*/
@TableField("try_on_image_url")
private String tryOnImageUrl;
/**
* 提示词
*/
@TableField("prompt")
private String prompt;
/**
* Python请求ID
*/
@TableField("python_request_id")
private String pythonRequestId;
/**
* 生成状态(0-处理中,1-已完成,2-失败)
*/
@TableField("generation_status")
private Integer generationStatus;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 是否收藏(0-否,1-是)
*/
@TableField("is_favorite")
private Integer isFavorite;
/**
* 原始试穿效果ID(用于重新生成)
*/
@TableField("original_try_on_id")
private Long originalTryOnId;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -0,0 +1,86 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 进店记录实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("visit_records")
public class VisitRecord {
/**
* 进店记录ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 顾客ID
*/
@TableField("customer_id")
private Long customerId;
/**
* 导购ID
*/
@TableField("sales_id")
private Long salesId;
/**
* 进店日期
*/
@TableField("visit_date")
private LocalDate visitDate;
/**
* 进店时间
*/
@TableField("visit_time")
private LocalDateTime visitTime;
/**
* 会话ID
*/
@TableField("session_id")
private String sessionId;
/**
* 状态(0-已结束,1-进行中)
*/
@TableField("status")
private Integer status;
/**
* 备注
*/
@TableField("notes")
private String notes;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -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", "外部服务调用失败");
}
}

View File

@@ -0,0 +1,284 @@
package com.aida.lanecarford.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* 统一处理应用程序中的各种异常,提供一致的错误响应格式
*
* @author AI Assistant
* @since 2024-01-01
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 错误代码常量
private static final String BUSINESS_ERROR = "BUSINESS_ERROR";
private static final String VALIDATION_ERROR = "VALIDATION_ERROR";
private static final String BIND_ERROR = "BIND_ERROR";
private static final String FILE_SIZE_EXCEEDED = "FILE_SIZE_EXCEEDED";
private static final String ILLEGAL_ARGUMENT = "ILLEGAL_ARGUMENT";
private static final String NULL_POINTER = "NULL_POINTER";
private static final String RUNTIME_ERROR = "RUNTIME_ERROR";
private static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
private static final String DATABASE_ERROR = "DATABASE_ERROR";
private static final String METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
private static final String NOT_FOUND = "NOT_FOUND";
private static final String MEDIA_TYPE_NOT_SUPPORTED = "MEDIA_TYPE_NOT_SUPPORTED";
private static final String MESSAGE_NOT_READABLE = "MESSAGE_NOT_READABLE";
private static final String MISSING_PARAMETER = "MISSING_PARAMETER";
private static final String TYPE_MISMATCH = "TYPE_MISMATCH";
private static final String CONSTRAINT_VIOLATION = "CONSTRAINT_VIOLATION";
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
logger.warn("业务异常 [{}]: {} - 请求路径: {}", e.getCode(), e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(BUSINESS_ERROR, e.getMessage(), request.getRequestURI(), null));
}
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
logger.warn("参数验证异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
.collect(Collectors.toMap(
error -> ((FieldError) error).getField(),
error -> error.getDefaultMessage(),
(existing, replacement) -> existing
));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(VALIDATION_ERROR, "参数验证失败", request.getRequestURI(), errors));
}
/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handleBindException(BindException e, HttpServletRequest request) {
logger.warn("绑定异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
.collect(Collectors.toMap(
error -> ((FieldError) error).getField(),
error -> error.getDefaultMessage(),
(existing, replacement) -> existing
));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(BIND_ERROR, "数据绑定失败", request.getRequestURI(), errors));
}
/**
* 处理约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
logger.warn("约束违反异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
Map<String, String> errors = violations.stream()
.collect(Collectors.toMap(
violation -> violation.getPropertyPath().toString(),
ConstraintViolation::getMessage,
(existing, replacement) -> existing
));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(CONSTRAINT_VIOLATION, "约束验证失败", request.getRequestURI(), errors));
}
/**
* 处理文件上传大小超限异常
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
logger.warn("文件上传大小超限: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(FILE_SIZE_EXCEEDED, "上传文件大小超过限制", request.getRequestURI(), null));
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
logger.warn("非法参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(ILLEGAL_ARGUMENT, "参数错误:" + e.getMessage(), request.getRequestURI(), null));
}
/**
* 处理缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Map<String, Object>> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
logger.warn("缺少请求参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
String message = String.format("缺少必需的请求参数: %s", e.getParameterName());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(MISSING_PARAMETER, message, request.getRequestURI(), null));
}
/**
* 处理参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Map<String, Object>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
logger.warn("参数类型不匹配异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
String message = String.format("参数 %s 的值 %s 类型不正确", e.getName(), e.getValue());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(TYPE_MISMATCH, message, request.getRequestURI(), null));
}
/**
* 处理HTTP请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, Object>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
logger.warn("HTTP请求方法不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
String message = String.format("不支持的请求方法: %s", e.getMethod());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(createErrorResponse(METHOD_NOT_ALLOWED, message, request.getRequestURI(), null));
}
/**
* 处理媒体类型不支持异常
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<Map<String, Object>> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e, HttpServletRequest request) {
logger.warn("媒体类型不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
.body(createErrorResponse(MEDIA_TYPE_NOT_SUPPORTED, "不支持的媒体类型", request.getRequestURI(), null));
}
/**
* 处理HTTP消息不可读异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
logger.warn("HTTP消息不可读异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse(MESSAGE_NOT_READABLE, "请求体格式错误", request.getRequestURI(), null));
}
/**
* 处理404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Map<String, Object>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
logger.warn("404异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(createErrorResponse(NOT_FOUND, "请求的资源不存在", request.getRequestURI(), null));
}
/**
* 处理数据库相关异常
*/
@ExceptionHandler({DataAccessException.class, SQLException.class, DataIntegrityViolationException.class})
public ResponseEntity<Map<String, Object>> handleDatabaseException(Exception e, HttpServletRequest request) {
logger.error("数据库异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI(), e);
String message = "数据库操作失败";
if (e instanceof DataIntegrityViolationException) {
message = "数据完整性约束违反";
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse(DATABASE_ERROR, message, request.getRequestURI(), null));
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<Map<String, Object>> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
logger.error("空指针异常 - 请求路径: {}", request.getRequestURI(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse(NULL_POINTER, "系统内部错误", request.getRequestURI(), null));
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
logger.error("运行时异常 - 请求路径: {}", request.getRequestURI(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse(RUNTIME_ERROR, "系统运行时错误", request.getRequestURI(), null));
}
/**
* 处理所有其他异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e, HttpServletRequest request) {
logger.error("未知异常 - 请求路径: {}", request.getRequestURI(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse(UNKNOWN_ERROR, "系统内部错误", request.getRequestURI(), null));
}
/**
* 创建统一的错误响应格式
*/
private Map<String, Object> createErrorResponse(String code, String message, String path, Map<String, String> errors) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("code", code);
response.put("message", message);
response.put("timestamp", System.currentTimeMillis());
response.put("path", path);
if (errors != null && !errors.isEmpty()) {
response.put("errors", errors);
}
return response;
}
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.Customer;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 顾客Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface CustomerMapper extends BaseMapper<Customer> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 顾客照片Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface CustomerPhotoMapper extends BaseMapper<CustomerPhoto> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.ModelPhoto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 模特照片Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface ModelPhotoMapper extends BaseMapper<ModelPhoto> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.Sales;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 导购Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface SalesMapper extends BaseMapper<Sales> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.Style;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 风格配置Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface StyleMapper extends BaseMapper<Style> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.TryOnEffect;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 试穿效果Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface TryOnEffectMapper extends BaseMapper<TryOnEffect> {
}

View File

@@ -0,0 +1,16 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.VisitRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 进店记录Mapper接口
*
* @author AI Assistant
* @since 2024-01-01
*/
@Mapper
public interface VisitRecordMapper extends BaseMapper<VisitRecord> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 顾客照片服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface CustomerPhotoService extends IService<CustomerPhoto> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.Customer;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 顾客服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface CustomerService extends IService<Customer> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.ModelPhoto;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 模特照片服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface ModelPhotoService extends IService<ModelPhoto> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.Sales;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 导购服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface SalesService extends IService<Sales> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.Style;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 风格配置服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface StyleService extends IService<Style> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.TryOnEffect;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 试穿效果服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface TryOnEffectService extends IService<TryOnEffect> {
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.VisitRecord;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 进店记录服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface VisitRecordService extends IService<VisitRecord> {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.aida.lanecarford.mapper.CustomerPhotoMapper;
import com.aida.lanecarford.service.CustomerPhotoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 顾客照片服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class CustomerPhotoServiceImpl extends ServiceImpl<CustomerPhotoMapper, CustomerPhoto> implements CustomerPhotoService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.Customer;
import com.aida.lanecarford.mapper.CustomerMapper;
import com.aida.lanecarford.service.CustomerService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 顾客服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.ModelPhoto;
import com.aida.lanecarford.mapper.ModelPhotoMapper;
import com.aida.lanecarford.service.ModelPhotoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 模特照片服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class ModelPhotoServiceImpl extends ServiceImpl<ModelPhotoMapper, ModelPhoto> implements ModelPhotoService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.Sales;
import com.aida.lanecarford.mapper.SalesMapper;
import com.aida.lanecarford.service.SalesService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 导购服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class SalesServiceImpl extends ServiceImpl<SalesMapper, Sales> implements SalesService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.mapper.StyleMapper;
import com.aida.lanecarford.service.StyleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 风格配置服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements StyleService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.mapper.TryOnEffectMapper;
import com.aida.lanecarford.service.TryOnEffectService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 试穿效果服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService {
}

View File

@@ -0,0 +1,20 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.entity.VisitRecord;
import com.aida.lanecarford.mapper.VisitRecordMapper;
import com.aida.lanecarford.service.VisitRecordService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 进店记录服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, VisitRecord> implements VisitRecordService {
}

View File

@@ -0,0 +1,194 @@
package com.aida.lanecarford.util;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
/**
* Bean转换工具类
*
* @author AI Assistant
* @since 2024-01-01
*/
public class BeanUtil {
/**
* 单个对象转换
*
* @param source 源对象
* @param targetClass 目标类型
* @param <T> 目标类型
* @return 转换后的对象
*/
public static <T> T convert(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
try {
T target = targetClass.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(source, target);
return target;
} catch (Exception e) {
throw new RuntimeException("对象转换失败", e);
}
}
/**
* 单个对象转换使用Supplier
*
* @param source 源对象
* @param targetSupplier 目标对象供应商
* @param <T> 目标类型
* @return 转换后的对象
*/
public static <T> T convert(Object source, Supplier<T> targetSupplier) {
if (source == null) {
return null;
}
T target = targetSupplier.get();
BeanUtils.copyProperties(source, target);
return target;
}
/**
* 列表对象转换
*
* @param sourceList 源列表
* @param targetClass 目标类型
* @param <T> 目标类型
* @return 转换后的列表
*/
public static <T> List<T> convertList(List<?> sourceList, Class<T> targetClass) {
if (CollectionUtils.isEmpty(sourceList)) {
return Collections.emptyList();
}
List<T> targetList = new ArrayList<>(sourceList.size());
for (Object source : sourceList) {
T target = convert(source, targetClass);
if (target != null) {
targetList.add(target);
}
}
return targetList;
}
/**
* 列表对象转换使用Supplier
*
* @param sourceList 源列表
* @param targetSupplier 目标对象供应商
* @param <T> 目标类型
* @return 转换后的列表
*/
public static <T> List<T> convertList(List<?> sourceList, Supplier<T> targetSupplier) {
if (CollectionUtils.isEmpty(sourceList)) {
return Collections.emptyList();
}
List<T> targetList = new ArrayList<>(sourceList.size());
for (Object source : sourceList) {
T target = convert(source, targetSupplier);
if (target != null) {
targetList.add(target);
}
}
return targetList;
}
/**
* 复制属性忽略null值
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyPropertiesIgnoreNull(Object source, Object target) {
if (source == null || target == null) {
return;
}
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
/**
* 获取对象中为null的属性名
*
* @param source 源对象
* @return null属性名数组
*/
private static String[] getNullPropertyNames(Object source) {
final java.beans.BeanInfo beanInfo;
try {
beanInfo = java.beans.Introspector.getBeanInfo(source.getClass());
} catch (java.beans.IntrospectionException e) {
throw new RuntimeException("获取Bean信息失败", e);
}
final java.beans.PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
final java.util.Set<String> emptyNames = new java.util.HashSet<>();
for (java.beans.PropertyDescriptor pd : propertyDescriptors) {
final java.lang.reflect.Method readMethod = pd.getReadMethod();
if (readMethod != null) {
try {
final Object value = readMethod.invoke(source);
if (value == null) {
emptyNames.add(pd.getName());
}
} catch (Exception e) {
// 忽略异常
}
}
}
return emptyNames.toArray(new String[0]);
}
/**
* 判断对象是否为空
*
* @param obj 对象
* @return 是否为空
*/
public static boolean isEmpty(Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof String) {
return ((String) obj).trim().isEmpty();
}
if (obj instanceof java.util.Collection) {
return ((java.util.Collection<?>) obj).isEmpty();
}
if (obj instanceof java.util.Map) {
return ((java.util.Map<?, ?>) obj).isEmpty();
}
if (obj.getClass().isArray()) {
return java.lang.reflect.Array.getLength(obj) == 0;
}
return false;
}
/**
* 判断对象是否不为空
*
* @param obj 对象
* @return 是否不为空
*/
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
}

View File

@@ -0,0 +1,262 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 文件工具类
*
* @author AI Assistant
* @since 2024-01-01
*/
public class FileUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
/**
* 支持的图片格式
*/
private static final List<String> SUPPORTED_IMAGE_FORMATS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "webp"
);
/**
* 最大文件大小10MB
*/
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
/**
* 上传根目录
*/
private static final String UPLOAD_ROOT_DIR = "uploads";
/**
* 验证文件是否为图片
*/
public static boolean isImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
return false;
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
return false;
}
String extension = getFileExtension(originalFilename).toLowerCase();
return SUPPORTED_IMAGE_FORMATS.contains(extension);
}
/**
* 验证文件大小
*/
public static boolean isValidFileSize(MultipartFile file) {
return file != null && file.getSize() <= MAX_FILE_SIZE;
}
/**
* 获取文件扩展名
*/
public static String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "";
}
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return "";
}
return filename.substring(lastDotIndex + 1);
}
/**
* 生成唯一文件名
*/
public static String generateUniqueFileName(String originalFilename) {
String extension = getFileExtension(originalFilename);
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String uuid = UUID.randomUUID().toString().replace("-", "");
return timestamp + "_" + uuid + "." + extension;
}
/**
* 创建目录结构
*/
public static String createDirectoryStructure(String category) {
String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String fullPath = UPLOAD_ROOT_DIR + File.separator + category + File.separator + dateDir;
try {
Path path = Paths.get(fullPath);
Files.createDirectories(path);
return fullPath;
} catch (IOException e) {
logger.error("创建目录失败: {}", fullPath, e);
throw BusinessException.fileUploadFailed();
}
}
/**
* 保存文件
*/
public static String saveFile(MultipartFile file, String category) {
// 验证文件
validateFile(file);
// 创建目录
String directoryPath = createDirectoryStructure(category);
// 生成文件名
String filename = generateUniqueFileName(file.getOriginalFilename());
// 完整文件路径
String fullPath = directoryPath + File.separator + filename;
try {
// 保存文件
Path filePath = Paths.get(fullPath);
Files.write(filePath, file.getBytes());
logger.info("文件保存成功: {}", fullPath);
// 返回相对路径(用于数据库存储和访问)
return fullPath.replace(File.separator, "/");
} catch (IOException e) {
logger.error("文件保存失败: {}", fullPath, e);
throw BusinessException.fileUploadFailed();
}
}
/**
* 删除文件
*/
public static boolean deleteFile(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
try {
Path path = Paths.get(filePath.replace("/", File.separator));
boolean deleted = Files.deleteIfExists(path);
if (deleted) {
logger.info("文件删除成功: {}", filePath);
} else {
logger.warn("文件不存在或删除失败: {}", filePath);
}
return deleted;
} catch (IOException e) {
logger.error("文件删除失败: {}", filePath, e);
return false;
}
}
/**
* 检查文件是否存在
*/
public static boolean fileExists(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
Path path = Paths.get(filePath.replace("/", File.separator));
return Files.exists(path);
}
/**
* 获取文件大小(字节)
*/
public static long getFileSize(String filePath) {
if (!fileExists(filePath)) {
return 0;
}
try {
Path path = Paths.get(filePath.replace("/", File.separator));
return Files.size(path);
} catch (IOException e) {
logger.error("获取文件大小失败: {}", filePath, e);
return 0;
}
}
/**
* 格式化文件大小
*/
public static String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
/**
* 验证文件
*/
private static void validateFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw BusinessException.invalidParameter("file");
}
if (!isImageFile(file)) {
throw BusinessException.invalidFileFormat();
}
if (!isValidFileSize(file)) {
throw BusinessException.fileSizeExceeded();
}
}
/**
* 获取文件的MIME类型
*/
public static String getContentType(String filename) {
String extension = getFileExtension(filename).toLowerCase();
switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "bmp":
return "image/bmp";
case "webp":
return "image/webp";
default:
return "application/octet-stream";
}
}
/**
* 清理过期文件(可用于定时任务)
*/
public static void cleanupExpiredFiles(String directoryPath, int daysToKeep) {
// TODO: 实现文件清理逻辑
logger.info("清理过期文件: {} 天前的文件", daysToKeep);
}
}

View File

@@ -0,0 +1,246 @@
package com.aida.lanecarford.util;
import java.util.regex.Pattern;
/**
* 验证工具类
*
* @author AI Assistant
* @since 2024-01-01
*/
public class ValidationUtil {
/**
* 邮箱正则表达式
*/
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
);
/**
* 手机号正则表达式(中国大陆)
*/
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^1[3-9]\\d{9}$"
);
/**
* 密码正则表达式至少8位包含字母和数字
*/
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
"^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$"
);
/**
* 用户名正则表达式4-20位字母、数字、下划线
*/
private static final Pattern USERNAME_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_]{4,20}$"
);
/**
* 验证邮箱格式
*/
public static boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
/**
* 验证手机号格式
*/
public static boolean isValidPhone(String phone) {
return phone != null && PHONE_PATTERN.matcher(phone).matches();
}
/**
* 验证密码强度
*/
public static boolean isValidPassword(String password) {
return password != null && PASSWORD_PATTERN.matcher(password).matches();
}
/**
* 验证用户名格式
*/
public static boolean isValidUsername(String username) {
return username != null && USERNAME_PATTERN.matcher(username).matches();
}
/**
* 验证字符串是否为空或null
*/
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
/**
* 验证字符串是否不为空
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
/**
* 验证字符串长度是否在指定范围内
*/
public static boolean isValidLength(String str, int minLength, int maxLength) {
if (str == null) {
return false;
}
int length = str.length();
return length >= minLength && length <= maxLength;
}
/**
* 验证数字是否在指定范围内
*/
public static boolean isInRange(Number number, Number min, Number max) {
if (number == null || min == null || max == null) {
return false;
}
double value = number.doubleValue();
double minValue = min.doubleValue();
double maxValue = max.doubleValue();
return value >= minValue && value <= maxValue;
}
/**
* 验证是否为正整数
*/
public static boolean isPositiveInteger(Number number) {
if (number == null) {
return false;
}
return number.longValue() > 0 && number.doubleValue() == number.longValue();
}
/**
* 验证是否为非负整数
*/
public static boolean isNonNegativeInteger(Number number) {
if (number == null) {
return false;
}
return number.longValue() >= 0 && number.doubleValue() == number.longValue();
}
/**
* 验证身份证号格式(简单验证)
*/
public static boolean isValidIdCard(String idCard) {
if (isEmpty(idCard)) {
return false;
}
// 18位身份证号
if (idCard.length() == 18) {
return Pattern.matches("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$", idCard);
}
// 15位身份证号
if (idCard.length() == 15) {
return Pattern.matches("^[1-9]\\d{5}\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}$", idCard);
}
return false;
}
/**
* 验证URL格式
*/
public static boolean isValidUrl(String url) {
if (isEmpty(url)) {
return false;
}
return Pattern.matches("^(https?|ftp)://[^\\s/$.?#].[^\\s]*$", url);
}
/**
* 验证IP地址格式
*/
public static boolean isValidIpAddress(String ip) {
if (isEmpty(ip)) {
return false;
}
return Pattern.matches("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$", ip);
}
/**
* 验证颜色代码格式(十六进制)
*/
public static boolean isValidColorCode(String colorCode) {
if (isEmpty(colorCode)) {
return false;
}
return Pattern.matches("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", colorCode);
}
/**
* 验证日期格式yyyy-MM-dd
*/
public static boolean isValidDateFormat(String date) {
if (isEmpty(date)) {
return false;
}
return Pattern.matches("^\\d{4}-\\d{2}-\\d{2}$", date);
}
/**
* 验证时间格式HH:mm:ss
*/
public static boolean isValidTimeFormat(String time) {
if (isEmpty(time)) {
return false;
}
return Pattern.matches("^([01]?\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", time);
}
/**
* 验证数字字符串
*/
public static boolean isNumeric(String str) {
if (isEmpty(str)) {
return false;
}
return Pattern.matches("^-?\\d+(\\.\\d+)?$", str);
}
/**
* 验证只包含字母
*/
public static boolean isAlpha(String str) {
if (isEmpty(str)) {
return false;
}
return Pattern.matches("^[a-zA-Z]+$", str);
}
/**
* 验证只包含字母和数字
*/
public static boolean isAlphanumeric(String str) {
if (isEmpty(str)) {
return false;
}
return Pattern.matches("^[a-zA-Z0-9]+$", str);
}
/**
* 验证中文字符
*/
public static boolean isChinese(String str) {
if (isEmpty(str)) {
return false;
}
return Pattern.matches("^[\\u4e00-\\u9fa5]+$", str);
}
}

View File

@@ -0,0 +1,50 @@
# Spring Boot 应用配置
spring:
# 应用基本信息
application:
name: lanecarford-ai-styling-assistant
# 数据源配置 - MySQL
datasource:
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
# 服务器配置
server:
port: 8080
# Swagger 文档配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
# 日志配置
logging:
level:
com.aida.lanecarford: DEBUG

View File

@@ -0,0 +1,146 @@
-- Lane Carford AI系统基础架构数据库表结构
-- 创建数据库
CREATE DATABASE IF NOT EXISTS lanecarford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE lanecarford;
-- 1. 导购表
CREATE TABLE sales (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '导购ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(加密后)',
real_name VARCHAR(100) NOT NULL COMMENT '真实姓名',
employee_id VARCHAR(50) UNIQUE COMMENT '员工编号',
store_id VARCHAR(50) COMMENT '门店ID',
store_name VARCHAR(100) COMMENT '门店名称',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(100) COMMENT '邮箱',
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_username (username),
INDEX idx_employee_id (employee_id),
INDEX idx_store_id (store_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导购表';
-- 2. 顾客表
CREATE TABLE customers (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客ID',
name VARCHAR(100) NOT NULL COMMENT '顾客姓名',
email VARCHAR(100) NOT NULL COMMENT '顾客邮箱',
phone VARCHAR(20) COMMENT '手机号',
gender VARCHAR(10) COMMENT '性别',
age_range VARCHAR(20) COMMENT '年龄段',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_email (email),
INDEX idx_phone (phone),
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客表';
-- 3. 进店记录表
CREATE TABLE visit_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '进店记录ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
sales_id BIGINT NOT NULL COMMENT '导购ID',
visit_date DATE NOT NULL COMMENT '进店日期',
visit_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '进店时间',
session_id VARCHAR(100) COMMENT '会话ID',
status TINYINT DEFAULT 1 COMMENT '状态(0-已结束,1-进行中)',
notes TEXT COMMENT '备注',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (sales_id) REFERENCES sales(id) ON DELETE CASCADE,
INDEX idx_customer_id (customer_id),
INDEX idx_sales_id (sales_id),
INDEX idx_visit_date (visit_date),
INDEX idx_session_id (session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='进店记录表';
-- 4. 风格配置表
CREATE TABLE styles (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '风格配置ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
is_selected TINYINT DEFAULT 0 COMMENT '是否选中(0-未选中,1-已选中)',
style_image_url VARCHAR(500) COMMENT '风格图片URL',
python_request_id VARCHAR(100) COMMENT 'Python请求ID',
generation_status TINYINT DEFAULT 0 COMMENT '生成状态(0-处理中,1-已完成,2-失败)',
error_message TEXT COMMENT '错误信息',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
INDEX idx_customer_id (customer_id),
INDEX idx_visit_record_id (visit_record_id),
INDEX idx_python_request_id (python_request_id),
INDEX idx_is_selected (is_selected),
INDEX idx_generation_status (generation_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='风格配置表';
-- 5. 模特照片表
CREATE TABLE model_photos (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '模特照片ID',
photo_url VARCHAR(500) NOT NULL COMMENT '模特照片URL',
photo_name VARCHAR(200) COMMENT '照片名称',
gender VARCHAR(10) NOT NULL COMMENT '性别',
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
sort_order INT DEFAULT 0 COMMENT '排序权重',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_gender (gender),
INDEX idx_is_active (is_active),
INDEX idx_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模特照片表';
-- 6. 顾客照片表
CREATE TABLE customer_photos (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客照片ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
photo_url VARCHAR(500) NOT NULL COMMENT '照片URL',
is_primary TINYINT DEFAULT 0 COMMENT '是否为主照片(0-否,1-是)',
upload_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
INDEX idx_customer_id (customer_id),
INDEX idx_visit_record_id (visit_record_id),
INDEX idx_is_primary (is_primary)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客照片表';
-- 8. 试穿效果表
CREATE TABLE try_on_effects (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '试穿效果ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
styles_id BIGINT NOT NULL COMMENT '风格ID',
model_photo_id BIGINT COMMENT '模特照片ID',
customer_photo_id BIGINT COMMENT '顾客照片ID',
prompt VARCHAR(500) COMMENT '提示词',
original_try_on_id BIGINT COMMENT '原试穿效果ID,当is_regenerated为1时才会有值',
is_regenerated TINYINT DEFAULT 0 COMMENT '是否由生成结果重新生成(0-否,1-是)',
result_image_url VARCHAR(500) COMMENT '试穿结果图片URL',
request_id VARCHAR(100) COMMENT '请求ID',
generation_status VARCHAR(20) DEFAULT 'pending' COMMENT '生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)',
error_message TEXT COMMENT '错误信息',
is_favorite TINYINT DEFAULT 0 COMMENT '是否喜欢的最终造型(0-否,1-是)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
FOREIGN KEY (styles_id) REFERENCES styles(id) ON DELETE CASCADE,
FOREIGN KEY (model_photo_id) REFERENCES model_photos(id) ON DELETE SET NULL,
FOREIGN KEY (customer_photo_id) REFERENCES customer_photos(id) ON DELETE SET NULL,
INDEX idx_customer_id (customer_id),
INDEX idx_visit_record_id (visit_record_id),
INDEX idx_styles_id (styles_id),
INDEX idx_request_id (request_id),
INDEX idx_generation_status (generation_status),
INDEX idx_is_favorite (is_favorite)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='试穿效果表';

View File

@@ -0,0 +1,13 @@
package com.aida.lanecarford;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class LanecarfordApplicationTests {
@Test
void contextLoads() {
}
}