docs(deployment): 更新部署文档并添加自动化部署脚本
- 更新了 DEPLOYMENT.md 文档,增加了更多部署细节和说明 - 添加了 Linux 和 Windows 平台的自动化部署脚本 - 更新了 README.md,增加了部署相关说明 - 调整了 .env 文件配置,以适应新的部署流程 - 移除了部分不必要的代码和配置
This commit is contained in:
45
README.md
45
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
|
||||
|
||||
@@ -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
|
||||
VITE_FEATURE_DEBUG=false
|
||||
@@ -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
|
||||
187
admin-system/src/api/mockData.ts
Normal file
187
admin-system/src/api/mockData.ts
Normal file
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}, mockSystemAPI)
|
||||
|
||||
export default systemAPI
|
||||
@@ -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<ApiResponse<User>>(`/users/${id}/status`, { status })
|
||||
|
||||
|
||||
export default {
|
||||
// 开发环境使用模拟数据
|
||||
const userAPI = createMockWrapper({
|
||||
getUsers,
|
||||
getUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
batchUpdateUserStatus
|
||||
}
|
||||
}, mockUserAPI)
|
||||
|
||||
export default userAPI
|
||||
28
admin-system/src/config/mock.ts
Normal file
28
admin-system/src/config/mock.ts
Normal file
@@ -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
|
||||
}
|
||||
44
admin-system/test-mock.js
Normal file
44
admin-system/test-mock.js
Normal file
@@ -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🎉 模拟数据测试完成!')
|
||||
326
backend-java/PERFORMANCE_OPTIMIZATION.md
Normal file
326
backend-java/PERFORMANCE_OPTIMIZATION.md
Normal file
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> 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<String, Object> redisTemplate;
|
||||
|
||||
// 本地缓存(Caffeine)
|
||||
private Cache<String, Object> 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后端服务的性能和稳定性。
|
||||
159
backend-java/README.md
Normal file
159
backend-java/README.md
Normal file
@@ -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) 文件。
|
||||
56
backend-java/animal-service/pom.xml
Normal file
56
backend-java/animal-service/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>animal-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> 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<Animal> animals = animalService.searchAnimals(keyword, species, minPrice, maxPrice, page, pageSize);
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> 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<Animal> animals = animalService.getAnimals(merchantId, page, pageSize, species, status);
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> getAnimalStatistics() {
|
||||
Map<String, Object> statistics = animalService.getAnimalStatistics();
|
||||
return ApiResponse.success(statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个动物详情
|
||||
*/
|
||||
@GetMapping("/{animalId}")
|
||||
public ApiResponse<Animal> getAnimal(@PathVariable Long animalId) {
|
||||
Animal animal = animalService.getAnimalById(animalId);
|
||||
return ApiResponse.success(animal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动物信息
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<Map<String, Object>> createAnimal(
|
||||
@RequestHeader("userId") Long merchantId,
|
||||
@RequestBody Animal animal) {
|
||||
|
||||
animal.setMerchantId(merchantId);
|
||||
Long animalId = animalService.createAnimal(animal);
|
||||
Animal createdAnimal = animalService.getAnimalById(animalId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("animal", createdAnimal);
|
||||
result.put("message", "动物信息创建成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动物信息
|
||||
*/
|
||||
@PutMapping("/{animalId}")
|
||||
public ApiResponse<Map<String, Object>> updateAnimal(
|
||||
@PathVariable Long animalId,
|
||||
@RequestBody Animal animal) {
|
||||
|
||||
Animal updatedAnimal = animalService.updateAnimal(animalId, animal);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("animal", updatedAnimal);
|
||||
result.put("message", "动物信息更新成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动物信息
|
||||
*/
|
||||
@DeleteMapping("/{animalId}")
|
||||
public ApiResponse<Map<String, Object>> deleteAnimal(@PathVariable Long animalId) {
|
||||
boolean deleted = animalService.deleteAnimal(animalId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "动物信息删除成功");
|
||||
result.put("animalId", animalId);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Animal> {
|
||||
|
||||
/**
|
||||
* 根据商家ID获取动物列表
|
||||
* @param merchantId 商家ID
|
||||
* @return 动物列表
|
||||
*/
|
||||
@Select("SELECT * FROM animals WHERE merchant_id = #{merchantId} ORDER BY created_at DESC")
|
||||
List<Animal> selectByMerchantId(@Param("merchantId") Long merchantId);
|
||||
|
||||
/**
|
||||
* 根据物种获取动物列表
|
||||
* @param species 物种
|
||||
* @return 动物列表
|
||||
*/
|
||||
@Select("SELECT * FROM animals WHERE species = #{species} ORDER BY created_at DESC")
|
||||
List<Animal> selectBySpecies(@Param("species") String species);
|
||||
|
||||
/**
|
||||
* 根据状态获取动物列表
|
||||
* @param status 状态
|
||||
* @return 动物列表
|
||||
*/
|
||||
@Select("SELECT * FROM animals WHERE status = #{status} ORDER BY created_at DESC")
|
||||
List<Animal> selectByStatus(@Param("status") String status);
|
||||
}
|
||||
@@ -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<Animal> {
|
||||
|
||||
/**
|
||||
* 获取动物列表
|
||||
* @param merchantId 商家ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param species 物种
|
||||
* @param status 状态
|
||||
* @return 动物分页列表
|
||||
*/
|
||||
IPage<Animal> 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<String, Object> getAnimalStatistics();
|
||||
|
||||
/**
|
||||
* 搜索动物
|
||||
* @param keyword 关键词
|
||||
* @param species 物种
|
||||
* @param minPrice 最低价格
|
||||
* @param maxPrice 最高价格
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 动物分页列表
|
||||
*/
|
||||
IPage<Animal> searchAnimals(String keyword, String species, Double minPrice, Double maxPrice, Integer page, Integer pageSize);
|
||||
}
|
||||
@@ -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<AnimalMapper, Animal> implements AnimalService {
|
||||
|
||||
@Override
|
||||
public IPage<Animal> getAnimals(Long merchantId, Integer page, Integer pageSize, String species, String status) {
|
||||
QueryWrapper<Animal> 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<Animal> 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<String, Object> getAnimalStatistics() {
|
||||
// 获取动物总数
|
||||
int total = Math.toIntExact(this.count());
|
||||
|
||||
// 按物种统计
|
||||
QueryWrapper<Animal> speciesWrapper = new QueryWrapper<>();
|
||||
speciesWrapper.select("species", "COUNT(*) as count");
|
||||
speciesWrapper.groupBy("species");
|
||||
List<Map<String, Object>> speciesStats = this.listMaps(speciesWrapper);
|
||||
|
||||
// 按状态统计
|
||||
QueryWrapper<Animal> statusWrapper = new QueryWrapper<>();
|
||||
statusWrapper.select("status", "COUNT(*) as count");
|
||||
statusWrapper.groupBy("status");
|
||||
List<Map<String, Object>> statusStats = this.listMaps(statusWrapper);
|
||||
|
||||
// 构建返回结果
|
||||
Map<String, Object> statistics = new HashMap<>();
|
||||
statistics.put("total", total);
|
||||
statistics.put("bySpecies", speciesStats);
|
||||
statistics.put("byStatus", statusStats);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Animal> searchAnimals(String keyword, String species, Double minPrice, Double maxPrice, Integer page, Integer pageSize) {
|
||||
QueryWrapper<Animal> 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<Animal> pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10);
|
||||
return this.page(pageObj, queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
96
backend-java/auth-service/pom.xml
Normal file
96
backend-java/auth-service/pom.xml
Normal file
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>auth-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- BCrypt -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RabbitMQ -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<AuthResult> 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<AuthResult> login(@RequestBody LoginRequest request) {
|
||||
AuthResult result = authService.login(request.getUsername(), request.getPassword());
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<User> getCurrentUser(@RequestHeader("userId") Long userId) {
|
||||
User user = authService.getCurrentUser(userId);
|
||||
return ApiResponse.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户个人信息
|
||||
*/
|
||||
@PutMapping("/profile")
|
||||
public ApiResponse<User> updateProfile(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestBody User user) {
|
||||
User updatedUser = authService.updateProfile(userId, user);
|
||||
return ApiResponse.success(updatedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@PutMapping("/password")
|
||||
public ApiResponse<Map<String, String>> changePassword(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestBody ChangePasswordRequest request) {
|
||||
boolean success = authService.changePassword(userId, request.getCurrentPassword(), request.getNewPassword());
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", success ? "密码修改成功" : "密码修改失败");
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信登录/注册
|
||||
*/
|
||||
@PostMapping("/wechat")
|
||||
public ApiResponse<AuthResult> wechatLogin(@RequestBody WechatLoginRequest request) {
|
||||
AuthResult result = authService.wechatLogin(request.getCode(), request.getUserInfo());
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
@PostMapping("/admin/login")
|
||||
public ApiResponse<AuthResult> adminLogin(@RequestBody LoginRequest request) {
|
||||
AuthResult result = authService.adminLogin(request.getUsername(), request.getPassword());
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/validate")
|
||||
public ApiResponse<Boolean> 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<User> {
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
* @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);
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<UserMapper, User> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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 <T> 声明类型
|
||||
* @return 声明值
|
||||
*/
|
||||
public <T> T extractClaim(String token, Function<Claims, T> 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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
36
backend-java/auth-service/src/main/resources/application.yml
Normal file
36
backend-java/auth-service/src/main/resources/application.yml
Normal file
@@ -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
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private ValueOperations<String, Object> 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);
|
||||
}
|
||||
}
|
||||
44
backend-java/build-services.sh
Executable file
44
backend-java/build-services.sh
Executable file
@@ -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
|
||||
36
backend-java/common/pom.xml
Normal file
36
backend-java/common/pom.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new StringRedisSerializer());
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Void> handleBusinessException(BusinessException e) {
|
||||
return ApiResponse.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ApiResponse<Void> handleException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return ApiResponse.error(500, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.jiebanke.common.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private boolean success;
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
private long timestamp;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.success = true;
|
||||
response.code = 200;
|
||||
response.message = "操作成功";
|
||||
response.data = data;
|
||||
response.timestamp = System.currentTimeMillis();
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.success = true;
|
||||
response.code = 200;
|
||||
response.message = message;
|
||||
response.data = data;
|
||||
response.timestamp = System.currentTimeMillis();
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.success = false;
|
||||
response.code = code;
|
||||
response.message = message;
|
||||
response.timestamp = System.currentTimeMillis();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
147
backend-java/docker-compose.yml
Normal file
147
backend-java/docker-compose.yml
Normal file
@@ -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
|
||||
32
backend-java/eureka-server/pom.xml
Normal file
32
backend-java/eureka-server/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eureka-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Eureka Server -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
17
backend-java/eureka-server/target/classes/application.yml
Normal file
17
backend-java/eureka-server/target/classes/application.yml
Normal file
@@ -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
|
||||
Binary file not shown.
54
backend-java/gateway-service/pom.xml
Normal file
54
backend-java/gateway-service/pom.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gateway-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Cloud Gateway -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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/
|
||||
38
backend-java/gateway-service/target/classes/application.yml
Normal file
38
backend-java/gateway-service/target/classes/application.yml
Normal file
@@ -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/
|
||||
Binary file not shown.
56
backend-java/order-service/pom.xml
Normal file
56
backend-java/order-service/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>order-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> createOrder(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestBody Order order) {
|
||||
|
||||
Long orderId = orderService.createOrder(order, userId);
|
||||
Order createdOrder = orderService.getOrderById(orderId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("order", createdOrder);
|
||||
result.put("message", "订单创建成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
*/
|
||||
@GetMapping("/{orderId}")
|
||||
public ApiResponse<Order> getOrder(@PathVariable Long orderId) {
|
||||
Order order = orderService.getOrderById(orderId);
|
||||
return ApiResponse.success(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
@GetMapping
|
||||
public ApiResponse<Map<String, Object>> getUserOrders(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
|
||||
IPage<Order> orders = orderService.getUserOrders(userId, page, pageSize, status);
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> updateOrderStatus(
|
||||
@PathVariable Long orderId,
|
||||
@RequestBody Map<String, String> requestBody,
|
||||
@RequestHeader("userId") Long userId) {
|
||||
|
||||
String status = requestBody.get("status");
|
||||
Order updatedOrder = orderService.updateOrderStatus(orderId, status, userId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("order", updatedOrder);
|
||||
result.put("message", "订单状态更新成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单
|
||||
*/
|
||||
@DeleteMapping("/{orderId}")
|
||||
public ApiResponse<Map<String, Object>> deleteOrder(
|
||||
@PathVariable Long orderId,
|
||||
@RequestHeader("userId") Long userId) {
|
||||
|
||||
boolean deleted = orderService.deleteOrder(orderId, userId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "订单删除成功");
|
||||
result.put("orderId", orderId);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ApiResponse<Map<String, Object>> getOrderStatistics(@RequestHeader("merchantId") Long merchantId) {
|
||||
Map<String, Object> statistics = orderService.getOrderStats(merchantId);
|
||||
return ApiResponse.success(statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员获取所有订单
|
||||
*/
|
||||
@GetMapping("/admin")
|
||||
public ApiResponse<Map<String, Object>> 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<Order> orders = orderService.getAllOrders(page, pageSize, status, merchantId, userId);
|
||||
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Order> {
|
||||
|
||||
/**
|
||||
* 根据用户ID获取订单列表
|
||||
* @param userId 用户ID
|
||||
* @return 订单列表
|
||||
*/
|
||||
@Select("SELECT * FROM orders WHERE user_id = #{userId} ORDER BY created_at DESC")
|
||||
List<Order> selectByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据状态获取订单列表
|
||||
* @param status 状态
|
||||
* @return 订单列表
|
||||
*/
|
||||
@Select("SELECT * FROM orders WHERE status = #{status} ORDER BY created_at DESC")
|
||||
List<Order> selectByStatus(@Param("status") String status);
|
||||
|
||||
/**
|
||||
* 根据订单号获取订单
|
||||
* @param orderNo 订单号
|
||||
* @return 订单
|
||||
*/
|
||||
@Select("SELECT * FROM orders WHERE order_no = #{orderNo}")
|
||||
Order selectByOrderNo(@Param("orderNo") String orderNo);
|
||||
}
|
||||
@@ -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<Order> {
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @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<Order> 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<String, Object> getOrderStats(Long merchantId);
|
||||
|
||||
/**
|
||||
* 获取所有订单(管理员)
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param status 状态
|
||||
* @param merchantId 商家ID
|
||||
* @param userId 用户ID
|
||||
* @return 订单分页列表
|
||||
*/
|
||||
IPage<Order> getAllOrders(Integer page, Integer pageSize, String status, Long merchantId, Long userId);
|
||||
}
|
||||
@@ -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<OrderMapper, Order> 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<Order> getUserOrders(Long userId, Integer page, Integer pageSize, String status) {
|
||||
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId);
|
||||
|
||||
if (status != null && !status.isEmpty()) {
|
||||
queryWrapper.eq("status", status);
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc("created_at");
|
||||
|
||||
Page<Order> 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<String, Object> getOrderStats(Long merchantId) {
|
||||
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("merchant_id", merchantId);
|
||||
|
||||
int totalOrders = Math.toIntExact(this.count(queryWrapper));
|
||||
|
||||
QueryWrapper<Order> pendingWrapper = new QueryWrapper<>();
|
||||
pendingWrapper.eq("merchant_id", merchantId).eq("status", "pending");
|
||||
int pendingOrders = Math.toIntExact(this.count(pendingWrapper));
|
||||
|
||||
QueryWrapper<Order> processingWrapper = new QueryWrapper<>();
|
||||
processingWrapper.eq("merchant_id", merchantId).eq("status", "processing");
|
||||
int processingOrders = Math.toIntExact(this.count(processingWrapper));
|
||||
|
||||
QueryWrapper<Order> completedWrapper = new QueryWrapper<>();
|
||||
completedWrapper.eq("merchant_id", merchantId).eq("status", "completed");
|
||||
int completedOrders = Math.toIntExact(this.count(completedWrapper));
|
||||
|
||||
QueryWrapper<Order> cancelledWrapper = new QueryWrapper<>();
|
||||
cancelledWrapper.eq("merchant_id", merchantId).eq("status", "cancelled");
|
||||
int cancelledOrders = Math.toIntExact(this.count(cancelledWrapper));
|
||||
|
||||
QueryWrapper<Order> failedWrapper = new QueryWrapper<>();
|
||||
failedWrapper.eq("merchant_id", merchantId).eq("status", "failed");
|
||||
int failedOrders = Math.toIntExact(this.count(failedWrapper));
|
||||
|
||||
// 计算总收入
|
||||
QueryWrapper<Order> revenueWrapper = new QueryWrapper<>();
|
||||
revenueWrapper.eq("merchant_id", merchantId).select("SUM(amount) as totalRevenue");
|
||||
Map<String, Object> revenueMap = this.getMap(revenueWrapper);
|
||||
BigDecimal totalRevenue = revenueMap != null && revenueMap.get("totalRevenue") != null ?
|
||||
new BigDecimal(revenueMap.get("totalRevenue").toString()) : BigDecimal.ZERO;
|
||||
|
||||
Map<String, Object> 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<Order> getAllOrders(Integer page, Integer pageSize, String status, Long merchantId, Long userId) {
|
||||
QueryWrapper<Order> 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<Order> pageObj = new Page<>(page != null ? page : 1, pageSize != null ? pageSize : 10);
|
||||
return this.page(pageObj, queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
163
backend-java/pom.xml
Normal file
163
backend-java/pom.xml
Normal file
@@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>结伴客Java后端</name>
|
||||
<description>结伴客Java微服务架构后端系统</description>
|
||||
|
||||
<modules>
|
||||
<module>eureka-server</module>
|
||||
<module>gateway-service</module>
|
||||
<module>auth-service</module>
|
||||
<module>user-service</module>
|
||||
<module>travel-service</module>
|
||||
<module>animal-service</module>
|
||||
<module>order-service</module>
|
||||
<module>promotion-service</module>
|
||||
<module>common</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring.boot.version>3.1.0</spring.boot.version>
|
||||
<spring.cloud.version>2022.0.3</spring.cloud.version>
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
<mybatis.plus.version>3.5.3.1</mybatis.plus.version>
|
||||
<junit.version>5.9.2</junit.version>
|
||||
<mockito.version>5.2.0</mockito.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatis.plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- RabbitMQ -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit 5 -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
56
backend-java/promotion-service/pom.xml
Normal file
56
backend-java/promotion-service/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>promotion-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> getPromotionActivities(
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String status) {
|
||||
|
||||
IPage<PromotionActivity> activities = promotionService.getPromotionActivities(page, pageSize, name, status);
|
||||
|
||||
Map<String, Object> 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<PromotionActivity> getPromotionActivity(@PathVariable Long id) {
|
||||
PromotionActivity activity = promotionService.getPromotionActivityById(id);
|
||||
return ApiResponse.success(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建推广活动
|
||||
*/
|
||||
@PostMapping("/activities")
|
||||
public ApiResponse<Map<String, Object>> createPromotionActivity(@RequestBody PromotionActivity activity) {
|
||||
Long activityId = promotionService.createPromotionActivity(activity);
|
||||
PromotionActivity createdActivity = promotionService.getPromotionActivityById(activityId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("activity", createdActivity);
|
||||
result.put("message", "推广活动创建成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新推广活动
|
||||
*/
|
||||
@PutMapping("/activities/{id}")
|
||||
public ApiResponse<Map<String, Object>> updatePromotionActivity(
|
||||
@PathVariable Long id,
|
||||
@RequestBody PromotionActivity activity) {
|
||||
|
||||
PromotionActivity updatedActivity = promotionService.updatePromotionActivity(id, activity);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("activity", updatedActivity);
|
||||
result.put("message", "推广活动更新成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除推广活动
|
||||
*/
|
||||
@DeleteMapping("/activities/{id}")
|
||||
public ApiResponse<Map<String, Object>> deletePromotionActivity(@PathVariable Long id) {
|
||||
boolean deleted = promotionService.deletePromotionActivity(id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "推广活动删除成功");
|
||||
result.put("id", id);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停推广活动
|
||||
*/
|
||||
@PostMapping("/activities/{id}/pause")
|
||||
public ApiResponse<Map<String, Object>> pausePromotionActivity(@PathVariable Long id) {
|
||||
boolean paused = promotionService.pausePromotionActivity(id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "推广活动已暂停");
|
||||
result.put("id", id);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复推广活动
|
||||
*/
|
||||
@PostMapping("/activities/{id}/resume")
|
||||
public ApiResponse<Map<String, Object>> resumePromotionActivity(@PathVariable Long id) {
|
||||
boolean resumed = promotionService.resumePromotionActivity(id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "推广活动已恢复");
|
||||
result.put("id", id);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取奖励记录列表
|
||||
*/
|
||||
@GetMapping("/rewards")
|
||||
public ApiResponse<Map<String, Object>> 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<RewardRecord> rewards = promotionService.getRewardRecords(page, pageSize, user, rewardType, status);
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> issueReward(@PathVariable Long id) {
|
||||
boolean issued = promotionService.issueReward(id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "奖励已发放");
|
||||
result.put("id", id);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推广统计数据
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ApiResponse<Map<String, Object>> getPromotionStatistics() {
|
||||
Map<String, Object> statistics = promotionService.getPromotionStatistics();
|
||||
return ApiResponse.success(statistics);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<PromotionActivity> {
|
||||
|
||||
/**
|
||||
* 根据状态获取推广活动列表
|
||||
* @param status 状态
|
||||
* @return 推广活动列表
|
||||
*/
|
||||
@Select("SELECT * FROM promotion_activities WHERE status = #{status} ORDER BY created_at DESC")
|
||||
List<PromotionActivity> selectByStatus(@Param("status") String status);
|
||||
|
||||
/**
|
||||
* 根据名称模糊查询推广活动
|
||||
* @param name 名称
|
||||
* @return 推广活动列表
|
||||
*/
|
||||
@Select("SELECT * FROM promotion_activities WHERE name LIKE CONCAT('%', #{name}, '%') ORDER BY created_at DESC")
|
||||
List<PromotionActivity> 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);
|
||||
}
|
||||
@@ -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<RewardRecord> {
|
||||
|
||||
/**
|
||||
* 根据用户ID获取奖励记录列表
|
||||
* @param userId 用户ID
|
||||
* @return 奖励记录列表
|
||||
*/
|
||||
@Select("SELECT * FROM reward_records WHERE user_id = #{userId} ORDER BY created_at DESC")
|
||||
List<RewardRecord> selectByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据状态获取奖励记录列表
|
||||
* @param status 状态
|
||||
* @return 奖励记录列表
|
||||
*/
|
||||
@Select("SELECT * FROM reward_records WHERE status = #{status} ORDER BY created_at DESC")
|
||||
List<RewardRecord> 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);
|
||||
}
|
||||
@@ -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<PromotionActivity> {
|
||||
|
||||
/**
|
||||
* 获取推广活动列表
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param name 活动名称
|
||||
* @param status 状态
|
||||
* @return 推广活动分页列表
|
||||
*/
|
||||
IPage<PromotionActivity> 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<RewardRecord> getRewardRecords(Integer page, Integer pageSize, String user, String rewardType, String status);
|
||||
|
||||
/**
|
||||
* 发放奖励
|
||||
* @param id 奖励记录ID
|
||||
* @return 是否发放成功
|
||||
*/
|
||||
boolean issueReward(Long id);
|
||||
|
||||
/**
|
||||
* 获取推广统计数据
|
||||
* @return 统计数据
|
||||
*/
|
||||
Map<String, Object> getPromotionStatistics();
|
||||
}
|
||||
@@ -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<PromotionActivityMapper, PromotionActivity> implements PromotionService {
|
||||
|
||||
@Autowired
|
||||
private PromotionActivityMapper promotionActivityMapper;
|
||||
|
||||
@Autowired
|
||||
private RewardRecordMapper rewardRecordMapper;
|
||||
|
||||
@Override
|
||||
public IPage<PromotionActivity> getPromotionActivities(Integer page, Integer pageSize, String name, String status) {
|
||||
QueryWrapper<PromotionActivity> 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<PromotionActivity> 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<RewardRecord> getRewardRecords(Integer page, Integer pageSize, String user, String rewardType, String status) {
|
||||
QueryWrapper<RewardRecord> 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<RewardRecord> 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<String, Object> getPromotionStatistics() {
|
||||
// 获取活动总数
|
||||
int totalActivities = Math.toIntExact(this.count());
|
||||
|
||||
// 获取进行中的活动数
|
||||
QueryWrapper<PromotionActivity> activeWrapper = new QueryWrapper<>();
|
||||
activeWrapper.eq("status", "active");
|
||||
int activeActivities = Math.toIntExact(this.count(activeWrapper));
|
||||
|
||||
// 获取奖励记录总数
|
||||
int totalRewards = Math.toIntExact(rewardRecordMapper.selectCount(null));
|
||||
|
||||
// 获取已发放的奖励数
|
||||
QueryWrapper<RewardRecord> issuedWrapper = new QueryWrapper<>();
|
||||
issuedWrapper.eq("status", "issued");
|
||||
int issuedRewards = Math.toIntExact(rewardRecordMapper.selectCount(issuedWrapper));
|
||||
|
||||
// 计算总奖励金额
|
||||
QueryWrapper<RewardRecord> amountWrapper = new QueryWrapper<>();
|
||||
amountWrapper.eq("status", "issued").select("SUM(reward_amount) as totalAmount");
|
||||
Map<String, Object> amountMap = rewardRecordMapper.selectMap(amountWrapper);
|
||||
BigDecimal totalAmount = amountMap != null && amountMap.get("totalAmount") != null ?
|
||||
new BigDecimal(amountMap.get("totalAmount").toString()) : BigDecimal.ZERO;
|
||||
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
137
backend-java/scripts/init-database.sql
Normal file
137
backend-java/scripts/init-database.sql
Normal file
@@ -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);
|
||||
92
backend-java/start-services.sh
Executable file
92
backend-java/start-services.sh
Executable file
@@ -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"
|
||||
29
backend-java/stop-services.sh
Executable file
29
backend-java/stop-services.sh
Executable file
@@ -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 "清理完成"
|
||||
62
backend-java/travel-service/pom.xml
Normal file
62
backend-java/travel-service/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>travel-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> getTravelPlans(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
|
||||
IPage<TravelPlan> plans = travelPlanService.getTravelPlans(userId, page, pageSize, status);
|
||||
|
||||
Map<String, Object> 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<TravelPlan> getTravelPlan(@PathVariable Long planId) {
|
||||
TravelPlan plan = travelPlanService.getTravelPlanById(planId);
|
||||
return ApiResponse.success(plan);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建旅行计划
|
||||
*/
|
||||
@PostMapping("/plans")
|
||||
public ApiResponse<Map<String, Object>> createTravelPlan(
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestBody TravelPlan travelPlan) {
|
||||
|
||||
Long planId = travelPlanService.createTravelPlan(userId, travelPlan);
|
||||
TravelPlan plan = travelPlanService.getTravelPlanById(planId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("plan", plan);
|
||||
result.put("message", "旅行计划创建成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新旅行计划
|
||||
*/
|
||||
@PutMapping("/plans/{planId}")
|
||||
public ApiResponse<Map<String, Object>> updateTravelPlan(
|
||||
@PathVariable Long planId,
|
||||
@RequestHeader("userId") Long userId,
|
||||
@RequestBody TravelPlan travelPlan) {
|
||||
|
||||
TravelPlan updatedPlan = travelPlanService.updateTravelPlan(planId, userId, travelPlan);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("plan", updatedPlan);
|
||||
result.put("message", "旅行计划更新成功");
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除旅行计划
|
||||
*/
|
||||
@DeleteMapping("/plans/{planId}")
|
||||
public ApiResponse<Map<String, Object>> deleteTravelPlan(
|
||||
@PathVariable Long planId,
|
||||
@RequestHeader("userId") Long userId) {
|
||||
|
||||
boolean deleted = travelPlanService.deleteTravelPlan(planId, userId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("message", "旅行计划删除成功");
|
||||
result.put("planId", planId);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户旅行统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ApiResponse<TravelStats> getTravelStats(@RequestHeader("userId") Long userId) {
|
||||
TravelStats stats = travelPlanService.getUserTravelStats(userId);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TravelPlan> {
|
||||
|
||||
/**
|
||||
* 根据用户ID获取旅行计划列表
|
||||
* @param userId 用户ID
|
||||
* @return 旅行计划列表
|
||||
*/
|
||||
@Select("SELECT * FROM travel_plans WHERE user_id = #{userId} ORDER BY created_at DESC")
|
||||
List<TravelPlan> selectByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据状态获取旅行计划列表
|
||||
* @param status 状态
|
||||
* @return 旅行计划列表
|
||||
*/
|
||||
@Select("SELECT * FROM travel_plans WHERE status = #{status} ORDER BY created_at DESC")
|
||||
List<TravelPlan> selectByStatus(@Param("status") String status);
|
||||
}
|
||||
@@ -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<TravelPlan> {
|
||||
|
||||
/**
|
||||
* 获取旅行计划列表
|
||||
* @param userId 用户ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param status 状态
|
||||
* @return 旅行计划分页列表
|
||||
*/
|
||||
IPage<TravelPlan> 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TravelPlanMapper, TravelPlan> implements TravelPlanService {
|
||||
|
||||
@Override
|
||||
public IPage<TravelPlan> getTravelPlans(Long userId, Integer page, Integer pageSize, String status) {
|
||||
QueryWrapper<TravelPlan> 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<TravelPlan> 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<TravelPlan> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId);
|
||||
|
||||
List<TravelPlan> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
74
backend-java/user-service/pom.xml
Normal file
74
backend-java/user-service/pom.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>backend-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>user-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eureka Client -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RabbitMQ -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 结伴客公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.jiebanke</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> validateToken(@RequestHeader("Authorization") String token);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
25
backend-java/user-service/src/main/resources/application.yml
Normal file
25
backend-java/user-service/src/main/resources/application.yml
Normal file
@@ -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/
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private ValueOperations<String, Object> 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);
|
||||
}
|
||||
}
|
||||
57
backend/.env
57
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
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user