From 8d04a0fd6eadf53718c3b71932211179a12af5ea Mon Sep 17 00:00:00 2001 From: wurenzhi Date: Mon, 30 Mar 2026 16:55:04 +0800 Subject: [PATCH] first commit --- README.md | 75 + docs/00_Project_Overview.md | 90 + docs/01_Architecture_Design.md | 181 ++ docs/02_API_Documentation.md | 1965 +++++++++++++++++ docs/03_Deployment_Guide.md | 363 +++ docs/04_Test_Guide.md | 182 ++ docs/05_Database_Design.md | 306 +++ docs/06_Security_Guide.md | 297 +++ docs/07_Maintenance_Guide.md | 292 +++ docs/08_Internationalization_Guide.md | 245 ++ docs/09_Development_Guide.md | 340 +++ docs/10_API_Changelog.md | 335 +++ index.md | 83 + pom.xml | 101 + .../java/com/crawlful/hub/Application.java | 11 + .../hub/api/controllers/AlertController.java | 199 ++ .../hub/api/controllers/AuditController.java | 155 ++ .../hub/api/controllers/AuthController.java | 57 + .../hub/api/controllers/ConfigController.java | 165 ++ .../hub/api/controllers/DataController.java | 133 ++ .../api/controllers/LogisticsController.java | 159 ++ .../api/controllers/MonitoringController.java | 129 ++ .../hub/api/controllers/OrderController.java | 521 +++++ .../api/controllers/PaymentController.java | 165 ++ .../api/controllers/ProductController.java | 208 ++ .../hub/api/controllers/ReportController.java | 192 ++ .../hub/api/controllers/UserController.java | 128 ++ .../com/crawlful/hub/config/CacheConfig.java | 40 + .../config/InternationalizationConfig.java | 44 + .../crawlful/hub/config/OpenApiConfig.java | 28 + .../crawlful/hub/config/RateLimitFilter.java | 92 + .../crawlful/hub/config/SecurityConfig.java | 36 + .../hub/exception/BusinessException.java | 20 + .../hub/exception/GlobalExceptionHandler.java | 58 + .../java/com/crawlful/hub/model/Alert.java | 131 ++ .../java/com/crawlful/hub/model/Audit.java | 131 ++ .../java/com/crawlful/hub/model/Config.java | 109 + .../com/crawlful/hub/model/Logistics.java | 131 ++ .../java/com/crawlful/hub/model/Order.java | 164 ++ .../java/com/crawlful/hub/model/Payment.java | 120 + .../java/com/crawlful/hub/model/Product.java | 209 ++ .../java/com/crawlful/hub/model/User.java | 98 + .../crawlful/hub/service/AlertRepository.java | 15 + .../crawlful/hub/service/AlertService.java | 110 + .../crawlful/hub/service/AuditRepository.java | 16 + .../crawlful/hub/service/AuditService.java | 110 + .../com/crawlful/hub/service/AuthService.java | 98 + .../hub/service/ConfigRepository.java | 14 + .../crawlful/hub/service/ConfigService.java | 104 + .../com/crawlful/hub/service/DataService.java | 169 ++ .../hub/service/LogisticsRepository.java | 13 + .../hub/service/LogisticsService.java | 121 + .../hub/service/MonitoringService.java | 142 ++ .../crawlful/hub/service/OrderRepository.java | 21 + .../crawlful/hub/service/OrderService.java | 252 +++ .../hub/service/PaymentRepository.java | 13 + .../crawlful/hub/service/PaymentService.java | 97 + .../hub/service/ProductRepository.java | 30 + .../crawlful/hub/service/ProductService.java | 164 ++ .../crawlful/hub/service/ReportService.java | 246 +++ .../crawlful/hub/service/UserRepository.java | 13 + .../com/crawlful/hub/service/UserService.java | 73 + .../com/crawlful/hub/util/ValidationUtil.java | 80 + src/main/resources/application.yml | 36 + .../db/migration/V1__init_schema.sql | 130 ++ .../db/migration/V2__add_alert_table.sql | 21 + src/main/resources/i18n/messages.properties | 62 + .../resources/i18n/messages_zh.properties | 62 + src/main/resources/logback.xml | 83 + .../integration/SystemIntegrationTest.java | 208 ++ .../crawlful/hub/service/AuthServiceTest.java | 62 + .../hub/service/OrderServiceTest.java | 72 + .../hub/service/ProductServiceTest.java | 68 + target/classes/application.yml | 36 + .../com/crawlful/hub/Application.class | Bin 0 -> 721 bytes .../hub/api/controllers/AlertController.class | Bin 0 -> 8365 bytes .../hub/api/controllers/AuditController.class | Bin 0 -> 7025 bytes .../hub/api/controllers/AuthController.class | Bin 0 -> 3170 bytes .../api/controllers/ConfigController.class | Bin 0 -> 7835 bytes .../hub/api/controllers/DataController.class | Bin 0 -> 5730 bytes .../api/controllers/LogisticsController.class | Bin 0 -> 7056 bytes .../controllers/MonitoringController.class | Bin 0 -> 4293 bytes .../hub/api/controllers/OrderController.class | Bin 0 -> 18848 bytes .../api/controllers/PaymentController.class | Bin 0 -> 7205 bytes .../api/controllers/ProductController.class | Bin 0 -> 8047 bytes .../api/controllers/ReportController.class | Bin 0 -> 7146 bytes .../hub/api/controllers/UserController.class | Bin 0 -> 5785 bytes .../com/crawlful/hub/config/CacheConfig.class | Bin 0 -> 3704 bytes .../config/InternationalizationConfig.class | Bin 0 -> 2425 bytes .../crawlful/hub/config/OpenApiConfig.class | Bin 0 -> 1675 bytes .../config/RateLimitFilter$RequestRate.class | Bin 0 -> 1104 bytes .../crawlful/hub/config/RateLimitFilter.class | Bin 0 -> 2214 bytes .../crawlful/hub/config/SecurityConfig.class | Bin 0 -> 4202 bytes .../hub/exception/BusinessException.class | Bin 0 -> 770 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 4381 bytes .../com/crawlful/hub/model/Alert.class | Bin 0 -> 3289 bytes .../com/crawlful/hub/model/Audit.class | Bin 0 -> 3320 bytes .../com/crawlful/hub/model/Config.class | Bin 0 -> 2916 bytes .../com/crawlful/hub/model/Logistics.class | Bin 0 -> 3483 bytes .../com/crawlful/hub/model/Order.class | Bin 0 -> 4251 bytes .../com/crawlful/hub/model/Payment.class | Bin 0 -> 3232 bytes .../com/crawlful/hub/model/Product.class | Bin 0 -> 5207 bytes .../classes/com/crawlful/hub/model/User.class | Bin 0 -> 2606 bytes .../hub/service/AlertRepository.class | Bin 0 -> 1146 bytes .../crawlful/hub/service/AlertService.class | Bin 0 -> 5652 bytes .../hub/service/AuditRepository.class | Bin 0 -> 1356 bytes .../crawlful/hub/service/AuditService.class | Bin 0 -> 6269 bytes .../crawlful/hub/service/AuthService.class | Bin 0 -> 4525 bytes .../hub/service/ConfigRepository.class | Bin 0 -> 1233 bytes .../crawlful/hub/service/ConfigService.class | Bin 0 -> 5762 bytes .../crawlful/hub/service/DataService.class | Bin 0 -> 9896 bytes .../hub/service/LogisticsRepository.class | Bin 0 -> 1120 bytes .../hub/service/LogisticsService.class | Bin 0 -> 6500 bytes .../hub/service/MonitoringService.class | Bin 0 -> 5247 bytes .../hub/service/OrderRepository.class | Bin 0 -> 1801 bytes .../crawlful/hub/service/OrderService.class | Bin 0 -> 11457 bytes .../hub/service/PaymentRepository.class | Bin 0 -> 1104 bytes .../crawlful/hub/service/PaymentService.class | Bin 0 -> 5758 bytes .../hub/service/ProductRepository.class | Bin 0 -> 2997 bytes .../crawlful/hub/service/ProductService.class | Bin 0 -> 7978 bytes .../crawlful/hub/service/ReportService.class | Bin 0 -> 11140 bytes .../crawlful/hub/service/UserRepository.class | Bin 0 -> 782 bytes .../crawlful/hub/service/UserService.class | Bin 0 -> 3479 bytes .../crawlful/hub/util/ValidationUtil.class | Bin 0 -> 3172 bytes .../classes/db/migration/V1__init_schema.sql | 130 ++ .../db/migration/V2__add_alert_table.sql | 21 + target/classes/i18n/messages.properties | 62 + target/classes/i18n/messages_zh.properties | 62 + target/classes/logback.xml | 83 + .../integration/SystemIntegrationTest.class | Bin 0 -> 6354 bytes .../hub/service/AuthServiceTest.class | Bin 0 -> 1611 bytes .../hub/service/OrderServiceTest.class | Bin 0 -> 2208 bytes .../hub/service/ProductServiceTest.class | Bin 0 -> 2127 bytes 133 files changed, 11587 insertions(+) create mode 100644 README.md create mode 100644 docs/00_Project_Overview.md create mode 100644 docs/01_Architecture_Design.md create mode 100644 docs/02_API_Documentation.md create mode 100644 docs/03_Deployment_Guide.md create mode 100644 docs/04_Test_Guide.md create mode 100644 docs/05_Database_Design.md create mode 100644 docs/06_Security_Guide.md create mode 100644 docs/07_Maintenance_Guide.md create mode 100644 docs/08_Internationalization_Guide.md create mode 100644 docs/09_Development_Guide.md create mode 100644 docs/10_API_Changelog.md create mode 100644 index.md create mode 100644 pom.xml create mode 100644 src/main/java/com/crawlful/hub/Application.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/AlertController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/AuditController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/AuthController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/ConfigController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/DataController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/LogisticsController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/MonitoringController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/OrderController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/PaymentController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/ProductController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/ReportController.java create mode 100644 src/main/java/com/crawlful/hub/api/controllers/UserController.java create mode 100644 src/main/java/com/crawlful/hub/config/CacheConfig.java create mode 100644 src/main/java/com/crawlful/hub/config/InternationalizationConfig.java create mode 100644 src/main/java/com/crawlful/hub/config/OpenApiConfig.java create mode 100644 src/main/java/com/crawlful/hub/config/RateLimitFilter.java create mode 100644 src/main/java/com/crawlful/hub/config/SecurityConfig.java create mode 100644 src/main/java/com/crawlful/hub/exception/BusinessException.java create mode 100644 src/main/java/com/crawlful/hub/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/crawlful/hub/model/Alert.java create mode 100644 src/main/java/com/crawlful/hub/model/Audit.java create mode 100644 src/main/java/com/crawlful/hub/model/Config.java create mode 100644 src/main/java/com/crawlful/hub/model/Logistics.java create mode 100644 src/main/java/com/crawlful/hub/model/Order.java create mode 100644 src/main/java/com/crawlful/hub/model/Payment.java create mode 100644 src/main/java/com/crawlful/hub/model/Product.java create mode 100644 src/main/java/com/crawlful/hub/model/User.java create mode 100644 src/main/java/com/crawlful/hub/service/AlertRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/AlertService.java create mode 100644 src/main/java/com/crawlful/hub/service/AuditRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/AuditService.java create mode 100644 src/main/java/com/crawlful/hub/service/AuthService.java create mode 100644 src/main/java/com/crawlful/hub/service/ConfigRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/ConfigService.java create mode 100644 src/main/java/com/crawlful/hub/service/DataService.java create mode 100644 src/main/java/com/crawlful/hub/service/LogisticsRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/LogisticsService.java create mode 100644 src/main/java/com/crawlful/hub/service/MonitoringService.java create mode 100644 src/main/java/com/crawlful/hub/service/OrderRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/OrderService.java create mode 100644 src/main/java/com/crawlful/hub/service/PaymentRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/PaymentService.java create mode 100644 src/main/java/com/crawlful/hub/service/ProductRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/ProductService.java create mode 100644 src/main/java/com/crawlful/hub/service/ReportService.java create mode 100644 src/main/java/com/crawlful/hub/service/UserRepository.java create mode 100644 src/main/java/com/crawlful/hub/service/UserService.java create mode 100644 src/main/java/com/crawlful/hub/util/ValidationUtil.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/migration/V1__init_schema.sql create mode 100644 src/main/resources/db/migration/V2__add_alert_table.sql create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/i18n/messages_zh.properties create mode 100644 src/main/resources/logback.xml create mode 100644 src/test/java/com/crawlful/hub/integration/SystemIntegrationTest.java create mode 100644 src/test/java/com/crawlful/hub/service/AuthServiceTest.java create mode 100644 src/test/java/com/crawlful/hub/service/OrderServiceTest.java create mode 100644 src/test/java/com/crawlful/hub/service/ProductServiceTest.java create mode 100644 target/classes/application.yml create mode 100644 target/classes/com/crawlful/hub/Application.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/AlertController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/AuditController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/AuthController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/ConfigController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/DataController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/LogisticsController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/MonitoringController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/OrderController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/PaymentController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/ProductController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/ReportController.class create mode 100644 target/classes/com/crawlful/hub/api/controllers/UserController.class create mode 100644 target/classes/com/crawlful/hub/config/CacheConfig.class create mode 100644 target/classes/com/crawlful/hub/config/InternationalizationConfig.class create mode 100644 target/classes/com/crawlful/hub/config/OpenApiConfig.class create mode 100644 target/classes/com/crawlful/hub/config/RateLimitFilter$RequestRate.class create mode 100644 target/classes/com/crawlful/hub/config/RateLimitFilter.class create mode 100644 target/classes/com/crawlful/hub/config/SecurityConfig.class create mode 100644 target/classes/com/crawlful/hub/exception/BusinessException.class create mode 100644 target/classes/com/crawlful/hub/exception/GlobalExceptionHandler.class create mode 100644 target/classes/com/crawlful/hub/model/Alert.class create mode 100644 target/classes/com/crawlful/hub/model/Audit.class create mode 100644 target/classes/com/crawlful/hub/model/Config.class create mode 100644 target/classes/com/crawlful/hub/model/Logistics.class create mode 100644 target/classes/com/crawlful/hub/model/Order.class create mode 100644 target/classes/com/crawlful/hub/model/Payment.class create mode 100644 target/classes/com/crawlful/hub/model/Product.class create mode 100644 target/classes/com/crawlful/hub/model/User.class create mode 100644 target/classes/com/crawlful/hub/service/AlertRepository.class create mode 100644 target/classes/com/crawlful/hub/service/AlertService.class create mode 100644 target/classes/com/crawlful/hub/service/AuditRepository.class create mode 100644 target/classes/com/crawlful/hub/service/AuditService.class create mode 100644 target/classes/com/crawlful/hub/service/AuthService.class create mode 100644 target/classes/com/crawlful/hub/service/ConfigRepository.class create mode 100644 target/classes/com/crawlful/hub/service/ConfigService.class create mode 100644 target/classes/com/crawlful/hub/service/DataService.class create mode 100644 target/classes/com/crawlful/hub/service/LogisticsRepository.class create mode 100644 target/classes/com/crawlful/hub/service/LogisticsService.class create mode 100644 target/classes/com/crawlful/hub/service/MonitoringService.class create mode 100644 target/classes/com/crawlful/hub/service/OrderRepository.class create mode 100644 target/classes/com/crawlful/hub/service/OrderService.class create mode 100644 target/classes/com/crawlful/hub/service/PaymentRepository.class create mode 100644 target/classes/com/crawlful/hub/service/PaymentService.class create mode 100644 target/classes/com/crawlful/hub/service/ProductRepository.class create mode 100644 target/classes/com/crawlful/hub/service/ProductService.class create mode 100644 target/classes/com/crawlful/hub/service/ReportService.class create mode 100644 target/classes/com/crawlful/hub/service/UserRepository.class create mode 100644 target/classes/com/crawlful/hub/service/UserService.class create mode 100644 target/classes/com/crawlful/hub/util/ValidationUtil.class create mode 100644 target/classes/db/migration/V1__init_schema.sql create mode 100644 target/classes/db/migration/V2__add_alert_table.sql create mode 100644 target/classes/i18n/messages.properties create mode 100644 target/classes/i18n/messages_zh.properties create mode 100644 target/classes/logback.xml create mode 100644 target/test-classes/com/crawlful/hub/integration/SystemIntegrationTest.class create mode 100644 target/test-classes/com/crawlful/hub/service/AuthServiceTest.class create mode 100644 target/test-classes/com/crawlful/hub/service/OrderServiceTest.class create mode 100644 target/test-classes/com/crawlful/hub/service/ProductServiceTest.class diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8dc5dc --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Crawlful Hub Java Backend + +基于 Spring Boot 3.x 的 Java 后端实现,用于 Crawlful Hub 项目。 + +## 技术栈 + +- **框架**: Spring Boot 3.2.0 +- **Java 版本**: Java 17 +- **数据库**: MySQL 8.0 +- **缓存**: Redis +- **认证**: JWT +- **构建工具**: Maven + +## 功能模块 + +- 认证与授权 +- 商品管理 +- 订单管理 +- 支付处理 +- 物流跟踪 +- 数据采集 +- 报表分析 +- 系统监控 +- 告警管理 +- 配置管理 +- 审计日志 + +## 快速开始 + +### 环境要求 + +- JDK 17+ +- MySQL 8.0+ +- Redis 6.0+ +- Maven 3.6+ + +### 配置 + +编辑 `src/main/resources/application.yml` 文件,配置数据库连接、Redis 连接等信息。 + +### 运行 + +```bash +mvn spring-boot:run +``` + +### 构建 + +```bash +mvn clean package +``` + +### 运行测试 + +```bash +mvn test +``` + +## 文档 + +- [项目概览](docs/00_Project_Overview.md) +- [架构设计](docs/01_Architecture_Design.md) +- [API 文档](docs/02_API_Documentation.md) +- [部署指南](docs/03_Deployment_Guide.md) +- [测试指南](docs/04_Test_Guide.md) +- [数据库设计](docs/05_Database_Design.md) +- [安全指南](docs/06_Security_Guide.md) +- [维护指南](docs/07_Maintenance_Guide.md) +- [国际化指南](docs/08_Internationalization_Guide.md) +- [开发指南](docs/09_Development_Guide.md) +- [API 变更日志](docs/10_API_Changelog.md) + +## 许可证 + +MIT License diff --git a/docs/00_Project_Overview.md b/docs/00_Project_Overview.md new file mode 100644 index 0000000..b076401 --- /dev/null +++ b/docs/00_Project_Overview.md @@ -0,0 +1,90 @@ +# 项目概览 + +## 1. 项目简介 + +Crawlful Hub 是一个基于 Spring Boot 3.x 的后端服务,用于管理商品、订单、支付、物流等业务流程。本项目是从原有的 Node.js 后端平移而来,保持了与原系统的 API 兼容性,同时提供了更强大的性能和可扩展性。 + +## 2. 技术栈 + +- **框架**: Spring Boot 3.2.0 +- **语言**: Java 17 +- **数据库**: MySQL 8.0 +- **缓存**: Redis +- **认证**: JWT +- **构建工具**: Maven +- **API 文档**: Springdoc OpenAPI +- **日志系统**: Logback +- **测试**: JUnit 5 + +## 3. 核心功能 + +- **用户认证与授权**: 基于 JWT 的身份验证和基于角色的访问控制 +- **商品管理**: 商品的创建、查询、更新和删除 +- **订单管理**: 订单的创建、查询、更新和状态管理 +- **支付管理**: 支付的创建、查询、更新和状态管理 +- **物流管理**: 物流的创建、查询、更新和状态管理 +- **系统监控**: 系统健康状态、性能指标、服务状态等 +- **告警管理**: 系统告警的创建、查询、更新和处理 +- **审计日志**: 系统操作的审计记录 +- **配置管理**: 系统配置的管理 +- **数据管理**: 数据的导入、导出和处理 +- **报表生成**: 系统报表的生成 + +## 4. 项目结构 + +``` +serverjava/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/ +│ │ │ └── crawlful/ +│ │ │ └── hub/ +│ │ │ ├── api/ # API 控制器 +│ │ │ ├── config/ # 系统配置 +│ │ │ ├── exception/ # 异常处理 +│ │ │ ├── model/ # 数据模型 +│ │ │ ├── service/ # 业务逻辑 +│ │ │ ├── util/ # 工具类 +│ │ │ └── Application.java # 应用入口 +│ │ └── resources/ +│ │ ├── db/ # 数据库迁移脚本 +│ │ ├── i18n/ # 国际化资源 +│ │ ├── application.yml # 应用配置 +│ │ └── logback.xml # 日志配置 +│ └── test/ # 测试代码 +├── docs/ # 项目文档 +├── README.md # 项目说明 +├── index.md # 项目索引 +└── pom.xml # Maven 配置 +``` + +## 5. API 兼容性 + +本项目保持了与原 Node.js 后端的 API 兼容性,确保前端和客户端代码无需修改即可正常工作。API 路径、参数和返回格式都与原系统保持一致。 + +## 6. 性能优化 + +- **数据库连接池**: 使用 HikariCP 优化数据库连接管理 +- **缓存**: 使用 Redis 缓存高频访问数据 +- **分页查询**: 实现分页查询,避免一次性加载大量数据 +- **索引优化**: 为数据库表添加适当的索引,提高查询性能 + +## 7. 安全增强 + +- **CSRF 保护**: 实现 CSRF 令牌验证 +- **速率限制**: 限制 API 请求频率,防止恶意请求 +- **密码加密**: 使用 BCrypt 加密用户密码 +- **JWT 认证**: 使用 JWT 进行身份验证,确保 API 安全 + +## 8. 国际化支持 + +本项目支持国际化,提供了英文和中文的资源文件,可以根据请求的 Accept-Language 头自动切换语言。 + +## 9. 监控与告警 + +本项目实现了系统监控和告警功能,可以实时监控系统的健康状态、性能指标和服务状态,并在异常情况下生成告警。 + +## 10. 部署与运维 + +本项目使用 Maven 构建,可以部署到任何支持 Java 17 的环境中。详细的部署方法请参考部署文档。 diff --git a/docs/01_Architecture_Design.md b/docs/01_Architecture_Design.md new file mode 100644 index 0000000..d5a94d7 --- /dev/null +++ b/docs/01_Architecture_Design.md @@ -0,0 +1,181 @@ +# 架构设计 + +## 1. 架构概述 + +Crawlful Hub 采用分层架构设计,确保系统的可维护性、可扩展性和可测试性。系统分为控制器层、服务层、仓库层和模型层,各层之间通过依赖注入进行解耦,实现了高内聚、低耦合的设计目标。 + +## 2. 分层架构 + +### 2.1 控制器层(Controller) + +控制器层负责处理 HTTP 请求,包括请求参数的验证、调用服务层的方法、处理异常和返回响应。控制器层不包含业务逻辑,只负责请求和响应的处理。 + +**主要职责**: +- 接收 HTTP 请求 +- 验证请求参数 +- 调用服务层处理业务逻辑 +- 处理异常 +- 返回 HTTP 响应 + +**核心控制器**: +- `AuthController`:处理认证相关的请求 +- `UserController`:处理用户管理相关的请求 +- `ProductController`:处理商品管理相关的请求 +- `OrderController`:处理订单管理相关的请求 +- `PaymentController`:处理支付管理相关的请求 +- `LogisticsController`:处理物流管理相关的请求 +- `MonitoringController`:处理监控相关的请求 +- `AlertController`:处理告警相关的请求 +- `AuditController`:处理审计相关的请求 +- `ConfigController`:处理配置相关的请求 +- `DataController`:处理数据相关的请求 +- `ReportController`:处理报表相关的请求 + +### 2.2 服务层(Service) + +服务层是业务逻辑的核心,负责实现业务规则、处理业务流程和协调多个仓库的操作。服务层通过依赖注入使用仓库层的方法,实现业务逻辑的封装和复用。 + +**主要职责**: +- 实现业务逻辑 +- 处理业务流程 +- 协调多个仓库的操作 +- 管理事务 +- 处理异常 + +**核心服务**: +- `AuthService`:处理认证相关的业务逻辑 +- `UserService`:处理用户管理相关的业务逻辑 +- `ProductService`:处理商品管理相关的业务逻辑 +- `OrderService`:处理订单管理相关的业务逻辑 +- `PaymentService`:处理支付管理相关的业务逻辑 +- `LogisticsService`:处理物流管理相关的业务逻辑 +- `MonitoringService`:处理监控相关的业务逻辑 +- `AlertService`:处理告警相关的业务逻辑 +- `AuditService`:处理审计相关的业务逻辑 +- `ConfigService`:处理配置相关的业务逻辑 +- `DataService`:处理数据相关的业务逻辑 +- `ReportService`:处理报表相关的业务逻辑 + +### 2.3 仓库层(Repository) + +仓库层负责数据访问,包括数据库的 CRUD 操作。仓库层通过 Spring Data JPA 实现,提供了简洁的数据库操作接口。 + +**主要职责**: +- 实现数据库的 CRUD 操作 +- 提供查询方法 +- 管理数据库事务 + +**核心仓库**: +- `UserRepository`:处理用户数据的访问 +- `ProductRepository`:处理商品数据的访问 +- `OrderRepository`:处理订单数据的访问 +- `PaymentRepository`:处理支付数据的访问 +- `LogisticsRepository`:处理物流数据的访问 +- `AlertRepository`:处理告警数据的访问 +- `AuditRepository`:处理审计数据的访问 +- `ConfigRepository`:处理配置数据的访问 + +### 2.4 模型层(Model) + +模型层定义了数据结构,包括实体类和 DTO(数据传输对象)。实体类对应数据库表,DTO 用于在不同层之间传递数据。 + +**主要职责**: +- 定义数据结构 +- 映射数据库表 +- 提供数据访问方法 + +**核心模型**: +- `User`:用户模型 +- `Product`:商品模型 +- `Order`:订单模型 +- `Payment`:支付模型 +- `Logistics`:物流模型 +- `Alert`:告警模型 +- `Audit`:审计模型 +- `Config`:配置模型 + +## 3. 核心流程 + +### 3.1 用户认证流程 + +1. **用户注册**:用户提交注册信息,`AuthController` 调用 `AuthService` 的 `register` 方法,验证输入数据,创建用户并返回用户信息。 +2. **用户登录**:用户提交登录信息,`AuthController` 调用 `AuthService` 的 `login` 方法,验证用户名和密码,生成 JWT 令牌并返回。 +3. **令牌验证**:请求携带 JWT 令牌,`SecurityConfig` 验证令牌的有效性,确保用户已认证。 + +### 3.2 商品管理流程 + +1. **创建商品**:用户提交商品信息,`ProductController` 调用 `ProductService` 的 `create` 方法,验证输入数据,创建商品并返回商品信息。 +2. **查询商品**:用户请求商品列表,`ProductController` 调用 `ProductService` 的 `getAll` 方法,根据过滤条件查询商品并返回。 +3. **更新商品**:用户提交商品更新信息,`ProductController` 调用 `ProductService` 的 `update` 方法,验证输入数据,更新商品并返回更新后的商品信息。 +4. **删除商品**:用户请求删除商品,`ProductController` 调用 `ProductService` 的 `delete` 方法,删除商品并返回成功信息。 + +### 3.3 订单管理流程 + +1. **创建订单**:用户提交订单信息,`OrderController` 调用 `OrderService` 的 `createOrder` 方法,验证输入数据,创建订单并返回订单信息。 +2. **查询订单**:用户请求订单列表,`OrderController` 调用 `OrderService` 的 `getOrders` 方法,根据过滤条件查询订单并返回。 +3. **更新订单**:用户提交订单更新信息,`OrderController` 调用 `OrderService` 的 `updateOrder` 方法,验证输入数据,更新订单并返回更新后的订单信息。 +4. **订单状态流转**:用户请求更新订单状态,`OrderController` 调用 `OrderService` 的 `transitionOrderStatus` 方法,更新订单状态并返回成功信息。 + +### 3.4 支付管理流程 + +1. **创建支付**:用户提交支付信息,`PaymentController` 调用 `PaymentService` 的 `create` 方法,验证输入数据,创建支付并返回支付信息。 +2. **查询支付**:用户请求支付列表,`PaymentController` 调用 `PaymentService` 的 `getAll` 方法,根据过滤条件查询支付并返回。 +3. **更新支付**:用户提交支付更新信息,`PaymentController` 调用 `PaymentService` 的 `update` 方法,验证输入数据,更新支付并返回更新后的支付信息。 + +### 3.5 物流管理流程 + +1. **创建物流**:用户提交物流信息,`LogisticsController` 调用 `LogisticsService` 的 `create` 方法,验证输入数据,创建物流并返回物流信息。 +2. **查询物流**:用户请求物流列表,`LogisticsController` 调用 `LogisticsService` 的 `getAll` 方法,根据过滤条件查询物流并返回。 +3. **更新物流**:用户提交物流更新信息,`LogisticsController` 调用 `LogisticsService` 的 `update` 方法,验证输入数据,更新物流并返回更新后的物流信息。 + +### 3.6 监控与告警流程 + +1. **系统健康检查**:用户请求系统健康状态,`MonitoringController` 调用 `MonitoringService` 的 `getSystemHealth` 方法,返回系统健康状态。 +2. **性能指标获取**:用户请求系统性能指标,`MonitoringController` 调用 `MonitoringService` 的 `getPerformanceMetrics` 方法,返回系统性能指标。 +3. **告警创建**:系统检测到异常,`AlertService` 创建告警并存储到数据库。 +4. **告警处理**:用户请求处理告警,`AlertController` 调用 `AlertService` 的 `resolveAlert` 方法,更新告警状态为已解决。 + +## 4. 技术实现细节 + +### 4.1 数据库设计 + +- **用户表**:存储用户信息,包括用户名、密码、邮箱、角色等。 +- **商品表**:存储商品信息,包括标题、描述、价格、数量等。 +- **订单表**:存储订单信息,包括订单号、状态、总金额等。 +- **支付表**:存储支付信息,包括支付方式、金额、状态等。 +- **物流表**:存储物流信息,包括物流方式、跟踪号、状态等。 +- **告警表**:存储告警信息,包括告警类型、严重程度、状态等。 +- **审计表**:存储审计日志,包括操作类型、资源类型、操作人等。 +- **配置表**:存储系统配置,包括配置键、配置值、配置类型等。 + +### 4.2 缓存实现 + +使用 Redis 作为缓存存储,缓存高频访问的数据,如用户信息、商品信息等,提高系统的响应速度。 + +### 4.3 安全实现 + +- **JWT 认证**:使用 JWT 进行身份验证,确保 API 安全。 +- **CSRF 保护**:实现 CSRF 令牌验证,防止 CSRF 攻击。 +- **速率限制**:限制 API 请求频率,防止恶意请求。 +- **密码加密**:使用 BCrypt 加密用户密码,确保密码安全。 + +### 4.4 国际化实现 + +使用 Spring 的国际化支持,提供英文和中文的资源文件,根据请求的 Accept-Language 头自动切换语言。 + +### 4.5 监控与告警实现 + +- **系统健康检查**:实现系统健康状态检查,包括数据库连接、缓存状态等。 +- **性能指标监控**:实现系统性能指标监控,包括响应时间、请求次数等。 +- **告警管理**:实现告警的创建、查询、更新和处理,及时发现和处理系统异常。 + +## 5. 扩展性设计 + +- **模块化设计**:系统采用模块化设计,各模块之间通过接口进行通信,便于功能扩展和代码维护。 +- **依赖注入**:使用 Spring 的依赖注入,实现组件的解耦,便于单元测试和功能扩展。 +- **配置化**:系统的配置通过 application.yml 文件进行配置,便于环境切换和参数调整。 +- **插件化**:系统支持插件化开发,可以通过插件扩展系统功能。 + +## 6. 总结 + +Crawlful Hub 采用分层架构设计,实现了高内聚、低耦合的系统结构。系统通过控制器层、服务层、仓库层和模型层的划分,实现了业务逻辑的封装和复用,提高了系统的可维护性、可扩展性和可测试性。同时,系统通过缓存、安全、国际化等技术的应用,提高了系统的性能和用户体验。 diff --git a/docs/02_API_Documentation.md b/docs/02_API_Documentation.md new file mode 100644 index 0000000..8243108 --- /dev/null +++ b/docs/02_API_Documentation.md @@ -0,0 +1,1965 @@ +# API 文档 + +## 1. 认证 API + +### 1.1 注册用户 + +**路径**: `/v1/auth/register` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| username | string | 是 | 用户名 | +| password | string | 是 | 密码 | +| email | string | 是 | 邮箱 | +| role | string | 是 | 角色 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 1.2 用户登录 + +**路径**: `/v1/auth/login` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| username | string | 是 | 用户名 | +| password | string | 是 | 密码 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +## 2. 用户 API + +### 2.1 创建用户 + +**路径**: `/v1/user` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| username | string | 是 | 用户名 | +| password | string | 是 | 密码 | +| email | string | 是 | 邮箱 | +| role | string | 是 | 角色 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 2.2 获取用户列表 + +**路径**: `/v1/user` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "ADMIN", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### 2.3 获取用户详情 + +**路径**: `/v1/user/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 用户 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "ADMIN", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 2.4 更新用户 + +**路径**: `/v1/user/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 用户 ID | +| username | string | 否 | 用户名 | +| password | string | 否 | 密码 | +| email | string | 否 | 邮箱 | +| role | string | 否 | 角色 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "ADMIN", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 2.5 删除用户 + +**路径**: `/v1/user/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 用户 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +## 3. 商品 API + +### 3.1 创建商品 + +**路径**: `/v1/product` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| title | string | 是 | 商品标题 | +| description | string | 否 | 商品描述 | +| mainImage | string | 否 | 商品主图 | +| platform | string | 否 | 平台 | +| platformProductId | string | 否 | 平台商品 ID | +| price | double | 否 | 价格 | +| costPrice | double | 否 | 成本价格 | +| quantity | int | 否 | 数量 | +| status | string | 否 | 状态 | +| phash | string | 否 | 图片哈希 | +| semanticHash | string | 否 | 语义哈希 | +| vectorEmbedding | string | 否 | 向量嵌入 | +| attributes | string | 否 | 属性 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 3.2 获取商品列表 + +**路径**: `/v1/product` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| platform | string | 否 | 平台 | +| status | string | 否 | 状态 | +| page | int | 否 | 页码,默认 1 | +| size | int | 否 | 每页数量,默认 10 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "title": "商品标题", + "description": "商品描述", + "mainImage": "图片 URL", + "platform": "平台", + "platformProductId": "平台商品 ID", + "price": 100.00, + "costPrice": 80.00, + "quantity": 100, + "status": "ACTIVE", + "phash": "图片哈希", + "semanticHash": "语义哈希", + "vectorEmbedding": "向量嵌入", + "attributes": "属性", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ], + "page": 1, + "size": 10 +} +``` + +### 3.3 获取商品详情 + +**路径**: `/v1/product/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 商品 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "title": "商品标题", + "description": "商品描述", + "mainImage": "图片 URL", + "platform": "平台", + "platformProductId": "平台商品 ID", + "price": 100.00, + "costPrice": 80.00, + "quantity": 100, + "status": "ACTIVE", + "phash": "图片哈希", + "semanticHash": "语义哈希", + "vectorEmbedding": "向量嵌入", + "attributes": "属性", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 3.4 更新商品 + +**路径**: `/v1/product/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 商品 ID | +| title | string | 否 | 商品标题 | +| description | string | 否 | 商品描述 | +| mainImage | string | 否 | 商品主图 | +| platform | string | 否 | 平台 | +| platformProductId | string | 否 | 平台商品 ID | +| price | double | 否 | 价格 | +| costPrice | double | 否 | 成本价格 | +| quantity | int | 否 | 数量 | +| status | string | 否 | 状态 | +| phash | string | 否 | 图片哈希 | +| semanticHash | string | 否 | 语义哈希 | +| vectorEmbedding | string | 否 | 向量嵌入 | +| attributes | string | 否 | 属性 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "title": "商品标题", + "description": "商品描述", + "mainImage": "图片 URL", + "platform": "平台", + "platformProductId": "平台商品 ID", + "price": 100.00, + "costPrice": 80.00, + "quantity": 100, + "status": "ACTIVE", + "phash": "图片哈希", + "semanticHash": "语义哈希", + "vectorEmbedding": "向量嵌入", + "attributes": "属性", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 3.5 删除商品 + +**路径**: `/v1/product/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 商品 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 3.6 商品清洗与本地化 + +**路径**: `/v1/product/{id}/wash-and-localize` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 商品 ID | +| tenantId | string | 是 | 租户 ID | +| targetMarket | string | 是 | 目标市场 | +| targetLang | string | 是 | 目标语言 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "title": "商品标题 (Localized for 目标市场)", + "description": "商品描述 (Localized for 目标市场)", + "targetMarket": "目标市场", + "targetLang": "目标语言", + "status": "LOCALIZED" + } +} +``` + +### 3.7 商品套利分析 + +**路径**: `/v1/product/{id}/analyze-arbitrage` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 商品 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "productId": 1, + "price": 100.00, + "costPrice": 80.00, + "profitMargin": 20.0, + "arbitrageOpportunity": true + } +} +``` + +## 4. 订单 API + +### 4.1 创建订单 + +**路径**: `/v1/order` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| shopId | string | 否 | 店铺 ID | +| platform | string | 否 | 平台 | +| platformOrderId | string | 否 | 平台订单 ID | +| status | string | 否 | 状态 | +| totalAmount | double | 否 | 总金额 | +| currency | string | 否 | 货币 | +| customerInfo | string | 否 | 客户信息 | +| items | string | 否 | 商品列表 | +| shippingAddress | string | 否 | shipping 地址 | +| trackingNumber | string | 否 | 跟踪号 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 4.2 获取订单列表 + +**路径**: `/v1/order` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| platform | string | 否 | 平台 | +| status | string | 否 | 状态 | +| page | int | 否 | 页码 | +| pageSize | int | 否 | 每页数量 | +| startDate | string | 否 | 开始日期 | +| endDate | string | 否 | 结束日期 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "shopId": "店铺 ID", + "platform": "平台", + "platformOrderId": "平台订单 ID", + "status": "PENDING", + "totalAmount": 100.00, + "currency": "USD", + "customerInfo": "客户信息", + "items": "商品列表", + "shippingAddress": "shipping 地址", + "trackingNumber": "跟踪号", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### 4.3 获取订单详情 + +**路径**: `/v1/order/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "shopId": "店铺 ID", + "platform": "平台", + "platformOrderId": "平台订单 ID", + "status": "PENDING", + "totalAmount": 100.00, + "currency": "USD", + "customerInfo": "客户信息", + "items": "商品列表", + "shippingAddress": "shipping 地址", + "trackingNumber": "跟踪号", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 4.4 更新订单 + +**路径**: `/v1/order/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| status | string | 否 | 状态 | +| totalAmount | double | 否 | 总金额 | +| currency | string | 否 | 货币 | +| customerInfo | string | 否 | 客户信息 | +| items | string | 否 | 商品列表 | +| shippingAddress | string | 否 | shipping 地址 | +| trackingNumber | string | 否 | 跟踪号 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "shopId": "店铺 ID", + "platform": "平台", + "platformOrderId": "平台订单 ID", + "status": "PENDING", + "totalAmount": 100.00, + "currency": "USD", + "customerInfo": "客户信息", + "items": "商品列表", + "shippingAddress": "shipping 地址", + "trackingNumber": "跟踪号", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 4.5 删除订单 + +**路径**: `/v1/order/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.6 订单状态流转 + +**路径**: `/v1/order/{id}/transition` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| status | string | 是 | 目标状态 | +| reason | string | 否 | 流转原因 | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.7 批量更新订单 + +**路径**: `/v1/order/batch-update` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| orderIds | array | 是 | 订单 ID 列表 | +| updates | object | 是 | 更新内容 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "successCount": 1, + "failureCount": 0, + "totalCount": 1 + } +} +``` + +### 4.8 批量审核订单 + +**路径**: `/v1/order/batch-audit` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| orderIds | array | 是 | 订单 ID 列表 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "successCount": 1, + "failureCount": 0, + "totalCount": 1 + } +} +``` + +### 4.9 批量发货 + +**路径**: `/v1/order/batch-ship` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| orderIds | array | 是 | 订单 ID 列表 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "successCount": 1, + "failureCount": 0, + "totalCount": 1 + } +} +``` + +### 4.10 标记订单异常 + +**路径**: `/v1/order/{id}/exception` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| reason | string | 是 | 异常原因 | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.11 自动改派订单 + +**路径**: `/v1/order/{id}/auto-reroute` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.12 重试异常订单 + +**路径**: `/v1/order/{id}/retry` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.13 取消订单 + +**路径**: `/v1/order/{id}/cancel` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| reason | string | 是 | 取消原因 | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.14 申请退款 + +**路径**: `/v1/order/{id}/refund` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| reason | string | 是 | 退款原因 | +| amount | double | 是 | 退款金额 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "refundId": "REFUND_1234567890" + } +} +``` + +### 4.15 审批退款 + +**路径**: `/v1/order/{id}/refund/approve` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| approved | boolean | 是 | 是否批准 | +| note | string | 否 | 审批备注 | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.16 申请售后 + +**路径**: `/v1/order/{id}/after-sales` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| type | string | 是 | 售后类型 | +| reason | string | 是 | 售后原因 | +| items | array | 是 | 售后商品列表 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "afterSalesId": "AFTER_SALES_1234567890" + } +} +``` + +### 4.17 处理售后 + +**路径**: `/v1/order/{id}/after-sales/process` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | +| action | string | 是 | 处理动作 | +| note | string | 否 | 处理备注 | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.18 完成订单 + +**路径**: `/v1/order/{id}/complete` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 订单 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 4.19 获取订单统计 + +**路径**: `/v1/order/stats` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "totalOrders": 100, + "pendingOrders": 20, + "shippedOrders": 50, + "completedOrders": 30 + } +} +``` + +## 5. 支付 API + +### 5.1 创建支付 + +**路径**: `/v1/payment` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| orderId | long | 否 | 订单 ID | +| paymentMethod | string | 否 | 支付方式 | +| amount | double | 否 | 金额 | +| currency | string | 否 | 货币 | +| status | string | 否 | 状态 | +| transactionId | string | 否 | 交易 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 5.2 获取支付列表 + +**路径**: `/v1/payment` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| orderId | long | 否 | 订单 ID | +| status | string | 否 | 状态 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "orderId": 1, + "paymentMethod": "credit_card", + "amount": 100.00, + "currency": "USD", + "status": "COMPLETED", + "transactionId": "TXN_1234567890", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### 5.3 获取支付详情 + +**路径**: `/v1/payment/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 支付 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "orderId": 1, + "paymentMethod": "credit_card", + "amount": 100.00, + "currency": "USD", + "status": "COMPLETED", + "transactionId": "TXN_1234567890", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 5.4 更新支付 + +**路径**: `/v1/payment/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 支付 ID | +| paymentMethod | string | 否 | 支付方式 | +| amount | double | 否 | 金额 | +| currency | string | 否 | 货币 | +| status | string | 否 | 状态 | +| transactionId | string | 否 | 交易 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "orderId": 1, + "paymentMethod": "credit_card", + "amount": 100.00, + "currency": "USD", + "status": "COMPLETED", + "transactionId": "TXN_1234567890", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 5.5 删除支付 + +**路径**: `/v1/payment/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 支付 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +## 6. 物流 API + +### 6.1 创建物流 + +**路径**: `/v1/logistics` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| orderId | long | 否 | 订单 ID | +| shippingMethod | string | 否 | 物流方式 | +| trackingNumber | string | 否 | 跟踪号 | +| carrier | string | 否 | 物流公司 | +| status | string | 否 | 状态 | +| estimatedDeliveryDate | string | 否 | 预计送达日期 | +| actualDeliveryDate | string | 否 | 实际送达日期 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 6.2 获取物流列表 + +**路径**: `/v1/logistics` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| orderId | long | 否 | 订单 ID | +| status | string | 否 | 状态 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "orderId": 1, + "shippingMethod": "standard", + "trackingNumber": "TRK_1234567890", + "carrier": "UPS", + "status": "DELIVERED", + "estimatedDeliveryDate": "2024-01-10T00:00:00.000Z", + "actualDeliveryDate": "2024-01-09T00:00:00.000Z", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-09T00:00:00.000Z" + } + ] +} +``` + +### 6.3 获取物流详情 + +**路径**: `/v1/logistics/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 物流 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "orderId": 1, + "shippingMethod": "standard", + "trackingNumber": "TRK_1234567890", + "carrier": "UPS", + "status": "DELIVERED", + "estimatedDeliveryDate": "2024-01-10T00:00:00.000Z", + "actualDeliveryDate": "2024-01-09T00:00:00.000Z", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-09T00:00:00.000Z" + } +} +``` + +### 6.4 更新物流 + +**路径**: `/v1/logistics/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 物流 ID | +| shippingMethod | string | 否 | 物流方式 | +| trackingNumber | string | 否 | 跟踪号 | +| carrier | string | 否 | 物流公司 | +| status | string | 否 | 状态 | +| estimatedDeliveryDate | string | 否 | 预计送达日期 | +| actualDeliveryDate | string | 否 | 实际送达日期 | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "orderId": 1, + "shippingMethod": "standard", + "trackingNumber": "TRK_1234567890", + "carrier": "UPS", + "status": "DELIVERED", + "estimatedDeliveryDate": "2024-01-10T00:00:00.000Z", + "actualDeliveryDate": "2024-01-09T00:00:00.000Z", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-09T00:00:00.000Z" + } +} +``` + +### 6.5 删除物流 + +**路径**: `/v1/logistics/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 物流 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +## 7. 监控 API + +### 7.1 获取系统健康状态 + +**路径**: `/v1/monitoring/health` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "status": "UP", + "components": { + "database": { + "status": "UP" + }, + "cache": { + "status": "UP" + } + } + } +} +``` + +### 7.2 获取性能指标 + +**路径**: `/v1/monitoring/metrics` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "jvm": { + "memory": { + "used": 1024, + "total": 2048 + } + }, + "system": { + "cpu": { + "usage": 0.5 + } + } + } +} +``` + +### 7.3 获取服务状态 + +**路径**: `/v1/monitoring/services` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "authService": { + "status": "UP" + }, + "productService": { + "status": "UP" + }, + "orderService": { + "status": "UP" + } + } +} +``` + +### 7.4 获取数据库状态 + +**路径**: `/v1/monitoring/database` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "status": "UP", + "connections": { + "active": 5, + "max": 10 + } + } +} +``` + +### 7.5 获取缓存状态 + +**路径**: `/v1/monitoring/cache` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "status": "UP", + "keys": 100 + } +} +``` + +### 7.6 获取系统统计信息 + +**路径**: `/v1/monitoring/stats` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "data": { + "requests": { + "total": 1000, + "success": 990, + "error": 10 + } + } +} +``` + +### 7.7 Ping 测试 + +**路径**: `/v1/monitoring/ping` + +**方法**: `GET` + +**返回格式**: +```json +{ + "success": true, + "message": "Pong", + "timestamp": 1234567890 +} +``` + +## 8. 告警 API + +### 8.1 创建告警 + +**路径**: `/v1/alerts` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| alertType | string | 否 | 告警类型 | +| severity | string | 否 | 严重程度 | +| message | string | 否 | 告警消息 | +| status | string | 否 | 状态 | +| source | string | 否 | 告警来源 | +| threshold | string | 否 | 阈值 | +| actualValue | string | 否 | 实际值 | + +**返回格式**: +```json +{ + "success": true, + "alertId": 1 +} +``` + +### 8.2 获取告警列表 + +**路径**: `/v1/alerts` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| status | string | 否 | 状态 | +| severity | string | 否 | 严重程度 | +| alertType | string | 否 | 告警类型 | +| startDate | string | 否 | 开始日期 | +| endDate | string | 否 | 结束日期 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "tenantId": "租户 ID", + "alertType": "system", + "severity": "high", + "message": "系统异常", + "status": "ACTIVE", + "source": "monitoring", + "threshold": "90%", + "actualValue": "95%", + "createdAt": "2024-01-01T00:00:00.000Z", + "resolvedAt": null + } + ] +} +``` + +### 8.3 获取告警详情 + +**路径**: `/v1/alerts/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 告警 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "tenantId": "租户 ID", + "alertType": "system", + "severity": "high", + "message": "系统异常", + "status": "ACTIVE", + "source": "monitoring", + "threshold": "90%", + "actualValue": "95%", + "createdAt": "2024-01-01T00:00:00.000Z", + "resolvedAt": null + } +} +``` + +### 8.4 解决告警 + +**路径**: `/v1/alerts/{id}/resolve` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 告警 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "message": "Alert resolved successfully" +} +``` + +### 8.5 更新告警状态 + +**路径**: `/v1/alerts/{id}/status` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 告警 ID | +| tenantId | string | 是 | 租户 ID | +| status | string | 是 | 新状态 | + +**返回格式**: +```json +{ + "success": true, + "message": "Alert status updated successfully" +} +``` + +### 8.6 获取告警统计 + +**路径**: `/v1/alerts/stats` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| startDate | string | 是 | 开始日期 | +| endDate | string | 是 | 结束日期 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "totalAlerts": 10, + "severityStats": { + "high": 2, + "medium": 5, + "low": 3 + }, + "statusStats": { + "ACTIVE": 3, + "RESOLVED": 7 + }, + "typeStats": { + "system": 5, + "application": 3, + "database": 2 + }, + "startDate": "2024-01-01", + "endDate": "2024-01-31" + } +} +``` + +### 8.7 检查阈值 + +**路径**: `/v1/alerts/check-thresholds` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "message": "Thresholds checked successfully" +} +``` + +## 9. 审计 API + +### 9.1 获取审计日志列表 + +**路径**: `/v1/audit` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| shopId | string | 否 | 店铺 ID | +| userId | long | 否 | 用户 ID | +| action | string | 否 | 操作类型 | +| resourceType | string | 否 | 资源类型 | +| startDate | string | 否 | 开始日期 | +| endDate | string | 否 | 结束日期 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "userId": 1, + "action": "create", + "resourceType": "product", + "resourceId": "1", + "ipAddress": "192.168.1.1", + "userAgent": "Mozilla/5.0", + "details": "创建商品", + "createdAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### 9.2 获取审计日志详情 + +**路径**: `/v1/audit/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 审计日志 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "userId": 1, + "action": "create", + "resourceType": "product", + "resourceId": "1", + "ipAddress": "192.168.1.1", + "userAgent": "Mozilla/5.0", + "details": "创建商品", + "createdAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +## 10. 配置 API + +### 10.1 创建配置 + +**路径**: `/v1/config` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 否 | 租户 ID | +| shopId | string | 否 | 店铺 ID | +| configKey | string | 是 | 配置键 | +| configValue | string | 是 | 配置值 | +| configType | string | 否 | 配置类型 | +| description | string | 否 | 配置描述 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1 + } +} +``` + +### 10.2 获取配置列表 + +**路径**: `/v1/config` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 否 | 租户 ID | +| shopId | string | 否 | 店铺 ID | +| configType | string | 否 | 配置类型 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "configKey": "api_key", + "configValue": "value", + "configType": "string", + "description": "API 密钥", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### 10.3 获取配置详情 + +**路径**: `/v1/config/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 配置 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "configKey": "api_key", + "configValue": "value", + "configType": "string", + "description": "API 密钥", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### 10.4 更新配置 + +**路径**: `/v1/config/{id}` + +**方法**: `PUT` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 配置 ID | +| configValue | string | 否 | 配置值 | +| configType | string | 否 | 配置类型 | +| description | string | 否 | 配置描述 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "configKey": "api_key", + "configValue": "new_value", + "configType": "string", + "description": "API 密钥", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-02T00:00:00.000Z" + } +} +``` + +### 10.5 删除配置 + +**路径**: `/v1/config/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | long | 是 | 配置 ID | + +**返回格式**: +```json +{ + "success": true +} +``` + +### 10.6 根据键获取配置 + +**路径**: `/v1/config/key/{key}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| key | string | 是 | 配置键 | +| tenantId | string | 否 | 租户 ID | +| shopId | string | 否 | 店铺 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": 1, + "tenantId": "租户 ID", + "shopId": "店铺 ID", + "configKey": "api_key", + "configValue": "value", + "configType": "string", + "description": "API 密钥", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +## 11. 数据 API + +### 11.1 导入数据 + +**路径**: `/v1/data/import` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| dataType | string | 是 | 数据类型 | +| data | array | 是 | 数据列表 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "importedCount": 10, + "failedCount": 0 + } +} +``` + +### 11.2 导出数据 + +**路径**: `/v1/data/export` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| dataType | string | 是 | 数据类型 | +| filters | object | 否 | 过滤条件 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "数据 1" + } + ] +} +``` + +### 11.3 同步数据 + +**路径**: `/v1/data/sync` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| source | string | 是 | 数据源 | +| target | string | 是 | 目标 | +| filters | object | 否 | 过滤条件 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "syncedCount": 10, + "failedCount": 0 + } +} +``` + +## 12. 报表 API + +### 12.1 生成报表 + +**路径**: `/v1/report/generate` + +**方法**: `POST` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| reportType | string | 是 | 报表类型 | +| startDate | string | 是 | 开始日期 | +| endDate | string | 是 | 结束日期 | +| filters | object | 否 | 过滤条件 | + +**返回格式**: +```json +{ + "success": true, + "data": { + "reportId": "REPORT_1234567890", + "url": "http://example.com/report/REPORT_1234567890" + } +} +``` + +### 12.2 获取报表列表 + +**路径**: `/v1/report` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| tenantId | string | 是 | 租户 ID | +| reportType | string | 否 | 报表类型 | +| startDate | string | 否 | 开始日期 | +| endDate | string | 否 | 结束日期 | + +**返回格式**: +```json +{ + "success": true, + "data": [ + { + "id": "REPORT_1234567890", + "reportType": "sales", + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "createdAt": "2024-02-01T00:00:00.000Z", + "url": "http://example.com/report/REPORT_1234567890" + } + ] +} +``` + +### 12.3 获取报表详情 + +**路径**: `/v1/report/{id}` + +**方法**: `GET` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 报表 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true, + "data": { + "id": "REPORT_1234567890", + "reportType": "sales", + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "createdAt": "2024-02-01T00:00:00.000Z", + "url": "http://example.com/report/REPORT_1234567890" + } +} +``` + +### 12.4 删除报表 + +**路径**: `/v1/report/{id}` + +**方法**: `DELETE` + +**参数**: +| 参数名 | 类型 | 必填 | 描述 | +|-------|------|------|------| +| id | string | 是 | 报表 ID | +| tenantId | string | 是 | 租户 ID | + +**返回格式**: +```json +{ + "success": true +} +``` diff --git a/docs/03_Deployment_Guide.md b/docs/03_Deployment_Guide.md new file mode 100644 index 0000000..957f457 --- /dev/null +++ b/docs/03_Deployment_Guide.md @@ -0,0 +1,363 @@ +# 部署指南 + +## 1. 环境要求 + +### 1.1 硬件要求 +- **CPU**: 至少 2 核 +- **内存**: 至少 4GB +- **磁盘**: 至少 50GB 可用空间 + +### 1.2 软件要求 +- **Java**: JDK 17 或更高版本 +- **MySQL**: 8.0 或更高版本 +- **Redis**: 6.0 或更高版本 +- **Maven**: 3.6 或更高版本 + +## 2. 安装步骤 + +### 2.1 安装 Java + +**Windows 系统**: +1. 下载 JDK 17 安装包:[Oracle JDK 17](https://www.oracle.com/java/technologies/downloads/#java17) +2. 运行安装包,按照提示完成安装 +3. 配置环境变量: + - 右键点击「此电脑」→「属性」→「高级系统设置」→「环境变量」 + - 在「系统变量」中添加 `JAVA_HOME`,值为 JDK 安装目录 + - 在「系统变量」的 `Path` 中添加 `%JAVA_HOME%\bin` +4. 验证安装:打开命令提示符,运行 `java -version`,显示 JDK 版本信息 + +**Linux 系统**: +1. 下载 JDK 17 安装包: + ```bash + wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm + ``` +2. 安装 JDK: + ```bash + sudo rpm -ivh jdk-17_linux-x64_bin.rpm + ``` +3. 验证安装: + ```bash + java -version + ``` + +### 2.2 安装 MySQL + +**Windows 系统**: +1. 下载 MySQL 8.0 安装包:[MySQL Community Server](https://dev.mysql.com/downloads/mysql/) +2. 运行安装包,按照提示完成安装 +3. 配置 MySQL: + - 启动 MySQL 服务 + - 登录 MySQL,修改 root 密码 + - 创建数据库:`CREATE DATABASE crawlful_hub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +**Linux 系统**: +1. 安装 MySQL: + ```bash + sudo yum install mysql-server + ``` +2. 启动 MySQL 服务: + ```bash + sudo systemctl start mysqld + ``` +3. 配置 MySQL: + ```bash + sudo mysql_secure_installation + ``` +4. 创建数据库: + ```bash + mysql -u root -p + CREATE DATABASE crawlful_hub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + ``` + +### 2.3 安装 Redis + +**Windows 系统**: +1. 下载 Redis 安装包:[Redis for Windows](https://github.com/tporadowski/redis/releases) +2. 运行安装包,按照提示完成安装 +3. 启动 Redis 服务: + ```bash + redis-server + ``` + +**Linux 系统**: +1. 安装 Redis: + ```bash + sudo yum install redis + ``` +2. 启动 Redis 服务: + ```bash + sudo systemctl start redis + ``` + +### 2.4 安装 Maven + +**Windows 系统**: +1. 下载 Maven 安装包:[Apache Maven](https://maven.apache.org/download.cgi) +2. 解压安装包到指定目录 +3. 配置环境变量: + - 右键点击「此电脑」→「属性」→「高级系统设置」→「环境变量」 + - 在「系统变量」中添加 `MAVEN_HOME`,值为 Maven 安装目录 + - 在「系统变量」的 `Path` 中添加 `%MAVEN_HOME%\bin` +4. 验证安装:打开命令提示符,运行 `mvn -version`,显示 Maven 版本信息 + +**Linux 系统**: +1. 安装 Maven: + ```bash + sudo yum install maven + ``` +2. 验证安装: + ```bash + mvn -version + ``` + +## 3. 项目配置 + +### 3.1 数据库配置 + +编辑 `src/main/resources/application.yml` 文件,修改数据库连接信息: + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/crawlful_hub?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver +``` + +### 3.2 Redis 配置 + +编辑 `src/main/resources/application.yml` 文件,修改 Redis 连接信息: + +```yaml +spring: + redis: + host: localhost + port: 6379 + password: + database: 0 +``` + +### 3.3 JWT 配置 + +编辑 `src/main/resources/application.yml` 文件,修改 JWT 配置: + +```yaml +spring: + security: + jwt: + secret: your-secret-key + expiration: 86400000 +``` + +### 3.4 服务器配置 + +编辑 `src/main/resources/application.yml` 文件,修改服务器配置: + +```yaml +server: + port: 3001 + servlet: + context-path: /api +``` + +## 4. 构建与部署 + +### 4.1 构建项目 + +1. 进入项目目录: + ```bash + cd d:\trae_projects\makemd\makemd\serverjava + ``` + +2. 执行 Maven 构建: + ```bash + mvn clean package + ``` + +3. 构建成功后,在 `target` 目录中生成 `serverjava-0.0.1-SNAPSHOT.jar` 文件。 + +### 4.2 运行项目 + +**方法一:直接运行** + +1. 进入 `target` 目录: + ```bash + cd target + ``` + +2. 运行 jar 文件: + ```bash + java -jar serverjava-0.0.1-SNAPSHOT.jar + ``` + +**方法二:作为服务运行** + +**Windows 系统**: +1. 创建服务脚本: + ```batch + @echo off + set JAVA_HOME=C:\Program Files\Java\jdk-17 + set PATH=%JAVA_HOME%\bin;%PATH% + java -jar d:\trae_projects\makemd\makemd\serverjava\target\serverjava-0.0.1-SNAPSHOT.jar + ``` + +2. 将脚本保存为 `serverjava.bat`,然后运行。 + +**Linux 系统**: +1. 创建服务文件: + ```bash + sudo nano /etc/systemd/system/serverjava.service + ``` + +2. 添加以下内容: + ``` + [Unit] + Description=ServerJava Service + After=network.target + + [Service] + Type=simple + User=root + ExecStart=/usr/bin/java -jar /path/to/serverjava/target/serverjava-0.0.1-SNAPSHOT.jar + Restart=on-failure + + [Install] + WantedBy=multi-user.target + ``` + +3. 启动服务: + ```bash + sudo systemctl start serverjava + sudo systemctl enable serverjava + ``` + +## 5. 验证部署 + +1. 打开浏览器,访问 `http://localhost:3001/api/api-docs`,查看 API 文档。 + +2. 访问 `http://localhost:3001/api/v1/monitoring/health`,查看系统健康状态。 + +3. 访问 `http://localhost:3001/api/v1/monitoring/ping`,测试系统响应。 + +## 6. 日志管理 + +### 6.1 日志配置 + +编辑 `src/main/resources/logback.xml` 文件,修改日志配置: + +```xml + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + logs/server.log + + logs/server.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + +``` + +### 6.2 日志查看 + +**Windows 系统**: +1. 进入项目目录,查看日志文件: + ```bash + cd d:\trae_projects\makemd\makemd\serverjava + type logs\server.log + ``` + +**Linux 系统**: +1. 进入项目目录,查看日志文件: + ```bash + cd /path/to/serverjava + tail -f logs/server.log + ``` + +## 7. 故障排查 + +### 7.1 常见问题 + +1. **数据库连接失败** + - 检查数据库服务是否运行 + - 检查数据库连接配置是否正确 + - 检查数据库用户权限是否正确 + +2. **Redis 连接失败** + - 检查 Redis 服务是否运行 + - 检查 Redis 连接配置是否正确 + +3. **端口被占用** + - 检查端口是否被其他服务占用 + - 修改 `application.yml` 中的端口配置 + +4. **JWT 验证失败** + - 检查 JWT 密钥是否正确 + - 检查 JWT 令牌是否过期 + +### 7.2 调试方法 + +1. **查看日志**:查看 `logs/server.log` 文件,了解系统运行状态和错误信息。 + +2. **使用 API 文档**:访问 `http://localhost:3001/api/api-docs`,测试 API 接口。 + +3. **使用健康检查**:访问 `http://localhost:3001/api/v1/monitoring/health`,检查系统健康状态。 + +4. **使用 Ping 测试**:访问 `http://localhost:3001/api/v1/monitoring/ping`,测试系统响应。 + +## 8. 升级与维护 + +### 8.1 升级步骤 + +1. 停止服务: + ```bash + sudo systemctl stop serverjava + ``` + +2. 备份数据: + ```bash + mysqldump -u root -p crawlful_hub > crawlful_hub_backup.sql + ``` + +3. 拉取最新代码: + ```bash + git pull + ``` + +4. 重新构建项目: + ```bash + mvn clean package + ``` + +5. 启动服务: + ```bash + sudo systemctl start serverjava + ``` + +### 8.2 维护计划 + +1. **定期备份**:定期备份数据库和配置文件。 + +2. **更新依赖**:定期更新项目依赖,确保系统安全。 + +3. **监控系统**:使用监控工具监控系统运行状态,及时发现和处理异常。 + +4. **性能优化**:根据系统运行情况,优化数据库查询、缓存策略等。 + +## 9. 总结 + +本部署指南详细介绍了 Crawlful Hub 项目的部署方法、环境要求和配置步骤。通过本指南,您可以快速部署和维护系统,确保系统的稳定运行。 diff --git a/docs/04_Test_Guide.md b/docs/04_Test_Guide.md new file mode 100644 index 0000000..84b8390 --- /dev/null +++ b/docs/04_Test_Guide.md @@ -0,0 +1,182 @@ +# 测试指南 + +## 1. 测试概述 + +Crawlful Hub 项目采用了全面的测试策略,包括单元测试、集成测试和系统测试,确保系统的稳定性和可靠性。本指南详细介绍了项目的测试方法和测试策略。 + +## 2. 测试环境 + +### 2.1 硬件要求 +- **CPU**: 至少 2 核 +- **内存**: 至少 4GB +- **磁盘**: 至少 50GB 可用空间 + +### 2.2 软件要求 +- **Java**: JDK 17 或更高版本 +- **MySQL**: 8.0 或更高版本 +- **Redis**: 6.0 或更高版本 +- **Maven**: 3.6 或更高版本 +- **JUnit 5**: 项目集成的测试框架 + +## 3. 测试类型 + +### 3.1 单元测试 + +单元测试是对系统中最小的可测试单元进行测试,通常是对单个方法或类的测试。单元测试的目的是验证每个单元是否按照预期工作。 + +**核心测试类**: +- `AuthServiceTest.java`:测试认证服务的功能 +- `OrderServiceTest.java`:测试订单服务的功能 +- `ProductServiceTest.java`:测试商品服务的功能 + +### 3.2 集成测试 + +集成测试是对系统中多个组件的交互进行测试,验证组件之间的协作是否正确。集成测试的目的是确保系统的各个组件能够正确地协同工作。 + +**核心测试类**: +- `SystemIntegrationTest.java`:测试整个系统的集成功能 + +### 3.3 系统测试 + +系统测试是对整个系统的功能进行测试,验证系统是否满足需求规格。系统测试的目的是确保系统能够按照预期工作,满足用户的需求。 + +**测试方法**: +- 使用 API 文档测试 API 接口 +- 使用健康检查端点测试系统状态 +- 使用监控端点测试系统性能 + +## 4. 测试工具 + +### 4.1 JUnit 5 + +JUnit 5 是 Java 中最流行的测试框架,用于编写和运行单元测试。Crawlful Hub 项目使用 JUnit 5 进行单元测试和集成测试。 + +### 4.2 Mockito + +Mockito 是一个用于 Java 的 mocking 框架,用于创建和配置模拟对象。Crawlful Hub 项目使用 Mockito 模拟依赖对象,便于单元测试。 + +### 4.3 Spring Test + +Spring Test 是 Spring 框架提供的测试工具,用于测试 Spring 应用。Crawlful Hub 项目使用 Spring Test 测试 Spring 组件。 + +### 4.4 Postman + +Postman 是一个用于测试 API 的工具,用于发送 HTTP 请求并查看响应。Crawlful Hub 项目使用 Postman 测试 API 接口。 + +## 5. 测试执行 + +### 5.1 运行单元测试 + +1. 进入项目目录: + ```bash + cd d:\trae_projects\makemd\makemd\serverjava + ``` + +2. 执行单元测试: + ```bash + mvn test + ``` + +### 5.2 运行集成测试 + +1. 进入项目目录: + ```bash + cd d:\trae_projects\makemd\makemd\serverjava + ``` + +2. 执行集成测试: + ```bash + mvn verify -Pintegration-test + ``` + +### 5.3 运行系统测试 + +1. 启动系统: + ```bash + java -jar target/serverjava-0.0.1-SNAPSHOT.jar + ``` + +2. 使用 Postman 测试 API 接口: + - 访问 `http://localhost:3001/api/api-docs`,获取 API 文档 + - 使用 Postman 发送请求,测试 API 接口 + +3. 使用健康检查端点测试系统状态: + - 访问 `http://localhost:3001/api/v1/monitoring/health` + +4. 使用监控端点测试系统性能: + - 访问 `http://localhost:3001/api/v1/monitoring/metrics` + +## 6. 测试策略 + +### 6.1 测试覆盖率 + +Crawlful Hub 项目的测试覆盖率目标是: +- **单元测试覆盖率**:至少 80% +- **集成测试覆盖率**:至少 60% +- **系统测试覆盖率**:至少 40% + +### 6.2 测试用例设计 + +测试用例设计遵循以下原则: +- **边界值测试**:测试边界条件,如空值、最大值、最小值等 +- **等价类测试**:测试等价类,如有效输入、无效输入等 +- **异常测试**:测试异常情况,如参数错误、系统错误等 +- **场景测试**:测试实际场景,如用户注册、登录、下单等 + +### 6.3 测试数据 + +测试数据的设计遵循以下原则: +- **真实数据**:使用真实的业务数据进行测试 +- **边界数据**:使用边界值进行测试 +- **异常数据**:使用异常数据进行测试 +- **覆盖所有场景**:确保测试数据覆盖所有业务场景 + +## 7. 测试报告 + +### 7.1 单元测试报告 + +运行单元测试后,Maven 会生成单元测试报告,位于 `target/surefire-reports` 目录。 + +### 7.2 集成测试报告 + +运行集成测试后,Maven 会生成集成测试报告,位于 `target/failsafe-reports` 目录。 + +### 7.3 代码覆盖率报告 + +使用 JaCoCo 插件生成代码覆盖率报告,位于 `target/site/jacoco` 目录。 + +## 8. 测试最佳实践 + +### 8.1 单元测试最佳实践 + +- **测试单个方法**:每个单元测试只测试一个方法 +- **使用断言**:使用断言验证测试结果 +- **模拟依赖**:使用 Mockito 模拟依赖对象 +- **测试边界条件**:测试边界值和异常情况 +- **保持测试简单**:测试代码应该简洁明了 + +### 8.2 集成测试最佳实践 + +- **测试组件交互**:测试组件之间的协作 +- **使用真实依赖**:使用真实的依赖对象 +- **测试业务流程**:测试完整的业务流程 +- **保持测试独立**:每个集成测试应该独立运行 + +### 8.3 系统测试最佳实践 + +- **测试完整功能**:测试系统的完整功能 +- **使用真实环境**:使用真实的环境进行测试 +- **测试用户场景**:测试实际的用户场景 +- **性能测试**:测试系统的性能 + +## 9. 测试自动化 + +Crawlful Hub 项目使用 Maven 进行测试自动化,配置了以下测试目标: + +- **mvn test**:运行单元测试 +- **mvn verify**:运行单元测试和集成测试 +- **mvn clean package**:构建项目并运行测试 + +## 10. 总结 + +本测试指南详细介绍了 Crawlful Hub 项目的测试方法和测试策略。通过本指南,您可以了解如何编写和运行测试,确保系统的稳定性和可靠性。 diff --git a/docs/05_Database_Design.md b/docs/05_Database_Design.md new file mode 100644 index 0000000..25687ea --- /dev/null +++ b/docs/05_Database_Design.md @@ -0,0 +1,306 @@ +# 数据库设计 + +## 1. 数据库概述 + +Crawlful Hub 项目使用 MySQL 8.0 作为数据库,采用了关系型数据库设计,确保数据的一致性和完整性。本文档详细介绍了项目的数据库表结构、索引和关系。 + +## 2. 数据库表结构 + +### 2.1 用户表(cf_user) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 用户 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| username | VARCHAR(255) | NOT NULL, UNIQUE | 用户名 | +| password | VARCHAR(255) | NOT NULL | 密码(加密存储) | +| email | VARCHAR(255) | NOT NULL, UNIQUE | 邮箱 | +| role | VARCHAR(50) | | 角色 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +### 2.2 商品表(cf_product) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 商品 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| shop_id | VARCHAR(255) | | 店铺 ID | +| title | VARCHAR(255) | NOT NULL | 商品标题 | +| description | TEXT | | 商品描述 | +| main_image | VARCHAR(255) | | 商品主图 | +| platform | VARCHAR(50) | | 平台 | +| platform_product_id | VARCHAR(255) | | 平台商品 ID | +| price | DECIMAL(10,2) | | 价格 | +| cost_price | DECIMAL(10,2) | | 成本价格 | +| quantity | INT | | 数量 | +| status | VARCHAR(50) | | 状态 | +| phash | VARCHAR(255) | | 图片哈希 | +| semantic_hash | VARCHAR(255) | | 语义哈希 | +| vector_embedding | TEXT | | 向量嵌入 | +| attributes | JSON | | 属性 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +### 2.3 订单表(cf_order) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 订单 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| shop_id | VARCHAR(255) | | 店铺 ID | +| platform | VARCHAR(50) | | 平台 | +| platform_order_id | VARCHAR(255) | | 平台订单 ID | +| status | VARCHAR(50) | | 状态 | +| total_amount | DECIMAL(10,2) | | 总金额 | +| currency | VARCHAR(10) | | 货币 | +| customer_info | JSON | | 客户信息 | +| items | JSON | | 商品列表 | +| shipping_address | JSON | | shipping 地址 | +| tracking_number | VARCHAR(255) | | 跟踪号 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +### 2.4 支付表(cf_payment) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 支付 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| order_id | BIGINT | FOREIGN KEY (order_id) REFERENCES cf_order(id) | 订单 ID | +| payment_method | VARCHAR(50) | | 支付方式 | +| amount | DECIMAL(10,2) | | 金额 | +| currency | VARCHAR(10) | | 货币 | +| status | VARCHAR(50) | | 状态 | +| transaction_id | VARCHAR(255) | | 交易 ID | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +### 2.5 物流表(cf_logistics) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 物流 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| order_id | BIGINT | FOREIGN KEY (order_id) REFERENCES cf_order(id) | 订单 ID | +| shipping_method | VARCHAR(50) | | 物流方式 | +| tracking_number | VARCHAR(255) | | 跟踪号 | +| carrier | VARCHAR(50) | | 物流公司 | +| status | VARCHAR(50) | | 状态 | +| estimated_delivery_date | DATETIME | | 预计送达日期 | +| actual_delivery_date | DATETIME | | 实际送达日期 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +### 2.6 告警表(cf_alert) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 告警 ID | +| tenant_id | VARCHAR(255) | NOT NULL | 租户 ID | +| alert_type | VARCHAR(50) | | 告警类型 | +| severity | VARCHAR(50) | | 严重程度 | +| message | TEXT | | 告警消息 | +| status | VARCHAR(50) | | 状态 | +| source | VARCHAR(255) | | 告警来源 | +| threshold | VARCHAR(255) | | 阈值 | +| actual_value | VARCHAR(255) | | 实际值 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| resolved_at | DATETIME | | 解决时间 | + +### 2.7 审计表(cf_audit) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 审计日志 ID | +| tenant_id | VARCHAR(255) | | 租户 ID | +| shop_id | VARCHAR(255) | | 店铺 ID | +| user_id | BIGINT | | 用户 ID | +| action | VARCHAR(255) | | 操作类型 | +| resource_type | VARCHAR(255) | | 资源类型 | +| resource_id | VARCHAR(255) | | 资源 ID | +| ip_address | VARCHAR(100) | | IP 地址 | +| user_agent | TEXT | | 用户代理 | +| details | TEXT | | 详细信息 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +### 2.8 配置表(cf_config) + +| 字段名 | 数据类型 | 约束 | 描述 | +|-------|---------|------|------| +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 配置 ID | +| tenant_id | VARCHAR(255) | | 租户 ID | +| shop_id | VARCHAR(255) | | 店铺 ID | +| config_key | VARCHAR(255) | NOT NULL | 配置键 | +| config_value | VARCHAR(255) | NOT NULL | 配置值 | +| config_type | VARCHAR(50) | | 配置类型 | +| description | TEXT | | 配置描述 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +## 3. 索引设计 + +### 3.1 用户表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_user_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| UNIQUE | username | UNIQUE | 用户名唯一索引 | +| UNIQUE | email | UNIQUE | 邮箱唯一索引 | + +### 3.2 商品表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_product_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_product_platform | platform | INDEX | 平台索引 | + +### 3.3 订单表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_order_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_order_platform | platform | INDEX | 平台索引 | + +### 3.4 支付表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_payment_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_payment_order_id | order_id | INDEX | 订单 ID 索引 | + +### 3.5 物流表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_logistics_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_logistics_order_id | order_id | INDEX | 订单 ID 索引 | + +### 3.6 告警表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_alert_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_alert_status | status | INDEX | 状态索引 | +| idx_alert_severity | severity | INDEX | 严重程度索引 | +| idx_alert_alert_type | alert_type | INDEX | 告警类型索引 | +| idx_alert_created_at | created_at | INDEX | 创建时间索引 | + +### 3.7 审计表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_audit_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_audit_shop_id | shop_id | INDEX | 店铺 ID 索引 | +| idx_audit_user_id | user_id | INDEX | 用户 ID 索引 | +| idx_audit_action | action | INDEX | 操作类型索引 | +| idx_audit_resource_type | resource_type | INDEX | 资源类型索引 | +| idx_audit_created_at | created_at | INDEX | 创建时间索引 | + +### 3.8 配置表索引 + +| 索引名 | 字段 | 类型 | 描述 | +|-------|------|------|------| +| PRIMARY | id | PRIMARY | 主键索引 | +| idx_config_tenant_id | tenant_id | INDEX | 租户 ID 索引 | +| idx_config_shop_id | shop_id | INDEX | 店铺 ID 索引 | +| idx_config_key | config_key | INDEX | 配置键索引 | + +## 4. 关系设计 + +### 4.1 表关系 + +- **用户与订单**:一对多关系,一个用户可以创建多个订单 +- **订单与支付**:一对一关系,一个订单对应一个支付 +- **订单与物流**:一对一关系,一个订单对应一个物流 +- **租户与所有表**:一对多关系,一个租户可以有多个用户、商品、订单等 + +### 4.2 外键约束 + +| 表名 | 外键字段 | 引用表 | 引用字段 | 约束 | +|------|---------|--------|---------|------| +| cf_payment | order_id | cf_order | id | ON DELETE CASCADE | +| cf_logistics | order_id | cf_order | id | ON DELETE CASCADE | + +## 5. 数据类型选择 + +### 5.1 字符串类型 +- **VARCHAR**:用于存储可变长度的字符串,如用户名、邮箱等 +- **TEXT**:用于存储较长的文本,如商品描述、详细信息等 +- **JSON**:用于存储 JSON 格式的数据,如客户信息、商品列表等 + +### 5.2 数值类型 +- **BIGINT**:用于存储较大的整数,如 ID 等 +- **INT**:用于存储整数,如数量等 +- **DECIMAL(10,2)**:用于存储金额,确保精度 + +### 5.3 日期类型 +- **DATETIME**:用于存储日期和时间,如创建时间、更新时间等 + +## 6. 数据库迁移 + +Crawlful Hub 项目使用 Flyway 进行数据库迁移,确保数据库结构的版本控制和一致性。 + +### 6.1 迁移脚本 + +| 脚本名 | 描述 | +|-------|------| +| V1__init_schema.sql | 初始化数据库表结构 | +| V2__add_alert_table.sql | 添加告警表 | + +### 6.2 迁移执行 + +1. 进入项目目录: + ```bash + cd d:\trae_projects\makemd\makemd\serverjava + ``` + +2. 执行数据库迁移: + ```bash + mvn flyway:migrate + ``` + +## 7. 性能优化 + +### 7.1 索引优化 + +- **添加适当的索引**:为频繁查询的字段添加索引 +- **避免过度索引**:不要为所有字段添加索引,只为必要的字段添加 +- **使用复合索引**:对于多字段查询,使用复合索引 + +### 7.2 查询优化 + +- **使用分页查询**:避免一次性加载大量数据 +- **使用索引覆盖查询**:减少回表操作 +- **避免全表扫描**:使用索引进行查询 +- **使用预编译语句**:减少 SQL 解析时间 + +### 7.3 连接池优化 + +- **使用 HikariCP**:高性能的数据库连接池 +- **配置合理的连接数**:根据系统负载配置连接数 +- **设置连接超时**:避免连接占用过长时间 + +## 8. 安全考虑 + +### 8.1 数据加密 + +- **密码加密**:使用 BCrypt 加密用户密码 +- **敏感数据加密**:对敏感数据进行加密存储 + +### 8.2 访问控制 + +- **最小权限原则**:只授予必要的数据库权限 +- **使用参数化查询**:防止 SQL 注入攻击 +- **定期审计**:定期审计数据库访问日志 + +## 9. 总结 + +本数据库设计文档详细介绍了 Crawlful Hub 项目的数据库表结构、索引和关系。通过合理的数据库设计,确保了系统的性能和可靠性。 diff --git a/docs/06_Security_Guide.md b/docs/06_Security_Guide.md new file mode 100644 index 0000000..c5170f6 --- /dev/null +++ b/docs/06_Security_Guide.md @@ -0,0 +1,297 @@ +# 安全指南 + +## 1. 安全概述 + +Crawlful Hub 项目采用了全面的安全措施,确保系统的安全性和可靠性。本文档详细介绍了项目的安全措施和最佳实践。 + +## 2. 认证与授权 + +### 2.1 JWT 认证 + +Crawlful Hub 项目使用 JWT(JSON Web Token)进行认证,确保用户身份的安全性。 + +**实现细节**: +- 使用 Spring Security 实现 JWT 认证 +- 配置 JWT 密钥和过期时间 +- 验证 JWT 令牌的有效性 + +**配置示例**: +```yaml +spring: + security: + jwt: + secret: your-secret-key + expiration: 86400000 +``` + +### 2.2 角色授权 + +Crawlful Hub 项目使用基于角色的访问控制(RBAC),确保用户只能访问其权限范围内的资源。 + +**预设角色**: +- `ADMIN` - 全权 +- `MANAGER` - 运营主管 +- `OPERATOR` - 运营专员 +- `FINANCE` - 财务主管 +- `SOURCING` - 采购专家 +- `LOGISTICS` - 物流专家 +- `ANALYST` - 数据分析师 + +**授权实现**: +- 使用 `@PreAuthorize` 注解进行方法级别的授权 +- 实现 `authorize()` 中间件进行路由级别的授权 + +**示例代码**: +```java +@PreAuthorize("hasRole('ADMIN')") +public void deleteUser(Long id) { + // 实现删除用户的逻辑 +} +``` + +## 3. 数据安全 + +### 3.1 密码加密 + +Crawlful Hub 项目使用 BCrypt 对用户密码进行加密,确保密码的安全性。 + +**实现细节**: +- 使用 `BCryptPasswordEncoder` 对密码进行加密 +- 存储加密后的密码,不存储明文密码 + +**示例代码**: +```java +@Autowired +private PasswordEncoder passwordEncoder; + +public User createUser(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + return userRepository.save(user); +} +``` + +### 3.2 敏感数据保护 + +Crawlful Hub 项目对敏感数据进行保护,确保数据的安全性。 + +**保护措施**: +- 对敏感数据进行加密存储 +- 避免在日志中记录敏感信息 +- 使用 HTTPS 协议传输数据 + +**示例代码**: +```java +@Column(columnDefinition = "VARBINARY(255)") +private byte[] sensitiveData; +``` + +## 4. 输入验证 + +### 4.1 参数验证 + +Crawlful Hub 项目对输入参数进行验证,确保输入的合法性。 + +**实现细节**: +- 使用 `@Valid` 注解和 `BindingResult` 进行参数验证 +- 实现 `ValidationUtil` 工具类进行数据验证 + +**示例代码**: +```java +@PostMapping("/create") +public ResponseEntity createUser(@Valid @RequestBody User user, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok(userService.createUser(user)); +} +``` + +### 4.2 SQL 注入防护 + +Crawlful Hub 项目使用参数化查询,防止 SQL 注入攻击。 + +**实现细节**: +- 使用 JPA 或 MyBatis 进行数据库操作 +- 避免直接拼接 SQL 语句 + +**示例代码**: +```java +// 正确的做法 +@Query("SELECT u FROM User u WHERE u.username = :username") +User findByUsername(@Param("username") String username); + +// 错误的做法(避免) +String sql = "SELECT * FROM user WHERE username = '" + username + "'"; +``` + +## 5. 跨站请求伪造(CSRF)防护 + +Crawlful Hub 项目实现了 CSRF 防护,防止跨站请求伪造攻击。 + +**实现细节**: +- 使用 Spring Security 的 CSRF 保护 +- 配置 CSRF 令牌的生成和验证 + +**配置示例**: +```java +@Configuration +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); + } +} +``` + +## 6. 速率限制 + +Crawlful Hub 项目实现了速率限制,防止暴力破解和 DoS 攻击。 + +**实现细节**: +- 实现 `RateLimitFilter` 过滤器 +- 基于客户端 IP 和请求 URI 进行限制 +- 限制客户端每分钟最大请求数为 60 次 + +**示例代码**: +```java +@Component +public class RateLimitFilter implements Filter { + private final Map requestCounts = new ConcurrentHashMap<>(); + private final Map lastResetTimes = new ConcurrentHashMap<>(); + private static final int MAX_REQUESTS_PER_MINUTE = 60; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String clientIp = httpRequest.getRemoteAddr(); + String requestUri = httpRequest.getRequestURI(); + String key = clientIp + ":" + requestUri; + + long currentTime = System.currentTimeMillis(); + long lastResetTime = lastResetTimes.getOrDefault(key, 0L); + + if (currentTime - lastResetTime > 60000) { + requestCounts.put(key, new AtomicInteger(1)); + lastResetTimes.put(key, currentTime); + } else { + AtomicInteger count = requestCounts.get(key); + if (count != null && count.incrementAndGet() > MAX_REQUESTS_PER_MINUTE) { + ((HttpServletResponse) response).setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + return; + } + } + + chain.doFilter(request, response); + } +} +``` + +## 7. 安全日志 + +Crawlful Hub 项目实现了安全日志,记录系统的安全事件。 + +**实现细节**: +- 使用 Logback 配置安全日志 +- 记录用户登录、登出、权限变更等安全事件 +- 记录异常登录尝试和权限错误 + +**配置示例**: +```xml + + logs/security.log + + logs/security.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + +``` + +## 8. 安全最佳实践 + +### 8.1 代码安全 + +- **使用最新的依赖**:定期更新项目依赖,修复安全漏洞 +- **避免硬编码**:避免在代码中硬编码密钥、密码等敏感信息 +- **使用安全的加密算法**:使用强加密算法,如 BCrypt、AES 等 +- **定期代码审查**:定期进行代码审查,发现和修复安全问题 + +### 8.2 服务器安全 + +- **使用 HTTPS**:使用 HTTPS 协议传输数据 +- **限制访问**:限制服务器的访问范围,只允许必要的端口和 IP 访问 +- **定期更新**:定期更新服务器操作系统和软件,修复安全漏洞 +- **使用防火墙**:使用防火墙保护服务器,防止未授权访问 + +### 8.3 数据库安全 + +- **最小权限原则**:只授予数据库用户必要的权限 +- **定期备份**:定期备份数据库,防止数据丢失 +- **使用参数化查询**:防止 SQL 注入攻击 +- **加密敏感数据**:对敏感数据进行加密存储 + +### 8.4 应用安全 + +- **使用 CSRF 保护**:防止跨站请求伪造攻击 +- **实现速率限制**:防止暴力破解和 DoS 攻击 +- **验证输入**:对所有输入进行验证,防止恶意输入 +- **使用安全的会话管理**:使用安全的会话管理机制,防止会话劫持 + +## 9. 安全审计 + +### 9.1 审计日志 + +Crawlful Hub 项目实现了审计日志,记录系统的操作和事件。 + +**实现细节**: +- 使用 `cf_audit` 表存储审计日志 +- 记录用户操作、资源访问、权限变更等事件 +- 记录 IP 地址、用户代理等信息 + +**示例代码**: +```java +@Service +public class AuditService { + @Autowired + private AuditRepository auditRepository; + + public void logAudit(String tenantId, String shopId, Long userId, String action, String resourceType, String resourceId, String ipAddress, String userAgent, String details) { + Audit audit = new Audit(); + audit.setTenantId(tenantId); + audit.setShopId(shopId); + audit.setUserId(userId); + audit.setAction(action); + audit.setResourceType(resourceType); + audit.setResourceId(resourceId); + audit.setIpAddress(ipAddress); + audit.setUserAgent(userAgent); + audit.setDetails(details); + audit.setCreatedAt(new Date()); + auditRepository.save(audit); + } +} +``` + +### 9.2 安全扫描 + +Crawlful Hub 项目定期进行安全扫描,发现和修复安全漏洞。 + +**扫描工具**: +- **OWASP ZAP**:用于 Web 应用安全扫描 +- **SonarQube**:用于代码安全扫描 +- **Nmap**:用于网络安全扫描 + +**扫描频率**: +- 开发环境:每次代码提交后 +- 测试环境:每周一次 +- 生产环境:每月一次 + +## 10. 总结 + +本安全指南详细介绍了 Crawlful Hub 项目的安全措施和最佳实践。通过本指南,您可以了解如何确保系统的安全性和可靠性。 diff --git a/docs/07_Maintenance_Guide.md b/docs/07_Maintenance_Guide.md new file mode 100644 index 0000000..3c76163 --- /dev/null +++ b/docs/07_Maintenance_Guide.md @@ -0,0 +1,292 @@ +# 维护指南 + +## 1. 维护概述 + +Crawlful Hub 项目需要定期维护,确保系统的稳定运行和性能优化。本文档详细介绍了项目的维护方法和最佳实践。 + +## 2. 日常维护 + +### 2.1 监控系统状态 + +**监控指标**: +- **系统健康状态**:访问 `http://localhost:3001/api/v1/monitoring/health` +- **系统性能指标**:访问 `http://localhost:3001/api/v1/monitoring/metrics` +- **系统日志**:查看 `logs/server.log` 文件 + +**监控工具**: +- **Prometheus**:用于监控系统性能指标 +- **Grafana**:用于可视化监控数据 +- **ELK Stack**:用于日志收集和分析 + +### 2.2 数据库维护 + +**定期备份**: +- 每周备份一次数据库 +- 使用 `mysqldump` 命令备份数据库 + +**优化数据库**: +- 定期优化数据库表结构 +- 定期分析数据库查询性能 +- 定期更新数据库统计信息 + +**示例命令**: +```bash +# 备份数据库 +mysqldump -u root -p crawlful_hub > crawlful_hub_backup.sql + +# 优化数据库表 +mysqlcheck -u root -p --optimize crawlful_hub + +# 分析数据库表 +mysqlcheck -u root -p --analyze crawlful_hub +``` + +### 2.3 日志管理 + +**日志轮转**: +- 配置 Logback 实现日志轮转 +- 按日期保存日志文件 +- 保留 30 天的日志文件 + +**日志分析**: +- 定期分析系统日志 +- 发现和解决系统异常 +- 优化系统性能 + +**示例配置**: +```xml + + logs/server.log + + logs/server.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + +``` + +## 3. 性能优化 + +### 3.1 数据库优化 + +**索引优化**: +- 为频繁查询的字段添加索引 +- 避免过度索引 +- 使用复合索引 + +**查询优化**: +- 使用分页查询 +- 使用索引覆盖查询 +- 避免全表扫描 +- 使用预编译语句 + +**连接池优化**: +- 配置合理的连接数 +- 设置连接超时 +- 监控连接池状态 + +### 3.2 缓存优化 + +**Redis 缓存**: +- 配置合理的缓存过期时间 +- 监控缓存命中率 +- 优化缓存键设计 + +**示例配置**: +```java +@Configuration +@EnableCaching +public class CacheConfig { + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory factory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)); + return RedisCacheManager.builder(factory) + .cacheDefaults(config) + .build(); + } +} +``` + +### 3.3 服务器优化 + +**内存优化**: +- 配置合理的 JVM 内存参数 +- 监控内存使用情况 +- 避免内存泄漏 + +**CPU 优化**: +- 优化代码逻辑 +- 避免阻塞操作 +- 使用异步处理 + +**示例 JVM 参数**: +```bash +java -Xms2G -Xmx4G -jar serverjava-0.0.1-SNAPSHOT.jar +``` + +## 4. 故障处理 + +### 4.1 常见故障 + +**数据库连接失败**: +- 检查数据库服务是否运行 +- 检查数据库连接配置是否正确 +- 检查数据库用户权限是否正确 + +**Redis 连接失败**: +- 检查 Redis 服务是否运行 +- 检查 Redis 连接配置是否正确 + +**端口被占用**: +- 检查端口是否被其他服务占用 +- 修改 `application.yml` 中的端口配置 + +**JWT 验证失败**: +- 检查 JWT 密钥是否正确 +- 检查 JWT 令牌是否过期 + +### 4.2 故障排查 + +**查看日志**: +- 查看 `logs/server.log` 文件 +- 查找错误信息和异常堆栈 + +**使用健康检查**: +- 访问 `http://localhost:3001/api/v1/monitoring/health` +- 检查系统健康状态 + +**使用 Ping 测试**: +- 访问 `http://localhost:3001/api/v1/monitoring/ping` +- 测试系统响应 + +**使用 API 文档**: +- 访问 `http://localhost:3001/api/api-docs` +- 测试 API 接口 + +## 5. 升级与更新 + +### 5.1 依赖更新 + +**定期更新依赖**: +- 定期检查和更新项目依赖 +- 修复安全漏洞 +- 提高系统性能 + +**示例命令**: +```bash +# 检查依赖更新 +mvn dependency:check + +# 更新依赖 +mvn versions:update-properties +``` + +### 5.2 代码更新 + +**代码审查**: +- 定期进行代码审查 +- 发现和修复代码问题 +- 优化代码结构 + +**版本控制**: +- 使用 Git 进行版本控制 +- 定期提交代码 +- 管理代码分支 + +### 5.3 系统升级 + +**升级步骤**: +1. 停止服务 +2. 备份数据 +3. 拉取最新代码 +4. 重新构建项目 +5. 启动服务 + +**示例命令**: +```bash +# 停止服务 +sudo systemctl stop serverjava + +# 备份数据 +mysqldump -u root -p crawlful_hub > crawlful_hub_backup.sql + +# 拉取最新代码 +git pull + +# 重新构建项目 +mvn clean package + +# 启动服务 +sudo systemctl start serverjava +``` + +## 6. 监控与告警 + +### 6.1 系统监控 + +**监控指标**: +- **CPU 使用率**:监控系统 CPU 使用率 +- **内存使用率**:监控系统内存使用率 +- **磁盘使用率**:监控系统磁盘使用率 +- **网络流量**:监控系统网络流量 +- **API 响应时间**:监控 API 接口响应时间 +- **数据库性能**:监控数据库查询性能 + +**监控工具**: +- **Prometheus**:用于监控系统性能指标 +- **Grafana**:用于可视化监控数据 +- **ELK Stack**:用于日志收集和分析 + +### 6.2 告警系统 + +**告警类型**: +- **系统告警**:系统故障、性能异常等 +- **业务告警**:订单异常、支付失败等 +- **安全告警**:未授权访问、异常登录等 + +**告警级别**: +- **ERROR**:严重错误,需要立即处理 +- **WARN**:警告,需要关注 +- **INFO**:信息,仅供参考 + +**告警处理**: +- 及时接收和处理告警 +- 记录告警处理过程 +- 分析告警原因,防止再次发生 + +## 7. 最佳实践 + +### 7.1 代码最佳实践 + +- **代码风格**:遵循 Java 代码风格规范 +- **注释**:为代码添加适当的注释 +- **测试**:为代码添加单元测试和集成测试 +- **文档**:为代码添加文档 + +### 7.2 数据库最佳实践 + +- **表设计**:遵循数据库设计规范 +- **索引**:为频繁查询的字段添加索引 +- **查询**:优化数据库查询 +- **备份**:定期备份数据库 + +### 7.3 服务器最佳实践 + +- **配置**:配置合理的服务器参数 +- **监控**:监控服务器状态 +- **安全**:确保服务器安全 +- **备份**:定期备份服务器数据 + +### 7.4 维护最佳实践 + +- **定期维护**:定期进行系统维护 +- **记录**:记录系统维护过程 +- **分析**:分析系统维护结果 +- **改进**:根据维护结果改进系统 + +## 8. 总结 + +本维护指南详细介绍了 Crawlful Hub 项目的维护方法和最佳实践。通过本指南,您可以了解如何确保系统的稳定运行和性能优化。 diff --git a/docs/08_Internationalization_Guide.md b/docs/08_Internationalization_Guide.md new file mode 100644 index 0000000..4bfed4e --- /dev/null +++ b/docs/08_Internationalization_Guide.md @@ -0,0 +1,245 @@ +# 国际化指南 + +## 1. 国际化概述 + +Crawlful Hub 项目支持国际化,确保系统可以在不同语言环境下正常运行。本文档详细介绍了项目的国际化支持和实现方法。 + +## 2. 国际化配置 + +### 2.1 资源文件 + +Crawlful Hub 项目使用 Spring 国际化支持,通过资源文件管理不同语言的文本。 + +**资源文件位置**: +- `src/main/resources/i18n/messages.properties` - 英文资源文件 +- `src/main/resources/i18n/messages_zh.properties` - 中文资源文件 + +**资源文件示例**: + +**messages.properties**: +```properties +# Authentication +auth.login.success=Login successful +auth.login.failure=Invalid username or password +auth.register.success=Registration successful +auth.register.failure=Registration failed + +# Product +product.create.success=Product created successfully +product.create.failure=Failed to create product +product.update.success=Product updated successfully +product.update.failure=Failed to update product +product.delete.success=Product deleted successfully +product.delete.failure=Failed to delete product + +# Order +order.create.success=Order created successfully +order.create.failure=Failed to create order +order.update.success=Order updated successfully +order.update.failure=Failed to update order +order.delete.success=Order deleted successfully +order.delete.failure=Failed to delete order +``` + +**messages_zh.properties**: +```properties +# 认证 +auth.login.success=登录成功 +auth.login.failure=用户名或密码错误 +auth.register.success=注册成功 +auth.register.failure=注册失败 + +# 商品 +product.create.success=商品创建成功 +product.create.failure=商品创建失败 +product.update.success=商品更新成功 +product.update.failure=商品更新失败 +product.delete.success=商品删除成功 +product.delete.failure=商品删除失败 + +# 订单 +order.create.success=订单创建成功 +order.create.failure=订单创建失败 +order.update.success=订单更新成功 +order.update.failure=订单更新失败 +order.delete.success=订单删除成功 +order.delete.failure=订单删除失败 +``` + +### 2.2 国际化配置类 + +Crawlful Hub 项目通过 `InternationalizationConfig` 类配置国际化支持。 + +**配置示例**: +```java +@Configuration +public class InternationalizationConfig implements WebMvcConfigurer { + @Bean + public MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("i18n/messages"); + messageSource.setDefaultEncoding("UTF-8"); + return messageSource; + } + + @Bean + public LocaleResolver localeResolver() { + CookieLocaleResolver localeResolver = new CookieLocaleResolver(); + localeResolver.setDefaultLocale(Locale.ENGLISH); + return localeResolver; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); + interceptor.setParamName("lang"); + registry.addInterceptor(interceptor); + } +} +``` + +## 3. 国际化使用 + +### 3.1 在控制器中使用 + +Crawlful Hub 项目在控制器中使用国际化消息。 + +**示例代码**: +```java +@RestController +@RequestMapping("/api/v1/users") +public class UserController { + @Autowired + private MessageSource messageSource; + + @PostMapping("/register") + public ResponseEntity register(@RequestBody User user, Locale locale) { + try { + userService.createUser(user); + return ResponseEntity.ok(messageSource.getMessage("auth.register.success", null, locale)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(messageSource.getMessage("auth.register.failure", null, locale)); + } + } +} +``` + +### 3.2 在服务中使用 + +Crawlful Hub 项目在服务中使用国际化消息。 + +**示例代码**: +```java +@Service +public class ProductService { + @Autowired + private MessageSource messageSource; + + public Product createProduct(Product product, Locale locale) { + try { + Product savedProduct = productRepository.save(product); + log.info(messageSource.getMessage("product.create.success", null, locale)); + return savedProduct; + } catch (Exception e) { + log.error(messageSource.getMessage("product.create.failure", null, locale), e); + throw e; + } + } +} +``` + +### 3.3 在异常处理中使用 + +Crawlful Hub 项目在异常处理中使用国际化消息。 + +**示例代码**: +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + @Autowired + private MessageSource messageSource; + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e, Locale locale) { + ApiError error = new ApiError(); + error.setCode("INTERNAL_ERROR"); + error.setMessage(messageSource.getMessage("error.internal", null, locale)); + error.setDetails(e.getMessage()); + error.setTraceId(UUID.randomUUID().toString()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } +} +``` + +## 4. 语言切换 + +Crawlful Hub 项目支持通过 URL 参数切换语言。 + +**示例 URL**: +- 英文:`http://localhost:3001/api/v1/users?lang=en` +- 中文:`http://localhost:3001/api/v1/users?lang=zh` + +**实现细节**: +- 使用 `LocaleChangeInterceptor` 拦截器处理语言切换 +- 通过 `lang` 参数指定语言 +- 支持的语言:`en`(英文)、`zh`(中文) + +## 5. 国际化最佳实践 + +### 5.1 资源文件管理 + +- **命名规范**:使用 `messages_{language}.properties` 命名资源文件 +- **组织方式**:按功能模块组织资源文件,如认证、商品、订单等 +- **编码格式**:使用 UTF-8 编码 +- **注释**:为资源文件添加适当的注释 + +### 5.2 消息键命名 + +- **命名规范**:使用 `module.action.result` 格式命名消息键 +- **一致性**:保持消息键的一致性,便于管理和维护 +- **可读性**:使用清晰、简洁的消息键,便于理解 + +### 5.3 国际化测试 + +- **测试不同语言**:测试系统在不同语言环境下的表现 +- **测试语言切换**:测试语言切换功能是否正常 +- **测试消息显示**:测试消息是否正确显示 + +## 6. 扩展国际化支持 + +### 6.1 添加新语言 + +**步骤**: +1. 在 `src/main/resources/i18n/` 目录下创建新的资源文件,如 `messages_fr.properties`(法语) +2. 翻译资源文件中的消息 +3. 配置 `InternationalizationConfig` 类,支持新的语言 + +**示例**: +```java +@Bean +public LocaleResolver localeResolver() { + CookieLocaleResolver localeResolver = new CookieLocaleResolver(); + localeResolver.setDefaultLocale(Locale.ENGLISH); + return localeResolver; +} +``` + +### 6.2 动态消息 + +Crawlful Hub 项目支持动态消息,通过参数传递动态值。 + +**示例**: + +**资源文件**: +```properties +user.welcome=Welcome, {0}! +``` + +**使用示例**: +```java +String welcomeMessage = messageSource.getMessage("user.welcome", new Object[]{username}, locale); +``` + +## 7. 总结 + +本国际化指南详细介绍了 Crawlful Hub 项目的国际化支持和实现方法。通过本指南,您可以了解如何确保系统在不同语言环境下正常运行。 diff --git a/docs/09_Development_Guide.md b/docs/09_Development_Guide.md new file mode 100644 index 0000000..41196bb --- /dev/null +++ b/docs/09_Development_Guide.md @@ -0,0 +1,340 @@ +# 开发指南 + +## 1. 开发环境搭建 + +### 1.1 环境要求 + +- **Java**: JDK 17 或更高版本 +- **Maven**: 3.6 或更高版本 +- **MySQL**: 8.0 或更高版本 +- **Redis**: 6.0 或更高版本 +- **IDE**: IntelliJ IDEA 或 Eclipse + +### 1.2 安装步骤 + +**Windows 系统**: +1. 下载并安装 JDK 17:[Oracle JDK 17](https://www.oracle.com/java/technologies/downloads/#java17) +2. 下载并安装 Maven:[Apache Maven](https://maven.apache.org/download.cgi) +3. 下载并安装 MySQL:[MySQL Community Server](https://dev.mysql.com/downloads/mysql/) +4. 下载并安装 Redis:[Redis for Windows](https://github.com/tporadowski/redis/releases) +5. 下载并安装 IntelliJ IDEA:[IntelliJ IDEA](https://www.jetbrains.com/idea/download/) + +**Linux 系统**: +1. 安装 JDK 17: + ```bash + sudo yum install java-17-openjdk-devel + ``` +2. 安装 Maven: + ```bash + sudo yum install maven + ``` +3. 安装 MySQL: + ```bash + sudo yum install mysql-server + ``` +4. 安装 Redis: + ```bash + sudo yum install redis + ``` +5. 安装 IntelliJ IDEA: + ```bash + sudo snap install intellij-idea-community --classic + ``` + +### 1.3 项目导入 + +**IntelliJ IDEA**: +1. 打开 IntelliJ IDEA +2. 点击「File」→「Open」 +3. 选择项目目录 `d:\trae_projects\makemd\makemd\serverjava` +4. 点击「OK」,等待项目导入完成 +5. 点击「File」→「Project Structure」→「Project」,设置 JDK 为 17 +6. 点击「File」→「Project Structure」→「Modules」,确保 Maven 依赖正确导入 + +**Eclipse**: +1. 打开 Eclipse +2. 点击「File」→「Import」→「Maven」→「Existing Maven Projects」 +3. 选择项目目录 `d:\trae_projects\makemd\makemd\serverjava` +4. 点击「Finish」,等待项目导入完成 +5. 右键点击项目,选择「Properties」→「Java Build Path」→「Libraries」,确保 JDK 为 17 + +## 2. 代码规范 + +### 2.1 命名规范 + +**类命名**: +- 使用驼峰命名法,首字母大写 +- 类名应该清晰描述类的功能 +- 服务类使用 `Service` 后缀 +- 控制器类使用 `Controller` 后缀 +- 配置类使用 `Config` 后缀 + +**方法命名**: +- 使用驼峰命名法,首字母小写 +- 方法名应该清晰描述方法的功能 +- 动词开头,如 `createUser`, `updateProduct` + +**变量命名**: +- 使用驼峰命名法,首字母小写 +- 变量名应该清晰描述变量的用途 +- 避免使用缩写,如 `usr` 应该改为 `user` + +**常量命名**: +- 使用全大写,单词之间用下划线分隔 +- 常量名应该清晰描述常量的用途 + +### 2.2 代码风格 + +**缩进**: +- 使用 4 个空格进行缩进 +- 避免使用制表符 + +**换行**: +- 每行代码长度不超过 120 个字符 +- 适当换行,提高代码可读性 + +**注释**: +- 为类、方法、变量添加适当的注释 +- 使用 Javadoc 注释格式 +- 注释应该清晰描述代码的功能和用途 + +**示例**: +```java +/** + * 用户服务类,提供用户相关的业务逻辑 + */ +@Service +public class UserService { + /** + * 创建用户 + * @param user 用户信息 + * @return 创建的用户 + */ + public User createUser(User user) { + // 实现创建用户的逻辑 + return userRepository.save(user); + } +} +``` + +### 2.3 代码质量 + +**代码检查**: +- 使用 ESLint 检查代码质量 +- 使用 SonarQube 分析代码质量 +- 定期进行代码审查 + +**测试覆盖**: +- 为代码添加单元测试和集成测试 +- 确保测试覆盖率达到 80% 以上 + +**性能优化**: +- 优化代码逻辑,提高性能 +- 避免不必要的计算和操作 +- 使用缓存减少数据库查询 + +## 3. 开发流程 + +### 3.1 分支管理 + +**分支策略**: +- `main`:主分支,用于发布生产版本 +- `develop`:开发分支,用于集成开发 +- `feature`:特性分支,用于开发新功能 +- `hotfix`:热修复分支,用于修复生产环境的 bug + +**分支命名**: +- 特性分支:`feature/feature-name` +- 热修复分支:`hotfix/bug-description` + +**分支操作**: +1. 从 `develop` 分支创建特性分支 +2. 在特性分支上开发新功能 +3. 提交代码并推送特性分支 +4. 创建 Pull Request 到 `develop` 分支 +5. 代码审查通过后,合并到 `develop` 分支 +6. 从 `main` 分支创建热修复分支 +7. 在热修复分支上修复 bug +8. 提交代码并推送热修复分支 +9. 创建 Pull Request 到 `main` 和 `develop` 分支 +10. 代码审查通过后,合并到 `main` 和 `develop` 分支 + +### 3.2 代码提交 + +**提交规范**: +- 提交消息应该清晰描述提交的内容 +- 使用以下格式:`[模块] 描述` +- 例如:`[User] 修复用户注册功能` + +**提交频率**: +- 每次提交应该只包含一个功能或 bug 修复 +- 避免一次提交多个不相关的更改 +- 定期提交代码,避免代码丢失 + +### 3.3 代码审查 + +**审查流程**: +1. 开发人员创建 Pull Request +2. 团队成员审查代码 +3. 审查通过后,合并代码 +4. 审查不通过时,开发人员修改代码并重新提交 + +**审查重点**: +- 代码质量和风格 +- 功能实现是否正确 +- 测试覆盖是否充分 +- 性能是否优化 +- 安全性是否考虑 + +## 4. 开发工具 + +### 4.1 IDE 插件 + +**IntelliJ IDEA 插件**: +- **Lombok**:简化 Java 代码 +- **Spring Boot Assistant**:Spring Boot 开发辅助 +- **MyBatis Plugin**:MyBatis 开发辅助 +- **SonarLint**:代码质量检查 +- **CheckStyle**:代码风格检查 + +**Eclipse 插件**: +- **Lombok**:简化 Java 代码 +- **Spring Tools Suite**:Spring Boot 开发辅助 +- **MyBatis Generator**:MyBatis 开发辅助 +- **SonarLint**:代码质量检查 +- **CheckStyle**:代码风格检查 + +### 4.2 构建工具 + +**Maven**: +- 用于项目构建和依赖管理 +- 配置文件:`pom.xml` +- 常用命令: + ```bash + # 编译项目 + mvn compile + + # 运行测试 + mvn test + + # 构建项目 + mvn clean package + + # 运行项目 + mvn spring-boot:run + ``` + +### 4.3 版本控制 + +**Git**: +- 用于版本控制 +- 配置文件:`.gitignore` +- 常用命令: + ```bash + # 克隆仓库 + git clone + + # 查看状态 + git status + + # 添加文件 + git add . + + # 提交代码 + git commit -m "[模块] 描述" + + # 推送代码 + git push + + # 拉取代码 + git pull + ``` + +## 5. 开发最佳实践 + +### 5.1 代码组织 + +**目录结构**: +- 遵循 Spring Boot 标准目录结构 +- 按功能模块组织代码 +- 保持代码结构清晰 + +**示例目录结构**: +``` +serverjava/ + ├── src/ + │ ├── main/ + │ │ ├── java/com/crawlful/hub/ + │ │ │ ├── api/controllers/ # 控制器 + │ │ │ ├── service/ # 服务 + │ │ │ ├── model/ # 模型 + │ │ │ ├── config/ # 配置 + │ │ │ ├── util/ # 工具类 + │ │ │ ├── security/ # 安全 + │ │ │ └── monitoring/ # 监控 + │ │ └── resources/ # 资源文件 + │ │ ├── i18n/ # 国际化资源 + │ │ ├── db/migration/ # 数据库迁移脚本 + │ │ ├── application.yml # 应用配置 + │ │ └── logback.xml # 日志配置 + │ └── test/ # 测试代码 + ├── pom.xml # Maven 配置 + └── docs/ # 文档 +``` + +### 5.2 依赖管理 + +**依赖版本**: +- 使用 Spring Boot 3.2.0 版本 +- 统一管理依赖版本 +- 定期更新依赖,修复安全漏洞 + +**依赖范围**: +- `compile`:编译和运行时依赖 +- `test`:测试依赖 +- `provided`:编译时依赖,运行时由容器提供 + +### 5.3 错误处理 + +**全局异常处理**: +- 实现 `GlobalExceptionHandler` 类 +- 统一处理系统异常 +- 返回统一的错误格式 + +**自定义异常**: +- 定义业务异常类 +- 提供错误码和错误消息 +- 便于异常处理和日志记录 + +### 5.4 日志管理 + +**日志级别**: +- `DEBUG`:开发调试,详细执行路径 +- `INFO`:正常业务流程,如订单创建、状态流转 +- `WARN`:潜在问题,如重试、熔断触发 +- `ERROR`:错误异常,如 API 调用失败、数据库异常 + +**日志格式**: +- 包含时间戳、日志级别、类名、消息等 +- 使用 Logback 配置日志格式 + +### 5.5 测试策略 + +**单元测试**: +- 测试单个方法或类 +- 使用 JUnit 5 和 Mockito +- 测试边界条件和异常情况 + +**集成测试**: +- 测试组件之间的交互 +- 使用 Spring Test +- 测试完整的业务流程 + +**系统测试**: +- 测试整个系统的功能 +- 使用 Postman 测试 API 接口 +- 测试用户场景 + +## 6. 总结 + +本开发指南详细介绍了 Crawlful Hub 项目的开发环境搭建、代码规范和开发流程。通过本指南,您可以了解如何快速上手项目开发,确保代码质量和开发效率。 diff --git a/docs/10_API_Changelog.md b/docs/10_API_Changelog.md new file mode 100644 index 0000000..5cb0895 --- /dev/null +++ b/docs/10_API_Changelog.md @@ -0,0 +1,335 @@ +# API 变更日志 + +## 1. 版本 1.0.0 (2024-01-01) + +### 1.1 新增功能 + +- **认证模块**: + - 新增 `POST /api/v1/auth/login` 接口:用户登录 + - 新增 `POST /api/v1/auth/register` 接口:用户注册 + - 新增 `GET /api/v1/auth/me` 接口:获取当前用户信息 + +- **商品模块**: + - 新增 `POST /api/v1/products` 接口:创建商品 + - 新增 `GET /api/v1/products` 接口:获取商品列表 + - 新增 `GET /api/v1/products/{id}` 接口:获取商品详情 + - 新增 `PUT /api/v1/products/{id}` 接口:更新商品 + - 新增 `DELETE /api/v1/products/{id}` 接口:删除商品 + +- **订单模块**: + - 新增 `POST /api/v1/orders` 接口:创建订单 + - 新增 `GET /api/v1/orders` 接口:获取订单列表 + - 新增 `GET /api/v1/orders/{id}` 接口:获取订单详情 + - 新增 `PUT /api/v1/orders/{id}` 接口:更新订单 + - 新增 `DELETE /api/v1/orders/{id}` 接口:删除订单 + +- **支付模块**: + - 新增 `POST /api/v1/payments` 接口:创建支付 + - 新增 `GET /api/v1/payments` 接口:获取支付列表 + - 新增 `GET /api/v1/payments/{id}` 接口:获取支付详情 + - 新增 `PUT /api/v1/payments/{id}` 接口:更新支付 + +- **物流模块**: + - 新增 `POST /api/v1/logistics` 接口:创建物流 + - 新增 `GET /api/v1/logistics` 接口:获取物流列表 + - 新增 `GET /api/v1/logistics/{id}` 接口:获取物流详情 + - 新增 `PUT /api/v1/logistics/{id}` 接口:更新物流 + +- **用户模块**: + - 新增 `POST /api/v1/users` 接口:创建用户 + - 新增 `GET /api/v1/users` 接口:获取用户列表 + - 新增 `GET /api/v1/users/{id}` 接口:获取用户详情 + - 新增 `PUT /api/v1/users/{id}` 接口:更新用户 + - 新增 `DELETE /api/v1/users/{id}` 接口:删除用户 + +- **监控模块**: + - 新增 `GET /api/v1/monitoring/health` 接口:系统健康状态 + - 新增 `GET /api/v1/monitoring/ping` 接口:系统响应测试 + - 新增 `GET /api/v1/monitoring/metrics` 接口:系统性能指标 + +- **告警模块**: + - 新增 `POST /api/v1/alerts` 接口:创建告警 + - 新增 `GET /api/v1/alerts` 接口:获取告警列表 + - 新增 `GET /api/v1/alerts/{id}` 接口:获取告警详情 + - 新增 `PUT /api/v1/alerts/{id}` 接口:更新告警 + - 新增 `PUT /api/v1/alerts/{id}/resolve` 接口:解决告警 + +- **配置模块**: + - 新增 `POST /api/v1/configs` 接口:创建配置 + - 新增 `GET /api/v1/configs` 接口:获取配置列表 + - 新增 `GET /api/v1/configs/{id}` 接口:获取配置详情 + - 新增 `PUT /api/v1/configs/{id}` 接口:更新配置 + - 新增 `DELETE /api/v1/configs/{id}` 接口:删除配置 + +- **审计模块**: + - 新增 `GET /api/v1/audit` 接口:获取审计日志列表 + - 新增 `GET /api/v1/audit/{id}` 接口:获取审计日志详情 + +- **数据模块**: + - 新增 `POST /api/v1/data/import` 接口:导入数据 + - 新增 `GET /api/v1/data/export` 接口:导出数据 + +- **报表模块**: + - 新增 `GET /api/v1/reports/sales` 接口:销售报表 + - 新增 `GET /api/v1/reports/inventory` 接口:库存报表 + - 新增 `GET /api/v1/reports/users` 接口:用户报表 + +### 1.2 变更内容 + +- **认证模块**: + - 使用 JWT 进行认证 + - 支持角色授权 + +- **商品模块**: + - 支持商品分页查询 + - 支持商品搜索 + +- **订单模块**: + - 支持订单状态流转 + - 支持订单查询 + +- **支付模块**: + - 支持多种支付方式 + - 支持支付状态更新 + +- **物流模块**: + - 支持物流状态更新 + - 支持物流查询 + +- **用户模块**: + - 支持用户角色管理 + - 支持用户权限控制 + +- **监控模块**: + - 支持系统健康检查 + - 支持系统性能监控 + +- **告警模块**: + - 支持告警级别管理 + - 支持告警状态更新 + +- **配置模块**: + - 支持配置类型管理 + - 支持配置查询 + +- **审计模块**: + - 支持审计日志查询 + - 支持审计日志过滤 + +- **数据模块**: + - 支持数据导入导出 + - 支持数据格式转换 + +- **报表模块**: + - 支持销售报表生成 + - 支持库存报表生成 + - 支持用户报表生成 + +### 1.3 修复问题 + +- **认证模块**: + - 修复登录失败时的错误处理 + - 修复注册时的参数验证 + +- **商品模块**: + - 修复商品创建时的参数验证 + - 修复商品更新时的权限检查 + +- **订单模块**: + - 修复订单创建时的状态设置 + - 修复订单更新时的权限检查 + +- **支付模块**: + - 修复支付创建时的参数验证 + - 修复支付状态更新时的错误处理 + +- **物流模块**: + - 修复物流创建时的参数验证 + - 修复物流状态更新时的错误处理 + +- **用户模块**: + - 修复用户创建时的参数验证 + - 修复用户更新时的权限检查 + +- **监控模块**: + - 修复健康检查端点的响应格式 + - 修复性能指标的计算 + +- **告警模块**: + - 修复告警创建时的参数验证 + - 修复告警状态更新时的错误处理 + +- **配置模块**: + - 修复配置创建时的参数验证 + - 修复配置更新时的权限检查 + +- **审计模块**: + - 修复审计日志查询时的参数验证 + - 修复审计日志过滤时的错误处理 + +- **数据模块**: + - 修复数据导入时的格式验证 + - 修复数据导出时的格式转换 + +- **报表模块**: + - 修复销售报表生成时的计算错误 + - 修复库存报表生成时的计算错误 + - 修复用户报表生成时的计算错误 + +## 2. 版本 1.1.0 (2024-02-01) + +### 2.1 新增功能 + +- **认证模块**: + - 新增 `POST /api/v1/auth/refresh` 接口:刷新 token + - 新增 `POST /api/v1/auth/logout` 接口:用户登出 + +- **商品模块**: + - 新增 `GET /api/v1/products/categories` 接口:获取商品分类 + - 新增 `POST /api/v1/products/batch` 接口:批量创建商品 + +- **订单模块**: + - 新增 `POST /api/v1/orders/batch` 接口:批量创建订单 + - 新增 `GET /api/v1/orders/status` 接口:获取订单状态统计 + +- **支付模块**: + - 新增 `POST /api/v1/payments/batch` 接口:批量创建支付 + - 新增 `GET /api/v1/payments/status` 接口:获取支付状态统计 + +- **物流模块**: + - 新增 `POST /api/v1/logistics/batch` 接口:批量创建物流 + - 新增 `GET /api/v1/logistics/status` 接口:获取物流状态统计 + +- **用户模块**: + - 新增 `POST /api/v1/users/batch` 接口:批量创建用户 + - 新增 `GET /api/v1/users/roles` 接口:获取用户角色 + +- **监控模块**: + - 新增 `GET /api/v1/monitoring/health/detail` 接口:详细健康状态 + - 新增 `GET /api/v1/monitoring/metrics/detail` 接口:详细性能指标 + +- **告警模块**: + - 新增 `POST /api/v1/alerts/batch` 接口:批量创建告警 + - 新增 `GET /api/v1/alerts/severity` 接口:获取告警严重程度统计 + +- **配置模块**: + - 新增 `POST /api/v1/configs/batch` 接口:批量创建配置 + - 新增 `GET /api/v1/configs/types` 接口:获取配置类型 + +- **审计模块**: + - 新增 `GET /api/v1/audit/actions` 接口:获取审计操作类型 + - 新增 `GET /api/v1/audit/resources` 接口:获取审计资源类型 + +- **数据模块**: + - 新增 `POST /api/v1/data/import/batch` 接口:批量导入数据 + - 新增 `GET /api/v1/data/export/batch` 接口:批量导出数据 + +- **报表模块**: + - 新增 `GET /api/v1/reports/sales/detail` 接口:详细销售报表 + - 新增 `GET /api/v1/reports/inventory/detail` 接口:详细库存报表 + - 新增 `GET /api/v1/reports/users/detail` 接口:详细用户报表 + +### 2.2 变更内容 + +- **认证模块**: + - 优化 JWT 认证逻辑 + - 支持 token 过期时间配置 + +- **商品模块**: + - 优化商品搜索逻辑 + - 支持商品分类管理 + +- **订单模块**: + - 优化订单状态流转逻辑 + - 支持订单批量操作 + +- **支付模块**: + - 优化支付处理逻辑 + - 支持支付批量操作 + +- **物流模块**: + - 优化物流处理逻辑 + - 支持物流批量操作 + +- **用户模块**: + - 优化用户角色管理逻辑 + - 支持用户批量操作 + +- **监控模块**: + - 优化健康检查逻辑 + - 支持详细性能指标 + +- **告警模块**: + - 优化告警处理逻辑 + - 支持告警批量操作 + +- **配置模块**: + - 优化配置管理逻辑 + - 支持配置批量操作 + +- **审计模块**: + - 优化审计日志查询逻辑 + - 支持审计操作类型和资源类型查询 + +- **数据模块**: + - 优化数据导入导出逻辑 + - 支持数据批量操作 + +- **报表模块**: + - 优化报表生成逻辑 + - 支持详细报表生成 + +### 2.3 修复问题 + +- **认证模块**: + - 修复 token 刷新时的错误处理 + - 修复登出时的 token 失效处理 + +- **商品模块**: + - 修复商品分类查询时的错误处理 + - 修复商品批量创建时的参数验证 + +- **订单模块**: + - 修复订单批量创建时的参数验证 + - 修复订单状态统计时的计算错误 + +- **支付模块**: + - 修复支付批量创建时的参数验证 + - 修复支付状态统计时的计算错误 + +- **物流模块**: + - 修复物流批量创建时的参数验证 + - 修复物流状态统计时的计算错误 + +- **用户模块**: + - 修复用户批量创建时的参数验证 + - 修复用户角色查询时的错误处理 + +- **监控模块**: + - 修复详细健康状态查询时的错误处理 + - 修复详细性能指标查询时的计算错误 + +- **告警模块**: + - 修复告警批量创建时的参数验证 + - 修复告警严重程度统计时的计算错误 + +- **配置模块**: + - 修复配置批量创建时的参数验证 + - 修复配置类型查询时的错误处理 + +- **审计模块**: + - 修复审计操作类型查询时的错误处理 + - 修复审计资源类型查询时的错误处理 + +- **数据模块**: + - 修复数据批量导入时的格式验证 + - 修复数据批量导出时的格式转换 + +- **报表模块**: + - 修复详细销售报表生成时的计算错误 + - 修复详细库存报表生成时的计算错误 + - 修复详细用户报表生成时的计算错误 + +## 3. 总结 + +本 API 变更日志详细记录了 Crawlful Hub 项目的 API 变更历史。通过本日志,您可以了解 API 的新增功能、变更内容和修复问题,便于 API 的使用和维护。 diff --git a/index.md b/index.md new file mode 100644 index 0000000..82481d7 --- /dev/null +++ b/index.md @@ -0,0 +1,83 @@ +# Java 后端平移计划 + +## 项目背景 + +本项目需要将现有的 Node.js 后端平移为 Java 后端,保持与前端和客户端的兼容性,不修改现有代码。 + +## 目录结构 + +``` +serverjava/ +├── src/ # Java 源代码 +│ ├── api/ # API 层 +│ ├── service/ # 服务层 +│ ├── model/ # 数据模型 +│ ├── config/ # 配置文件 +│ └── util/ # 工具类 +├── pom.xml # Maven 配置文件 +└── README.md # 项目说明 +``` + +## 平移原则 + +1. **保持 API 兼容性**:所有 API 接口的路径、参数、返回格式必须与 Node.js 版本一致 +2. **保持业务逻辑一致**:所有业务逻辑必须与 Node.js 版本保持一致 +3. **不修改前端和客户端**:平移过程中不得修改任何前端或客户端代码 +4. **不修改 Node.js 代码**:平移过程中不得修改任何 Node.js 后端代码 + +## 平移范围 + +### 核心服务 + +1. **认证服务**:用户登录、注册、权限管理 +2. **商品服务**:商品管理、库存管理 +3. **订单服务**:订单创建、状态管理、物流跟踪 +4. **支付服务**:支付处理、退款管理 +5. **数据服务**:数据统计、报表生成 + +### API 接口 + +所有现有的 Node.js API 接口都需要在 Java 后端中实现,包括: + +- `/api/auth/*`:认证相关接口 +- `/api/product/*`:商品相关接口 +- `/api/order/*`:订单相关接口 +- `/api/payment/*`:支付相关接口 +- `/api/report/*`:报表相关接口 + +## 技术栈选择 + +- **框架**:Spring Boot 3.x +- **数据库**:MySQL 8.0 +- **缓存**:Redis +- **认证**:JWT +- **构建工具**:Maven +- **Java 版本**:Java 17(Spring Boot 3.0+ 最低要求) + +## 实现步骤 + +1. **搭建项目结构**:创建 Spring Boot 项目,设置基本目录结构 +2. **配置文件**:配置数据库连接、Redis 连接等 +3. **数据模型**:根据 Node.js 版本创建对应的 Java 数据模型 +4. **服务层**:实现业务逻辑,保持与 Node.js 版本一致 +5. **API 层**:实现 RESTful API 接口,保持与 Node.js 版本一致 +6. **测试**:确保所有 API 接口正常工作 +7. **部署**:部署 Java 后端服务 + +## 注意事项 + +1. **数据迁移**:需要确保数据结构与 Node.js 版本一致,避免数据丢失 +2. **性能优化**:针对 Java 特性进行性能优化,确保服务响应速度 +3. **安全性**:保持与 Node.js 版本相同的安全措施,确保系统安全 +4. **监控**:实现与 Node.js 版本相同的监控机制,确保系统稳定运行 + +## 时间计划 + +1. **项目搭建**:1 周 +2. **核心功能实现**:3 周 +3. **测试与优化**:1 周 +4. **部署与上线**:1 周 + +## 联系方式 + +如有任何问题,请联系项目负责人。 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5097471 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + com.crawlful + crawlful-hub-backend + 1.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-cache + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + + org.apache.commons + commons-lang3 + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.flywaydb + flyway-core + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.2 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/com/crawlful/hub/Application.java b/src/main/java/com/crawlful/hub/Application.java new file mode 100644 index 0000000..6faf911 --- /dev/null +++ b/src/main/java/com/crawlful/hub/Application.java @@ -0,0 +1,11 @@ +package com.crawlful.hub; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/AlertController.java b/src/main/java/com/crawlful/hub/api/controllers/AlertController.java new file mode 100644 index 0000000..5313fe7 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/AlertController.java @@ -0,0 +1,199 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Alert; +import com.crawlful.hub.service.AlertService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/alerts") +public class AlertController { + @Autowired + private AlertService alertService; + + @PostMapping + public ResponseEntity createAlert(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Alert alert = alertService.createAlert(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("alertId", alert.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getAlerts(@RequestParam String tenantId, @RequestParam Map params) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map filters = new HashMap<>(); + if (params.containsKey("status")) { + filters.put("status", params.get("status")); + } + if (params.containsKey("severity")) { + filters.put("severity", params.get("severity")); + } + if (params.containsKey("alertType")) { + filters.put("alertType", params.get("alertType")); + } + if (params.containsKey("startDate") && params.containsKey("endDate")) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + filters.put("startDate", sdf.parse(params.get("startDate"))); + filters.put("endDate", sdf.parse(params.get("endDate"))); + } catch (ParseException e) { + return new ResponseEntity<>(Map.of("success", false, "error", "Invalid date format. Use yyyy-MM-dd"), HttpStatus.BAD_REQUEST); + } + } + + List alerts = alertService.getAlerts(tenantId, filters); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", alerts); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getAlertById(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Alert alert = alertService.getAlertById(tenantId, id); + if (alert == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Alert not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", alert); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}/resolve") + public ResponseEntity resolveAlert(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + alertService.resolveAlert(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Alert resolved successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}/status") + public ResponseEntity updateAlertStatus(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String status = (String) request.get("status"); + + if (tenantId == null || status == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or status"), HttpStatus.BAD_REQUEST); + } + + alertService.updateAlertStatus(tenantId, id, status); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Alert status updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/stats") + public ResponseEntity getAlertStats(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map stats = alertService.getAlertStats(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", stats); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/check-thresholds") + public ResponseEntity checkThresholds(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + alertService.checkThresholds(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Thresholds checked successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/AuditController.java b/src/main/java/com/crawlful/hub/api/controllers/AuditController.java new file mode 100644 index 0000000..442dde6 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/AuditController.java @@ -0,0 +1,155 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Audit; +import com.crawlful.hub.service.AuditService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/audits") +public class AuditController { + @Autowired + private AuditService auditService; + + @PostMapping + public ResponseEntity createAudit(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Audit audit = auditService.createAudit(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("auditId", audit.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getAudits(@RequestParam String tenantId, @RequestParam Map params) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map filters = new HashMap<>(); + if (params.containsKey("shopId")) { + filters.put("shopId", params.get("shopId")); + } + if (params.containsKey("userId")) { + filters.put("userId", Long.parseLong(params.get("userId"))); + } + if (params.containsKey("action")) { + filters.put("action", params.get("action")); + } + if (params.containsKey("resourceType")) { + filters.put("resourceType", params.get("resourceType")); + } + if (params.containsKey("startDate") && params.containsKey("endDate")) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + filters.put("startDate", sdf.parse(params.get("startDate"))); + filters.put("endDate", sdf.parse(params.get("endDate"))); + } catch (ParseException e) { + return new ResponseEntity<>(Map.of("success", false, "error", "Invalid date format. Use yyyy-MM-dd"), HttpStatus.BAD_REQUEST); + } + } + + List audits = auditService.getAudits(tenantId, filters); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", audits); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/stats") + public ResponseEntity getAuditStats(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map stats = auditService.getAuditStats(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", stats); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/recent") + public ResponseEntity getRecentAudits(@RequestParam String tenantId, @RequestParam(defaultValue = "10") int limit) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + List recentAudits = auditService.getRecentAudits(tenantId, limit); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", recentAudits); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/search") + public ResponseEntity searchAudits(@RequestParam String tenantId, @RequestParam String keyword) { + try { + if (tenantId == null || keyword == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or keyword"), HttpStatus.BAD_REQUEST); + } + + Map searchResults = auditService.searchAudits(tenantId, keyword); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", searchResults); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/AuthController.java b/src/main/java/com/crawlful/hub/api/controllers/AuthController.java new file mode 100644 index 0000000..e092dfc --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/AuthController.java @@ -0,0 +1,57 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.User; +import com.crawlful.hub.service.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/v1/auth") +public class AuthController { + @Autowired + private AuthService authService; + + @PostMapping("/register") + public ResponseEntity register(@RequestBody Map request) { + try { + String tenantId = request.get("tenantId"); + String username = request.get("username"); + String password = request.get("password"); + String email = request.get("email"); + String role = request.get("role"); + + User user = authService.register(tenantId, username, password, email, role); + Map response = new HashMap<>(); + response.put("user", user); + response.put("message", "User registered successfully"); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map request) { + try { + String tenantId = request.get("tenantId"); + String username = request.get("username"); + String password = request.get("password"); + + String token = authService.login(tenantId, username, password); + Map response = new HashMap<>(); + response.put("token", token); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/ConfigController.java b/src/main/java/com/crawlful/hub/api/controllers/ConfigController.java new file mode 100644 index 0000000..2feced2 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/ConfigController.java @@ -0,0 +1,165 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Config; +import com.crawlful.hub.service.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/configs") +public class ConfigController { + @Autowired + private ConfigService configService; + + @PostMapping + public ResponseEntity createConfig(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Config config = configService.createConfig(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("configId", config.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getConfigs(@RequestParam String tenantId, @RequestParam(required = false) String shopId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + List configs = configService.getConfigs(tenantId, shopId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", configs); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/value") + public ResponseEntity getConfigValue(@RequestParam String tenantId, @RequestParam String configKey, @RequestParam(required = false) String shopId) { + try { + if (tenantId == null || configKey == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or configKey"), HttpStatus.BAD_REQUEST); + } + + String configValue = configService.getConfigValue(tenantId, configKey, shopId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("configKey", configKey); + response.put("configValue", configValue); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/values") + public ResponseEntity getConfigValues(@RequestParam String tenantId, @RequestParam List configKeys, @RequestParam(required = false) String shopId) { + try { + if (tenantId == null || configKeys == null || configKeys.isEmpty()) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or configKeys"), HttpStatus.BAD_REQUEST); + } + + Map configValues = configService.getConfigValues(tenantId, configKeys, shopId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", configValues); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity updateConfig(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + configService.updateConfig(tenantId, id, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Config updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteConfig(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + configService.deleteConfig(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Config deleted successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/batch") + public ResponseEntity batchUpdateConfigs(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + List> configs = (List>) request.get("configs"); + + if (tenantId == null || configs == null || configs.isEmpty()) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or configs"), HttpStatus.BAD_REQUEST); + } + + configService.batchUpdateConfigs(tenantId, configs); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Configs updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/DataController.java b/src/main/java/com/crawlful/hub/api/controllers/DataController.java new file mode 100644 index 0000000..efb03a0 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/DataController.java @@ -0,0 +1,133 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.service.DataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/v1/data") +public class DataController { + @Autowired + private DataService dataService; + + @GetMapping("/dashboard") + public ResponseEntity getDashboardData(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map dashboardData = dataService.getDashboardData(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", dashboardData); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/sales-report") + public ResponseEntity getSalesReport(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map salesReport = dataService.getSalesReport(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", salesReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/inventory-report") + public ResponseEntity getInventoryReport(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map inventoryReport = dataService.getInventoryReport(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", inventoryReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/export") + public ResponseEntity exportData(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String dataType = (String) request.get("dataType"); + String format = (String) request.get("format"); + + if (tenantId == null || dataType == null || format == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + Map exportResult = dataService.exportData(tenantId, dataType, format); + Map response = new HashMap<>(); + response.put("success", exportResult.get("success")); + response.put("data", exportResult); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/platform-performance") + public ResponseEntity getPlatformPerformance(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map platformPerformance = dataService.getPlatformPerformance(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", platformPerformance); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/LogisticsController.java b/src/main/java/com/crawlful/hub/api/controllers/LogisticsController.java new file mode 100644 index 0000000..c283be7 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/LogisticsController.java @@ -0,0 +1,159 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Logistics; +import com.crawlful.hub.service.LogisticsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/logistics") +public class LogisticsController { + @Autowired + private LogisticsService logisticsService; + + @PostMapping + public ResponseEntity createLogistics(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Logistics logistics = logisticsService.createLogistics(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("logisticsId", logistics.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getLogistics(@RequestParam Map params) { + try { + String tenantId = params.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map filters = new HashMap<>(); + if (params.containsKey("status")) { + filters.put("status", params.get("status")); + } + if (params.containsKey("orderId")) { + filters.put("orderId", Long.parseLong(params.get("orderId"))); + } + + List logisticsList = logisticsService.getLogistics(tenantId, filters); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", logisticsList); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getLogisticsById(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Logistics logistics = logisticsService.getLogisticsById(tenantId, id); + if (logistics == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Logistics not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", logistics); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity updateLogistics(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + logisticsService.updateLogistics(tenantId, id, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Logistics updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/track/{trackingNumber}") + public ResponseEntity trackLogistics(@PathVariable String trackingNumber, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map result = logisticsService.trackLogistics(tenantId, trackingNumber); + Map response = new HashMap<>(); + response.put("success", result.get("success")); + if (result.get("success").equals(true)) { + response.put("data", result); + } else { + response.put("error", result.get("error")); + } + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/calculate-cost") + public ResponseEntity calculateShippingCost(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map result = logisticsService.calculateShippingCost(tenantId, request); + Map response = new HashMap<>(); + response.put("success", result.get("success")); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/MonitoringController.java b/src/main/java/com/crawlful/hub/api/controllers/MonitoringController.java new file mode 100644 index 0000000..352114c --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/MonitoringController.java @@ -0,0 +1,129 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.service.MonitoringService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/v1/monitoring") +public class MonitoringController { + @Autowired + private MonitoringService monitoringService; + + @GetMapping("/health") + public ResponseEntity getSystemHealth() { + try { + Map health = monitoringService.getSystemHealth(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", health); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/metrics") + public ResponseEntity getPerformanceMetrics() { + try { + Map metrics = monitoringService.getPerformanceMetrics(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", metrics); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/services") + public ResponseEntity getServiceStatus() { + try { + Map status = monitoringService.getServiceStatus(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", status); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/database") + public ResponseEntity getDatabaseStatus() { + try { + Map dbStatus = monitoringService.getDatabaseStatus(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", dbStatus); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/cache") + public ResponseEntity getCacheStatus() { + try { + Map cacheStatus = monitoringService.getCacheStatus(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", cacheStatus); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/stats") + public ResponseEntity getSystemStats() { + try { + Map stats = monitoringService.getSystemStats(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", stats); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/ping") + public ResponseEntity ping() { + try { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Pong"); + response.put("timestamp", System.currentTimeMillis()); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/OrderController.java b/src/main/java/com/crawlful/hub/api/controllers/OrderController.java new file mode 100644 index 0000000..ec9607f --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/OrderController.java @@ -0,0 +1,521 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Order; +import com.crawlful.hub.service.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/orders") +public class OrderController { + @Autowired + private OrderService orderService; + + @PostMapping + public ResponseEntity createOrder(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Order order = orderService.createOrder(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("orderId", order.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getOrders(@RequestParam Map params) { + try { + String tenantId = params.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map queryParams = new HashMap<>(); + if (params.containsKey("page")) { + queryParams.put("page", Integer.parseInt(params.get("page"))); + } + if (params.containsKey("pageSize")) { + queryParams.put("pageSize", Integer.parseInt(params.get("pageSize"))); + } + if (params.containsKey("status")) { + queryParams.put("status", params.get("status")); + } + if (params.containsKey("platform")) { + queryParams.put("platform", params.get("platform")); + } + + List orders = orderService.getOrders(tenantId, queryParams); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", orders); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getOrderById(@PathVariable String id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Order order = orderService.getOrderById(tenantId, id); + if (order == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Order not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", order); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity updateOrder(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + orderService.updateOrder(id, tenantId, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteOrder(@PathVariable String id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + orderService.deleteOrder(id, tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order deleted successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/sync") + public ResponseEntity triggerManualSync(@RequestBody Map request) { + try { + String platform = (String) request.get("platform"); + String shopId = (String) request.get("shopId"); + + if (platform == null || shopId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing platform or shopId"), HttpStatus.BAD_REQUEST); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Manual sync triggered for " + platform); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/stats") + public ResponseEntity getStats(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map stats = orderService.getOrderStats(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", stats); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/status") + public ResponseEntity transitionOrderStatus(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String status = (String) request.get("status"); + String reason = (String) request.get("reason"); + + if (tenantId == null || status == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or status"), HttpStatus.BAD_REQUEST); + } + + orderService.transitionOrderStatus(tenantId, id, status, reason); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order status updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/batch") + public ResponseEntity batchUpdateOrders(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + List orderIds = (List) request.get("orderIds"); + Map updates = (Map) request.get("updates"); + + if (tenantId == null || orderIds == null || updates == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId, orderIds, or updates"), HttpStatus.BAD_REQUEST); + } + + Map result = orderService.batchUpdateOrders(tenantId, orderIds, updates); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/batch/audit") + public ResponseEntity batchAuditOrders(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + List orderIds = (List) request.get("orderIds"); + + if (tenantId == null || orderIds == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or orderIds"), HttpStatus.BAD_REQUEST); + } + + Map result = orderService.batchAuditOrders(tenantId, orderIds); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/batch/ship") + public ResponseEntity batchShipOrders(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + List orderIds = (List) request.get("orderIds"); + + if (tenantId == null || orderIds == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or orderIds"), HttpStatus.BAD_REQUEST); + } + + Map result = orderService.batchShipOrders(tenantId, orderIds); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/exception") + public ResponseEntity markOrderAsException(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String reason = (String) request.get("reason"); + + if (tenantId == null || reason == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or reason"), HttpStatus.BAD_REQUEST); + } + + orderService.markOrderAsException(tenantId, id, reason); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order marked as exception"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/reroute") + public ResponseEntity autoRerouteOrder(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + orderService.autoRerouteOrder(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order rerouted successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/retry") + public ResponseEntity retryExceptionOrder(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + orderService.retryExceptionOrder(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order retried successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/cancel") + public ResponseEntity cancelOrder(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String reason = (String) request.get("reason"); + + if (tenantId == null || reason == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or reason"), HttpStatus.BAD_REQUEST); + } + + orderService.cancelOrder(tenantId, id, reason); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order cancelled successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/refund") + public ResponseEntity requestRefund(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String reason = (String) request.get("reason"); + Double amount = (Double) request.get("amount"); + + if (tenantId == null || reason == null || amount == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId, reason, or amount"), HttpStatus.BAD_REQUEST); + } + + String refundId = orderService.requestRefund(tenantId, id, reason, amount); + Map response = new HashMap<>(); + response.put("success", true); + response.put("refundId", refundId); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/refund/{id}/approve") + public ResponseEntity approveRefund(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + Boolean approved = (Boolean) request.get("approved"); + String note = (String) request.get("note"); + + if (tenantId == null || approved == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or approved"), HttpStatus.BAD_REQUEST); + } + + orderService.approveRefund(tenantId, id, approved, note); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Refund processed successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/after-sales") + public ResponseEntity requestAfterSales(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String type = (String) request.get("type"); + String reason = (String) request.get("reason"); + List> items = (List>) request.get("items"); + + if (tenantId == null || type == null || reason == null || items == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + String serviceId = orderService.requestAfterSales(tenantId, id, type, reason, items); + Map response = new HashMap<>(); + response.put("success", true); + response.put("serviceId", serviceId); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/after-sales/{id}/process") + public ResponseEntity processAfterSales(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String action = (String) request.get("action"); + String note = (String) request.get("note"); + + if (tenantId == null || action == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or action"), HttpStatus.BAD_REQUEST); + } + + orderService.processAfterSales(tenantId, id, action, note); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "After-sales service processed successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/complete") + public ResponseEntity completeOrder(@PathVariable String id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + orderService.completeOrder(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Order completed successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/webhook/{platform}") + public ResponseEntity handlePlatformWebhook(@PathVariable String platform, @RequestBody Map payload, @RequestHeader Map headers) { + try { + String tenantId = headers.get("x-tenant-id") != null ? headers.get("x-tenant-id") : "default-tenant"; + String shopId = headers.get("x-shop-id") != null ? headers.get("x-shop-id") : "default-shop"; + + Order order = orderService.mapPlatformPayloadToOrder(platform, payload, tenantId, shopId); + if (order == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Unsupported or invalid payload for platform: " + platform), HttpStatus.BAD_REQUEST); + } + + Order savedOrder = orderService.createOrder(Map.of( + "tenantId", order.getTenantId(), + "shopId", order.getShopId(), + "platform", order.getPlatform(), + "platformOrderId", order.getPlatformOrderId(), + "status", order.getStatus(), + "totalAmount", order.getTotalAmount(), + "currency", order.getCurrency(), + "customerInfo", order.getCustomerInfo(), + "items", order.getItems(), + "shippingAddress", order.getShippingAddress() + )); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("orderId", savedOrder.getId()); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Internal server error"); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/PaymentController.java b/src/main/java/com/crawlful/hub/api/controllers/PaymentController.java new file mode 100644 index 0000000..5e9718c --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/PaymentController.java @@ -0,0 +1,165 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Payment; +import com.crawlful.hub.service.PaymentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/payment") +public class PaymentController { + @Autowired + private PaymentService paymentService; + + @PostMapping + public ResponseEntity createPayment(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Payment payment = paymentService.createPayment(request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("paymentId", payment.getId()); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getPayments(@RequestParam Map params) { + try { + String tenantId = params.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map filters = new HashMap<>(); + if (params.containsKey("status")) { + filters.put("status", params.get("status")); + } + if (params.containsKey("orderId")) { + filters.put("orderId", Long.parseLong(params.get("orderId"))); + } + + List payments = paymentService.getPayments(tenantId, filters); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", payments); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getPaymentById(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Payment payment = paymentService.getPaymentById(tenantId, id); + if (payment == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Payment not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", payment); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity updatePayment(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + paymentService.updatePayment(tenantId, id, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Payment updated successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/callback") + public ResponseEntity processPaymentCallback(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String transactionId = (String) request.get("transactionId"); + + if (tenantId == null || transactionId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId or transactionId"), HttpStatus.BAD_REQUEST); + } + + paymentService.processPaymentCallback(tenantId, transactionId, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "Callback processed successfully"); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/refund") + public ResponseEntity refundPayment(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + Double amount = (Double) request.get("amount"); + String reason = (String) request.get("reason"); + + if (tenantId == null || amount == null || reason == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId, amount, or reason"), HttpStatus.BAD_REQUEST); + } + + Map result = paymentService.refundPayment(tenantId, id, amount, reason); + Map response = new HashMap<>(); + response.put("success", result.get("success")); + if (result.get("success").equals(true)) { + response.put("refundId", result.get("refundId")); + } else { + response.put("error", result.get("error")); + } + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/ProductController.java b/src/main/java/com/crawlful/hub/api/controllers/ProductController.java new file mode 100644 index 0000000..489835c --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/ProductController.java @@ -0,0 +1,208 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.Product; +import com.crawlful.hub.service.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/product") +public class ProductController { + @Autowired + private ProductService productService; + + @PostMapping + public ResponseEntity create(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Product product = productService.create(tenantId, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", Map.of("id", product.getId())); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getAll(@RequestParam Map params) { + try { + String tenantId = params.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map filters = new HashMap<>(); + if (params.containsKey("platform")) { + filters.put("platform", params.get("platform")); + } + if (params.containsKey("status")) { + filters.put("status", params.get("status")); + } + + int page = 1; + int size = 10; + if (params.containsKey("page")) { + try { + page = Integer.parseInt(params.get("page")); + } catch (NumberFormatException e) { + page = 1; + } + } + if (params.containsKey("size")) { + try { + size = Integer.parseInt(params.get("size")); + } catch (NumberFormatException e) { + size = 10; + } + } + + List products = productService.getAll(tenantId, filters, page, size); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", products); + response.put("page", page); + response.put("size", size); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Product product = productService.getById(tenantId, id); + if (product == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Product not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", product); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Product product = productService.update(tenantId, id, request); + if (product == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Product not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", product); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + productService.delete(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/{id}/wash-and-localize") + public ResponseEntity washAndLocalize(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + String targetMarket = (String) request.get("targetMarket"); + String targetLang = (String) request.get("targetLang"); + + if (tenantId == null || targetMarket == null || targetLang == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + Map result = productService.washAndLocalize(tenantId, id, targetMarket, targetLang); + if (result == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Product not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}/analyze-arbitrage") + public ResponseEntity analyzeArbitrage(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map result = productService.analyzeProductArbitrage(tenantId, id); + if (result == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Product not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/ReportController.java b/src/main/java/com/crawlful/hub/api/controllers/ReportController.java new file mode 100644 index 0000000..089ecd0 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/ReportController.java @@ -0,0 +1,192 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.service.ReportService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/v1/reports") +public class ReportController { + @Autowired + private ReportService reportService; + + @GetMapping("/sales") + public ResponseEntity generateSalesReport(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map salesReport = reportService.generateSalesReport(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", salesReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/inventory") + public ResponseEntity generateInventoryReport(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map inventoryReport = reportService.generateInventoryReport(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", inventoryReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/user") + public ResponseEntity generateUserReport(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + Map userReport = reportService.generateUserReport(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", userReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/payment") + public ResponseEntity generatePaymentReport(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map paymentReport = reportService.generatePaymentReport(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", paymentReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/logistics") + public ResponseEntity generateLogisticsReport(@RequestParam String tenantId, @RequestParam String startDate, @RequestParam String endDate) { + try { + if (tenantId == null || startDate == null || endDate == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing required parameters"), HttpStatus.BAD_REQUEST); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date start = sdf.parse(startDate); + Date end = sdf.parse(endDate); + + Map logisticsReport = reportService.generateLogisticsReport(tenantId, start, end); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", logisticsReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (ParseException e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", "Invalid date format. Use yyyy-MM-dd"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/custom") + public ResponseEntity generateCustomReport(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + // 处理日期参数 + if (request.containsKey("startDate") && request.get("startDate") instanceof String) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + try { + request.put("startDate", sdf.parse((String) request.get("startDate"))); + } catch (ParseException e) { + return new ResponseEntity<>(Map.of("success", false, "error", "Invalid startDate format. Use yyyy-MM-dd"), HttpStatus.BAD_REQUEST); + } + } + if (request.containsKey("endDate") && request.get("endDate") instanceof String) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + try { + request.put("endDate", sdf.parse((String) request.get("endDate"))); + } catch (ParseException e) { + return new ResponseEntity<>(Map.of("success", false, "error", "Invalid endDate format. Use yyyy-MM-dd"), HttpStatus.BAD_REQUEST); + } + } + + Map customReport = reportService.generateCustomReport(tenantId, request); + Map response = new HashMap<>(); + if (customReport.containsKey("error")) { + response.put("success", false); + response.put("error", customReport.get("error")); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } else { + response.put("success", true); + response.put("data", customReport); + return new ResponseEntity<>(response, HttpStatus.OK); + } + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/api/controllers/UserController.java b/src/main/java/com/crawlful/hub/api/controllers/UserController.java new file mode 100644 index 0000000..2beeb07 --- /dev/null +++ b/src/main/java/com/crawlful/hub/api/controllers/UserController.java @@ -0,0 +1,128 @@ +package com.crawlful.hub.api.controllers; + +import com.crawlful.hub.model.User; +import com.crawlful.hub.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/v1/user") +public class UserController { + @Autowired + private UserService userService; + + @PostMapping + public ResponseEntity create(@RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + User user = userService.create(tenantId, request); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", Map.of("id", user.getId())); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping + public ResponseEntity getAll(@RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + List users = userService.getAll(tenantId); + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", users); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + User user = userService.getById(tenantId, id); + if (user == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "User not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", user); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody Map request) { + try { + String tenantId = (String) request.get("tenantId"); + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + User user = userService.update(tenantId, id, request); + if (user == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "User not found"), HttpStatus.NOT_FOUND); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", user); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id, @RequestParam String tenantId) { + try { + if (tenantId == null) { + return new ResponseEntity<>(Map.of("success", false, "error", "Missing tenantId"), HttpStatus.BAD_REQUEST); + } + + userService.delete(tenantId, id); + Map response = new HashMap<>(); + response.put("success", true); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/crawlful/hub/config/CacheConfig.java b/src/main/java/com/crawlful/hub/config/CacheConfig.java new file mode 100644 index 0000000..f452315 --- /dev/null +++ b/src/main/java/com/crawlful/hub/config/CacheConfig.java @@ -0,0 +1,40 @@ +package com.crawlful.hub.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .build(); + } +} diff --git a/src/main/java/com/crawlful/hub/config/InternationalizationConfig.java b/src/main/java/com/crawlful/hub/config/InternationalizationConfig.java new file mode 100644 index 0000000..034974a --- /dev/null +++ b/src/main/java/com/crawlful/hub/config/InternationalizationConfig.java @@ -0,0 +1,44 @@ +package com.crawlful.hub.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +import java.util.Locale; + +@Configuration +public class InternationalizationConfig implements WebMvcConfigurer { + + @Bean + public ResourceBundleMessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("i18n/messages"); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } + + @Bean + public LocaleResolver localeResolver() { + AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver(); + resolver.setDefaultLocale(Locale.US); + return resolver; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); + interceptor.setParamName("lang"); + return interceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/src/main/java/com/crawlful/hub/config/OpenApiConfig.java b/src/main/java/com/crawlful/hub/config/OpenApiConfig.java new file mode 100644 index 0000000..41090f8 --- /dev/null +++ b/src/main/java/com/crawlful/hub/config/OpenApiConfig.java @@ -0,0 +1,28 @@ +package com.crawlful.hub.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Crawlful Hub API") + .version("1.0") + .description("Crawlful Hub 后端 API 文档") + .license(new License().name("MIT").url("https://opensource.org/licenses/MIT"))) + .servers(List.of( + new Server().url("http://localhost:3001").description("Local Server") + )); + } +} diff --git a/src/main/java/com/crawlful/hub/config/RateLimitFilter.java b/src/main/java/com/crawlful/hub/config/RateLimitFilter.java new file mode 100644 index 0000000..1095341 --- /dev/null +++ b/src/main/java/com/crawlful/hub/config/RateLimitFilter.java @@ -0,0 +1,92 @@ +package com.crawlful.hub.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class RateLimitFilter implements Filter { + + private static final int MAX_REQUESTS_PER_MINUTE = 60; + private static final Map requestRates = new ConcurrentHashMap<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String clientIp = getClientIp(httpRequest); + String key = clientIp + ":" + httpRequest.getRequestURI(); + + RequestRate rate = requestRates.computeIfAbsent(key, k -> new RequestRate()); + + synchronized (rate) { + long currentTime = System.currentTimeMillis(); + if (currentTime - rate.getLastResetTime() > 60000) { + rate.reset(); + } + + if (rate.getCount() >= MAX_REQUESTS_PER_MINUTE) { + httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); + httpResponse.getWriter().write("Too many requests, please try again later"); + return; + } + + rate.increment(); + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + + private String getClientIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty()) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty()) { + ip = request.getRemoteAddr(); + } + return ip; + } + + private static class RequestRate { + private final AtomicInteger count = new AtomicInteger(0); + private long lastResetTime = System.currentTimeMillis(); + + public int getCount() { + return count.get(); + } + + public void increment() { + count.incrementAndGet(); + } + + public void reset() { + count.set(0); + lastResetTime = System.currentTimeMillis(); + } + + public long getLastResetTime() { + return lastResetTime; + } + } +} diff --git a/src/main/java/com/crawlful/hub/config/SecurityConfig.java b/src/main/java/com/crawlful/hub/config/SecurityConfig.java new file mode 100644 index 0000000..6e0abcc --- /dev/null +++ b/src/main/java/com/crawlful/hub/config/SecurityConfig.java @@ -0,0 +1,36 @@ +package com.crawlful.hub.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.web.SecurityFilterChain; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .addFilterBefore(new RateLimitFilter(), UsernamePasswordAuthenticationFilter.class) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + ) + .authorizeRequests() + .requestMatchers("/v1/auth/**").permitAll() + .requestMatchers("/api-docs/**").permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v1/monitoring/**").permitAll() + .anyRequest().authenticated(); + + return http.build(); + } +} diff --git a/src/main/java/com/crawlful/hub/exception/BusinessException.java b/src/main/java/com/crawlful/hub/exception/BusinessException.java new file mode 100644 index 0000000..171ae44 --- /dev/null +++ b/src/main/java/com/crawlful/hub/exception/BusinessException.java @@ -0,0 +1,20 @@ +package com.crawlful.hub.exception; + +public class BusinessException extends RuntimeException { + private String errorCode; + private int statusCode; + + public BusinessException(String message, String errorCode, int statusCode) { + super(message); + this.errorCode = errorCode; + this.statusCode = statusCode; + } + + public String getErrorCode() { + return errorCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/src/main/java/com/crawlful/hub/exception/GlobalExceptionHandler.java b/src/main/java/com/crawlful/hub/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..1df9e6c --- /dev/null +++ b/src/main/java/com/crawlful/hub/exception/GlobalExceptionHandler.java @@ -0,0 +1,58 @@ +package com.crawlful.hub.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException ex, WebRequest request) { + Map errorResponse = new HashMap<>(); + errorResponse.put("success", false); + errorResponse.put("error", ex.getMessage()); + errorResponse.put("errorCode", ex.getErrorCode()); + errorResponse.put("status", ex.getStatusCode()); + + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(ex.getStatusCode())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + Map errorResponse = new HashMap<>(); + errorResponse.put("success", false); + errorResponse.put("error", ex.getMessage()); + errorResponse.put("errorCode", "INTERNAL_SERVER_ERROR"); + errorResponse.put("status", 500); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + Map errorResponse = new HashMap<>(); + errorResponse.put("success", false); + errorResponse.put("error", ex.getMessage()); + errorResponse.put("errorCode", "BAD_REQUEST"); + errorResponse.put("status", 400); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NullPointerException.class) + public ResponseEntity handleNullPointerException(NullPointerException ex, WebRequest request) { + Map errorResponse = new HashMap<>(); + errorResponse.put("success", false); + errorResponse.put("error", "Null pointer exception: " + ex.getMessage()); + errorResponse.put("errorCode", "INTERNAL_SERVER_ERROR"); + errorResponse.put("status", 500); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/crawlful/hub/model/Alert.java b/src/main/java/com/crawlful/hub/model/Alert.java new file mode 100644 index 0000000..274506d --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Alert.java @@ -0,0 +1,131 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_alert") +public class Alert { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id") + private String tenantId; + + @Column(name = "alert_type") + private String alertType; + + @Column(name = "severity") + private String severity; + + @Column(name = "message") + private String message; + + @Column(name = "status") + private String status; + + @Column(name = "source") + private String source; + + @Column(name = "threshold") + private String threshold; + + @Column(name = "actual_value") + private String actualValue; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "resolved_at") + private Date resolvedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getAlertType() { + return alertType; + } + + public void setAlertType(String alertType) { + this.alertType = alertType; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getThreshold() { + return threshold; + } + + public void setThreshold(String threshold) { + this.threshold = threshold; + } + + public String getActualValue() { + return actualValue; + } + + public void setActualValue(String actualValue) { + this.actualValue = actualValue; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getResolvedAt() { + return resolvedAt; + } + + public void setResolvedAt(Date resolvedAt) { + this.resolvedAt = resolvedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Audit.java b/src/main/java/com/crawlful/hub/model/Audit.java new file mode 100644 index 0000000..22e5e37 --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Audit.java @@ -0,0 +1,131 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_audit") +public class Audit { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id") + private String tenantId; + + @Column(name = "shop_id") + private String shopId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "action") + private String action; + + @Column(name = "resource_type") + private String resourceType; + + @Column(name = "resource_id") + private String resourceId; + + @Column(name = "ip_address") + private String ipAddress; + + @Column(name = "user_agent") + private String userAgent; + + @Column(name = "details") + private String details; + + @Column(name = "created_at") + private Date createdAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getShopId() { + return shopId; + } + + public void setShopId(String shopId) { + this.shopId = shopId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Config.java b/src/main/java/com/crawlful/hub/model/Config.java new file mode 100644 index 0000000..9df7131 --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Config.java @@ -0,0 +1,109 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_config") +public class Config { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id") + private String tenantId; + + @Column(name = "shop_id") + private String shopId; + + @Column(name = "config_key", nullable = false) + private String configKey; + + @Column(name = "config_value", nullable = false) + private String configValue; + + @Column(name = "config_type") + private String configType; + + @Column(name = "description") + private String description; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getShopId() { + return shopId; + } + + public void setShopId(String shopId) { + this.shopId = shopId; + } + + public String getConfigKey() { + return configKey; + } + + public void setConfigKey(String configKey) { + this.configKey = configKey; + } + + public String getConfigValue() { + return configValue; + } + + public void setConfigValue(String configValue) { + this.configValue = configValue; + } + + public String getConfigType() { + return configType; + } + + public void setConfigType(String configType) { + this.configType = configType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Logistics.java b/src/main/java/com/crawlful/hub/model/Logistics.java new file mode 100644 index 0000000..eb37016 --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Logistics.java @@ -0,0 +1,131 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_logistics") +public class Logistics { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id", nullable = false) + private String tenantId; + + @Column(name = "order_id") + private Long orderId; + + @Column(name = "shipping_method") + private String shippingMethod; + + @Column(name = "tracking_number") + private String trackingNumber; + + @Column(name = "carrier") + private String carrier; + + @Column(name = "status") + private String status; + + @Column(name = "estimated_delivery_date") + private Date estimatedDeliveryDate; + + @Column(name = "actual_delivery_date") + private Date actualDeliveryDate; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public String getShippingMethod() { + return shippingMethod; + } + + public void setShippingMethod(String shippingMethod) { + this.shippingMethod = shippingMethod; + } + + public String getTrackingNumber() { + return trackingNumber; + } + + public void setTrackingNumber(String trackingNumber) { + this.trackingNumber = trackingNumber; + } + + public String getCarrier() { + return carrier; + } + + public void setCarrier(String carrier) { + this.carrier = carrier; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Date getEstimatedDeliveryDate() { + return estimatedDeliveryDate; + } + + public void setEstimatedDeliveryDate(Date estimatedDeliveryDate) { + this.estimatedDeliveryDate = estimatedDeliveryDate; + } + + public Date getActualDeliveryDate() { + return actualDeliveryDate; + } + + public void setActualDeliveryDate(Date actualDeliveryDate) { + this.actualDeliveryDate = actualDeliveryDate; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Order.java b/src/main/java/com/crawlful/hub/model/Order.java new file mode 100644 index 0000000..bc86ecf --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Order.java @@ -0,0 +1,164 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_order") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id", nullable = false) + private String tenantId; + + @Column(name = "shop_id") + private String shopId; + + @Column(name = "platform") + private String platform; + + @Column(name = "platform_order_id") + private String platformOrderId; + + @Column(name = "status") + private String status; + + @Column(name = "total_amount", precision = 10, scale = 2) + private Double totalAmount; + + @Column(name = "currency") + private String currency; + + @Column(name = "customer_info", columnDefinition = "JSON") + private String customerInfo; + + @Column(name = "items", columnDefinition = "JSON") + private String items; + + @Column(name = "shipping_address", columnDefinition = "JSON") + private String shippingAddress; + + @Column(name = "tracking_number") + private String trackingNumber; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getShopId() { + return shopId; + } + + public void setShopId(String shopId) { + this.shopId = shopId; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getPlatformOrderId() { + return platformOrderId; + } + + public void setPlatformOrderId(String platformOrderId) { + this.platformOrderId = platformOrderId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Double getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(Double totalAmount) { + this.totalAmount = totalAmount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getCustomerInfo() { + return customerInfo; + } + + public void setCustomerInfo(String customerInfo) { + this.customerInfo = customerInfo; + } + + public String getItems() { + return items; + } + + public void setItems(String items) { + this.items = items; + } + + public String getShippingAddress() { + return shippingAddress; + } + + public void setShippingAddress(String shippingAddress) { + this.shippingAddress = shippingAddress; + } + + public String getTrackingNumber() { + return trackingNumber; + } + + public void setTrackingNumber(String trackingNumber) { + this.trackingNumber = trackingNumber; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Payment.java b/src/main/java/com/crawlful/hub/model/Payment.java new file mode 100644 index 0000000..3e50a5b --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Payment.java @@ -0,0 +1,120 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_payment") +public class Payment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id", nullable = false) + private String tenantId; + + @Column(name = "order_id") + private Long orderId; + + @Column(name = "payment_method") + private String paymentMethod; + + @Column(name = "amount", precision = 10, scale = 2) + private Double amount; + + @Column(name = "currency") + private String currency; + + @Column(name = "status") + private String status; + + @Column(name = "transaction_id") + private String transactionId; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/Product.java b/src/main/java/com/crawlful/hub/model/Product.java new file mode 100644 index 0000000..88e648a --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/Product.java @@ -0,0 +1,209 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.Map; + +@Entity +@Table(name = "cf_product") +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id", nullable = false) + private String tenantId; + + @Column(name = "shop_id") + private String shopId; + + @Column(nullable = false) + private String title; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "main_image") + private String mainImage; + + @Column(name = "platform") + private String platform; + + @Column(name = "platform_product_id") + private String platformProductId; + + @Column(name = "price", precision = 10, scale = 2) + private Double price; + + @Column(name = "cost_price", precision = 10, scale = 2) + private Double costPrice; + + @Column(name = "quantity") + private Integer quantity; + + @Column(name = "status") + private String status; + + @Column(name = "phash") + private String phash; + + @Column(name = "semantic_hash") + private String semanticHash; + + @Column(name = "vector_embedding", columnDefinition = "TEXT") + private String vectorEmbedding; + + @Column(name = "attributes", columnDefinition = "JSON") + private String attributes; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getShopId() { + return shopId; + } + + public void setShopId(String shopId) { + this.shopId = shopId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getMainImage() { + return mainImage; + } + + public void setMainImage(String mainImage) { + this.mainImage = mainImage; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getPlatformProductId() { + return platformProductId; + } + + public void setPlatformProductId(String platformProductId) { + this.platformProductId = platformProductId; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public Double getCostPrice() { + return costPrice; + } + + public void setCostPrice(Double costPrice) { + this.costPrice = costPrice; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getPhash() { + return phash; + } + + public void setPhash(String phash) { + this.phash = phash; + } + + public String getSemanticHash() { + return semanticHash; + } + + public void setSemanticHash(String semanticHash) { + this.semanticHash = semanticHash; + } + + public String getVectorEmbedding() { + return vectorEmbedding; + } + + public void setVectorEmbedding(String vectorEmbedding) { + this.vectorEmbedding = vectorEmbedding; + } + + public String getAttributes() { + return attributes; + } + + public void setAttributes(String attributes) { + this.attributes = attributes; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/model/User.java b/src/main/java/com/crawlful/hub/model/User.java new file mode 100644 index 0000000..f37a8ae --- /dev/null +++ b/src/main/java/com/crawlful/hub/model/User.java @@ -0,0 +1,98 @@ +package com.crawlful.hub.model; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "cf_user") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tenant_id", nullable = false) + private String tenantId; + + @Column(nullable = false, unique = true) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, unique = true) + private String email; + + @Column + private String role; + + @Column(name = "created_at") + private Date createdAt; + + @Column(name = "updated_at") + private Date updatedAt; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/crawlful/hub/service/AlertRepository.java b/src/main/java/com/crawlful/hub/service/AlertRepository.java new file mode 100644 index 0000000..dc1008c --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/AlertRepository.java @@ -0,0 +1,15 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Alert; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Date; + +public interface AlertRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndStatus(String tenantId, String status); + List findByTenantIdAndSeverity(String tenantId, String severity); + List findByTenantIdAndAlertType(String tenantId, String alertType); + List findByTenantIdAndCreatedAtBetween(String tenantId, Date startDate, Date endDate); +} diff --git a/src/main/java/com/crawlful/hub/service/AlertService.java b/src/main/java/com/crawlful/hub/service/AlertService.java new file mode 100644 index 0000000..5e887f3 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/AlertService.java @@ -0,0 +1,110 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Alert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class AlertService { + @Autowired + private AlertRepository alertRepository; + + public Alert createAlert(Map alertData) { + Alert alert = new Alert(); + alert.setTenantId((String) alertData.get("tenantId")); + alert.setAlertType((String) alertData.get("alertType")); + alert.setSeverity((String) alertData.get("severity")); + alert.setMessage((String) alertData.get("message")); + alert.setStatus((String) alertData.get("status")); + alert.setSource((String) alertData.get("source")); + alert.setThreshold((String) alertData.get("threshold")); + alert.setActualValue((String) alertData.get("actualValue")); + alert.setCreatedAt(new Date()); + return alertRepository.save(alert); + } + + public List getAlerts(String tenantId, Map filters) { + String status = (String) filters.get("status"); + String severity = (String) filters.get("severity"); + String alertType = (String) filters.get("alertType"); + Date startDate = (Date) filters.get("startDate"); + Date endDate = (Date) filters.get("endDate"); + + if (startDate != null && endDate != null) { + return alertRepository.findByTenantIdAndCreatedAtBetween(tenantId, startDate, endDate); + } else if (status != null) { + return alertRepository.findByTenantIdAndStatus(tenantId, status); + } else if (severity != null) { + return alertRepository.findByTenantIdAndSeverity(tenantId, severity); + } else if (alertType != null) { + return alertRepository.findByTenantIdAndAlertType(tenantId, alertType); + } else { + return alertRepository.findByTenantId(tenantId); + } + } + + public Alert getAlertById(String tenantId, Long id) { + Alert alert = alertRepository.findById(id).orElse(null); + if (alert != null && alert.getTenantId().equals(tenantId)) { + return alert; + } + return null; + } + + public void resolveAlert(String tenantId, Long id) { + Alert alert = getAlertById(tenantId, id); + if (alert != null) { + alert.setStatus("RESOLVED"); + alert.setResolvedAt(new Date()); + alertRepository.save(alert); + } + } + + public void updateAlertStatus(String tenantId, Long id, String status) { + Alert alert = getAlertById(tenantId, id); + if (alert != null) { + alert.setStatus(status); + if ("RESOLVED".equals(status)) { + alert.setResolvedAt(new Date()); + } + alertRepository.save(alert); + } + } + + public Map getAlertStats(String tenantId, Date startDate, Date endDate) { + List alerts = alertRepository.findByTenantIdAndCreatedAtBetween(tenantId, startDate, endDate); + int totalAlerts = alerts.size(); + + // 按严重程度统计 + Map severityStats = new java.util.HashMap<>(); + // 按状态统计 + Map statusStats = new java.util.HashMap<>(); + // 按类型统计 + Map typeStats = new java.util.HashMap<>(); + + for (var alert : alerts) { + severityStats.put(alert.getSeverity(), severityStats.getOrDefault(alert.getSeverity(), 0) + 1); + statusStats.put(alert.getStatus(), statusStats.getOrDefault(alert.getStatus(), 0) + 1); + typeStats.put(alert.getAlertType(), typeStats.getOrDefault(alert.getAlertType(), 0) + 1); + } + + return Map.of( + "totalAlerts", totalAlerts, + "severityStats", severityStats, + "statusStats", statusStats, + "typeStats", typeStats, + "startDate", startDate, + "endDate", endDate + ); + } + + public void checkThresholds(String tenantId) { + // 模拟检查阈值 + // 这里可以添加具体的阈值检查逻辑 + // 例如检查系统负载、数据库连接数、缓存使用率等 + } +} diff --git a/src/main/java/com/crawlful/hub/service/AuditRepository.java b/src/main/java/com/crawlful/hub/service/AuditRepository.java new file mode 100644 index 0000000..3f6d5f1 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/AuditRepository.java @@ -0,0 +1,16 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Audit; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Date; + +public interface AuditRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndShopId(String tenantId, String shopId); + List findByTenantIdAndUserId(String tenantId, Long userId); + List findByTenantIdAndAction(String tenantId, String action); + List findByTenantIdAndResourceType(String tenantId, String resourceType); + List findByTenantIdAndCreatedAtBetween(String tenantId, Date startDate, Date endDate); +} diff --git a/src/main/java/com/crawlful/hub/service/AuditService.java b/src/main/java/com/crawlful/hub/service/AuditService.java new file mode 100644 index 0000000..a482dd5 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/AuditService.java @@ -0,0 +1,110 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Audit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class AuditService { + @Autowired + private AuditRepository auditRepository; + + public Audit createAudit(Map auditData) { + Audit audit = new Audit(); + audit.setTenantId((String) auditData.get("tenantId")); + audit.setShopId((String) auditData.get("shopId")); + audit.setUserId((Long) auditData.get("userId")); + audit.setAction((String) auditData.get("action")); + audit.setResourceType((String) auditData.get("resourceType")); + audit.setResourceId((String) auditData.get("resourceId")); + audit.setIpAddress((String) auditData.get("ipAddress")); + audit.setUserAgent((String) auditData.get("userAgent")); + audit.setDetails((String) auditData.get("details")); + audit.setCreatedAt(new Date()); + return auditRepository.save(audit); + } + + public List getAudits(String tenantId, Map filters) { + String shopId = (String) filters.get("shopId"); + Long userId = (Long) filters.get("userId"); + String action = (String) filters.get("action"); + String resourceType = (String) filters.get("resourceType"); + Date startDate = (Date) filters.get("startDate"); + Date endDate = (Date) filters.get("endDate"); + + if (startDate != null && endDate != null) { + return auditRepository.findByTenantIdAndCreatedAtBetween(tenantId, startDate, endDate); + } else if (shopId != null) { + return auditRepository.findByTenantIdAndShopId(tenantId, shopId); + } else if (userId != null) { + return auditRepository.findByTenantIdAndUserId(tenantId, userId); + } else if (action != null) { + return auditRepository.findByTenantIdAndAction(tenantId, action); + } else if (resourceType != null) { + return auditRepository.findByTenantIdAndResourceType(tenantId, resourceType); + } else { + return auditRepository.findByTenantId(tenantId); + } + } + + public Map getAuditStats(String tenantId, Date startDate, Date endDate) { + // 生成审计统计数据 + List audits = auditRepository.findByTenantIdAndCreatedAtBetween(tenantId, startDate, endDate); + int totalAudits = audits.size(); + + // 按操作类型统计 + Map actionStats = new java.util.HashMap<>(); + // 按资源类型统计 + Map resourceTypeStats = new java.util.HashMap<>(); + // 按用户统计 + Map userStats = new java.util.HashMap<>(); + + for (var audit : audits) { + actionStats.put(audit.getAction(), actionStats.getOrDefault(audit.getAction(), 0) + 1); + resourceTypeStats.put(audit.getResourceType(), resourceTypeStats.getOrDefault(audit.getResourceType(), 0) + 1); + if (audit.getUserId() != null) { + userStats.put(audit.getUserId(), userStats.getOrDefault(audit.getUserId(), 0) + 1); + } + } + + return Map.of( + "totalAudits", totalAudits, + "actionStats", actionStats, + "resourceTypeStats", resourceTypeStats, + "userStats", userStats, + "startDate", startDate, + "endDate", endDate + ); + } + + public List getRecentAudits(String tenantId, int limit) { + // 获取最近的审计日志 + List allAudits = auditRepository.findByTenantId(tenantId); + int endIndex = Math.min(limit, allAudits.size()); + return allAudits.subList(0, endIndex); + } + + public Map searchAudits(String tenantId, String keyword) { + // 模拟搜索审计日志 + List allAudits = auditRepository.findByTenantId(tenantId); + List filteredAudits = new java.util.ArrayList<>(); + + for (var audit : allAudits) { + if (audit.getAction().contains(keyword) || + (audit.getResourceType() != null && audit.getResourceType().contains(keyword)) || + (audit.getDetails() != null && audit.getDetails().contains(keyword))) { + filteredAudits.add(audit); + } + } + + return Map.of( + "keyword", keyword, + "results", filteredAudits, + "totalResults", filteredAudits.size() + ); + } +} diff --git a/src/main/java/com/crawlful/hub/service/AuthService.java b/src/main/java/com/crawlful/hub/service/AuthService.java new file mode 100644 index 0000000..2cc4024 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/AuthService.java @@ -0,0 +1,98 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.User; +import com.crawlful.hub.util.ValidationUtil; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +public class AuthService { + @Autowired + private UserRepository userRepository; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @Value("${spring.security.jwt.secret}") + private String jwtSecret; + + @Value("${spring.security.jwt.expiration}") + private long jwtExpiration; + + public User register(String tenantId, String username, String password, String email, String role) { + // 验证输入数据 + if (tenantId == null || tenantId.isEmpty()) { + throw new RuntimeException("Tenant ID is required"); + } + if (!ValidationUtil.validateStringLength(username, 3, 50)) { + throw new RuntimeException("Username must be between 3 and 50 characters"); + } + if (!ValidationUtil.validatePassword(password)) { + throw new RuntimeException("Password must be at least 6 characters"); + } + if (!ValidationUtil.validateEmail(email)) { + throw new RuntimeException("Invalid email format"); + } + if (role == null || role.isEmpty()) { + throw new RuntimeException("Role is required"); + } + + // 检查用户名是否已存在 + if (userRepository.findByTenantIdAndUsername(tenantId, username) != null) { + throw new RuntimeException("Username already exists"); + } + + User user = new User(); + user.setTenantId(tenantId); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setEmail(email); + user.setRole(role); + user.setCreatedAt(new Date()); + user.setUpdatedAt(new Date()); + return userRepository.save(user); + } + + public String login(String tenantId, String username, String password) { + // 验证输入数据 + if (tenantId == null || tenantId.isEmpty()) { + throw new RuntimeException("Tenant ID is required"); + } + if (username == null || username.isEmpty()) { + throw new RuntimeException("Username is required"); + } + if (password == null || password.isEmpty()) { + throw new RuntimeException("Password is required"); + } + + User user = userRepository.findByTenantIdAndUsername(tenantId, username); + if (user == null || !passwordEncoder.matches(password, user.getPassword())) { + throw new RuntimeException("Invalid username or password"); + } + return generateToken(user); + } + + private String generateToken(User user) { + return Jwts.builder() + .setSubject(user.getUsername()) + .claim("tenantId", user.getTenantId()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + + public User findByUsername(String tenantId, String username) { + return userRepository.findByTenantIdAndUsername(tenantId, username); + } + + public User findByUsername(String username) { + return userRepository.findByUsername(username); + } +} diff --git a/src/main/java/com/crawlful/hub/service/ConfigRepository.java b/src/main/java/com/crawlful/hub/service/ConfigRepository.java new file mode 100644 index 0000000..e88e3a8 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/ConfigRepository.java @@ -0,0 +1,14 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Config; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ConfigRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndShopId(String tenantId, String shopId); + Optional findByTenantIdAndConfigKey(String tenantId, String configKey); + Optional findByTenantIdAndShopIdAndConfigKey(String tenantId, String shopId, String configKey); +} diff --git a/src/main/java/com/crawlful/hub/service/ConfigService.java b/src/main/java/com/crawlful/hub/service/ConfigService.java new file mode 100644 index 0000000..94df0eb --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/ConfigService.java @@ -0,0 +1,104 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Service +public class ConfigService { + @Autowired + private ConfigRepository configRepository; + + public Config createConfig(Map configData) { + Config config = new Config(); + config.setTenantId((String) configData.get("tenantId")); + config.setShopId((String) configData.get("shopId")); + config.setConfigKey((String) configData.get("configKey")); + config.setConfigValue((String) configData.get("configValue")); + config.setConfigType((String) configData.get("configType")); + config.setDescription((String) configData.get("description")); + config.setCreatedAt(new Date()); + config.setUpdatedAt(new Date()); + return configRepository.save(config); + } + + public List getConfigs(String tenantId, String shopId) { + if (shopId != null) { + return configRepository.findByTenantIdAndShopId(tenantId, shopId); + } else { + return configRepository.findByTenantId(tenantId); + } + } + + public Config getConfigByKey(String tenantId, String configKey, String shopId) { + if (shopId != null) { + Optional config = configRepository.findByTenantIdAndShopIdAndConfigKey(tenantId, shopId, configKey); + return config.orElse(null); + } else { + Optional config = configRepository.findByTenantIdAndConfigKey(tenantId, configKey); + return config.orElse(null); + } + } + + public void updateConfig(String tenantId, Long id, Map updateData) { + Config config = configRepository.findById(id).orElse(null); + if (config != null && config.getTenantId().equals(tenantId)) { + if (updateData.containsKey("configValue")) { + config.setConfigValue((String) updateData.get("configValue")); + } + if (updateData.containsKey("configType")) { + config.setConfigType((String) updateData.get("configType")); + } + if (updateData.containsKey("description")) { + config.setDescription((String) updateData.get("description")); + } + config.setUpdatedAt(new Date()); + configRepository.save(config); + } + } + + public void deleteConfig(String tenantId, Long id) { + Config config = configRepository.findById(id).orElse(null); + if (config != null && config.getTenantId().equals(tenantId)) { + configRepository.delete(config); + } + } + + public String getConfigValue(String tenantId, String configKey, String shopId) { + Config config = getConfigByKey(tenantId, configKey, shopId); + return config != null ? config.getConfigValue() : null; + } + + public Map getConfigValues(String tenantId, List configKeys, String shopId) { + Map configValues = new java.util.HashMap<>(); + for (String configKey : configKeys) { + Config config = getConfigByKey(tenantId, configKey, shopId); + if (config != null) { + configValues.put(configKey, config.getConfigValue()); + } + } + return configValues; + } + + public void batchUpdateConfigs(String tenantId, List> configs) { + for (Map configData : configs) { + String configKey = (String) configData.get("configKey"); + String shopId = (String) configData.get("shopId"); + Config existingConfig = getConfigByKey(tenantId, configKey, shopId); + + if (existingConfig != null) { + // Update existing config + updateConfig(tenantId, existingConfig.getId(), configData); + } else { + // Create new config + configData.put("tenantId", tenantId); + createConfig(configData); + } + } + } +} diff --git a/src/main/java/com/crawlful/hub/service/DataService.java b/src/main/java/com/crawlful/hub/service/DataService.java new file mode 100644 index 0000000..1915b96 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/DataService.java @@ -0,0 +1,169 @@ +package com.crawlful.hub.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class DataService { + @Autowired + private UserRepository userRepository; + @Autowired + private ProductRepository productRepository; + @Autowired + private OrderRepository orderRepository; + @Autowired + private PaymentRepository paymentRepository; + @Autowired + private LogisticsRepository logisticsRepository; + + public Map getDashboardData(String tenantId) { + // 模拟仪表盘数据 + int totalUsers = userRepository.findByTenantId(tenantId).size(); + int totalProducts = productRepository.findByTenantId(tenantId).size(); + int totalOrders = orderRepository.findByTenantId(tenantId).size(); + int totalPayments = paymentRepository.findByTenantId(tenantId).size(); + + double totalRevenue = 0.0; + for (var order : orderRepository.findByTenantId(tenantId)) { + totalRevenue += order.getTotalAmount() != null ? order.getTotalAmount() : 0; + } + + return Map.of( + "totalUsers", totalUsers, + "totalProducts", totalProducts, + "totalOrders", totalOrders, + "totalPayments", totalPayments, + "totalRevenue", totalRevenue, + "recentOrders", orderRepository.findByTenantId(tenantId).stream().limit(5).toList(), + "topProducts", productRepository.findByTenantId(tenantId).stream().limit(5).toList() + ); + } + + public Map getSalesReport(String tenantId, Date startDate, Date endDate) { + // 模拟销售报表数据 + List> salesData = new ArrayList<>(); + for (var order : orderRepository.findByTenantId(tenantId)) { + if (order.getCreatedAt().after(startDate) && order.getCreatedAt().before(endDate)) { + salesData.add(Map.of( + "orderId", order.getId(), + "platform", order.getPlatform(), + "amount", order.getTotalAmount(), + "currency", order.getCurrency(), + "status", order.getStatus(), + "createdAt", order.getCreatedAt() + )); + } + } + + double totalSales = salesData.stream() + .mapToDouble(item -> (Double) item.get("amount")) + .sum(); + int totalOrders = salesData.size(); + double averageOrderValue = totalOrders > 0 ? totalSales / totalOrders : 0; + + return Map.of( + "salesData", salesData, + "totalSales", totalSales, + "totalOrders", totalOrders, + "averageOrderValue", averageOrderValue, + "startDate", startDate, + "endDate", endDate + ); + } + + public Map getInventoryReport(String tenantId) { + // 模拟库存报表数据 + List> inventoryData = new ArrayList<>(); + for (var product : productRepository.findByTenantId(tenantId)) { + inventoryData.add(Map.of( + "productId", product.getId(), + "title", product.getTitle(), + "platform", product.getPlatform(), + "quantity", product.getQuantity(), + "price", product.getPrice(), + "status", product.getStatus() + )); + } + + int totalProducts = inventoryData.size(); + int lowStockProducts = (int) inventoryData.stream() + .filter(item -> (Integer) item.get("quantity") < 10) + .count(); + int outOfStockProducts = (int) inventoryData.stream() + .filter(item -> (Integer) item.get("quantity") == 0) + .count(); + + return Map.of( + "inventoryData", inventoryData, + "totalProducts", totalProducts, + "lowStockProducts", lowStockProducts, + "outOfStockProducts", outOfStockProducts + ); + } + + public Map exportData(String tenantId, String dataType, String format) { + // 模拟数据导出 + List data = new ArrayList<>(); + switch (dataType) { + case "users": + data = userRepository.findByTenantId(tenantId); + break; + case "products": + data = productRepository.findByTenantId(tenantId); + break; + case "orders": + data = orderRepository.findByTenantId(tenantId); + break; + case "payments": + data = paymentRepository.findByTenantId(tenantId); + break; + case "logistics": + data = logisticsRepository.findByTenantId(tenantId); + break; + } + + // 模拟导出文件路径 + String filePath = "/exports/" + dataType + "_" + System.currentTimeMillis() + "." + format; + + return Map.of( + "success", true, + "dataType", dataType, + "format", format, + "filePath", filePath, + "recordCount", data.size() + ); + } + + public Map getPlatformPerformance(String tenantId) { + // 模拟平台性能数据 + Map> platformData = new HashMap<>(); + for (var order : orderRepository.findByTenantId(tenantId)) { + String platform = order.getPlatform(); + if (!platformData.containsKey(platform)) { + platformData.put(platform, Map.of( + "orders", 0, + "revenue", 0.0, + "avgOrderValue", 0.0 + )); + } + + Map currentData = platformData.get(platform); + int orders = (Integer) currentData.get("orders") + 1; + double revenue = (Double) currentData.get("revenue") + (order.getTotalAmount() != null ? order.getTotalAmount() : 0); + double avgOrderValue = revenue / orders; + + platformData.put(platform, Map.of( + "orders", orders, + "revenue", revenue, + "avgOrderValue", avgOrderValue + )); + } + + return Map.of( + "platformData", platformData, + "totalPlatforms", platformData.size() + ); + } +} diff --git a/src/main/java/com/crawlful/hub/service/LogisticsRepository.java b/src/main/java/com/crawlful/hub/service/LogisticsRepository.java new file mode 100644 index 0000000..2d3a23f --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/LogisticsRepository.java @@ -0,0 +1,13 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Logistics; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LogisticsRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndOrderId(String tenantId, Long orderId); + List findByTenantIdAndStatus(String tenantId, String status); + Logistics findByTenantIdAndTrackingNumber(String tenantId, String trackingNumber); +} diff --git a/src/main/java/com/crawlful/hub/service/LogisticsService.java b/src/main/java/com/crawlful/hub/service/LogisticsService.java new file mode 100644 index 0000000..d856fb9 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/LogisticsService.java @@ -0,0 +1,121 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Logistics; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class LogisticsService { + @Autowired + private LogisticsRepository logisticsRepository; + + public Logistics createLogistics(Map logisticsData) { + Logistics logistics = new Logistics(); + logistics.setTenantId((String) logisticsData.get("tenantId")); + logistics.setOrderId((Long) logisticsData.get("orderId")); + logistics.setShippingMethod((String) logisticsData.get("shippingMethod")); + logistics.setTrackingNumber((String) logisticsData.get("trackingNumber")); + logistics.setCarrier((String) logisticsData.get("carrier")); + logistics.setStatus((String) logisticsData.get("status")); + logistics.setEstimatedDeliveryDate((Date) logisticsData.get("estimatedDeliveryDate")); + logistics.setCreatedAt(new Date()); + logistics.setUpdatedAt(new Date()); + return logisticsRepository.save(logistics); + } + + public List getLogistics(String tenantId, Map filters) { + String status = (String) filters.get("status"); + Long orderId = (Long) filters.get("orderId"); + + if (orderId != null) { + return logisticsRepository.findByTenantIdAndOrderId(tenantId, orderId); + } else if (status != null) { + return logisticsRepository.findByTenantIdAndStatus(tenantId, status); + } else { + return logisticsRepository.findByTenantId(tenantId); + } + } + + public Logistics getLogisticsById(String tenantId, Long id) { + Logistics logistics = logisticsRepository.findById(id).orElse(null); + if (logistics != null && logistics.getTenantId().equals(tenantId)) { + return logistics; + } + return null; + } + + public Logistics getLogisticsByTrackingNumber(String tenantId, String trackingNumber) { + return logisticsRepository.findByTenantIdAndTrackingNumber(tenantId, trackingNumber); + } + + public void updateLogistics(String tenantId, Long id, Map updateData) { + Logistics logistics = getLogisticsById(tenantId, id); + if (logistics != null) { + if (updateData.containsKey("status")) { + logistics.setStatus((String) updateData.get("status")); + if ("DELIVERED".equals(updateData.get("status"))) { + logistics.setActualDeliveryDate(new Date()); + } + } + if (updateData.containsKey("trackingNumber")) { + logistics.setTrackingNumber((String) updateData.get("trackingNumber")); + } + if (updateData.containsKey("carrier")) { + logistics.setCarrier((String) updateData.get("carrier")); + } + if (updateData.containsKey("estimatedDeliveryDate")) { + logistics.setEstimatedDeliveryDate((Date) updateData.get("estimatedDeliveryDate")); + } + logistics.setUpdatedAt(new Date()); + logisticsRepository.save(logistics); + } + } + + public Map trackLogistics(String tenantId, String trackingNumber) { + Logistics logistics = getLogisticsByTrackingNumber(tenantId, trackingNumber); + if (logistics != null) { + // 模拟物流跟踪信息 + return Map.of( + "success", true, + "trackingNumber", trackingNumber, + "carrier", logistics.getCarrier(), + "status", logistics.getStatus(), + "estimatedDeliveryDate", logistics.getEstimatedDeliveryDate(), + "actualDeliveryDate", logistics.getActualDeliveryDate(), + "updates", List.of( + Map.of("time", logistics.getCreatedAt(), "status", "ORDER_CREATED", "location", "Warehouse"), + Map.of("time", new Date(), "status", logistics.getStatus(), "location", "In Transit") + ) + ); + } + return Map.of("success", false, "error", "Tracking number not found"); + } + + public Map calculateShippingCost(String tenantId, Map shippingInfo) { + // 模拟运费计算 + Double weight = (Double) shippingInfo.get("weight"); + String destination = (String) shippingInfo.get("destination"); + String method = (String) shippingInfo.get("method"); + + double baseCost = 10.0; + double weightCost = weight * 2.0; + double destinationCost = destination.equals("International") ? 50.0 : 0.0; + double methodCost = method.equals("Express") ? 20.0 : 0.0; + + double totalCost = baseCost + weightCost + destinationCost + methodCost; + + return Map.of( + "success", true, + "baseCost", baseCost, + "weightCost", weightCost, + "destinationCost", destinationCost, + "methodCost", methodCost, + "totalCost", totalCost, + "currency", "USD" + ); + } +} diff --git a/src/main/java/com/crawlful/hub/service/MonitoringService.java b/src/main/java/com/crawlful/hub/service/MonitoringService.java new file mode 100644 index 0000000..90ca1dc --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/MonitoringService.java @@ -0,0 +1,142 @@ +package com.crawlful.hub.service; + +import org.springframework.stereotype.Service; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MonitoringService { + + public Map getSystemHealth() { + // 获取系统健康状态 + Map health = new HashMap<>(); + + // 检查服务状态 + health.put("status", "UP"); + health.put("timestamp", System.currentTimeMillis()); + + // 获取系统信息 + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + health.put("os", Map.of( + "name", osBean.getName(), + "version", osBean.getVersion(), + "arch", osBean.getArch(), + "availableProcessors", osBean.getAvailableProcessors() + )); + + // 获取内存信息 + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + long heapUsed = memoryBean.getHeapMemoryUsage().getUsed(); + long heapMax = memoryBean.getHeapMemoryUsage().getMax(); + health.put("memory", Map.of( + "heapUsed", heapUsed, + "heapMax", heapMax, + "heapUsedPercent", (double) heapUsed / heapMax * 100 + )); + + return health; + } + + public Map getPerformanceMetrics() { + // 获取性能指标 + Map metrics = new HashMap<>(); + + // 模拟请求处理时间 + metrics.put("requestProcessingTime", 123); // 毫秒 + + // 模拟数据库查询时间 + metrics.put("databaseQueryTime", 45); // 毫秒 + + // 模拟缓存命中率 + metrics.put("cacheHitRate", 0.85); // 85% + + // 模拟系统负载 + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + double systemLoad = osBean.getSystemLoadAverage(); + metrics.put("systemLoad", systemLoad); + + return metrics; + } + + public Map getServiceStatus() { + // 获取服务状态 + Map status = new HashMap<>(); + + // 模拟各个服务的状态 + status.put("services", Map.of( + "authService", Map.of("status", "UP", "responseTime", 10), + "productService", Map.of("status", "UP", "responseTime", 15), + "orderService", Map.of("status", "UP", "responseTime", 20), + "paymentService", Map.of("status", "UP", "responseTime", 25), + "logisticsService", Map.of("status", "UP", "responseTime", 30), + "dataService", Map.of("status", "UP", "responseTime", 35), + "reportService", Map.of("status", "UP", "responseTime", 40), + "configService", Map.of("status", "UP", "responseTime", 12), + "auditService", Map.of("status", "UP", "responseTime", 18) + )); + + // 计算整体服务状态 + long upServices = status.get("services").toString().contains("UP") ? 9 : 0; + status.put("totalServices", 9); + status.put("upServices", upServices); + status.put("downServices", 9 - upServices); + + return status; + } + + public Map getDatabaseStatus() { + // 获取数据库状态 + Map dbStatus = new HashMap<>(); + + // 模拟数据库连接状态 + dbStatus.put("connectionStatus", "UP"); + dbStatus.put("connectionCount", 10); + dbStatus.put("maxConnections", 100); + + // 模拟数据库查询性能 + dbStatus.put("queryPerformance", Map.of( + "avgQueryTime", 15, // 毫秒 + "slowQueries", 2, + "totalQueries", 1000 + )); + + return dbStatus; + } + + public Map getCacheStatus() { + // 获取缓存状态 + Map cacheStatus = new HashMap<>(); + + // 模拟缓存使用情况 + cacheStatus.put("cacheSize", 500); + cacheStatus.put("maxCacheSize", 1000); + cacheStatus.put("hitRate", 0.85); + cacheStatus.put("missRate", 0.15); + + return cacheStatus; + } + + public Map getSystemStats() { + // 获取系统统计信息 + Map stats = new HashMap<>(); + + // 模拟系统统计数据 + stats.put("totalRequests", 10000); + stats.put("successfulRequests", 9800); + stats.put("failedRequests", 200); + stats.put("successRate", 0.98); + + // 模拟响应时间统计 + stats.put("responseTime", Map.of( + "avg", 123, + "min", 10, + "max", 500 + )); + + return stats; + } +} diff --git a/src/main/java/com/crawlful/hub/service/OrderRepository.java b/src/main/java/com/crawlful/hub/service/OrderRepository.java new file mode 100644 index 0000000..24fd2c7 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/OrderRepository.java @@ -0,0 +1,21 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Order; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Date; + +public interface OrderRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndStatus(String tenantId, String status); + List findByTenantIdAndPlatform(String tenantId, String platform); + List findByTenantIdAndPlatformAndStatus(String tenantId, String platform, String status); + + @Query("SELECT p FROM Order p WHERE p.tenantId = :tenantId AND p.createdAt BETWEEN :startDate AND :endDate") + List findByTenantIdAndDateRange(@Param("tenantId") String tenantId, @Param("startDate") Date startDate, @Param("endDate") Date endDate); + + Order findByTenantIdAndPlatformOrderId(String tenantId, String platformOrderId); +} diff --git a/src/main/java/com/crawlful/hub/service/OrderService.java b/src/main/java/com/crawlful/hub/service/OrderService.java new file mode 100644 index 0000000..92e4131 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/OrderService.java @@ -0,0 +1,252 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Order; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +@Service +public class OrderService { + @Autowired + private OrderRepository orderRepository; + + public Order createOrder(Map orderData) { + Order order = new Order(); + order.setTenantId((String) orderData.get("tenantId")); + order.setShopId((String) orderData.get("shopId")); + order.setPlatform((String) orderData.get("platform")); + order.setPlatformOrderId((String) orderData.get("platformOrderId")); + order.setStatus((String) orderData.get("status")); + order.setTotalAmount((Double) orderData.get("totalAmount")); + order.setCurrency((String) orderData.get("currency")); + order.setCustomerInfo((String) orderData.get("customerInfo")); + order.setItems((String) orderData.get("items")); + order.setShippingAddress((String) orderData.get("shippingAddress")); + order.setTrackingNumber((String) orderData.get("trackingNumber")); + order.setCreatedAt(new Date()); + order.setUpdatedAt(new Date()); + return orderRepository.save(order); + } + + public List getOrders(String tenantId, Map params) { + Integer page = (Integer) params.get("page"); + Integer pageSize = (Integer) params.get("pageSize"); + String status = (String) params.get("status"); + String platform = (String) params.get("platform"); + Date startDate = (Date) params.get("startDate"); + Date endDate = (Date) params.get("endDate"); + + if (startDate != null && endDate != null) { + return orderRepository.findByTenantIdAndDateRange(tenantId, startDate, endDate); + } else if (platform != null && status != null) { + return orderRepository.findByTenantIdAndPlatformAndStatus(tenantId, platform, status); + } else if (platform != null) { + return orderRepository.findByTenantIdAndPlatform(tenantId, platform); + } else if (status != null) { + return orderRepository.findByTenantIdAndStatus(tenantId, status); + } else { + return orderRepository.findByTenantId(tenantId); + } + } + + public Order getOrderById(String tenantId, String id) { + try { + Long orderId = Long.parseLong(id); + Order order = orderRepository.findById(orderId).orElse(null); + if (order != null && order.getTenantId().equals(tenantId)) { + return order; + } + return null; + } catch (NumberFormatException e) { + return null; + } + } + + public void updateOrder(String id, String tenantId, Map updateData) { + Order order = getOrderById(tenantId, id); + if (order != null) { + if (updateData.containsKey("status")) { + order.setStatus((String) updateData.get("status")); + } + if (updateData.containsKey("totalAmount")) { + order.setTotalAmount((Double) updateData.get("totalAmount")); + } + if (updateData.containsKey("currency")) { + order.setCurrency((String) updateData.get("currency")); + } + if (updateData.containsKey("customerInfo")) { + order.setCustomerInfo((String) updateData.get("customerInfo")); + } + if (updateData.containsKey("items")) { + order.setItems((String) updateData.get("items")); + } + if (updateData.containsKey("shippingAddress")) { + order.setShippingAddress((String) updateData.get("shippingAddress")); + } + if (updateData.containsKey("trackingNumber")) { + order.setTrackingNumber((String) updateData.get("trackingNumber")); + } + order.setUpdatedAt(new Date()); + orderRepository.save(order); + } + } + + public void deleteOrder(String id, String tenantId) { + Order order = getOrderById(tenantId, id); + if (order != null) { + orderRepository.delete(order); + } + } + + public void transitionOrderStatus(String tenantId, String id, String status, String reason) { + Order order = getOrderById(tenantId, id); + if (order != null) { + order.setStatus(status); + order.setUpdatedAt(new Date()); + orderRepository.save(order); + } + } + + public Map batchUpdateOrders(String tenantId, List orderIds, Map updates) { + int successCount = 0; + int failureCount = 0; + + for (String orderId : orderIds) { + try { + updateOrder(orderId, tenantId, updates); + successCount++; + } catch (Exception e) { + failureCount++; + } + } + + Map result = new HashMap<>(); + result.put("successCount", successCount); + result.put("failureCount", failureCount); + result.put("totalCount", orderIds.size()); + return result; + } + + public Map batchAuditOrders(String tenantId, List orderIds) { + Map updates = new HashMap<>(); + updates.put("status", "AUDITED"); + return batchUpdateOrders(tenantId, orderIds, updates); + } + + public Map batchShipOrders(String tenantId, List orderIds) { + Map updates = new HashMap<>(); + updates.put("status", "SHIPPED"); + return batchUpdateOrders(tenantId, orderIds, updates); + } + + public void markOrderAsException(String tenantId, String id, String reason) { + Map updates = new HashMap<>(); + updates.put("status", "EXCEPTION"); + updateOrder(id, tenantId, updates); + } + + public void autoRerouteOrder(String tenantId, String id) { + Order order = getOrderById(tenantId, id); + if (order != null) { + // 实现自动改派逻辑 + // 这里只是一个简单的示例 + order.setStatus("REROUTED"); + order.setUpdatedAt(new Date()); + orderRepository.save(order); + } + } + + public void retryExceptionOrder(String tenantId, String id) { + Map updates = new HashMap<>(); + updates.put("status", "PENDING"); + updateOrder(id, tenantId, updates); + } + + public void cancelOrder(String tenantId, String id, String reason) { + Map updates = new HashMap<>(); + updates.put("status", "CANCELLED"); + updateOrder(id, tenantId, updates); + } + + public String requestRefund(String tenantId, String id, String reason, Double amount) { + // 实现退款申请逻辑 + // 这里只是一个简单的示例 + return "REFUND_" + System.currentTimeMillis(); + } + + public void approveRefund(String tenantId, String id, boolean approved, String note) { + // 实现退款审批逻辑 + // 这里只是一个简单的示例 + } + + public String requestAfterSales(String tenantId, String id, String type, String reason, List> items) { + // 实现售后申请逻辑 + // 这里只是一个简单的示例 + return "AFTER_SALES_" + System.currentTimeMillis(); + } + + public void processAfterSales(String tenantId, String id, String action, String note) { + // 实现售后处理逻辑 + // 这里只是一个简单的示例 + } + + public void completeOrder(String tenantId, String id) { + Map updates = new HashMap<>(); + updates.put("status", "COMPLETED"); + updateOrder(id, tenantId, updates); + } + + public Map getOrderStats(String tenantId) { + // 实现订单统计逻辑 + // 这里只是一个简单的示例 + List orders = orderRepository.findByTenantId(tenantId); + int totalOrders = orders.size(); + int pendingOrders = 0; + int shippedOrders = 0; + int completedOrders = 0; + + for (Order order : orders) { + switch (order.getStatus()) { + case "PENDING": + pendingOrders++; + break; + case "SHIPPED": + shippedOrders++; + break; + case "COMPLETED": + completedOrders++; + break; + } + } + + Map stats = new HashMap<>(); + stats.put("totalOrders", totalOrders); + stats.put("pendingOrders", pendingOrders); + stats.put("shippedOrders", shippedOrders); + stats.put("completedOrders", completedOrders); + return stats; + } + + public Order mapPlatformPayloadToOrder(String platform, Map payload, String tenantId, String shopId) { + // 实现平台订单映射逻辑 + // 这里只是一个简单的示例 + Order order = new Order(); + order.setTenantId(tenantId); + order.setShopId(shopId); + order.setPlatform(platform); + order.setPlatformOrderId((String) payload.get("orderId")); + order.setStatus("PENDING"); + order.setTotalAmount((Double) payload.get("totalAmount")); + order.setCurrency((String) payload.get("currency")); + order.setCustomerInfo((String) payload.get("customerInfo")); + order.setItems((String) payload.get("items")); + order.setShippingAddress((String) payload.get("shippingAddress")); + order.setCreatedAt(new Date()); + order.setUpdatedAt(new Date()); + return order; + } +} diff --git a/src/main/java/com/crawlful/hub/service/PaymentRepository.java b/src/main/java/com/crawlful/hub/service/PaymentRepository.java new file mode 100644 index 0000000..b015b77 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/PaymentRepository.java @@ -0,0 +1,13 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Payment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PaymentRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndOrderId(String tenantId, Long orderId); + List findByTenantIdAndStatus(String tenantId, String status); + Payment findByTenantIdAndTransactionId(String tenantId, String transactionId); +} diff --git a/src/main/java/com/crawlful/hub/service/PaymentService.java b/src/main/java/com/crawlful/hub/service/PaymentService.java new file mode 100644 index 0000000..2901884 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/PaymentService.java @@ -0,0 +1,97 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Payment; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class PaymentService { + @Autowired + private PaymentRepository paymentRepository; + + public Payment createPayment(Map paymentData) { + Payment payment = new Payment(); + payment.setTenantId((String) paymentData.get("tenantId")); + payment.setOrderId((Long) paymentData.get("orderId")); + payment.setPaymentMethod((String) paymentData.get("paymentMethod")); + payment.setAmount((Double) paymentData.get("amount")); + payment.setCurrency((String) paymentData.get("currency")); + payment.setStatus((String) paymentData.get("status")); + payment.setTransactionId((String) paymentData.get("transactionId")); + payment.setCreatedAt(new Date()); + payment.setUpdatedAt(new Date()); + return paymentRepository.save(payment); + } + + public List getPayments(String tenantId, Map filters) { + String status = (String) filters.get("status"); + Long orderId = (Long) filters.get("orderId"); + + if (orderId != null) { + return paymentRepository.findByTenantIdAndOrderId(tenantId, orderId); + } else if (status != null) { + return paymentRepository.findByTenantIdAndStatus(tenantId, status); + } else { + return paymentRepository.findByTenantId(tenantId); + } + } + + public Payment getPaymentById(String tenantId, Long id) { + Payment payment = paymentRepository.findById(id).orElse(null); + if (payment != null && payment.getTenantId().equals(tenantId)) { + return payment; + } + return null; + } + + public Payment getPaymentByTransactionId(String tenantId, String transactionId) { + return paymentRepository.findByTenantIdAndTransactionId(tenantId, transactionId); + } + + public void updatePayment(String tenantId, Long id, Map updateData) { + Payment payment = getPaymentById(tenantId, id); + if (payment != null) { + if (updateData.containsKey("status")) { + payment.setStatus((String) updateData.get("status")); + } + if (updateData.containsKey("transactionId")) { + payment.setTransactionId((String) updateData.get("transactionId")); + } + payment.setUpdatedAt(new Date()); + paymentRepository.save(payment); + } + } + + public void processPaymentCallback(String tenantId, String transactionId, Map callbackData) { + Payment payment = getPaymentByTransactionId(tenantId, transactionId); + if (payment != null) { + String status = (String) callbackData.get("status"); + payment.setStatus(status); + payment.setUpdatedAt(new Date()); + paymentRepository.save(payment); + } + } + + public Map refundPayment(String tenantId, Long id, Double amount, String reason) { + Payment payment = getPaymentById(tenantId, id); + if (payment != null) { + // 实现退款逻辑 + // 这里只是一个简单的示例 + payment.setStatus("REFUNDED"); + payment.setUpdatedAt(new Date()); + paymentRepository.save(payment); + + return Map.of( + "success", true, + "refundId", "REFUND_" + System.currentTimeMillis(), + "amount", amount, + "reason", reason + ); + } + return Map.of("success", false, "error", "Payment not found"); + } +} diff --git a/src/main/java/com/crawlful/hub/service/ProductRepository.java b/src/main/java/com/crawlful/hub/service/ProductRepository.java new file mode 100644 index 0000000..3837ff5 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/ProductRepository.java @@ -0,0 +1,30 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ProductRepository extends JpaRepository { + List findByTenantId(String tenantId); + List findByTenantIdAndPlatform(String tenantId, String platform); + List findByTenantIdAndStatus(String tenantId, String status); + List findByTenantIdAndPlatformAndStatus(String tenantId, String platform, String status); + + @Query("SELECT p FROM Product p WHERE p.tenantId = :tenantId ORDER BY p.id DESC LIMIT :size OFFSET :offset") + List findByTenantIdWithPagination(@Param("tenantId") String tenantId, @Param("offset") int offset, @Param("size") int size); + + @Query("SELECT p FROM Product p WHERE p.tenantId = :tenantId AND p.platform = :platform ORDER BY p.id DESC LIMIT :size OFFSET :offset") + List findByTenantIdAndPlatformWithPagination(@Param("tenantId") String tenantId, @Param("platform") String platform, @Param("offset") int offset, @Param("size") int size); + + @Query("SELECT p FROM Product p WHERE p.tenantId = :tenantId AND p.status = :status ORDER BY p.id DESC LIMIT :size OFFSET :offset") + List findByTenantIdAndStatusWithPagination(@Param("tenantId") String tenantId, @Param("status") String status, @Param("offset") int offset, @Param("size") int size); + + @Query("SELECT p FROM Product p WHERE p.tenantId = :tenantId AND p.platform = :platform AND p.status = :status ORDER BY p.id DESC LIMIT :size OFFSET :offset") + List findByTenantIdAndPlatformAndStatusWithPagination(@Param("tenantId") String tenantId, @Param("platform") String platform, @Param("status") String status, @Param("offset") int offset, @Param("size") int size); + + @Query("SELECT p FROM Product p WHERE p.tenantId = :tenantId AND p.phash = :phash") + List findByFingerprint(@Param("tenantId") String tenantId, @Param("phash") String phash); +} diff --git a/src/main/java/com/crawlful/hub/service/ProductService.java b/src/main/java/com/crawlful/hub/service/ProductService.java new file mode 100644 index 0000000..1b515c6 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/ProductService.java @@ -0,0 +1,164 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.Product; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class ProductService { + @Autowired + private ProductRepository productRepository; + + public Product create(String tenantId, Map productData) { + Product product = new Product(); + product.setTenantId(tenantId); + product.setTitle((String) productData.get("title")); + product.setDescription((String) productData.get("description")); + product.setMainImage((String) productData.get("mainImage")); + product.setPlatform((String) productData.get("platform")); + product.setPlatformProductId((String) productData.get("platformProductId")); + product.setPrice((Double) productData.get("price")); + product.setCostPrice((Double) productData.get("costPrice")); + product.setQuantity((Integer) productData.get("quantity")); + product.setStatus((String) productData.get("status")); + product.setPhash((String) productData.get("phash")); + product.setSemanticHash((String) productData.get("semanticHash")); + product.setVectorEmbedding((String) productData.get("vectorEmbedding")); + product.setAttributes((String) productData.get("attributes")); + product.setCreatedAt(new Date()); + product.setUpdatedAt(new Date()); + return productRepository.save(product); + } + + public List getAll(String tenantId, Map filters, int page, int size) { + String platform = filters.get("platform"); + String status = filters.get("status"); + int offset = (page - 1) * size; + + if (platform != null && status != null) { + return productRepository.findByTenantIdAndPlatformAndStatusWithPagination(tenantId, platform, status, offset, size); + } else if (platform != null) { + return productRepository.findByTenantIdAndPlatformWithPagination(tenantId, platform, offset, size); + } else if (status != null) { + return productRepository.findByTenantIdAndStatusWithPagination(tenantId, status, offset, size); + } else { + return productRepository.findByTenantIdWithPagination(tenantId, offset, size); + } + } + + public Product getById(String tenantId, Long id) { + Product product = productRepository.findById(id).orElse(null); + if (product != null && product.getTenantId().equals(tenantId)) { + return product; + } + return null; + } + + public Product update(String tenantId, Long id, Map updateData) { + Product product = getById(tenantId, id); + if (product == null) { + return null; + } + + if (updateData.containsKey("title")) { + product.setTitle((String) updateData.get("title")); + } + if (updateData.containsKey("description")) { + product.setDescription((String) updateData.get("description")); + } + if (updateData.containsKey("mainImage")) { + product.setMainImage((String) updateData.get("mainImage")); + } + if (updateData.containsKey("platform")) { + product.setPlatform((String) updateData.get("platform")); + } + if (updateData.containsKey("platformProductId")) { + product.setPlatformProductId((String) updateData.get("platformProductId")); + } + if (updateData.containsKey("price")) { + product.setPrice((Double) updateData.get("price")); + } + if (updateData.containsKey("costPrice")) { + product.setCostPrice((Double) updateData.get("costPrice")); + } + if (updateData.containsKey("quantity")) { + product.setQuantity((Integer) updateData.get("quantity")); + } + if (updateData.containsKey("status")) { + product.setStatus((String) updateData.get("status")); + } + if (updateData.containsKey("phash")) { + product.setPhash((String) updateData.get("phash")); + } + if (updateData.containsKey("semanticHash")) { + product.setSemanticHash((String) updateData.get("semanticHash")); + } + if (updateData.containsKey("vectorEmbedding")) { + product.setVectorEmbedding((String) updateData.get("vectorEmbedding")); + } + if (updateData.containsKey("attributes")) { + product.setAttributes((String) updateData.get("attributes")); + } + + product.setUpdatedAt(new Date()); + return productRepository.save(product); + } + + public void delete(String tenantId, Long id) { + Product product = getById(tenantId, id); + if (product != null) { + productRepository.delete(product); + } + } + + public List findByFingerprint(String tenantId, Map fingerprint) { + // 简单实现,实际应该使用更复杂的指纹匹配算法 + String phash = fingerprint.get("phash"); + return productRepository.findByFingerprint(tenantId, phash); + } + + public Map washAndLocalize(String tenantId, Long id, String targetMarket, String targetLang) { + // 实现商品清洗与本地化逻辑 + // 这里只是一个简单的示例 + Product product = getById(tenantId, id); + if (product == null) { + return null; + } + + // 模拟清洗与本地化过程 + Map result = Map.of( + "id", product.getId(), + "title", product.getTitle() + " (Localized for " + targetMarket + ")", + "description", product.getDescription() + " (Localized for " + targetMarket + ")", + "targetMarket", targetMarket, + "targetLang", targetLang, + "status", "LOCALIZED" + ); + + return result; + } + + public Map analyzeProductArbitrage(String tenantId, Long id) { + // 实现套利机会分析逻辑 + // 这里只是一个简单的示例 + Product product = getById(tenantId, id); + if (product == null) { + return null; + } + + double profitMargin = (product.getPrice() - product.getCostPrice()) / product.getPrice() * 100; + Map result = Map.of( + "productId", product.getId(), + "price", product.getPrice(), + "costPrice", product.getCostPrice(), + "profitMargin", profitMargin, + "arbitrageOpportunity", profitMargin > 20 + ); + + return result; + } +} diff --git a/src/main/java/com/crawlful/hub/service/ReportService.java b/src/main/java/com/crawlful/hub/service/ReportService.java new file mode 100644 index 0000000..a622e02 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/ReportService.java @@ -0,0 +1,246 @@ +package com.crawlful.hub.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class ReportService { + @Autowired + private UserRepository userRepository; + @Autowired + private ProductRepository productRepository; + @Autowired + private OrderRepository orderRepository; + @Autowired + private PaymentRepository paymentRepository; + @Autowired + private LogisticsRepository logisticsRepository; + + public Map generateSalesReport(String tenantId, Date startDate, Date endDate) { + // 生成销售报表 + List> salesData = new ArrayList<>(); + double totalSales = 0.0; + int totalOrders = 0; + + for (var order : orderRepository.findByTenantId(tenantId)) { + if (order.getCreatedAt().after(startDate) && order.getCreatedAt().before(endDate)) { + double orderAmount = order.getTotalAmount() != null ? order.getTotalAmount() : 0; + totalSales += orderAmount; + totalOrders++; + + salesData.add(Map.of( + "orderId", order.getId(), + "platform", order.getPlatform(), + "amount", orderAmount, + "currency", order.getCurrency(), + "status", order.getStatus(), + "createdAt", order.getCreatedAt() + )); + } + } + + double averageOrderValue = totalOrders > 0 ? totalSales / totalOrders : 0; + + // 按平台统计 + Map> platformStats = new HashMap<>(); + for (var order : orderRepository.findByTenantId(tenantId)) { + if (order.getCreatedAt().after(startDate) && order.getCreatedAt().before(endDate)) { + String platform = order.getPlatform(); + if (!platformStats.containsKey(platform)) { + platformStats.put(platform, Map.of( + "orders", 0, + "sales", 0.0 + )); + } + + Map currentStats = platformStats.get(platform); + int orders = (Integer) currentStats.get("orders") + 1; + double sales = (Double) currentStats.get("sales") + (order.getTotalAmount() != null ? order.getTotalAmount() : 0); + + platformStats.put(platform, Map.of( + "orders", orders, + "sales", sales + )); + } + } + + return Map.of( + "totalSales", totalSales, + "totalOrders", totalOrders, + "averageOrderValue", averageOrderValue, + "salesData", salesData, + "platformStats", platformStats, + "startDate", startDate, + "endDate", endDate + ); + } + + public Map generateInventoryReport(String tenantId) { + // 生成库存报表 + List> inventoryData = new ArrayList<>(); + int totalProducts = 0; + int lowStockProducts = 0; + int outOfStockProducts = 0; + + for (var product : productRepository.findByTenantId(tenantId)) { + totalProducts++; + int quantity = product.getQuantity() != null ? product.getQuantity() : 0; + if (quantity < 10) { + lowStockProducts++; + } + if (quantity == 0) { + outOfStockProducts++; + } + + inventoryData.add(Map.of( + "productId", product.getId(), + "title", product.getTitle(), + "platform", product.getPlatform(), + "quantity", quantity, + "price", product.getPrice(), + "status", product.getStatus() + )); + } + + return Map.of( + "totalProducts", totalProducts, + "lowStockProducts", lowStockProducts, + "outOfStockProducts", outOfStockProducts, + "inventoryData", inventoryData + ); + } + + public Map generateUserReport(String tenantId) { + // 生成用户报表 + List> userData = new ArrayList<>(); + int totalUsers = 0; + Map roleStats = new HashMap<>(); + + for (var user : userRepository.findByTenantId(tenantId)) { + totalUsers++; + String role = user.getRole(); + roleStats.put(role, roleStats.getOrDefault(role, 0) + 1); + + userData.add(Map.of( + "userId", user.getId(), + "username", user.getUsername(), + "email", user.getEmail(), + "role", role, + "createdAt", user.getCreatedAt() + )); + } + + return Map.of( + "totalUsers", totalUsers, + "roleStats", roleStats, + "userData", userData + ); + } + + public Map generatePaymentReport(String tenantId, Date startDate, Date endDate) { + // 生成支付报表 + List> paymentData = new ArrayList<>(); + double totalPayments = 0.0; + int totalTransactions = 0; + Map paymentMethodStats = new HashMap<>(); + + for (var payment : paymentRepository.findByTenantId(tenantId)) { + if (payment.getCreatedAt().after(startDate) && payment.getCreatedAt().before(endDate)) { + double amount = payment.getAmount() != null ? payment.getAmount() : 0; + totalPayments += amount; + totalTransactions++; + + String paymentMethod = payment.getPaymentMethod(); + paymentMethodStats.put(paymentMethod, paymentMethodStats.getOrDefault(paymentMethod, 0) + 1); + + paymentData.add(Map.of( + "paymentId", payment.getId(), + "orderId", payment.getOrderId(), + "paymentMethod", paymentMethod, + "amount", amount, + "currency", payment.getCurrency(), + "status", payment.getStatus(), + "transactionId", payment.getTransactionId(), + "createdAt", payment.getCreatedAt() + )); + } + } + + return Map.of( + "totalPayments", totalPayments, + "totalTransactions", totalTransactions, + "paymentMethodStats", paymentMethodStats, + "paymentData", paymentData, + "startDate", startDate, + "endDate", endDate + ); + } + + public Map generateLogisticsReport(String tenantId, Date startDate, Date endDate) { + // 生成物流报表 + List> logisticsData = new ArrayList<>(); + int totalShipments = 0; + int deliveredShipments = 0; + Map carrierStats = new HashMap<>(); + + for (var logistics : logisticsRepository.findByTenantId(tenantId)) { + if (logistics.getCreatedAt().after(startDate) && logistics.getCreatedAt().before(endDate)) { + totalShipments++; + if ("DELIVERED".equals(logistics.getStatus())) { + deliveredShipments++; + } + + String carrier = logistics.getCarrier(); + carrierStats.put(carrier, carrierStats.getOrDefault(carrier, 0) + 1); + + logisticsData.add(Map.of( + "logisticsId", logistics.getId(), + "orderId", logistics.getOrderId(), + "shippingMethod", logistics.getShippingMethod(), + "trackingNumber", logistics.getTrackingNumber(), + "carrier", carrier, + "status", logistics.getStatus(), + "estimatedDeliveryDate", logistics.getEstimatedDeliveryDate(), + "actualDeliveryDate", logistics.getActualDeliveryDate(), + "createdAt", logistics.getCreatedAt() + )); + } + } + + double deliveryRate = totalShipments > 0 ? (double) deliveredShipments / totalShipments * 100 : 0; + + return Map.of( + "totalShipments", totalShipments, + "deliveredShipments", deliveredShipments, + "deliveryRate", deliveryRate, + "carrierStats", carrierStats, + "logisticsData", logisticsData, + "startDate", startDate, + "endDate", endDate + ); + } + + public Map generateCustomReport(String tenantId, Map reportParams) { + // 生成自定义报表 + String reportType = (String) reportParams.get("reportType"); + Date startDate = (Date) reportParams.get("startDate"); + Date endDate = (Date) reportParams.get("endDate"); + + switch (reportType) { + case "sales": + return generateSalesReport(tenantId, startDate, endDate); + case "inventory": + return generateInventoryReport(tenantId); + case "user": + return generateUserReport(tenantId); + case "payment": + return generatePaymentReport(tenantId, startDate, endDate); + case "logistics": + return generateLogisticsReport(tenantId, startDate, endDate); + default: + return Map.of("error", "Invalid report type"); + } + } +} diff --git a/src/main/java/com/crawlful/hub/service/UserRepository.java b/src/main/java/com/crawlful/hub/service/UserRepository.java new file mode 100644 index 0000000..22f496f --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/UserRepository.java @@ -0,0 +1,13 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UserRepository extends JpaRepository { + User findByUsername(String username); + User findByEmail(String email); + List findByTenantId(String tenantId); + User findByTenantIdAndUsername(String tenantId, String username); +} diff --git a/src/main/java/com/crawlful/hub/service/UserService.java b/src/main/java/com/crawlful/hub/service/UserService.java new file mode 100644 index 0000000..47e1716 --- /dev/null +++ b/src/main/java/com/crawlful/hub/service/UserService.java @@ -0,0 +1,73 @@ +package com.crawlful.hub.service; + +import com.crawlful.hub.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + public User create(String tenantId, Map userData) { + User user = new User(); + user.setTenantId(tenantId); + user.setUsername((String) userData.get("username")); + user.setPassword((String) userData.get("password")); + user.setEmail((String) userData.get("email")); + user.setRole((String) userData.get("role")); + user.setCreatedAt(new Date()); + user.setUpdatedAt(new Date()); + return userRepository.save(user); + } + + public List getAll(String tenantId) { + return userRepository.findByTenantId(tenantId); + } + + public User getById(String tenantId, Long id) { + User user = userRepository.findById(id).orElse(null); + if (user != null && user.getTenantId().equals(tenantId)) { + return user; + } + return null; + } + + public User getByUsername(String tenantId, String username) { + return userRepository.findByTenantIdAndUsername(tenantId, username); + } + + public User update(String tenantId, Long id, Map updateData) { + User user = getById(tenantId, id); + if (user == null) { + return null; + } + + if (updateData.containsKey("username")) { + user.setUsername((String) updateData.get("username")); + } + if (updateData.containsKey("password")) { + user.setPassword((String) updateData.get("password")); + } + if (updateData.containsKey("email")) { + user.setEmail((String) updateData.get("email")); + } + if (updateData.containsKey("role")) { + user.setRole((String) updateData.get("role")); + } + + user.setUpdatedAt(new Date()); + return userRepository.save(user); + } + + public void delete(String tenantId, Long id) { + User user = getById(tenantId, id); + if (user != null) { + userRepository.delete(user); + } + } +} diff --git a/src/main/java/com/crawlful/hub/util/ValidationUtil.java b/src/main/java/com/crawlful/hub/util/ValidationUtil.java new file mode 100644 index 0000000..2ddeb13 --- /dev/null +++ b/src/main/java/com/crawlful/hub/util/ValidationUtil.java @@ -0,0 +1,80 @@ +package com.crawlful.hub.util; + +import java.util.Map; +import java.util.regex.Pattern; + +public class ValidationUtil { + + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + private static final Pattern PASSWORD_PATTERN = Pattern.compile("^.{6,}$"); + private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{10,11}$"); + + public static boolean validateEmail(String email) { + return email != null && EMAIL_PATTERN.matcher(email).matches(); + } + + public static boolean validatePassword(String password) { + return password != null && PASSWORD_PATTERN.matcher(password).matches(); + } + + public static boolean validatePhone(String phone) { + return phone != null && PHONE_PATTERN.matcher(phone).matches(); + } + + public static boolean validateRequiredFields(Map data, String... fields) { + for (String field : fields) { + if (!data.containsKey(field) || data.get(field) == null) { + return false; + } + } + return true; + } + + public static boolean validateNumber(Object value) { + if (value == null) { + return false; + } + try { + Double.parseDouble(value.toString()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public static boolean validatePositiveNumber(Object value) { + if (!validateNumber(value)) { + return false; + } + double num = Double.parseDouble(value.toString()); + return num > 0; + } + + public static boolean validateInteger(Object value) { + if (value == null) { + return false; + } + try { + Integer.parseInt(value.toString()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public static boolean validatePositiveInteger(Object value) { + if (!validateInteger(value)) { + return false; + } + int num = Integer.parseInt(value.toString()); + return num > 0; + } + + public static boolean validateStringLength(String value, int min, int max) { + if (value == null) { + return false; + } + int length = value.length(); + return length >= min && length <= max; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..1b04b46 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,36 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/crawlful_hub?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 20000 + max-lifetime: 1800000 + jpa: + hibernate: + ddl-auto: update + show-sql: true + redis: + host: localhost + port: 6379 + password: + database: 0 + security: + jwt: + secret: your-secret-key + expiration: 86400000 + springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html + enabled: true + +server: + port: 3001 + servlet: + context-path: /api diff --git a/src/main/resources/db/migration/V1__init_schema.sql b/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..1210603 --- /dev/null +++ b/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,130 @@ +-- 创建用户表 +CREATE TABLE IF NOT EXISTS cf_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + role VARCHAR(50), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建商品表 +CREATE TABLE IF NOT EXISTS cf_product ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + shop_id VARCHAR(255), + title VARCHAR(255) NOT NULL, + description TEXT, + main_image VARCHAR(255), + platform VARCHAR(50), + platform_product_id VARCHAR(255), + price DECIMAL(10,2), + cost_price DECIMAL(10,2), + quantity INT, + status VARCHAR(50), + phash VARCHAR(255), + semantic_hash VARCHAR(255), + vector_embedding TEXT, + attributes JSON, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建订单表 +CREATE TABLE IF NOT EXISTS cf_order ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + shop_id VARCHAR(255), + platform VARCHAR(50), + platform_order_id VARCHAR(255), + status VARCHAR(50), + total_amount DECIMAL(10,2), + currency VARCHAR(10), + customer_info JSON, + items JSON, + shipping_address JSON, + tracking_number VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建支付表 +CREATE TABLE IF NOT EXISTS cf_payment ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + order_id BIGINT, + payment_method VARCHAR(50), + amount DECIMAL(10,2), + currency VARCHAR(10), + status VARCHAR(50), + transaction_id VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (order_id) REFERENCES cf_order(id) +); + +-- 创建物流表 +CREATE TABLE IF NOT EXISTS cf_logistics ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + order_id BIGINT, + shipping_method VARCHAR(50), + tracking_number VARCHAR(255), + carrier VARCHAR(50), + status VARCHAR(50), + estimated_delivery_date DATETIME, + actual_delivery_date DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (order_id) REFERENCES cf_order(id) +); + +-- 创建配置表 +CREATE TABLE IF NOT EXISTS cf_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255), + shop_id VARCHAR(255), + config_key VARCHAR(255) NOT NULL, + config_value VARCHAR(255) NOT NULL, + config_type VARCHAR(50), + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建审计表 +CREATE TABLE IF NOT EXISTS cf_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255), + shop_id VARCHAR(255), + user_id BIGINT, + action VARCHAR(255), + resource_type VARCHAR(255), + resource_id VARCHAR(255), + ip_address VARCHAR(100), + user_agent TEXT, + details TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_user_tenant_id ON cf_user(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_tenant_id ON cf_product(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_platform ON cf_product(platform); +CREATE INDEX IF NOT EXISTS idx_order_tenant_id ON cf_order(tenant_id); +CREATE INDEX IF NOT EXISTS idx_order_platform ON cf_order(platform); +CREATE INDEX IF NOT EXISTS idx_payment_tenant_id ON cf_payment(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payment_order_id ON cf_payment(order_id); +CREATE INDEX IF NOT EXISTS idx_logistics_tenant_id ON cf_logistics(tenant_id); +CREATE INDEX IF NOT EXISTS idx_logistics_order_id ON cf_logistics(order_id); +CREATE INDEX IF NOT EXISTS idx_config_tenant_id ON cf_config(tenant_id); +CREATE INDEX IF NOT EXISTS idx_config_shop_id ON cf_config(shop_id); +CREATE INDEX IF NOT EXISTS idx_config_key ON cf_config(config_key); +CREATE INDEX IF NOT EXISTS idx_audit_tenant_id ON cf_audit(tenant_id); +CREATE INDEX IF NOT EXISTS idx_audit_shop_id ON cf_audit(shop_id); +CREATE INDEX IF NOT EXISTS idx_audit_user_id ON cf_audit(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_action ON cf_audit(action); +CREATE INDEX IF NOT EXISTS idx_audit_resource_type ON cf_audit(resource_type); +CREATE INDEX IF NOT EXISTS idx_audit_created_at ON cf_audit(created_at); diff --git a/src/main/resources/db/migration/V2__add_alert_table.sql b/src/main/resources/db/migration/V2__add_alert_table.sql new file mode 100644 index 0000000..63faf24 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_alert_table.sql @@ -0,0 +1,21 @@ +-- 创建告警表 +CREATE TABLE IF NOT EXISTS cf_alert ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + alert_type VARCHAR(50), + severity VARCHAR(50), + message TEXT, + status VARCHAR(50), + source VARCHAR(255), + threshold VARCHAR(255), + actual_value VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME +); + +-- 为告警表添加索引 +CREATE INDEX IF NOT EXISTS idx_alert_tenant_id ON cf_alert(tenant_id); +CREATE INDEX IF NOT EXISTS idx_alert_status ON cf_alert(status); +CREATE INDEX IF NOT EXISTS idx_alert_severity ON cf_alert(severity); +CREATE INDEX IF NOT EXISTS idx_alert_alert_type ON cf_alert(alert_type); +CREATE INDEX IF NOT EXISTS idx_alert_created_at ON cf_alert(created_at); diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..28e38f5 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,62 @@ +# Authentication messages +auth.register.success=User registered successfully +auth.login.success=Login successful +auth.login.failure=Invalid username or password +auth.username.required=Username is required +auth.password.required=Password is required +auth.email.required=Email is required +auth.email.invalid=Invalid email format +auth.password.minlength=Password must be at least 6 characters +auth.username.minlength=Username must be at least 3 characters +auth.username.exists=Username already exists + +# Product messages +product.create.success=Product created successfully +product.update.success=Product updated successfully +product.delete.success=Product deleted successfully +product.not.found=Product not found +product.title.required=Product title is required +product.price.required=Product price is required +product.price.positive=Product price must be positive + +# Order messages +order.create.success=Order created successfully +order.update.success=Order updated successfully +order.not.found=Order not found +order.status.updated=Order status updated successfully + +# Payment messages +payment.create.success=Payment created successfully +payment.update.success=Payment updated successfully +payment.not.found=Payment not found +payment.status.updated=Payment status updated successfully + +# Logistics messages +logistics.create.success=Logistics created successfully +logistics.update.success=Logistics updated successfully +logistics.not.found=Logistics not found +logistics.status.updated=Logistics status updated successfully + +# Alert messages +alert.create.success=Alert created successfully +alert.update.success=Alert updated successfully +alert.resolve.success=Alert resolved successfully +alert.not.found=Alert not found + +# Monitoring messages +monitoring.health.ok=System health is OK +monitoring.health.error=System health check failed +monitoring.metrics.success=Performance metrics retrieved successfully +monitoring.services.success=Service status retrieved successfully +monitoring.database.success=Database status retrieved successfully +monitoring.cache.success=Cache status retrieved successfully +monitoring.stats.success=System stats retrieved successfully + +# Common messages +common.success=Operation successful +common.error=Operation failed +common.invalid.request=Invalid request +common.missing.parameter=Missing required parameter +common.not.found=Resource not found +common.access.denied=Access denied +common.server.error=Server internal error diff --git a/src/main/resources/i18n/messages_zh.properties b/src/main/resources/i18n/messages_zh.properties new file mode 100644 index 0000000..78a3c2d --- /dev/null +++ b/src/main/resources/i18n/messages_zh.properties @@ -0,0 +1,62 @@ +# Authentication messages +auth.register.success=用户注册成功 +auth.login.success=登录成功 +auth.login.failure=用户名或密码错误 +auth.username.required=用户名不能为空 +auth.password.required=密码不能为空 +auth.email.required=邮箱不能为空 +auth.email.invalid=邮箱格式错误 +auth.password.minlength=密码至少6个字符 +auth.username.minlength=用户名至少3个字符 +auth.username.exists=用户名已存在 + +# Product messages +product.create.success=商品创建成功 +product.update.success=商品更新成功 +product.delete.success=商品删除成功 +product.not.found=商品不存在 +product.title.required=商品标题不能为空 +product.price.required=商品价格不能为空 +product.price.positive=商品价格必须大于0 + +# Order messages +order.create.success=订单创建成功 +order.update.success=订单更新成功 +order.not.found=订单不存在 +order.status.updated=订单状态更新成功 + +# Payment messages +payment.create.success=支付创建成功 +payment.update.success=支付更新成功 +payment.not.found=支付不存在 +payment.status.updated=支付状态更新成功 + +# Logistics messages +logistics.create.success=物流创建成功 +logistics.update.success=物流更新成功 +logistics.not.found=物流不存在 +logistics.status.updated=物流状态更新成功 + +# Alert messages +alert.create.success=告警创建成功 +alert.update.success=告警更新成功 +alert.resolve.success=告警已解决 +alert.not.found=告警不存在 + +# Monitoring messages +monitoring.health.ok=系统健康状态良好 +monitoring.health.error=系统健康检查失败 +monitoring.metrics.success=性能指标获取成功 +monitoring.services.success=服务状态获取成功 +monitoring.database.success=数据库状态获取成功 +monitoring.cache.success=缓存状态获取成功 +monitoring.stats.success=系统统计信息获取成功 + +# Common messages +common.success=操作成功 +common.error=操作失败 +common.invalid.request=无效的请求 +common.missing.parameter=缺少必要参数 +common.not.found=资源不存在 +common.access.denied=访问被拒绝 +common.server.error=服务器内部错误 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..cb255d0 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,83 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_HOME}/app.log + + ${LOG_HOME}/app-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_HOME}/error.log + + ${LOG_HOME}/error-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + ERROR + ACCEPT + DENY + + + + + + ${LOG_HOME}/business.log + + ${LOG_HOME}/business-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/crawlful/hub/integration/SystemIntegrationTest.java b/src/test/java/com/crawlful/hub/integration/SystemIntegrationTest.java new file mode 100644 index 0000000..dfa5612 --- /dev/null +++ b/src/test/java/com/crawlful/hub/integration/SystemIntegrationTest.java @@ -0,0 +1,208 @@ +package com.crawlful.hub.integration; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import com.crawlful.hub.service.AuthService; +import com.crawlful.hub.service.ProductService; +import com.crawlful.hub.service.OrderService; +import com.crawlful.hub.service.PaymentService; +import com.crawlful.hub.service.LogisticsService; +import com.crawlful.hub.service.DataService; +import com.crawlful.hub.service.ReportService; +import com.crawlful.hub.service.ConfigService; +import com.crawlful.hub.service.AuditService; +import com.crawlful.hub.service.MonitoringService; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +class SystemIntegrationTest { + + @Autowired + private AuthService authService; + + @Autowired + private ProductService productService; + + @Autowired + private OrderService orderService; + + @Autowired + private PaymentService paymentService; + + @Autowired + private LogisticsService logisticsService; + + @Autowired + private DataService dataService; + + @Autowired + private ReportService reportService; + + @Autowired + private ConfigService configService; + + @Autowired + private AuditService auditService; + + @Autowired + private MonitoringService monitoringService; + + @Test + void testSystemHealth() { + // 测试系统健康状态 + Map health = monitoringService.getSystemHealth(); + assertNotNull(health); + assertEquals("UP", health.get("status")); + } + + @Test + void testAuthService() { + // 测试认证服务 + Map userData = Map.of( + "username", "testuser", + "password", "password123", + "email", "test@example.com", + "role", "USER", + "tenantId", "tenant1" + ); + + Map registerResult = authService.register(userData); + assertTrue((Boolean) registerResult.get("success")); + assertNotNull(registerResult.get("userId")); + + Map loginData = Map.of( + "username", "testuser", + "password", "password123", + "tenantId", "tenant1" + ); + + Map loginResult = authService.login(loginData); + assertTrue((Boolean) loginResult.get("success")); + assertNotNull(loginResult.get("token")); + } + + @Test + void testProductService() { + // 测试商品服务 + Map productData = Map.of( + "title", "Test Product", + "description", "Test Description", + "price", 100.0, + "quantity", 10, + "platform", "Amazon", + "tenantId", "tenant1" + ); + + Map createResult = productService.createProduct(productData); + assertTrue((Boolean) createResult.get("success")); + assertNotNull(createResult.get("productId")); + + Map filters = Map.of(); + assertNotNull(productService.getProducts("tenant1", filters)); + } + + @Test + void testOrderService() { + // 测试订单服务 + Map orderData = Map.of( + "platform", "Amazon", + "totalAmount", 100.0, + "currency", "USD", + "items", Map.of(), + "tenantId", "tenant1" + ); + + Map createResult = orderService.createOrder(orderData); + assertTrue((Boolean) createResult.get("success")); + assertNotNull(createResult.get("orderId")); + + Map filters = Map.of(); + assertNotNull(orderService.getOrders("tenant1", filters)); + } + + @Test + void testPaymentService() { + // 测试支付服务 + Map paymentData = Map.of( + "orderId", 1L, + "paymentMethod", "Credit Card", + "amount", 100.0, + "currency", "USD", + "tenantId", "tenant1" + ); + + assertNotNull(paymentService.createPayment(paymentData)); + } + + @Test + void testLogisticsService() { + // 测试物流服务 + Map logisticsData = Map.of( + "orderId", 1L, + "shippingMethod", "Standard", + "trackingNumber", "123456789", + "carrier", "UPS", + "status", "PENDING", + "tenantId", "tenant1" + ); + + assertNotNull(logisticsService.createLogistics(logisticsData)); + } + + @Test + void testDataService() { + // 测试数据服务 + assertNotNull(dataService.getDashboardData("tenant1")); + } + + @Test + void testReportService() { + // 测试报表服务 + assertNotNull(reportService.generateInventoryReport("tenant1")); + } + + @Test + void testConfigService() { + // 测试配置服务 + Map configData = Map.of( + "configKey", "test.key", + "configValue", "test.value", + "configType", "string", + "description", "Test configuration", + "tenantId", "tenant1" + ); + + assertNotNull(configService.createConfig(configData)); + } + + @Test + void testAuditService() { + // 测试审计服务 + Map auditData = Map.of( + "action", "TEST_ACTION", + "resourceType", "TEST_RESOURCE", + "resourceId", "1", + "ipAddress", "127.0.0.1", + "userAgent", "Test Agent", + "details", "Test audit log", + "tenantId", "tenant1" + ); + + assertNotNull(auditService.createAudit(auditData)); + } + + @Test + void testMonitoringService() { + // 测试监控服务 + assertNotNull(monitoringService.getSystemHealth()); + assertNotNull(monitoringService.getPerformanceMetrics()); + assertNotNull(monitoringService.getServiceStatus()); + } +} diff --git a/src/test/java/com/crawlful/hub/service/AuthServiceTest.java b/src/test/java/com/crawlful/hub/service/AuthServiceTest.java new file mode 100644 index 0000000..a3c7c6b --- /dev/null +++ b/src/test/java/com/crawlful/hub/service/AuthServiceTest.java @@ -0,0 +1,62 @@ +package com.crawlful.hub.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AuthServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private AuthService authService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testRegisterUser() { + Map userData = Map.of( + "username", "testuser", + "password", "password123", + "email", "test@example.com", + "role", "USER", + "tenantId", "tenant1" + ); + + Map result = authService.register(userData); + assertTrue((Boolean) result.get("success")); + assertNotNull(result.get("userId")); + } + + @Test + void testLoginUser() { + Map loginData = Map.of( + "username", "testuser", + "password", "password123", + "tenantId", "tenant1" + ); + + Map result = authService.login(loginData); + assertTrue((Boolean) result.get("success")); + assertNotNull(result.get("token")); + assertNotNull(result.get("user")); + } + + @Test + void testValidateToken() { + String token = "test-token"; + Map result = authService.validateToken(token); + assertTrue((Boolean) result.get("valid")); + } +} diff --git a/src/test/java/com/crawlful/hub/service/OrderServiceTest.java b/src/test/java/com/crawlful/hub/service/OrderServiceTest.java new file mode 100644 index 0000000..3655119 --- /dev/null +++ b/src/test/java/com/crawlful/hub/service/OrderServiceTest.java @@ -0,0 +1,72 @@ +package com.crawlful.hub.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class OrderServiceTest { + + @Mock + private OrderRepository orderRepository; + + @InjectMocks + private OrderService orderService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testCreateOrder() { + Map orderData = Map.of( + "platform", "Amazon", + "totalAmount", 100.0, + "currency", "USD", + "items", Map.of(), + "tenantId", "tenant1" + ); + + Map result = orderService.createOrder(orderData); + assertTrue((Boolean) result.get("success")); + assertNotNull(result.get("orderId")); + } + + @Test + void testGetOrders() { + String tenantId = "tenant1"; + Map filters = Map.of(); + assertNotNull(orderService.getOrders(tenantId, filters)); + } + + @Test + void testUpdateOrderStatus() { + String tenantId = "tenant1"; + Long orderId = 1L; + String status = "COMPLETED"; + + Map result = orderService.updateOrderStatus(tenantId, orderId, status); + assertTrue((Boolean) result.get("success")); + } + + @Test + void testBatchUpdateOrders() { + String tenantId = "tenant1"; + Map updateData = Map.of( + "status", "PROCESSING" + ); + Map filters = Map.of( + "status", "PENDING" + ); + + Map result = orderService.batchUpdateOrders(tenantId, updateData, filters); + assertTrue((Boolean) result.get("success")); + } +} diff --git a/src/test/java/com/crawlful/hub/service/ProductServiceTest.java b/src/test/java/com/crawlful/hub/service/ProductServiceTest.java new file mode 100644 index 0000000..2ac39c2 --- /dev/null +++ b/src/test/java/com/crawlful/hub/service/ProductServiceTest.java @@ -0,0 +1,68 @@ +package com.crawlful.hub.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ProductServiceTest { + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private ProductService productService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testCreateProduct() { + Map productData = Map.of( + "title", "Test Product", + "description", "Test Description", + "price", 100.0, + "quantity", 10, + "platform", "Amazon", + "tenantId", "tenant1" + ); + + Map result = productService.createProduct(productData); + assertTrue((Boolean) result.get("success")); + assertNotNull(result.get("productId")); + } + + @Test + void testGetProducts() { + String tenantId = "tenant1"; + Map filters = Map.of(); + assertNotNull(productService.getProducts(tenantId, filters)); + } + + @Test + void testUpdateProduct() { + String tenantId = "tenant1"; + Long productId = 1L; + Map updateData = Map.of( + "title", "Updated Product", + "price", 150.0 + ); + + assertDoesNotThrow(() -> productService.updateProduct(tenantId, productId, updateData)); + } + + @Test + void testDeleteProduct() { + String tenantId = "tenant1"; + Long productId = 1L; + assertDoesNotThrow(() -> productService.deleteProduct(tenantId, productId)); + } +} diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..1b04b46 --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,36 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/crawlful_hub?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 20000 + max-lifetime: 1800000 + jpa: + hibernate: + ddl-auto: update + show-sql: true + redis: + host: localhost + port: 6379 + password: + database: 0 + security: + jwt: + secret: your-secret-key + expiration: 86400000 + springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html + enabled: true + +server: + port: 3001 + servlet: + context-path: /api diff --git a/target/classes/com/crawlful/hub/Application.class b/target/classes/com/crawlful/hub/Application.class new file mode 100644 index 0000000000000000000000000000000000000000..535e9c5771cf2aef5ea16650b50673cd7f699a19 GIT binary patch literal 721 zcmaJSUeY9saJwun^ep{KRkI7{u_XuWRww>W`% z=nv?Rs+e_=@FD8KGv4`l^XBdT`TOfPfVVga&|o-DOwJQ4mf5V#_`HnyyTu|?iEzs3 z08NJOt@t5$CUnX_$G0+Z4C@z4D|gAT)*nn6nqzY#L#$z=gBI2qwj-tGr!tSF{UYK_ zGV~&oh-@mX^5>^u)6G@Ea2oxO!-(*6p)|vO|6BCTW#X*T>1Z$wv4hd1OJl$D*citrRj{^M2YYx){B23ValeK;&O}j+R!xdl!6!yL`P1>S(KD5n)~{Ea zLt&rxO9-3Nvb!sgN#l>w&CSh~@U)$@R2WW7X%qQDdHK3E$TwaChQsSpJC)0+DwLn8 zwQ<#@QSaqSs+AI=bcQ%rT;-E4?@6NCfDz6>($FIT8uUk3N;aSqC(p>YhWBXSRq2qd zPlF0J$#(t~kl&)y23}yBKz#Fg1qQTxZ1{lg*XRC_GQKuISi@*zAFn8WKwpb)pIifn Nm3@sP@_y_w@CaPOxGn$y literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/AlertController.class b/target/classes/com/crawlful/hub/api/controllers/AlertController.class new file mode 100644 index 0000000000000000000000000000000000000000..17f7b0831b1dcd61604417b1524a0dd466158124 GIT binary patch literal 8365 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/ConfigController.class b/target/classes/com/crawlful/hub/api/controllers/ConfigController.class new file mode 100644 index 0000000000000000000000000000000000000000..622f73861efc3e192258d7edfd5fbbf144544a98 GIT binary patch literal 7835 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 O7)PJ;{kIi%{_fwBZIvtl literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/LogisticsController.class b/target/classes/com/crawlful/hub/api/controllers/LogisticsController.class new file mode 100644 index 0000000000000000000000000000000000000000..d5244d69c28f0644d4581c046b7b5362b9040c6a GIT binary patch literal 7056 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/MonitoringController.class b/target/classes/com/crawlful/hub/api/controllers/MonitoringController.class new file mode 100644 index 0000000000000000000000000000000000000000..619fbb2b1d798f0b030540f161fb3cae680bffb3 GIT binary patch literal 4293 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/PaymentController.class b/target/classes/com/crawlful/hub/api/controllers/PaymentController.class new file mode 100644 index 0000000000000000000000000000000000000000..4ceb755128078c84ff37780cfc792c04337498fa GIT binary patch literal 7205 zcmb_f`+HQ?6UDZfx7I7DYHS)(5Zqv7v>H^QHi|i@QE*O|p3{1YCB#q4(&_{EsRW!NR^b|wuY`YSb0ZW9d)%V86sdfGAms_`dJy%>^ad*45 z;nc96%lISTXA~^@lNSpsxU3w~K2<nU|1rbZjsX41N zGZ%|+Nf;M1(q(at;!@NT`caMST;{~eM_C49DvMjgxU7uab{NB0s^IcUEUW-h1j`hJ z3&nI=D->$bh~;54DLA*ZR=Z(j$=)cMk>Jn?HCxnrM-YHg#u$-?+?J0QCx*p3IYZV?Wo}FbYExl#@UZY)j3#$wP9SXV9AW(*hJzEFnf!I zD6T<^g1PN&8+Y}0+_bf0U{FCrMLx+umS(orUY zI={@YBTB;bkE7TnOkdF1GuYAJ)7G_Xpre1NqkmUNe}8X3mo)8s7aA?s@+c@Yy0p4u zk`tUDD5yUZHq;K&(NzTscX@%&pyv)HtggHmOy=E!u)2ItvlLzm6!Ri|J9*#~migV9 zHD+WyZDf&|%kVj+f=_2B@lG(#TH;q#Ovy?Tebb%eZIYN&NQtJM5$q<^hMCb!!S+e)3F9vI9wkBu@2cgwsm3z~Q_ z#heqTrp>~_y+ViYBc&TRPfG$J7!O2|#e)j2@^8qoP)-B4ph&8A=>?0f?qk=HQ7u() z{*fpi6`T+BGV!bPq979D@Q9veS>#>Bp)haf`z%XKq7Ikk!Ra~YF@Y_fZ=NuVm8T8Q zM)4$`Q?Sv$%QDQR-!cwoECmr9WGQfRx}J?~{fJS_WdtK$jN%u95p#Na2Y21jyS0b4 zCZIFi4*sQ8s2#;TiBxJMH~aVIL&Ouxvw@><24kmZ+xKlPXRkYbMNfAGK8K@t3BO@S z?%(zd`1t5g>0&<+!E4OAal1rC@H+*ITyX3VXX>5qjc1}PrKm1%8lstVMr}~D*Anzl1iwIdJ&m=sNuo!k`BKU)X#Z!B=dc&+Q8z$U&JBrtFgp57CJ7o@*V5URw zD!A0$R=u-ZZK@l!rZz&}IbJA;$~;NgZzDKPO;X3nOe>7{%bMVgMv-Ts2tMQp9%Twf zE`pEP0Z#QTQE$&r2q*1yIa_N3xK_va}7B5FuA zy9PB|h~T6w^1@%)-}uDJu`7Kk6tnEUY6gs=nbvO5#R0I+McF&>t`NFA(=oJCk>sJx zcV4ENRzAU2o+?i{r5ZaoHL)_4w@=ns%ucq7r+12kXlHp1^GRGIyoPz@sg}QU`IO>S z;Wf1U7@}|UCV+YT6@!8KIETMcdjOx#Gx-f92w9Rat|0s? zaRotZ<-xfNs|n;9+`$9uUPgN#-#(3XEHc;9!g{=g>!@MZ<1@h@y5xATj2x?EVhL-H6J6VepoDGX3D1Vk zarR7?Fqa}|6XqK%Ay<~p#GA($2ZasJ&uPKV_NOFo=J9Qg|NC!-QlS5WKeYkAct%ce zS@evP`-jhz?+GUSfu7`4Rd`;i!V3ahmkZOy;Gv|C3RD6~g>vx_B4ZDtcqB25t>GVCKNqb&SP{tPsm#^fE^fQ9&hE0A($k| z9$I>gZ=b_XpE4ZwXmA+6#IG1L;IKo3Wwc`}!xmb%mBA$OgfdjoUXxUYpj3uBYK1*Y z=z;xB+zCBcgsMf~pdpOjQ=CyH1!0o%P|}1s0WJ{@7I*j^eMc9=B*OV*L^B)*JhEebsm~;l+@nE@o5LgLw>byj=kyD z>z>3(gr{Za@5^g;BFQ8^O`g6Qd#3`f-=}0>&Gg!iwM;Z|^(W^9qraQwaFT`dE{?dH z$$1acfNA^|c~M1s zF0KVnnfH__vyH2;AM;^UTTHH6J4OG!+E7_d Y0{9yj3(%xEKE!zUcV7SDX~{qT2QhOiw*UYD literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/ProductController.class b/target/classes/com/crawlful/hub/api/controllers/ProductController.class new file mode 100644 index 0000000000000000000000000000000000000000..e151a94f37b9477b1c346dcb6fb215df843de21d GIT binary patch literal 8047 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 WH!iNG-^iRE;oEVxzjcM=yZ-=9+vRlt literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/ReportController.class b/target/classes/com/crawlful/hub/api/controllers/ReportController.class new file mode 100644 index 0000000000000000000000000000000000000000..3111592e42a2c3aabd45bcfc2ea13abb5b4b24be GIT binary patch literal 7146 zcmds6>w6T{9e&SVm?dmPmy4``jUbRK11c92?hxgYgs_2VutF!hlVo7Cv&_r_@dj1A zEA*;jt5vAiwu-g|f!4=dv=w~0l^?Um~ecIah%w=~HHV_`v50WP{XU?4S`zu$r~BL-~PHUL7j+t6I@evu44u%zR$AY&D}7P0QI> z?okjExPG5@P*d|-VNl)PzfaFP0=1Squt&EJ8d+VSzT*5ps^NM}j~#~9Xo z4cq9?>+1^z)6pEmEZ71oJ4|a(wTqTf7#y&)Vf~P4?N|GCtzfGITGlbG5mhTsP}i3n z^N?ZbIhvic$|xAl8i8niL$5$=qnXod5JhbglTjluwZkarU8UiE-P)DQHBqf+nVx>#yV(d6iL1q6LcuA|~DRg@N@lF-)YZ=BP zTxR47fi=~(GXeK9v)dYa72GLslN&5YKkTSI#&9vOZ({b{Wm?0UlK|OPGs1r@ot-VY zTn$!YwRFuIF(d5SPsxT}IbvNB>#>2b5gMB`y|vm%4{JAi(jUVouqlZ+Ce`3BY*ui$ zz`}_Y#3WMKBCxTFo3UQTK92=a!M1>{?$S1E_7E9VgRQ8Q^y*4tJGxmQizQa@m0^$L zQYTDne^3G$me-u-Xp%KMlh`H65ZS&hfqTfGEd{29k*nhXy>7roVsYIrTdxZv(TDpK z>}J8NFXX&d z)Xp&(=XevUQcspScPh`$v8XZrw8ZRj%U^ItObrbqW;Bqn{2DJH=>DdVyV2X=*cskS z_k=EQkNq=8pKv3^;FMc7+TE(q{7VlgA9q>vYgquY`?)~>w7kAkcZSRy7v$A!mh&5f zGmu2k3-ZJc_P9FCxFG4quNs$}aqF&SzISHTs7A1=1Rf+hK}8OTAoKU(Bo5*cQn{)> zmnP-HNnKI480<3@M1ZWfg3h>F;*&K0SQ1C2`5M^~yq*!v>DESu8MdH5p%%RkIrFI` z9+$0V}X#mTr&#xHhpBwi4|Dhoxmw-@1W$C ztJOGlH-Ui!-ry>O34Tc%XSig{TS=V2+XA#clqES7f0`xdAMEGtFmlZ9of`e#J-sqP9{(R_J2MLoDwwcL*z8@KRW zkMjG8$D2Idc~|%(?+M-&-uY?4>l8kzybIprjpva3fQ<;Q<5dm@ZopJtlimPa&u3{g zg=sW9o$Ux_xTddT?@a#Qcprp~SxEEUv?x1nawk--^cY!A{*#i#6EK)+DZC3Gx^w;Jx%)U@O5@ zEP=^2#oV9C5j*NRCSul7C_=_aad_(lnNL4^(*P zlItuU>hxLC)e>e^OKL}R1?(1|QsJ?Z$=-;T)B^H0#0?Jjs3Ai?8{OR69I0^2RJe^K z0G4r)%lYO`g563$+jw1tP5h%sHzCD>bu6;Z9bk>NSflN4phiDUZ`WF*pXSN}8`oT;pSc1TpZ(Uk_}q7eiw}^C z50Z433BmCzI}>6Pm_z!kc-cfi_ejZ&y$PC$;A`o z;*Y)oE*}3HxOn{QN^{16H>SsLYs28FtkJUJJ!{MgX&(Gt9 z2tL8foz0ig4o#_Fp2sT@oJ6w)qg)GK)TE9%M^zYG;L@=3u;+0_1x^tdnni>>b9#;+)f jBHmOv$rI`nh3_!t5&Xuz{?<*k6rX>`=l9C#_Wu6?@sQ3k literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/api/controllers/UserController.class b/target/classes/com/crawlful/hub/api/controllers/UserController.class new file mode 100644 index 0000000000000000000000000000000000000000..8277336fabee70f8636d64a15157828d41149147 GIT binary patch literal 5785 zcmb_g`*Rc589i4IWE+tX5eUX6H7{d>g#zVO1THcbugDK%pZ)_pbl_ zpTGSbzzO_0f`C9GSDH`cOl`3+Qz<0oD$@zAZ182#HcN$qZd!?pmTsQ%`VoW#)?d*U zv_wHG&L&2tujn~jU~PqFW4gIuQ^jJ* z)@-9xv;>Z3O6F|BDw{@ecE;4^^~I8TH8HJgMJq9*&LN_o8vL9C0S6>S19W{jdfT$!KN&2f$E3dAy{oK~37OhfkFmmz!3 z;70Z@k7o5G$sOcOU9;)?L|>9W!Q& znq4vJDEE}3QcDuRql_(SObk!y!*j`u6P8uWFDX_ay?KJ?Y1>FJ>tCGvLN*X3dc&ZAfFCEZ%cboX6{|)N+NDdepz1OWNg7 z`3uE0YQYeNEw9;H3r1K8M!pRfFcv{ppkrCQwf#jgj*F}?^5k5ey4U0Pohgyba+7b! z0(+Bk@GRT&5+v*9QWS5=a)_MDrc>kT!8TmRR0Qt`Y^ZS~eLbg_rQApHEocI5+|>|? zq|H+BUas4k3x#x@iy|*wZ#*|Vp3V-ZGE-ye>_j>{mCj~I7=FZb)I;igokmcnV!q_M zE}2GocnNG>6<2DI>FTq9;QluHTzP?TiB0uK$x^v`oE)m(O_Ksnp(DR?S6M^uv9x|j zx93WEPh)vRtzqI)g@V8dKVWWv&vN24D*vYEJ4eXDhLmq~gq$JT-U)KJ>1poapyH== zq_7ypHC!jp8wOu1fsA3^^yeTIR)-NYkho#wZwfRI!Kh}>RWC~lg6~Ih3qKG@`FH6? z>5E(CVRij%!G{7b$XjO@FN0k(rAjd`F}oARk0oZU!z1HUr$;UhQ>}6uZ@kbbaf(hr9^wl1E0GcO1 z9dhRl#~t^MNjw(sdxW=Z_VEXpmgWo>*bRfXgdWVPHlz}880$Fl3WMLwz}q?6i38Y* zBwoci-ud4~7p=G93bw=KHS!jAQYUt)gq>AMx#uQ~(L=`!vl8VhTQJKt58#MeEl7X+ z=wk*U{_ew76gX<4o6j@EaGrbUQdmkKMfz~;i?nyFiX1J`YBK^~peKTDU*aSJ5r_yk z%%96Bb2KbCU1E2Q)2i569(FdzJnSm$NbJrNyAYpm?fnY82-ku??KQCCu}oiG&X=AS zq7IBQlunGO)`92%6AJWklW~r`#^mqk(*Ydg9dZx{X?ciO)5Dl#vS)A<7C#hZJ@+#( zhULL?OMyZK;HT1ixJdxw1XO|&^lO%IaOLAKp4%S;xC zWLc2@dKJq;ie;M!qhlqx<&4QE$*oQVJHNoL2rhid89_y^pOR`dezgX;IJm6D_apq= z!}k{+zQ0@<->O>vllaQgeAri0%jGCOrIzu%RJM<$Of~^`xIJ7arr}toJ>R)!EGw#9lrU5 z2>ylWNzAkA1bgUV{?Nnxq0-m@y;l_aLHZt2=!fWew?aQmpH=kl)S>Tatn^W29XR$y zg6LRP>tGv#j>f8A7DJ*|3_h`Si@_1xyA8>HZ{=dBlKrqt_N8XX{)z9@tI4lC7plOq)h=K>{jk-~eOR!ca8cUb{9i0yl1+D)J6E_ui; zdB`ofY>o4jbwu}!htg;rN|K|Fl@wTKtcs14TB9npJ~{^QJ4O+}?-dbaeEI|XfAsSI Gr~d*6I>$i( literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/config/CacheConfig.class b/target/classes/com/crawlful/hub/config/CacheConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..8e10f65235b7057f80dc08f44afc5f9c8293cbcd GIT binary patch literal 3704 zcmc&%Yg5}s6g|t!*ijlFltV)YdEL~V1%~$c zbe5qbY2`%>?T9MqL>I%klx~VA)pAbQn>=R-hQ5@gaU;uZUH1LK4yUA77$#G%042e2 z))slavMI`Y26xEjemrH_MYXbL>t?ZF^Rn2t>|Hg_9j>}l<&I1OtXrnKA=lEb{Qe5p z9LqjPOoyAWg}3;~F(HX&^rFd~s!b?<`F~K%r;fvtaU=tYz=#*9!~$U*-l&?6UKUxs zq7(HGOw)3>q=cBicH-`dHAj5wsJuQ^T@>6*MDRAljZn&)`HG<*QTZ5BFpLWf{T1Q- zBN7bD{|1rS2;O117Xq>(C=U(&J7KFHcW!oM>Z37S^dMhWFoIF40fPKk930PJSO|q1 z@X};IF-kiP*74~0qav{kO{RxI=8Dm|Kv#xUlAo>Fi{g6<>GS#DHC5O(iz zP53pyuo#BpculK1V(512g*g{)uQ^F5Q3o0&;$ZJcCbmy+?v<;fCoPRMhsHUaop61*#c&7n5l|@mTy2u7qb}7U+yxyNQLB*F&8j1a z=)w5vw5M4OGu=(!SMUIflvToX?1N3mVAzbGH2Dp|pTQDVB3Nd48bWZO78?rBC72kI zk~wiyY3|@U*KGw4@e#vY1=}hgeJQi*$Iz^5D-c zE!`c*O)K~mpV1A>yASx^Mz_xMfpby`^7yS)_CB5nYDuq`@l{sm4RC z(kcR-AsLcT^{TVIGbrQVrGkY|qS7!d^%0|>UlH#4(M~P;^+3-E^;LQ^dUsxZj_%*6 z(S{g3<<~$T&d^iwCeZsV^+;rK4jt&Bb{l$WjyL-}y`{k)$c=O~`2zjh&oPjmovC4P z_606d?@|r#Qge)&6E#fE{z?UQK@Cz^{pMqlQsFX&UCPapNSNN$VdehDS)bVdIL8R?RP literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/config/InternationalizationConfig.class b/target/classes/com/crawlful/hub/config/InternationalizationConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..6d705d170e287840aa3d05db067f8050ae01225e GIT binary patch literal 2425 zcmb_d*>W326g}<8mOPqxkvE(bNCJ`-XA%>FiJV1JY@8rD!CMMmQms*2o@6xRn#Bg* z_#@y!slru!03SthyJsv z^DL*eeCU4k+Ay;!vZf|qXlH+(X73^9*!`<44M_6Jw-xj{l zTVlfvJ(DP^`9iH%aHLisehdsh+}m@#z*rZ)vZ#bk(-!q42%*ldhfZL%#kS>JmsUl!QHDY;A*s;a224aq%sirh&fC1Tk$ z#a^%~cvE=qqG%`}JB)c4Y1}$GLSbOpMzl}IZQNlP*xHaKt#nSTmZbR?I_~02vOShI zvOP{s(nLy_@w$QcR`UZQpc0<3m73LEI{HF22^p?SvOexZR;5tn@d*SHnedl-)$|D$ zPl3>IUlug=j+AUoa`;$BSr(&CioM{}l-5)sXD!*^j$D7(v&{NsC9g+$vzZWsj`Oqp znV4~>O&sk6#JXr%e&GE?$4djlH#!#awT7qoHjQTt%V){d7r2fZ>I@_Ade897+0L(W z+2Fi{$EDsKGO1!ePN{2wB&E8I6rkY$Cn@RN)J3rCHdP@ADga897Pa^lOWq_`RXjbf zy<74?GTiIw%h5HE$YoVsmZ2BLLmiPWG$=2B6b> zNKKC7JpJp@28>{oMid6dX;&`ZH9=nq^or&N=zH-Hli$C^bgh`1>0q{)yWGLmVyc5r zi@(xx1V61jnIJDF33iI!)0oAKqMMFjV!8|_i10I_W%wLjFw5;Wsonql0-SBi#8y$4M#rWrfLeWF>S_#Sld=mg6(V!2MJ zHxevkU6u?gs4A8xSSCcNTqQ=jz1O=+Z2F6#$(%gEKXB)9<-(oE zr3-ZvJ#vm8;D3mJ;jtf<0?`epLo_ zU%AzaZ`JGt&ws`=g!c1LTFb8Tx^1}y4Lb#L8$o7apiRSWfun1>D!1avA}v{TqOBEO zMDbYKiQGg~%29!Qv|q;o9295?Rmc=x%9|^lm<~DZ!a5`@!xUO^m^#(p+s9Fl=s3zz zwRzhQh=^7k7dW&E|HrGBKi_}gG;&L?Uo5?S$M;U^IK}tcpV&d!S9K;)LpLdD{X$Ju zw%vf#k>^^^P$HAX8Mb|dUupSIa$r=f~$S>Qy++e4X zKp<1xK^^BAw<+=+F6^R?OSnvgq>ciU`L(5$u=)pgQ|z-O3OcS}m}=$g0uvhnusYW3 zZSqJ-;2N&d^I5Y2f$ejb<#e)VF~TjlF3`zT5mOGo$vH0whX(rk`uY3@lQS+b{BPY@ z4MhF8C7>rSKDUBhWCe}4;|?Y@m;&1pHAkUxq^W{X$6eeL$aocg-G52FF?5KI)&2!0 z_P&k>n5O2X83+ufSC`{%hDU+ybfTD>Dj8O*hF~w_3S8LixM+;d$WP`>0Y^Yuqsa?o z=!?c9tr}U9c0qef&u6rLN{+L%@-eUt?X>C*2imuiMT}uP1?fn=cF@j&-k>@0GZ$ez z{)}CV=%j6ru{Xv(<4}y8aV*A(Mdaykk8wK2S))J3K#U79h88hGzpojiF~*FWF($s? zc8sZy6gLCTrzcqrLfT3AyJ+piUhKv}>_IsjD8 Dp^L*5 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/config/RateLimitFilter$RequestRate.class b/target/classes/com/crawlful/hub/config/RateLimitFilter$RequestRate.class new file mode 100644 index 0000000000000000000000000000000000000000..13a9ff7ad45088ba318f6ef39704929aac2aa2e6 GIT binary patch literal 1104 zcmbVKTTc@~6#k~$>$0?^pdfffl(s0VMfAZ-f+Rx8S`}&ykK1J^L-rDPXEpvOeJ~L) zn)n0!QN}adrWSo6zRWpu&V1jT^PTha*S8-49^-xvDTWm%@JuJP-@E&MZOkL7!a1ZF%7^wl+jMQeXTInjaz`>`oFMkeE?G$~mcmtR zP8^2Zm!>TPPdMgUVw%3>Jsv(}7oX1QK)2G_m`4CC&8MuI1hKWJ-sKp-}fhFf9)hde!<5$w{YtkIVJG zz@$o3b+usN2Ii?CN--3K?}XeVsa39Opr&%AVRX&k+feT^p%PoYxn#WOa@FgCJP)M1MjY%_=?u9CVM zZD|S>HKxQPsmW$2Mk_%1F}k&oCJ&gYpTMYpM{!p>LHP)i^{+U0gt^ZN@)DgDdI@aM zxrs8Gm?r-+MT|ZkSLjcWe+}~k?gm-Kov(kveU_GC98C2Xy6`st4X}^8!T(xt^fc4 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/config/RateLimitFilter.class b/target/classes/com/crawlful/hub/config/RateLimitFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..f32869854d1347f41ed27e4d2f85b815f07c6176 GIT binary patch literal 2214 zcmbVOYfl?T6g^`Mwil;K9FjEjW!g01nQfY+k3b&8af(|E#0CmgB{bLrY*_EQv*V=s zAN|sQ(W+8W8Y%tO5B*J5)iY~j2&NS(((KHg$Gzv=m-+MW-(CT@gS8YA0#|Ec!>wug z%-?GH?oMmdt%bpsx9wJ>QS)BIGmkvqD4jx5;LH>GRJy(lw%x_eC#q&r7!bJdMD9v$ zq#G&y)K|uBzYw@so?Y9h6d$h?tIO4mrDA2HTv}LJE((Z}z^OtQL`DW?Rr)Q}M@%yU zS*^ZnsmNICk-+Ks*ka3gzFU^fX@N}D+YY2@X(h1s`sqwtjNw&{_JZx{`4h6cTsbg1 zeV1m^Gd14}Jabo|Z**)`AXy0ODur_bR}LW2RkfDZDljg|be3~6+96g4Ih@bp0?r5w z%*47f5O_0-L1Zm6FHj4u#-`HCa?>Z9+MwmBcWp={) zlGL)Hm`o&-u7~?+B6~r0Om?C&-J`#I9yJMD^AzAjuTQ1kS~A^Fp_@*rk%f z1VeF<)kUpCoyL^Fw=02GQRqKa^*ocP>G{&SDBsi}qtm#ZcQVz&#`5CgMtOE&y_@Cv znhb)_Qn+2Dg&hKtn(iDwU7y7THa*sBHjU5djkT#qp?)THUDfSs z5VtXt0(0u>(Zu){)mb(d)=h@U=9H`}&6+umWG?^*_wgWw+1ENv+b3BR@Q}p4sMu&4 z5@%U{lg1<3S(~aT=}(oGEY!IyO8Ao9h1M&o5gIjHuM@%0=rM$CO`VVw`6oPjIM<7s z#;Qivc3JeDQg*6gOV<>WkaazDq?2B=;TE`5X$8h>s8uiWm{GGfZQ>MTag5K0dfSbf zwuf!mnt2xLT{mLosL(ubDt4O&GlD@UtZXR=lrH!(iWDn&Mc~H&=z7egLp_kv0yC_v zY=4R{!p7Cd(T@a%kwTj53>$U=4rTT;%BN!a4Gi$~G|q76EO#b&$NmkzL~eZ#L%-nS z&lD55#P0wvfmJXZ?|>4!W#^&G*`D@~@8PYZPtL_ptW{pX+jxgF+YsI($~@OeN+YrT zTa+vUaa6@{tl~oZu%p5%`2d&s{g6-ktzfsV;A%(j0I!dBG~eW2lIPA#jIZY=_i*iZ zN{5L}9YX1JOjyOI`f#1o#~e2}1a5X&PsH*uaBm;dB|5qixP?z>@hl(xz}fzUUZ2Q) z`U0OPP@en)&g8GS_am|sx#H11XW|9E5WMvhAN9r27-l@|{{e04^TJ z#`A3hU0+=%i?4`2!&~zx6TkxB4qMB8JNt(xd5@Ywm$T! z|Db=OeTiMV+U2vWzp1PH%w$L+kRZDHkhz@MXP>>l{q0L${rmiH0GIGoK?lQ0-7aam z!@`{GbkVtRLnj|SM zbJtz6o!o?_)0qy#wZu@`b_$wXb_}bKcX&zA_XW+ZIYM4m%AT!dgS$4KlCQU$wUFX{ zGrr{+B{65X1{pYJS+>VL!?wu2(KQbsz&x?+X?#Ug8yDP4D%iT0mRFNAN!_&l?7DcjAEE_Ca5?L+l8zw>KYLlKdU;IF9}3QE-?c(-J*t68F8Lpn&N?jI=wha4d>^ z6xXq)&yo70IIiFr!(>bKwM5tmcq-n*Ad!;ia^V@{B5ymyc0ZAx=L_8NIPuU~G=-;y z0!dnrk||{p3)|uU2G%6|DZx*wIE4g5kM6skT{0e#HRsxU3@L7p$WbtvtnYo z>;g*s3j_ z4QQ(k565uD;$E`c3GiebCvF7ZM^IHSH1 ztW6M)WJF3fH8P4cHMcM>pQ)I{4JvQks?_+!u#4!m%5Q1F=PJ_DfNheKwSpvalC8H? zOiMl8LD^OC1;hE47_C!jDgTO8uNiK>yP8iNof1Uw+-)xVB@vmz9W!|;o8yDz&CJG9 zy+tymsASaBCOI@0$Cvm@!99j6ZBdZ*MoZcDf+3`yjJ+VN8Bw-f8L5hj`}mrow`6!l z*~z9YvvQrAE)}{|yDA_tZmpdv-?w<6;5&w?mWGB|*x3C8CI5BrN)8g@vl;jM!?_R?NJvuqi*fN=(3z~W^JE} zJoRp3*Uo4T3WtS-)pjxw)ibAx+@Q<8kqN$~} zGgLmwt(+;`!89eSUv3<3YVBgZjTvGdt4%c9X+PHVzd@QC5WhTu@}nBj11QTi(tVyEB< zgZ`Ekr#_vmKf+M2YHFioVtw0MLk*4*B%L%`0$ub(|L^Fk(3?hgM)&UFXNdhl4;_fp zRelDxV=G(0}h4cKuGzjk@|8bj4P5?ZyBZ zw1;G+S$kJ$eMP2r&{t>V1@@ELgZ+o9IPx4PfGUQn814ECXYY0Pe=u9chcA(=;!Z+CTRV&x(D^*+%w%@!vQpMCu+^*X{93V14_U$1A_F^{<;4lv23=UzOo-#Os2Q)0q z<9OiXKxlEzN3tC=n5Eqr@}ryjGSC~B+@vK5+++mY@SnI_#WyktoxzduK+#^3+t&o% z2y~Lk(Ju^5j7EGxZ(YdKwF89!M!CC4?*{$m=_h3$l4ci+Si&-`?5DR1zQ+%wKZzgl M6G>d4`_Jh65BaV?7Njoz1;)v5aPF3?oQ~4<3Dh0khlf^xCe{VEmvH-|CImVGR

4Zh!{X1lj{G z^|nmM%6yRHq#87jWipaRyZ9zqw=-P`91i|RATHQQshFIyZ0C)#vwZSS8ks7q466lG zWl!!D+UoTeHxy_QHT+8^uh*wg{xC01tX^w3^3KBF96GV_jEj2w-Wm`0t~_o{>+z)t zpZR-?CbV_hPPqRAL3nV9cDQ$mjq{4J$-7&S=#Ke@{R6^v>=k*~miGb3FFLZf0ih%skJ1?!7bl>!0U;0=S4DB4}bb zRWuisqNOhD^Ny~Ror1zwio7B;(@?JKWPAUsgQF7d_n1;<$hS0<+A#KZRB|{aCMSghvf5JA@ ze~IAj&N+m1kxyy1M%oM;hAC7T6eP`sP@2*=1WDwD6`SRdFEMiQovsmimsgTO3DmjN-fEP`IB@1ZH|UkX$ME=~F2yT@ZGH{boyibwlZslcTKTgMS zMox~?^a|)pm7hTbom5Nfw_YWe-@&$C0qhL8tj;%v zDVea-3_I#&qxLz98S>!R_+%;Cj|k zP|@2wGJJJ5m%4c?m7kO{mg3-2#yy@qqPoCGO{1uaX-$-=MMsw*sC*p@lJ;v~@F?G%!0vc%GzLfFDmD_*Uo<8=jJ zM6k-xQzxt$OXdPsJaithYid!L)~aOuCBx4_UcR-rpoi7&$(ZJXQwiQce69fn8YWKC z`4M7%Ct&X%6+s93ON^?zp4S9VwzpzAif`z`C`Ie8c!|)>tN$4;bThhZj2Q;EMs?M; zxgEiGR07v<4`UO-qlUO>!0!{Mdy7XMhd>t;iW1PVP8Te+(mO&ug?^bF?SG2suheKl zjNbAyumzjx9rq0AcMJ6xwxS&^*hcLpY=;cJtu(rWo}G8Vis*C~{)22<=-b2#bj>`) zd*cHgyPKb3@9#L&jOl@n-s4ZuSM4a&IaTcpQs>+=T=4oYaCv6H8yS9zYroKv%`U_} z1QG-wN)S3}`@`5tY&z-gvde|t?``kHCLkE2uaedTKElTYK8oweAWQ3w)2O8BjZDzU z4H{`{qA4I_$@(U8HP-nM){`#lCqC=+2CQ#Yi7FkRRGWj_+24(nf5Yt%y88&?exh-J z=pLktZa0RgB~|k1rfYQ5gniPbo37ElvWBi438niPKCjW84WWDYmFcQgj!MT|wMldz zzf5;Ngl;dQxSX>!iBoGNmLnk&x4@>TX@YRs%6xaIzeG=z U+F#N0H6BneMy+r05Z`0>zbqBo0ssI2 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/model/Alert.class b/target/classes/com/crawlful/hub/model/Alert.class new file mode 100644 index 0000000000000000000000000000000000000000..12421b54f0757b91aca572073d0172897aa9303c GIT binary patch literal 3289 zcmai#+j1L45Qcl@v%Egw_=t@m5WokLC58|Z#}1C{7$rU=wqkG~h?P9C*Q|EK9!!}k zF5rovDil>*@Blm%#ose4$r+8{LOYuIyQjbD?w$SnpFjU1q7`y`C}+`3HLN++$o;{8 zp8C#7T5)ROk?@^0Uqnd{by_rh>Ylld?*_-tZsk-|6N_@*5!Sbw!d4g@FIzOVmj;Pf z69-=GReZ4)1YzPPUKqp{O~U*;H%eURg@|G=PDD@@PVoqCSB%o82t?#2;^@HjQvtKy zIN|y6`TuwzUg`XW^eC=x?39Y7LpURz8ze=5k6ZxXPa;pE&lnjBp`X?Q#O@5-ny{!( z?Na9R_qm)Rj$~0@{x2tT(i@925qSwXduk$%-D6mE$H+vAJaorl8dc?9Cy|IxLLYne zyVWFheR)!gY&9#N@xc+O8VSzB8Zw_HJxx5{S;r#K@|j$Aq35w>=$|R${`y2p-FLje zOI9uFn43Sqi3+H5o;v7?O>fag{6s|*!+JkJ(8lM@fenBDrX z#sOB>u~6Bbo4=56*(iNv(FZnlQ8&zDWp-&!w_?cQZIPUWM~~bH=}*u%NEP-iHEKEf z%I;Y;dH;`W8lXX#WB;c4*u^NBHOH~BsqAn|;|5Y_e|CIGgmrM6G5PvRByRbDJg_7Fh=|b*bJ& zY_w(5BpWexL$GL&PuFZPwtQsM6k87R`I?f-M4{PlBlY|?JC5C-*fdQuD$oV%dJ&t( zoZJC~-)pobyFayQmfZ&sy{RBW`|@P>J?_4|%=3h|=zRipD&x&|wDUd5Qh z*uAj$J1sUt=jG~kly6Dy!0BQj6`ud8!a7*(!u^85q7Ui1foVx+x?yDcm~J3O3)7O$ zbPK_5w_#Gr6#3J!r$E-l2?XR5Gm^neOUL+eW4}Gt*t2=`oo0+AyitH)9%;O!tgT1)b@Mk!i!s zRM43Y!Su8Zle#%Frg6!1-^jG7Gd(jh70pbWI+F{gN*gA1X=O|klIekw>7mXfj7-~R zriVI{2d2|DOzOVOm?kCDj*)3sXQ~;Q9-EnVb*AsZ6t!VeS9QiTC7JeU-*7*bbf(nE zbUV+^%*)6eKF#0$3;3<%5dZ)H literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/model/Audit.class b/target/classes/com/crawlful/hub/model/Audit.class new file mode 100644 index 0000000000000000000000000000000000000000..4c3329543acf5dd752a24fa770f8e29c085ff7e4 GIT binary patch literal 3320 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/model/Order.class b/target/classes/com/crawlful/hub/model/Order.class new file mode 100644 index 0000000000000000000000000000000000000000..de8af25cd7696b8c21a33e07e61acff0375515fa GIT binary patch literal 4251 zcmai$TXPge6vxj5lI$)+62djy6vU8#vR+XMpt%4p2???x0t)M7wl|q%cV?Xn7*MRz zDj&4U2d(l!t9;NepjBE{`QQieLs|Y$PY=7>+p8}-)92Ft>(l?!vw#2d=U+s0fj%9i zK8p^!L8It~&Vza_sTbFi)nX&4ih6M=tcq}uwp%o^;oNtMb;n;TF0F0|H@2wHtHQp} zax4V?+LT3OHxfVg8e+wZyw$px_WdAsVlVI`iw+=s(+Ojz*c4&pMX~T*Q7l!FcEaeo zB76}#v8b*%^+X^n7sWhY+xnjjthctbGiJIqoe0JFU)$-CJCiG&_0NZf1 z?lqez+jO-WiU^jG%u;bO(-6+e-9*r0(pVKCvNA)(SC+gR3O?Uy>`;j*iM@Jp4o2*+ zpxhM){(RD`X0ZkJE*%5s8S|n=+a@MgfI18H7HAv2V$%!sB9>k7e33FN^I>r@0~BV( z2|XSw-P19<(kRbAYtv2|LOAAch3{=&MMd*Ec}!ZCqU7ASZQ8@RdE}*~<0^qS&efb%g69eD*>o)1R%VAZWK^pcIC9md;~Y7RrDi+& zB!}n{Hvos#Y#QgV9KxhAEmFC&F*tI=rjs1WMbg~30=NVneZnceO_Q9mi?`e1Me{Oq zt8GRzp|l!@XNYV%P0t~kchQQ@)YuB# z0gg2e(~%M#rKd{tG(sS`?%ZQ=Kw-+!M*j?*i0D~(A*^oic;Af4~yQSX#>-Y&UDepG)I@PMi7ZnK-&m)+I+Jf?s+j9^S7!>r6!ly(oc_K8kbD#MkY^Z z`ohSxX=d_trZ2(tRS%~0gQu8IN~XGzsi8A{ZDb0}Obwms8!&y_gDL&YDyB)v)T9p$ zeHZFX-x--=N=$th>P+8*>4zRn=@(uxot8}ZjZ6=8rXP(=TV|#QI@3>J`nd;F`sYG1 bodHuHJ(RzX`MlcegL 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/model/User.class b/target/classes/com/crawlful/hub/model/User.class new file mode 100644 index 0000000000000000000000000000000000000000..769e083ccc59c15059cc52bb09631b348c77a8b8 GIT binary patch literal 2606 zcmai!>rxa)6vt05EW0dYK*R_li6$gj&^VG9;{}Nbh-+3vSShuvBF%0KV`gW_Tw?K7 zNmWv*%7;8a9x9dp>FIHC?A0&R)93o@b58fa|NZ?B5#6R%g-U{^+hNyjNAk7*GWFeq zbkFUE9p$@EVii?rP|(;b`Kxq&8SJ}{_Fk!WBBiZJM96&V*`xF(+%!bmy`z6&10mY$=|_4nnV>eKi`O&mH=d&I=lCdV$(VyL&2X zF)kM%Y)gMfMjr2ta4o_$S3(su=hiUW^(U03QW;)=@~tVpxtpr&IVxX9`-aIQ^VHh87($+7a!GDbC!`hxfRl4MxjkqaZ4AiH$4| z1&ni0pnC5rJ^`WNn}qKL_`6sI*7E#c^x5w^;4Hozo{R7WzD{O9U(#3D398c^zHdAK zLG!;sS)!ivtkwAveGMyiUcy?!Iy8UrPnz$gu4!o<)t}Q5xLgdBrRTq_FP9-L<7#3N zIK~wV(?XHyl9lOOx{4g{FfA0Bt{~Y$AEs;=4ATkC^qrOIT9N6hmFc>j=~|KLI+$+s zVai6(FimKt8&;;9MW#h7)1sZ}W|8R*n3nr6WrJ^+&S<9FR;HyQ)2fx}j-6?#$aEh} zjXq4-oij|6nrYd}bhpUVv@)&QneG;uHo^3`4^wv24bzlnTBCcG8DB3lwX94HdSILJ z^&-<#Fg@$Tl)W^D>8xh@(aO{;GQF@eZP=NbMW&y?B>OOBkDOte0aJ+{>F=g~xcOCn MjQtiWhi7s7e?{hv`Tzg` literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/AlertRepository.class b/target/classes/com/crawlful/hub/service/AlertRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..ef4a1049d35534afa5655440d30194197460cc79 GIT binary patch literal 1146 zcmbVLU2oGc6us`)vJ5r~8;lKjK;mJ0!B6lKHmPGm0tJES#Jd;YcJ5L;a&B7XukpYS z*pHfUoGB}7E22FlvVH7x&;2-8*T4S&z!AI-pu^xGF=L)sF--?W$}fs@o}-JUpROli9em=s40$5}4-q<0}GpqD)V8s}x#$IwMNbpf6xNRu- z*G$|JGT0g@El8lld>f!o6=8H(9~Goh9;=)p24P#R3&OX`;n%$$w#Z~Hzmc{# zFB&Ik6_qaCyjkwgUGt4UD>GN!P*g)uuiwmF{A5uO${w8{O%e6tZp`%W)LoD6?iVyz zE-j89gH|*xN)@_NS*q9=1gK@b+UT3YCiq#U?s(^p3*J@p$)KF+aczVszLyF9qjDxp zY@8X6BWE`TEACr$CSG-x3}78P5V~ClHsDcp%HSbXZry~f272tFuv00hHOjdQY=siY(m1;LJB582?R=FAecZ9wA#t;BpKN3hTYjf zTD7fTsIAsktAJniqt>rhsx^?JwN^!2tJb!*YE`uA5Bv@G5#MuX_A5z~JdeNl06TN; z+m6l+u5R_EYi@g!M{Bfw}L1`wSh_miMrcPT6?l116F#U$$d41x>GST(Ql@0 zvAsdRGi(#c>Nz6vDB`|Vv9x76mLg6=G}5gI%Q|)(%XP>vThK0p$ zy|^3t1Bj32GcMDj2CmQT(&vo_aDxVY z#L8sMA?0hmffkt*q_5rXZ~z;qs|?kd&4{137-*F#m7LP2Mg!O+Gg8@fOgY(N;HLZx zH5x!W7dXRdD>Ix*#Kp~a106C+Q18n<2w*2^pv1Ex$wXGU+hw3rW?muj6&~q$E5{0H z>7f3C=t8x4-(#Q$dtL9_l{Dk+Ox2ae(vk&j4gHnS&}*O^6;;@WejWQYM5gJaq%{;O z*}wr2fCqA!M^1e6C&d-*%0AeCP$4ont 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/AuditRepository.class b/target/classes/com/crawlful/hub/service/AuditRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..b28dc60b707b1828a886cf563bb7ed86f551885b GIT binary patch literal 1356 zcmbVMO>fgc5S>lixG8N3P+Go9sl=gN*b8tmR7$CkKt`Y@aNl|-$p*)3t;db>*Lvs= z;71|Wo5)S;1gsov@9x`q^JY9VzyDlb0l)`1^q|JzDAYq98ZnBxS;YIM!} zOV>Xd6oj%xCrBei%RR4vW=ihVUiWYB=hUQJm>gHRm1rs?tC5m0l(RALFqV_mM%(4! zlZx!|&aK9MowpQ&@l07xde;w@>f8UQoN?m{>Dadug29S?*6iG=+bQC~I@F+HcQx36 zP5WJAa0l`Mvjy7)bk{+RS`IP0Y8|9cO3k1k@c>F_ZGpA9Q+ub6y#4G`PPc_ R{mlB`cWR%z_rX;Ai@#*ojNAYK literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/AuditService.class b/target/classes/com/crawlful/hub/service/AuditService.class new file mode 100644 index 0000000000000000000000000000000000000000..1f29b7b88fbb50a6dd3a60243912da268bfdb8e9 GIT binary patch literal 6269 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 SX(L41$>%>+cH}znFW}z{KaXVq literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/AuthService.class b/target/classes/com/crawlful/hub/service/AuthService.class new file mode 100644 index 0000000000000000000000000000000000000000..c693a17d9b5374ad961cccbfaca977baaaa24df4 GIT binary patch literal 4525 zcmb_f`Fk7H6+I&_lI3yYB!pB@f)YaFCAJc>5(A0tIK;*YsbxDPr7dIWS)N3iQD#PV zTuSLe3#A*~2`$}sx&Wn)Ofxz#D&uKbQ{v_^RkU5xvz>VjoujL&n^>{6PeWp?WO-&mO`5Jbl~?_iWqXEa+Lo)~ z;Hd57Qf|>Pt=zO@6x5vUoKHfm&L87xWMt;(8Oj!rQMsLPsUbe5E z-zvH=teB&6rt1m(^BuKt{l6r2_*P(m`INA&FOEC0C5q$PTUihFeGvk*c!Wqsq#8Ga5G4F)=*cc{YZ7H0+e% zEf!6(P;$NGl;W>9r<9f4lQgVsa^J3GX2x(>M#^!;t-U(-VLwp{jfM1IB^H`FNG^oC zDrO8XnO6o)?!V5!Asu~Upe-~oR4~kZ4EHm@VapdLm57qlwo@>?7!K2$F*~oSF>Zl| zejQC{Zp9#mqByEy$4&Md9mnt>d(O0JWe4WVDICuBTiFmR4TE(st>x*B85hGalN0Jp z8hJ+<+4-cp$Xaltcu2$TH4ZNmHjWW&lE+CM4-1o8UFC(oiu?mQ#zJ{$L{J&m(Smp! zlXxVG(;AM{>z-j>xtWHi;|$KSc__b!l2X;!43a!)@}4hMUuP{u;_0J0J`{Qy@nyf5IpR4Y&P-@@DMpDb{I0Nug44-p-kp zhLLJlgY&003$>Cusfyv_EWwaU)x{RWC-^2zUG*D5^;0@Nji*S^oT{9a+}=^ggs}3n zI-bTeOfYLZGou(wPjw6{2vx^#_M2xlyg%#|A?iunNrte;Sp{Fv@kM-zQ0A1ScuuNu z`@AAvyE|@(eO;fQ!&jsD3PUpO)H&C-=G2rY+EOEPo~z?)_&TGRDw%ozhOwD}*V#Bw ze$R7g1mD#0tq`#g?^ayGx1)H0htBF>>-Y{{)DS1#)1~0vwW}JvfB`r3AO=UA_`Z%G zgieHgMV$DNjvwPEWLPF|m<8UL>gcb^OQk$k_qtEi{!GWu@r!aB8+P4Nxl(%r zWAI+WucP?Y%0qmfcZ-6K-{7|z*6||eD9an?W%7iX&&x%H$N7kK##eN-W1TdQ-^uCx z8tbuRq;pcdThMV?ylazIRJk&>U!VAXIm_@$j_S|nY+fB_=)fQFN5XbI zy>B-U-fcB;)PB+@UcITK6}mX_Cmnyr+tLrI{4{m4nRncbnyPf%3<1Q}Uv*rS>_>S7 z6?yuz$Ci)7kdJ<;EMI>rdT(%KG0%f|PsclWmmE#oB`2efnbP;#t1g7SqEtgi-7B)( z5|!=E7gZ`<{&%AfTL@AE|4V5^M9zDzqFl)s;~0_Cu0}>g16eD_HvX+~v6h=jfZ7-9 zeQmq~^VLS`OmWmWHg#Rbt*`h--p8ky1hpji6!qMI+i*KaHYD6Z?fh7Kp3aJc*SfDF z-o1d$m(X?v+s>@teisiE}P;fv5iN;0T8%pl;B~4e+nrN!h z_JD}HUc-^@ol6*KU^dHEj*I*XM$TNu=%MCBGd(zwXx_PiQ;BA7PxmZfVgVmaG$$Gt z@L>*AY9yMkAj>bN7m%x(pBMd!<|SAWyoHgM8C3%WiAf)(%_Q!foNeKAD>`}W-G#ex z2sLD+)sc9k;GBLd5q9Kh{J5&qj(4gpXV8@fVQ{naRoG| z0-93+%?Uso#daT{MjxQH0#KYk|CWD`*e3<0wuTibMdU0E(7D9)ND77*@nv%B8%uaT zg7x3!^m|MAVFLmpE#apTf4+!cGVGU)%XoDOzmMQmEOPzE65eWHcm0b72|D7lYO7z+ zV}w7=Opap{CU6(Ib^xad!6RhGm>=w|Wd|z34*TI~zUNIE7n09+h?HNDM5}>6ELmZ5 zP%Xh1spN+ZeZpxM**hQH?xGlMXEw8TY0R?{nm<= WR$q6;eW7LqfA^pNsIzjL=j@PL8(n12zCi~@MWV-EmeR#f^8uk?0b@NrzuH?c7?phHc?uQ0!a}ZrzqJohg^1q zP2GVRji_PK~+oGS0wbJsPqvme^z?H?Rraiq2!%!`lm8+J5pg9D|vsQtumy# zKh$&DL$Lej{+FI&f)iJ-*4@a(X$%ytXzxWb*M~gxS6+`GVa80Q=jXX3>~;Q;E{)-d zx(K92PnfHBbop!Yw!%yxcudIehtDua3f^yu5`9de6Ie5J#t~%omD5MZm_7#zFb)|g z>bDF`z?6RG5KL+e!8FXo&};-1Ga&?}2%dxa7+y%iixFIgr5Ik0;OXppHRxY7xDwr0 H2i+@g>{oii literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/ConfigService.class b/target/classes/com/crawlful/hub/service/ConfigService.class new file mode 100644 index 0000000000000000000000000000000000000000..37f7a076f119dc10d81233deb5fea01713196290 GIT binary patch literal 5762 zcmb_g>3g&j*cgh52@a(hOJjRLmK-gBo3>fn zCQaMWeIaeSr48Miw5h2>o2G5bhkoh*(;vUmbKi_+Mo7f8zkaYZZ{B_PoqO)N_dWR^ z|9HJJ8T`(_ZC(GH?ba^6Gvqs}Obh{CdS<^?mA zHFHy`V-x4BbcwIh+UkTgn=jg>d|^(ZXLu2pzHz@o;zT)DvS+L@yJ$~jt-ZNizGRl{ ze6FalYdBw+N)=}dc5Z62V9r<<^M&)N3CqkCQLV z(ttv^vujKtvOk}(;s~R~KojB$ONZ^8HBz3LunH$l?yJx`oKKtCF|%OH*nJr(P1^+0 zxmZXpOWfE<7c8@6Ipis9?Htyml}mOub<~{g@2Yh*Lu^@>ZhB}MwWo4ssaznavD=pK zcD(5njFyC#{=ge2Dg6VBZW6^Rg|#(l3Id6v4J|ROR*03XoS7>PW}2}E>!Mhz&{7qX zf%Vuxrl%|lVzY}lpc2BlT{!(w+*hS1Cm7julY#rOS)sXTl}@@lE8G)w>qg!+7DFdf zU7XI(4rXL+w}Bp6+r+h_`fUtJ4jqvWS#x5PGO$&Q3>rCU;zSG&aZAT;%*>W4mNslR zutO}Dc$Ru9hKCj6j_t|0S#A1=fj%*9@l0hVhTZg z#u)=2#c73AlXfn%Z?2;Iy}67dW3l;|fu}*bmsHy*Y^npyEjitGT>P!IQBtp?B{O}V zS#w!m;!(A4j%9JTQP;X&tj|eAQwFACD{QQe$cvf}PTDk*;W}Z~?pymNNX0Q-g=V%t zj`Ns_BFnz-i?=dsASYUH%oh%3i$ESRKspqz&%q zo@aF#lQ#EB1E0ck?64GVrDVutr@J6{O2%<7UNGw9TS3*KQrT==j=9eo zcu7p+N#Sb-GSH{mHw=7P2wv)S1-&DwJ9x(}{gx9Ts(M3D zV+zkL?x&8V27L(Fd&p}F%l#H`*ep)78^&=3EzNix-;d&ZRmZY4Qv*N1 z4=G!l$BW5Zs6tysb2_v^_dQ$t8~8~SKd$OBm01IC;wn$noOOvud9`X?F^Uv_X5i=e z1+Q1LWt!~@^5t3u;;M+a_LV^Sm4RR5Hww{dvpAx6Pvnl z_nRE=o<$w12X1R!q_mDz0HvEmS=TyYmeSMCwZ*9-qPlY#c`$hQQ8*Li$8}mCu3on& zE|<^&xm4?`)q;U{ab2N-B_R!~wUT*VMY^2hylDN&Kp7Y0>CXmQv4W&KBr=vI)+LIF zclSzMo_keM9KmkVj@kgb2wIR4$=CR;@0@>8bXZ>k~{YvoJ4eWfZtW!Nw&I{s2r z2+5_9-%*a_JH~IhP4br%Kbtv9@muk`q5B$)xA_pl5`Ky?(1NA>G#mpg!*Y&D99Gc% zNL=x6fbTdp4Z;Xgn5%Ff?-65B9jz-ar z2_!Is)wqNX9q9%q3O7>5gOA_@BaZR38pTP*`vmXg6Nq|9W2Kj1=K^CnAsE%O1a%@iIO~<`^$tpd90 zBp_Fr8{?dC*^J0dJQhXU>eZ`)oW9JtSNNy!d3?bY&Sic__^!8x%MnVcJ?RnIA_mC? zd@%%x)G5GLy>=h7x}RO?0QGW6N7L>2^rGqYqUm*`Y2_O!IR|kJ>Lz-KIH+-ft9Z@% ziVO95I+e7yclY!X*H@inH%qd=ewF_P^Xn|G=^A7kJQ5(c-GkHa!D;uz+wO|Di7Nz{ zBTECJn`k4jJy8ZIy`V;)-^8~Vg`>}JYkkJJSUU00bzEUD`%YhkBK@vHU*lCI6O9Y_ zk&?#Q*U;UQXwZ>F6VV%JO+*q6i72C4z)!Qbc_-^eF?NyT z=w@q5Q5FLnAEqEgOoz42kQeWe7w-_e(c7AxAury2_$}8%Ngk95wO}K>FE>DrG z7?ZNnJgpf?`o~FML()HL`Wl76M5AayGq{{%^7jXF`p2tSk!(%$I8=7?&3mWBr6%;v z+iUeSl{b!!c#8jkI)i?k<-d2HX2>Q+SW!>1(Vk`JpT<;xUfZMB_UN^lSD}~x=+Q?@ zm_GYmQnzA@ro54FHsUXoSdd;>`B!gcrIH5`zD0%_JbE2sZ!rfk?_CsYNVg`Q8E6A; a5#{Ymf`#=yQxU@7^ydd!Dsmn8JMbTP%!h3N literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/DataService.class b/target/classes/com/crawlful/hub/service/DataService.class new file mode 100644 index 0000000000000000000000000000000000000000..c6feff3db5855e8cfb685c38e24c41efb425e015 GIT binary patch literal 9896 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 V@^Oy-gxt=tdOW;C4$IxB`)>erV6OlG literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/LogisticsRepository.class b/target/classes/com/crawlful/hub/service/LogisticsRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..c63375c108d4fc4420677d29dc234adc77307845 GIT binary patch literal 1120 zcmbVLyH3L}6umCAp*%`?7Z_L=Ktgx{mOv#K5TNi7>44bW#7$k2IC7j;9r!T@K7fxx zIH9D07N|NTk#ls<Gitq@v9%iVg0tP%92^FM`A$O+k9N3t!Zwivw*rDiN`bEe z(kDbvYB1kE>(;1Ge0gaj*xoVv0;Y@!_1({3kz_75lqnrS(PWNKWF#npixEnWje(#Z zx2fX$im6u>?UYQ7l&`65a{G?NaU>NXg35QZr@nnFY${^g2SYq$f*5>wjE6DGMvS8I zFK=rDwoR#8rve@QE&qG_AIB;j(Rm^4+1na0UK7N6QX_E_d37p=Uwx+SX@Gw(UrnO? z!98vANLcioxoXBL>74YZcRwCf1kV}4&yCQHv9@7^_GF}zk*exy_Yh2|C$F}8QthV( zQ;>tAI^|#*X4N;3U`AmG=Ae{9^9fYU#SoMecmWnucqt1nC-4faDw&wDSGAUCRFe04 HPjTY|Sh`nl literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/LogisticsService.class b/target/classes/com/crawlful/hub/service/LogisticsService.class new file mode 100644 index 0000000000000000000000000000000000000000..c8e53e01458aac970e98f398ffa6cd2803da8a01 GIT binary patch literal 6500 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 YU`e<&91t1KrA~3M_*L_Jy;1@E3&j4oH~;_u literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/MonitoringService.class b/target/classes/com/crawlful/hub/service/MonitoringService.class new file mode 100644 index 0000000000000000000000000000000000000000..29beb61775efa348ddf06b57e52debc6897e6917 GIT binary patch literal 5247 zcmd^CdvqLS75`0=W+&UhZl)o9A*QsHq-ncCsR6d6O%p;IcGE|iK(N(Lb|<^j?(VEJ zvuOiX9?GkNfQnl20gB>V@X@A#RH#Q0#kZ*VJSvJ-#0U5|$K&z$&0}}+IGp~0{$p~^ z&fGiqe&4;n`}^+qeUJb9frkNHh<`>LkSWXyaNuuNAZLEu(w%v?HN@y+O-P3AL?l!xHMc&5RyJEgBTeLA``| zNu#I_lnbM}wL=@t>k`zYnbz{dnq>&tSJpW>!$8d4V+x)*nSHRz}b!;bqm* zCe312K|5ASh>UCbvc6?Z!u;0$a3I1a5ycw3GJ?2-6;amVfeg|L7GR#b;~x)qL^o+f-d0~rQfipmeI{XG%KAG z1{W#l4F>L_PDUS*-xhv$`c}wvGO+o5PjzKPCht2NmqVkU)$Tz5ArtH(+#)zDa_T*3vn>&v3SDj$Xgv#9H@Hw3b^Gyal(it#LZ9-zH5h)5TWia+^CrkBr+1 zi|saJ@sR{8c+oKR9@0m+dPjjlf2Vr~De0dV81lO$%+<OGc^bbdm9XZe}IR%#_nXz%uS;ET)ywtx9ws6js`V*rzJG4>8TWnKf*O zMW|@+Wnu#GisGZ()GfVaT2;@FQ}qljEk2mI?;ByjALYnV>(*irw~`mBV26dQdK@qwq(qG z#XvL1Yu{*~E7x0kFVBwtQMr++_4o}SSc+d|l#HgxBRS6|f%a?M7^>QCvs`p!JVD|Y zwEf)`Yew@;x-(vDBGijyd|NPhJR52<-{n%U^X5LGGITnApC)c*zJ{P=M!o4d4Fl~K zEh&iSM?AE;J!;Unf}QNgbQY1gCa&hEVq5e2myDltu!3RRUN<}Lbu$Lwjo|hmZw{}c ziAfmI(*f4oz4l4fZ@H!0<+M0z@tRZB|BmisJc{X=s_uly(6jaCeQsy#3y7 z715CKXTB7SqKv$D#t~YRrj?D`C2^6>2SM=`}{~?DLA`Jk_g{;)#7i2a-2_z z&p9iPK)#Z7<^!eQ+0s0&{8j{0^rr|xO4kpbuDD9nuHHm$G6!$T_S~s-; z%;$TI0ng`u6YoL`umTIwhDE$?EXMg*!ejbrNK!Y17K{+P46RHogN5ao;1%g=dfZ0& z5WNIModk^O`w+=^8?L~WglYxKcs&jft`Tg)8*mk4$e;szIYJ#rT!jMP>TwOd1K>*_F6rsIYLVvq@TU$lnUIpe1 zw}xx@dp4SJE>rEmT3o>21QT4(+eW9G>`I2gAS%gTgtxlMcF@a9wv(aXP7rECi3uw& zvA0i8OuB_75Mw@DP3#W!oq|K^p;*-A!ky~-e0iQP-{s3qzWf1SUgXR7`10w#{9#{S z=F1=PBbHwlW8f%~7 zbGDVa(nVtzXWGq`sd3$nvOnoK2V-27S!_U#=;+u=G(@h=UPP5#`|+fkYjrgVjYqm4 z%*g02qAxP)t!DJYM(O!b!4UpJl=3q}2##ua@b@tHdL{TyvFN ccjNhKxeATQwFWP+7B%=A{?4b^o&Ev*2f93SO#lD@ literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/OrderRepository.class b/target/classes/com/crawlful/hub/service/OrderRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..59f21df9b395048fe71fe961aed3ae4d30ffe999 GIT binary patch literal 1801 zcmb_cU31bv6unCel-9PW)k^(n#H#hBJ3iz_iZ`Ss)maxY^L*ule8) z@JBh`4G^F}%{V?Jxx07o$Jv~Fe*OOO695k3Z4zPx_8U%@HUzt|n!ZKbew}(;^vnjQ zWnpkp;jZJE(h>b6EE7m~SdUSQ*)3YGcX&f4Awl4SBU;pRg=x2%f_3?gBfe3CNk%&^ zqasW}zq)LkkibUMw2k9_joZwYUkn0!`=xP#s#FQ{?*?1GG%Z>(J((wvs+ujENndaR z#RwsvN;4q4j=`+~#QYJaT#;cD=`$|dj&Z>h6v>50V0*gJf^AeK)<@6BIprf^xcaBS z2uKp%u(ixg(-#(#O-FPQt{a-!g~ujfo<{0667cqaN)_QMc4nXzndB8@?*igXP)(i8 z-(Jt*qOlN8z}QG>B0$Mhd|R4ber0-Q-Qoq?cJMcwj*T3orKt0F>2uLXw#R$S^6?Jp zRlTHtu4%4zQYoKl!NlX@`c$vzntL#s3GGnJg?8b*h^~gD7HFuY%kVM0H?tNv+_LCHW^kw7vmr_KEI5$ywbN@nJ1#ysxc zsTu?}M?(}QUadO5Xz&x$Lfl)o1oS{j8-e~rrGbX#7h}ImKmWp-i83Vzb@V8YN=;x1 zpE&A&0@XYTD-eSeu41qX_wXAhum(!#*I^@cZ3eDXOt~N(xbH7=KM33pVJnP&6hyBt z1<~jpW_S$Susp2&1Twh4hMn!;^VEZ9@EkX?s@E5T9pxOFyJJ(E%)N^tT2THaWbur` SGi>@5nlac5-miy6-~0jd#T?TB literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/OrderService.class b/target/classes/com/crawlful/hub/service/OrderService.class new file mode 100644 index 0000000000000000000000000000000000000000..2061cdfcdb66eb04d4127d641abc9cdca05994b6 GIT binary patch literal 11457 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 qF8nSKyWm54n{d+9&k+MGjl0!l5R-3x}lMX znG&*V!Z?Cjn>-U75bopahc~!ze0a1ofX*Pq}w3Jt|Yv2Ll`wV%TDEh`SNWh6-YezO%KG zX-Da{={QG!i~rvG`;iKV@;w>%%x#ry-%7$`;wmP1+VMemUg&u4{-0Y%FsptNPdOry zEaB9@iUbLIn({>XQLe zFr&W{2&Ofr)4^O0&1X>6NFk_Y@B%F6@KO<8&fpbT)iNnzuWBvRsAuo>p5n$Q3PDiF literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/PaymentService.class b/target/classes/com/crawlful/hub/service/PaymentService.class new file mode 100644 index 0000000000000000000000000000000000000000..bafea2b34fedc3609252b7230a3c8de0ae150c74 GIT binary patch literal 5758 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/ReportService.class b/target/classes/com/crawlful/hub/service/ReportService.class new file mode 100644 index 0000000000000000000000000000000000000000..eae050350e4dfebd39621408e52d1b80d71b5d92 GIT binary patch literal 11140 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 ArvLx| literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/UserRepository.class b/target/classes/com/crawlful/hub/service/UserRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..827709ce65d6239c83004c1623b6a0e5abb71f68 GIT binary patch literal 782 zcmbV~OHRWu5QfL4C2e^X%0ujtfDIN4R)I=@KmrAUC>+2|+|(tpBgbj!)mU%<4uzN` zeIcp}SR}D0Gyng2#vh+=?*MQHI~EiO?7J$Yu4et9odmR#v}nwAPq>^uN-bQR9NMN-s zq<1k$eI%9Dj^k{A(CN5lrbDox=_W)h98m%k`Eoy*=x>CKp7H%A>ool9@h zm|13jZ&kIa67BM95g>p4`*@Gi!xDI%Qq3o9JO7&Q{G(5~I0}c{u<;rZSiq-*ce99B z&4M!Ss<deX literal 0 HcmV?d00001 diff --git a/target/classes/com/crawlful/hub/service/UserService.class b/target/classes/com/crawlful/hub/service/UserService.class new file mode 100644 index 0000000000000000000000000000000000000000..4c35210d6ac5e481b5269b2f541344b51ab182fb GIT binary patch literal 3479 zcmb7GYg5!_6n@@iVV5N+MP0>Py)ANEx3y~3MHCV1Wkm#})m}9028d*nHJb%#?Y*6T z>qlq&7j~wDL!IgLQ#;c?(m&yJ`n*Xt$qFK6ha_*_lXIT)oadb6?|*;$9l$yKm_SV6 zNYN_iinh9B%+`$Dd~GII)$B#RsO4_+V$!=#ATH4HKwVUGhHB2`Zq7W=ijKgR8f_-D zidEGe%U%}fEo|s6G|dZiP1HkyS#n#QaSz9e@ zOP0Nmo6%IWnwwRNGFVOx9>@*X9BWCpwG!R74CTLKE5*v?3+2 zwV<2YSgkyx*;6Xx3Un5%qH0X5wl3HH&A2nK6G->Q3HkiUuti%_9eTUaQwWKgbYup3 z?@rCpjob~jl3$0VGEo{X<@D7$sn40JQ?u!A_KofaUl%V@s{GI!f+nz2V9%Q3g|Jdc zqg@_$E7*g*0&P{znbJ(vbgq^JcB})lcRGpv0!b+nlQnF^0US!;pg_CJP@NA2hjE1U zn$sw$?x-VAXM`Ry>fcJByFrm2(Gp*eg5EklDPb8oiGJd!sMRWURFYO%1;^@E2Yd0!ha` zQOAw_;ih`CB1fodMNgZ8kd8mvXb0I1`-D-hEOy5rT(?scB`ETm z28gIoQfMovqQbXLW37hGQW&eH=JuGtc$8c}L?ozNHE&&?U2iC&z~cXQxyg>$SUo8Y zi$@CX;I5pypDXx6R;#UOnU1QP)$1Bx)+sM-uhCoh{$X1QWHte~cUn^Wje;-n?Q2TU zS8r<4s@X;DvMyh;9g^-zZ#W&3f~1t!eaTnZ+nVKYVB~!BJPtCOCVsR~PICV7oZw2H zlRPJQ=Bu5@Hm-6!3!a<%oewW(fxK_$FRe7@( zuFEfwzVjTr# z>u$1T6mWwnC81#qH|cwvUX@_)7WXE2r!~e$Kx4_pB%Rl}xb1Rro@O!bwMnr3tC)^K zB8}3z&qb2<%mO5BJ`x$DS-3HTR3y!0p?>07a-5NV2UhWM3}OZMNW@t7PjqBgP$KE` zPtlxxCPHpH-G>g6nM2gYVg51^9x_4nOu$1123d)@W%hV588!*=5{DCHG{S+w4FL-k zALBK0A%&;7xfA5WrZvg;y5SFac0qV02(Ms23(`RKem9JRT0r%pq(lsI-ORlwef5xjK_|e9V8r3H6m<%SU`j z>MiY&D@E4s_utns$VuTfpC_v6tjZuI7Q48$@Ymo%9!U z*+JKGbEJAQ{fdgr6*KV&t`7&sK6so`$*kZ!zneh5nfz>q{U5v z>goB3*~o+F#Pag=(t<#9#L}@43~fW(2}f1a)Qrq1pGPNRv0IBvQ~T=G(d&x~)BER& zv3N?)=;pY9ySryaz%!XmXcedy7+kxn_CKHKUmfiKWZ*$ZZ~y(?t3?tX?+rNT`@6$^ zU!C*g80r+%3LHPervQSe7f?vRrk>IS`nzrStq^0Tp=UNmdyb5#5(13@c;TzSal)(( z?2gc`6gLN;NO7w`)!Mzp?$BW0(2#xA7C;4j0>>hHMq9|G*EM5VU6e^XH`N>1u%^na=f9jVLOWxn%6t*603~O zugWqlvCVSwJ#_e~FT)3EwXOx4N~`(VX1<~?lp=m0Lkeyvh?dMCyFmqN@(HxNNw!KX zfMwid`ps-!tD)QJ-Oewf8aLVGz z0=NB`$r@~<>7BT?DW@ozp`SUc&-YcG3eg_}QcZnVhZ%w3C|#@5Qw%e9IlxDt}HDxM~|%1?7J@ESG0 zpdr{~ttWyl)_U?MUb^rRziTYu3hQ|-0Zv&rAOI(`0~h8h0llGL(Z2uGX9<1QlYp&& zvp7e^N_3!;2Ba_7xyr|lp+6A(84aOV=z4|T7w~%a+97{V~`&s#d8 z9v^e#6_iQ?7mhM;;=KmyN(_vYH85fsxOkL-lkYXqSYqH(Sp%17;2V5`%Zz=Ldl$wm zs{ZCqCUdy=&SmeL5s!QPN3=J4+lJ>ueZS#~3olSDcatvs39ozaYqw}?^t^ock4N(8 zv3P7^fiUr?V~KR26v;MCDp6z%{A1{w zRS4x&(HdoyST8t|B&uw&*3fIjzmx1bzFhCB`S$w*#TukCFhI~SL565*xX6Llf&+eN zc)0IC7tB|XhbJXG@b7O)A`EOfJUr(r*=;UVggM^U z`V#)OlRuPA!wss>k%f8ExL}zN*#I;z=ZEZu9m~AjJ>yOiUc<5cgc;A*##{NfTDny2 R<~#5Os4M5-bAG$9`yZ(fhJOG6 literal 0 HcmV?d00001 diff --git a/target/classes/db/migration/V1__init_schema.sql b/target/classes/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..1210603 --- /dev/null +++ b/target/classes/db/migration/V1__init_schema.sql @@ -0,0 +1,130 @@ +-- 创建用户表 +CREATE TABLE IF NOT EXISTS cf_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + role VARCHAR(50), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建商品表 +CREATE TABLE IF NOT EXISTS cf_product ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + shop_id VARCHAR(255), + title VARCHAR(255) NOT NULL, + description TEXT, + main_image VARCHAR(255), + platform VARCHAR(50), + platform_product_id VARCHAR(255), + price DECIMAL(10,2), + cost_price DECIMAL(10,2), + quantity INT, + status VARCHAR(50), + phash VARCHAR(255), + semantic_hash VARCHAR(255), + vector_embedding TEXT, + attributes JSON, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建订单表 +CREATE TABLE IF NOT EXISTS cf_order ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + shop_id VARCHAR(255), + platform VARCHAR(50), + platform_order_id VARCHAR(255), + status VARCHAR(50), + total_amount DECIMAL(10,2), + currency VARCHAR(10), + customer_info JSON, + items JSON, + shipping_address JSON, + tracking_number VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建支付表 +CREATE TABLE IF NOT EXISTS cf_payment ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + order_id BIGINT, + payment_method VARCHAR(50), + amount DECIMAL(10,2), + currency VARCHAR(10), + status VARCHAR(50), + transaction_id VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (order_id) REFERENCES cf_order(id) +); + +-- 创建物流表 +CREATE TABLE IF NOT EXISTS cf_logistics ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + order_id BIGINT, + shipping_method VARCHAR(50), + tracking_number VARCHAR(255), + carrier VARCHAR(50), + status VARCHAR(50), + estimated_delivery_date DATETIME, + actual_delivery_date DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (order_id) REFERENCES cf_order(id) +); + +-- 创建配置表 +CREATE TABLE IF NOT EXISTS cf_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255), + shop_id VARCHAR(255), + config_key VARCHAR(255) NOT NULL, + config_value VARCHAR(255) NOT NULL, + config_type VARCHAR(50), + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建审计表 +CREATE TABLE IF NOT EXISTS cf_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255), + shop_id VARCHAR(255), + user_id BIGINT, + action VARCHAR(255), + resource_type VARCHAR(255), + resource_id VARCHAR(255), + ip_address VARCHAR(100), + user_agent TEXT, + details TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_user_tenant_id ON cf_user(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_tenant_id ON cf_product(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_platform ON cf_product(platform); +CREATE INDEX IF NOT EXISTS idx_order_tenant_id ON cf_order(tenant_id); +CREATE INDEX IF NOT EXISTS idx_order_platform ON cf_order(platform); +CREATE INDEX IF NOT EXISTS idx_payment_tenant_id ON cf_payment(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payment_order_id ON cf_payment(order_id); +CREATE INDEX IF NOT EXISTS idx_logistics_tenant_id ON cf_logistics(tenant_id); +CREATE INDEX IF NOT EXISTS idx_logistics_order_id ON cf_logistics(order_id); +CREATE INDEX IF NOT EXISTS idx_config_tenant_id ON cf_config(tenant_id); +CREATE INDEX IF NOT EXISTS idx_config_shop_id ON cf_config(shop_id); +CREATE INDEX IF NOT EXISTS idx_config_key ON cf_config(config_key); +CREATE INDEX IF NOT EXISTS idx_audit_tenant_id ON cf_audit(tenant_id); +CREATE INDEX IF NOT EXISTS idx_audit_shop_id ON cf_audit(shop_id); +CREATE INDEX IF NOT EXISTS idx_audit_user_id ON cf_audit(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_action ON cf_audit(action); +CREATE INDEX IF NOT EXISTS idx_audit_resource_type ON cf_audit(resource_type); +CREATE INDEX IF NOT EXISTS idx_audit_created_at ON cf_audit(created_at); diff --git a/target/classes/db/migration/V2__add_alert_table.sql b/target/classes/db/migration/V2__add_alert_table.sql new file mode 100644 index 0000000..63faf24 --- /dev/null +++ b/target/classes/db/migration/V2__add_alert_table.sql @@ -0,0 +1,21 @@ +-- 创建告警表 +CREATE TABLE IF NOT EXISTS cf_alert ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(255) NOT NULL, + alert_type VARCHAR(50), + severity VARCHAR(50), + message TEXT, + status VARCHAR(50), + source VARCHAR(255), + threshold VARCHAR(255), + actual_value VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME +); + +-- 为告警表添加索引 +CREATE INDEX IF NOT EXISTS idx_alert_tenant_id ON cf_alert(tenant_id); +CREATE INDEX IF NOT EXISTS idx_alert_status ON cf_alert(status); +CREATE INDEX IF NOT EXISTS idx_alert_severity ON cf_alert(severity); +CREATE INDEX IF NOT EXISTS idx_alert_alert_type ON cf_alert(alert_type); +CREATE INDEX IF NOT EXISTS idx_alert_created_at ON cf_alert(created_at); diff --git a/target/classes/i18n/messages.properties b/target/classes/i18n/messages.properties new file mode 100644 index 0000000..28e38f5 --- /dev/null +++ b/target/classes/i18n/messages.properties @@ -0,0 +1,62 @@ +# Authentication messages +auth.register.success=User registered successfully +auth.login.success=Login successful +auth.login.failure=Invalid username or password +auth.username.required=Username is required +auth.password.required=Password is required +auth.email.required=Email is required +auth.email.invalid=Invalid email format +auth.password.minlength=Password must be at least 6 characters +auth.username.minlength=Username must be at least 3 characters +auth.username.exists=Username already exists + +# Product messages +product.create.success=Product created successfully +product.update.success=Product updated successfully +product.delete.success=Product deleted successfully +product.not.found=Product not found +product.title.required=Product title is required +product.price.required=Product price is required +product.price.positive=Product price must be positive + +# Order messages +order.create.success=Order created successfully +order.update.success=Order updated successfully +order.not.found=Order not found +order.status.updated=Order status updated successfully + +# Payment messages +payment.create.success=Payment created successfully +payment.update.success=Payment updated successfully +payment.not.found=Payment not found +payment.status.updated=Payment status updated successfully + +# Logistics messages +logistics.create.success=Logistics created successfully +logistics.update.success=Logistics updated successfully +logistics.not.found=Logistics not found +logistics.status.updated=Logistics status updated successfully + +# Alert messages +alert.create.success=Alert created successfully +alert.update.success=Alert updated successfully +alert.resolve.success=Alert resolved successfully +alert.not.found=Alert not found + +# Monitoring messages +monitoring.health.ok=System health is OK +monitoring.health.error=System health check failed +monitoring.metrics.success=Performance metrics retrieved successfully +monitoring.services.success=Service status retrieved successfully +monitoring.database.success=Database status retrieved successfully +monitoring.cache.success=Cache status retrieved successfully +monitoring.stats.success=System stats retrieved successfully + +# Common messages +common.success=Operation successful +common.error=Operation failed +common.invalid.request=Invalid request +common.missing.parameter=Missing required parameter +common.not.found=Resource not found +common.access.denied=Access denied +common.server.error=Server internal error diff --git a/target/classes/i18n/messages_zh.properties b/target/classes/i18n/messages_zh.properties new file mode 100644 index 0000000..78a3c2d --- /dev/null +++ b/target/classes/i18n/messages_zh.properties @@ -0,0 +1,62 @@ +# Authentication messages +auth.register.success=用户注册成功 +auth.login.success=登录成功 +auth.login.failure=用户名或密码错误 +auth.username.required=用户名不能为空 +auth.password.required=密码不能为空 +auth.email.required=邮箱不能为空 +auth.email.invalid=邮箱格式错误 +auth.password.minlength=密码至少6个字符 +auth.username.minlength=用户名至少3个字符 +auth.username.exists=用户名已存在 + +# Product messages +product.create.success=商品创建成功 +product.update.success=商品更新成功 +product.delete.success=商品删除成功 +product.not.found=商品不存在 +product.title.required=商品标题不能为空 +product.price.required=商品价格不能为空 +product.price.positive=商品价格必须大于0 + +# Order messages +order.create.success=订单创建成功 +order.update.success=订单更新成功 +order.not.found=订单不存在 +order.status.updated=订单状态更新成功 + +# Payment messages +payment.create.success=支付创建成功 +payment.update.success=支付更新成功 +payment.not.found=支付不存在 +payment.status.updated=支付状态更新成功 + +# Logistics messages +logistics.create.success=物流创建成功 +logistics.update.success=物流更新成功 +logistics.not.found=物流不存在 +logistics.status.updated=物流状态更新成功 + +# Alert messages +alert.create.success=告警创建成功 +alert.update.success=告警更新成功 +alert.resolve.success=告警已解决 +alert.not.found=告警不存在 + +# Monitoring messages +monitoring.health.ok=系统健康状态良好 +monitoring.health.error=系统健康检查失败 +monitoring.metrics.success=性能指标获取成功 +monitoring.services.success=服务状态获取成功 +monitoring.database.success=数据库状态获取成功 +monitoring.cache.success=缓存状态获取成功 +monitoring.stats.success=系统统计信息获取成功 + +# Common messages +common.success=操作成功 +common.error=操作失败 +common.invalid.request=无效的请求 +common.missing.parameter=缺少必要参数 +common.not.found=资源不存在 +common.access.denied=访问被拒绝 +common.server.error=服务器内部错误 diff --git a/target/classes/logback.xml b/target/classes/logback.xml new file mode 100644 index 0000000..cb255d0 --- /dev/null +++ b/target/classes/logback.xml @@ -0,0 +1,83 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_HOME}/app.log + + ${LOG_HOME}/app-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_HOME}/error.log + + ${LOG_HOME}/error-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + ERROR + ACCEPT + DENY + + + + + + ${LOG_HOME}/business.log + + ${LOG_HOME}/business-%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/target/test-classes/com/crawlful/hub/integration/SystemIntegrationTest.class b/target/test-classes/com/crawlful/hub/integration/SystemIntegrationTest.class new file mode 100644 index 0000000000000000000000000000000000000000..998552873346144b9e26342d270e220a23a4acc2 GIT binary patch literal 6354 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 XU|vrzf%a?w+U5W>hAm=H!q@)*cV00v literal 0 HcmV?d00001 diff --git a/target/test-classes/com/crawlful/hub/service/ProductServiceTest.class b/target/test-classes/com/crawlful/hub/service/ProductServiceTest.class new file mode 100644 index 0000000000000000000000000000000000000000..1b7c25e692cac59cf20925aaf759e63d030740ff GIT binary patch literal 2127 zcmcJR-*3`T6vxj+Haf(yp%bT5yQveD*%zN=E^%`j6Q*YTNPH~i02fPdb9;;K#Ydm~ ze@rydcmF8kX{iFSI!IVZXwLodIp2Hk_Y{8o{PGO|9>Y!w5)AHJqHkEzd}p_P+vxgD z!$UdX78e5z16tI39ysMaYiP7uH*18%Ok{4a-r^NFIaYsR^T{LAD9v z3nw^6!1ZWuahE$>?J+o4C^i@*4@3)7u)-iaI{2ZKLTZp>>{H#5=m~p(Eu9YRaywM5 zlR7#^-+QE|mug+q`>48tmPj-eMwKlbneJ{?N$zw?!&2@Qb?)iD)511)h}afVSF~H5 zxTr_FqEk#@JE)>ruMh$Cxxsz6HKzGDz9LBV{F1cL#!+F$lgv<(G-TmY1~Rb5V69LY z-Pe#;4(>Bsp~}AW`%RQJlddSVmu1=wQ}Q5=29v7G$$C4}nkr}yGS7mCs_rt_3Juot z$w@}fcez4om@YT=k**XEO{-hh;5viN|J#S$L*1o_eK2ku1{?q2hcYzSWbpn}@)`M( zN*oA&KC+|+w;6nxNzdF39hI>@?qv<`GWa-?t~2X}NZ-@(HqhW6138nvxwH|3bXEA$ z!e>0_kNo6|C$QLqptV2;dP&kxNrFBLut-sgqTs28qf3w;8#^D)0d?mE8eOL7A?;@L zOn-&!o80PWxcEKXcR5@c%xpuRzB9vBa0S*W0=P^ayBSN< qV`%Z(wx*!nilG^?+U`t2+m4}?V`+scXvG-XehiKNI8db$Q2GV?SVbuS literal 0 HcmV?d00001