From b2d940e0144c98abba04ab23973386d58d5e02ff Mon Sep 17 00:00:00 2001 From: aiotagro Date: Wed, 10 Sep 2025 14:16:27 +0800 Subject: [PATCH] =?UTF-8?q?docs(deployment):=20=E6=9B=B4=E6=96=B0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E6=A1=A3=E5=B9=B6=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新了 DEPLOYMENT.md 文档,增加了更多部署细节和说明 - 添加了 Linux 和 Windows 平台的自动化部署脚本 - 更新了 README.md,增加了部署相关说明 - 调整了 .env 文件配置,以适应新的部署流程 - 移除了部分不必要的代码和配置 --- README.md | 45 +++ admin-system/.env.production | 12 +- admin-system/src/api/index.ts | 28 +- admin-system/src/api/mockData.ts | 187 ++++++++++ admin-system/src/api/system.ts | 22 +- admin-system/src/api/user.ts | 9 +- admin-system/src/config/mock.ts | 28 ++ admin-system/test-mock.js | 44 +++ backend-java/PERFORMANCE_OPTIMIZATION.md | 326 ++++++++++++++++++ backend-java/README.md | 159 +++++++++ backend-java/animal-service/pom.xml | 56 +++ .../jiebanke/animal/AnimalApplication.java | 19 + .../animal/controller/AnimalController.java | 138 ++++++++ .../com/jiebanke/animal/entity/Animal.java | 23 ++ .../jiebanke/animal/mapper/AnimalMapper.java | 37 ++ .../animal/service/AnimalService.java | 69 ++++ .../service/impl/AnimalServiceImpl.java | 156 +++++++++ .../src/main/resources/application.yml | 32 ++ backend-java/auth-service/pom.xml | 96 ++++++ .../com/jiebanke/auth/AuthApplication.java | 26 ++ .../auth/controller/AuthController.java | 187 ++++++++++ .../java/com/jiebanke/auth/entity/User.java | 23 ++ .../com/jiebanke/auth/mapper/UserMapper.java | 68 ++++ .../auth/service/AuthRabbitMQService.java | 46 +++ .../auth/service/AuthRedisService.java | 53 +++ .../com/jiebanke/auth/service/AuthResult.java | 11 + .../jiebanke/auth/service/AuthService.java | 61 ++++ .../auth/service/impl/AuthServiceImpl.java | 255 ++++++++++++++ .../java/com/jiebanke/auth/util/JwtUtil.java | 107 ++++++ .../src/main/resources/application.yml | 36 ++ .../auth/service/AuthRabbitMQServiceTest.java | 55 +++ .../auth/service/AuthRedisServiceTest.java | 112 ++++++ backend-java/build-services.sh | 44 +++ backend-java/common/pom.xml | 36 ++ .../jiebanke/common/config/FeignConfig.java | 22 ++ .../common/config/RabbitMQConfig.java | 102 ++++++ .../jiebanke/common/config/RedisConfig.java | 20 ++ .../jiebanke/common/entity/BaseEntity.java | 11 + .../common/exception/BusinessException.java | 23 ++ .../exception/GlobalExceptionHandler.java | 20 ++ .../com/jiebanke/common/vo/ApiResponse.java | 41 +++ .../jiebanke/common/config/FeignConfig.class | Bin 0 -> 1017 bytes .../common/config/RabbitMQConfig.class | Bin 0 -> 4809 bytes .../jiebanke/common/config/RedisConfig.class | Bin 0 -> 1155 bytes .../jiebanke/common/entity/BaseEntity.class | Bin 0 -> 2952 bytes .../common/exception/BusinessException.class | Bin 0 -> 2357 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 1541 bytes .../com/jiebanke/common/vo/ApiResponse.class | Bin 0 -> 4861 bytes backend-java/docker-compose.yml | 147 ++++++++ backend-java/eureka-server/pom.xml | 32 ++ .../eureka/EurekaServerApplication.java | 13 + .../src/main/resources/application.yml | 17 + .../target/classes/application.yml | 17 + .../eureka/EurekaServerApplication.class | Bin 0 -> 808 bytes backend-java/gateway-service/pom.xml | 54 +++ .../jiebanke/gateway/GatewayApplication.java | 34 ++ .../src/main/resources/application.yml | 38 ++ .../target/classes/application.yml | 38 ++ .../jiebanke/gateway/GatewayApplication.class | Bin 0 -> 3944 bytes backend-java/order-service/pom.xml | 56 +++ .../com/jiebanke/order/OrderApplication.java | 19 + .../order/controller/OrderController.java | 140 ++++++++ .../java/com/jiebanke/order/entity/Order.java | 18 + .../jiebanke/order/mapper/OrderMapper.java | 37 ++ .../jiebanke/order/service/OrderService.java | 70 ++++ .../order/service/impl/OrderServiceImpl.java | 159 +++++++++ .../src/main/resources/application.yml | 32 ++ backend-java/pom.xml | 163 +++++++++ backend-java/promotion-service/pom.xml | 56 +++ .../promotion/PromotionApplication.java | 19 + .../controller/PromotionController.java | 175 ++++++++++ .../promotion/entity/PromotionActivity.java | 22 ++ .../promotion/entity/RewardRecord.java | 22 ++ .../mapper/PromotionActivityMapper.java | 46 +++ .../promotion/mapper/RewardRecordMapper.java | 38 ++ .../promotion/service/PromotionService.java | 88 +++++ .../service/impl/PromotionServiceImpl.java | 181 ++++++++++ .../src/main/resources/application.yml | 32 ++ backend-java/scripts/init-database.sql | 137 ++++++++ backend-java/start-services.sh | 92 +++++ backend-java/stop-services.sh | 29 ++ backend-java/travel-service/pom.xml | 62 ++++ .../jiebanke/travel/TravelApplication.java | 19 + .../controller/TravelPlanController.java | 115 ++++++ .../jiebanke/travel/entity/TravelPlan.java | 22 ++ .../travel/mapper/TravelPlanMapper.java | 29 ++ .../travel/service/TravelPlanService.java | 60 ++++ .../jiebanke/travel/service/TravelStats.java | 14 + .../service/impl/TravelPlanServiceImpl.java | 148 ++++++++ .../src/main/resources/application.yml | 32 ++ backend-java/user-service/pom.xml | 74 ++++ .../com/jiebanke/user/UserApplication.java | 15 + .../user/client/AuthServiceClient.java | 14 + .../com/jiebanke/user/config/FeignConfig.java | 31 ++ .../user/service/UserRabbitMQService.java | 44 +++ .../user/service/UserRedisService.java | 44 +++ .../src/main/resources/application.yml | 25 ++ .../user/service/UserRabbitMQServiceTest.java | 53 +++ .../user/service/UserRedisServiceTest.java | 92 +++++ backend/.env | 57 ++- backend/README_SCRIPTS.md | 181 ++++++++++ backend/package.json | 12 +- backend/restart.sh | 107 ++++++ backend/scripts/test-database-connection.js | 10 +- backend/start.sh | 93 +++++ backend/status.sh | 90 +++++ backend/stop.sh | 72 ++++ docs/DEPLOYMENT.md | 298 +++++++++++++++- package-lock.json | 176 +--------- scripts/README.md | 104 ++++++ scripts/deploy.ps1 | 111 ++++++ scripts/deploy.sh | 127 +++++++ scripts/package.json | 25 +- website/package.json | 15 + 114 files changed, 6990 insertions(+), 247 deletions(-) create mode 100644 admin-system/src/api/mockData.ts create mode 100644 admin-system/src/config/mock.ts create mode 100644 admin-system/test-mock.js create mode 100644 backend-java/PERFORMANCE_OPTIMIZATION.md create mode 100644 backend-java/README.md create mode 100644 backend-java/animal-service/pom.xml create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/AnimalApplication.java create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/controller/AnimalController.java create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/entity/Animal.java create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/mapper/AnimalMapper.java create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/service/AnimalService.java create mode 100644 backend-java/animal-service/src/main/java/com/jiebanke/animal/service/impl/AnimalServiceImpl.java create mode 100644 backend-java/animal-service/src/main/resources/application.yml create mode 100644 backend-java/auth-service/pom.xml create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/AuthApplication.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/controller/AuthController.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/entity/User.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/mapper/UserMapper.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRabbitMQService.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRedisService.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthResult.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthService.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/service/impl/AuthServiceImpl.java create mode 100644 backend-java/auth-service/src/main/java/com/jiebanke/auth/util/JwtUtil.java create mode 100644 backend-java/auth-service/src/main/resources/application.yml create mode 100644 backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRabbitMQServiceTest.java create mode 100644 backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRedisServiceTest.java create mode 100755 backend-java/build-services.sh create mode 100644 backend-java/common/pom.xml create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/config/FeignConfig.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/config/RabbitMQConfig.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/config/RedisConfig.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/entity/BaseEntity.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/exception/BusinessException.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/exception/GlobalExceptionHandler.java create mode 100644 backend-java/common/src/main/java/com/jiebanke/common/vo/ApiResponse.java create mode 100644 backend-java/common/target/classes/com/jiebanke/common/config/FeignConfig.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/config/RabbitMQConfig.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/config/RedisConfig.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/entity/BaseEntity.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/exception/BusinessException.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/exception/GlobalExceptionHandler.class create mode 100644 backend-java/common/target/classes/com/jiebanke/common/vo/ApiResponse.class create mode 100644 backend-java/docker-compose.yml create mode 100644 backend-java/eureka-server/pom.xml create mode 100644 backend-java/eureka-server/src/main/java/com/jiebanke/eureka/EurekaServerApplication.java create mode 100644 backend-java/eureka-server/src/main/resources/application.yml create mode 100644 backend-java/eureka-server/target/classes/application.yml create mode 100644 backend-java/eureka-server/target/classes/com/jiebanke/eureka/EurekaServerApplication.class create mode 100644 backend-java/gateway-service/pom.xml create mode 100644 backend-java/gateway-service/src/main/java/com/jiebanke/gateway/GatewayApplication.java create mode 100644 backend-java/gateway-service/src/main/resources/application.yml create mode 100644 backend-java/gateway-service/target/classes/application.yml create mode 100644 backend-java/gateway-service/target/classes/com/jiebanke/gateway/GatewayApplication.class create mode 100644 backend-java/order-service/pom.xml create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/OrderApplication.java create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/controller/OrderController.java create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/entity/Order.java create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/mapper/OrderMapper.java create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/service/OrderService.java create mode 100644 backend-java/order-service/src/main/java/com/jiebanke/order/service/impl/OrderServiceImpl.java create mode 100644 backend-java/order-service/src/main/resources/application.yml create mode 100644 backend-java/pom.xml create mode 100644 backend-java/promotion-service/pom.xml create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/PromotionApplication.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/controller/PromotionController.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/PromotionActivity.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/RewardRecord.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/PromotionActivityMapper.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/RewardRecordMapper.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/PromotionService.java create mode 100644 backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/impl/PromotionServiceImpl.java create mode 100644 backend-java/promotion-service/src/main/resources/application.yml create mode 100644 backend-java/scripts/init-database.sql create mode 100755 backend-java/start-services.sh create mode 100755 backend-java/stop-services.sh create mode 100644 backend-java/travel-service/pom.xml create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/TravelApplication.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/controller/TravelPlanController.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/entity/TravelPlan.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/mapper/TravelPlanMapper.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelPlanService.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelStats.java create mode 100644 backend-java/travel-service/src/main/java/com/jiebanke/travel/service/impl/TravelPlanServiceImpl.java create mode 100644 backend-java/travel-service/src/main/resources/application.yml create mode 100644 backend-java/user-service/pom.xml create mode 100644 backend-java/user-service/src/main/java/com/jiebanke/user/UserApplication.java create mode 100644 backend-java/user-service/src/main/java/com/jiebanke/user/client/AuthServiceClient.java create mode 100644 backend-java/user-service/src/main/java/com/jiebanke/user/config/FeignConfig.java create mode 100644 backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRabbitMQService.java create mode 100644 backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRedisService.java create mode 100644 backend-java/user-service/src/main/resources/application.yml create mode 100644 backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRabbitMQServiceTest.java create mode 100644 backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRedisServiceTest.java create mode 100644 backend/README_SCRIPTS.md create mode 100755 backend/restart.sh create mode 100755 backend/start.sh create mode 100755 backend/status.sh create mode 100755 backend/stop.sh create mode 100644 scripts/README.md create mode 100644 scripts/deploy.ps1 create mode 100644 scripts/deploy.sh create mode 100644 website/package.json diff --git a/README.md b/README.md index a9af2a0..8c59e73 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ cd backend && npm run test-api # 数据库初始化 cd backend && npm run db:reset + +# 部署脚本 (Linux/Mac) +cd scripts && ./deploy.sh all + +# 部署脚本 (Windows PowerShell) +cd scripts && .\deploy.ps1 all ``` ### 环境配置 @@ -90,6 +96,45 @@ cp backend/.env.example backend/.env cp admin-system/.env.example admin-system/.env ``` +## ☁️ 部署 + +项目支持多种部署方式: + +### 自动部署脚本 +在 `scripts/` 目录中提供了自动部署脚本,支持 Linux/Mac 和 Windows 系统: + +```bash +# Linux/Mac 部署所有模块 +cd scripts && chmod +x deploy.sh && ./deploy.sh all + +# Windows PowerShell 部署所有模块 +cd scripts && .\deploy.ps1 all +``` + +支持的部署选项: +- `all` - 部署所有模块 +- `backend` - 部署后端服务 +- `admin` - 部署后台管理系统 +- `website` - 部署官方网站 +- `mini-program` - 构建微信小程序 + +### Docker 容器化部署 +每个模块都提供了 Docker 配置文件,可以使用 docker-compose 进行部署: + +```bash +# 启动所有服务 +docker-compose up -d + +# 启动指定服务 +docker-compose up -d backend + +# 查看服务状态 +docker-compose ps +``` + +### 手动部署 +每个模块也可以手动部署到服务器,具体说明请参考各模块目录中的 DEPLOYMENT.md 文件。 + ## 🌐 访问地址 - **后端API**: https://api.jiebanke.com diff --git a/admin-system/.env.production b/admin-system/.env.production index 384c8bd..dcd14dc 100644 --- a/admin-system/.env.production +++ b/admin-system/.env.production @@ -1,13 +1,11 @@ # 生产环境配置 -NODE_ENV=production +VITE_APP_NAME=结伴客后台管理系统 +VITE_APP_VERSION=1.0.0 # API配置 -VITE_API_BASE_URL=https://api.jiebanke.com/api/v1 -VITE_API_TIMEOUT=15000 +VITE_API_BASE_URL=https://api.jiebanke.com/api +VITE_API_TIMEOUT=10000 # 功能开关 VITE_FEATURE_ANALYTICS=true -VITE_FEATURE_DEBUG=false - -# 性能优化 -VITE_COMPRESSION=true \ No newline at end of file +VITE_FEATURE_DEBUG=false \ No newline at end of file diff --git a/admin-system/src/api/index.ts b/admin-system/src/api/index.ts index 5d05c18..96aeeae 100644 --- a/admin-system/src/api/index.ts +++ b/admin-system/src/api/index.ts @@ -1,11 +1,16 @@ import axios from 'axios' import { message } from 'ant-design-vue' import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import { mockAPI } from './mockData' +import { createMockWrapper } from '@/config/mock' // API基础配置 const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100/api' const timeout = parseInt(import.meta.env.VITE_API_TIMEOUT || '10000') +// 检查是否使用模拟数据(注释掉未使用的变量) +// const useMock = import.meta.env.VITE_USE_MOCK === 'true' || (!baseURL || baseURL.includes('localhost')) && import.meta.env.DEV + // 创建axios实例 const api: AxiosInstance = axios.create({ baseURL, @@ -116,8 +121,8 @@ export const request = { api.patch(url, data, config).then(res => res.data) } -// 认证相关API -export const authAPI = { +// 认证相关API(开发环境使用模拟数据) +export const authAPI = createMockWrapper({ // 管理员登录 login: (credentials: { username: string; password: string }) => request.post<{ @@ -149,15 +154,7 @@ export const authAPI = { // 退出登录 logout: () => request.post('/auth/logout') -} - -export * from './user' -export * from './merchant' -export * from './travel' -export * from './animal' -export * from './order' -export * from './promotion' -export * from './system' +}, mockAPI.auth) // 为避免命名冲突,单独导出模块 export { default as userAPI } from './user' @@ -168,4 +165,13 @@ export { default as orderAPI } from './order' export { default as promotionAPI } from './promotion' export { default as systemAPI } from './system' +// 重新导出特定类型以避免冲突 +export type { ApiResponse } from './user' +export type { Merchant } from './merchant' +export type { Travel } from './travel' +export type { Animal } from './animal' +export type { Order } from './order' +export type { Promotion } from './promotion' +export type { SystemStats } from './system' + export default api \ No newline at end of file diff --git a/admin-system/src/api/mockData.ts b/admin-system/src/api/mockData.ts new file mode 100644 index 0000000..b21e10f --- /dev/null +++ b/admin-system/src/api/mockData.ts @@ -0,0 +1,187 @@ +// 模拟数据服务 +import { message } from 'ant-design-vue' + +// 模拟延迟 +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +// 模拟用户数据 +const mockUsers = [ + { id: 1, username: 'admin', nickname: '系统管理员', role: 'admin', status: 'active', createdAt: '2024-01-01' }, + { id: 2, username: 'user1', nickname: '旅行爱好者', role: 'user', status: 'active', createdAt: '2024-01-02' }, + { id: 3, username: 'merchant1', nickname: '花店老板', role: 'merchant', status: 'active', createdAt: '2024-01-03' } +] + +// 模拟商家数据 +const mockMerchants = [ + { id: 1, name: '鲜花坊', type: 'flower', status: 'approved', contact: '13800138001', createdAt: '2024-01-05' }, + { id: 2, name: '快乐农场', type: 'farm', status: 'approved', contact: '13800138002', createdAt: '2024-01-06' } +] + +// 模拟旅行数据 +const mockTravels = [ + { id: 1, userId: 2, destination: '西藏', startDate: '2024-06-01', endDate: '2024-06-10', status: 'active', createdAt: '2024-01-10' }, + { id: 2, userId: 2, destination: '云南', startDate: '2024-07-01', endDate: '2024-07-07', status: 'active', createdAt: '2024-01-11' } +] + +// 模拟动物数据 +const mockAnimals = [ + { id: 1, name: '小白', type: 'sheep', merchantId: 2, status: 'available', price: 500, createdAt: '2024-01-15' }, + { id: 2, name: '小花', type: 'cow', merchantId: 2, status: 'claimed', price: 1000, createdAt: '2024-01-16' } +] + +// 模拟订单数据 +const mockOrders = [ + { id: 1, userId: 2, merchantId: 1, amount: 199, status: 'completed', createdAt: '2024-01-20' }, + { id: 2, userId: 2, merchantId: 1, amount: 299, status: 'pending', createdAt: '2024-01-21' } +] + +// 模拟API响应格式 +const createSuccessResponse = (data: any) => ({ + success: true, + data, + message: '操作成功' +}) + +// 模拟分页响应 +const createPaginatedResponse = (data: any[], page: number, pageSize: number, total: number) => ({ + success: true, + data: { + list: data, + pagination: { + current: page, + pageSize, + total, + totalPages: Math.ceil(total / pageSize) + } + } +}) + +// 模拟认证API +export const mockAuthAPI = { + login: async (credentials: { username: string; password: string }) => { + await delay(1000) + + if (credentials.username === 'admin' && credentials.password === 'admin123') { + const adminUser = mockUsers.find(u => u.username === 'admin') + return createSuccessResponse({ + token: 'mock-jwt-token-for-admin', + admin: adminUser + }) + } + + message.error('用户名或密码错误') + throw new Error('登录失败') + }, + + getCurrentUser: async () => { + await delay(500) + const adminUser = mockUsers.find(u => u.username === 'admin') + return createSuccessResponse({ + admin: adminUser + }) + } +} + +// 模拟用户API +export const mockUserAPI = { + getUsers: async (params: any = {}) => { + await delay(800) + const { page = 1, pageSize = 10 } = params + const start = (page - 1) * pageSize + const end = start + pageSize + const paginatedData = mockUsers.slice(start, end) + + return createPaginatedResponse(paginatedData, page, pageSize, mockUsers.length) + }, + + getUserById: async (id: number) => { + await delay(500) + const user = mockUsers.find(u => u.id === id) + if (user) { + return createSuccessResponse(user) + } + message.error('用户不存在') + throw new Error('用户不存在') + } +} + +// 模拟商家API +export const mockMerchantAPI = { + getMerchants: async (params: any = {}) => { + await delay(800) + const { page = 1, limit = 10 } = params + const start = (page - 1) * limit + const end = start + limit + const paginatedData = mockMerchants.slice(start, end) + + return createPaginatedResponse(paginatedData, page, limit, mockMerchants.length) + } +} + +// 模拟旅行API +export const mockTravelAPI = { + getTravels: async (params: any = {}) => { + await delay(800) + const { page = 1, limit = 10 } = params + const start = (page - 1) * limit + const end = start + limit + const paginatedData = mockTravels.slice(start, end) + + return createPaginatedResponse(paginatedData, page, limit, mockTravels.length) + } +} + +// 模拟动物API +export const mockAnimalAPI = { + getAnimals: async (params: any = {}) => { + await delay(800) + const { page = 1, limit = 10 } = params + const start = (page - 1) * limit + const end = start + limit + const paginatedData = mockAnimals.slice(start, end) + + return createPaginatedResponse(paginatedData, page, limit, mockAnimals.length) + } +} + +// 模拟订单API +export const mockOrderAPI = { + getOrders: async (params: any = {}) => { + await delay(800) + const { page = 1, limit = 10 } = params + const start = (page - 1) * limit + const end = start + limit + const paginatedData = mockOrders.slice(start, end) + + return createPaginatedResponse(paginatedData, page, limit, mockOrders.length) + } +} + +// 模拟系统统计API +export const mockSystemAPI = { + getSystemStats: async () => { + await delay(600) + return createSuccessResponse({ + userCount: mockUsers.length, + merchantCount: mockMerchants.length, + travelCount: mockTravels.length, + animalCount: mockAnimals.length, + orderCount: mockOrders.length, + todayUserCount: 5, + todayOrderCount: 3 + }) + } +} + +// 导出所有模拟API +export const mockAPI = { + auth: mockAuthAPI, + user: mockUserAPI, + merchant: mockMerchantAPI, + travel: mockTravelAPI, + animal: mockAnimalAPI, + order: mockOrderAPI, + system: mockSystemAPI +} + +export default mockAPI \ No newline at end of file diff --git a/admin-system/src/api/system.ts b/admin-system/src/api/system.ts index 7b03d85..af41a74 100644 --- a/admin-system/src/api/system.ts +++ b/admin-system/src/api/system.ts @@ -1,4 +1,6 @@ import { request } from '.' +import { mockSystemAPI } from './mockData' +import { createMockWrapper } from '@/config/mock' // 服务类型 export type ServiceType = 'database' | 'cache' | 'mq' @@ -88,9 +90,20 @@ export const getSystemConfigs = (params?: SystemConfigQueryParams) => export const updateSystemConfig = (id: string, data: SystemConfigUpdateData) => request.put<{ success: boolean; code: number; message: string }>(`/admin/system-configs/${id}`, data) +// 定义系统统计数据类型 +export interface SystemStats { + userCount: number + merchantCount: number + travelCount: number + animalCount: number + orderCount: number + todayUserCount: number + todayOrderCount: number +} + // 获取系统统计信息 export const getSystemStats = () => - request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/stats') + request.get<{ success: boolean; code: number; message: string; data: SystemStats }>('/admin/system/stats') // 获取系统日志 export const getSystemLogs = (params?: { page?: number; limit?: number; level?: string }) => @@ -104,7 +117,8 @@ export const getSystemSettings = () => export const updateSystemSettings = (data: any) => request.put<{ success: boolean; code: number; message: string }>(`/admin/system/settings`, data) -export default { +// 开发环境使用模拟数据 +const systemAPI = createMockWrapper({ getServices, updateServiceStatus, startService, @@ -118,4 +132,6 @@ export default { getSystemLogs, getSystemSettings, updateSystemSettings -} \ No newline at end of file +}, mockSystemAPI) + +export default systemAPI \ No newline at end of file diff --git a/admin-system/src/api/user.ts b/admin-system/src/api/user.ts index 08f2bc0..f58376b 100644 --- a/admin-system/src/api/user.ts +++ b/admin-system/src/api/user.ts @@ -1,4 +1,6 @@ import { request } from '.' +import { mockUserAPI } from './mockData' +import { createMockWrapper } from '@/config/mock' // 定义用户相关类型 export interface User { @@ -101,11 +103,14 @@ export const updateUserStatus = (id: number, status: string) => request.put>(`/users/${id}/status`, { status }) -export default { +// 开发环境使用模拟数据 +const userAPI = createMockWrapper({ getUsers, getUser, createUser, updateUser, deleteUser, batchUpdateUserStatus -} \ No newline at end of file +}, mockUserAPI) + +export default userAPI \ No newline at end of file diff --git a/admin-system/src/config/mock.ts b/admin-system/src/config/mock.ts new file mode 100644 index 0000000..d8c8efa --- /dev/null +++ b/admin-system/src/config/mock.ts @@ -0,0 +1,28 @@ +// 模拟数据配置 +import { mockAPI } from '@/api/mockData' + +// 检查是否启用模拟模式 +const isMockMode = import.meta.env.VITE_USE_MOCK === 'true' || !import.meta.env.VITE_API_BASE_URL + +// 模拟API包装器 +export const createMockWrapper = (realAPI: any, mockAPI: any) => { + if (isMockMode) { + console.log('🔧 使用模拟数据模式') + return mockAPI + } + return realAPI +} + +// 替换真实API为模拟API(开发环境) +if (isMockMode && import.meta.env.DEV) { + console.log('🚀 开发环境启用模拟数据') + + // 重写全局API对象 + const globalAPI = (window as any).$api = (window as any).$api || {} + Object.assign(globalAPI, mockAPI) +} + +export default { + isMockMode, + mockAPI +} \ No newline at end of file diff --git a/admin-system/test-mock.js b/admin-system/test-mock.js new file mode 100644 index 0000000..6dbdc48 --- /dev/null +++ b/admin-system/test-mock.js @@ -0,0 +1,44 @@ +// 测试模拟数据功能 +const mockAPI = require('./src/api/mockData.ts') + +console.log('🧪 测试模拟数据API...') + +// 测试登录功能 +console.log('\n1. 测试登录功能') +mockAPI.mockAuthAPI.login({ username: 'admin', password: 'admin123' }) + .then(response => { + console.log('✅ 登录成功:', response.data.admin.username) + return mockAPI.mockAuthAPI.getCurrentUser() + }) + .then(response => { + console.log('✅ 获取当前用户成功:', response.data.admin.nickname) + }) + .catch(error => { + console.log('❌ 登录测试失败:', error.message) + }) + +// 测试用户列表 +console.log('\n2. 测试用户列表') +mockAPI.mockUserAPI.getUsers({ page: 1, pageSize: 5 }) + .then(response => { + console.log('✅ 获取用户列表成功:', response.data.list.length + '个用户') + }) + .catch(error => { + console.log('❌ 用户列表测试失败:', error.message) + }) + +// 测试系统统计 +console.log('\n3. 测试系统统计') +mockAPI.mockSystemAPI.getSystemStats() + .then(response => { + console.log('✅ 获取系统统计成功:') + console.log(' - 用户数:', response.data.userCount) + console.log(' - 商家数:', response.data.merchantCount) + console.log(' - 旅行数:', response.data.travelCount) + console.log(' - 动物数:', response.data.animalCount) + }) + .catch(error => { + console.log('❌ 系统统计测试失败:', error.message) + }) + +console.log('\n🎉 模拟数据测试完成!') \ No newline at end of file diff --git a/backend-java/PERFORMANCE_OPTIMIZATION.md b/backend-java/PERFORMANCE_OPTIMIZATION.md new file mode 100644 index 0000000..7102712 --- /dev/null +++ b/backend-java/PERFORMANCE_OPTIMIZATION.md @@ -0,0 +1,326 @@ +# 结伴客Java后端性能优化指南 + +## 1. JVM调优 + +### 1.1 堆内存设置 +``` +# 堆内存大小设置(根据服务器配置调整) +-Xms512m +-Xmx2g + +# 新生代大小设置 +-Xmn256m + +# Metaspace大小设置 +-XX:MetaspaceSize=256m +-XX:MaxMetaspaceSize=512m +``` + +### 1.2 垃圾回收器选择 +``` +# G1垃圾回收器(适用于大堆内存) +-XX:+UseG1GC +-XX:MaxGCPauseMillis=200 +-XX:G1HeapRegionSize=16m + +# 或者CMS垃圾回收器(适用于低延迟要求) +-XX:+UseConcMarkSweepGC +-XX:+UseCMSInitiatingOccupancyOnly +-XX:CMSInitiatingOccupancyFraction=70 +``` + +## 2. 数据库连接池优化 + +### 2.1 HikariCP配置(在application.yml中) +```yaml +spring: + datasource: + hikari: + # 连接池大小 + maximum-pool-size: 20 + # 最小空闲连接数 + minimum-idle: 5 + # 连接超时时间 + connection-timeout: 30000 + # 空闲超时时间 + idle-timeout: 600000 + # 最大生命周期 + max-lifetime: 1800000 + # 连接测试查询 + connection-test-query: SELECT 1 +``` + +## 3. Redis性能优化 + +### 3.1 Redis连接池配置 +```yaml +spring: + redis: + lettuce: + pool: + # 最大连接数 + max-active: 20 + # 最大空闲连接数 + max-idle: 10 + # 最小空闲连接数 + min-idle: 5 + # 获取连接最大等待时间 + max-wait: 2000ms +``` + +### 3.2 Redis序列化优化 +```java +@Bean +public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // 使用更高效的序列化方式 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return template; +} +``` + +## 4. RabbitMQ性能优化 + +### 4.1 连接池配置 +```yaml +spring: + rabbitmq: + listener: + simple: + # 并发消费者数量 + concurrency: 5 + # 最大并发消费者数量 + max-concurrency: 20 + # 每个消费者预取的消息数量 + prefetch: 10 +``` + +## 5. Feign客户端优化 + +### 5.1 Feign配置 +```java +@Configuration +public class FeignConfig { + + @Bean + public Request.Options options() { + // 连接超时时间和读取超时时间 + return new Request.Options(5000, 10000); + } + + @Bean + public Retryer retryer() { + // 重试策略 + return new Retryer.Default(1000, 2000, 3); + } +} +``` + +## 6. 线程池优化 + +### 6.1 自定义线程池 +```java +@Configuration +public class ThreadPoolConfig { + + @Bean("taskExecutor") + public ExecutorService taskExecutor() { + return new ThreadPoolExecutor( + 10, // 核心线程数 + 20, // 最大线程数 + 60L, // 空闲线程存活时间 + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), // 任务队列 + new ThreadFactoryBuilder().setNameFormat("task-pool-%d").build(), + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 + ); + } +} +``` + +## 7. 缓存策略优化 + +### 7.1 多级缓存设计 +```java +@Service +public class UserService { + + @Autowired + private RedisTemplate redisTemplate; + + // 本地缓存(Caffeine) + private Cache localCache = Caffeine.newBuilder() + .maximumSize(1000) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + + public User getUserById(Long userId) { + // 1. 先查本地缓存 + User user = (User) localCache.getIfPresent("user:" + userId); + if (user != null) { + return user; + } + + // 2. 再查Redis缓存 + user = (User) redisTemplate.opsForValue().get("user:" + userId); + if (user != null) { + localCache.put("user:" + userId, user); + return user; + } + + // 3. 最后查数据库 + user = userMapper.selectById(userId); + if (user != null) { + redisTemplate.opsForValue().set("user:" + userId, user, 30, TimeUnit.MINUTES); + localCache.put("user:" + userId, user); + } + + return user; + } +} +``` + +## 8. 数据库查询优化 + +### 8.1 MyBatis-Plus分页优化 +```java +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + // 乐观锁插件 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + return interceptor; + } +} +``` + +### 8.2 索引优化建议 +```sql +-- 用户表索引优化 +CREATE INDEX idx_user_phone ON users(phone); +CREATE INDEX idx_user_email ON users(email); +CREATE INDEX idx_user_status ON users(status); + +-- 旅行表索引优化 +CREATE INDEX idx_travel_creator ON travels(creator_id); +CREATE INDEX idx_travel_status ON travels(status); +CREATE INDEX idx_travel_start_time ON travels(start_time); + +-- 动物表索引优化 +CREATE INDEX idx_animal_shelter ON animals(shelter_id); +CREATE INDEX idx_animal_status ON animals(status); + +-- 订单表索引优化 +CREATE INDEX idx_order_user ON orders(user_id); +CREATE INDEX idx_order_status ON orders(status); +CREATE INDEX idx_order_create_time ON orders(create_time); +``` + +## 9. API网关优化 + +### 9.1 限流配置 +```yaml +spring: + cloud: + gateway: + routes: + - id: user-service + uri: lb://user-service + predicates: + - Path=/api/users/** + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 +``` + +## 10. 监控和日志优化 + +### 10.1 Actuator配置 +```yaml +management: + endpoints: + web: + exposure: + include: health,info,metrics,httptrace + endpoint: + health: + show-details: always +``` + +### 10.2 日志配置优化 +```yaml +logging: + level: + com.jiebanke: INFO + org.springframework: WARN + org.mybatis: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + file: + name: logs/application.log +``` + +## 11. Docker部署优化 + +### 11.1 JVM参数优化(Dockerfile) +```dockerfile +FROM openjdk:17-jdk-slim + +LABEL maintainer="jiebanke-team" + +WORKDIR /app + +COPY target/*.jar app.jar + +# JVM参数优化 +ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200" + +EXPOSE 8080 + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] +``` + +## 12. 负载均衡优化 + +### 12.1 Ribbon配置 +```yaml +ribbon: + # 连接超时时间 + ConnectTimeout: 3000 + # 读取超时时间 + ReadTimeout: 10000 + # 是否启用重试 + OkToRetryOnAllOperations: false + # 切换实例重试次数 + MaxAutoRetriesNextServer: 1 + # 当前实例重试次数 + MaxAutoRetries: 0 +``` + +## 13. 性能测试建议 + +### 13.1 压力测试工具 +- JMeter:用于API接口压力测试 +- wrk:用于HTTP基准测试 +- ab (Apache Bench):简单的HTTP性能测试工具 + +### 13.2 监控工具 +- Prometheus + Grafana:系统指标监控 +- ELK Stack:日志分析 +- SkyWalking:分布式追踪 + +通过以上优化措施,可以显著提升结伴客Java后端服务的性能和稳定性。 \ No newline at end of file diff --git a/backend-java/README.md b/backend-java/README.md new file mode 100644 index 0000000..bddd082 --- /dev/null +++ b/backend-java/README.md @@ -0,0 +1,159 @@ +# 结伴客Java后端 + +结伴客Java微服务架构后端系统,基于Spring Boot和Spring Cloud实现。 + +## 系统架构 + +- **服务注册与发现**: Eureka Server +- **API网关**: Spring Cloud Gateway +- **认证服务**: Auth Service +- **用户服务**: User Service +- **旅行服务**: Travel Service +- **动物服务**: Animal Service +- **订单服务**: Order Service +- **推广服务**: Promotion Service + +## 环境要求 + +- **JDK**: Java 17 +- **构建工具**: Maven 3.6+ +- **数据库**: MySQL 8.0+ +- **缓存**: Redis 6.0+ +- **消息队列**: RabbitMQ 3.8+ + +## 环境安装 + +### 1. 安装Java 17 + +#### macOS (使用Homebrew) +```bash +brew install openjdk@17 +``` + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install openjdk-17-jdk +``` + +#### CentOS/RHEL +```bash +sudo yum install java-17-openjdk-devel +``` + +### 2. 安装Maven + +#### macOS (使用Homebrew) +```bash +brew install maven +``` + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install maven +``` + +#### CentOS/RHEL +```bash +sudo yum install maven +``` + +### 3. 验证安装 + +```bash +java -version +mvn -version +``` + +应该看到类似以下输出: +``` +openjdk version "17.0.8" 2023-07-18 +Apache Maven 3.8.6 +``` + +## 数据库配置 + +1. 安装MySQL 8.0+ +2. 创建数据库: + ```sql + CREATE DATABASE jiebanke CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + ``` +3. 执行初始化脚本: + ```bash + mysql -u root -p jiebanke < scripts/init-database.sql + ``` + +## 缓存和消息队列 + +1. 安装Redis 6.0+ +2. 安装RabbitMQ 3.8+ + +## 构建项目 + +```bash +# 进入项目目录 +cd backend-java + +# 清理并构建项目 +./build-services.sh +``` + +## 运行服务 + +### 方式一:使用启动脚本(推荐) + +```bash +# 进入项目目录 +cd backend-java + +# 启动所有服务 +./start-services.sh + +# 停止所有服务 +./stop-services.sh +``` + +### 方式二:手动启动 + +1. 启动Eureka Server: + ```bash + cd eureka-server + mvn spring-boot:run + ``` + +2. 等待Eureka启动完成后,依次启动其他服务: + ```bash + # 在新的终端窗口中启动Auth Service + cd auth-service + mvn spring-boot:run + + # 在新的终端窗口中启动User Service + cd user-service + mvn spring-boot:run + + # 以此类推启动其他服务... + ``` + +## 访问服务 + +- **Eureka Dashboard**: http://localhost:8761 +- **API Gateway**: http://localhost:8080 +- **API文档**: http://localhost:8080/doc.html + +## 服务端口 + +| 服务名称 | 端口 | +|---------|------| +| Eureka Server | 8761 | +| API Gateway | 8080 | +| Auth Service | 8081 | +| User Service | 8082 | +| Travel Service | 8083 | +| Animal Service | 8084 | +| Order Service | 8085 | +| Promotion Service | 8086 | + +## 性能优化 + +详细的性能优化指南请参考 [PERFORMANCE_OPTIMIZATION.md](PERFORMANCE_OPTIMIZATION.md) 文件。 \ No newline at end of file diff --git a/backend-java/animal-service/pom.xml b/backend-java/animal-service/pom.xml new file mode 100644 index 0000000..f55fd0c --- /dev/null +++ b/backend-java/animal-service/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + animal-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/AnimalApplication.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/AnimalApplication.java new file mode 100644 index 0000000..e3e4661 --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/AnimalApplication.java @@ -0,0 +1,19 @@ +package com.jiebanke.animal; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.jiebanke.animal.mapper") +@ComponentScan(basePackages = "com.jiebanke") +public class AnimalApplication { + public static void main(String[] args) { + SpringApplication.run(AnimalApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/controller/AnimalController.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/controller/AnimalController.java new file mode 100644 index 0000000..76e8c23 --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/controller/AnimalController.java @@ -0,0 +1,138 @@ +package com.jiebanke.animal.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.jiebanke.animal.entity.Animal; +import com.jiebanke.animal.service.AnimalService; +import com.jiebanke.common.vo.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/animals") +public class AnimalController { + + @Autowired + private AnimalService animalService; + + /** + * 搜索动物(公开接口) + */ + @GetMapping("/search") + public ApiResponse> searchAnimals( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String species, + @RequestParam(required = false) Double minPrice, + @RequestParam(required = false) Double maxPrice, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize) { + + IPage animals = animalService.searchAnimals(keyword, species, minPrice, maxPrice, page, pageSize); + + Map result = new HashMap<>(); + result.put("animals", animals.getRecords()); + result.put("pagination", Map.of( + "page", animals.getCurrent(), + "pageSize", animals.getSize(), + "total", animals.getTotal(), + "totalPages", animals.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 获取动物列表(商家) + */ + @GetMapping + public ApiResponse> getAnimals( + @RequestHeader("userId") Long merchantId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String species, + @RequestParam(required = false) String status) { + + IPage animals = animalService.getAnimals(merchantId, page, pageSize, species, status); + + Map result = new HashMap<>(); + result.put("animals", animals.getRecords()); + result.put("pagination", Map.of( + "page", animals.getCurrent(), + "pageSize", animals.getSize(), + "total", animals.getTotal(), + "totalPages", animals.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 获取动物统计信息 + */ + @GetMapping("/stats") + public ApiResponse> getAnimalStatistics() { + Map statistics = animalService.getAnimalStatistics(); + return ApiResponse.success(statistics); + } + + /** + * 获取单个动物详情 + */ + @GetMapping("/{animalId}") + public ApiResponse getAnimal(@PathVariable Long animalId) { + Animal animal = animalService.getAnimalById(animalId); + return ApiResponse.success(animal); + } + + /** + * 创建动物信息 + */ + @PostMapping + public ApiResponse> createAnimal( + @RequestHeader("userId") Long merchantId, + @RequestBody Animal animal) { + + animal.setMerchantId(merchantId); + Long animalId = animalService.createAnimal(animal); + Animal createdAnimal = animalService.getAnimalById(animalId); + + Map result = new HashMap<>(); + result.put("animal", createdAnimal); + result.put("message", "动物信息创建成功"); + + return ApiResponse.success(result); + } + + /** + * 更新动物信息 + */ + @PutMapping("/{animalId}") + public ApiResponse> updateAnimal( + @PathVariable Long animalId, + @RequestBody Animal animal) { + + Animal updatedAnimal = animalService.updateAnimal(animalId, animal); + + Map result = new HashMap<>(); + result.put("animal", updatedAnimal); + result.put("message", "动物信息更新成功"); + + return ApiResponse.success(result); + } + + /** + * 删除动物信息 + */ + @DeleteMapping("/{animalId}") + public ApiResponse> deleteAnimal(@PathVariable Long animalId) { + boolean deleted = animalService.deleteAnimal(animalId); + + Map result = new HashMap<>(); + result.put("message", "动物信息删除成功"); + result.put("animalId", animalId); + + return ApiResponse.success(result); + } +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/entity/Animal.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/entity/Animal.java new file mode 100644 index 0000000..a080efe --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/entity/Animal.java @@ -0,0 +1,23 @@ +package com.jiebanke.animal.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Animal extends BaseEntity { + private Long merchantId; + private String name; + private String species; + private String breed; + private LocalDate birthDate; + private String personality; + private String farmLocation; + private BigDecimal price; + private Integer claimCount; + private String status; +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/mapper/AnimalMapper.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/mapper/AnimalMapper.java new file mode 100644 index 0000000..6fe424e --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/mapper/AnimalMapper.java @@ -0,0 +1,37 @@ +package com.jiebanke.animal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.animal.entity.Animal; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface AnimalMapper extends BaseMapper { + + /** + * 根据商家ID获取动物列表 + * @param merchantId 商家ID + * @return 动物列表 + */ + @Select("SELECT * FROM animals WHERE merchant_id = #{merchantId} ORDER BY created_at DESC") + List selectByMerchantId(@Param("merchantId") Long merchantId); + + /** + * 根据物种获取动物列表 + * @param species 物种 + * @return 动物列表 + */ + @Select("SELECT * FROM animals WHERE species = #{species} ORDER BY created_at DESC") + List selectBySpecies(@Param("species") String species); + + /** + * 根据状态获取动物列表 + * @param status 状态 + * @return 动物列表 + */ + @Select("SELECT * FROM animals WHERE status = #{status} ORDER BY created_at DESC") + List selectByStatus(@Param("status") String status); +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/AnimalService.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/AnimalService.java new file mode 100644 index 0000000..9b9d569 --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/AnimalService.java @@ -0,0 +1,69 @@ +package com.jiebanke.animal.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.jiebanke.animal.entity.Animal; + +import java.util.List; +import java.util.Map; + +public interface AnimalService extends IService { + + /** + * 获取动物列表 + * @param merchantId 商家ID + * @param page 页码 + * @param pageSize 每页数量 + * @param species 物种 + * @param status 状态 + * @return 动物分页列表 + */ + IPage getAnimals(Long merchantId, Integer page, Integer pageSize, String species, String status); + + /** + * 获取单个动物详情 + * @param animalId 动物ID + * @return 动物信息 + */ + Animal getAnimalById(Long animalId); + + /** + * 创建动物 + * @param animal 动物信息 + * @return 创建的动物ID + */ + Long createAnimal(Animal animal); + + /** + * 更新动物信息 + * @param animalId 动物ID + * @param animal 更新的动物信息 + * @return 更新后的动物信息 + */ + Animal updateAnimal(Long animalId, Animal animal); + + /** + * 删除动物 + * @param animalId 动物ID + * @return 是否删除成功 + */ + boolean deleteAnimal(Long animalId); + + /** + * 获取动物统计信息 + * @return 统计信息 + */ + Map getAnimalStatistics(); + + /** + * 搜索动物 + * @param keyword 关键词 + * @param species 物种 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param page 页码 + * @param pageSize 每页数量 + * @return 动物分页列表 + */ + IPage searchAnimals(String keyword, String species, Double minPrice, Double maxPrice, Integer page, Integer pageSize); +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/impl/AnimalServiceImpl.java b/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/impl/AnimalServiceImpl.java new file mode 100644 index 0000000..89b9559 --- /dev/null +++ b/backend-java/animal-service/src/main/java/com/jiebanke/animal/service/impl/AnimalServiceImpl.java @@ -0,0 +1,156 @@ +package com.jiebanke.animal.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jiebanke.animal.entity.Animal; +import com.jiebanke.animal.mapper.AnimalMapper; +import com.jiebanke.animal.service.AnimalService; +import com.jiebanke.common.exception.BusinessException; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class AnimalServiceImpl extends ServiceImpl implements AnimalService { + + @Override + public IPage getAnimals(Long merchantId, Integer page, Integer pageSize, String species, String status) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (merchantId != null) { + queryWrapper.eq("merchant_id", merchantId); + } + + if (species != null && !species.isEmpty()) { + queryWrapper.eq("species", species); + } + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } + + @Override + public Animal getAnimalById(Long animalId) { + Animal animal = this.getById(animalId); + if (animal == null) { + throw new BusinessException("动物不存在"); + } + return animal; + } + + @Override + public Long createAnimal(Animal animal) { + this.save(animal); + return animal.getId(); + } + + @Override + public Animal updateAnimal(Long animalId, Animal animal) { + Animal existingAnimal = this.getById(animalId); + if (existingAnimal == null) { + throw new BusinessException("动物不存在"); + } + + // 更新字段 + if (animal.getName() != null) { + existingAnimal.setName(animal.getName()); + } + if (animal.getSpecies() != null) { + existingAnimal.setSpecies(animal.getSpecies()); + } + if (animal.getBreed() != null) { + existingAnimal.setBreed(animal.getBreed()); + } + if (animal.getBirthDate() != null) { + existingAnimal.setBirthDate(animal.getBirthDate()); + } + if (animal.getPersonality() != null) { + existingAnimal.setPersonality(animal.getPersonality()); + } + if (animal.getFarmLocation() != null) { + existingAnimal.setFarmLocation(animal.getFarmLocation()); + } + if (animal.getPrice() != null) { + existingAnimal.setPrice(animal.getPrice()); + } + if (animal.getStatus() != null) { + existingAnimal.setStatus(animal.getStatus()); + } + + this.updateById(existingAnimal); + return existingAnimal; + } + + @Override + public boolean deleteAnimal(Long animalId) { + return this.removeById(animalId); + } + + @Override + public Map getAnimalStatistics() { + // 获取动物总数 + int total = Math.toIntExact(this.count()); + + // 按物种统计 + QueryWrapper speciesWrapper = new QueryWrapper<>(); + speciesWrapper.select("species", "COUNT(*) as count"); + speciesWrapper.groupBy("species"); + List> speciesStats = this.listMaps(speciesWrapper); + + // 按状态统计 + QueryWrapper statusWrapper = new QueryWrapper<>(); + statusWrapper.select("status", "COUNT(*) as count"); + statusWrapper.groupBy("status"); + List> statusStats = this.listMaps(statusWrapper); + + // 构建返回结果 + Map statistics = new HashMap<>(); + statistics.put("total", total); + statistics.put("bySpecies", speciesStats); + statistics.put("byStatus", statusStats); + + return statistics; + } + + @Override + public IPage searchAnimals(String keyword, String species, Double minPrice, Double maxPrice, Integer page, Integer pageSize) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("status", "available"); + + if (keyword != null && !keyword.isEmpty()) { + queryWrapper.and(wrapper -> wrapper + .like("name", keyword) + .or() + .like("personality", keyword) + .or() + .like("species", keyword)); + } + + if (species != null && !species.isEmpty()) { + queryWrapper.eq("species", species); + } + + if (minPrice != null) { + queryWrapper.ge("price", minPrice); + } + + if (maxPrice != null) { + queryWrapper.le("price", maxPrice); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } +} \ No newline at end of file diff --git a/backend-java/animal-service/src/main/resources/application.yml b/backend-java/animal-service/src/main/resources/application.yml new file mode 100644 index 0000000..4566c3e --- /dev/null +++ b/backend-java/animal-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8084 + +spring: + application: + name: animal-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto \ No newline at end of file diff --git a/backend-java/auth-service/pom.xml b/backend-java/auth-service/pom.xml new file mode 100644 index 0000000..a885701 --- /dev/null +++ b/backend-java/auth-service/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + auth-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + org.springframework.security + spring-security-crypto + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-amqp + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/AuthApplication.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/AuthApplication.java new file mode 100644 index 0000000..319862b --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/AuthApplication.java @@ -0,0 +1,26 @@ +package com.jiebanke.auth; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.jiebanke.auth.mapper") +@ComponentScan(basePackages = "com.jiebanke") +public class AuthApplication { + public static void main(String[] args) { + SpringApplication.run(AuthApplication.class, args); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/controller/AuthController.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/controller/AuthController.java new file mode 100644 index 0000000..cdda871 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/controller/AuthController.java @@ -0,0 +1,187 @@ +package com.jiebanke.auth.controller; + +import com.jiebanke.auth.service.AuthRedisService; +import com.jiebanke.common.vo.ApiResponse; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @Value("${jwt.secret}") + private String jwtSecret; + + @Autowired + private AuthRedisService authRedisService; + + /** + * 用户注册 + */ + @PostMapping("/register") + public ApiResponse register(@RequestBody RegisterRequest request) { + User user = new User(); + user.setUsername(request.getUsername()); + user.setEmail(request.getEmail()); + user.setPhone(request.getPhone()); + user.setRealName(request.getNickname()); + + AuthResult result = authService.register(user, request.getPassword()); + return ApiResponse.success(result); + } + + /** + * 用户登录 + */ + @PostMapping("/login") + public ApiResponse login(@RequestBody LoginRequest request) { + AuthResult result = authService.login(request.getUsername(), request.getPassword()); + return ApiResponse.success(result); + } + + /** + * 获取当前用户信息 + */ + @GetMapping("/me") + public ApiResponse getCurrentUser(@RequestHeader("userId") Long userId) { + User user = authService.getCurrentUser(userId); + return ApiResponse.success(user); + } + + /** + * 更新用户个人信息 + */ + @PutMapping("/profile") + public ApiResponse updateProfile( + @RequestHeader("userId") Long userId, + @RequestBody User user) { + User updatedUser = authService.updateProfile(userId, user); + return ApiResponse.success(updatedUser); + } + + /** + * 修改密码 + */ + @PutMapping("/password") + public ApiResponse> changePassword( + @RequestHeader("userId") Long userId, + @RequestBody ChangePasswordRequest request) { + boolean success = authService.changePassword(userId, request.getCurrentPassword(), request.getNewPassword()); + + Map result = new HashMap<>(); + result.put("message", success ? "密码修改成功" : "密码修改失败"); + return ApiResponse.success(result); + } + + /** + * 微信登录/注册 + */ + @PostMapping("/wechat") + public ApiResponse wechatLogin(@RequestBody WechatLoginRequest request) { + AuthResult result = authService.wechatLogin(request.getCode(), request.getUserInfo()); + return ApiResponse.success(result); + } + + /** + * 管理员登录 + */ + @PostMapping("/admin/login") + public ApiResponse adminLogin(@RequestBody LoginRequest request) { + AuthResult result = authService.adminLogin(request.getUsername(), request.getPassword()); + return ApiResponse.success(result); + } + + @GetMapping("/validate") + public ApiResponse validateToken(@RequestHeader("Authorization") String token) { + try { + // 移除 "Bearer " 前缀 + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + // 解析JWT令牌 + Claims claims = Jwts.parserBuilder() + .setSigningKey(jwtSecret.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + + // 从Redis中获取用户登录状态 + Long userId = authRedisService.getUserLoginStatus(token); + if (userId != null && userId.equals(claims.get("userId", Long.class))) { + return ApiResponse.success(true); + } + + return ApiResponse.success(false); + } catch (Exception e) { + return ApiResponse.success(false); + } + } + + // 请求体类 + static class RegisterRequest { + private String username; + private String password; + private String nickname; + private String email; + private String phone; + + // Getters and setters + 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 getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + } + + static class LoginRequest { + private String username; + private String password; + + // Getters and setters + 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; } + } + + static class ChangePasswordRequest { + private String currentPassword; + private String newPassword; + + // Getters and setters + public String getCurrentPassword() { return currentPassword; } + public void setCurrentPassword(String currentPassword) { this.currentPassword = currentPassword; } + + public String getNewPassword() { return newPassword; } + public void setNewPassword(String newPassword) { this.newPassword = newPassword; } + } + + static class WechatLoginRequest { + private String code; + private Object userInfo; + + // Getters and setters + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + + public Object getUserInfo() { return userInfo; } + public void setUserInfo(Object userInfo) { this.userInfo = userInfo; } + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/entity/User.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/entity/User.java new file mode 100644 index 0000000..a0258a8 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/entity/User.java @@ -0,0 +1,23 @@ +package com.jiebanke.auth.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +public class User extends BaseEntity { + private String username; + private String password; + private String email; + private String phone; + private String realName; + private String idCard; + private String status; + private BigDecimal balance; + private Integer creditScore; + private LocalDateTime lastLogin; +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/mapper/UserMapper.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/mapper/UserMapper.java new file mode 100644 index 0000000..6a50446 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/mapper/UserMapper.java @@ -0,0 +1,68 @@ +package com.jiebanke.auth.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.auth.entity.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +@Mapper +public interface UserMapper extends BaseMapper { + + /** + * 根据用户名查找用户 + * @param username 用户名 + * @return 用户 + */ + @Select("SELECT * FROM users WHERE username = #{username}") + User selectByUsername(@Param("username") String username); + + /** + * 根据邮箱查找用户 + * @param email 邮箱 + * @return 用户 + */ + @Select("SELECT * FROM users WHERE email = #{email}") + User selectByEmail(@Param("email") String email); + + /** + * 根据手机号查找用户 + * @param phone 手机号 + * @return 用户 + */ + @Select("SELECT * FROM users WHERE phone = #{phone}") + User selectByPhone(@Param("phone") String phone); + + /** + * 检查用户名是否存在 + * @param username 用户名 + * @return 是否存在 + */ + @Select("SELECT COUNT(*) FROM users WHERE username = #{username}") + int existsByUsername(@Param("username") String username); + + /** + * 检查邮箱是否存在 + * @param email 邮箱 + * @return 是否存在 + */ + @Select("SELECT COUNT(*) FROM users WHERE email = #{email}") + int existsByEmail(@Param("email") String email); + + /** + * 检查手机号是否存在 + * @param phone 手机号 + * @return 是否存在 + */ + @Select("SELECT COUNT(*) FROM users WHERE phone = #{phone}") + int existsByPhone(@Param("phone") String phone); + + /** + * 更新最后登录时间 + * @param id 用户ID + * @return 更新记录数 + */ + @Update("UPDATE users SET last_login = NOW(), updated_at = NOW() WHERE id = #{id}") + int updateLastLogin(@Param("id") Long id); +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRabbitMQService.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRabbitMQService.java new file mode 100644 index 0000000..cb55afe --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRabbitMQService.java @@ -0,0 +1,46 @@ +package com.jiebanke.auth.service; + +import com.jiebanke.common.config.RabbitMQConfig; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class AuthRabbitMQService { + + @Autowired + private RabbitTemplate rabbitTemplate; + + // 发送登录成功消息 + public void sendLoginSuccessMessage(Long userId, String username, String ip) { + Map message = new HashMap<>(); + message.put("userId", userId); + message.put("username", username); + message.put("ip", ip); + message.put("eventType", "USER_LOGIN_SUCCESS"); + + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + message + ); + } + + // 发送登录失败消息 + public void sendLoginFailureMessage(String username, String ip, String reason) { + Map message = new HashMap<>(); + message.put("username", username); + message.put("ip", ip); + message.put("reason", reason); + message.put("eventType", "USER_LOGIN_FAILURE"); + + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + message + ); + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRedisService.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRedisService.java new file mode 100644 index 0000000..79935f7 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthRedisService.java @@ -0,0 +1,53 @@ +package com.jiebanke.auth.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +public class AuthRedisService { + + @Autowired + private RedisTemplate redisTemplate; + + // 缓存验证码 + public void cacheVerificationCode(String phone, String code) { + redisTemplate.opsForValue().set("verification:code:" + phone, code, 5, TimeUnit.MINUTES); + } + + // 获取缓存的验证码 + public String getCachedVerificationCode(String phone) { + return (String) redisTemplate.opsForValue().get("verification:code:" + phone); + } + + // 删除缓存的验证码 + public void removeCachedVerificationCode(String phone) { + redisTemplate.delete("verification:code:" + phone); + } + + // 缓存登录失败次数 + public void cacheLoginFailures(String identifier, Integer failures) { + redisTemplate.opsForValue().set("login:failures:" + identifier, failures, 1, TimeUnit.HOURS); + } + + // 获取登录失败次数 + public Integer getLoginFailures(String identifier) { + return (Integer) redisTemplate.opsForValue().get("login:failures:" + identifier); + } + + // 增加登录失败次数 + public void incrementLoginFailures(String identifier) { + Integer failures = getLoginFailures(identifier); + if (failures == null) { + failures = 0; + } + redisTemplate.opsForValue().set("login:failures:" + identifier, failures + 1, 1, TimeUnit.HOURS); + } + + // 清除登录失败次数 + public void clearLoginFailures(String identifier) { + redisTemplate.delete("login:failures:" + identifier); + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthResult.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthResult.java new file mode 100644 index 0000000..3ee6a58 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthResult.java @@ -0,0 +1,11 @@ +package com.jiebanke.auth.service; + +import com.jiebanke.auth.entity.User; +import lombok.Data; + +@Data +public class AuthResult { + private User user; + private String token; + private String message; +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthService.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthService.java new file mode 100644 index 0000000..fa87491 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/AuthService.java @@ -0,0 +1,61 @@ +package com.jiebanke.auth.service; + +import com.jiebanke.auth.entity.User; + +public interface AuthService { + + /** + * 用户注册 + * @param user 用户信息 + * @return 注册后的用户信息和Token + */ + AuthResult register(User user, String password); + + /** + * 用户登录 + * @param username 用户名/邮箱/手机号 + * @param password 密码 + * @return 登录后的用户信息和Token + */ + AuthResult login(String username, String password); + + /** + * 获取当前用户信息 + * @param userId 用户ID + * @return 用户信息 + */ + User getCurrentUser(Long userId); + + /** + * 更新用户信息 + * @param userId 用户ID + * @param user 更新的用户信息 + * @return 更新后的用户信息 + */ + User updateProfile(Long userId, User user); + + /** + * 修改密码 + * @param userId 用户ID + * @param currentPassword 当前密码 + * @param newPassword 新密码 + * @return 是否修改成功 + */ + boolean changePassword(Long userId, String currentPassword, String newPassword); + + /** + * 微信登录/注册 + * @param code 微信授权码 + * @param userInfo 微信用户信息 + * @return 登录后的用户信息和Token + */ + AuthResult wechatLogin(String code, Object userInfo); + + /** + * 管理员登录 + * @param username 用户名/邮箱/手机号 + * @param password 密码 + * @return 登录后的管理员信息和Token + */ + AuthResult adminLogin(String username, String password); +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/impl/AuthServiceImpl.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..1e69b31 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/service/impl/AuthServiceImpl.java @@ -0,0 +1,255 @@ +package com.jiebanke.auth.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jiebanke.auth.entity.User; +import com.jiebanke.auth.mapper.UserMapper; +import com.jiebanke.auth.service.AuthResult; +import com.jiebanke.auth.service.AuthService; +import com.jiebanke.auth.util.JwtUtil; +import com.jiebanke.common.exception.BusinessException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class AuthServiceImpl extends ServiceImpl implements AuthService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @Override + public AuthResult register(User user, String password) { + // 检查用户名是否已存在 + if (userMapper.existsByUsername(user.getUsername()) > 0) { + throw new BusinessException("用户名已存在"); + } + + // 检查邮箱是否已存在 + if (user.getEmail() != null && userMapper.existsByEmail(user.getEmail()) > 0) { + throw new BusinessException("邮箱已存在"); + } + + // 检查手机号是否已存在 + if (user.getPhone() != null && userMapper.existsByPhone(user.getPhone()) > 0) { + throw new BusinessException("手机号已存在"); + } + + // 加密密码 + String hashedPassword = passwordEncoder.encode(password); + user.setPassword(hashedPassword); + + // 设置默认值 + if (user.getStatus() == null) { + user.setStatus("active"); + } + + // 创建新用户 + this.save(user); + + // 生成Token + String token = jwtUtil.generateToken(user.getId()); + + // 更新最后登录时间 + userMapper.updateLastLogin(user.getId()); + + // 创建返回结果 + AuthResult result = new AuthResult(); + result.setUser(user); + result.setToken(token); + result.setMessage("注册成功"); + + return result; + } + + @Override + public AuthResult login(String username, String password) { + if (username == null || username.isEmpty() || password == null || password.isEmpty()) { + throw new BusinessException("用户名和密码不能为空"); + } + + // 查找用户(支持用户名、邮箱、手机号登录) + User user = userMapper.selectByUsername(username); + if (user == null) { + user = userMapper.selectByEmail(username); + } + if (user == null) { + user = userMapper.selectByPhone(username); + } + + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 检查用户状态 + if (!"active".equals(user.getStatus())) { + throw new BusinessException("账户已被禁用"); + } + + // 验证密码 + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new BusinessException("密码错误"); + } + + // 生成Token + String token = jwtUtil.generateToken(user.getId()); + + // 更新最后登录时间 + userMapper.updateLastLogin(user.getId()); + + // 创建返回结果 + AuthResult result = new AuthResult(); + result.setUser(user); + result.setToken(token); + result.setMessage("登录成功"); + + return result; + } + + @Override + public User getCurrentUser(Long userId) { + User user = this.getById(userId); + if (user == null) { + throw new BusinessException("用户不存在"); + } + return user; + } + + @Override + public User updateProfile(Long userId, User user) { + User existingUser = this.getById(userId); + if (existingUser == null) { + throw new BusinessException("用户不存在"); + } + + // 更新字段 + if (user.getRealName() != null) { + existingUser.setRealName(user.getRealName()); + } + if (user.getEmail() != null) { + existingUser.setEmail(user.getEmail()); + } + if (user.getPhone() != null) { + existingUser.setPhone(user.getPhone()); + } + + this.updateById(existingUser); + return existingUser; + } + + @Override + public boolean changePassword(Long userId, String currentPassword, String newPassword) { + if (currentPassword == null || currentPassword.isEmpty() || newPassword == null || newPassword.isEmpty()) { + throw new BusinessException("当前密码和新密码不能为空"); + } + + if (newPassword.length() < 6) { + throw new BusinessException("新密码长度不能少于6位"); + } + + User user = this.getById(userId); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 验证当前密码 + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new BusinessException("当前密码错误"); + } + + // 加密新密码 + String hashedPassword = passwordEncoder.encode(newPassword); + user.setPassword(hashedPassword); + + // 更新密码 + return this.updateById(user); + } + + @Override + public AuthResult wechatLogin(String code, Object userInfo) { + if (code == null || code.isEmpty()) { + throw new BusinessException("微信授权码不能为空"); + } + + // 这里应该调用微信API获取openid和unionid + // 模拟获取微信用户信息 + String openid = "mock_openid_" + System.currentTimeMillis(); + + // 查找是否已存在微信用户 + // 注意:在实际实现中,应该有一个专门的字段来存储微信openid + User user = null; + + if (user == null) { + // 创建新用户(微信注册) + user = new User(); + user.setUsername("wx_" + openid.substring(Math.max(0, openid.length() - 8))); + String randomPassword = String.valueOf(System.currentTimeMillis()).substring(0, 8); + String hashedPassword = passwordEncoder.encode(randomPassword); + user.setPassword(hashedPassword); + user.setRealName("微信用户"); + user.setStatus("active"); + + this.save(user); + } + + // 生成Token + String token = jwtUtil.generateToken(user.getId()); + + // 创建返回结果 + AuthResult result = new AuthResult(); + result.setUser(user); + result.setToken(token); + result.setMessage("微信登录成功"); + + return result; + } + + @Override + public AuthResult adminLogin(String username, String password) { + if (username == null || username.isEmpty() || password == null || password.isEmpty()) { + throw new BusinessException("用户名和密码不能为空"); + } + + // 查找用户(支持用户名、邮箱、手机号登录) + User user = userMapper.selectByUsername(username); + if (user == null) { + user = userMapper.selectByEmail(username); + } + if (user == null) { + user = userMapper.selectByPhone(username); + } + + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 检查用户状态 + if (!"active".equals(user.getStatus())) { + throw new BusinessException("账户已被禁用"); + } + + // 验证密码 + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new BusinessException("密码错误"); + } + + // 生成Token + String token = jwtUtil.generateToken(user.getId()); + + // 更新最后登录时间 + userMapper.updateLastLogin(user.getId()); + + // 创建返回结果 + AuthResult result = new AuthResult(); + result.setUser(user); + result.setToken(token); + result.setMessage("管理员登录成功"); + + return result; + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/java/com/jiebanke/auth/util/JwtUtil.java b/backend-java/auth-service/src/main/java/com/jiebanke/auth/util/JwtUtil.java new file mode 100644 index 0000000..b2c7343 --- /dev/null +++ b/backend-java/auth-service/src/main/java/com/jiebanke/auth/util/JwtUtil.java @@ -0,0 +1,107 @@ +package com.jiebanke.auth.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:mySecretKey}") + private String secret; + + @Value("${jwt.expiration:604800}") + private Long expiration; + + /** + * 生成JWT Token + * @param userId 用户ID + * @return JWT Token + */ + public String generateToken(Long userId) { + Map claims = new HashMap<>(); + return createToken(claims, userId.toString()); + } + + /** + * 从JWT Token中提取用户ID + * @param token JWT Token + * @return 用户ID + */ + public Long extractUserId(String token) { + return Long.valueOf(extractClaim(token, Claims::getSubject)); + } + + /** + * 验证JWT Token是否有效 + * @param token JWT Token + * @param userId 用户ID + * @return 是否有效 + */ + public Boolean validateToken(String token, Long userId) { + final Long extractedUserId = extractUserId(token); + return (extractedUserId.equals(userId) && !isTokenExpired(token)); + } + + /** + * 从JWT Token中提取声明 + * @param token JWT Token + * @param claimsResolver 声明解析器 + * @param 声明类型 + * @return 声明值 + */ + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * 从JWT Token中提取所有声明 + * @param token JWT Token + * @return 所有声明 + */ + private Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } + + /** + * 检查JWT Token是否过期 + * @param token JWT Token + * @return 是否过期 + */ + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * 从JWT Token中提取过期时间 + * @param token JWT Token + * @return 过期时间 + */ + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + /** + * 创建JWT Token + * @param claims 声明 + * @param subject 主题 + * @return JWT Token + */ + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/main/resources/application.yml b/backend-java/auth-service/src/main/resources/application.yml new file mode 100644 index 0000000..8a4c2fc --- /dev/null +++ b/backend-java/auth-service/src/main/resources/application.yml @@ -0,0 +1,36 @@ +server: + port: 8081 + +spring: + application: + name: auth-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +jwt: + secret: mySecretKey + expiration: 604800 + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto \ No newline at end of file diff --git a/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRabbitMQServiceTest.java b/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRabbitMQServiceTest.java new file mode 100644 index 0000000..2bf8097 --- /dev/null +++ b/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRabbitMQServiceTest.java @@ -0,0 +1,55 @@ +package com.jiebanke.auth.service; + +import com.jiebanke.common.config.RabbitMQConfig; +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 org.springframework.amqp.rabbit.core.RabbitTemplate; + +import static org.mockito.Mockito.*; + +class AuthRabbitMQServiceTest { + + @Mock + private RabbitTemplate rabbitTemplate; + + @InjectMocks + private AuthRabbitMQService authRabbitMQService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendLoginSuccessMessage() { + Long userId = 1L; + String username = "testUser"; + String ip = "127.0.0.1"; + + authRabbitMQService.sendLoginSuccessMessage(userId, username, ip); + + verify(rabbitTemplate).convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + anyMap() + ); + } + + @Test + void testSendLoginFailureMessage() { + String username = "testUser"; + String ip = "127.0.0.1"; + String reason = "Invalid credentials"; + + authRabbitMQService.sendLoginFailureMessage(username, ip, reason); + + verify(rabbitTemplate).convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + anyMap() + ); + } +} \ No newline at end of file diff --git a/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRedisServiceTest.java b/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRedisServiceTest.java new file mode 100644 index 0000000..5486396 --- /dev/null +++ b/backend-java/auth-service/src/test/java/com/jiebanke/auth/service/AuthRedisServiceTest.java @@ -0,0 +1,112 @@ +package com.jiebanke.auth.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 org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AuthRedisServiceTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private AuthRedisService authRedisService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Test + void testCacheVerificationCode() { + String phone = "13800138000"; + String code = "123456"; + + authRedisService.cacheVerificationCode(phone, code); + + verify(valueOperations).set(eq("verification:code:" + phone), eq(code), anyLong(), any()); + } + + @Test + void testGetCachedVerificationCode() { + String phone = "13800138000"; + String code = "123456"; + when(valueOperations.get("verification:code:" + phone)).thenReturn(code); + + String result = authRedisService.getCachedVerificationCode(phone); + + assertEquals(code, result); + verify(valueOperations).get("verification:code:" + phone); + } + + @Test + void testRemoveCachedVerificationCode() { + String phone = "13800138000"; + + authRedisService.removeCachedVerificationCode(phone); + + verify(redisTemplate).delete("verification:code:" + phone); + } + + @Test + void testCacheLoginFailures() { + String identifier = "testUser"; + Integer failures = 3; + + authRedisService.cacheLoginFailures(identifier, failures); + + verify(valueOperations).set(eq("login:failures:" + identifier), eq(failures), anyLong(), any()); + } + + @Test + void testGetLoginFailures() { + String identifier = "testUser"; + Integer failures = 3; + when(valueOperations.get("login:failures:" + identifier)).thenReturn(failures); + + Integer result = authRedisService.getLoginFailures(identifier); + + assertEquals(failures, result); + verify(valueOperations).get("login:failures:" + identifier); + } + + @Test + void testIncrementLoginFailures() { + String identifier = "testUser"; + when(valueOperations.get("login:failures:" + identifier)).thenReturn(null); + + authRedisService.incrementLoginFailures(identifier); + + verify(valueOperations).set(eq("login:failures:" + identifier), eq(1), anyLong(), any()); + } + + @Test + void testIncrementLoginFailuresWithExistingValue() { + String identifier = "testUser"; + when(valueOperations.get("login:failures:" + identifier)).thenReturn(2); + + authRedisService.incrementLoginFailures(identifier); + + verify(valueOperations).set(eq("login:failures:" + identifier), eq(3), anyLong(), any()); + } + + @Test + void testClearLoginFailures() { + String identifier = "testUser"; + + authRedisService.clearLoginFailures(identifier); + + verify(redisTemplate).delete("login:failures:" + identifier); + } +} \ No newline at end of file diff --git a/backend-java/build-services.sh b/backend-java/build-services.sh new file mode 100755 index 0000000..5046eca --- /dev/null +++ b/backend-java/build-services.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# 构建结伴客Java后端服务脚本 + +echo "开始构建结伴客Java后端服务..." + +# 清理之前的构建 +echo "清理项目..." +mvn clean + +# 构建所有模块 +echo "构建所有模块..." +mvn install + +# 检查构建是否成功 +if [ $? -eq 0 ]; then + echo "构建成功!" + + # 显示构建结果 + echo "构建产物位置:" + echo " Eureka Server: eureka-server/target/" + echo " Gateway Service: gateway-service/target/" + echo " Auth Service: auth-service/target/" + echo " User Service: user-service/target/" + echo " Travel Service: travel-service/target/" + echo " Animal Service: animal-service/target/" + echo " Order Service: order-service/target/" + echo " Promotion Service: promotion-service/target/" + + # 复制jar包到各自目录以便Docker构建 + echo "复制jar包..." + cp eureka-server/target/eureka-server.jar eureka-server/ + cp gateway-service/target/gateway-service.jar gateway-service/ + cp auth-service/target/auth-service.jar auth-service/ + cp user-service/target/user-service.jar user-service/ + cp travel-service/target/travel-service.jar travel-service/ + cp animal-service/target/animal-service.jar animal-service/ + cp order-service/target/order-service.jar order-service/ + cp promotion-service/target/promotion-service.jar promotion-service/ + + echo "所有服务构建完成,可以使用docker-compose启动服务" +else + echo "构建失败,请检查错误信息" +fi \ No newline at end of file diff --git a/backend-java/common/pom.xml b/backend-java/common/pom.xml new file mode 100644 index 0000000..9e13092 --- /dev/null +++ b/backend-java/common/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + common + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-validation + + + \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/config/FeignConfig.java b/backend-java/common/src/main/java/com/jiebanke/common/config/FeignConfig.java new file mode 100644 index 0000000..a8023e5 --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/config/FeignConfig.java @@ -0,0 +1,22 @@ +package com.jiebanke.common.config; + +import feign.Request; +import feign.Retryer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class FeignConfig { + + @Bean + public Request.Options options() { + return new Request.Options(5, TimeUnit.SECONDS, 30, TimeUnit.SECONDS, true); + } + + @Bean + public Retryer retryer() { + return new Retryer.Default(100, 1000, 3); + } +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/config/RabbitMQConfig.java b/backend-java/common/src/main/java/com/jiebanke/common/config/RabbitMQConfig.java new file mode 100644 index 0000000..9873443 --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/config/RabbitMQConfig.java @@ -0,0 +1,102 @@ +package com.jiebanke.common.config; + +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + // 定义队列名称 + public static final String USER_QUEUE = "user.queue"; + public static final String TRAVEL_QUEUE = "travel.queue"; + public static final String ANIMAL_QUEUE = "animal.queue"; + public static final String ORDER_QUEUE = "order.queue"; + public static final String PROMOTION_QUEUE = "promotion.queue"; + + // 定义交换机名称 + public static final String EXCHANGE_NAME = "jiebanke.exchange"; + + // 定义路由键 + public static final String USER_ROUTING_KEY = "user.routing.key"; + public static final String TRAVEL_ROUTING_KEY = "travel.routing.key"; + public static final String ANIMAL_ROUTING_KEY = "animal.routing.key"; + public static final String ORDER_ROUTING_KEY = "order.routing.key"; + public static final String PROMOTION_ROUTING_KEY = "promotion.routing.key"; + + // 队列声明 + @Bean + public Queue userQueue() { + return new Queue(USER_QUEUE, true); + } + + @Bean + public Queue travelQueue() { + return new Queue(TRAVEL_QUEUE, true); + } + + @Bean + public Queue animalQueue() { + return new Queue(ANIMAL_QUEUE, true); + } + + @Bean + public Queue orderQueue() { + return new Queue(ORDER_QUEUE, true); + } + + @Bean + public Queue promotionQueue() { + return new Queue(PROMOTION_QUEUE, true); + } + + // 交换机声明 + @Bean + public TopicExchange exchange() { + return new TopicExchange(EXCHANGE_NAME); + } + + // 绑定关系 + @Bean + public Binding userBinding() { + return BindingBuilder.bind(userQueue()).to(exchange()).with(USER_ROUTING_KEY); + } + + @Bean + public Binding travelBinding() { + return BindingBuilder.bind(travelQueue()).to(exchange()).with(TRAVEL_ROUTING_KEY); + } + + @Bean + public Binding animalBinding() { + return BindingBuilder.bind(animalQueue()).to(exchange()).with(ANIMAL_ROUTING_KEY); + } + + @Bean + public Binding orderBinding() { + return BindingBuilder.bind(orderQueue()).to(exchange()).with(ORDER_ROUTING_KEY); + } + + @Bean + public Binding promotionBinding() { + return BindingBuilder.bind(promotionQueue()).to(exchange()).with(PROMOTION_ROUTING_KEY); + } + + // 消息转换器 + @Bean + public MessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } + + // RabbitTemplate配置 + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(messageConverter()); + return template; + } +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/config/RedisConfig.java b/backend-java/common/src/main/java/com/jiebanke/common/config/RedisConfig.java new file mode 100644 index 0000000..508350a --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/config/RedisConfig.java @@ -0,0 +1,20 @@ +package com.jiebanke.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/entity/BaseEntity.java b/backend-java/common/src/main/java/com/jiebanke/common/entity/BaseEntity.java new file mode 100644 index 0000000..a4ac4f6 --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/entity/BaseEntity.java @@ -0,0 +1,11 @@ +package com.jiebanke.common.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class BaseEntity { + private Long id; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/exception/BusinessException.java b/backend-java/common/src/main/java/com/jiebanke/common/exception/BusinessException.java new file mode 100644 index 0000000..4269717 --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/exception/BusinessException.java @@ -0,0 +1,23 @@ +package com.jiebanke.common.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class BusinessException extends RuntimeException { + private int code; + private String message; + + public BusinessException(String message) { + super(message); + this.code = 400; + this.message = message; + } + + public BusinessException(int code, String message) { + super(message); + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/exception/GlobalExceptionHandler.java b/backend-java/common/src/main/java/com/jiebanke/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..af75f3f --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,20 @@ +package com.jiebanke.common.exception; + +import com.jiebanke.common.vo.ApiResponse; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ApiResponse handleBusinessException(BusinessException e) { + return ApiResponse.error(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception e) { + e.printStackTrace(); + return ApiResponse.error(500, "服务器内部错误"); + } +} \ No newline at end of file diff --git a/backend-java/common/src/main/java/com/jiebanke/common/vo/ApiResponse.java b/backend-java/common/src/main/java/com/jiebanke/common/vo/ApiResponse.java new file mode 100644 index 0000000..871ae2c --- /dev/null +++ b/backend-java/common/src/main/java/com/jiebanke/common/vo/ApiResponse.java @@ -0,0 +1,41 @@ +package com.jiebanke.common.vo; + +import lombok.Data; + +@Data +public class ApiResponse { + private boolean success; + private int code; + private String message; + private T data; + private long timestamp; + + public static ApiResponse success(T data) { + ApiResponse response = new ApiResponse<>(); + response.success = true; + response.code = 200; + response.message = "操作成功"; + response.data = data; + response.timestamp = System.currentTimeMillis(); + return response; + } + + public static ApiResponse success(T data, String message) { + ApiResponse response = new ApiResponse<>(); + response.success = true; + response.code = 200; + response.message = message; + response.data = data; + response.timestamp = System.currentTimeMillis(); + return response; + } + + public static ApiResponse error(int code, String message) { + ApiResponse response = new ApiResponse<>(); + response.success = false; + response.code = code; + response.message = message; + response.timestamp = System.currentTimeMillis(); + return response; + } +} \ No newline at end of file diff --git a/backend-java/common/target/classes/com/jiebanke/common/config/FeignConfig.class b/backend-java/common/target/classes/com/jiebanke/common/config/FeignConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..69bac0938bda0c16bf1d9451f53fe91b50ed384b GIT binary patch literal 1017 zcma)4OK;Oa5dOAF;0xkF00}C3GE_?2NE|omG;A90hV{D8pT(&X zNO0#zA;wNfBUGuD<(-|`nQy*%{QmR(CxBOY=wU(NQQySjP*G3nBMMoGjb@<_)gas^ zHPBo6-9uTRK9t8Y97#P0cX~tWJAukerImXvP-<;<1+5xa zwN`Y-XxC+xw6g(tmYh8yE3llmC0E*2+$zd854ezR0WSf6*QB;j+bSD-eQueaX4D0q z7ahZ71yh@Mm*=s;^Z0M#9`1AToqxsG-m4_xp6qdV6KsI@lXo6`ifk|W^jHg73YLqH zPf`8Gmj(EIX3xMfe?V>G!15An0xMW$ulm&MGRq9;Sq=>NKhXHtxN?fCzt}pDwlat2 j&(PL!<3hBX=b$yuK`Y^Qp7YL(-Cfon@L%B^0q*?;7770U literal 0 HcmV?d00001 diff --git a/backend-java/common/target/classes/com/jiebanke/common/config/RabbitMQConfig.class b/backend-java/common/target/classes/com/jiebanke/common/config/RabbitMQConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..1c02901f57881ecd6334bd64e32d4968e7967ede GIT binary patch literal 4809 zcmd5Rwi@U{fm|RYu~-!+$q*K@yK#1d#oyqc z@l(&ybM(7Es;BQ>ve||4WPx*p5A5Q#X_!7S}o)|YfKRsYjHy}EY;9E%Zw;5 zmY!$wE6PqjlT+gG9)XEYXy(|f+8(-%=8iiK7YpUmYJO#BUD=Mq2RQ0@#ErIvPR|{% zBMgS;SBQEY$35@DWY5PCc?QXzry=r$>u-qr30muV9imQkeGkz;B{0>EKxoR9$EvPc zPYLuVQxyUOOGceZ&<~#tLlROLxvZ+Qd|RtBUZT||hGa5p)M&FpxoTr?aKPGAO#-vo z$R&oglCZDEOenaVo<;`ns2ty`+bDypgC5*b}LENUkYSGb!scq47L>~h=XRAavy z{J<_^i@mba)TgEw8PykJ@DN?-mZ`$I!Q=21f&H@1nQ1f+SzSh(TWZr;LB=rQlA8HQ zmJ*IVa?KHyt4#JMScXhx>!`&daU__BM|L>TwW;LEO=HJ>$So5u7(;Fyct~ID3vpN?@LL2|L(_V=dSs2n~BG3UFCn=)qHW@xHu zq9ZQTNkB-olZtbQhJq{MdFmF1=Up7u2$3%+W`PnthlBr5)Z7i^M=f`8Q44J4%O161 z=b-U}wn4~^3o5nliFes!HtwIte%!VQxp_fx^Nn$K*`<%2#!Qno7`BXuj9ZKoxP})* z?${{a6yFi}Jwiy2&lKDO_DSBkyVq##z%=yxYxp?bR05;iX_!k)YhmYrZPBe{7X8q% zO~yuLnbs_WA1$P^?sd|kp(`&5$wY7^JgC(Kb82Y$bl>NcyiBCjmMLN_oB3A zw0Vs!t9DCz)wQwn^)hESd;}gwZ51(m_zBeJP6voRd6HGr=nekXw6MLZqA?<4v!fImWfBYL42R8={(|}K z$uD2S*Z&~ugRa_4Kuz-19-mR|wovWK8P#TmYERFoc2B7G%^B6^g=*hIre_uw1zA`S zsyzdxr`obWEhAKW9zktYptdYjTaTcY6{xKX)$$S43Ieqaq1t8ywW2_+EL1B&xn~wC w0=1VywHL7cA8LL6KK)*xMg?m9@Po6PfAshLPlyljZxqjp9iUJH1`SZ(0;Z<%B>(^b literal 0 HcmV?d00001 diff --git a/backend-java/common/target/classes/com/jiebanke/common/config/RedisConfig.class b/backend-java/common/target/classes/com/jiebanke/common/config/RedisConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..602795cd5548d7b21495a4057461156abd284ee7 GIT binary patch literal 1155 zcmbVLO>Yx15PjaHO|v94gtkyVw;-Ve3HE|GkWvXEdRnN|gh(8njT@Zq+L6~Olz+vk z5=e08Mideja0+d!pW46)&ZgK9#52&vFOP}(hpxLr7cej zq7Oa^v>E!zI88q>JQkm{IZf%d;`^Fe;HgJQn_IqVSqN6i0 z@QAXpBogQaZ)`L@87D71x17Xc$%uh@O)K0 zr&BaBnM@??eTJPJZkhBF_Ncp;#AB$ul}SQ9(ArxvhH_meW>qT;O0;A e9Ky;`s|%>^47GsUS=605-d*w^WH}D*!TSSg1xqae literal 0 HcmV?d00001 diff --git a/backend-java/common/target/classes/com/jiebanke/common/entity/BaseEntity.class b/backend-java/common/target/classes/com/jiebanke/common/entity/BaseEntity.class new file mode 100644 index 0000000000000000000000000000000000000000..f684fcdc68f59eb50f31f92aba8cd74fff7fe322 GIT binary patch literal 2952 zcma)8U31e`5IxtHEX4>CK9XRPQVIbQ+rj-V*n|Qhfq(;)gwO(QQS949#FkQ9PRozr zz0)61UU;B0E$#5oXaC|%i54(VH)_NuZEjL(9Olt6#kapl!keOWdZ zZMsnJsd$#`hNJe8Qu9|G`aE9V7Bf7^V&K!Ad`H^x_sCd%!q>;pgg|~26>2ekZF{GqP3mFUv2v1SkX@-} zy8>mbVfsT`OcO$4y%IrUnw<;2U0IvA*P|+$t9El$b$~!}d}2<8Ub1i!rx?xk8*}qB z40n!%G-R_?W0xj_Skhy$Q`2KH|EBLP_zlNh5!lx*aBvkuZ7GczTr@Dd<(h;x3zxv7 zj19ZilGnZxIM$2&7;llwJ-nt@ER-=XFkH9SqUA{Wzkt+wF!veci#O%rzovYph&o$8dxAKGjfwb^W# zDKWvTI_Gj6P1o#ZQ#K8J$!Xg~Mz8b?kkW&%hO$;67rc%v7h5HXgqm}d5^BH%E9VK< z9YpZ@Qg3mW;%+C`UHmUl7F_l0Pw0O_A%Wd|Du{Zj4zxl5_F^A*oE#W>jicf1IpWB8 zjiUqGb2N%KdN?Xlmrz+`b3dd1M@rqfsQ^@-VQxxN1S(1n;!WCKrfZAO;qCfV#ZW#E zz24`6?fO)ORX#b3yWJ^48IJH&iG`xHllErnxO^L!Mw z$v?3pqdmv&N3bR{+Ea|T5!s?nNVb^D=n9j1jy;dCGoxoxF;Ko}5R@;P8AE~02xMf; zHb{HSfRbJ9bvF!jvjh)uBIwFNe2cf)atVyV!8_C?kwpdXQqph{vv`k^j(MEL`;=0s zk&F*08E9YxA5t>$5zoxQGR(iR%YgU?Lk5!kGb!#5#lE;m*ZlZ(Gm2B>TJ?&qs8pH! zUo+{Ondpo^ykGq4bh{KMs8hG~=0NGRHWid&bfC1#LYM=&M=&OzqIgf~vqeoAGFoIY zRe3^FzNiN#Rwj7K%;v^Uvj!ULlalemG?{2|CMBCg{Mr3A;s6@1LFlQ=@E&r;H8 z8OAwEdO+tlNWGv!Bb`E!LM|`(?CcXtF$ld4Xk)akstA^f!8*v7wFEQF^O)}Dk>6d$ zKpsaFg}>s0Vk}(pT%O@F@Txg760srFhpvbF&WQX4*Lz>jts_#=s`_IAi(J(;z0LnS Q_?$cSYv~Kz!!k1e0WNJVVE_OC literal 0 HcmV?d00001 diff --git a/backend-java/common/target/classes/com/jiebanke/common/exception/BusinessException.class b/backend-java/common/target/classes/com/jiebanke/common/exception/BusinessException.class new file mode 100644 index 0000000000000000000000000000000000000000..81b5a2ffe04b003b9a5c59b557f263549cc9280d GIT binary patch literal 2357 zcmb7GT~ixn6n@@hlMU$tf$~ub)>f#^7md}bg+LV$+9;t8bm|P=bO}4KknCc1!`Oe} zrQ?j_oxR~gXDprRg`+n*`kU1HoZXOs$)HZMyXTxe?{m(1&UxSD_rG5J2w)x$bVL*i z6}MK{v#l-D*|$WgxlY0QykZ^rHl@XeXFHbXEuUC)XbPA2%tNzKHJzQpdc*PUn$>!* zpjF&$OF@+s^ct6%JJbeO+evg!}bt!fa=t(etEX5ALOY1I5(+f%r;`acV1o+If}_x$u7 z%a;@tqUj9l9dEKxat>|JX6yyWaeY&mWaz=ws$1J~_Y3zd$EusYwN30KA<8E#4W%=k zfk>lw5=KjxgqI`@twjmn4H-?6l-CT5;*v;H2F5Wl$X35@;0DqPqcwBiT5_F=>3?GT zyOccNbbMxZqiccPQi)riHOwlPoN3vEtv##a7c-j@IB#GYS%pN!be6?Ci2`ovctau8 z9)fU*fsD8+_w0l;@f2dNziZVM3~Am}Ynplk^C^A9H!J&P^Pt)5#IEVFKAD{YW)0lI zC1QQNUMeqh>gtx)sQOH>4UpV$8}*8{V$1l9bPWAX$(!jfx~}i}b@QNX`Md76m&77I z(6L0e+m+eQq06ll_9Ucx#jN;l{fYE}34D2a+MC`X;C<8Cu3FyIs_X7I4vL)(IAuAr z@ZpmKtBd=mr~FTPbjs9(STd{C4coVh{c$WLaG%w5Fp!o_J+Jatn?0CU8Fr=QaFCa( zrsr9nj*k?schJzSFdb!uE9bkiPazAq#+iz8j3fMT>jx4yKA$peJQjGrKy5wn00y(U z=PWgRS%jTXT^?k5Lw!$DZ-g}AD2Lt^WKnfRW8R1%aJA`3AMUn^{OyZ9;@i*xs zd8D#=!58bKL?2>S!^9s6(65H;TIi{u4kcJYoCqUu*Ajn>wm7xRd`e9P%HkKx z{(!zC3K6Cjy2xEpH$sOdIL{zV{0mU(yZ;3A3a)eknx##+iU$O=nU$s5G_ymVIC#ys1t^D$34bhT^2cNj-9>dPME8n4M0xDK3Gd0&e{o=IJ@{EfC45jS($ zZxPduaz8_Tg;?|`_Y+h(<HtmGQe7N(`On^^JH*4r|mVXSL~HabZmR1p`lEOpj^D zO?OQi96i-wJ!tUlFlvjSlcj`ahvxDisbce+M-zC1ieE^!@I;9MM6*ffj%9o z$M_Q?Itu*qCsUIXx=V>Ogq7yTq_%&KTbNDQTbb~1r^J|pWTWJe?E?8VZL;U`sp4Gr zIcDZ3QpH{A$>aHe5mCuUB9(gzefk(PkLM@4^N{tF|6daT+$7GQGQie0Yk@(bYw8g z(3VW)7~*%`g3!^1E(7i8VCc?BM?9>Q^1@r@d0Q~_Wn7EfIqpfdu5ZSJ73niv&m6?J zG{f*p=;ob@PjtQ?dC%}T`47w{n{=wBMb$Ou%5q8gW!Lcqadu^8(cwYGqoCyfVVTJ^ zGM;l~Aw5gHBTE%0kR_3mzN8G!IgT4}RXR%G&5Y|6O~33(r})(4C9&>$Yv#Jhn|bLJ zOdj1eqZHK(Ou+3`ZdWLnsh06K6w;hA9m5#WaD+kW=`Z4HGBt1%qYPS61WG!gjusT$ zaRVoCl0m1FMJg#T(&0cdwJRgpK!ussaT;edj4_P0BHvp#17~rLLKU9pdJH#`3(bK0 ztUn`%x@)?NlF5)x?gEVV_p7PJgIl-M%3w=pbX>%whD!|n4Shwv8Zg6`t3(uJ18%J? zd)yM8m|__G{`U3ut9RQQo7*p6>^$Gxd9(3jYm2Isbt|4F?n(8+M_Yd(lZuOBcHa`E zw??9H0?)PS2bwEXr6tl-Y!V55XP}-6i4N*DTA4I58r#P|q4NVxV$i8q$3Q0zQE${P z(3qeV!(nt2NDs|p=tUomhiSK;+Q1Vq3xgr>Z90u9>xqvTqIT>vPQ4Fp8u&(tKUBL{ z$5(vk2}8Mc0m+Cd)q-g}WSXxtC5UCBm;LO!yoX8;;d>D$02NM+E)mfsh;E$7Vz?5z McQqtZ377_c1I7!l(EtDd literal 0 HcmV?d00001 diff --git a/backend-java/common/target/classes/com/jiebanke/common/vo/ApiResponse.class b/backend-java/common/target/classes/com/jiebanke/common/vo/ApiResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..b4db8232dcefc3b87829150f5c4d2c8ac2f8907a GIT binary patch literal 4861 zcmbVQZF3t}6@IRzm1K3DC?-y9=gmo+Shkdkmbcb#kT^+WV<*KnYT^c(wY;@AmUdlf zWe8B-S|Ajd@eF(*3^V;kzJwW=!D*)q416gJzXSR!DCIeOS1*p##Pv*+z4xB8=REg3 z=Vg`FyJEh$6`-ipoTGzBG|TC3c3&o*9L@XL<7I!4VE%eQvQ zv`NZ37o1hs@~dUO99dkH6W^gZ-zsg;_>97H=_Z@ud&9F&z6DKP9A6x5eR(oXKfAFo zPQ6LNKgF9Sq0nRE5DqKE#vRx3Cy6*cobRNd&xC=Ptd5w7Bay%{1@nVn{^H+%`RxaH zxBm0i+afz*q7zA3oiy5WLIHuchNgEyTi<#jhd-X z61$l;>@`sm=E!a=l8F4r^SL*hHHHnlT=vQe&!?yNW{BVxKL*?$E!LoQI+cZ5*^zn{ z-jVsLD>X9hZaNi*44-#h&$oPswJTLt^h(R#dhP|=wab=ouSf>_Ymz$gRojxDM?_3P(cc((Vf{pZWP`4(HNuGwKo2=M=9e6aoG@O?PQu7~8Ps@QQ z)dBQ0r%Ej`!nZVn9U8S(K#f>X(Oj6c*MuROo)IrG`?ac7K%GuXUOyY~|Tb_d9`D-?F z%9Pj-GA_D9b_K_cePTY170v@J)roC2QTD^4x;uzvTGD?GlsgldiG!Qk&vwwKmfxafO#2 zUUI>!mJ9Y}NBUx_X~sP*b4nrIzG$0#)bl)b!SnozU$!>pY=6yLsU-0|{2-3+v!OPI zp5tzMoU1x=`c{{%g71~@$aR0H@ayIXh1%NS70X>I+Lggs&s(o z&27|QZqjc(-k-}tCn9||MpHSHWnfLMxQ(2ahDe|mm(u5jj z#!tI!JyS)iQn4#>+*LT!5}Wnl z$Wp5-Sns2p1C#4+j$s1_xYv?ye2S-AwAR6A`p-}|-oe4yOmrK)nFILeHu^K&N4If2 zGqR0=%)2|+&r^OTw(sn<+oS@ya++`>|q;1RC=PrQ(WO0tz zp57tbSC{1|6O^B>qkJYnd5=Ej)4sF+=#&K zXGk^J7M%S7m8aYfPmnWD$-XQps zq2`$4ZFT;C>9K(s*sBZ9$+qZ)4;}OL@447Z%r9W7jp!ZK>e_u6DeG44Z1L6vCHV`Ya`I$q;Pp4| z1}E1?4LK4H5~<96B>IepSPev^CJ@EyA~HkK(e|`V5(FJ|>}+tmp4Ke>dNS$4CA>W)vRR`;Ub&X|x+T(X{fv zNm^-bnz!%ZPW!6Yp{nIh!*nfUqWBgucd#b(?D`H~=U#g28~nYAA9E+4EI+|d@iX-O EA9+%P0{{R3 literal 0 HcmV?d00001 diff --git a/backend-java/docker-compose.yml b/backend-java/docker-compose.yml new file mode 100644 index 0000000..6fa800e --- /dev/null +++ b/backend-java/docker-compose.yml @@ -0,0 +1,147 @@ +version: '3.8' + +services: + # MySQL数据库 + mysql: + image: mysql:8.0 + container_name: jiebanke-mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: jiebanke + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./scripts/init-database.sql:/docker-entrypoint-initdb.d/init-database.sql + networks: + - jiebanke-network + + # Redis缓存 + redis: + image: redis:6.0 + container_name: jiebanke-redis + ports: + - "6379:6379" + networks: + - jiebanke-network + + # RabbitMQ消息队列 + rabbitmq: + image: rabbitmq:3.8-management + container_name: jiebanke-rabbitmq + ports: + - "5672:5672" + - "15672:15672" + networks: + - jiebanke-network + + # Eureka服务注册中心 + eureka-server: + build: + context: ./eureka-server + container_name: jiebanke-eureka + ports: + - "8761:8761" + networks: + - jiebanke-network + depends_on: + - mysql + - redis + - rabbitmq + + # API网关 + gateway-service: + build: + context: ./gateway-service + container_name: jiebanke-gateway + ports: + - "8080:8080" + networks: + - jiebanke-network + depends_on: + - eureka-server + + # 认证服务 + auth-service: + build: + context: ./auth-service + container_name: jiebanke-auth + ports: + - "8081:8081" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + + # 用户服务 + user-service: + build: + context: ./user-service + container_name: jiebanke-user + ports: + - "8082:8082" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + + # 旅行服务 + travel-service: + build: + context: ./travel-service + container_name: jiebanke-travel + ports: + - "8083:8083" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + + # 动物服务 + animal-service: + build: + context: ./animal-service + container_name: jiebanke-animal + ports: + - "8084:8084" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + + # 订单服务 + order-service: + build: + context: ./order-service + container_name: jiebanke-order + ports: + - "8085:8085" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + + # 推广服务 + promotion-service: + build: + context: ./promotion-service + container_name: jiebanke-promotion + ports: + - "8086:8086" + networks: + - jiebanke-network + depends_on: + - eureka-server + - mysql + +volumes: + mysql_data: + +networks: + jiebanke-network: + driver: bridge \ No newline at end of file diff --git a/backend-java/eureka-server/pom.xml b/backend-java/eureka-server/pom.xml new file mode 100644 index 0000000..b97c511 --- /dev/null +++ b/backend-java/eureka-server/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + eureka-server + jar + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/eureka-server/src/main/java/com/jiebanke/eureka/EurekaServerApplication.java b/backend-java/eureka-server/src/main/java/com/jiebanke/eureka/EurekaServerApplication.java new file mode 100644 index 0000000..136165e --- /dev/null +++ b/backend-java/eureka-server/src/main/java/com/jiebanke/eureka/EurekaServerApplication.java @@ -0,0 +1,13 @@ +package com.jiebanke.eureka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaServerApplication { + public static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/eureka-server/src/main/resources/application.yml b/backend-java/eureka-server/src/main/resources/application.yml new file mode 100644 index 0000000..b4c0e4b --- /dev/null +++ b/backend-java/eureka-server/src/main/resources/application.yml @@ -0,0 +1,17 @@ +server: + port: 8761 + +spring: + application: + name: eureka-server + +eureka: + instance: + hostname: localhost + client: + register-with-eureka: false + fetch-registry: false + service-url: + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ + server: + enable-self-preservation: false \ No newline at end of file diff --git a/backend-java/eureka-server/target/classes/application.yml b/backend-java/eureka-server/target/classes/application.yml new file mode 100644 index 0000000..b4c0e4b --- /dev/null +++ b/backend-java/eureka-server/target/classes/application.yml @@ -0,0 +1,17 @@ +server: + port: 8761 + +spring: + application: + name: eureka-server + +eureka: + instance: + hostname: localhost + client: + register-with-eureka: false + fetch-registry: false + service-url: + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ + server: + enable-self-preservation: false \ No newline at end of file diff --git a/backend-java/eureka-server/target/classes/com/jiebanke/eureka/EurekaServerApplication.class b/backend-java/eureka-server/target/classes/com/jiebanke/eureka/EurekaServerApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..471a7add2d8735b98a0c32127e4eb602f88df12b GIT binary patch literal 808 zcmb7Cv2qhJ5Pfo-oO2iw90&wLLFbqObu@;|BqmfhFoPLWQ0Xr6A^T49=yV~!MFm5_ z2k=putV9@JaTGMW(ysRHd#n9@_45~i*LV`4!|*IKC0{6+3cZwE)>bYBKMQUu?S-@_ zt5u;g;gr!4x(vGuaUpmibk5(W3z<2FtyfAbcg(OcJeo0dC+1wn*uZulo7iI5NtBlF z>N1t~qeu(MFi1=$ikYy=-*19l_eE6<&y)Y4G^Tc?P?}+X_&Hfqn>wp>K0cboxQo3A z1BQ{YIj>eeKDVNjUyWVz)ELL7A=nVw$35I9IkqO?blAY16r!reZJm-f_{?Z0FCCv4 zJy&^cz1l_}3j4GdA#6_C_gat{;;E@^Cf_PQy2F2b&I>P|;ow89ohs!_RmwNh+PLs} zgnr!0F^oXe&Jfu^Qr-;YlxEW)ODx~C0%V1$&$*UvUZ~5zD^!K$;b+=QZc-lODQL8` zf{|8x^eQ1{O0I8df!4iChOC{Hg1va|9L?Ec8J}<4n=(N%K!%S25j^LyPwwe fF=as25ml`*df3MUig)lZ*aJKw@83Qqe+XOyv+vzG literal 0 HcmV?d00001 diff --git a/backend-java/gateway-service/pom.xml b/backend-java/gateway-service/pom.xml new file mode 100644 index 0000000..82c3ba4 --- /dev/null +++ b/backend-java/gateway-service/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + gateway-service + jar + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/gateway-service/src/main/java/com/jiebanke/gateway/GatewayApplication.java b/backend-java/gateway-service/src/main/java/com/jiebanke/gateway/GatewayApplication.java new file mode 100644 index 0000000..f73b552 --- /dev/null +++ b/backend-java/gateway-service/src/main/java/com/jiebanke/gateway/GatewayApplication.java @@ -0,0 +1,34 @@ +package com.jiebanke.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +@EnableDiscoveryClient +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } + + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("auth-service", r -> r.path("/api/auth/**") + .uri("lb://auth-service")) + .route("user-service", r -> r.path("/api/users/**") + .uri("lb://user-service")) + .route("travel-service", r -> r.path("/api/travel/**") + .uri("lb://travel-service")) + .route("animal-service", r -> r.path("/api/animals/**") + .uri("lb://animal-service")) + .route("order-service", r -> r.path("/api/orders/**") + .uri("lb://order-service")) + .route("promotion-service", r -> r.path("/api/promotion/**") + .uri("lb://promotion-service")) + .build(); + } +} \ No newline at end of file diff --git a/backend-java/gateway-service/src/main/resources/application.yml b/backend-java/gateway-service/src/main/resources/application.yml new file mode 100644 index 0000000..20b2330 --- /dev/null +++ b/backend-java/gateway-service/src/main/resources/application.yml @@ -0,0 +1,38 @@ +server: + port: 8080 + +spring: + application: + name: gateway-service + cloud: + gateway: + routes: + - id: auth-service + uri: lb://auth-service + predicates: + - Path=/api/auth/** + - id: user-service + uri: lb://user-service + predicates: + - Path=/api/users/** + - id: travel-service + uri: lb://travel-service + predicates: + - Path=/api/travel/** + - id: animal-service + uri: lb://animal-service + predicates: + - Path=/api/animals/** + - id: order-service + uri: lb://order-service + predicates: + - Path=/api/orders/** + - id: promotion-service + uri: lb://promotion-service + predicates: + - Path=/api/promotion/** + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/backend-java/gateway-service/target/classes/application.yml b/backend-java/gateway-service/target/classes/application.yml new file mode 100644 index 0000000..20b2330 --- /dev/null +++ b/backend-java/gateway-service/target/classes/application.yml @@ -0,0 +1,38 @@ +server: + port: 8080 + +spring: + application: + name: gateway-service + cloud: + gateway: + routes: + - id: auth-service + uri: lb://auth-service + predicates: + - Path=/api/auth/** + - id: user-service + uri: lb://user-service + predicates: + - Path=/api/users/** + - id: travel-service + uri: lb://travel-service + predicates: + - Path=/api/travel/** + - id: animal-service + uri: lb://animal-service + predicates: + - Path=/api/animals/** + - id: order-service + uri: lb://order-service + predicates: + - Path=/api/orders/** + - id: promotion-service + uri: lb://promotion-service + predicates: + - Path=/api/promotion/** + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/backend-java/gateway-service/target/classes/com/jiebanke/gateway/GatewayApplication.class b/backend-java/gateway-service/target/classes/com/jiebanke/gateway/GatewayApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..b9f4f5fd9364347ff976044758246485a9dd5699 GIT binary patch literal 3944 zcmb_eX>-&@5PgGfv|a=@YcRJtV!$?sxIhKd(XfbNyXh8< zaZBX6Ga-yo?u#3I$~Y6gPnJryWpdwg9Sw~Pt>b)>8#Z@FjZ67)VfqYHF7HCeE zwkmSVVQBBVo{P*H@;%EL&2|r`u^eq0G7LSgH)@niQa7ObEW02)BM#6P%Cg-HMu}^a#cKuw$G3`N z*eYAnE5~tt#Sn61S2AcN7<_iA(I>c*O=AN#YUp6-PmE!G9_iSG&BRCLI@xlmYvC@= z6R6H=z*2aCLFa)#wyP|>Ny`)&N~5hhwqZL%6Q@*}V%SK#DmDhbWg8=bW6I<)PFDux zb2oNq=wUd2AAGJFr(-8}krB#whC6ZQRx)B#&L`LZVpvLcJ2k}&H?;wr-xn`Jf1YB*g} z&_kZ2aF))_;3dnDR5yBh(s&p-4d)q-)yvo#V|84>MJjkD?vF7HCRXjbL)_=OHkA?; zj1(>tEA0FcLz!M>MH+*+qG5<3Td(~wbRCa^uD6E3vt((B-3-+mr+(XIC00klz^Jev zuN$EzzA_)#F|AG9NN;}% zbg8$5luMduqd)ypPllrr?q%YQNbMpiF(1ngOLb zE3R^YAwA>6vgJ{3_b?xex+l8$~1kmif6 z7^uiz*KtkBUW;V!=y*!W-i~A+>M)h;gGlz7jxiKsK` z!}oLtM%L9ttM&{u{K(KT|59pn!UJQ_R81EiHj|qT)RO?sG@|aCo*I1_^k(#K>iHe1 z-)OK1X?n_OK*tjGR14^9DSb6#8CojJhiJ8&OJ!KkA85TA>8EK1v{IwB5k?B_SVeM% z)mTF-*3ww+w2t1=n7d@6)En3_jrEx=Gw1}S(UsXfgFWGB?+o^bqXRQI6pkL8!9(Hb z$PA9E(KL?zN;sh}+sJ014UN>mFCz<9({Bsyy%XyRq5~(eiH^Tb+1VcMU-6YJ!AbOi zwWcLO$Wm$LNeZ9zx~-@2Pn^2ekU29njdPhx(|9D>Gz`EB3fan70NQXBj{yWn+?osS zaRv880$e-6txAL=TFwRcw1VRaaH|P!O(GmIdM>zvf*VPITT5{365%Kr=7O^n+(ZK0 zdV<@K2uFD|7o4NuN(pcs1h?^CIAVpTm{L}3l*i`NcR>G1`u7q)ZxbtDz{_|=jb6nY vcvFqu!n=4+jo!yc_*jiT!RPowjqc)We1mTZBSV + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + order-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/OrderApplication.java b/backend-java/order-service/src/main/java/com/jiebanke/order/OrderApplication.java new file mode 100644 index 0000000..70f64fc --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/OrderApplication.java @@ -0,0 +1,19 @@ +package com.jiebanke.order; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.jiebanke.order.mapper") +@ComponentScan(basePackages = "com.jiebanke") +public class OrderApplication { + public static void main(String[] args) { + SpringApplication.run(OrderApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/controller/OrderController.java b/backend-java/order-service/src/main/java/com/jiebanke/order/controller/OrderController.java new file mode 100644 index 0000000..5442b50 --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/controller/OrderController.java @@ -0,0 +1,140 @@ +package com.jiebanke.order.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.jiebanke.common.vo.ApiResponse; +import com.jiebanke.order.entity.Order; +import com.jiebanke.order.service.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/orders") +public class OrderController { + + @Autowired + private OrderService orderService; + + /** + * 创建订单 + */ + @PostMapping + public ApiResponse> createOrder( + @RequestHeader("userId") Long userId, + @RequestBody Order order) { + + Long orderId = orderService.createOrder(order, userId); + Order createdOrder = orderService.getOrderById(orderId); + + Map result = new HashMap<>(); + result.put("order", createdOrder); + result.put("message", "订单创建成功"); + + return ApiResponse.success(result); + } + + /** + * 获取订单详情 + */ + @GetMapping("/{orderId}") + public ApiResponse getOrder(@PathVariable Long orderId) { + Order order = orderService.getOrderById(orderId); + return ApiResponse.success(order); + } + + /** + * 获取用户订单列表 + */ + @GetMapping + public ApiResponse> getUserOrders( + @RequestHeader("userId") Long userId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String status) { + + IPage orders = orderService.getUserOrders(userId, page, pageSize, status); + + Map result = new HashMap<>(); + result.put("orders", orders.getRecords()); + result.put("pagination", Map.of( + "page", orders.getCurrent(), + "pageSize", orders.getSize(), + "total", orders.getTotal(), + "totalPages", orders.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 更新订单状态 + */ + @PutMapping("/{orderId}/status") + public ApiResponse> updateOrderStatus( + @PathVariable Long orderId, + @RequestBody Map requestBody, + @RequestHeader("userId") Long userId) { + + String status = requestBody.get("status"); + Order updatedOrder = orderService.updateOrderStatus(orderId, status, userId); + + Map result = new HashMap<>(); + result.put("order", updatedOrder); + result.put("message", "订单状态更新成功"); + + return ApiResponse.success(result); + } + + /** + * 删除订单 + */ + @DeleteMapping("/{orderId}") + public ApiResponse> deleteOrder( + @PathVariable Long orderId, + @RequestHeader("userId") Long userId) { + + boolean deleted = orderService.deleteOrder(orderId, userId); + + Map result = new HashMap<>(); + result.put("message", "订单删除成功"); + result.put("orderId", orderId); + + return ApiResponse.success(result); + } + + /** + * 获取订单统计信息 + */ + @GetMapping("/statistics") + public ApiResponse> getOrderStatistics(@RequestHeader("merchantId") Long merchantId) { + Map statistics = orderService.getOrderStats(merchantId); + return ApiResponse.success(statistics); + } + + /** + * 管理员获取所有订单 + */ + @GetMapping("/admin") + public ApiResponse> getAllOrders( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String status, + @RequestParam(required = false) Long merchantId, + @RequestParam(required = false) Long userId) { + + IPage orders = orderService.getAllOrders(page, pageSize, status, merchantId, userId); + + Map result = new HashMap<>(); + result.put("orders", orders.getRecords()); + result.put("pagination", Map.of( + "page", orders.getCurrent(), + "pageSize", orders.getSize(), + "total", orders.getTotal(), + "totalPages", orders.getPages() + )); + + return ApiResponse.success(result); + } +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/entity/Order.java b/backend-java/order-service/src/main/java/com/jiebanke/order/entity/Order.java new file mode 100644 index 0000000..d7e3494 --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/entity/Order.java @@ -0,0 +1,18 @@ +package com.jiebanke.order.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Order extends BaseEntity { + private Long userId; + private String orderNo; + private BigDecimal amount; + private String status; + private String type; + private String description; +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/mapper/OrderMapper.java b/backend-java/order-service/src/main/java/com/jiebanke/order/mapper/OrderMapper.java new file mode 100644 index 0000000..e4c78fd --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/mapper/OrderMapper.java @@ -0,0 +1,37 @@ +package com.jiebanke.order.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.order.entity.Order; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface OrderMapper extends BaseMapper { + + /** + * 根据用户ID获取订单列表 + * @param userId 用户ID + * @return 订单列表 + */ + @Select("SELECT * FROM orders WHERE user_id = #{userId} ORDER BY created_at DESC") + List selectByUserId(@Param("userId") Long userId); + + /** + * 根据状态获取订单列表 + * @param status 状态 + * @return 订单列表 + */ + @Select("SELECT * FROM orders WHERE status = #{status} ORDER BY created_at DESC") + List selectByStatus(@Param("status") String status); + + /** + * 根据订单号获取订单 + * @param orderNo 订单号 + * @return 订单 + */ + @Select("SELECT * FROM orders WHERE order_no = #{orderNo}") + Order selectByOrderNo(@Param("orderNo") String orderNo); +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/service/OrderService.java b/backend-java/order-service/src/main/java/com/jiebanke/order/service/OrderService.java new file mode 100644 index 0000000..8d5215e --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/service/OrderService.java @@ -0,0 +1,70 @@ +package com.jiebanke.order.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.jiebanke.order.entity.Order; + +import java.util.Map; + +public interface OrderService extends IService { + + /** + * 创建订单 + * @param order 订单信息 + * @param userId 用户ID + * @return 创建的订单ID + */ + Long createOrder(Order order, Long userId); + + /** + * 根据ID获取订单 + * @param orderId 订单ID + * @return 订单信息 + */ + Order getOrderById(Long orderId); + + /** + * 获取用户订单列表 + * @param userId 用户ID + * @param page 页码 + * @param pageSize 每页数量 + * @param status 状态 + * @return 订单分页列表 + */ + IPage getUserOrders(Long userId, Integer page, Integer pageSize, String status); + + /** + * 更新订单状态 + * @param orderId 订单ID + * @param status 新状态 + * @param userId 操作人ID + * @return 更新后的订单 + */ + Order updateOrderStatus(Long orderId, String status, Long userId); + + /** + * 删除订单(软删除) + * @param orderId 订单ID + * @param userId 用户ID + * @return 是否删除成功 + */ + boolean deleteOrder(Long orderId, Long userId); + + /** + * 获取订单统计信息 + * @param merchantId 商家ID + * @return 统计信息 + */ + Map getOrderStats(Long merchantId); + + /** + * 获取所有订单(管理员) + * @param page 页码 + * @param pageSize 每页数量 + * @param status 状态 + * @param merchantId 商家ID + * @param userId 用户ID + * @return 订单分页列表 + */ + IPage getAllOrders(Integer page, Integer pageSize, String status, Long merchantId, Long userId); +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/java/com/jiebanke/order/service/impl/OrderServiceImpl.java b/backend-java/order-service/src/main/java/com/jiebanke/order/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..306fd14 --- /dev/null +++ b/backend-java/order-service/src/main/java/com/jiebanke/order/service/impl/OrderServiceImpl.java @@ -0,0 +1,159 @@ +package com.jiebanke.order.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jiebanke.common.exception.BusinessException; +import com.jiebanke.order.entity.Order; +import com.jiebanke.order.mapper.OrderMapper; +import com.jiebanke.order.service.OrderService; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +public class OrderServiceImpl extends ServiceImpl implements OrderService { + + @Override + public Long createOrder(Order order, Long userId) { + // 生成订单号 + String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + order.setOrderNo(orderNo); + order.setUserId(userId); + + // 设置默认状态 + if (order.getStatus() == null) { + order.setStatus("pending"); + } + + this.save(order); + return order.getId(); + } + + @Override + public Order getOrderById(Long orderId) { + Order order = this.getById(orderId); + if (order == null) { + throw new BusinessException("订单不存在"); + } + return order; + } + + @Override + public IPage getUserOrders(Long userId, Integer page, Integer pageSize, String status) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_id", userId); + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } + + @Override + public Order updateOrderStatus(Long orderId, String status, Long userId) { + Order existingOrder = this.getById(orderId); + if (existingOrder == null) { + throw new BusinessException("订单不存在"); + } + + // 验证状态是否有效 + String[] validStatuses = {"pending", "processing", "completed", "cancelled", "failed"}; + boolean isValidStatus = false; + for (String validStatus : validStatuses) { + if (validStatus.equals(status)) { + isValidStatus = true; + break; + } + } + + if (!isValidStatus) { + throw new BusinessException("无效的订单状态"); + } + + existingOrder.setStatus(status); + this.updateById(existingOrder); + return existingOrder; + } + + @Override + public boolean deleteOrder(Long orderId, Long userId) { + return this.removeById(orderId); + } + + @Override + public Map getOrderStats(Long merchantId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("merchant_id", merchantId); + + int totalOrders = Math.toIntExact(this.count(queryWrapper)); + + QueryWrapper pendingWrapper = new QueryWrapper<>(); + pendingWrapper.eq("merchant_id", merchantId).eq("status", "pending"); + int pendingOrders = Math.toIntExact(this.count(pendingWrapper)); + + QueryWrapper processingWrapper = new QueryWrapper<>(); + processingWrapper.eq("merchant_id", merchantId).eq("status", "processing"); + int processingOrders = Math.toIntExact(this.count(processingWrapper)); + + QueryWrapper completedWrapper = new QueryWrapper<>(); + completedWrapper.eq("merchant_id", merchantId).eq("status", "completed"); + int completedOrders = Math.toIntExact(this.count(completedWrapper)); + + QueryWrapper cancelledWrapper = new QueryWrapper<>(); + cancelledWrapper.eq("merchant_id", merchantId).eq("status", "cancelled"); + int cancelledOrders = Math.toIntExact(this.count(cancelledWrapper)); + + QueryWrapper failedWrapper = new QueryWrapper<>(); + failedWrapper.eq("merchant_id", merchantId).eq("status", "failed"); + int failedOrders = Math.toIntExact(this.count(failedWrapper)); + + // 计算总收入 + QueryWrapper revenueWrapper = new QueryWrapper<>(); + revenueWrapper.eq("merchant_id", merchantId).select("SUM(amount) as totalRevenue"); + Map revenueMap = this.getMap(revenueWrapper); + BigDecimal totalRevenue = revenueMap != null && revenueMap.get("totalRevenue") != null ? + new BigDecimal(revenueMap.get("totalRevenue").toString()) : BigDecimal.ZERO; + + Map stats = new HashMap<>(); + stats.put("totalOrders", totalOrders); + stats.put("pendingOrders", pendingOrders); + stats.put("processingOrders", processingOrders); + stats.put("completedOrders", completedOrders); + stats.put("cancelledOrders", cancelledOrders); + stats.put("failedOrders", failedOrders); + stats.put("totalRevenue", totalRevenue); + + return stats; + } + + @Override + public IPage getAllOrders(Integer page, Integer pageSize, String status, Long merchantId, Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + if (merchantId != null) { + queryWrapper.eq("merchant_id", merchantId); + } + + if (userId != null) { + queryWrapper.eq("user_id", userId); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } +} \ No newline at end of file diff --git a/backend-java/order-service/src/main/resources/application.yml b/backend-java/order-service/src/main/resources/application.yml new file mode 100644 index 0000000..883e008 --- /dev/null +++ b/backend-java/order-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8085 + +spring: + application: + name: order-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto \ No newline at end of file diff --git a/backend-java/pom.xml b/backend-java/pom.xml new file mode 100644 index 0000000..8f56e35 --- /dev/null +++ b/backend-java/pom.xml @@ -0,0 +1,163 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + pom + 结伴客Java后端 + 结伴客Java微服务架构后端系统 + + + eureka-server + gateway-service + auth-service + user-service + travel-service + animal-service + order-service + promotion-service + common + + + + 17 + 17 + UTF-8 + 3.1.0 + 2022.0.3 + 8.0.33 + 3.5.3.1 + 5.9.2 + 5.2.0 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud.version} + pom + import + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis.plus.version} + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + ${spring.boot.version} + + + + + org.springframework.boot + spring-boot-starter-amqp + ${spring.boot.version} + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + ${spring.cloud.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + + + com.jiebanke + common + ${project.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/backend-java/promotion-service/pom.xml b/backend-java/promotion-service/pom.xml new file mode 100644 index 0000000..e7c423b --- /dev/null +++ b/backend-java/promotion-service/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + promotion-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/PromotionApplication.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/PromotionApplication.java new file mode 100644 index 0000000..887e2ee --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/PromotionApplication.java @@ -0,0 +1,19 @@ +package com.jiebanke.promotion; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan({"com.jiebanke.promotion.mapper"}) +@ComponentScan(basePackages = "com.jiebanke") +public class PromotionApplication { + public static void main(String[] args) { + SpringApplication.run(PromotionApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/controller/PromotionController.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/controller/PromotionController.java new file mode 100644 index 0000000..15326b7 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/controller/PromotionController.java @@ -0,0 +1,175 @@ +package com.jiebanke.promotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.jiebanke.common.vo.ApiResponse; +import com.jiebanke.promotion.entity.PromotionActivity; +import com.jiebanke.promotion.entity.RewardRecord; +import com.jiebanke.promotion.service.PromotionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/promotion") +public class PromotionController { + + @Autowired + private PromotionService promotionService; + + /** + * 获取推广活动列表 + */ + @GetMapping("/activities") + public ApiResponse> getPromotionActivities( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String name, + @RequestParam(required = false) String status) { + + IPage activities = promotionService.getPromotionActivities(page, pageSize, name, status); + + Map result = new HashMap<>(); + result.put("activities", activities.getRecords()); + result.put("pagination", Map.of( + "current", activities.getCurrent(), + "pageSize", activities.getSize(), + "total", activities.getTotal(), + "totalPages", activities.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 获取推广活动详情 + */ + @GetMapping("/activities/{id}") + public ApiResponse getPromotionActivity(@PathVariable Long id) { + PromotionActivity activity = promotionService.getPromotionActivityById(id); + return ApiResponse.success(activity); + } + + /** + * 创建推广活动 + */ + @PostMapping("/activities") + public ApiResponse> createPromotionActivity(@RequestBody PromotionActivity activity) { + Long activityId = promotionService.createPromotionActivity(activity); + PromotionActivity createdActivity = promotionService.getPromotionActivityById(activityId); + + Map result = new HashMap<>(); + result.put("activity", createdActivity); + result.put("message", "推广活动创建成功"); + + return ApiResponse.success(result); + } + + /** + * 更新推广活动 + */ + @PutMapping("/activities/{id}") + public ApiResponse> updatePromotionActivity( + @PathVariable Long id, + @RequestBody PromotionActivity activity) { + + PromotionActivity updatedActivity = promotionService.updatePromotionActivity(id, activity); + + Map result = new HashMap<>(); + result.put("activity", updatedActivity); + result.put("message", "推广活动更新成功"); + + return ApiResponse.success(result); + } + + /** + * 删除推广活动 + */ + @DeleteMapping("/activities/{id}") + public ApiResponse> deletePromotionActivity(@PathVariable Long id) { + boolean deleted = promotionService.deletePromotionActivity(id); + + Map result = new HashMap<>(); + result.put("message", "推广活动删除成功"); + result.put("id", id); + + return ApiResponse.success(result); + } + + /** + * 暂停推广活动 + */ + @PostMapping("/activities/{id}/pause") + public ApiResponse> pausePromotionActivity(@PathVariable Long id) { + boolean paused = promotionService.pausePromotionActivity(id); + + Map result = new HashMap<>(); + result.put("message", "推广活动已暂停"); + result.put("id", id); + + return ApiResponse.success(result); + } + + /** + * 恢复推广活动 + */ + @PostMapping("/activities/{id}/resume") + public ApiResponse> resumePromotionActivity(@PathVariable Long id) { + boolean resumed = promotionService.resumePromotionActivity(id); + + Map result = new HashMap<>(); + result.put("message", "推广活动已恢复"); + result.put("id", id); + + return ApiResponse.success(result); + } + + /** + * 获取奖励记录列表 + */ + @GetMapping("/rewards") + public ApiResponse> getRewardRecords( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String user, + @RequestParam(required = false) String rewardType, + @RequestParam(required = false) String status) { + + IPage rewards = promotionService.getRewardRecords(page, pageSize, user, rewardType, status); + + Map result = new HashMap<>(); + result.put("rewards", rewards.getRecords()); + result.put("pagination", Map.of( + "current", rewards.getCurrent(), + "pageSize", rewards.getSize(), + "total", rewards.getTotal(), + "totalPages", rewards.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 发放奖励 + */ + @PostMapping("/rewards/{id}/issue") + public ApiResponse> issueReward(@PathVariable Long id) { + boolean issued = promotionService.issueReward(id); + + Map result = new HashMap<>(); + result.put("message", "奖励已发放"); + result.put("id", id); + + return ApiResponse.success(result); + } + + /** + * 获取推广统计数据 + */ + @GetMapping("/statistics") + public ApiResponse> getPromotionStatistics() { + Map statistics = promotionService.getPromotionStatistics(); + return ApiResponse.success(statistics); + } +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/PromotionActivity.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/PromotionActivity.java new file mode 100644 index 0000000..b58759e --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/PromotionActivity.java @@ -0,0 +1,22 @@ +package com.jiebanke.promotion.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PromotionActivity extends BaseEntity { + private String name; + private String description; + private String rewardType; + private BigDecimal rewardAmount; + private String status; + private LocalDateTime startTime; + private LocalDateTime endTime; + private Integer maxParticipants; + private Integer currentParticipants; +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/RewardRecord.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/RewardRecord.java new file mode 100644 index 0000000..0aabd52 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/entity/RewardRecord.java @@ -0,0 +1,22 @@ +package com.jiebanke.promotion.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RewardRecord extends BaseEntity { + private Long userId; + private String userName; + private String userPhone; + private Long activityId; + private String activityName; + private String rewardType; + private BigDecimal rewardAmount; + private String status; + private LocalDateTime issuedAt; +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/PromotionActivityMapper.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/PromotionActivityMapper.java new file mode 100644 index 0000000..6728ce1 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/PromotionActivityMapper.java @@ -0,0 +1,46 @@ +package com.jiebanke.promotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.promotion.entity.PromotionActivity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +@Mapper +public interface PromotionActivityMapper extends BaseMapper { + + /** + * 根据状态获取推广活动列表 + * @param status 状态 + * @return 推广活动列表 + */ + @Select("SELECT * FROM promotion_activities WHERE status = #{status} ORDER BY created_at DESC") + List selectByStatus(@Param("status") String status); + + /** + * 根据名称模糊查询推广活动 + * @param name 名称 + * @return 推广活动列表 + */ + @Select("SELECT * FROM promotion_activities WHERE name LIKE CONCAT('%', #{name}, '%') ORDER BY created_at DESC") + List selectByName(@Param("name") String name); + + /** + * 暂停推广活动 + * @param id 活动ID + * @return 更新记录数 + */ + @Update("UPDATE promotion_activities SET status = 'paused', updated_at = NOW() WHERE id = #{id}") + int pauseActivity(@Param("id") Long id); + + /** + * 恢复推广活动 + * @param id 活动ID + * @return 更新记录数 + */ + @Update("UPDATE promotion_activities SET status = 'active', updated_at = NOW() WHERE id = #{id}") + int resumeActivity(@Param("id") Long id); +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/RewardRecordMapper.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/RewardRecordMapper.java new file mode 100644 index 0000000..f9e3568 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/mapper/RewardRecordMapper.java @@ -0,0 +1,38 @@ +package com.jiebanke.promotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.promotion.entity.RewardRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +@Mapper +public interface RewardRecordMapper extends BaseMapper { + + /** + * 根据用户ID获取奖励记录列表 + * @param userId 用户ID + * @return 奖励记录列表 + */ + @Select("SELECT * FROM reward_records WHERE user_id = #{userId} ORDER BY created_at DESC") + List selectByUserId(@Param("userId") Long userId); + + /** + * 根据状态获取奖励记录列表 + * @param status 状态 + * @return 奖励记录列表 + */ + @Select("SELECT * FROM reward_records WHERE status = #{status} ORDER BY created_at DESC") + List selectByStatus(@Param("status") String status); + + /** + * 发放奖励 + * @param id 奖励记录ID + * @return 更新记录数 + */ + @Update("UPDATE reward_records SET status = 'issued', issued_at = NOW() WHERE id = #{id}") + int issueReward(@Param("id") Long id); +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/PromotionService.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/PromotionService.java new file mode 100644 index 0000000..75892e8 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/PromotionService.java @@ -0,0 +1,88 @@ +package com.jiebanke.promotion.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.jiebanke.promotion.entity.PromotionActivity; +import com.jiebanke.promotion.entity.RewardRecord; + +import java.util.Map; + +public interface PromotionService extends IService { + + /** + * 获取推广活动列表 + * @param page 页码 + * @param pageSize 每页数量 + * @param name 活动名称 + * @param status 状态 + * @return 推广活动分页列表 + */ + IPage getPromotionActivities(Integer page, Integer pageSize, String name, String status); + + /** + * 获取推广活动详情 + * @param id 活动ID + * @return 推广活动 + */ + PromotionActivity getPromotionActivityById(Long id); + + /** + * 创建推广活动 + * @param activity 活动信息 + * @return 创建的活动ID + */ + Long createPromotionActivity(PromotionActivity activity); + + /** + * 更新推广活动 + * @param id 活动ID + * @param activity 更新的活动信息 + * @return 更新后的活动 + */ + PromotionActivity updatePromotionActivity(Long id, PromotionActivity activity); + + /** + * 删除推广活动 + * @param id 活动ID + * @return 是否删除成功 + */ + boolean deletePromotionActivity(Long id); + + /** + * 暂停推广活动 + * @param id 活动ID + * @return 是否暂停成功 + */ + boolean pausePromotionActivity(Long id); + + /** + * 恢复推广活动 + * @param id 活动ID + * @return 是否恢复成功 + */ + boolean resumePromotionActivity(Long id); + + /** + * 获取奖励记录列表 + * @param page 页码 + * @param pageSize 每页数量 + * @param user 用户 + * @param rewardType 奖励类型 + * @param status 状态 + * @return 奖励记录分页列表 + */ + IPage getRewardRecords(Integer page, Integer pageSize, String user, String rewardType, String status); + + /** + * 发放奖励 + * @param id 奖励记录ID + * @return 是否发放成功 + */ + boolean issueReward(Long id); + + /** + * 获取推广统计数据 + * @return 统计数据 + */ + Map getPromotionStatistics(); +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/impl/PromotionServiceImpl.java b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/impl/PromotionServiceImpl.java new file mode 100644 index 0000000..3d4ed88 --- /dev/null +++ b/backend-java/promotion-service/src/main/java/com/jiebanke/promotion/service/impl/PromotionServiceImpl.java @@ -0,0 +1,181 @@ +package com.jiebanke.promotion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jiebanke.common.exception.BusinessException; +import com.jiebanke.promotion.entity.PromotionActivity; +import com.jiebanke.promotion.entity.RewardRecord; +import com.jiebanke.promotion.mapper.PromotionActivityMapper; +import com.jiebanke.promotion.mapper.RewardRecordMapper; +import com.jiebanke.promotion.service.PromotionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +@Service +public class PromotionServiceImpl extends ServiceImpl implements PromotionService { + + @Autowired + private PromotionActivityMapper promotionActivityMapper; + + @Autowired + private RewardRecordMapper rewardRecordMapper; + + @Override + public IPage getPromotionActivities(Integer page, Integer pageSize, String name, String status) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (name != null && !name.isEmpty()) { + queryWrapper.like("name", name); + } + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } + + @Override + public PromotionActivity getPromotionActivityById(Long id) { + PromotionActivity activity = this.getById(id); + if (activity == null) { + throw new BusinessException("推广活动不存在"); + } + return activity; + } + + @Override + public Long createPromotionActivity(PromotionActivity activity) { + this.save(activity); + return activity.getId(); + } + + @Override + public PromotionActivity updatePromotionActivity(Long id, PromotionActivity activity) { + PromotionActivity existingActivity = this.getById(id); + if (existingActivity == null) { + throw new BusinessException("推广活动不存在"); + } + + // 更新字段 + if (activity.getName() != null) { + existingActivity.setName(activity.getName()); + } + if (activity.getDescription() != null) { + existingActivity.setDescription(activity.getDescription()); + } + if (activity.getRewardType() != null) { + existingActivity.setRewardType(activity.getRewardType()); + } + if (activity.getRewardAmount() != null) { + existingActivity.setRewardAmount(activity.getRewardAmount()); + } + if (activity.getStatus() != null) { + existingActivity.setStatus(activity.getStatus()); + } + if (activity.getStartTime() != null) { + existingActivity.setStartTime(activity.getStartTime()); + } + if (activity.getEndTime() != null) { + existingActivity.setEndTime(activity.getEndTime()); + } + if (activity.getMaxParticipants() != null) { + existingActivity.setMaxParticipants(activity.getMaxParticipants()); + } + if (activity.getCurrentParticipants() != null) { + existingActivity.setCurrentParticipants(activity.getCurrentParticipants()); + } + + this.updateById(existingActivity); + return existingActivity; + } + + @Override + public boolean deletePromotionActivity(Long id) { + return this.removeById(id); + } + + @Override + public boolean pausePromotionActivity(Long id) { + int result = promotionActivityMapper.pauseActivity(id); + return result > 0; + } + + @Override + public boolean resumePromotionActivity(Long id) { + int result = promotionActivityMapper.resumeActivity(id); + return result > 0; + } + + @Override + public IPage getRewardRecords(Integer page, Integer pageSize, String user, String rewardType, String status) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (user != null && !user.isEmpty()) { + queryWrapper.like("user_name", user).or().like("user_phone", user); + } + + if (rewardType != null && !rewardType.isEmpty()) { + queryWrapper.eq("reward_type", rewardType); + } + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return rewardRecordMapper.selectPage(pageObj, queryWrapper); + } + + @Override + public boolean issueReward(Long id) { + int result = rewardRecordMapper.issueReward(id); + return result > 0; + } + + @Override + public Map getPromotionStatistics() { + // 获取活动总数 + int totalActivities = Math.toIntExact(this.count()); + + // 获取进行中的活动数 + QueryWrapper activeWrapper = new QueryWrapper<>(); + activeWrapper.eq("status", "active"); + int activeActivities = Math.toIntExact(this.count(activeWrapper)); + + // 获取奖励记录总数 + int totalRewards = Math.toIntExact(rewardRecordMapper.selectCount(null)); + + // 获取已发放的奖励数 + QueryWrapper issuedWrapper = new QueryWrapper<>(); + issuedWrapper.eq("status", "issued"); + int issuedRewards = Math.toIntExact(rewardRecordMapper.selectCount(issuedWrapper)); + + // 计算总奖励金额 + QueryWrapper amountWrapper = new QueryWrapper<>(); + amountWrapper.eq("status", "issued").select("SUM(reward_amount) as totalAmount"); + Map amountMap = rewardRecordMapper.selectMap(amountWrapper); + BigDecimal totalAmount = amountMap != null && amountMap.get("totalAmount") != null ? + new BigDecimal(amountMap.get("totalAmount").toString()) : BigDecimal.ZERO; + + Map statistics = new HashMap<>(); + statistics.put("totalActivities", totalActivities); + statistics.put("activeActivities", activeActivities); + statistics.put("totalRewards", totalRewards); + statistics.put("issuedRewards", issuedRewards); + statistics.put("totalAmount", totalAmount); + + return statistics; + } +} \ No newline at end of file diff --git a/backend-java/promotion-service/src/main/resources/application.yml b/backend-java/promotion-service/src/main/resources/application.yml new file mode 100644 index 0000000..6d19d0a --- /dev/null +++ b/backend-java/promotion-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8086 + +spring: + application: + name: promotion-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto \ No newline at end of file diff --git a/backend-java/scripts/init-database.sql b/backend-java/scripts/init-database.sql new file mode 100644 index 0000000..a08edcb --- /dev/null +++ b/backend-java/scripts/init-database.sql @@ -0,0 +1,137 @@ +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS jiebanke CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE jiebanke; + +-- 创建管理员表 +CREATE TABLE IF NOT EXISTS admins ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100), + role VARCHAR(20) DEFAULT 'admin', + status VARCHAR(20) DEFAULT 'active', + last_login TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建用户表 +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100) UNIQUE, + phone VARCHAR(20), + real_name VARCHAR(100), + id_card VARCHAR(20), + status VARCHAR(20) DEFAULT 'active', + balance DECIMAL(15,2) DEFAULT 0.00, + credit_score INT DEFAULT 100, + last_login TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建订单表 +CREATE TABLE IF NOT EXISTS orders ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + order_no VARCHAR(50) NOT NULL UNIQUE, + amount DECIMAL(15,2) NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + type VARCHAR(20) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建旅行计划表 +CREATE TABLE IF NOT EXISTS travel_plans ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + destination VARCHAR(100) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + budget DECIMAL(10,2), + interests TEXT, + description TEXT, + visibility VARCHAR(20) DEFAULT 'public', + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建动物表 +CREATE TABLE IF NOT EXISTS animals ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + merchant_id BIGINT NOT NULL, + name VARCHAR(50) NOT NULL, + species VARCHAR(50) NOT NULL, + breed VARCHAR(50), + birth_date DATE, + personality TEXT, + farm_location VARCHAR(255), + price DECIMAL(10,2) NOT NULL, + claim_count INT DEFAULT 0, + status VARCHAR(20) DEFAULT 'available', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建推广活动表 +CREATE TABLE IF NOT EXISTS promotion_activities ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + reward_type VARCHAR(20), + reward_amount DECIMAL(10,2), + status VARCHAR(20) DEFAULT 'active', + start_time TIMESTAMP, + end_time TIMESTAMP, + max_participants INT, + current_participants INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 创建奖励记录表 +CREATE TABLE IF NOT EXISTS reward_records ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + user_name VARCHAR(50), + user_phone VARCHAR(20), + activity_id BIGINT, + activity_name VARCHAR(100), + reward_type VARCHAR(20), + reward_amount DECIMAL(10,2), + status VARCHAR(20) DEFAULT 'pending', + issued_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 插入默认管理员账号 +INSERT INTO admins (username, password, email, role) VALUES +('admin', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@jiebanke.com', 'super_admin'), +('manager', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'manager@jiebanke.com', 'admin'); + +-- 插入测试用户账号 +INSERT INTO users (username, password, email, phone, real_name, id_card, balance, credit_score) VALUES +('user1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'user1@jiebanke.com', '13800138001', '张三', '110101199001011234', 1000.00, 95), +('user2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'user2@jiebanke.com', '13800138002', '李四', '110101199002022345', 500.00, 85); + +-- 创建索引 +CREATE INDEX idx_admins_username ON admins(username); +CREATE INDEX idx_admins_email ON admins(email); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_phone ON users(phone); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_orders_order_no ON orders(order_no); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_travel_plans_user_id ON travel_plans(user_id); +CREATE INDEX idx_travel_plans_destination ON travel_plans(destination); +CREATE INDEX idx_animals_species ON animals(species); +CREATE INDEX idx_animals_status ON animals(status); +CREATE INDEX idx_promotion_activities_status ON promotion_activities(status); +CREATE INDEX idx_reward_records_user_id ON reward_records(user_id); +CREATE INDEX idx_reward_records_activity_id ON reward_records(activity_id); \ No newline at end of file diff --git a/backend-java/start-services.sh b/backend-java/start-services.sh new file mode 100755 index 0000000..5a8040b --- /dev/null +++ b/backend-java/start-services.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# 启动结伴客Java后端服务脚本 + +# 启动顺序:Eureka Server -> 其他服务 -> Gateway + +echo "开始启动结伴客Java后端服务..." + +# 启动Eureka Server +echo "正在启动Eureka Server..." +cd eureka-server +mvn spring-boot:run > eureka.log 2>&1 & +EUREKA_PID=$! +cd .. + +sleep 10 + +# 启动Auth Service +echo "正在启动Auth Service..." +cd auth-service +mvn spring-boot:run > auth.log 2>&1 & +AUTH_PID=$! +cd .. + +sleep 5 + +# 启动User Service +echo "正在启动User Service..." +cd user-service +mvn spring-boot:run > user.log 2>&1 & +USER_PID=$! +cd .. + +sleep 5 + +# 启动Travel Service +echo "正在启动Travel Service..." +cd travel-service +mvn spring-boot:run > travel.log 2>&1 & +TRAVEL_PID=$! +cd .. + +sleep 5 + +# 启动Animal Service +echo "正在启动Animal Service..." +cd animal-service +mvn spring-boot:run > animal.log 2>&1 & +ANIMAL_PID=$! +cd .. + +sleep 5 + +# 启动Order Service +echo "正在启动Order Service..." +cd order-service +mvn spring-boot:run > order.log 2>&1 & +ORDER_PID=$! +cd .. + +sleep 5 + +# 启动Promotion Service +echo "正在启动Promotion Service..." +cd promotion-service +mvn spring-boot:run > promotion.log 2>&1 & +PROMOTION_PID=$! +cd .. + +sleep 5 + +# 启动Gateway Service +echo "正在启动Gateway Service..." +cd gateway-service +mvn spring-boot:run > gateway.log 2>&1 & +GATEWAY_PID=$! +cd .. + +echo "所有服务已启动!" +echo "Eureka Server PID: $EUREKA_PID" +echo "Auth Service PID: $AUTH_PID" +echo "User Service PID: $USER_PID" +echo "Travel Service PID: $TRAVEL_PID" +echo "Animal Service PID: $ANIMAL_PID" +echo "Order Service PID: $ORDER_PID" +echo "Promotion Service PID: $PROMOTION_PID" +echo "Gateway Service PID: $GATEWAY_PID" + +echo "服务访问地址:" +echo "Eureka Dashboard: http://localhost:8761" +echo "API Gateway: http://localhost:8080" +echo "API文档: http://localhost:8080/doc.html" \ No newline at end of file diff --git a/backend-java/stop-services.sh b/backend-java/stop-services.sh new file mode 100755 index 0000000..03c9337 --- /dev/null +++ b/backend-java/stop-services.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# 停止结伴客Java后端服务脚本 + +echo "开始停止结伴客Java后端服务..." + +# 查找并停止所有相关的Java进程 +PIDS=$(ps aux | grep "com.jiebanke" | grep -v grep | awk '{print $2}') + +if [ -z "$PIDS" ]; then + echo "没有找到正在运行的结伴客服务" +else + echo "正在停止以下进程: $PIDS" + kill $PIDS + echo "服务已停止" +fi + +# 清理日志文件 +echo "清理日志文件..." +rm -f eureka-server/eureka.log +rm -f auth-service/auth.log +rm -f user-service/user.log +rm -f travel-service/travel.log +rm -f animal-service/animal.log +rm -f order-service/order.log +rm -f promotion-service/promotion.log +rm -f gateway-service/gateway.log + +echo "清理完成" \ No newline at end of file diff --git a/backend-java/travel-service/pom.xml b/backend-java/travel-service/pom.xml new file mode 100644 index 0000000..197395b --- /dev/null +++ b/backend-java/travel-service/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + travel-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/TravelApplication.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/TravelApplication.java new file mode 100644 index 0000000..891088b --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/TravelApplication.java @@ -0,0 +1,19 @@ +package com.jiebanke.travel; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.jiebanke.travel.mapper") +@ComponentScan(basePackages = "com.jiebanke") +public class TravelApplication { + public static void main(String[] args) { + SpringApplication.run(TravelApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/controller/TravelPlanController.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/controller/TravelPlanController.java new file mode 100644 index 0000000..6f13b3a --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/controller/TravelPlanController.java @@ -0,0 +1,115 @@ +package com.jiebanke.travel.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.jiebanke.common.vo.ApiResponse; +import com.jiebanke.travel.entity.TravelPlan; +import com.jiebanke.travel.service.TravelPlanService; +import com.jiebanke.travel.service.TravelStats; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/travel") +public class TravelPlanController { + + @Autowired + private TravelPlanService travelPlanService; + + /** + * 获取旅行计划列表 + */ + @GetMapping("/plans") + public ApiResponse> getTravelPlans( + @RequestHeader("userId") Long userId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String status) { + + IPage plans = travelPlanService.getTravelPlans(userId, page, pageSize, status); + + Map result = new HashMap<>(); + result.put("plans", plans.getRecords()); + result.put("pagination", Map.of( + "page", plans.getCurrent(), + "pageSize", plans.getSize(), + "total", plans.getTotal(), + "totalPages", plans.getPages() + )); + + return ApiResponse.success(result); + } + + /** + * 获取单个旅行计划详情 + */ + @GetMapping("/plans/{planId}") + public ApiResponse getTravelPlan(@PathVariable Long planId) { + TravelPlan plan = travelPlanService.getTravelPlanById(planId); + return ApiResponse.success(plan); + } + + /** + * 创建旅行计划 + */ + @PostMapping("/plans") + public ApiResponse> createTravelPlan( + @RequestHeader("userId") Long userId, + @RequestBody TravelPlan travelPlan) { + + Long planId = travelPlanService.createTravelPlan(userId, travelPlan); + TravelPlan plan = travelPlanService.getTravelPlanById(planId); + + Map result = new HashMap<>(); + result.put("plan", plan); + result.put("message", "旅行计划创建成功"); + + return ApiResponse.success(result); + } + + /** + * 更新旅行计划 + */ + @PutMapping("/plans/{planId}") + public ApiResponse> updateTravelPlan( + @PathVariable Long planId, + @RequestHeader("userId") Long userId, + @RequestBody TravelPlan travelPlan) { + + TravelPlan updatedPlan = travelPlanService.updateTravelPlan(planId, userId, travelPlan); + + Map result = new HashMap<>(); + result.put("plan", updatedPlan); + result.put("message", "旅行计划更新成功"); + + return ApiResponse.success(result); + } + + /** + * 删除旅行计划 + */ + @DeleteMapping("/plans/{planId}") + public ApiResponse> deleteTravelPlan( + @PathVariable Long planId, + @RequestHeader("userId") Long userId) { + + boolean deleted = travelPlanService.deleteTravelPlan(planId, userId); + + Map result = new HashMap<>(); + result.put("message", "旅行计划删除成功"); + result.put("planId", planId); + + return ApiResponse.success(result); + } + + /** + * 获取用户旅行统计 + */ + @GetMapping("/stats") + public ApiResponse getTravelStats(@RequestHeader("userId") Long userId) { + TravelStats stats = travelPlanService.getUserTravelStats(userId); + return ApiResponse.success(stats); + } +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/entity/TravelPlan.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/entity/TravelPlan.java new file mode 100644 index 0000000..58f1c38 --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/entity/TravelPlan.java @@ -0,0 +1,22 @@ +package com.jiebanke.travel.entity; + +import com.jiebanke.common.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TravelPlan extends BaseEntity { + private Long userId; + private String destination; + private LocalDate startDate; + private LocalDate endDate; + private BigDecimal budget; + private String interests; + private String description; + private String visibility; + private String status; +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/mapper/TravelPlanMapper.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/mapper/TravelPlanMapper.java new file mode 100644 index 0000000..a05f476 --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/mapper/TravelPlanMapper.java @@ -0,0 +1,29 @@ +package com.jiebanke.travel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jiebanke.travel.entity.TravelPlan; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface TravelPlanMapper extends BaseMapper { + + /** + * 根据用户ID获取旅行计划列表 + * @param userId 用户ID + * @return 旅行计划列表 + */ + @Select("SELECT * FROM travel_plans WHERE user_id = #{userId} ORDER BY created_at DESC") + List selectByUserId(@Param("userId") Long userId); + + /** + * 根据状态获取旅行计划列表 + * @param status 状态 + * @return 旅行计划列表 + */ + @Select("SELECT * FROM travel_plans WHERE status = #{status} ORDER BY created_at DESC") + List selectByStatus(@Param("status") String status); +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelPlanService.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelPlanService.java new file mode 100644 index 0000000..3a7f2f2 --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelPlanService.java @@ -0,0 +1,60 @@ +package com.jiebanke.travel.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.jiebanke.travel.entity.TravelPlan; + +import java.util.List; + +public interface TravelPlanService extends IService { + + /** + * 获取旅行计划列表 + * @param userId 用户ID + * @param page 页码 + * @param pageSize 每页数量 + * @param status 状态 + * @return 旅行计划分页列表 + */ + IPage getTravelPlans(Long userId, Integer page, Integer pageSize, String status); + + /** + * 获取单个旅行计划详情 + * @param planId 计划ID + * @return 旅行计划 + */ + TravelPlan getTravelPlanById(Long planId); + + /** + * 创建旅行计划 + * @param userId 用户ID + * @param travelPlan 旅行计划信息 + * @return 创建的旅行计划ID + */ + Long createTravelPlan(Long userId, TravelPlan travelPlan); + + /** + * 更新旅行计划 + * @param planId 计划ID + * @param userId 用户ID + * @param travelPlan 更新的旅行计划信息 + * @return 更新后的旅行计划 + */ + TravelPlan updateTravelPlan(Long planId, Long userId, TravelPlan travelPlan); + + /** + * 删除旅行计划 + * @param planId 计划ID + * @param userId 用户ID + * @return 是否删除成功 + */ + boolean deleteTravelPlan(Long planId, Long userId); + + /** + * 获取用户旅行统计 + * @param userId 用户ID + * @return 统计信息 + */ + TravelStats getUserTravelStats(Long userId); +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelStats.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelStats.java new file mode 100644 index 0000000..e96d73b --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/TravelStats.java @@ -0,0 +1,14 @@ +package com.jiebanke.travel.service; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TravelStats { + private Integer totalPlans; + private Integer completedPlans; + private Integer planningPlans; + private Integer cancelledPlans; + private BigDecimal totalBudget; +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/impl/TravelPlanServiceImpl.java b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/impl/TravelPlanServiceImpl.java new file mode 100644 index 0000000..d9f49cb --- /dev/null +++ b/backend-java/travel-service/src/main/java/com/jiebanke/travel/service/impl/TravelPlanServiceImpl.java @@ -0,0 +1,148 @@ +package com.jiebanke.travel.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jiebanke.common.exception.BusinessException; +import com.jiebanke.travel.entity.TravelPlan; +import com.jiebanke.travel.mapper.TravelPlanMapper; +import com.jiebanke.travel.service.TravelPlanService; +import com.jiebanke.travel.service.TravelStats; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TravelPlanServiceImpl extends ServiceImpl implements TravelPlanService { + + @Override + public IPage getTravelPlans(Long userId, Integer page, Integer pageSize, String status) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (userId != null) { + queryWrapper.eq("user_id", userId); + } + + if (status != null && !status.isEmpty()) { + queryWrapper.eq("status", status); + } + + queryWrapper.orderByDesc("created_at"); + + Page pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10); + return this.page(pageObj, queryWrapper); + } + + @Override + public TravelPlan getTravelPlanById(Long planId) { + TravelPlan travelPlan = this.getById(planId); + if (travelPlan == null) { + throw new BusinessException("旅行计划不存在"); + } + return travelPlan; + } + + @Override + public Long createTravelPlan(Long userId, TravelPlan travelPlan) { + travelPlan.setUserId(userId); + this.save(travelPlan); + return travelPlan.getId(); + } + + @Override + public TravelPlan updateTravelPlan(Long planId, Long userId, TravelPlan travelPlan) { + TravelPlan existingPlan = this.getById(planId); + if (existingPlan == null) { + throw new BusinessException("旅行计划不存在"); + } + + if (!existingPlan.getUserId().equals(userId)) { + throw new BusinessException("没有权限修改此旅行计划"); + } + + // 更新字段 + if (travelPlan.getDestination() != null) { + existingPlan.setDestination(travelPlan.getDestination()); + } + if (travelPlan.getStartDate() != null) { + existingPlan.setStartDate(travelPlan.getStartDate()); + } + if (travelPlan.getEndDate() != null) { + existingPlan.setEndDate(travelPlan.getEndDate()); + } + if (travelPlan.getBudget() != null) { + existingPlan.setBudget(travelPlan.getBudget()); + } + if (travelPlan.getInterests() != null) { + existingPlan.setInterests(travelPlan.getInterests()); + } + if (travelPlan.getDescription() != null) { + existingPlan.setDescription(travelPlan.getDescription()); + } + if (travelPlan.getVisibility() != null) { + existingPlan.setVisibility(travelPlan.getVisibility()); + } + if (travelPlan.getStatus() != null) { + existingPlan.setStatus(travelPlan.getStatus()); + } + + this.updateById(existingPlan); + return existingPlan; + } + + @Override + public boolean deleteTravelPlan(Long planId, Long userId) { + TravelPlan existingPlan = this.getById(planId); + if (existingPlan == null) { + throw new BusinessException("旅行计划不存在"); + } + + if (!existingPlan.getUserId().equals(userId)) { + throw new BusinessException("没有权限删除此旅行计划"); + } + + return this.removeById(planId); + } + + @Override + public TravelStats getUserTravelStats(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_id", userId); + + List plans = this.list(queryWrapper); + + TravelStats stats = new TravelStats(); + stats.setTotalPlans(plans.size()); + + int completedPlans = 0; + int planningPlans = 0; + int cancelledPlans = 0; + java.math.BigDecimal totalBudget = java.math.BigDecimal.ZERO; + + for (TravelPlan plan : plans) { + switch (plan.getStatus()) { + case "completed": + completedPlans++; + break; + case "planning": + planningPlans++; + break; + case "cancelled": + cancelledPlans++; + break; + } + + if (plan.getBudget() != null) { + totalBudget = totalBudget.add(plan.getBudget()); + } + } + + stats.setCompletedPlans(completedPlans); + stats.setPlanningPlans(planningPlans); + stats.setCancelledPlans(cancelledPlans); + stats.setTotalBudget(totalBudget); + + return stats; + } +} \ No newline at end of file diff --git a/backend-java/travel-service/src/main/resources/application.yml b/backend-java/travel-service/src/main/resources/application.yml new file mode 100644 index 0000000..7421ff9 --- /dev/null +++ b/backend-java/travel-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8083 + +spring: + application: + name: travel-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto \ No newline at end of file diff --git a/backend-java/user-service/pom.xml b/backend-java/user-service/pom.xml new file mode 100644 index 0000000..9b148ce --- /dev/null +++ b/backend-java/user-service/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.jiebanke + backend-java + 1.0.0 + + + user-service + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-amqp + + + + + com.jiebanke + common + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend-java/user-service/src/main/java/com/jiebanke/user/UserApplication.java b/backend-java/user-service/src/main/java/com/jiebanke/user/UserApplication.java new file mode 100644 index 0000000..22e22c8 --- /dev/null +++ b/backend-java/user-service/src/main/java/com/jiebanke/user/UserApplication.java @@ -0,0 +1,15 @@ +package com.jiebanke.user; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients(basePackages = "com.jiebanke.user.client") +public class UserApplication { + public static void main(String[] args) { + SpringApplication.run(UserApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-java/user-service/src/main/java/com/jiebanke/user/client/AuthServiceClient.java b/backend-java/user-service/src/main/java/com/jiebanke/user/client/AuthServiceClient.java new file mode 100644 index 0000000..5ed1d77 --- /dev/null +++ b/backend-java/user-service/src/main/java/com/jiebanke/user/client/AuthServiceClient.java @@ -0,0 +1,14 @@ +package com.jiebanke.user.client; + +import com.jiebanke.common.vo.ApiResponse; +import com.jiebanke.user.config.FeignConfig; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "auth-service", configuration = FeignConfig.class) +public interface AuthServiceClient { + + @GetMapping("/api/auth/validate") + ApiResponse validateToken(@RequestHeader("Authorization") String token); +} \ No newline at end of file diff --git a/backend-java/user-service/src/main/java/com/jiebanke/user/config/FeignConfig.java b/backend-java/user-service/src/main/java/com/jiebanke/user/config/FeignConfig.java new file mode 100644 index 0000000..690119c --- /dev/null +++ b/backend-java/user-service/src/main/java/com/jiebanke/user/config/FeignConfig.java @@ -0,0 +1,31 @@ +package com.jiebanke.user.config; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; + +@Configuration +public class FeignConfig { + + @Bean + public RequestInterceptor requestInterceptor() { + return new RequestInterceptor() { + @Override + public void apply(RequestTemplate template) { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + String authorization = request.getHeader("Authorization"); + if (authorization != null) { + template.header("Authorization", authorization); + } + } + } + }; + } +} \ No newline at end of file diff --git a/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRabbitMQService.java b/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRabbitMQService.java new file mode 100644 index 0000000..2645fe9 --- /dev/null +++ b/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRabbitMQService.java @@ -0,0 +1,44 @@ +package com.jiebanke.user.service; + +import com.jiebanke.common.config.RabbitMQConfig; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class UserRabbitMQService { + + @Autowired + private RabbitTemplate rabbitTemplate; + + // 发送用户注册消息 + public void sendUserRegistrationMessage(Long userId, String username) { + Map message = new HashMap<>(); + message.put("userId", userId); + message.put("username", username); + message.put("eventType", "USER_REGISTERED"); + + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + message + ); + } + + // 发送用户更新消息 + public void sendUserUpdateMessage(Long userId, String username) { + Map message = new HashMap<>(); + message.put("userId", userId); + message.put("username", username); + message.put("eventType", "USER_UPDATED"); + + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + message + ); + } +} \ No newline at end of file diff --git a/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRedisService.java b/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRedisService.java new file mode 100644 index 0000000..93546db --- /dev/null +++ b/backend-java/user-service/src/main/java/com/jiebanke/user/service/UserRedisService.java @@ -0,0 +1,44 @@ +package com.jiebanke.user.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +public class UserRedisService { + + @Autowired + private RedisTemplate redisTemplate; + + // 缓存用户信息 + public void cacheUserInfo(Long userId, Object userInfo) { + redisTemplate.opsForValue().set("user:" + userId, userInfo, 30, TimeUnit.MINUTES); + } + + // 获取缓存的用户信息 + public Object getCachedUserInfo(Long userId) { + return redisTemplate.opsForValue().get("user:" + userId); + } + + // 删除缓存的用户信息 + public void removeCachedUserInfo(Long userId) { + redisTemplate.delete("user:" + userId); + } + + // 缓存用户登录状态 + public void cacheUserLoginStatus(String token, Long userId) { + redisTemplate.opsForValue().set("login:token:" + token, userId, 7, TimeUnit.DAYS); + } + + // 获取用户登录状态 + public Long getUserLoginStatus(String token) { + return (Long) redisTemplate.opsForValue().get("login:token:" + token); + } + + // 删除用户登录状态 + public void removeUserLoginStatus(String token) { + redisTemplate.delete("login:token:" + token); + } +} \ No newline at end of file diff --git a/backend-java/user-service/src/main/resources/application.yml b/backend-java/user-service/src/main/resources/application.yml new file mode 100644 index 0000000..efcdc3f --- /dev/null +++ b/backend-java/user-service/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8082 + +spring: + application: + name: user-service + datasource: + url: jdbc:mysql://localhost:3306/jiebanke?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: localhost + port: 6379 + database: 0 + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRabbitMQServiceTest.java b/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRabbitMQServiceTest.java new file mode 100644 index 0000000..5105a95 --- /dev/null +++ b/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRabbitMQServiceTest.java @@ -0,0 +1,53 @@ +package com.jiebanke.user.service; + +import com.jiebanke.common.config.RabbitMQConfig; +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 org.springframework.amqp.rabbit.core.RabbitTemplate; + +import static org.mockito.Mockito.*; + +class UserRabbitMQServiceTest { + + @Mock + private RabbitTemplate rabbitTemplate; + + @InjectMocks + private UserRabbitMQService userRabbitMQService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendUserRegistrationMessage() { + Long userId = 1L; + String username = "testUser"; + + userRabbitMQService.sendUserRegistrationMessage(userId, username); + + verify(rabbitTemplate).convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + anyMap() + ); + } + + @Test + void testSendUserUpdateMessage() { + Long userId = 1L; + String username = "testUser"; + + userRabbitMQService.sendUserUpdateMessage(userId, username); + + verify(rabbitTemplate).convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.USER_ROUTING_KEY, + anyMap() + ); + } +} \ No newline at end of file diff --git a/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRedisServiceTest.java b/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRedisServiceTest.java new file mode 100644 index 0000000..0029b98 --- /dev/null +++ b/backend-java/user-service/src/test/java/com/jiebanke/user/service/UserRedisServiceTest.java @@ -0,0 +1,92 @@ +package com.jiebanke.user.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 org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserRedisServiceTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private UserRedisService userRedisService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Test + void testCacheUserInfo() { + Long userId = 1L; + String userInfo = "testUser"; + + userRedisService.cacheUserInfo(userId, userInfo); + + verify(valueOperations).set(eq("user:" + userId), eq(userInfo), anyLong(), any()); + } + + @Test + void testGetCachedUserInfo() { + Long userId = 1L; + String userInfo = "testUser"; + when(valueOperations.get("user:" + userId)).thenReturn(userInfo); + + Object result = userRedisService.getCachedUserInfo(userId); + + assertEquals(userInfo, result); + verify(valueOperations).get("user:" + userId); + } + + @Test + void testRemoveCachedUserInfo() { + Long userId = 1L; + + userRedisService.removeCachedUserInfo(userId); + + verify(redisTemplate).delete("user:" + userId); + } + + @Test + void testCacheUserLoginStatus() { + String token = "testToken"; + Long userId = 1L; + + userRedisService.cacheUserLoginStatus(token, userId); + + verify(valueOperations).set(eq("login:token:" + token), eq(userId), anyLong(), any()); + } + + @Test + void testGetUserLoginStatus() { + String token = "testToken"; + Long userId = 1L; + when(valueOperations.get("login:token:" + token)).thenReturn(userId); + + Long result = userRedisService.getUserLoginStatus(token); + + assertEquals(userId, result); + verify(valueOperations).get("login:token:" + token); + } + + @Test + void testRemoveUserLoginStatus() { + String token = "testToken"; + + userRedisService.removeUserLoginStatus(token); + + verify(redisTemplate).delete("login:token:" + token); + } +} \ No newline at end of file diff --git a/backend/.env b/backend/.env index 3cfd7ce..14e4d44 100644 --- a/backend/.env +++ b/backend/.env @@ -1,42 +1,15 @@ # 服务器配置 NODE_ENV=development -PORT=3100 +PORT=3000 HOST=0.0.0.0 -ENABLE_SWAGGER=true -# MySQL数据库配置 -DB_HOST=129.211.213.226 -DB_PORT=9527 +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 DB_USER=root -DB_PASSWORD=aiotAiot123! -DB_NAME=jiebandata - -# 测试环境数据库 -TEST_DB_HOST=192.168.0.240 -TEST_DB_PORT=3306 -TEST_DB_USER=root -TEST_DB_PASSWORD=aiot$Aiot123 -TEST_DB_NAME=jiebandata - -# 生产环境数据库 -PROD_DB_HOST=129.211.213.226 -PROD_DB_PORT=9527 -PROD_DB_USER=root -PROD_DB_PASSWORD=aiotAiot123! -PROD_DB_NAME=jiebandata - -# Redis配置 -REDIS_HOST=redis.jiebanke.com -REDIS_PORT=6379 -REDIS_PASSWORD= -REDIS_DB=0 - -# RabbitMQ配置 -RABBITMQ_HOST=rabbitmq.jiebanke.com -RABBITMQ_PORT=5672 -RABBITMQ_USERNAME=guest -RABBITMQ_PASSWORD=guest -RABBITMQ_VHOST=/ +DB_PASSWORD= +DB_NAME=jiebanke_dev +DB_NAME_TEST=jiebanke_test # JWT配置 JWT_SECRET=your-super-secret-jwt-key-change-this-in-production @@ -48,4 +21,18 @@ WECHAT_SECRET=your-wechat-secret # 文件上传配置 UPLOAD_MAX_SIZE=10485760 -UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif \ No newline at end of file +UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif + +# Redis配置(可选) +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# MySQL连接池配置 +DB_CONNECTION_LIMIT=10 +DB_CHARSET=utf8mb4 +DB_TIMEZONE=+08:00 + +# 调试配置 +DEBUG=jiebanke:* +LOG_LEVEL=info \ No newline at end of file diff --git a/backend/README_SCRIPTS.md b/backend/README_SCRIPTS.md new file mode 100644 index 0000000..b82f278 --- /dev/null +++ b/backend/README_SCRIPTS.md @@ -0,0 +1,181 @@ +# 结伴客后端服务管理脚本 + +## 概述 + +本目录包含用于管理结伴客后端服务的一组脚本,包括启动、停止、重启和状态检查等功能。 + +## 脚本说明 + +### start.sh - 启动脚本 + +用于启动结伴客后端服务。 + +#### 使用方法 + +```bash +# 生产模式启动(默认) +./start.sh + +# 开发模式启动(支持热重载) +./start.sh dev + +# 显示帮助信息 +./start.sh help +``` + +#### 功能特点 + +- 自动检查并安装依赖(如果未安装) +- 自动复制环境变量文件(如果不存在) +- 支持生产模式和开发模式 +- 开发模式下优先使用 nodemon(如果已安装) + +### stop.sh - 停止脚本 + +用于停止正在运行的结伴客后端服务。 + +#### 使用方法 + +```bash +# 停止服务 +./stop.sh + +# 显示帮助信息 +./stop.sh help +``` + +#### 功能特点 + +- 自动查找并停止所有相关的后端服务进程 +- 优雅地停止进程,超时后强制终止 +- 显示详细的进程停止信息 + +### restart.sh - 重启脚本 + +用于重启结伴客后端服务。 + +#### 使用方法 + +```bash +# 生产模式重启(默认) +./restart.sh + +# 开发模式重启 +./restart.sh dev + +# 显示帮助信息 +./restart.sh help +``` + +#### 功能特点 + +- 结合了 stop.sh 和 start.sh 的所有功能 +- 支持生产模式和开发模式 +- 在停止和启动之间添加了延迟以确保服务完全停止 + +### status.sh - 状态检查脚本 + +用于检查结伴客后端服务的运行状态。 + +#### 使用方法 + +```bash +# 检查服务状态 +./status.sh + +# 显示详细信息 +./status.sh detail + +# 显示帮助信息 +./status.sh help +``` + +#### 功能特点 + +- 显示服务是否正在运行 +- 显示相关的进程信息 +- 详细模式下显示端口占用、工作目录等信息 + +## 使用示例 + +### 日常使用 + +```bash +# 进入后端目录 +cd backend + +# 启动服务 +./start.sh + +# 检查服务状态 +./status.sh + +# 重启服务 +./restart.sh + +# 停止服务 +./stop.sh +``` + +### 开发环境使用 + +```bash +# 进入后端目录 +cd backend + +# 以开发模式启动(支持热重载) +./start.sh dev + +# 检查服务状态 +./status.sh + +# 重启服务(保持开发模式) +./restart.sh dev + +# 停止服务 +./stop.sh +``` + +## 注意事项 + +1. 首次运行脚本前,请确保已安装 Node.js 和 npm +2. 脚本会自动检查依赖并安装(如果未安装) +3. 如果没有安装 nodemon,开发模式将回退到使用 node +4. 脚本需要在后端项目根目录下运行 +5. 确保运行脚本的用户具有足够的权限 + +## 故障排除 + +### 权限问题 + +如果遇到权限问题,请为脚本添加执行权限: + +```bash +chmod +x start.sh stop.sh restart.sh status.sh +``` + +或者使用 npm 命令: + +```bash +npm run start-scripts +``` + +### 服务无法启动 + +1. 检查端口是否被占用: + ```bash + netstat -tlnp | grep :3000 + ``` + +2. 检查环境变量配置是否正确 + +3. 查看详细日志信息 + +### 服务无法停止 + +1. 脚本会自动等待10秒后强制终止进程 +2. 如果仍有问题,可以手动终止进程: + ```bash + ps aux | grep "node src/server.js" + kill -9 + ``` \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 682a115..9175f93 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,12 +6,14 @@ "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", + "build": "echo \"Building backend project...\" && npm run lint", "test": "jest", "lint": "eslint src/**/*.js", "migrate": "node src/utils/migrate.js", "init-test-data": "node scripts/init-test-data.js", "test-api": "node scripts/test-api-endpoints.js", - "test-db": "node scripts/test-database-connection.js" + "test-db": "node scripts/test-database-connection.js", + "start-scripts": "chmod +x *.sh" }, "keywords": [ "mini-program", @@ -48,10 +50,6 @@ "devDependencies": { "eslint": "^8.56.0", "jest": "^29.7.0", - "nodemon": "^3.1.10", - "supertest": "^6.3.3" - }, - "engines": { - "node": ">=16.0.0" + "nodemon": "^3.1.10" } -} +} \ No newline at end of file diff --git a/backend/restart.sh b/backend/restart.sh new file mode 100755 index 0000000..8900545 --- /dev/null +++ b/backend/restart.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# 结伴客后端服务重启脚本 + +# 设置颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 停止服务 +stop_server() { + echo -e "${BLUE}正在停止结伴客后端服务...${NC}" + + # 查找并停止结伴客后端服务进程 + PIDS=$(ps aux | grep "node src/server.js" | grep -v grep | awk '{print $2}') + + if [ -z "$PIDS" ]; then + echo -e "${YELLOW}未找到正在运行的结伴客后端服务进程${NC}" + return 0 + fi + + echo -e "${BLUE}找到以下结伴客后端服务进程: $PIDS${NC}" + + for PID in $PIDS; do + echo -e "${BLUE}正在停止进程 $PID...${NC}" + kill $PID + + # 等待进程结束 + COUNT=0 + while kill -0 $PID 2>/dev/null; do + sleep 1 + COUNT=$((COUNT + 1)) + if [ $COUNT -gt 10 ]; then + echo -e "${YELLOW}进程 $PID 未能正常停止,正在强制终止...${NC}" + kill -9 $PID + break + fi + done + + echo -e "${GREEN}进程 $PID 已停止${NC}" + done + + echo -e "${GREEN}结伴客后端服务已停止${NC}" +} + +# 启动服务 +start_server() { + echo -e "${BLUE}正在启动结伴客后端服务...${NC}" + + # 检查是否提供了参数 + if [ "$1" = "dev" ]; then + # 开发模式 + if command -v nodemon &> /dev/null; then + nodemon src/server.js + else + echo -e "${YELLOW}未安装 nodemon,使用 node 运行...${NC}" + node src/server.js + fi + else + # 生产模式 + node src/server.js + fi +} + +# 重启服务 +restart_server() { + stop_server + sleep 2 + start_server "$1" +} + +# 显示帮助信息 +show_help() { + echo "结伴客后端服务重启脚本" + echo "" + echo "使用方法:" + echo " ./restart.sh - 重启服务(生产模式)" + echo " ./restart.sh dev - 重启服务(开发模式)" + echo " ./restart.sh help - 显示帮助信息" + echo "" + echo "说明:" + echo " 生产模式: 使用 node 直接运行服务" + echo " 开发模式: 使用 nodemon 运行服务(支持热重载)" +} + +# 主逻辑 +main() { + echo -e "${GREEN}========== 结伴客后端服务重启脚本 ==========${NC}" + + # 检查参数 + case "$1" in + "help"|"-h"|"--help") + show_help + ;; + "dev") + restart_server "dev" + ;; + *) + restart_server + ;; + esac +} + +# 执行主逻辑 +main "$@" \ No newline at end of file diff --git a/backend/scripts/test-database-connection.js b/backend/scripts/test-database-connection.js index 24317d0..7ddc924 100644 --- a/backend/scripts/test-database-connection.js +++ b/backend/scripts/test-database-connection.js @@ -8,13 +8,13 @@ const mysql = require('mysql2/promise'); const config = require('../config/env'); -// 数据库配置 +// 数据库配置 - 使用环境变量,优先使用开发环境配置 const dbConfig = { - host: process.env.DB_HOST || '129.211.213.226', - port: process.env.DB_PORT || 9527, + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'Aiot123', - database: process.env.DB_NAME || 'jiebandata', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'jiebanke_dev', charset: process.env.DB_CHARSET || 'utf8mb4', timezone: process.env.DB_TIMEZONE || '+08:00', connectTimeout: 10000 diff --git a/backend/start.sh b/backend/start.sh new file mode 100755 index 0000000..609bca5 --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# 结伴客后端服务启动脚本 + +# 设置颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 检查是否已经安装依赖 +check_dependencies() { + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}检测到未安装依赖,正在安装...${NC}" + npm install + if [ $? -ne 0 ]; then + echo -e "${RED}依赖安装失败!${NC}" + exit 1 + fi + echo -e "${GREEN}依赖安装完成!${NC}" + fi +} + +# 检查环境变量文件 +check_env() { + if [ ! -f ".env" ]; then + echo -e "${YELLOW}未找到 .env 文件,正在复制示例文件...${NC}" + if [ -f ".env.example" ]; then + cp .env.example .env + echo -e "${GREEN}.env 文件已创建,请根据需要修改配置!${NC}" + else + echo -e "${RED}未找到 .env.example 文件!${NC}" + fi + fi +} + +# 启动服务 +start_server() { + echo -e "${GREEN}正在启动结伴客后端服务...${NC}" + + # 检查是否提供了参数 + if [ "$1" = "dev" ]; then + # 开发模式 + if command -v nodemon &> /dev/null; then + nodemon src/server.js + else + echo -e "${YELLOW}未安装 nodemon,使用 node 运行...${NC}" + node src/server.js + fi + else + # 生产模式 + node src/server.js + fi +} + +# 显示帮助信息 +show_help() { + echo "结伴客后端服务启动脚本" + echo "" + echo "使用方法:" + echo " ./start.sh - 以生产模式启动服务" + echo " ./start.sh dev - 以开发模式启动服务" + echo " ./start.sh help - 显示帮助信息" + echo "" + echo "说明:" + echo " 生产模式: 使用 node 直接运行服务" + echo " 开发模式: 使用 nodemon 运行服务(支持热重载)" +} + +# 主逻辑 +main() { + echo -e "${GREEN}========== 结伴客后端服务启动脚本 ==========${NC}" + + # 检查参数 + case "$1" in + "help"|"-h"|"--help") + show_help + ;; + "dev") + check_dependencies + check_env + start_server "dev" + ;; + *) + check_dependencies + check_env + start_server + ;; + esac +} + +# 执行主逻辑 +main "$@" \ No newline at end of file diff --git a/backend/status.sh b/backend/status.sh new file mode 100755 index 0000000..32f6cc0 --- /dev/null +++ b/backend/status.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# 结伴客后端服务状态检查脚本 + +# 设置颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 检查服务状态 +check_status() { + echo -e "${BLUE}正在检查结伴客后端服务状态...${NC}" + + # 查找结伴客后端服务进程 + PROCESSES=$(ps aux | grep "node src/server.js" | grep -v grep) + PIDS=$(echo "$PROCESSES" | awk '{print $2}') + + if [ -z "$PIDS" ]; then + echo -e "${RED}状态: 未运行${NC}" + return 1 + else + echo -e "${GREEN}状态: 运行中${NC}" + echo -e "${BLUE}进程信息:${NC}" + echo "$PROCESSES" + return 0 + fi +} + +# 显示详细信息 +show_details() { + echo -e "${BLUE}========== 结伴客后端服务详细信息 ==========${NC}" + + # 显示进程信息 + echo -e "${BLUE}进程信息:${NC}" + ps aux | grep "node src/server.js" | grep -v grep || echo -e "${YELLOW}未找到相关进程${NC}" + + # 显示端口占用情况 + echo -e "${BLUE}端口占用情况:${NC}" + netstat -tlnp | grep :3000 || echo -e "${YELLOW}未检测到3000端口占用${NC}" + + # 显示工作目录 + echo -e "${BLUE}当前工作目录:${NC}" + echo "$(pwd)" + + # 显示Node.js版本 + echo -e "${BLUE}Node.js版本:${NC}" + node --version || echo -e "${YELLOW}未安装Node.js${NC}" + + # 显示npm版本 + echo -e "${BLUE}npm版本:${NC}" + npm --version || echo -e "${YELLOW}未安装npm${NC}" +} + +# 显示帮助信息 +show_help() { + echo "结伴客后端服务状态检查脚本" + echo "" + echo "使用方法:" + echo " ./status.sh - 检查服务状态" + echo " ./status.sh detail - 显示详细信息" + echo " ./status.sh help - 显示帮助信息" +} + +# 主逻辑 +main() { + echo -e "${GREEN}========== 结伴客后端服务状态检查 ==========${NC}" + + # 检查参数 + case "$1" in + "help"|"-h"|"--help") + show_help + ;; + "detail") + show_details + ;; + *) + if check_status; then + echo -e "${GREEN}结伴客后端服务正在正常运行${NC}" + else + echo -e "${RED}结伴客后端服务未运行${NC}" + exit 1 + fi + ;; + esac +} + +# 执行主逻辑 +main "$@" \ No newline at end of file diff --git a/backend/stop.sh b/backend/stop.sh new file mode 100755 index 0000000..a95a48a --- /dev/null +++ b/backend/stop.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# 结伴客后端服务停止脚本 + +# 设置颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 停止服务 +stop_server() { + echo -e "${GREEN}正在停止结伴客后端服务...${NC}" + + # 查找并停止结伴客后端服务进程 + PIDS=$(ps aux | grep "node src/server.js" | grep -v grep | awk '{print $2}') + + if [ -z "$PIDS" ]; then + echo -e "${YELLOW}未找到正在运行的结伴客后端服务进程${NC}" + return 0 + fi + + echo -e "${GREEN}找到以下结伴客后端服务进程: $PIDS${NC}" + + for PID in $PIDS; do + echo -e "${GREEN}正在停止进程 $PID...${NC}" + kill $PID + + # 等待进程结束 + COUNT=0 + while kill -0 $PID 2>/dev/null; do + sleep 1 + COUNT=$((COUNT + 1)) + if [ $COUNT -gt 10 ]; then + echo -e "${YELLOW}进程 $PID 未能正常停止,正在强制终止...${NC}" + kill -9 $PID + break + fi + done + + echo -e "${GREEN}进程 $PID 已停止${NC}" + done + + echo -e "${GREEN}结伴客后端服务已停止${NC}" +} + +# 显示帮助信息 +show_help() { + echo "结伴客后端服务停止脚本" + echo "" + echo "使用方法:" + echo " ./stop.sh - 停止所有结伴客后端服务进程" + echo " ./stop.sh help - 显示帮助信息" +} + +# 主逻辑 +main() { + echo -e "${GREEN}========== 结伴客后端服务停止脚本 ==========${NC}" + + # 检查参数 + case "$1" in + "help"|"-h"|"--help") + show_help + ;; + *) + stop_server + ;; + esac +} + +# 执行主逻辑 +main "$@" \ No newline at end of file diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d55a556..faa167c 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,7 +1,9 @@ -# 🚀 部署指南 +# 📦 部署指南 - 文件同步说明 -## 📋 部署概述 -本文档详细描述了结伴客项目的部署流程,包括开发环境、测试环境和生产环境的部署步骤。 +## 📋 当前部署范围 +本文档说明了结伴客项目的文件同步流程。当前部署仅涉及文件同步操作,不包括自动构建、环境配置或服务重启等后续步骤。 + +注意:本文档适用于需要了解当前部署流程的开发人员和运维人员。请知悉当前部署流程的限制和要求。 ## 🛠️ 环境要求 @@ -456,4 +458,292 @@ rclone copy backup-* remote:backups/ ``` --- -*最后更新: 2024年* 📅 \ No newline at end of file +*最后更新: 2024年* 📅 + +# 结伴客项目部署指南 + +## 概述 + +n g本文档详细说明了如何将结伴客项目部署到生产环境。项目包含三个主要模块: +1. 后端服务 (backend) +2. 后台管理系统 (admin-system) +3. 官方网站 (website) + +注意:微信小程序不需要部署到远程服务器,需要使用微信开发者工具进行构建和上传。 + +## 部署环境要求 + +### 服务器要求 +- CentOS 7+ (推荐 CentOS 8) +- Node.js 16.x+ +- MySQL 8.0+ +- Nginx 1.18+ +- Docker 和 Docker Compose (可选,用于容器化部署) + +### 本地开发环境要求 +- Node.js 16.x+ +- SSH 客户端 +- rsync (用于文件同步) + +## 自动部署脚本 + +项目提供了自动部署脚本,支持 Linux/Mac 和 Windows 系统。 + +### Linux/Mac 部署脚本 + +脚本位置:`scripts/deploy.sh` + +使用方法: +```bash +# 进入脚本目录 +cd scripts + +# 给脚本添加执行权限 +chmod +x deploy.sh + +# 部署所有模块 +./deploy.sh all + +# 部署指定模块 +./deploy.sh backend # 部署后端服务 +./deploy.sh admin # 部署后台管理系统 +./deploy.sh website # 部署官方网站 +./deploy.sh mini-program # 构建微信小程序 +``` + +### Windows PowerShell 部署脚本 + +脚本位置:`scripts/deploy.ps1` + +使用方法: +```powershell +# 进入脚本目录 +cd scripts + +# 部署所有模块 +.\deploy.ps1 all + +# 部署指定模块 +.\deploy.ps1 backend # 部署后端服务 +.\deploy.ps1 admin # 部署后台管理系统 +.\deploy.ps1 website # 部署官方网站 +``` + +## 手动部署 + +### 后端服务部署 + +1. 构建项目: +```bash +cd backend +npm run build +``` + +2. 同步文件到服务器: +```bash +rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + --exclude logs \ + ./ root@1.13.156.49:/data/nodejs/jiebanke/ +``` + +3. 在服务器上安装依赖: +```bash +ssh root@1.13.156.49 +cd /data/nodejs/jiebanke +npm install --production +``` + +4. 重启服务: +```bash +# 使用 PM2 管理服务(推荐) +pm2 restart jiebanke-backend + +# 或使用 systemd +systemctl restart jiebanke-backend +``` + +### 后台管理系统部署 + +1. 构建项目: +```bash +cd admin-system +npm run build +``` + +2. 同步文件到服务器: +```bash +rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + --exclude dist \ + ./ root@1.13.156.49:/data/vue/jiebanke/ +``` + +3. 在服务器上安装依赖: +```bash +ssh root@1.13.156.49 +cd /data/vue/jiebanke +npm install --production +``` + +### 官方网站部署 + +1. 同步文件到服务器: +```bash +rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + ./website/ root@1.13.156.49:/data/website/jiebanke/ +``` + +2. 配置 Nginx 服务器指向该目录。 + +### 微信小程序构建 + +微信小程序需要使用微信开发者工具进行构建和上传: +1. 打开微信开发者工具 +2. 导入 `mini-program` 目录 +3. 点击"上传"按钮 +4. 在微信公众平台提交审核 + +## 容器化部署 + +项目支持使用 Docker 进行容器化部署。 + +### 使用 docker-compose 部署 + +```bash +# 构建并启动所有服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs backend +``` + +### 单独构建镜像 + +每个模块都可以单独构建 Docker 镜像: + +```bash +# 后端服务 +cd backend +docker build -t jiebanke/backend . + +# 后台管理系统 +cd admin-system +docker build -t jiebanke/admin . + +# 官方网站 +cd website +docker build -t jiebanke/website . +``` + +## 部署目录结构 + +远程服务器 CentOS (IP: 1.13.156.49) 上各项目的部署目录如下: + +``` +/ +├── data/ +│ ├── nodejs/ +│ │ └── jiebanke/ # 后端服务部署目录 +│ ├── vue/ +│ │ └── jiebanke/ # 后台管理系统部署目录 +│ └── website/ +│ └── jiebanke/ # 官方网站部署目录 +``` + +## 环境变量配置 + +每个模块都需要配置相应的环境变量: + +### 后端服务 +创建 `.env` 文件: +```env +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=jiebanke +DB_PASSWORD=your_password +DB_NAME=jiebanke + +# JWT 密钥 +JWT_SECRET=your_jwt_secret_key + +# 其他配置... +``` + +### 后台管理系统 +创建 `.env.production` 文件: +```env +VITE_APP_TITLE=结伴客后台管理系统 +VITE_API_BASE_URL=https://api.jiebanke.com +VITE_APP_VERSION=v1.0.0 +``` + +## 常见问题和解决方案 + +### SSH 连接问题 +确保本地 SSH 公钥已添加到远程服务器的 `~/.ssh/authorized_keys` 文件中。 + +### 权限问题 +确保部署脚本有执行权限: +```bash +chmod +x scripts/deploy.sh +``` + +### 文件同步问题 +如果 rsync 命令执行失败,请检查: +1. 本地是否安装了 rsync +2. 远程服务器 SSH 连接是否正常 +3. 目标目录权限是否正确 + +## 监控和日志 + +### 后端服务日志 +```bash +# 使用 PM2 查看日志 +pm2 logs jiebanke-backend + +# 或查看日志文件 +tail -f /var/log/jiebanke/backend.log +``` + +### Nginx 日志 +```bash +# 访问日志 +tail -f /var/log/nginx/access.log + +# 错误日志 +tail -f /var/log/nginx/error.log +``` + +## 回滚方案 + +如果部署出现问题,可以通过以下方式回滚: + +1. 使用 Git 版本回滚: +```bash +git checkout v1.0.0 # 回滚到指定版本 +``` + +2. 使用 Docker 镜像回滚: +```bash +docker-compose down +docker rmi jiebanke/backend:latest +docker pull jiebanke/backend:v1.0.0 +docker-compose up -d +``` + +## 性能优化建议 + +1. 使用 Nginx 反向代理和缓存 +2. 启用 Gzip 压缩 +3. 使用 CDN 加速静态资源 +4. 数据库查询优化 +5. 使用 Redis 缓存热点数据 diff --git a/package-lock.json b/package-lock.json index 34ec71c..c749707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,11 +89,7 @@ "devDependencies": { "eslint": "^8.56.0", "jest": "^29.7.0", - "nodemon": "^3.1.10", - "supertest": "^6.3.3" - }, - "engines": { - "node": ">=16.0.0" + "nodemon": "^3.1.10" } }, "backend/node_modules/bcryptjs": { @@ -3733,19 +3729,6 @@ "sparse-bitfield": "^3.0.3" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@node-ipc/js-queue": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", @@ -3794,16 +3777,6 @@ "node": ">= 8" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -6388,13 +6361,6 @@ "node": ">=8" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.16.1.tgz", @@ -7606,16 +7572,6 @@ "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", "license": "MIT" }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", @@ -7816,13 +7772,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -8508,17 +8457,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -9619,13 +9557,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", @@ -9965,22 +9896,6 @@ "node": ">= 6" } }, - "node_modules/formidable": { - "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/formidable/-/formidable-2.1.5.tgz", - "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", @@ -12039,10 +11954,6 @@ "resolved": "admin-system", "link": true }, - "node_modules/jiebanke-api-tests": { - "resolved": "scripts", - "link": true - }, "node_modules/jiebanke-backend": { "resolved": "backend", "link": true @@ -12051,6 +11962,14 @@ "resolved": "mini-program", "link": true }, + "node_modules/jiebanke-scripts": { + "resolved": "scripts", + "link": true + }, + "node_modules/jiebanke-website": { + "resolved": "website", + "link": true + }, "node_modules/jimp": { "version": "0.10.3", "resolved": "https://registry.npmmirror.com/jimp/-/jimp-0.10.3.tgz", @@ -17383,70 +17302,6 @@ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, - "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmmirror.com/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", @@ -20281,13 +20136,14 @@ } }, "scripts": { - "name": "jiebanke-api-tests", + "name": "jiebanke-scripts", "version": "1.0.0", - "license": "MIT", - "dependencies": { - "axios": "^1.6.0" - }, - "devDependencies": {} + "license": "MIT" + }, + "website": { + "name": "jiebanke-website", + "version": "1.0.0", + "license": "MIT" } } } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..35de666 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,104 @@ +# 结伴客部署脚本 + +## 概述 + +本目录包含用于将结伴客项目部署到远程 CentOS 服务器的脚本。 + +## 脚本说明 + +### deploy.sh (Bash 脚本 - 适用于 Linux/Mac) + +使用 rsync 将项目文件同步到远程服务器。 + +#### 使用方法 + +```bash +# 给脚本添加执行权限 +chmod +x deploy.sh + +# 部署所有模块 +./deploy.sh all + +# 部署后端服务 +./deploy.sh backend + +# 部署后台管理系统 +./deploy.sh admin + +# 部署官方网站 +./deploy.sh website +``` + +### deploy.ps1 (PowerShell 脚本 - 适用于 Windows) + +Windows 系统下的部署脚本,同样使用 rsync 进行文件同步。 + +#### 使用方法 + +```powershell +# 部署所有模块 +.\deploy.ps1 all + +# 部署后端服务 +.\deploy.ps1 backend + +# 部署后台管理系统 +.\deploy.ps1 admin + +# 部署官方网站 +.\deploy.ps1 website +``` + +## 部署要求 + +1. **rsync** 必须安装在本地机器上 + - Linux/Mac: 通常预装,如果没有可以使用包管理器安装 + - Windows: 需要安装 rsync for Windows,例如通过 WSL 或 Git for Windows + +2. **SSH 访问权限** 到远程服务器 (1.13.156.49) + - 确保本地 SSH 公钥已添加到远程服务器的 `~/.ssh/authorized_keys` 文件中 + +Ai哦推荐考拉¥741515 +## 部署目录结构 + +远程服务器 CentOS (IP: 1.13.156.49) 上各项目的部署目录如下: + +``` +/ +├── data/ +│ ├── nodejs/ +│ │ └── jiebanke/ # 后端服务部署目录 +│ ├── vue/ +│ │ └── jiebanke/ # 后台管理系统部署目录 +│ └── website/ +│ └── jiebanke/ # 官方网站部署目录 +``` + +## 工作流程 + +1. 使用 rsync 同步文件到远程服务器 +2. 排除 node_modules、.git 等不需要的文件和目录 + +## 注意事项 + +- 部署脚本只进行文件同步,不执行构建操作 +- 微信小程序不需要部署到远程服务器,需使用微信开发者工具进行构建和上传 +- 部署过程中会自动排除 node_modules、.git 等目录 +- 脚本使用 `--delete` 参数,会删除远程服务器上已不存在于本地的文件 + +## 故障排除 + +### rsync 命令未找到 +确保已安装 rsync: +- Ubuntu/Debian: `sudo apt-get install rsync` +- CentOS/RHEL: `sudo yum install rsync` +- macOS: 通常预装 +- Windows: 安装 Git for Windows 或 WSL + +### SSH 连接失败 +确保: +1. SSH 公钥已添加到远程服务器 +2. 可以手动通过 `ssh root@1.13.156.49` 连接服务器 + +### 权限问题 +确保运行脚本的用户有足够权限执行 rsync 和访问项目文件。 \ No newline at end of file diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 new file mode 100644 index 0000000..04061a6 --- /dev/null +++ b/scripts/deploy.ps1 @@ -0,0 +1,111 @@ +# 结伴客项目部署脚本 (PowerShell 版本) +# 用于将项目部署到远程 CentOS 服务器 + +param( + [Parameter(Mandatory=$false)] + [string]$Target = "" +) + +# 配置信息 +$REMOTE_HOST = "1.13.156.49" +$REMOTE_USER = "root" +$REMOTE_PASSWORD = "Aiotjkl$741515" +$BACKEND_REMOTE_PATH = "/data/nodejs/jiebanke" +$ADMIN_SYSTEM_REMOTE_PATH = "/data/vue/jiebanke" +$WEBSITE_REMOTE_PATH = "/data/website/jiebanke" + +# 颜色输出函数 +function Write-ColorOutput($ForegroundColor) { + # Save the current color + $fc = $host.UI.RawUI.ForegroundColor + $host.UI.RawUI.ForegroundColor = $ForegroundColor + # Output the content + if ($args) { + Write-Output $args + } + else { + $input | Write-Output + } + # Restore the original color + $host.UI.RawUI.ForegroundColor = $fc +} + +Write-ColorOutput Green "========== 结伴客项目部署脚本 ==========" + +# 检查是否提供了部署目标参数 +if ($Target -eq "") { + Write-ColorOutput Yellow "使用方法:" + Write-Output " .\deploy.ps1 all - 部署所有模块" + Write-Output " .\deploy.ps1 backend - 部署后端服务" + Write-Output " .\deploy.ps1 admin - 部署后台管理系统" + Write-Output " .\deploy.ps1 website - 部署官方网站" + exit 1 +} + +# 部署后端服务 +function Deploy-Backend { + Write-ColorOutput Green "正在部署后端服务到 $REMOTE_HOST:$BACKEND_REMOTE_PATH ..." + + # 同步文件到远程服务器 + Write-ColorOutput Green "正在同步文件到远程服务器..." + $rsyncArgs = "-avz --delete --exclude node_modules --exclude .git --exclude logs ../backend/ $REMOTE_USER@$REMOTE_HOST`:$BACKEND_REMOTE_PATH/" + Start-Process -FilePath "rsync" -ArgumentList $rsyncArgs -NoNewWindow -Wait + + Write-ColorOutput Green "后端服务部署完成" +} + +# 部署后台管理系统 +function Deploy-AdminSystem { + Write-ColorOutput Green "正在部署后台管理系统到 $REMOTE_HOST:$ADMIN_SYSTEM_REMOTE_PATH ..." + + # 同步文件到远程服务器 + Write-ColorOutput Green "正在同步文件到远程服务器..." + $rsyncArgs = "-avz --delete --exclude node_modules --exclude .git --exclude dist ../admin-system/ $REMOTE_USER@$REMOTE_HOST`:$ADMIN_SYSTEM_REMOTE_PATH/" + Start-Process -FilePath "rsync" -ArgumentList $rsyncArgs -NoNewWindow -Wait + + Write-ColorOutput Green "后台管理系统部署完成" +} + +# 部署官方网站 +function Deploy-Website { + Write-ColorOutput Green "正在部署官方网站到 $REMOTE_HOST:$WEBSITE_REMOTE_PATH ..." + + # 同步文件到远程服务器 + Write-ColorOutput Green "正在同步文件到远程服务器..." + $rsyncArgs = "-avz --delete --exclude node_modules --exclude .git ../website/ $REMOTE_USER@$REMOTE_HOST`:$WEBSITE_REMOTE_PATH/" + Start-Process -FilePath "rsync" -ArgumentList $rsyncArgs -NoNewWindow -Wait + + Write-ColorOutput Green "官方网站部署完成" +} + +# 部署所有模块 +function Deploy-All { + Write-ColorOutput Green "开始部署所有模块..." + Deploy-Backend + Deploy-AdminSystem + Deploy-Website + Write-ColorOutput Green "所有模块部署完成" +} + +# 根据参数执行相应操作 +switch ($Target) { + "backend" { + Deploy-Backend + } + "admin" { + Deploy-AdminSystem + } + "website" { + Deploy-Website + } + "all" { + Deploy-All + } + default { + Write-ColorOutput Red "未知参数: $Target" + Write-ColorOutput Yellow "请使用: all, backend, admin, website" + exit 1 + } +} + +Write-ColorOutput Green "========== 部署完成 ==========" \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..c9606dc --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# 结伴客项目部署脚本 +# 用于将项目部署到远程 CentOS 服务器 + +set -e # 遇到错误时退出 + +# 配置信息 +REMOTE_HOST="1.13.156.49" +REMOTE_USER="root" +REMOTE_PASSWORD="Aiotjkl$743838" +BACKEND_REMOTE_PATH="/data/nodejs/jiebanke" +ADMIN_SYSTEM_REMOTE_PATH="/data/vue/jiebanke" +WEBSITE_REMOTE_PATH="/data/website/jiebanke" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}========== 结伴客项目部署脚本 ==========${NC}" + +# 检查是否提供了部署目标参数 +if [ $# -eq 0 ]; then + echo -e "${YELLOW}使用方法:${NC}" + echo " ./deploy.sh all - 部署所有模块" + echo " ./deploy.sh backend - 部署后端服务" + echo " ./deploy.sh admin - 部署后台管理系统" + echo " ./deploy.sh website - 部署官方网站" + exit 1 +fi + +# 检查rsync是否安装 +check_rsync() { + if ! command -v rsync &> /dev/null; then + echo -e "${RED}错误: 未找到 rsync 命令,请先安装 rsync${NC}" + exit 1 + fi +} + +# 部署后端服务 +deploy_backend() { + echo -e "${BLUE}正在部署后端服务到 $REMOTE_HOST:$BACKEND_REMOTE_PATH ...${NC}" + + # 检查rsync + check_rsync + + # 同步文件到远程服务器 + echo -e "${BLUE}正在同步文件到远程服务器...${NC}" + rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + --exclude logs \ + ../backend/ $REMOTE_USER@$REMOTE_HOST:$BACKEND_REMOTE_PATH/ + + echo -e "${GREEN}后端服务部署完成${NC}" +} + +# 部署后台管理系统 +deploy_admin_system() { + echo -e "${BLUE}正在部署后台管理系统到 $REMOTE_HOST:$ADMIN_SYSTEM_REMOTE_PATH ...${NC}" + + # 检查rsync + check_rsync + + # 同步文件到远程服务器 + echo -e "${BLUE}正在同步文件到远程服务器...${NC}" + rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + --exclude dist \ + ../admin-system/ $REMOTE_USER@$REMOTE_HOST:$ADMIN_SYSTEM_REMOTE_PATH/ + + echo -e "${GREEN}后台管理系统部署完成${NC}" +} + +# 部署官方网站 +deploy_website() { + echo -e "${BLUE}正在部署官方网站到 $REMOTE_HOST:$WEBSITE_REMOTE_PATH ...${NC}" + + # 检查rsync + check_rsync + + # 同步文件到远程服务器 + echo -e "${BLUE}正在同步文件到远程服务器...${NC}" + rsync -avz --delete \ + --exclude node_modules \ + --exclude .git \ + ../website/ $REMOTE_USER@$REMOTE_HOST:$WEBSITE_REMOTE_PATH/ + + echo -e "${GREEN}官方网站部署完成${NC}" +} + + +# 部署所有模块 +deploy_all() { + echo -e "${BLUE}开始部署所有模块...${NC}" + deploy_backend + deploy_admin_system + deploy_website + echo -e "${GREEN}所有模块部署完成${NC}" +} + +# 根据参数执行相应操作 +case $1 in + "backend") + deploy_backend + ;; + "admin") + deploy_admin_system + ;; + "website") + deploy_website + ;; + "all") + deploy_all + ;; + *) + echo -e "${RED}未知参数: $1${NC}" + echo -e "${YELLOW}请使用: all, backend, admin, website${NC}" + exit 1 + ;; +esac + +echo -e "${GREEN}========== 部署完成 ==========${NC}" \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json index 70ee495..b128d1c 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,21 +1,20 @@ { - "name": "jiebanke-api-tests", + "name": "jiebanke-scripts", "version": "1.0.0", - "description": "结伴客系统API测试脚本", - "main": "test-api.js", + "description": "结伴客项目部署和管理脚本", + "main": "index.js", "scripts": { - "test": "node test-api.js", - "test:health": "node -e \"require('./test-api.js').api.get('/health').then(r => console.log('健康检查:', r.data)).catch(console.error)\"" + "test": "echo \"Error: no test specified\" && exit 1", + "deploy": "echo \"请在项目根目录的 scripts 文件夹中运行 ./deploy.sh 或 .\\deploy.ps1\"", + "deploy:all": "echo \"请在项目根目录的 scripts 文件夹中运行 ./deploy.sh all 或 .\\deploy.ps1 all\"", + "deploy:backend": "echo \"请在项目根目录的 scripts 文件夹中运行 ./deploy.sh backend\"", + "deploy:admin": "echo \"请在项目根目录的 scripts 文件夹中运行 ./deploy.sh admin\"", + "deploy:website": "echo \"请在项目根目录的 scripts 文件夹中运行 ./deploy.sh website\"" }, - "dependencies": { - "axios": "^1.6.0" - }, - "devDependencies": {}, "keywords": [ - "api-testing", - "jiebanke", - "travel", - "animal-adoption" + "deployment", + "scripts", + "jiebanke" ], "author": "结伴客开发团队", "license": "MIT" diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..fda9801 --- /dev/null +++ b/website/package.json @@ -0,0 +1,15 @@ +{ + "name": "jiebanke-website", + "version": "1.0.0", + "description": "结伴客官方网站", + "scripts": { + "build": "echo \"Website is a static site, no build required.\"", + "dev": "echo \"Website is a static site, no dev server required.\"" + }, + "keywords": [ + "website", + "static" + ], + "author": "jiebanke-team", + "license": "MIT" +} \ No newline at end of file