docs(deployment): 更新部署文档并添加自动化部署脚本

- 更新了 DEPLOYMENT.md 文档,增加了更多部署细节和说明
- 添加了 Linux 和 Windows 平台的自动化部署脚本
- 更新了 README.md,增加了部署相关说明
- 调整了 .env 文件配置,以适应新的部署流程
- 移除了部分不必要的代码和配置
This commit is contained in:
2025-09-10 14:16:27 +08:00
parent 18fe719f94
commit b2d940e014
114 changed files with 6990 additions and 247 deletions

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View 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
View 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🎉 模拟数据测试完成!')

View 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
View 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) 文件。

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -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
View 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

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, "服务器内部错误");
}
}

View File

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

View 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

View 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>

View File

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

View 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

View 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

View 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>

View File

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

View 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/

View 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/

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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>

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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
View 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 "清理完成"

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View 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/

View File

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

View File

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

View File

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