first
demo
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
19
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
19
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
458
api.md
Normal file
458
api.md
Normal file
@ -0,0 +1,458 @@
|
||||
# 海角AI - API接口文档
|
||||
|
||||
## 用户认证模块
|
||||
|
||||
### 用户登录
|
||||
|
||||
**功能描述**: 用户通过邮箱和密码进行登录认证,登录成功后返回token和用户基本信息
|
||||
|
||||
**入参**:
|
||||
- `email`: string - 用户邮箱地址
|
||||
- `password`: string - 用户密码
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.token`: string - 访问令牌
|
||||
- `body.user`: object - 用户信息对象
|
||||
- `body.user.id`: int - 用户ID
|
||||
- `body.user.nickname`: string - 用户昵称
|
||||
- `body.user.email`: string - 用户邮箱
|
||||
- `body.user.avatar`: string - 用户头像URL
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/auth/login
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 用户注册
|
||||
|
||||
**功能描述**: 新用户注册,创建账户并返回用户信息
|
||||
|
||||
**入参**:
|
||||
- `email`: string - 用户邮箱地址
|
||||
- `password`: string - 用户密码
|
||||
- `nickname`: string - 用户昵称
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.user_id`: int - 新创建的用户ID
|
||||
- `body.token`: string - 访问令牌
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/auth/register
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 用户登出
|
||||
|
||||
**功能描述**: 用户登出,使当前token失效
|
||||
|
||||
**入参**: 无
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/auth/logout
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 获取用户信息
|
||||
|
||||
**功能描述**: 获取当前登录用户的详细信息
|
||||
|
||||
**入参**: 无(通过Authorization Header传递token)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.user_id`: int - 用户ID
|
||||
- `body.nickname`: string - 用户昵称
|
||||
- `body.email`: string - 用户邮箱
|
||||
- `body.avatar`: string - 用户头像URL
|
||||
- `body.created_at`: int - 注册时间戳
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/user/info
|
||||
**请求方式**: GET
|
||||
|
||||
---
|
||||
|
||||
## 聊天模块
|
||||
|
||||
### 创建会话
|
||||
|
||||
**功能描述**: 创建新的聊天会话,用于组织对话
|
||||
|
||||
**入参**:
|
||||
- `title`: string - 会话标题
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.session_id`: string - 会话ID
|
||||
- `body.title`: string - 会话标题
|
||||
- `body.created_at`: int - 创建时间戳
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/session
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 发送消息
|
||||
|
||||
**功能描述**: 向指定会话发送消息,AI会根据消息内容和配置参数生成回复
|
||||
|
||||
**入参**:
|
||||
- `session_id`: string - 会话ID
|
||||
- `content`: string - 消息内容
|
||||
- `model`: string - 使用的AI模型,默认为"Hunyuan"
|
||||
- `deep_thinking`: bool - 是否启用深度思考模式
|
||||
- `web_search`: bool - 是否启用联网搜索
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.user_message_id`: string - 用户消息ID
|
||||
- `body.ai_response`: object - AI回复对象
|
||||
- `body.ai_response.id`: string - AI消息ID
|
||||
- `body.ai_response.content`: string - AI回复内容
|
||||
- `body.ai_response.timestamp`: int - 回复时间戳
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/send
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 获取会话列表
|
||||
|
||||
**功能描述**: 获取用户的所有聊天会话列表,支持分页和关键词搜索
|
||||
|
||||
**入参**:
|
||||
- `page`: int - 页码(默认1)
|
||||
- `page_size`: int - 每页数量(默认20)
|
||||
- `keyword`: string - 搜索关键词(可选)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.sessions`: array - 会话列表
|
||||
- `body.sessions[].id`: string - 会话ID
|
||||
- `body.sessions[].title`: string - 会话标题
|
||||
- `body.sessions[].message_count`: int - 消息数量
|
||||
- `body.sessions[].last_message_time`: int - 最后消息时间戳
|
||||
- `body.total`: int - 总数量
|
||||
- `body.has_more`: bool - 是否还有更多
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/sessions
|
||||
**请求方式**: GET
|
||||
|
||||
---
|
||||
|
||||
### 获取会话消息
|
||||
|
||||
**功能描述**: 获取指定会话的消息历史记录,支持分页
|
||||
|
||||
**入参**:
|
||||
- `session_id`: string - 会话ID
|
||||
- `page`: int - 页码(默认1)
|
||||
- `page_size`: int - 每页数量(默认50)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.messages`: array - 消息列表
|
||||
- `body.messages[].id`: string - 消息ID
|
||||
- `body.messages[].role`: string - 消息角色(user/assistant)
|
||||
- `body.messages[].content`: string - 消息内容
|
||||
- `body.messages[].timestamp`: int - 消息时间戳
|
||||
- `body.messages[].is_favorited`: bool - 是否已收藏
|
||||
- `body.total`: int - 总数量
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/messages
|
||||
**请求方式**: GET
|
||||
|
||||
---
|
||||
|
||||
### 删除会话
|
||||
|
||||
**功能描述**: 删除指定的聊天会话及其所有消息
|
||||
|
||||
**入参**: 无(会话ID通过URL路径传递)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/session/{session_id}
|
||||
**请求方式**: DELETE
|
||||
|
||||
---
|
||||
|
||||
### 清空会话
|
||||
|
||||
**功能描述**: 清空指定会话的所有消息,但保留会话本身
|
||||
|
||||
**入参**: 无(会话ID通过URL路径传递)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/chat/session/{session_id}/clear
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
## 收藏模块
|
||||
|
||||
### 收藏消息
|
||||
|
||||
**功能描述**: 将指定消息添加到收藏列表
|
||||
|
||||
**入参**:
|
||||
- `message_id`: string - 消息ID
|
||||
- `session_id`: string - 会话ID
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.favorite_id`: string - 收藏记录ID
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/favorites/add
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
### 取消收藏
|
||||
|
||||
**功能描述**: 从收藏列表中移除指定消息
|
||||
|
||||
**入参**:
|
||||
- `message_id`: string - 消息ID
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/favorites/remove
|
||||
**请求方式**: DELETE
|
||||
|
||||
---
|
||||
|
||||
### 获取收藏列表
|
||||
|
||||
**功能描述**: 获取用户的所有收藏消息列表,支持分页
|
||||
|
||||
**入参**:
|
||||
- `page`: int - 页码(默认1)
|
||||
- `page_size`: int - 每页数量(默认20)
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.favorites`: array - 收藏列表
|
||||
- `body.favorites[].id`: string - 收藏记录ID
|
||||
- `body.favorites[].message_id`: string - 消息ID
|
||||
- `body.favorites[].content`: string - 消息内容
|
||||
- `body.favorites[].session_title`: string - 所属会话标题
|
||||
- `body.favorites[].created_at`: int - 收藏时间戳
|
||||
- `body.total`: int - 总数量
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/favorites/list
|
||||
**请求方式**: GET
|
||||
|
||||
---
|
||||
|
||||
## 文件上传模块
|
||||
|
||||
### 上传文件
|
||||
|
||||
**功能描述**: 上传文件(支持txt、pdf、md等格式)用于AI分析和处理
|
||||
|
||||
**入参**:
|
||||
- `file`: file - 上传的文件
|
||||
- `session_id`: string - 关联的会话ID
|
||||
|
||||
**返回参数**:
|
||||
- `error`: int - 错误码,0表示成功
|
||||
- `body`: object - 响应数据
|
||||
- `body.file_id`: string - 文件ID
|
||||
- `body.filename`: string - 文件名
|
||||
- `body.file_size`: int - 文件大小(字节)
|
||||
- `body.file_type`: string - 文件类型
|
||||
- `body.upload_time`: int - 上传时间戳
|
||||
- `message`: string - 响应消息
|
||||
- `success`: bool - 是否成功
|
||||
|
||||
**url地址**: /api/files/upload
|
||||
**请求方式**: POST
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 0 | 成功 |
|
||||
| 1001 | 参数错误 |
|
||||
| 1002 | 用户不存在 |
|
||||
| 1003 | 密码错误 |
|
||||
| 1004 | 邮箱已存在 |
|
||||
| 1005 | Token无效或已过期 |
|
||||
| 2001 | 会话不存在 |
|
||||
| 2002 | 消息发送失败 |
|
||||
| 2003 | 文件上传失败 |
|
||||
| 2004 | 文件格式不支持 |
|
||||
| 2005 | 文件大小超出限制 |
|
||||
| 3001 | 收藏失败 |
|
||||
| 3002 | 收藏记录不存在 |
|
||||
| 5000 | 服务器内部错误 |
|
||||
| 5001 | AI服务不可用 |
|
||||
| 5002 | 网络搜索服务不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 通用响应格式
|
||||
|
||||
所有API接口都采用统一的响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": 0,
|
||||
"body": {
|
||||
// 具体的响应数据
|
||||
},
|
||||
"message": "操作成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## 认证说明
|
||||
|
||||
除了登录和注册接口外,其他接口都需要在请求头中携带Authorization:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: http://localhost:8080
|
||||
- **超时时间**: 10秒
|
||||
- **默认Content-Type**: application/json
|
||||
- **支持的AI模型**: Hunyuan、基础模型、增强模型
|
||||
|
||||
## 请求示例
|
||||
|
||||
### 登录请求示例
|
||||
|
||||
```javascript
|
||||
// 请求
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
// 响应
|
||||
{
|
||||
"error": 0,
|
||||
"body": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"nickname": "用户昵称",
|
||||
"email": "user@example.com",
|
||||
"avatar": "https://example.com/avatar.jpg"
|
||||
}
|
||||
},
|
||||
"message": "登录成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 发送消息请求示例
|
||||
|
||||
```javascript
|
||||
// 请求
|
||||
POST /api/chat/send
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"session_id": "session_123",
|
||||
"content": "你好,请帮我解释一下Vue3的组合式API",
|
||||
"model": "Hunyuan",
|
||||
"deep_thinking": true,
|
||||
"web_search": false
|
||||
}
|
||||
|
||||
// 响应
|
||||
{
|
||||
"error": 0,
|
||||
"body": {
|
||||
"user_message_id": "msg_456",
|
||||
"ai_response": {
|
||||
"id": "msg_789",
|
||||
"content": "Vue3的组合式API是一种新的组织组件逻辑的方式...",
|
||||
"timestamp": 1692345678
|
||||
}
|
||||
},
|
||||
"message": "消息发送成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 文件上传请求示例
|
||||
|
||||
```javascript
|
||||
// 请求
|
||||
POST /api/files/upload
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
FormData:
|
||||
- file: [选择的文件]
|
||||
- session_id: "session_123"
|
||||
|
||||
// 响应
|
||||
{
|
||||
"error": 0,
|
||||
"body": {
|
||||
"file_id": "file_456",
|
||||
"filename": "document.pdf",
|
||||
"file_size": 1024000,
|
||||
"file_type": "application/pdf",
|
||||
"upload_time": 1692345678
|
||||
},
|
||||
"message": "文件上传成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
40
doc/TASK001-项目基础架构搭建.md
Normal file
40
doc/TASK001-项目基础架构搭建.md
Normal file
@ -0,0 +1,40 @@
|
||||
# TASK001 - 项目基础架构搭建
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK001
|
||||
- **任务名称**: 项目基础架构搭建
|
||||
- **版本**: V1.0
|
||||
- **状态**: 完成
|
||||
|
||||
## 任务描述
|
||||
搭建海角AI后端项目的基础架构,包括Spring Boot项目初始化、Maven依赖配置、项目目录结构规划、基础配置文件设置和开发环境配置。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] Spring Boot 3.x项目创建完成
|
||||
- [x] Maven依赖配置完整,包括web、jpa、security、jwt、redis、mysql等核心依赖
|
||||
- [x] 项目目录结构规范,包含controller、service、repository、entity、dto、config等包结构
|
||||
- [x] application.yml配置文件完成基础配置(数据库、服务器端口、日志等)
|
||||
- [x] 开发环境profiles配置(dev、test、prod)
|
||||
- [x] 项目能正常启动并访问健康检查接口
|
||||
- [x] 集成Swagger/OpenAPI文档工具
|
||||
- [x] 配置跨域处理
|
||||
- [x] 添加统一的日志配置
|
||||
|
||||
## 注意事项
|
||||
1. 使用Spring Boot 3.x最新稳定版本,确保与Java 17+兼容性
|
||||
2. Maven依赖版本要保持一致性,避免版本冲突
|
||||
3. 包命名遵循com.lxy.hsend的约定
|
||||
4. 配置文件中敏感信息使用环境变量或配置中心
|
||||
5. 预留AI服务调用的HTTP客户端配置(如RestTemplate或WebClient)
|
||||
6. 考虑文件上传大小限制配置
|
||||
7. 设置合理的数据库连接池配置
|
||||
8. 添加actuator健康检查端点
|
||||
9. 配置时区为Asia/Shanghai
|
||||
10. 预留Redis配置用于会话管理和缓存
|
||||
|
||||
## 相关文件
|
||||
- pom.xml
|
||||
- src/main/resources/application.yml
|
||||
- src/main/resources/application-dev.yml
|
||||
- src/main/java/com/lxy/hsend/config/
|
||||
- src/main/java/com/lxy/hsend/HsEndApplication.java
|
||||
41
doc/TASK002-数据库设计和初始化.md
Normal file
41
doc/TASK002-数据库设计和初始化.md
Normal file
@ -0,0 +1,41 @@
|
||||
# TASK002 - 数据库设计和初始化
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK002
|
||||
- **任务名称**: 数据库设计和初始化
|
||||
- **版本**: V1.0
|
||||
- **状态**: 完成
|
||||
|
||||
## 任务描述
|
||||
根据API文档设计数据库表结构,创建用户、会话、消息、收藏、文件等核心业务表,建立表之间的关联关系,编写数据库初始化脚本和JPA Entity实体类。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] 完成数据库表结构设计,包括:用户表、会话表、消息表、收藏表、文件表
|
||||
- [x] 建立表之间的外键关系和索引
|
||||
- [x] 编写SQL初始化脚本(schema.sql和data.sql) - 使用Flyway迁移脚本V1__Create_initial_tables.sql
|
||||
- [x] 创建对应的JPA Entity实体类 - User、Session、Message、Favorite、File
|
||||
- [x] 配置JPA/Hibernate相关属性 - application.yml中已配置
|
||||
- [x] 添加数据库版本管理工具(Flyway或Liquibase) - 已添加Flyway依赖和配置
|
||||
- [x] 创建Repository接口 - UserRepository、SessionRepository、MessageRepository、FavoriteRepository、FileRepository
|
||||
- [x] 验证数据库连接和表创建成功 - 测试通过,数据库连接正常
|
||||
- [x] 编写基础的数据库操作测试 - DatabaseConnectionTest测试基本CRUD操作成功
|
||||
|
||||
## 注意事项
|
||||
1. 用户表需包含:id、email、password_hash、nickname、avatar_url、created_at、updated_at等字段
|
||||
2. 会话表需包含:session_id(UUID)、user_id、title、message_count、last_message_time、created_at等字段
|
||||
3. 消息表需包含:message_id(UUID)、session_id、role(user/assistant)、content、timestamp、model_used、is_favorited等字段
|
||||
4. 收藏表需包含:favorite_id、user_id、message_id、session_id、created_at等字段
|
||||
5. 文件表需包含:file_id、user_id、session_id、filename、file_path、file_size、file_type、upload_time等字段
|
||||
6. 所有主键使用UUID或雪花ID,避免数据泄露
|
||||
7. 敏感字段需要考虑加密存储(如密码使用BCrypt)
|
||||
8. 添加软删除字段(deleted_at)用于数据恢复
|
||||
9. 时间字段统一使用BIGINT存储时间戳
|
||||
10. 考虑分表分库的预留设计(如用户ID取模)
|
||||
11. 为高频查询字段添加索引(如session_id、user_id、timestamp等)
|
||||
12. 设置合理的字符集(utf8mb4)支持emoji
|
||||
|
||||
## 相关文件
|
||||
- src/main/resources/db/migration/
|
||||
- src/main/java/com/lxy/hsend/entity/
|
||||
- src/main/java/com/lxy/hsend/repository/
|
||||
- src/main/resources/application.yml(数据库配置)
|
||||
43
doc/TASK003-基础配置和公共组件.md
Normal file
43
doc/TASK003-基础配置和公共组件.md
Normal file
@ -0,0 +1,43 @@
|
||||
# TASK003 - 基础配置和公共组件
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK003
|
||||
- **任务名称**: 基础配置和公共组件
|
||||
- **版本**: V1.0
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 任务描述
|
||||
实现项目的基础配置和公共组件,包括统一响应格式、全局异常处理、请求参数验证、跨域配置、JWT工具类、Redis配置等公共功能模块。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] 创建统一响应格式类(ApiResponse) - 完成,支持成功/失败响应和各种错误类型
|
||||
- [x] 实现全局异常处理器(GlobalExceptionHandler) - 完成,覆盖所有异常类型和错误码
|
||||
- [x] 配置参数验证注解和自定义验证器 - 完成,包括@ValidEmail、@ValidPassword等
|
||||
- [x] 实现JWT工具类(生成、验证、解析token) - 完成,支持token生成、验证、黑名单机制
|
||||
- [x] 配置Redis连接和序列化器 - 完成,包括缓存管理器和序列化配置
|
||||
- [x] 创建通用工具类(时间处理、字符串工具、加密工具等) - 完成TimeUtil、StringUtil、CryptoUtil
|
||||
- [x] 实现自定义注解(如@RequireAuth用于权限验证) - 完成@RequireAuth、@RequestLog、@RateLimit等
|
||||
- [x] 配置Web MVC相关设置 - 完成CORS、静态资源、拦截器配置
|
||||
- [x] 创建常量类和枚举类 - 完成Constants、ErrorCode枚举
|
||||
- [x] 实现分页工具类 - 完成PageUtil,支持分页查询和响应转换
|
||||
|
||||
## 注意事项
|
||||
1. ApiResponse格式需严格按照API文档中的统一响应格式:error、body、message、success字段
|
||||
2. 异常处理需要覆盖所有API文档中定义的错误码(1001-5002)
|
||||
3. JWT工具类需要支持token生成、验证、刷新和黑名单机制
|
||||
4. 参数验证需要支持邮箱格式、密码强度、文件大小等自定义规则
|
||||
5. Redis配置需要支持session存储和缓存功能
|
||||
6. 工具类方法需要线程安全
|
||||
7. 时间处理统一使用时间戳格式
|
||||
8. 密码加密使用BCrypt算法
|
||||
9. 文件上传需要支持类型验证和大小限制
|
||||
10. 异常信息不应暴露敏感的系统内部信息
|
||||
11. 添加请求日志记录功能
|
||||
12. 配置合理的超时时间和重试机制
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/common/
|
||||
- src/main/java/com/lxy/hsend/config/
|
||||
- src/main/java/com/lxy/hsend/util/
|
||||
- src/main/java/com/lxy/hsend/exception/
|
||||
- src/main/java/com/lxy/hsend/annotation/
|
||||
37
doc/TASK004-用户认证模块.md
Normal file
37
doc/TASK004-用户认证模块.md
Normal file
@ -0,0 +1,37 @@
|
||||
# TASK004 - 用户认证模块
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK004
|
||||
- **任务名称**: 用户认证模块
|
||||
- **版本**: V1.0
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 任务描述
|
||||
实现用户认证相关功能,包括用户注册、登录、登出、获取用户信息等接口,集成JWT认证机制,实现用户数据的增删改查操作。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] 实现用户注册接口(/api/auth/register) - 完成,支持邮箱、密码、昵称注册
|
||||
- [x] 实现用户登录接口(/api/auth/login) - 完成,返回JWT token和用户信息
|
||||
- [x] 实现用户登出接口(/api/auth/logout) - 完成,token加入黑名单机制
|
||||
- [x] 实现获取用户信息接口(/api/user/info) - 完成,支持当前用户和公开用户信息
|
||||
- [x] 集成JWT认证拦截器 - 完成,自动验证token和设置用户上下文
|
||||
- [x] 实现密码加密和验证 - 完成,使用BCrypt加密
|
||||
- [x] 实现邮箱格式验证和重复性检查 - 完成,注册时检查邮箱唯一性
|
||||
- [x] 创建UserService、UserController和相关DTO - 完成,完整的服务层和控制层
|
||||
- [x] 实现token黑名单机制 - 完成,使用Redis存储黑名单token
|
||||
- [x] 添加登录失败次数限制 - 完成,Redis记录失败次数,15分钟锁定
|
||||
|
||||
## 注意事项
|
||||
1. 注册时需要验证邮箱格式和唯一性
|
||||
2. 登录成功后返回JWT token和用户基本信息,不能返回密码
|
||||
9. 邮箱字段需要转换为小写存储
|
||||
12. 注册时可以设置默认头像
|
||||
13. 需要记录用户最后登录时间
|
||||
14. 实现用户状态管理(正常、锁定、删除)
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/controller/AuthController.java
|
||||
- src/main/java/com/lxy/hsend/controller/UserController.java
|
||||
- src/main/java/com/lxy/hsend/service/UserService.java
|
||||
- src/main/java/com/lxy/hsend/dto/auth/
|
||||
- src/main/java/com/lxy/hsend/config/JwtAuthenticationFilter.java
|
||||
41
doc/TASK005-聊天会话管理模块.md
Normal file
41
doc/TASK005-聊天会话管理模块.md
Normal file
@ -0,0 +1,41 @@
|
||||
# TASK005 - 聊天会话管理模块
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK005
|
||||
- **任务名称**: 聊天会话管理模块
|
||||
- **版本**: V1.0
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 任务描述
|
||||
实现聊天会话的管理功能,包括创建会话、获取会话列表、获取会话消息、删除会话、清空会话等操作,支持会话的搜索和分页功能。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 实现创建会话接口(/api/chat/session)
|
||||
- [ ] 实现获取会话列表接口(/api/chat/sessions)
|
||||
- [ ] 实现获取会话消息接口(/api/chat/messages)
|
||||
- [ ] 实现删除会话接口(/api/chat/session/{session_id})
|
||||
- [ ] 实现清空会话接口(/api/chat/session/{session_id}/clear)
|
||||
- [ ] 支持会话关键词搜索功能
|
||||
- [ ] 实现会话列表分页查询
|
||||
- [ ] 创建SessionService、SessionController和相关DTO
|
||||
- [ ] 实现会话权限验证(用户只能操作自己的会话)
|
||||
- [ ] 添加会话统计信息更新机制
|
||||
|
||||
## 注意事项
|
||||
1. 会话ID使用UUID格式,确保全局唯一性
|
||||
2. 创建会话时需要验证用户登录状态和标题合法性
|
||||
3. 获取会话列表需要按最后消息时间倒序排列
|
||||
4. 关键词搜索需要支持标题和消息内容的模糊匹配
|
||||
5. 分页参数需要设置合理的默认值和最大值限制
|
||||
6. 删除会话时需要级联删除相关的消息和收藏记录
|
||||
7. 清空会话只删除消息,保留会话本身
|
||||
8. 用户只能访问和操作自己创建的会话
|
||||
10. 需要实时更新会话的消息数量和最后消息时间
|
||||
12. 考虑会话的软删除机制,便于数据恢复
|
||||
14. 会话列表需要返回消息数量和最后消息时间
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/controller/SessionController.java
|
||||
- src/main/java/com/lxy/hsend/service/SessionService.java
|
||||
- src/main/java/com/lxy/hsend/dto/session/
|
||||
- src/main/java/com/lxy/hsend/repository/SessionRepository.java
|
||||
37
doc/TASK006-消息处理模块.md
Normal file
37
doc/TASK006-消息处理模块.md
Normal file
@ -0,0 +1,37 @@
|
||||
# TASK006 - 消息处理模块
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK006
|
||||
- **任务名称**: 消息处理模块
|
||||
- **版本**: V1.0
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 任务描述
|
||||
实现消息发送和处理功能,包括用户消息存储、AI响应生成、消息历史查询等核心聊天功能,为后续AI服务集成做好接口预留。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] 实现发送消息接口(/api/chat/send)
|
||||
- [x] 实现消息存储和查询功能
|
||||
- [x] 创建消息实体和相关DTO类
|
||||
- [x] 实现消息分页查询
|
||||
- [x] 添加消息内容验证和过滤
|
||||
- [x] 实现消息角色区分(user/assistant)
|
||||
- [x] 创建MessageService、MessageController
|
||||
- [x] 预留AI服务调用接口
|
||||
- [x] 实现消息时间戳记录
|
||||
- [x] 添加消息状态管理
|
||||
|
||||
## 注意事项
|
||||
1. 消息ID使用UUID格式确保唯一性
|
||||
8. 支持深度思考和联网搜索的参数传递
|
||||
9. 消息发送失败时需要返回明确的错误信息
|
||||
10. 考虑消息的编辑和删除功能预留
|
||||
12. 需要统计每个会话的消息数量
|
||||
14. 预留消息附件(图片、文件)的关联字段
|
||||
15. 实现消息的软删除机制
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/controller/MessageController.java
|
||||
- src/main/java/com/lxy/hsend/service/MessageService.java
|
||||
- src/main/java/com/lxy/hsend/dto/message/
|
||||
- src/main/java/com/lxy/hsend/repository/MessageRepository.java
|
||||
45
doc/TASK007-收藏功能模块.md
Normal file
45
doc/TASK007-收藏功能模块.md
Normal file
@ -0,0 +1,45 @@
|
||||
# TASK007 - 收藏功能模块
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK007
|
||||
- **任务名称**: 收藏功能模块
|
||||
- **版本**: V1.0
|
||||
- **状态**: ✅ 完成
|
||||
|
||||
## 任务描述
|
||||
实现用户消息收藏功能,包括添加收藏、取消收藏、获取收藏列表等操作,支持收藏消息的搜索和分页查询功能。
|
||||
|
||||
## 验收标准清单
|
||||
- [x] 实现收藏消息接口(/api/favorites/add)
|
||||
- [x] 实现取消收藏接口(/api/favorites/remove)
|
||||
- [x] 实现获取收藏列表接口(/api/favorites/list)
|
||||
- [x] 创建收藏实体和相关DTO类
|
||||
- [x] 实现收藏状态查询和更新
|
||||
- [x] 支持收藏列表分页功能
|
||||
- [x] 添加收藏重复性检查
|
||||
- [x] 创建FavoriteService、FavoriteController
|
||||
- [x] 实现收藏权限验证
|
||||
- [x] 添加收藏数量统计功能
|
||||
|
||||
## 注意事项
|
||||
1. 收藏记录需要关联用户ID、消息ID和会话ID
|
||||
2. 同一条消息不能被同一用户重复收藏
|
||||
3. 取消收藏时需要验证收藏记录的存在性
|
||||
4. 用户只能操作自己的收藏记录
|
||||
5. 收藏列表需要显示消息内容和所属会话标题
|
||||
6. 收藏列表按收藏时间倒序排列
|
||||
7. 删除消息时需要同步删除相关收藏记录
|
||||
8. 删除会话时需要同步删除相关收藏记录
|
||||
9. 收藏ID使用UUID格式确保唯一性
|
||||
10. 需要在消息查询时返回收藏状态
|
||||
11. 收藏列表支持按会话标题搜索
|
||||
12. 设置用户收藏数量的合理上限
|
||||
13. 收藏操作需要添加操作日志
|
||||
14. 考虑收藏的分类和标签功能预留
|
||||
15. 收藏列表需要显示消息的完整上下文信息
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/controller/FavoriteController.java
|
||||
- src/main/java/com/lxy/hsend/service/FavoriteService.java
|
||||
- src/main/java/com/lxy/hsend/dto/favorite/
|
||||
- src/main/java/com/lxy/hsend/repository/FavoriteRepository.java
|
||||
47
doc/TASK008-文件上传模块.md
Normal file
47
doc/TASK008-文件上传模块.md
Normal file
@ -0,0 +1,47 @@
|
||||
# TASK008 - 文件上传模块
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK008
|
||||
- **任务名称**: 文件上传模块
|
||||
- **版本**: V1.0
|
||||
- **状态**: 跳过该模块,暂时进行不开发
|
||||
|
||||
## 任务描述
|
||||
实现文件上传功能,支持txt、pdf、md等格式文件的上传、存储和管理,为AI文档分析功能提供基础支持。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 实现文件上传接口(/api/files/upload)
|
||||
- [ ] 支持多种文件格式验证(txt、pdf、md、docx等)
|
||||
- [ ] 实现文件大小限制和安全检查
|
||||
- [ ] 创建文件存储和管理功能
|
||||
- [ ] 实现文件元数据记录
|
||||
- [ ] 添加文件访问权限控制
|
||||
- [ ] 创建FileService、FileController和相关DTO
|
||||
- [ ] 实现文件内容解析预处理
|
||||
- [ ] 添加文件删除和清理功能
|
||||
- [ ] 实现文件上传进度反馈
|
||||
|
||||
## 注意事项
|
||||
1. 文件大小限制在10MB以内,防止服务器资源耗尽
|
||||
2. 支持的文件类型:.txt、.pdf、.md、.docx、.doc、.xlsx、.xls
|
||||
3. 文件名需要重命名,使用UUID+原扩展名避免冲突
|
||||
4. 上传的文件需要进行病毒扫描和安全检查
|
||||
5. 文件存储路径需要按日期分层组织(如/files/2024/01/01/)
|
||||
6. 记录文件的原始名称、存储路径、大小、类型等元数据
|
||||
7. 用户只能访问自己上传的文件
|
||||
8. 文件上传需要关联到具体的会话
|
||||
9. 实现文件的软删除,定期清理无效文件
|
||||
10. 添加文件上传失败的重试机制
|
||||
11. 支持断点续传功能(可选)
|
||||
12. 文件存储考虑使用OSS或本地文件系统
|
||||
13. 需要生成文件的访问URL
|
||||
14. 添加文件下载接口用于预览
|
||||
15. 实现文件内容的文本提取功能
|
||||
16. 设置用户文件数量和总大小限制
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/controller/FileController.java
|
||||
- src/main/java/com/lxy/hsend/service/FileService.java
|
||||
- src/main/java/com/lxy/hsend/dto/file/
|
||||
- src/main/java/com/lxy/hsend/repository/FileRepository.java
|
||||
- src/main/java/com/lxy/hsend/util/FileUtil.java
|
||||
46
doc/TASK009-AI服务集成.md
Normal file
46
doc/TASK009-AI服务集成.md
Normal file
@ -0,0 +1,46 @@
|
||||
# TASK009 - AI服务集成
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK009
|
||||
- **任务名称**: AI服务集成
|
||||
- **版本**: V1.0
|
||||
- **状态**: 计划中
|
||||
|
||||
## 任务描述
|
||||
集成外部AI服务接口,实现多模型支持、深度思考模式、联网搜索等AI增强功能,为用户提供智能对话服务。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 集成腾讯混元AI模型接口
|
||||
- [ ] 实现基础模型和增强模型切换
|
||||
- [ ] 集成深度思考模式功能
|
||||
- [ ] 实现联网搜索功能
|
||||
- [ ] 创建AI服务抽象层和实现类
|
||||
- [ ] 实现AI响应的流式处理
|
||||
- [ ] 添加AI服务的错误处理和重试机制
|
||||
- [ ] 实现会话上下文管理
|
||||
- [ ] 添加AI调用的监控和日志
|
||||
- [ ] 实现AI服务的负载均衡
|
||||
|
||||
## 注意事项
|
||||
1. AI模型支持:Hunyuan(混元)、基础模型、增强模型
|
||||
2. 深度思考模式需要调用更强的推理模型,响应时间较长
|
||||
3. 联网搜索需要整合搜索引擎API获取实时信息
|
||||
4. AI服务调用需要添加超时控制(如30秒)
|
||||
5. 实现AI响应的流式输出,提升用户体验
|
||||
6. 会话上下文需要控制长度,避免token超限
|
||||
7. AI服务异常时需要返回友好的错误提示
|
||||
8. 记录AI调用的token消耗和成本统计
|
||||
9. 实现AI服务的API密钥管理和轮换
|
||||
10. 添加AI内容的安全过滤和合规检查
|
||||
11. 支持AI模型的动态配置和热切换
|
||||
12. 实现AI响应的缓存机制优化性能
|
||||
13. 添加AI服务的限流和降级策略
|
||||
14. 文件上传后需要提取内容传递给AI分析
|
||||
15. 实现AI对话的记忆管理和遗忘机制
|
||||
16. 支持自定义AI系统提示词配置
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/service/ai/
|
||||
- src/main/java/com/lxy/hsend/config/AIConfig.java
|
||||
- src/main/java/com/lxy/hsend/dto/ai/
|
||||
- src/main/java/com/lxy/hsend/integration/
|
||||
46
doc/TASK010-安全性和权限控制.md
Normal file
46
doc/TASK010-安全性和权限控制.md
Normal file
@ -0,0 +1,46 @@
|
||||
# TASK010 - 安全性和权限控制
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK010
|
||||
- **任务名称**: 安全性和权限控制
|
||||
- **版本**: V1.0
|
||||
- **状态**: 计划中
|
||||
|
||||
## 任务描述
|
||||
加强系统安全性,实现接口鉴权、参数验证、防攻击措施、数据加密等安全功能,确保用户数据安全和系统稳定运行。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 实现JWT token验证和刷新机制
|
||||
- [ ] 添加接口访问频率限制(Rate Limiting)
|
||||
- [ ] 实现参数校验和SQL注入防护
|
||||
- [ ] 添加XSS攻击防护
|
||||
- [ ] 实现CSRF防护机制
|
||||
- [ ] 配置HTTPS和安全响应头
|
||||
- [ ] 实现敏感数据加密存储
|
||||
- [ ] 添加操作日志和审计功能
|
||||
- [ ] 实现IP白名单和黑名单机制
|
||||
- [ ] 配置跨域访问控制
|
||||
|
||||
## 注意事项
|
||||
1. 所有需要登录的接口都必须验证JWT token
|
||||
2. 实现token的自动续期机制,提升用户体验
|
||||
3. 对高频接口添加访问频率限制(如每分钟最多100次请求)
|
||||
4. 用户输入必须进行严格的参数验证和过滤
|
||||
5. 密码、密钥等敏感信息需要加密存储
|
||||
6. 记录所有重要操作的日志,包括用户行为和系统异常
|
||||
7. 生产环境必须启用HTTPS,禁用HTTP
|
||||
8. 配置安全响应头:X-Frame-Options、X-XSS-Protection等
|
||||
9. 实现账户锁定机制,防止暴力破解
|
||||
10. 文件上传需要检查文件类型和内容安全性
|
||||
11. AI生成内容需要进行敏感信息过滤
|
||||
12. 数据库查询需要使用预编译语句防止SQL注入
|
||||
13. 会话管理需要防止会话劫持
|
||||
14. 实现接口访问的白名单机制
|
||||
15. 添加异常访问的监控和告警
|
||||
16. 配置合理的CORS策略
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/security/
|
||||
- src/main/java/com/lxy/hsend/config/SecurityConfig.java
|
||||
- src/main/java/com/lxy/hsend/filter/
|
||||
- src/main/java/com/lxy/hsend/aspect/SecurityAspect.java
|
||||
46
doc/TASK011-性能优化和缓存.md
Normal file
46
doc/TASK011-性能优化和缓存.md
Normal file
@ -0,0 +1,46 @@
|
||||
# TASK011 - 性能优化和缓存
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK011
|
||||
- **任务名称**: 性能优化和缓存
|
||||
- **版本**: V1.0
|
||||
- **状态**: 计划中
|
||||
|
||||
## 任务描述
|
||||
实现系统性能优化和缓存机制,包括Redis缓存、数据库优化、接口性能监控等功能,提升系统响应速度和并发处理能力。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 配置Redis缓存和缓存策略
|
||||
- [ ] 实现热点数据缓存(用户信息、会话列表等)
|
||||
- [ ] 优化数据库查询和索引
|
||||
- [ ] 实现分页查询优化
|
||||
- [ ] 添加接口性能监控和统计
|
||||
- [ ] 实现数据库连接池优化
|
||||
- [ ] 配置异步处理机制
|
||||
- [ ] 实现AI响应结果缓存
|
||||
- [ ] 添加静态资源CDN配置
|
||||
- [ ] 实现数据预加载机制
|
||||
|
||||
## 注意事项
|
||||
1. 用户信息、会话列表等热点数据使用Redis缓存,设置合理的过期时间
|
||||
2. 缓存更新策略采用Cache-Aside模式,确保数据一致性
|
||||
3. 数据库查询添加必要的索引,特别是用户ID、会话ID等关联字段
|
||||
4. 分页查询使用游标分页替代传统offset分页,提升大数据量查询性能
|
||||
5. 接口响应时间监控,记录慢查询和异常情况
|
||||
6. 数据库连接池配置合理的最大连接数和超时时间
|
||||
7. AI服务调用使用异步处理,避免阻塞主线程
|
||||
8. 相同问题的AI响应可以缓存一定时间,减少重复调用
|
||||
9. 静态资源(头像、文件)使用CDN加速访问
|
||||
10. 系统启动时预加载常用配置和数据字典
|
||||
11. 实现数据库读写分离,提升查询性能
|
||||
12. 使用连接池管理Redis连接,避免连接泄露
|
||||
13. 实现缓存雪崩和缓存穿透的防护机制
|
||||
14. 大文件上传使用分片上传技术
|
||||
15. 配置JVM参数优化内存使用
|
||||
16. 实现系统资源使用率监控
|
||||
|
||||
## 相关文件
|
||||
- src/main/java/com/lxy/hsend/config/CacheConfig.java
|
||||
- src/main/java/com/lxy/hsend/config/RedisConfig.java
|
||||
- src/main/java/com/lxy/hsend/aspect/PerformanceAspect.java
|
||||
- src/main/java/com/lxy/hsend/service/cache/
|
||||
48
doc/TASK012-集成测试和文档完善.md
Normal file
48
doc/TASK012-集成测试和文档完善.md
Normal file
@ -0,0 +1,48 @@
|
||||
# TASK012 - 集成测试和文档完善
|
||||
|
||||
## 任务信息
|
||||
- **任务编号**: TASK012
|
||||
- **任务名称**: 集成测试和文档完善
|
||||
- **版本**: V1.0
|
||||
- **状态**: 计划中
|
||||
|
||||
## 任务描述
|
||||
编写完整的单元测试和集成测试,完善API文档和部署文档,确保系统功能正确性和可维护性,为项目上线做好准备。
|
||||
|
||||
## 验收标准清单
|
||||
- [ ] 编写单元测试覆盖所有Service层方法
|
||||
- [ ] 编写集成测试覆盖所有API接口
|
||||
- [ ] 实现数据库测试和Mock数据准备
|
||||
- [ ] 完善Swagger API文档注解
|
||||
- [ ] 编写部署文档和运维手册
|
||||
- [ ] 实现健康检查和监控接口
|
||||
- [ ] 添加系统性能基准测试
|
||||
- [ ] 创建Docker镜像和部署脚本
|
||||
- [ ] 编写用户使用手册
|
||||
- [ ] 实现自动化测试流程
|
||||
|
||||
## 注意事项
|
||||
1. 单元测试需要达到80%以上的代码覆盖率
|
||||
2. 集成测试需要覆盖所有API接口的正常和异常场景
|
||||
3. 使用TestContainers进行数据库集成测试
|
||||
4. Mock外部依赖(AI服务、文件存储等)进行隔离测试
|
||||
5. Swagger文档需要包含完整的参数说明和示例
|
||||
6. 部署文档需要包含环境要求、配置说明、启动步骤
|
||||
7. 健康检查接口需要检查数据库、Redis、外部服务连通性
|
||||
8. 性能测试需要模拟并发用户和大数据量场景
|
||||
9. Docker镜像需要优化大小和启动速度
|
||||
10. 测试数据需要支持自动清理和重置
|
||||
11. 文档需要包含常见问题解答和故障排查指南
|
||||
12. 实现CI/CD流水线自动化构建和部署
|
||||
13. 添加代码质量检查和安全扫描
|
||||
14. 准备压力测试报告和性能调优建议
|
||||
15. 编写数据库备份和恢复方案
|
||||
16. 制定系统监控和告警策略
|
||||
|
||||
## 相关文件
|
||||
- src/test/java/com/lxy/hsend/
|
||||
- docker/Dockerfile
|
||||
- docs/deployment.md
|
||||
- docs/api-guide.md
|
||||
- docs/troubleshooting.md
|
||||
- scripts/deploy.sh
|
||||
149
doc/任务总览.md
Normal file
149
doc/任务总览.md
Normal file
@ -0,0 +1,149 @@
|
||||
# 海角AI后端开发任务总览
|
||||
|
||||
## 项目概述
|
||||
海角AI是一个智能对话产品,采用经典的两栏式布局,为用户提供AI聊天、会话管理、消息收藏、文件上传等功能。本文档详细规划了后端开发的所有任务。
|
||||
|
||||
## 任务列表概览
|
||||
|
||||
| 任务编号 | 任务名称 | 版本 | 状态 | 优先级 | 预估工期 |
|
||||
|---------|---------|------|------|-------|---------|
|
||||
| TASK001 | 项目基础架构搭建 | V1.0 | 计划中 | 高 | 2天 |
|
||||
| TASK002 | 数据库设计和初始化 | V1.0 | 计划中 | 高 | 3天 |
|
||||
| TASK003 | 基础配置和公共组件 | V1.0 | 计划中 | 高 | 3天 |
|
||||
| TASK004 | 用户认证模块 | V1.0 | 计划中 | 高 | 4天 |
|
||||
| TASK005 | 聊天会话管理模块 | V1.0 | 计划中 | 中 | 3天 |
|
||||
| TASK006 | 消息处理模块 | V1.0 | 计划中 | 高 | 4天 |
|
||||
| TASK007 | 收藏功能模块 | V1.0 | 计划中 | 中 | 2天 |
|
||||
| TASK008 | 文件上传模块 | V1.0 | 计划中 | 中 | 3天 |
|
||||
| TASK009 | AI服务集成 | V1.0 | 计划中 | 高 | 5天 |
|
||||
| TASK010 | 安全性和权限控制 | V1.0 | 计划中 | 高 | 3天 |
|
||||
| TASK011 | 性能优化和缓存 | V1.0 | 计划中 | 中 | 3天 |
|
||||
| TASK012 | 集成测试和文档完善 | V1.0 | 计划中 | 中 | 4天 |
|
||||
|
||||
**总计预估工期:39天**
|
||||
|
||||
## 开发阶段规划
|
||||
|
||||
### 第一阶段:基础建设(7-8天)
|
||||
- TASK001:项目基础架构搭建
|
||||
- TASK002:数据库设计和初始化
|
||||
- TASK003:基础配置和公共组件
|
||||
|
||||
### 第二阶段:核心功能(11-13天)
|
||||
- TASK004:用户认证模块
|
||||
- TASK005:聊天会话管理模块
|
||||
- TASK006:消息处理模块
|
||||
- TASK009:AI服务集成
|
||||
|
||||
### 第三阶段:扩展功能(8-10天)
|
||||
- TASK007:收藏功能模块
|
||||
- TASK008:文件上传模块
|
||||
- TASK010:安全性和权限控制
|
||||
|
||||
### 第四阶段:优化完善(7天)
|
||||
- TASK011:性能优化和缓存
|
||||
- TASK012:集成测试和文档完善
|
||||
|
||||
## 技术栈说明
|
||||
|
||||
### 后端框架
|
||||
- **Spring Boot 3.x**:主要开发框架
|
||||
- **Spring Security**:安全认证框架
|
||||
- **Spring Data JPA**:数据持久层框架
|
||||
- **MySQL 8.0**:主数据库
|
||||
- **Redis**:缓存和会话存储
|
||||
|
||||
### 开发工具
|
||||
- **Maven**:项目构建和依赖管理
|
||||
- **JWT**:用户认证令牌
|
||||
- **Swagger/OpenAPI**:API文档生成
|
||||
- **Docker**:容器化部署
|
||||
- **Flyway**:数据库版本管理
|
||||
|
||||
### 外部服务
|
||||
- **腾讯混元AI**:AI对话服务
|
||||
- **文件存储**:本地存储或OSS
|
||||
- **搜索引擎API**:联网搜索功能
|
||||
|
||||
## API接口概览
|
||||
|
||||
### 用户认证模块(4个接口)
|
||||
- POST /api/auth/register - 用户注册
|
||||
- POST /api/auth/login - 用户登录
|
||||
- POST /api/auth/logout - 用户登出
|
||||
- GET /api/user/info - 获取用户信息
|
||||
|
||||
### 聊天模块(6个接口)
|
||||
- POST /api/chat/session - 创建会话
|
||||
- POST /api/chat/send - 发送消息
|
||||
- GET /api/chat/sessions - 获取会话列表
|
||||
- GET /api/chat/messages - 获取会话消息
|
||||
- DELETE /api/chat/session/{session_id} - 删除会话
|
||||
- POST /api/chat/session/{session_id}/clear - 清空会话
|
||||
|
||||
### 收藏模块(3个接口)
|
||||
- POST /api/favorites/add - 收藏消息
|
||||
- DELETE /api/favorites/remove - 取消收藏
|
||||
- GET /api/favorites/list - 获取收藏列表
|
||||
|
||||
### 文件上传模块(1个接口)
|
||||
- POST /api/files/upload - 上传文件
|
||||
|
||||
## 数据库表设计概览
|
||||
|
||||
### 核心业务表
|
||||
- **users**:用户表
|
||||
- **sessions**:会话表
|
||||
- **messages**:消息表
|
||||
- **favorites**:收藏表
|
||||
- **files**:文件表
|
||||
|
||||
### 系统表
|
||||
- **operation_logs**:操作日志表
|
||||
- **system_configs**:系统配置表
|
||||
|
||||
## 部署环境要求
|
||||
|
||||
### 硬件要求
|
||||
- **CPU**:4核心以上
|
||||
- **内存**:8GB以上
|
||||
- **存储**:50GB以上SSD
|
||||
|
||||
### 软件要求
|
||||
- **Java 17+**
|
||||
- **MySQL 8.0+**
|
||||
- **Redis 6.0+**
|
||||
- **Nginx**(反向代理)
|
||||
|
||||
## 风险控制
|
||||
|
||||
### 技术风险
|
||||
1. **AI服务依赖**:外部AI服务不稳定,需要实现降级和重试机制
|
||||
2. **并发处理**:高并发情况下的性能瓶颈,需要优化缓存和数据库
|
||||
3. **数据安全**:用户数据和会话内容的安全保护
|
||||
|
||||
### 业务风险
|
||||
1. **内容安全**:AI生成内容的合规性检查
|
||||
2. **用户体验**:响应时间和系统稳定性
|
||||
3. **扩展性**:后续功能扩展的架构支持
|
||||
|
||||
## 质量保证
|
||||
|
||||
### 代码质量
|
||||
- 代码覆盖率达到80%以上
|
||||
- 遵循阿里巴巴Java开发规范
|
||||
- 使用SonarQube进行代码质量检查
|
||||
|
||||
### 测试策略
|
||||
- 单元测试:所有Service层方法
|
||||
- 集成测试:所有API接口
|
||||
- 性能测试:并发用户场景模拟
|
||||
|
||||
### 文档要求
|
||||
- API文档:Swagger自动生成
|
||||
- 部署文档:详细的环境配置和部署步骤
|
||||
- 运维文档:监控、备份、故障处理指南
|
||||
|
||||
---
|
||||
|
||||
**备注**:本任务规划基于当前需求和技术选型,在实际开发过程中可能需要根据具体情况进行调整。建议采用敏捷开发模式,按阶段交付,及时收集反馈并迭代优化。
|
||||
16552
logs/hs-end.log
Normal file
16552
logs/hs-end.log
Normal file
File diff suppressed because it is too large
Load Diff
259
mvnw
vendored
Normal file
259
mvnw
vendored
Normal file
@ -0,0 +1,259 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
149
mvnw.cmd
vendored
Normal file
149
mvnw.cmd
vendored
Normal file
@ -0,0 +1,149 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
163
pom.xml
Normal file
163
pom.xml
Normal file
@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.4</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.lxy</groupId>
|
||||
<artifactId>hs-end</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>hs-end</name>
|
||||
<description>hs-end</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator for Health Check -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger/OpenAPI 3 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson for JSON processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Lang for Utility -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client for AI Service Integration -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- File Upload Support -->
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- H2 Database for Testing -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
13
src/main/java/com/lxy/hsend/HsEndApplication.java
Normal file
13
src/main/java/com/lxy/hsend/HsEndApplication.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.lxy.hsend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class HsEndApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HsEndApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
71
src/main/java/com/lxy/hsend/annotation/RateLimit.java
Normal file
71
src/main/java/com/lxy/hsend/annotation/RateLimit.java
Normal file
@ -0,0 +1,71 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 限流注解
|
||||
* 用于对接口进行访问频率限制
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
/**
|
||||
* 限流key的前缀,默认为方法的全限定名
|
||||
*/
|
||||
String key() default "";
|
||||
|
||||
/**
|
||||
* 时间窗口内允许的最大请求次数
|
||||
*/
|
||||
int count() default 100;
|
||||
|
||||
/**
|
||||
* 时间窗口大小
|
||||
*/
|
||||
int period() default 60;
|
||||
|
||||
/**
|
||||
* 时间单位
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 限流类型
|
||||
*/
|
||||
LimitType limitType() default LimitType.IP;
|
||||
|
||||
/**
|
||||
* 超出限制时的提示信息
|
||||
*/
|
||||
String message() default "访问频率过高,请稍后再试";
|
||||
|
||||
/**
|
||||
* 限流类型枚举
|
||||
*/
|
||||
enum LimitType {
|
||||
/**
|
||||
* 根据IP地址限流
|
||||
*/
|
||||
IP,
|
||||
|
||||
/**
|
||||
* 根据用户ID限流
|
||||
*/
|
||||
USER,
|
||||
|
||||
/**
|
||||
* 根据自定义key限流
|
||||
*/
|
||||
CUSTOM,
|
||||
|
||||
/**
|
||||
* 全局限流
|
||||
*/
|
||||
GLOBAL
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/lxy/hsend/annotation/RequestLog.java
Normal file
90
src/main/java/com/lxy/hsend/annotation/RequestLog.java
Normal file
@ -0,0 +1,90 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 请求日志注解
|
||||
* 用于标记需要记录请求日志的接口
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RequestLog {
|
||||
|
||||
/**
|
||||
* 操作描述
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 是否记录请求参数
|
||||
*/
|
||||
boolean logParams() default true;
|
||||
|
||||
/**
|
||||
* 是否记录响应结果
|
||||
*/
|
||||
boolean logResult() default true;
|
||||
|
||||
/**
|
||||
* 是否记录执行时间
|
||||
*/
|
||||
boolean logTime() default true;
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
OperationType type() default OperationType.OTHER;
|
||||
|
||||
/**
|
||||
* 操作类型枚举
|
||||
*/
|
||||
enum OperationType {
|
||||
/**
|
||||
* 查询操作
|
||||
*/
|
||||
QUERY,
|
||||
|
||||
/**
|
||||
* 新增操作
|
||||
*/
|
||||
CREATE,
|
||||
|
||||
/**
|
||||
* 更新操作
|
||||
*/
|
||||
UPDATE,
|
||||
|
||||
/**
|
||||
* 删除操作
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
/**
|
||||
* 登录操作
|
||||
*/
|
||||
LOGIN,
|
||||
|
||||
/**
|
||||
* 登出操作
|
||||
*/
|
||||
LOGOUT,
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
UPLOAD,
|
||||
|
||||
/**
|
||||
* 文件下载
|
||||
*/
|
||||
DOWNLOAD,
|
||||
|
||||
/**
|
||||
* 其他操作
|
||||
*/
|
||||
OTHER
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/RequireAuth.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/RequireAuth.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 需要认证的注解
|
||||
* 用于标记需要用户登录才能访问的接口
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RequireAuth {
|
||||
|
||||
/**
|
||||
* 是否可选认证(如果有token则验证,没有token则不验证)
|
||||
*/
|
||||
boolean optional() default false;
|
||||
|
||||
/**
|
||||
* 描述信息
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/ValidEmail.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/ValidEmail.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import com.lxy.hsend.annotation.validator.EmailValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 邮箱格式验证注解
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = EmailValidator.class)
|
||||
@Documented
|
||||
public @interface ValidEmail {
|
||||
|
||||
String message() default "邮箱格式不正确";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/ValidPassword.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/ValidPassword.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import com.lxy.hsend.annotation.validator.PasswordValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 密码格式验证注解
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = PasswordValidator.class)
|
||||
@Documented
|
||||
public @interface ValidPassword {
|
||||
|
||||
String message() default "密码格式不正确,密码长度应为6-20位,包含字母和数字";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.lxy.hsend.annotation.validator;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
/**
|
||||
* 邮箱格式验证器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidEmail constraintAnnotation) {
|
||||
// 初始化方法,可以获取注解参数
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String email, ConstraintValidatorContext context) {
|
||||
// 空值交给@NotNull等注解处理
|
||||
if (email == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return StringUtil.isValidEmail(email);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.lxy.hsend.annotation.validator;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidPassword;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
/**
|
||||
* 密码格式验证器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidPassword constraintAnnotation) {
|
||||
// 初始化方法,可以获取注解参数
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String password, ConstraintValidatorContext context) {
|
||||
// 空值交给@NotNull等注解处理
|
||||
if (password == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return StringUtil.isValidPassword(password);
|
||||
}
|
||||
}
|
||||
123
src/main/java/com/lxy/hsend/common/ApiResponse.java
Normal file
123
src/main/java/com/lxy/hsend/common/ApiResponse.java
Normal file
@ -0,0 +1,123 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一API响应格式
|
||||
*
|
||||
* @author lxy
|
||||
* @param <T> 响应数据类型
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 错误码,0表示成功
|
||||
*/
|
||||
private Integer error;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T body;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 成功响应(无数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success() {
|
||||
return new ApiResponse<T>(0, null, "操作成功", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(带数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<T>(0, data, "操作成功", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(带数据和消息)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
return new ApiResponse<T>(0, data, message, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(Integer errorCode, String message) {
|
||||
return new ApiResponse<T>(errorCode, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(带数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(Integer errorCode, String message, T data) {
|
||||
return new ApiResponse<T>(errorCode, data, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> validationError(String message) {
|
||||
return new ApiResponse<T>(1001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证失败
|
||||
*/
|
||||
public static <T> ApiResponse<T> authError(String message) {
|
||||
return new ApiResponse<T>(1002, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限不足
|
||||
*/
|
||||
public static <T> ApiResponse<T> forbiddenError(String message) {
|
||||
return new ApiResponse<T>(1003, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在
|
||||
*/
|
||||
public static <T> ApiResponse<T> notFoundError(String message) {
|
||||
return new ApiResponse<T>(1004, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务逻辑错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> businessError(String message) {
|
||||
return new ApiResponse<T>(2001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> systemError(String message) {
|
||||
return new ApiResponse<T>(5001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未知错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> unknownError() {
|
||||
return new ApiResponse<T>(5002, null, "系统出现未知错误", false);
|
||||
}
|
||||
}
|
||||
86
src/main/java/com/lxy/hsend/common/Constants.java
Normal file
86
src/main/java/com/lxy/hsend/common/Constants.java
Normal file
@ -0,0 +1,86 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
/**
|
||||
* 应用常量
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class Constants {
|
||||
|
||||
// JWT相关常量
|
||||
public static final String JWT_SECRET_KEY = "hs-end-jwt-secret-key-2024-secure-256bit-hmac-sha256";
|
||||
public static final String JWT_TOKEN_PREFIX = "Bearer ";
|
||||
public static final String JWT_HEADER_NAME = "Authorization";
|
||||
public static final long JWT_EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000L; // 7天
|
||||
public static final long JWT_REFRESH_TIME = 30 * 24 * 60 * 60 * 1000L; // 30天
|
||||
|
||||
// Redis相关常量
|
||||
public static final String REDIS_USER_SESSION_PREFIX = "user:session:";
|
||||
public static final String REDIS_JWT_BLACKLIST_PREFIX = "jwt:blacklist:";
|
||||
public static final String REDIS_USER_INFO_PREFIX = "user:info:";
|
||||
public static final String REDIS_RATE_LIMIT_PREFIX = "rate:limit:";
|
||||
public static final long REDIS_SESSION_EXPIRE_TIME = 7 * 24 * 60 * 60; // 7天(秒)
|
||||
public static final long REDIS_USER_INFO_EXPIRE_TIME = 30 * 60; // 30分钟(秒)
|
||||
|
||||
// 分页相关常量
|
||||
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||
public static final int MAX_PAGE_SIZE = 100;
|
||||
public static final int DEFAULT_PAGE_NUM = 1;
|
||||
|
||||
// 文件上传相关常量
|
||||
public static final long MAX_FILE_SIZE = 10 * 1024 * 1024L; // 10MB
|
||||
public static final String[] ALLOWED_FILE_TYPES = {
|
||||
"image/jpeg", "image/png", "image/gif", "image/webp",
|
||||
"application/pdf", "text/plain", "text/markdown",
|
||||
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
};
|
||||
public static final String FILE_UPLOAD_PATH = "uploads/";
|
||||
|
||||
// 用户相关常量
|
||||
public static final int PASSWORD_MIN_LENGTH = 6;
|
||||
public static final int PASSWORD_MAX_LENGTH = 20;
|
||||
public static final int NICKNAME_MIN_LENGTH = 1;
|
||||
public static final int NICKNAME_MAX_LENGTH = 50;
|
||||
|
||||
// 会话相关常量
|
||||
public static final int SESSION_TITLE_MAX_LENGTH = 100;
|
||||
public static final int MESSAGE_CONTENT_MAX_LENGTH = 10000;
|
||||
|
||||
// API相关常量
|
||||
public static final String API_PREFIX = "/api";
|
||||
public static final String API_VERSION = "v1";
|
||||
|
||||
// 时间格式常量
|
||||
public static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
public static final String TIMEZONE = "Asia/Shanghai";
|
||||
|
||||
// 请求限流常量
|
||||
public static final int RATE_LIMIT_REQUESTS = 100; // 每分钟最大请求数
|
||||
public static final int RATE_LIMIT_WINDOW = 60; // 时间窗口(秒)
|
||||
|
||||
// 邮箱验证正则
|
||||
public static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$";
|
||||
|
||||
// 密码验证正则(至少包含字母和数字)
|
||||
public static final String PASSWORD_REGEX = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{6,20}$";
|
||||
|
||||
// 用户状态
|
||||
public static final byte USER_STATUS_NORMAL = 1;
|
||||
public static final byte USER_STATUS_LOCKED = 2;
|
||||
public static final byte USER_STATUS_DELETED = 3;
|
||||
|
||||
// 消息角色
|
||||
public static final String MESSAGE_ROLE_USER = "user";
|
||||
public static final String MESSAGE_ROLE_ASSISTANT = "assistant";
|
||||
public static final String MESSAGE_ROLE_SYSTEM = "system";
|
||||
|
||||
// AI模型
|
||||
public static final String AI_MODEL_GPT35 = "gpt-3.5-turbo";
|
||||
public static final String AI_MODEL_GPT4 = "gpt-4";
|
||||
public static final String AI_MODEL_CLAUDE = "claude-3";
|
||||
|
||||
private Constants() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
74
src/main/java/com/lxy/hsend/common/ErrorCode.java
Normal file
74
src/main/java/com/lxy/hsend/common/ErrorCode.java
Normal file
@ -0,0 +1,74 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
/**
|
||||
* 错误码枚举
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public enum ErrorCode {
|
||||
|
||||
// 成功
|
||||
SUCCESS(0, "操作成功"),
|
||||
|
||||
// 客户端错误 1xxx
|
||||
VALIDATION_ERROR(1001, "参数验证失败"),
|
||||
AUTH_ERROR(1002, "认证失败"),
|
||||
FORBIDDEN_ERROR(1003, "权限不足"),
|
||||
NOT_FOUND_ERROR(1004, "资源不存在"),
|
||||
METHOD_NOT_ALLOWED(1005, "请求方法不支持"),
|
||||
RATE_LIMIT_ERROR(1006, "请求频率超限"),
|
||||
|
||||
// 业务逻辑错误 2xxx
|
||||
BUSINESS_ERROR(2001, "业务逻辑错误"),
|
||||
USER_NOT_FOUND(2002, "用户不存在"),
|
||||
USER_ALREADY_EXISTS(2003, "用户已存在"),
|
||||
RESOURCE_NOT_FOUND(2005, "资源不存在"),
|
||||
PASSWORD_ERROR(2004, "密码错误"),
|
||||
SESSION_NOT_FOUND(2005, "会话不存在"),
|
||||
MESSAGE_NOT_FOUND(2006, "消息不存在"),
|
||||
FILE_NOT_FOUND(2007, "文件不存在"),
|
||||
FILE_UPLOAD_ERROR(2008, "文件上传失败"),
|
||||
AI_SERVICE_ERROR(2009, "AI服务调用失败"),
|
||||
|
||||
// 第三方服务错误 3xxx
|
||||
EXTERNAL_SERVICE_ERROR(3001, "第三方服务异常"),
|
||||
AI_API_ERROR(3002, "AI接口调用失败"),
|
||||
REDIS_ERROR(3003, "Redis服务异常"),
|
||||
|
||||
// 数据访问错误 4xxx
|
||||
DATABASE_ERROR(4001, "数据库操作失败"),
|
||||
DATA_INTEGRITY_ERROR(4002, "数据完整性约束违反"),
|
||||
TRANSACTION_ERROR(4003, "事务处理失败"),
|
||||
|
||||
// 系统错误 5xxx
|
||||
SYSTEM_ERROR(5001, "系统内部错误"),
|
||||
UNKNOWN_ERROR(5002, "未知错误");
|
||||
|
||||
private final Integer code;
|
||||
private final String message;
|
||||
|
||||
ErrorCode(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误码获取枚举
|
||||
*/
|
||||
public static ErrorCode getByCode(Integer code) {
|
||||
for (ErrorCode errorCode : values()) {
|
||||
if (errorCode.getCode().equals(code)) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/lxy/hsend/config/JwtAuthenticationFilter.java
Normal file
159
src/main/java/com/lxy/hsend/config/JwtAuthenticationFilter.java
Normal file
@ -0,0 +1,159 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证拦截器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 不需要认证的路径
|
||||
private static final List<String> EXCLUDE_PATHS = Arrays.asList(
|
||||
"/api/auth/",
|
||||
"/api/health/",
|
||||
"/swagger-ui/",
|
||||
"/v3/api-docs/",
|
||||
"/actuator/",
|
||||
"/static/",
|
||||
"/uploads/"
|
||||
);
|
||||
|
||||
// 可选认证的路径(如果有token则验证,没有token也放行)
|
||||
private static final List<String> OPTIONAL_AUTH_PATHS = Arrays.asList(
|
||||
"/api/user/\\d+" // 匹配 /api/user/{userId} 格式
|
||||
);
|
||||
|
||||
@Autowired
|
||||
public JwtAuthenticationFilter(JwtUtil jwtUtil, ObjectMapper objectMapper) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
log.debug("JWT过滤器处理请求: {} {}", method, requestURI);
|
||||
|
||||
// 跳过OPTIONS请求
|
||||
if ("OPTIONS".equals(method)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否在排除列表中
|
||||
if (isExcludePath(requestURI)) {
|
||||
log.debug("路径在排除列表中,跳过认证: {}", requestURI);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否为可选认证路径
|
||||
boolean isOptionalAuth = isOptionalAuthPath(requestURI);
|
||||
|
||||
// 获取Token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
// 如果没有Token
|
||||
if (StringUtil.isEmpty(token)) {
|
||||
if (isOptionalAuth) {
|
||||
log.debug("可选认证路径,没有token,继续执行: {}", requestURI);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
} else {
|
||||
log.warn("缺少认证Token: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "请提供有效的认证Token");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证Token
|
||||
if (!jwtUtil.validateToken(token)) {
|
||||
log.warn("Token验证失败: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "Token无效或已过期,请重新登录");
|
||||
return;
|
||||
}
|
||||
|
||||
// Token验证通过,设置用户信息到请求属性
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
String email = jwtUtil.getEmailFromToken(token);
|
||||
|
||||
if (userId != null && email != null) {
|
||||
request.setAttribute("currentUserId", userId);
|
||||
request.setAttribute("currentUserEmail", email);
|
||||
log.debug("JWT认证成功: userId={}, email={}, uri={}", userId, email, requestURI);
|
||||
} else {
|
||||
log.warn("Token中用户信息不完整: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "Token信息不完整");
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JWT认证异常: {} - {}", requestURI, e.getMessage(), e);
|
||||
writeErrorResponse(response, ErrorCode.SYSTEM_ERROR, "认证系统异常");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在排除列表中
|
||||
*/
|
||||
private boolean isExcludePath(String requestURI) {
|
||||
return EXCLUDE_PATHS.stream().anyMatch(requestURI::startsWith);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为可选认证路径
|
||||
*/
|
||||
private boolean isOptionalAuthPath(String requestURI) {
|
||||
return OPTIONAL_AUTH_PATHS.stream()
|
||||
.anyMatch(pattern -> requestURI.matches("/api/user/\\d+"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应
|
||||
*/
|
||||
private void writeErrorResponse(HttpServletResponse response, ErrorCode errorCode, String message) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
response.setHeader("Access-Control-Allow-Headers", "*");
|
||||
|
||||
ApiResponse<Object> errorResponse = ApiResponse.error(errorCode.getCode(), message);
|
||||
String json = objectMapper.writeValueAsString(errorResponse);
|
||||
|
||||
response.getWriter().write(json);
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
88
src/main/java/com/lxy/hsend/config/RedisConfig.java
Normal file
88
src/main/java/com/lxy/hsend/config/RedisConfig.java
Normal file
@ -0,0 +1,88 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis配置类
|
||||
* 配置Redis连接和序列化方式
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 创建ObjectMapper实例并配置
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
jackson2JsonRedisSerializer.setObjectMapper(mapper);
|
||||
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// key采用String的序列化方式
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
// hash的key也采用String的序列化方式
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
// value序列化方式采用jackson
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
// hash的value序列化方式采用jackson
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存管理器配置
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||
// 创建ObjectMapper实例并配置
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// JSON序列化配置
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
jackson2JsonRedisSerializer.setObjectMapper(mapper);
|
||||
|
||||
// 配置序列化
|
||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofSeconds(Constants.REDIS_USER_INFO_EXPIRE_TIME)) // 默认缓存时间30分钟
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
|
||||
.disableCachingNullValues(); // 不缓存null值
|
||||
|
||||
return RedisCacheManager.builder(connectionFactory)
|
||||
.cacheDefaults(config)
|
||||
.transactionAware()
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/com/lxy/hsend/config/SecurityConfig.java
Normal file
70
src/main/java/com/lxy/hsend/config/SecurityConfig.java
Normal file
@ -0,0 +1,70 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security配置类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Autowired
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF保护(使用JWT,不需要CSRF保护)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 禁用默认的Session管理,使用无状态JWT
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 禁用默认的登录表单
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 配置请求授权
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 公开接口,无需认证
|
||||
.requestMatchers("/api/health/**").permitAll()
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/api/user/{userId}").permitAll() // 公开的用户信息接口
|
||||
|
||||
// Swagger相关接口
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
|
||||
|
||||
// Actuator监控接口
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
|
||||
// 静态资源
|
||||
.requestMatchers("/static/**", "/uploads/**").permitAll()
|
||||
|
||||
// 其他所有请求需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// 添加JWT认证拦截器(在Spring Security的UsernamePasswordAuthenticationFilter之前执行)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
42
src/main/java/com/lxy/hsend/config/SwaggerConfig.java
Normal file
42
src/main/java/com/lxy/hsend/config/SwaggerConfig.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger配置类
|
||||
* 配置API文档生成
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("海角AI - API接口文档")
|
||||
.version("1.0.0")
|
||||
.description("海角AI后端服务API接口文档,提供用户认证、聊天会话、消息处理、收藏管理、文件上传等功能。")
|
||||
.contact(new Contact()
|
||||
.name("海角AI团队")
|
||||
.email("contact@haijiao.ai"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("bearerAuth",
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.description("JWT认证令牌,格式:Bearer <token>")))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/lxy/hsend/config/WebConfig.java
Normal file
36
src/main/java/com/lxy/hsend/config/WebConfig.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置跨域访问等Web相关设置
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${cors.allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${cors.allowed-methods}")
|
||||
private String allowedMethods;
|
||||
|
||||
@Value("${cors.allowed-headers}")
|
||||
private String allowedHeaders;
|
||||
|
||||
@Value("${cors.allow-credentials}")
|
||||
private boolean allowCredentials;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns(allowedOrigins.split(","))
|
||||
.allowedMethods(allowedMethods.split(","))
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowCredentials(allowCredentials)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/lxy/hsend/config/WebMvcConfig.java
Normal file
95
src/main/java/com/lxy/hsend/config/WebMvcConfig.java
Normal file
@ -0,0 +1,95 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.*;
|
||||
|
||||
/**
|
||||
* Web MVC配置
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 配置CORS跨域
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置静态资源处理
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 文件上传资源
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:" + Constants.FILE_UPLOAD_PATH);
|
||||
|
||||
// Swagger UI资源
|
||||
registry.addResourceHandler("/swagger-ui/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/springdoc-openapi-ui/");
|
||||
|
||||
// 静态资源
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置路径匹配
|
||||
*/
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
// 设置路径匹配策略
|
||||
configurer.setUseTrailingSlashMatch(true);
|
||||
// 设置后缀模式匹配
|
||||
configurer.setUseSuffixPatternMatch(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置内容协商
|
||||
*/
|
||||
@Override
|
||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||
configurer
|
||||
.defaultContentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.favorParameter(false)
|
||||
.ignoreAcceptHeader(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 这里可以添加自定义拦截器
|
||||
// 例如:认证拦截器、日志拦截器等
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置视图解析器
|
||||
*/
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
// 配置JSON视图解析器
|
||||
registry.enableContentNegotiation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置消息转换器
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageConverters(java.util.List<org.springframework.http.converter.HttpMessageConverter<?>> converters) {
|
||||
// Spring Boot会自动配置Jackson消息转换器
|
||||
WebMvcConfigurer.super.configureMessageConverters(converters);
|
||||
}
|
||||
}
|
||||
130
src/main/java/com/lxy/hsend/controller/AuthController.java
Normal file
130
src/main/java/com/lxy/hsend/controller/AuthController.java
Normal file
@ -0,0 +1,130 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.LoginResponse;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.service.UserService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@Tag(name = "认证管理", description = "用户认证相关接口")
|
||||
public class AuthController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public AuthController(UserService userService, JwtUtil jwtUtil) {
|
||||
this.userService = userService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册", description = "通过邮箱和密码注册新用户")
|
||||
@RequestLog(value = "用户注册", type = RequestLog.OperationType.CREATE)
|
||||
public ApiResponse<UserInfoResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
UserInfoResponse userInfo = userService.register(request);
|
||||
return ApiResponse.success(userInfo, "注册成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "用户登录", description = "通过邮箱和密码登录")
|
||||
@RequestLog(value = "用户登录", type = RequestLog.OperationType.LOGIN)
|
||||
public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
LoginResponse loginResponse = userService.login(request);
|
||||
return ApiResponse.success(loginResponse, "登录成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@RequireAuth
|
||||
@Operation(summary = "用户登出", description = "用户登出,将token加入黑名单")
|
||||
@RequestLog(value = "用户登出", type = RequestLog.OperationType.LOGOUT)
|
||||
public ApiResponse<Void> logout(HttpServletRequest request) {
|
||||
// 从请求头获取token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null) {
|
||||
// 从token获取用户ID
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
if (userId != null) {
|
||||
userService.logout(userId, token);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse.success(null, "登出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
@Operation(summary = "刷新Token", description = "使用刷新Token获取新的访问Token")
|
||||
@RequestLog(value = "刷新Token", type = RequestLog.OperationType.OTHER)
|
||||
public ApiResponse<LoginResponse> refreshToken(@RequestParam String refreshToken) {
|
||||
try {
|
||||
// 验证刷新Token
|
||||
if (!jwtUtil.validateToken(refreshToken)) {
|
||||
return ApiResponse.authError("刷新Token无效");
|
||||
}
|
||||
|
||||
String tokenType = jwtUtil.getTokenTypeFromToken(refreshToken);
|
||||
if (!"refresh".equals(tokenType)) {
|
||||
return ApiResponse.authError("Token类型错误");
|
||||
}
|
||||
|
||||
// 生成新的访问Token
|
||||
String newAccessToken = jwtUtil.refreshToken(refreshToken);
|
||||
Long userId = jwtUtil.getUserIdFromToken(refreshToken);
|
||||
|
||||
// 获取用户信息
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
|
||||
LoginResponse loginResponse = new LoginResponse(newAccessToken, refreshToken,
|
||||
com.lxy.hsend.common.Constants.JWT_EXPIRATION_TIME / 1000, userInfo);
|
||||
|
||||
return ApiResponse.success(loginResponse, "Token刷新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("Token刷新失败: {}", e.getMessage());
|
||||
return ApiResponse.authError("Token刷新失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@GetMapping("/check-email")
|
||||
@Operation(summary = "检查邮箱", description = "检查邮箱是否已被注册")
|
||||
public ApiResponse<Boolean> checkEmail(@RequestParam String email) {
|
||||
boolean exists = userService.existsByEmail(email);
|
||||
return ApiResponse.success(exists, exists ? "邮箱已存在" : "邮箱可用");
|
||||
}
|
||||
}
|
||||
128
src/main/java/com/lxy/hsend/controller/FavoriteController.java
Normal file
128
src/main/java/com/lxy/hsend/controller/FavoriteController.java
Normal file
@ -0,0 +1,128 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RateLimit;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.service.FavoriteService;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 收藏控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/favorites")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "收藏管理", description = "消息收藏相关接口")
|
||||
public class FavoriteController {
|
||||
|
||||
private final FavoriteService favoriteService;
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 60, period = 60) // 每分钟最多60次收藏操作
|
||||
@Operation(summary = "添加收藏", description = "将指定消息添加到收藏列表")
|
||||
public ApiResponse<FavoriteResponse> addFavorite(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid AddFavoriteRequest request) {
|
||||
|
||||
log.info("用户添加收藏: user={}, messageId={}, sessionId={}",
|
||||
userDetails.getUsername(), request.getMessageId(), request.getSessionId());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.addFavorite(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 60, period = 60) // 每分钟最多60次取消收藏操作
|
||||
@Operation(summary = "取消收藏", description = "从收藏列表中移除指定消息")
|
||||
public ApiResponse<Void> removeFavorite(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid RemoveFavoriteRequest request) {
|
||||
|
||||
log.info("用户取消收藏: user={}, messageId={}",
|
||||
userDetails.getUsername(), request.getMessageId());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.removeFavorite(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收藏列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取收藏列表", description = "分页获取用户的收藏消息列表,支持关键词搜索")
|
||||
public ApiResponse<PageUtil.PageResponse<FavoriteResponse>> getFavoriteList(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@Parameter(description = "消息角色过滤") @RequestParam(defaultValue = "all") String messageRole) {
|
||||
|
||||
log.info("获取收藏列表: user={}, keyword={}, page={}, pageSize={}, messageRole={}",
|
||||
userDetails.getUsername(), keyword, page, pageSize, messageRole);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
FavoriteListRequest request = new FavoriteListRequest(keyword, page, pageSize, messageRole);
|
||||
return favoriteService.getFavoriteList(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否被收藏
|
||||
*/
|
||||
@GetMapping("/check/{messageId}")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "检查收藏状态", description = "检查指定消息是否已被当前用户收藏")
|
||||
public ApiResponse<Boolean> checkFavoriteStatus(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "消息ID") @PathVariable String messageId) {
|
||||
|
||||
log.info("检查收藏状态: user={}, messageId={}", userDetails.getUsername(), messageId);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.isFavorited(userId, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏数量
|
||||
*/
|
||||
@GetMapping("/count")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取收藏数量", description = "获取当前用户的总收藏数量")
|
||||
public ApiResponse<Long> getFavoriteCount(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
|
||||
log.info("获取收藏数量: user={}", userDetails.getUsername());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.getUserFavoriteCount(userId);
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/lxy/hsend/controller/HealthController.java
Normal file
47
src/main/java/com/lxy/hsend/controller/HealthController.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查控制器
|
||||
* 提供系统健康状态检查接口
|
||||
*/
|
||||
@Tag(name = "健康检查", description = "系统健康状态检查相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/health")
|
||||
public class HealthController {
|
||||
|
||||
@Operation(summary = "系统健康检查", description = "检查系统运行状态")
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("status", "UP");
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
response.put("service", "hs-end");
|
||||
response.put("version", "1.0.0");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "系统信息", description = "获取系统基本信息")
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<Map<String, Object>> systemInfo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("application", "海角AI后端服务");
|
||||
response.put("version", "1.0.0");
|
||||
response.put("java.version", System.getProperty("java.version"));
|
||||
response.put("os.name", System.getProperty("os.name"));
|
||||
response.put("startup.time", LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RateLimit;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.service.MessageService;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 消息控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "消息管理", description = "聊天消息相关接口")
|
||||
public class MessageController {
|
||||
|
||||
private final MessageService messageService;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 30, period = 60) // 每分钟最多30条消息
|
||||
@Operation(summary = "发送消息", description = "发送聊天消息并获取AI响应")
|
||||
public ApiResponse<MessageResponse> sendMessage(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid SendMessageRequest request) {
|
||||
|
||||
log.info("用户发送消息: user={}, sessionId={}, contentLength={}",
|
||||
userDetails.getUsername(), request.getSessionId(), request.getContent().length());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return messageService.sendMessage(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
@GetMapping("/messages")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取消息列表", description = "分页获取指定会话的消息列表")
|
||||
public ApiResponse<PageUtil.PageResponse<MessageResponse>> getMessages(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "会话ID") @RequestParam String sessionId,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@Parameter(description = "角色过滤") @RequestParam(defaultValue = "all") String role) {
|
||||
|
||||
log.info("获取消息列表: user={}, sessionId={}, page={}, pageSize={}, role={}",
|
||||
userDetails.getUsername(), sessionId, page, pageSize, role);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
MessageListRequest request = new MessageListRequest(sessionId, page, pageSize, role);
|
||||
return messageService.getMessages(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息详情
|
||||
*/
|
||||
@GetMapping("/message/{messageId}")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取消息详情", description = "根据消息ID获取消息详细信息")
|
||||
public ApiResponse<MessageResponse> getMessageById(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "消息ID") @PathVariable String messageId) {
|
||||
|
||||
log.info("获取消息详情: user={}, messageId={}", userDetails.getUsername(), messageId);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return messageService.getMessageById(userId, messageId);
|
||||
}
|
||||
}
|
||||
166
src/main/java/com/lxy/hsend/controller/SessionController.java
Normal file
166
src/main/java/com/lxy/hsend/controller/SessionController.java
Normal file
@ -0,0 +1,166 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.session.*;
|
||||
import com.lxy.hsend.service.SessionService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 会话控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequireAuth
|
||||
@Tag(name = "会话管理", description = "聊天会话管理相关接口")
|
||||
public class SessionController {
|
||||
|
||||
private final SessionService sessionService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public SessionController(SessionService sessionService, JwtUtil jwtUtil) {
|
||||
this.sessionService = sessionService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
@PostMapping("/session")
|
||||
@Operation(summary = "创建会话", description = "创建一个新的聊天会话")
|
||||
@RequestLog(value = "创建会话", type = RequestLog.OperationType.CREATE)
|
||||
public ApiResponse<SessionDetailResponse> createSession(
|
||||
@Valid @RequestBody CreateSessionRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.createSession(userId, request);
|
||||
|
||||
return ApiResponse.success(response, "会话创建成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
@GetMapping("/sessions")
|
||||
@Operation(summary = "获取会话列表", description = "获取用户的会话列表,支持搜索和分页")
|
||||
@RequestLog(value = "获取会话列表", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<Page<SessionListResponse>> getSessionList(
|
||||
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@Parameter(description = "排序字段") @RequestParam(defaultValue = "last_message_time") String sortBy,
|
||||
@Parameter(description = "排序方向") @RequestParam(defaultValue = "desc") String sortOrder,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
|
||||
// 构建搜索请求
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setKeyword(keyword);
|
||||
searchRequest.setPage(page);
|
||||
searchRequest.setPageSize(pageSize);
|
||||
searchRequest.setSortBy(sortBy);
|
||||
searchRequest.setSortOrder(sortOrder);
|
||||
|
||||
Page<SessionListResponse> response = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
return ApiResponse.success(response, "获取会话列表成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
@GetMapping("/session/{sessionId}")
|
||||
@Operation(summary = "获取会话详情", description = "根据会话ID获取会话的详细信息")
|
||||
@RequestLog(value = "获取会话详情", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<SessionDetailResponse> getSessionDetail(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.getSessionDetail(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(response, "获取会话详情成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
*/
|
||||
@PutMapping("/session/{sessionId}")
|
||||
@Operation(summary = "更新会话", description = "更新会话信息,如标题等")
|
||||
@RequestLog(value = "更新会话", type = RequestLog.OperationType.UPDATE)
|
||||
public ApiResponse<SessionDetailResponse> updateSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
@Valid @RequestBody UpdateSessionRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.updateSession(userId, sessionId, request);
|
||||
|
||||
return ApiResponse.success(response, "会话更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
@DeleteMapping("/session/{sessionId}")
|
||||
@Operation(summary = "删除会话", description = "删除指定的会话及其所有消息和收藏记录")
|
||||
@RequestLog(value = "删除会话", type = RequestLog.OperationType.DELETE)
|
||||
public ApiResponse<Void> deleteSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
sessionService.deleteSession(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(null, "会话删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话
|
||||
*/
|
||||
@PostMapping("/session/{sessionId}/clear")
|
||||
@Operation(summary = "清空会话", description = "清空会话中的所有消息,但保留会话本身")
|
||||
@RequestLog(value = "清空会话", type = RequestLog.OperationType.DELETE)
|
||||
public ApiResponse<Void> clearSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
sessionService.clearSession(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(null, "会话清空成功");
|
||||
}
|
||||
|
||||
// 获取会话消息列表的功能现在由 MessageController 处理
|
||||
|
||||
/**
|
||||
* 从请求中获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null && jwtUtil.validateToken(token)) {
|
||||
return jwtUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
throw new RuntimeException("无法获取用户信息");
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/lxy/hsend/controller/UserController.java
Normal file
98
src/main/java/com/lxy/hsend/controller/UserController.java
Normal file
@ -0,0 +1,98 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.user.UpdateUserRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.service.UserService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@Tag(name = "用户管理", description = "用户信息相关接口")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService, JwtUtil jwtUtil) {
|
||||
this.userService = userService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
@RequireAuth
|
||||
@Operation(summary = "获取用户信息", description = "获取当前登录用户的基本信息")
|
||||
@RequestLog(value = "获取用户信息", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<UserInfoResponse> getUserInfo(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
return ApiResponse.success(userInfo, "获取用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户信息
|
||||
*/
|
||||
@PutMapping("/info")
|
||||
@RequireAuth
|
||||
@Operation(summary = "更新用户信息", description = "更新当前登录用户的基本信息")
|
||||
@RequestLog(value = "更新用户信息", type = RequestLog.OperationType.UPDATE)
|
||||
public ApiResponse<UserInfoResponse> updateUserInfo(@Valid @RequestBody UpdateUserRequest request, HttpServletRequest httpRequest) {
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
UserInfoResponse userInfo = userService.updateUser(userId, request);
|
||||
return ApiResponse.success(userInfo, "更新用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息(公开接口,用于获取其他用户的基本信息)
|
||||
*/
|
||||
@GetMapping("/{userId}")
|
||||
@Operation(summary = "根据ID获取用户信息", description = "获取指定用户的基本信息(公开信息)")
|
||||
@RequestLog(value = "根据ID获取用户信息", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<UserInfoResponse> getUserById(@PathVariable Long userId) {
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
|
||||
// 对于公开接口,隐藏敏感信息
|
||||
UserInfoResponse publicInfo = new UserInfoResponse();
|
||||
publicInfo.setUserId(userInfo.getUserId());
|
||||
publicInfo.setNickname(userInfo.getNickname());
|
||||
publicInfo.setAvatarUrl(userInfo.getAvatarUrl());
|
||||
publicInfo.setCreatedAt(userInfo.getCreatedAt());
|
||||
// 不返回邮箱、状态、最后登录时间等敏感信息
|
||||
|
||||
return ApiResponse.success(publicInfo, "获取用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null && jwtUtil.validateToken(token)) {
|
||||
return jwtUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
throw new RuntimeException("无法获取用户信息");
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/lxy/hsend/dto/auth/LoginRequest.java
Normal file
33
src/main/java/com/lxy/hsend/dto/auth/LoginRequest.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 用户登录请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@ValidEmail(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 记住登录状态(可选,暂时保留)
|
||||
*/
|
||||
private Boolean rememberMe = false;
|
||||
}
|
||||
53
src/main/java/com/lxy/hsend/dto/auth/LoginResponse.java
Normal file
53
src/main/java/com/lxy/hsend/dto/auth/LoginResponse.java
Normal file
@ -0,0 +1,53 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户登录响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
/**
|
||||
* JWT访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* JWT刷新令牌
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
/**
|
||||
* 令牌过期时间(秒)
|
||||
*/
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
private UserInfoResponse userInfo;
|
||||
|
||||
/**
|
||||
* 构造方法(不包含tokenType)
|
||||
*/
|
||||
public LoginResponse(String accessToken, String refreshToken, Long expiresIn, UserInfoResponse userInfo) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.tokenType = "Bearer";
|
||||
this.expiresIn = expiresIn;
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/lxy/hsend/dto/auth/RegisterRequest.java
Normal file
49
src/main/java/com/lxy/hsend/dto/auth/RegisterRequest.java
Normal file
@ -0,0 +1,49 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import com.lxy.hsend.annotation.ValidPassword;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 用户注册请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@ValidEmail(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@ValidPassword(message = "密码格式不正确,密码长度应为6-20位,包含字母和数字")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 确认密码
|
||||
*/
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@NotBlank(message = "昵称不能为空")
|
||||
@Size(min = 1, max = 50, message = "昵称长度应在1-50个字符之间")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
private String avatarUrl;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 添加收藏请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AddFavoriteRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 收藏列表查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FavoriteListRequest {
|
||||
|
||||
/**
|
||||
* 搜索关键词(会话标题、消息内容)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页数量
|
||||
*/
|
||||
@Min(value = 1, message = "每页数量必须大于0")
|
||||
@Max(value = 100, message = "每页数量不能超过100")
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 消息类型过滤(user/assistant/all)
|
||||
*/
|
||||
private String messageRole = "all";
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 收藏响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class FavoriteResponse {
|
||||
|
||||
/**
|
||||
* 收藏ID
|
||||
*/
|
||||
private String favoriteId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String sessionTitle;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String messageContent;
|
||||
|
||||
/**
|
||||
* 消息角色(user/assistant)
|
||||
*/
|
||||
private String messageRole;
|
||||
|
||||
/**
|
||||
* 消息创建时间
|
||||
*/
|
||||
private LocalDateTime messageCreatedAt;
|
||||
|
||||
/**
|
||||
* 收藏时间
|
||||
*/
|
||||
private LocalDateTime favoritedAt;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 取消收藏请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RemoveFavoriteRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
private String messageId;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 消息列表查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MessageListRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页数量
|
||||
*/
|
||||
@Min(value = 1, message = "每页数量必须大于0")
|
||||
@Max(value = 100, message = "每页数量不能超过100")
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 消息类型过滤(user/assistant/all)
|
||||
*/
|
||||
private String role = "all";
|
||||
}
|
||||
63
src/main/java/com/lxy/hsend/dto/message/MessageResponse.java
Normal file
63
src/main/java/com/lxy/hsend/dto/message/MessageResponse.java
Normal file
@ -0,0 +1,63 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class MessageResponse {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息角色(user/assistant)
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 消息状态(pending/completed/failed)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 是否深度思考
|
||||
*/
|
||||
private Boolean deepThinking;
|
||||
|
||||
/**
|
||||
* 是否联网搜索
|
||||
*/
|
||||
private Boolean webSearch;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 发送消息请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SendMessageRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
@Size(min = 1, max = 4000, message = "消息内容长度必须在1-4000字符之间")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否深度思考
|
||||
*/
|
||||
private Boolean deepThinking = false;
|
||||
|
||||
/**
|
||||
* 是否联网搜索
|
||||
*/
|
||||
private Boolean webSearch = false;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 创建会话请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class CreateSessionRequest {
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@NotBlank(message = "会话标题不能为空")
|
||||
@Size(min = 1, max = 100, message = "会话标题长度应在1-100个字符之间")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 初始消息内容(可选)
|
||||
*/
|
||||
@Size(max = 10000, message = "消息内容长度不能超过10000个字符")
|
||||
private String initialMessage;
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 会话详情响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionDetailResponse {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息总数
|
||||
*/
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 最后消息时间
|
||||
*/
|
||||
private Long lastMessageTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 检查是否有消息
|
||||
*/
|
||||
public boolean hasMessages() {
|
||||
return messageCount != null && messageCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会话是否为空(无消息)
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return !hasMessages();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 会话列表响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionListResponse {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息总数
|
||||
*/
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 最后一条消息的内容摘要
|
||||
*/
|
||||
private String lastMessageContent;
|
||||
|
||||
/**
|
||||
* 最后消息时间
|
||||
*/
|
||||
private Long lastMessageTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 获取最后消息内容的摘要(限制长度)
|
||||
*/
|
||||
public String getLastMessageSummary() {
|
||||
if (lastMessageContent == null || lastMessageContent.trim().isEmpty()) {
|
||||
return "暂无消息";
|
||||
}
|
||||
|
||||
String content = lastMessageContent.trim();
|
||||
if (content.length() <= 50) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.substring(0, 47) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有消息
|
||||
*/
|
||||
public boolean hasMessages() {
|
||||
return messageCount != null && messageCount > 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 会话搜索请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class SessionSearchRequest {
|
||||
|
||||
/**
|
||||
* 搜索关键词(标题和消息内容)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Min(value = 1, message = "每页大小必须大于0")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/**
|
||||
* 排序字段(支持:created_at, updated_at, last_message_time)
|
||||
*/
|
||||
private String sortBy = "last_message_time";
|
||||
|
||||
/**
|
||||
* 排序方向(asc: 升序, desc: 降序)
|
||||
*/
|
||||
private String sortOrder = "desc";
|
||||
|
||||
/**
|
||||
* 检查是否有搜索关键词
|
||||
*/
|
||||
public boolean hasKeyword() {
|
||||
return keyword != null && !keyword.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取清理后的关键词
|
||||
*/
|
||||
public String getCleanKeyword() {
|
||||
return hasKeyword() ? keyword.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序方向是否为升序
|
||||
*/
|
||||
public boolean isAscOrder() {
|
||||
return "asc".equalsIgnoreCase(sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序方向是否为降序
|
||||
*/
|
||||
public boolean isDescOrder() {
|
||||
return "desc".equalsIgnoreCase(sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的排序字段
|
||||
*/
|
||||
public String getValidSortBy() {
|
||||
if (sortBy == null) {
|
||||
return "last_message_time";
|
||||
}
|
||||
|
||||
switch (sortBy.toLowerCase()) {
|
||||
case "created_at":
|
||||
case "updated_at":
|
||||
case "last_message_time":
|
||||
return sortBy.toLowerCase();
|
||||
default:
|
||||
return "last_message_time";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 更新会话请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class UpdateSessionRequest {
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@Size(min = 1, max = 100, message = "会话标题长度应在1-100个字符之间")
|
||||
private String title;
|
||||
}
|
||||
26
src/main/java/com/lxy/hsend/dto/user/UpdateUserRequest.java
Normal file
26
src/main/java/com/lxy/hsend/dto/user/UpdateUserRequest.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.lxy.hsend.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 更新用户信息请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class UpdateUserRequest {
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@Size(min = 1, max = 50, message = "昵称长度应在1-50个字符之间")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@Size(max = 500, message = "头像URL长度不能超过500个字符")
|
||||
private String avatarUrl;
|
||||
}
|
||||
93
src/main/java/com/lxy/hsend/dto/user/UserInfoResponse.java
Normal file
93
src/main/java/com/lxy/hsend/dto/user/UserInfoResponse.java
Normal file
@ -0,0 +1,93 @@
|
||||
package com.lxy.hsend.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户信息响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserInfoResponse {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* 用户状态(1-正常,2-锁定,3-删除)
|
||||
*/
|
||||
private Byte status;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
private Long lastLoginTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 获取状态描述
|
||||
*/
|
||||
public String getStatusText() {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "正常";
|
||||
case 2:
|
||||
return "锁定";
|
||||
case 3:
|
||||
return "删除";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户状态是否正常
|
||||
*/
|
||||
public boolean isNormal() {
|
||||
return status != null && status == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被锁定
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
return status != null && status == 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被删除
|
||||
*/
|
||||
public boolean isDeleted() {
|
||||
return status != null && status == 3;
|
||||
}
|
||||
}
|
||||
181
src/main/java/com/lxy/hsend/entity/Favorite.java
Normal file
181
src/main/java/com/lxy/hsend/entity/Favorite.java
Normal file
@ -0,0 +1,181 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 收藏实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "favorites",
|
||||
indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_message_id", columnList = "messageId"),
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_user_message", columnNames = {"user_id", "message_id"})
|
||||
}
|
||||
)
|
||||
public class Favorite {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "favorite_id", nullable = false, unique = true, length = 36)
|
||||
private String favoriteId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "message_id", nullable = false, length = 36)
|
||||
private String messageId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Favorite() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.favoriteId = UUID.randomUUID().toString();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Favorite(Long userId, String messageId, String sessionId) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.messageId = messageId;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFavoriteId() {
|
||||
return favoriteId;
|
||||
}
|
||||
|
||||
public void setFavoriteId(String favoriteId) {
|
||||
this.favoriteId = favoriteId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Favorite{" +
|
||||
"id=" + id +
|
||||
", favoriteId='" + favoriteId + '\'' +
|
||||
", userId=" + userId +
|
||||
", messageId='" + messageId + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
286
src/main/java/com/lxy/hsend/entity/File.java
Normal file
286
src/main/java/com/lxy/hsend/entity/File.java
Normal file
@ -0,0 +1,286 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "files", indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_file_type", columnList = "fileType"),
|
||||
@Index(name = "idx_upload_time", columnList = "uploadTime"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class File {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "file_id", nullable = false, unique = true, length = 36)
|
||||
private String fileId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "filename", nullable = false)
|
||||
private String filename;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "stored_filename", nullable = false)
|
||||
private String storedFilename;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 500)
|
||||
@Column(name = "file_path", nullable = false, length = 500)
|
||||
private String filePath;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "file_size", nullable = false)
|
||||
private Long fileSize;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 100)
|
||||
@Column(name = "file_type", nullable = false, length = 100)
|
||||
private String fileType;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "mime_type", length = 100)
|
||||
private String mimeType;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "upload_time", nullable = false)
|
||||
private Long uploadTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public File() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.fileId = UUID.randomUUID().toString();
|
||||
this.uploadTime = now;
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public File(Long userId, String filename, String storedFilename, String filePath,
|
||||
Long fileSize, String fileType, String mimeType) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.filename = filename;
|
||||
this.storedFilename = storedFilename;
|
||||
this.filePath = filePath;
|
||||
this.fileSize = fileSize;
|
||||
this.fileType = fileType;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getStoredFilename() {
|
||||
return storedFilename;
|
||||
}
|
||||
|
||||
public void setStoredFilename(String storedFilename) {
|
||||
this.storedFilename = storedFilename;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public Long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(Long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public Long getUploadTime() {
|
||||
return uploadTime;
|
||||
}
|
||||
|
||||
public void setUploadTime(Long uploadTime) {
|
||||
this.uploadTime = uploadTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public String getFileSizeFormatted() {
|
||||
if (fileSize == null) return "0 B";
|
||||
|
||||
long size = fileSize;
|
||||
String[] units = {"B", "KB", "MB", "GB", "TB"};
|
||||
int unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return size + " " + units[unitIndex];
|
||||
}
|
||||
|
||||
public boolean isImageFile() {
|
||||
if (mimeType == null) return false;
|
||||
return mimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
public boolean isDocumentFile() {
|
||||
if (fileType == null) return false;
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("pdf") || lowerType.equals("doc") ||
|
||||
lowerType.equals("docx") || lowerType.equals("txt") ||
|
||||
lowerType.equals("md");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "File{" +
|
||||
"id=" + id +
|
||||
", fileId='" + fileId + '\'' +
|
||||
", userId=" + userId +
|
||||
", filename='" + filename + '\'' +
|
||||
", fileSize=" + fileSize +
|
||||
", fileType='" + fileType + '\'' +
|
||||
", uploadTime=" + uploadTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
266
src/main/java/com/lxy/hsend/entity/Message.java
Normal file
266
src/main/java/com/lxy/hsend/entity/Message.java
Normal file
@ -0,0 +1,266 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 消息实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "messages", indexes = {
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_role", columnList = "role"),
|
||||
@Index(name = "idx_timestamp", columnList = "timestamp"),
|
||||
@Index(name = "idx_is_favorited", columnList = "isFavorited"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class Message {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "message_id", nullable = false, unique = true, length = 36)
|
||||
private String messageId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 20)
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
private String role; // user/assistant
|
||||
|
||||
@NotBlank
|
||||
@Lob
|
||||
@Column(name = "content", nullable = false, columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(name = "model_used", length = 50)
|
||||
private String modelUsed;
|
||||
|
||||
@Column(name = "deep_thinking")
|
||||
private Boolean deepThinking = false;
|
||||
|
||||
@Column(name = "web_search")
|
||||
private Boolean webSearch = false;
|
||||
|
||||
@Column(name = "is_favorited")
|
||||
private Boolean isFavorited = false;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private Long timestamp;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Message() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.messageId = UUID.randomUUID().toString();
|
||||
this.timestamp = now;
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Message(String sessionId, Long userId, String role, String content) {
|
||||
this();
|
||||
this.sessionId = sessionId;
|
||||
this.userId = userId;
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getModelUsed() {
|
||||
return modelUsed;
|
||||
}
|
||||
|
||||
public void setModelUsed(String modelUsed) {
|
||||
this.modelUsed = modelUsed;
|
||||
}
|
||||
|
||||
public Boolean getDeepThinking() {
|
||||
return deepThinking;
|
||||
}
|
||||
|
||||
public void setDeepThinking(Boolean deepThinking) {
|
||||
this.deepThinking = deepThinking;
|
||||
}
|
||||
|
||||
public Boolean getWebSearch() {
|
||||
return webSearch;
|
||||
}
|
||||
|
||||
public void setWebSearch(Boolean webSearch) {
|
||||
this.webSearch = webSearch;
|
||||
}
|
||||
|
||||
public Boolean getIsFavorited() {
|
||||
return isFavorited;
|
||||
}
|
||||
|
||||
public void setIsFavorited(Boolean isFavorited) {
|
||||
this.isFavorited = isFavorited;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public boolean isUserMessage() {
|
||||
return "user".equals(role);
|
||||
}
|
||||
|
||||
public boolean isAssistantMessage() {
|
||||
return "assistant".equals(role);
|
||||
}
|
||||
|
||||
public void toggleFavorite() {
|
||||
this.isFavorited = !Boolean.TRUE.equals(this.isFavorited);
|
||||
}
|
||||
|
||||
// 消息角色常量
|
||||
public static final String ROLE_USER = "user";
|
||||
public static final String ROLE_ASSISTANT = "assistant";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Message{" +
|
||||
"id=" + id +
|
||||
", messageId='" + messageId + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", role='" + role + '\'' +
|
||||
", isFavorited=" + isFavorited +
|
||||
", timestamp=" + timestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
192
src/main/java/com/lxy/hsend/entity/Session.java
Normal file
192
src/main/java/com/lxy/hsend/entity/Session.java
Normal file
@ -0,0 +1,192 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 会话实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sessions", indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_last_message_time", columnList = "lastMessageTime"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class Session {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, unique = true, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 200)
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(name = "message_count")
|
||||
private Integer messageCount = 0;
|
||||
|
||||
@Column(name = "last_message_time")
|
||||
private Long lastMessageTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Session() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.sessionId = UUID.randomUUID().toString();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Session(Long userId, String title) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Integer getMessageCount() {
|
||||
return messageCount;
|
||||
}
|
||||
|
||||
public void setMessageCount(Integer messageCount) {
|
||||
this.messageCount = messageCount;
|
||||
}
|
||||
|
||||
public Long getLastMessageTime() {
|
||||
return lastMessageTime;
|
||||
}
|
||||
|
||||
public void setLastMessageTime(Long lastMessageTime) {
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public void incrementMessageCount() {
|
||||
this.messageCount = (this.messageCount == null ? 0 : this.messageCount) + 1;
|
||||
this.lastMessageTime = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public void decrementMessageCount() {
|
||||
this.messageCount = Math.max(0, (this.messageCount == null ? 0 : this.messageCount) - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session{" +
|
||||
"id=" + id +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", userId=" + userId +
|
||||
", title='" + title + '\'' +
|
||||
", messageCount=" + messageCount +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
185
src/main/java/com/lxy/hsend/entity/User.java
Normal file
185
src/main/java/com/lxy/hsend/entity/User.java
Normal file
@ -0,0 +1,185 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_status", columnList = "status"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
@Size(max = 100)
|
||||
@Column(name = "email", nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 50)
|
||||
@Column(name = "nickname", nullable = false, length = 50)
|
||||
private String nickname;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "avatar_url", length = 500)
|
||||
private String avatarUrl;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "status", nullable = false, columnDefinition = "TINYINT")
|
||||
private Byte status = 1; // 1-正常,2-锁定,3-删除
|
||||
|
||||
@Column(name = "last_login_time")
|
||||
private Long lastLoginTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 构造函数
|
||||
public User() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Byte getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Byte status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getLastLoginTime() {
|
||||
return lastLoginTime;
|
||||
}
|
||||
|
||||
public void setLastLoginTime(Long lastLoginTime) {
|
||||
this.lastLoginTime = lastLoginTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public boolean isNormal() {
|
||||
return status == 1;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return status == 2;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
this.status = 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", email='" + email + '\'' +
|
||||
", nickname='" + nickname + '\'' +
|
||||
", status=" + status +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/lxy/hsend/exception/BusinessException.java
Normal file
76
src/main/java/com/lxy/hsend/exception/BusinessException.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.lxy.hsend.exception;
|
||||
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
public BusinessException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
// 便捷构造方法
|
||||
public static BusinessException of(ErrorCode errorCode) {
|
||||
return new BusinessException(errorCode);
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode, String message) {
|
||||
return new BusinessException(errorCode, message);
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode, String message, Throwable cause) {
|
||||
return new BusinessException(errorCode, message, cause);
|
||||
}
|
||||
|
||||
// 常用业务异常
|
||||
public static BusinessException userNotFound() {
|
||||
return new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException userAlreadyExists() {
|
||||
return new BusinessException(ErrorCode.USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
public static BusinessException passwordError() {
|
||||
return new BusinessException(ErrorCode.PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
public static BusinessException sessionNotFound() {
|
||||
return new BusinessException(ErrorCode.SESSION_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException messageNotFound() {
|
||||
return new BusinessException(ErrorCode.MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException fileNotFound() {
|
||||
return new BusinessException(ErrorCode.FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException fileUploadError(String message) {
|
||||
return new BusinessException(ErrorCode.FILE_UPLOAD_ERROR, message);
|
||||
}
|
||||
|
||||
public static BusinessException aiServiceError(String message) {
|
||||
return new BusinessException(ErrorCode.AI_SERVICE_ERROR, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
package com.lxy.hsend.exception;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 业务异常处理
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
log.warn("业务异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getErrorCode().getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证异常处理
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数验证异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定异常处理
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBindException(BindException e, HttpServletRequest request) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数绑定异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 约束违反异常处理
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
String message = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("约束违反异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少请求参数异常处理
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMissingParamException(MissingServletRequestParameterException e, HttpServletRequest request) {
|
||||
String message = String.format("缺少必需的请求参数: %s", e.getParameterName());
|
||||
log.warn("缺少请求参数异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型不匹配异常处理
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
String message = String.format("参数类型不匹配: %s", e.getName());
|
||||
log.warn("参数类型不匹配异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP消息不可读异常处理
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||
log.warn("HTTP消息不可读异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.validationError("请求体格式错误"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常处理
|
||||
*/
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleAuthenticationException(AuthenticationException e, HttpServletRequest request) {
|
||||
log.warn("认证异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.authError("认证失败,请重新登录"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 凭据错误异常处理
|
||||
*/
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBadCredentialsException(BadCredentialsException e, HttpServletRequest request) {
|
||||
log.warn("凭据错误异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.authError("用户名或密码错误"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限不足异常处理
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
|
||||
log.warn("权限不足异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.forbiddenError("权限不足,无法访问该资源"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求方法不支持异常处理
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
|
||||
log.warn("请求方法不支持异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED.getCode(), "请求方法不支持"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在异常处理
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
log.warn("资源不存在异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.notFoundError("请求的资源不存在"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传大小超限异常处理
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
|
||||
log.warn("文件上传大小超限异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.FILE_UPLOAD_ERROR.getCode(), "文件大小超过限制"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据完整性违反异常处理
|
||||
*/
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleDataIntegrityViolationException(DataIntegrityViolationException e, HttpServletRequest request) {
|
||||
log.error("数据完整性违反异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.DATA_INTEGRITY_ERROR.getCode(), "数据操作违反完整性约束"));
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL异常处理
|
||||
*/
|
||||
@ExceptionHandler(SQLException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleSQLException(SQLException e, HttpServletRequest request) {
|
||||
log.error("SQL异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.DATABASE_ERROR.getCode(), "数据库操作失败"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用异常处理
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception e, HttpServletRequest request) {
|
||||
log.error("未知异常: {} - {}", request.getRequestURI(), e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.systemError("系统内部错误,请稍后重试"));
|
||||
}
|
||||
}
|
||||
231
src/main/java/com/lxy/hsend/repository/FavoriteRepository.java
Normal file
231
src/main/java/com/lxy/hsend/repository/FavoriteRepository.java
Normal file
@ -0,0 +1,231 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 收藏数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
|
||||
|
||||
/**
|
||||
* 根据收藏ID查找收藏
|
||||
*/
|
||||
Optional<Favorite> findByFavoriteId(String favoriteId);
|
||||
|
||||
/**
|
||||
* 根据收藏ID查找未删除的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.favoriteId = :favoriteId AND f.deletedAt IS NULL")
|
||||
Optional<Favorite> findByFavoriteIdAndNotDeleted(@Param("favoriteId") String favoriteId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID查找收藏
|
||||
*/
|
||||
Optional<Favorite> findByUserIdAndMessageId(Long userId, String messageId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID查找未删除的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
Optional<Favorite> findByUserIdAndMessageIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID分页查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据时间范围查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.createdAt BETWEEN :startTime AND :endTime AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndCreatedAtBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 检查用户是否已收藏某条消息
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM Favorite f WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
boolean existsByUserIdAndMessageIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 统计用户收藏总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计会话收藏总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据消息ID查找所有收藏记录
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
List<Favorite> findByMessageIdAndNotDeleted(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 软删除收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.favoriteId = :favoriteId")
|
||||
void softDeleteByFavoriteId(@Param("favoriteId") String favoriteId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID软删除收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
void softDeleteByUserIdAndMessageId(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除会话的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除指定消息的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByMessageId(@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查收藏是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM Favorite f WHERE f.favoriteId = :favoriteId AND f.userId = :userId AND f.deletedAt IS NULL")
|
||||
boolean existsByFavoriteIdAndUserIdAndNotDeleted(@Param("favoriteId") String favoriteId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findRecentFavoritesByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计今日收藏数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.createdAt >= :todayStart AND f.deletedAt IS NULL")
|
||||
long countTodayFavorites(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计系统总收藏数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 查找收藏数最多的消息
|
||||
*/
|
||||
@Query("SELECT f.messageId, COUNT(f) as favoriteCount FROM Favorite f WHERE f.deletedAt IS NULL GROUP BY f.messageId ORDER BY favoriteCount DESC")
|
||||
Page<Object[]> findMostFavoritedMessages(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 删除会话的所有收藏记录
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM Favorite f WHERE f.sessionId = :sessionId")
|
||||
void deleteBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据关键词搜索用户收藏(搜索消息内容和会话标题)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT f FROM Favorite f
|
||||
JOIN Message m ON f.messageId = m.messageId
|
||||
JOIN Session s ON f.sessionId = s.sessionId
|
||||
WHERE f.userId = :userId
|
||||
AND f.deletedAt IS NULL
|
||||
AND m.deletedAt IS NULL
|
||||
AND s.deletedAt IS NULL
|
||||
AND (m.content LIKE %:keyword% OR s.title LIKE %:keyword%)
|
||||
ORDER BY f.createdAt DESC
|
||||
""")
|
||||
Page<Favorite> findByUserIdAndKeywordAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据关键词和消息角色搜索用户收藏
|
||||
*/
|
||||
@Query("""
|
||||
SELECT f FROM Favorite f
|
||||
JOIN Message m ON f.messageId = m.messageId
|
||||
JOIN Session s ON f.sessionId = s.sessionId
|
||||
WHERE f.userId = :userId
|
||||
AND f.deletedAt IS NULL
|
||||
AND m.deletedAt IS NULL
|
||||
AND s.deletedAt IS NULL
|
||||
AND m.role = :role
|
||||
AND (m.content LIKE %:keyword% OR s.title LIKE %:keyword%)
|
||||
ORDER BY f.createdAt DESC
|
||||
""")
|
||||
Page<Favorite> findByUserIdAndKeywordAndRoleAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("role") String role,
|
||||
Pageable pageable);
|
||||
}
|
||||
218
src/main/java/com/lxy/hsend/repository/FileRepository.java
Normal file
218
src/main/java/com/lxy/hsend/repository/FileRepository.java
Normal file
@ -0,0 +1,218 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.File;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 文件数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface FileRepository extends JpaRepository<File, Long> {
|
||||
|
||||
/**
|
||||
* 根据文件ID查找文件
|
||||
*/
|
||||
Optional<File> findByFileId(String fileId);
|
||||
|
||||
/**
|
||||
* 根据文件ID查找未删除的文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.fileId = :fileId AND f.deletedAt IS NULL")
|
||||
Optional<File> findByFileIdAndNotDeleted(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
Page<File> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据文件类型查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileType = :fileType AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFileTypeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("fileType") String fileType);
|
||||
|
||||
/**
|
||||
* 根据文件名搜索文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.filename LIKE %:filename% AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFilenameLikeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("filename") String filename);
|
||||
|
||||
/**
|
||||
* 根据MIME类型查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.mimeType = :mimeType AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndMimeTypeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("mimeType") String mimeType);
|
||||
|
||||
/**
|
||||
* 根据文件大小范围查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileSize BETWEEN :minSize AND :maxSize AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFileSizeBetween(@Param("userId") Long userId,
|
||||
@Param("minSize") Long minSize,
|
||||
@Param("maxSize") Long maxSize);
|
||||
|
||||
/**
|
||||
* 根据上传时间范围查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.uploadTime BETWEEN :startTime AND :endTime AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndUploadTimeBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM File f WHERE f.storedFilename = :storedFilename AND f.deletedAt IS NULL")
|
||||
boolean existsByStoredFilenameAndNotDeleted(@Param("storedFilename") String storedFilename);
|
||||
|
||||
/**
|
||||
* 统计用户文件总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计用户文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long sumFileSizeByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计会话文件数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据文件类型统计数量
|
||||
*/
|
||||
@Query("SELECT f.fileType, COUNT(f) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL GROUP BY f.fileType")
|
||||
List<Object[]> countByFileTypeAndUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找大文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileSize > :threshold AND f.deletedAt IS NULL ORDER BY f.fileSize DESC")
|
||||
List<File> findLargeFilesByUserId(@Param("userId") Long userId,
|
||||
@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 查找图片文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.mimeType LIKE 'image/%' AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findImageFilesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找文档文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileType IN ('pdf', 'doc', 'docx', 'txt', 'md') AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findDocumentFilesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 软删除文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.fileId = :fileId")
|
||||
void softDeleteByFileId(@Param("fileId") String fileId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除会话的所有文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查文件是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM File f WHERE f.fileId = :fileId AND f.userId = :userId AND f.deletedAt IS NULL")
|
||||
boolean existsByFileIdAndUserIdAndNotDeleted(@Param("fileId") String fileId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近上传的文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
Page<File> findRecentFilesByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找旧文件(用于清理)
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.uploadTime < :threshold AND f.deletedAt IS NULL")
|
||||
List<File> findOldFiles(@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 统计今日上传文件数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.uploadTime >= :todayStart AND f.deletedAt IS NULL")
|
||||
long countTodayUploads(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计今日上传文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.uploadTime >= :todayStart AND f.deletedAt IS NULL")
|
||||
long sumTodayUploadSize(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计系统总文件数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 统计系统文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.deletedAt IS NULL")
|
||||
long sumAllFileSize();
|
||||
|
||||
/**
|
||||
* 根据文件路径查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.filePath = :filePath AND f.deletedAt IS NULL")
|
||||
Optional<File> findByFilePathAndNotDeleted(@Param("filePath") String filePath);
|
||||
}
|
||||
199
src/main/java/com/lxy/hsend/repository/MessageRepository.java
Normal file
199
src/main/java/com/lxy/hsend/repository/MessageRepository.java
Normal file
@ -0,0 +1,199 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 消息数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||
|
||||
/**
|
||||
* 根据消息ID查找消息
|
||||
*/
|
||||
Optional<Message> findByMessageId(String messageId);
|
||||
|
||||
/**
|
||||
* 根据消息ID查找未删除的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.messageId = :messageId AND m.deletedAt IS NULL")
|
||||
Optional<Message> findByMessageIdAndNotDeleted(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找所有消息
|
||||
*/
|
||||
List<Message> findBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找未删除的消息,按时间排序
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndNotDeletedOrderByTimestamp(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查找未删除的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
Page<Message> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据角色查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.role = :role AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndRoleAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("role") String role);
|
||||
|
||||
/**
|
||||
* 查找收藏的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.isFavorited = true AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findFavoritedBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有收藏的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.isFavorited = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findFavoritedByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据内容关键词搜索消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.content LIKE %:keyword% AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndContentContaining(@Param("sessionId") String sessionId,
|
||||
@Param("keyword") String keyword);
|
||||
|
||||
/**
|
||||
* 根据AI模型查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.modelUsed = :model AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findByModelUsed(@Param("model") String model, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计会话消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 统计用户消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.userId = :userId AND m.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计用户收藏消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.userId = :userId AND m.isFavorited = true AND m.deletedAt IS NULL")
|
||||
long countFavoritedByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 获取会话的最后一条消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findLastMessageBySessionId(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 获取会话的第一条消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
Page<Message> findFirstMessageBySessionId(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找指定时间范围内的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.timestamp BETWEEN :startTime AND :endTime AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndTimestampBetween(@Param("sessionId") String sessionId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 更新消息收藏状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.isFavorited = :isFavorited, m.updatedAt = :updateTime WHERE m.messageId = :messageId")
|
||||
void updateFavoriteStatus(@Param("messageId") String messageId,
|
||||
@Param("isFavorited") Boolean isFavorited,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.messageId = :messageId")
|
||||
void softDeleteByMessageId(@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除会话的所有消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量取消收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.isFavorited = false, m.updatedAt = :updateTime WHERE m.messageId IN :messageIds")
|
||||
void unfavoriteMessages(@Param("messageIds") List<String> messageIds,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查消息是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(m) > 0 FROM Message m WHERE m.messageId = :messageId AND m.userId = :userId AND m.deletedAt IS NULL")
|
||||
boolean existsByMessageIdAndUserIdAndNotDeleted(@Param("messageId") String messageId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找使用深度思考的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.deepThinking = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findDeepThinkingMessages(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找使用联网搜索的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.webSearch = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findWebSearchMessages(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计各模型使用次数
|
||||
*/
|
||||
@Query("SELECT m.modelUsed, COUNT(m) FROM Message m WHERE m.modelUsed IS NOT NULL AND m.deletedAt IS NULL GROUP BY m.modelUsed")
|
||||
List<Object[]> countByModelUsage();
|
||||
|
||||
/**
|
||||
* 统计今日消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.timestamp >= :todayStart AND m.deletedAt IS NULL")
|
||||
long countTodayMessages(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 软删除会话的所有消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
void softDeleteBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
}
|
||||
162
src/main/java/com/lxy/hsend/repository/SessionRepository.java
Normal file
162
src/main/java/com/lxy/hsend/repository/SessionRepository.java
Normal file
@ -0,0 +1,162 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 会话数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface SessionRepository extends JpaRepository<Session, Long> {
|
||||
|
||||
/**
|
||||
* 根据会话ID查找会话
|
||||
*/
|
||||
Optional<Session> findBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.sessionId = :sessionId AND s.deletedAt IS NULL")
|
||||
Optional<Session> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有会话
|
||||
*/
|
||||
List<Session> findByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
List<Session> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
Page<Session> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据标题关键词搜索会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.title LIKE %:keyword% AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST")
|
||||
Page<Session> findByUserIdAndTitleContainingAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据关键词搜索会话(搜索标题和消息内容)
|
||||
*/
|
||||
@Query("SELECT DISTINCT s FROM Session s LEFT JOIN Message m ON s.sessionId = m.sessionId " +
|
||||
"WHERE s.userId = :userId AND s.deletedAt IS NULL " +
|
||||
"AND (s.title LIKE %:keyword% OR m.content LIKE %:keyword%) " +
|
||||
"ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
Page<Session> findByUserIdAndKeywordAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID和用户ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.sessionId = :sessionId AND s.userId = :userId AND s.deletedAt IS NULL")
|
||||
Optional<Session> findBySessionIdAndUserIdAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和时间范围查找会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.createdAt BETWEEN :startTime AND :endTime AND s.deletedAt IS NULL")
|
||||
List<Session> findByUserIdAndCreatedAtBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的会话总数
|
||||
*/
|
||||
@Query("SELECT COUNT(s) FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
Page<Session> findRecentSessionsByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找有消息的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.messageCount > 0 AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC")
|
||||
List<Session> findSessionsWithMessages(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找空会话(没有消息的会话)
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND (s.messageCount = 0 OR s.messageCount IS NULL) AND s.deletedAt IS NULL")
|
||||
List<Session> findEmptySessions(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 更新会话的消息统计
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.messageCount = :messageCount, s.lastMessageTime = :lastMessageTime, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void updateMessageStatistics(@Param("sessionId") String sessionId,
|
||||
@Param("messageCount") Integer messageCount,
|
||||
@Param("lastMessageTime") Long lastMessageTime,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.title = :title, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void updateTitle(@Param("sessionId") String sessionId,
|
||||
@Param("title") String title,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除会话
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.deletedAt = :deletedAt, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void softDeleteBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有会话
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.deletedAt = :deletedAt, s.updatedAt = :updateTime WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查会话是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(s) > 0 FROM Session s WHERE s.sessionId = :sessionId AND s.userId = :userId AND s.deletedAt IS NULL")
|
||||
boolean existsBySessionIdAndUserIdAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找长时间未活动的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.lastMessageTime < :threshold AND s.deletedAt IS NULL")
|
||||
List<Session> findInactiveSessions(@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 统计系统总会话数
|
||||
*/
|
||||
@Query("SELECT COUNT(s) FROM Session s WHERE s.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
}
|
||||
132
src/main/java/com/lxy/hsend/repository/UserRepository.java
Normal file
132
src/main/java/com/lxy/hsend/repository/UserRepository.java
Normal file
@ -0,0 +1,132 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.User;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 用户数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.email = :email AND u.deletedAt IS NULL")
|
||||
Optional<User> findByEmailAndNotDeleted(@Param("email") String email);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找未删除的用户(Spring Data方法命名)
|
||||
*/
|
||||
Optional<User> findByEmailAndDeletedAtIsNull(String email);
|
||||
|
||||
/**
|
||||
* 根据ID查找未删除的用户
|
||||
*/
|
||||
Optional<User> findByIdAndDeletedAtIsNull(Long id);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在(未删除)
|
||||
*/
|
||||
boolean existsByEmailAndDeletedAtIsNull(String email);
|
||||
|
||||
/**
|
||||
* 根据昵称查找用户
|
||||
*/
|
||||
List<User> findByNicknameContaining(String nickname);
|
||||
|
||||
/**
|
||||
* 根据状态查找用户
|
||||
*/
|
||||
List<User> findByStatus(Byte status);
|
||||
|
||||
/**
|
||||
* 查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
|
||||
List<User> findAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 分页查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
|
||||
Page<User> findAllNotDeleted(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在(排除指定用户)
|
||||
*/
|
||||
@Query("SELECT COUNT(u) > 0 FROM User u WHERE u.email = :email AND u.id != :userId AND u.deletedAt IS NULL")
|
||||
boolean existsByEmailAndNotUserId(@Param("email") String email, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据状态和创建时间范围查找用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.status = :status AND u.createdAt BETWEEN :startTime AND :endTime AND u.deletedAt IS NULL")
|
||||
List<User> findByStatusAndCreatedAtBetween(@Param("status") Byte status,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 统计正常状态的用户数量
|
||||
*/
|
||||
@Query("SELECT COUNT(u) FROM User u WHERE u.status = 1 AND u.deletedAt IS NULL")
|
||||
long countActiveUsers();
|
||||
|
||||
/**
|
||||
* 统计今日注册用户数量
|
||||
*/
|
||||
@Query("SELECT COUNT(u) FROM User u WHERE u.createdAt >= :todayStart AND u.deletedAt IS NULL")
|
||||
long countTodayRegistrations(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 更新用户最后登录时间
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.lastLoginTime = :loginTime, u.updatedAt = :updateTime WHERE u.id = :userId")
|
||||
void updateLastLoginTime(@Param("userId") Long userId,
|
||||
@Param("loginTime") Long loginTime,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除用户
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.deletedAt = :deletedAt, u.status = 3, u.updatedAt = :updateTime WHERE u.id = :userId")
|
||||
void softDeleteUser(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.status = :status, u.updatedAt = :updateTime WHERE u.id IN :userIds")
|
||||
void updateStatusByIds(@Param("userIds") List<Long> userIds,
|
||||
@Param("status") Byte status,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 查找长期未登录的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.lastLoginTime < :threshold AND u.deletedAt IS NULL")
|
||||
List<User> findInactiveUsers(@Param("threshold") Long threshold);
|
||||
}
|
||||
243
src/main/java/com/lxy/hsend/service/FavoriteService.java
Normal file
243
src/main/java/com/lxy/hsend/service/FavoriteService.java
Normal file
@ -0,0 +1,243 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 收藏服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FavoriteService {
|
||||
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* 用户收藏数量上限
|
||||
*/
|
||||
private static final long MAX_FAVORITES_PER_USER = 1000;
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<FavoriteResponse> addFavorite(Long userId, AddFavoriteRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证消息存在且属于用户
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(request.getMessageId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在"));
|
||||
|
||||
if (!message.getUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "无权限收藏此消息");
|
||||
}
|
||||
|
||||
// 3. 验证会话存在且属于用户
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 4. 检查是否已经收藏
|
||||
if (favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(userId, request.getMessageId())) {
|
||||
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "该消息已经收藏过了");
|
||||
}
|
||||
|
||||
// 5. 检查用户收藏数量是否超过限制
|
||||
long userFavoriteCount = favoriteRepository.countByUserIdAndNotDeleted(userId);
|
||||
if (userFavoriteCount >= MAX_FAVORITES_PER_USER) {
|
||||
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "收藏数量已达上限(" + MAX_FAVORITES_PER_USER + "条)");
|
||||
}
|
||||
|
||||
// 6. 创建收藏记录
|
||||
Favorite favorite = createFavorite(userId, request.getMessageId(), request.getSessionId());
|
||||
Favorite savedFavorite = favoriteRepository.save(favorite);
|
||||
|
||||
log.info("用户添加收藏成功: userId={}, messageId={}, favoriteId={}",
|
||||
userId, request.getMessageId(), savedFavorite.getFavoriteId());
|
||||
|
||||
// 7. 转换为响应DTO
|
||||
FavoriteResponse response = convertToFavoriteResponse(savedFavorite, message, session);
|
||||
return ApiResponse.success(response, "收藏成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<Void> removeFavorite(Long userId, RemoveFavoriteRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 查找收藏记录
|
||||
Favorite favorite = favoriteRepository.findByUserIdAndMessageIdAndNotDeleted(userId, request.getMessageId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "收藏记录不存在"));
|
||||
|
||||
// 3. 软删除收藏记录
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
favoriteRepository.softDeleteByUserIdAndMessageId(userId, request.getMessageId(), currentTime, currentTime);
|
||||
|
||||
log.info("用户取消收藏成功: userId={}, messageId={}, favoriteId={}",
|
||||
userId, request.getMessageId(), favorite.getFavoriteId());
|
||||
|
||||
return ApiResponse.success(null, "取消收藏成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收藏列表
|
||||
*/
|
||||
public ApiResponse<PageUtil.PageResponse<FavoriteResponse>> getFavoriteList(Long userId, FavoriteListRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 构建分页参数
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize());
|
||||
|
||||
// 3. 查询收藏列表
|
||||
Page<Favorite> favoritePage;
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
// 有关键词搜索
|
||||
if ("all".equals(request.getMessageRole())) {
|
||||
favoritePage = favoriteRepository.findByUserIdAndKeywordAndNotDeleted(userId, request.getKeyword(), pageable);
|
||||
} else {
|
||||
favoritePage = favoriteRepository.findByUserIdAndKeywordAndRoleAndNotDeleted(
|
||||
userId, request.getKeyword(), request.getMessageRole(), pageable);
|
||||
}
|
||||
} else {
|
||||
// 无关键词,查询所有收藏
|
||||
favoritePage = favoriteRepository.findByUserIdAndNotDeleted(userId, pageable);
|
||||
}
|
||||
|
||||
// 4. 转换为响应DTO列表
|
||||
List<FavoriteResponse> favoriteResponses = favoritePage.getContent().stream()
|
||||
.map(this::convertToFavoriteResponseWithLookup)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 5. 构建分页响应
|
||||
PageUtil.PageResponse<FavoriteResponse> pageResponse = new PageUtil.PageResponse<>(
|
||||
favoriteResponses,
|
||||
request.getPage(),
|
||||
request.getPageSize(),
|
||||
favoritePage.getTotalElements(),
|
||||
favoritePage.getTotalPages(),
|
||||
favoritePage.isFirst(),
|
||||
favoritePage.isLast(),
|
||||
favoritePage.hasNext(),
|
||||
favoritePage.hasPrevious()
|
||||
);
|
||||
|
||||
return ApiResponse.success(pageResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否被收藏
|
||||
*/
|
||||
public ApiResponse<Boolean> isFavorited(Long userId, String messageId) {
|
||||
boolean isFavorited = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(userId, messageId);
|
||||
return ApiResponse.success(isFavorited);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏数量
|
||||
*/
|
||||
public ApiResponse<Long> getUserFavoriteCount(Long userId) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 统计收藏数量
|
||||
long count = favoriteRepository.countByUserIdAndNotDeleted(userId);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建收藏对象
|
||||
*/
|
||||
private Favorite createFavorite(Long userId, String messageId, String sessionId) {
|
||||
Favorite favorite = new Favorite();
|
||||
favorite.setFavoriteId(UUID.randomUUID().toString());
|
||||
favorite.setUserId(userId);
|
||||
favorite.setMessageId(messageId);
|
||||
favorite.setSessionId(sessionId);
|
||||
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
favorite.setCreatedAt(currentTime);
|
||||
favorite.setUpdatedAt(currentTime);
|
||||
|
||||
return favorite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为收藏响应DTO(使用已有的消息和会话对象)
|
||||
*/
|
||||
private FavoriteResponse convertToFavoriteResponse(Favorite favorite, Message message, Session session) {
|
||||
return FavoriteResponse.builder()
|
||||
.favoriteId(favorite.getFavoriteId())
|
||||
.messageId(favorite.getMessageId())
|
||||
.sessionId(favorite.getSessionId())
|
||||
.sessionTitle(session.getTitle())
|
||||
.messageContent(message.getContent())
|
||||
.messageRole(message.getRole())
|
||||
.messageCreatedAt(TimeUtil.timestampToLocalDateTime(message.getCreatedAt()))
|
||||
.favoritedAt(TimeUtil.timestampToLocalDateTime(favorite.getCreatedAt()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为收藏响应DTO(需要查询消息和会话信息)
|
||||
*/
|
||||
private FavoriteResponse convertToFavoriteResponseWithLookup(Favorite favorite) {
|
||||
// 查询消息信息
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(favorite.getMessageId())
|
||||
.orElse(null);
|
||||
|
||||
// 查询会话信息
|
||||
Session session = sessionRepository.findBySessionIdAndNotDeleted(favorite.getSessionId())
|
||||
.orElse(null);
|
||||
|
||||
return FavoriteResponse.builder()
|
||||
.favoriteId(favorite.getFavoriteId())
|
||||
.messageId(favorite.getMessageId())
|
||||
.sessionId(favorite.getSessionId())
|
||||
.sessionTitle(session != null ? session.getTitle() : "未知会话")
|
||||
.messageContent(message != null ? message.getContent() : "消息已删除")
|
||||
.messageRole(message != null ? message.getRole() : "unknown")
|
||||
.messageCreatedAt(message != null ? TimeUtil.timestampToLocalDateTime(message.getCreatedAt()) : null)
|
||||
.favoritedAt(TimeUtil.timestampToLocalDateTime(favorite.getCreatedAt()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
244
src/main/java/com/lxy/hsend/service/MessageService.java
Normal file
244
src/main/java/com/lxy/hsend/service/MessageService.java
Normal file
@ -0,0 +1,244 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 消息服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MessageService {
|
||||
|
||||
private final MessageRepository messageRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<MessageResponse> sendMessage(Long userId, SendMessageRequest request) {
|
||||
// 1. 验证用户
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证会话
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 3. 内容过滤和验证
|
||||
String filteredContent = filterMessageContent(request.getContent());
|
||||
if (filteredContent.trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "消息内容不能为空");
|
||||
}
|
||||
|
||||
// 4. 创建用户消息
|
||||
Message userMessage = createMessage(
|
||||
userId,
|
||||
request.getSessionId(),
|
||||
"user",
|
||||
filteredContent,
|
||||
request.getDeepThinking(),
|
||||
request.getWebSearch()
|
||||
);
|
||||
|
||||
// 5. 保存用户消息
|
||||
Message savedUserMessage = messageRepository.save(userMessage);
|
||||
log.info("用户消息已保存: userId={}, sessionId={}, messageId={}",
|
||||
userId, request.getSessionId(), savedUserMessage.getMessageId());
|
||||
|
||||
// 6. 更新会话统计
|
||||
updateSessionStatistics(session, savedUserMessage);
|
||||
|
||||
// 7. 预留AI响应生成(暂时返回占位响应)
|
||||
// TODO: 集成AI服务生成响应
|
||||
Message aiMessage = createAIPlaceholderMessage(userId, request.getSessionId(), savedUserMessage);
|
||||
Message savedAiMessage = messageRepository.save(aiMessage);
|
||||
|
||||
// 8. 再次更新会话统计
|
||||
updateSessionStatistics(session, savedAiMessage);
|
||||
|
||||
// 9. 返回AI消息响应
|
||||
MessageResponse response = convertToMessageResponse(savedAiMessage);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
public ApiResponse<PageUtil.PageResponse<MessageResponse>> getMessages(Long userId, MessageListRequest request) {
|
||||
// 1. 验证用户
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证会话权限
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 3. 构建分页参数
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize());
|
||||
|
||||
// 4. 查询消息
|
||||
Page<Message> messagePage;
|
||||
if ("all".equals(request.getRole())) {
|
||||
messagePage = messageRepository.findBySessionIdAndNotDeleted(request.getSessionId(), pageable);
|
||||
} else {
|
||||
// 如果指定了角色过滤,需要添加对应的repository方法
|
||||
messagePage = messageRepository.findBySessionIdAndNotDeleted(request.getSessionId(), pageable);
|
||||
}
|
||||
|
||||
// 5. 转换为响应DTO
|
||||
List<MessageResponse> messageResponses = messagePage.getContent().stream()
|
||||
.filter(message -> "all".equals(request.getRole()) || request.getRole().equals(message.getRole()))
|
||||
.map(this::convertToMessageResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 6. 构建分页响应
|
||||
PageUtil.PageResponse<MessageResponse> pageResponse = new PageUtil.PageResponse<>(
|
||||
messageResponses,
|
||||
request.getPage(),
|
||||
request.getPageSize(),
|
||||
messagePage.getTotalElements(),
|
||||
messagePage.getTotalPages(),
|
||||
messagePage.isFirst(),
|
||||
messagePage.isLast(),
|
||||
messagePage.hasNext(),
|
||||
messagePage.hasPrevious()
|
||||
);
|
||||
|
||||
return ApiResponse.success(pageResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息ID获取消息详情
|
||||
*/
|
||||
public ApiResponse<MessageResponse> getMessageById(Long userId, String messageId) {
|
||||
// 1. 验证消息存在且属于用户
|
||||
if (!messageRepository.existsByMessageIdAndUserIdAndNotDeleted(messageId, userId)) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在");
|
||||
}
|
||||
|
||||
// 2. 获取消息
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(messageId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在"));
|
||||
|
||||
// 3. 转换为响应DTO
|
||||
MessageResponse response = convertToMessageResponse(message);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息对象
|
||||
*/
|
||||
private Message createMessage(Long userId, String sessionId, String role, String content,
|
||||
Boolean deepThinking, Boolean webSearch) {
|
||||
Message message = new Message();
|
||||
message.setMessageId(UUID.randomUUID().toString());
|
||||
message.setSessionId(sessionId);
|
||||
message.setUserId(userId);
|
||||
message.setRole(role);
|
||||
message.setContent(content);
|
||||
// 消息状态通过其他字段表示,无需单独status字段
|
||||
message.setDeepThinking(deepThinking != null ? deepThinking : false);
|
||||
message.setWebSearch(webSearch != null ? webSearch : false);
|
||||
message.setIsFavorited(false);
|
||||
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
message.setTimestamp(currentTime);
|
||||
message.setCreatedAt(currentTime);
|
||||
message.setUpdatedAt(currentTime);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AI占位响应消息
|
||||
*/
|
||||
private Message createAIPlaceholderMessage(Long userId, String sessionId, Message userMessage) {
|
||||
String aiContent = generateAIPlaceholderResponse(userMessage.getContent());
|
||||
|
||||
return createMessage(
|
||||
userId,
|
||||
sessionId,
|
||||
"assistant",
|
||||
aiContent,
|
||||
userMessage.getDeepThinking(),
|
||||
userMessage.getWebSearch()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI占位响应(后续将被实际AI服务替换)
|
||||
*/
|
||||
private String generateAIPlaceholderResponse(String userContent) {
|
||||
return "感谢您的提问。这是一个临时响应,实际的AI服务将在后续版本中集成。您的问题是:" + userContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息内容过滤
|
||||
*/
|
||||
private String filterMessageContent(String content) {
|
||||
if (content == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 基础过滤:去除首尾空格,限制长度
|
||||
String filtered = content.trim();
|
||||
|
||||
// TODO: 添加更多过滤规则(敏感词、恶意内容等)
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话统计
|
||||
*/
|
||||
private void updateSessionStatistics(Session session, Message message) {
|
||||
session.setMessageCount(session.getMessageCount() + 1);
|
||||
session.setLastMessageTime(message.getTimestamp());
|
||||
session.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
sessionRepository.save(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为消息响应DTO
|
||||
*/
|
||||
private MessageResponse convertToMessageResponse(Message message) {
|
||||
return MessageResponse.builder()
|
||||
.messageId(message.getMessageId())
|
||||
.sessionId(message.getSessionId())
|
||||
.role(message.getRole())
|
||||
.content(message.getContent())
|
||||
.status("completed")
|
||||
.deepThinking(message.getDeepThinking())
|
||||
.webSearch(message.getWebSearch())
|
||||
.createdAt(TimeUtil.timestampToLocalDateTime(message.getCreatedAt()))
|
||||
.updatedAt(TimeUtil.timestampToLocalDateTime(message.getUpdatedAt()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
307
src/main/java/com/lxy/hsend/service/SessionService.java
Normal file
307
src/main/java/com/lxy/hsend/service/SessionService.java
Normal file
@ -0,0 +1,307 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.session.*;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会话服务类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class SessionService {
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
|
||||
@Autowired
|
||||
public SessionService(SessionRepository sessionRepository,
|
||||
MessageRepository messageRepository,
|
||||
FavoriteRepository favoriteRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.favoriteRepository = favoriteRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
public SessionDetailResponse createSession(Long userId, CreateSessionRequest request) {
|
||||
log.info("创建会话: userId={}, title={}", userId, request.getTitle());
|
||||
|
||||
// 创建会话实体
|
||||
Session session = new Session();
|
||||
session.setSessionId(UUID.randomUUID().toString());
|
||||
session.setUserId(userId);
|
||||
session.setTitle(request.getTitle());
|
||||
session.setMessageCount(0);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
session.setCreatedAt(now);
|
||||
session.setUpdatedAt(now);
|
||||
|
||||
// 保存会话
|
||||
Session savedSession = sessionRepository.save(session);
|
||||
|
||||
// 如果有初始消息,创建第一条消息
|
||||
if (StringUtil.isNotEmpty(request.getInitialMessage())) {
|
||||
Message initialMessage = new Message();
|
||||
initialMessage.setSessionId(savedSession.getSessionId());
|
||||
initialMessage.setUserId(userId);
|
||||
initialMessage.setContent(request.getInitialMessage());
|
||||
initialMessage.setRole("user");
|
||||
initialMessage.setCreatedAt(now);
|
||||
initialMessage.setUpdatedAt(now);
|
||||
|
||||
messageRepository.save(initialMessage);
|
||||
|
||||
// 更新会话统计
|
||||
updateSessionStatistics(savedSession.getSessionId());
|
||||
}
|
||||
|
||||
log.info("会话创建成功: sessionId={}", savedSession.getSessionId());
|
||||
return convertToSessionDetailResponse(savedSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(支持搜索和分页)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<SessionListResponse> getSessionList(Long userId, SessionSearchRequest request) {
|
||||
log.debug("获取会话列表: userId={}, keyword={}, page={}, pageSize={}",
|
||||
userId, request.getKeyword(), request.getPage(), request.getPageSize());
|
||||
|
||||
// 创建分页对象
|
||||
Sort sort = createSort(request.getValidSortBy(), request.isDescOrder());
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize(), sort);
|
||||
|
||||
Page<Session> sessionPage;
|
||||
|
||||
// 根据是否有关键词选择不同的查询方法
|
||||
if (request.hasKeyword()) {
|
||||
sessionPage = sessionRepository.findByUserIdAndKeywordAndNotDeleted(
|
||||
userId, request.getCleanKeyword(), pageable);
|
||||
} else {
|
||||
sessionPage = sessionRepository.findByUserIdAndNotDeleted(userId, pageable);
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
return sessionPage.map(this::convertToSessionListResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public SessionDetailResponse getSessionDetail(Long userId, String sessionId) {
|
||||
log.debug("获取会话详情: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
Session session = findSessionByIdAndUserId(sessionId, userId);
|
||||
return convertToSessionDetailResponse(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
public SessionDetailResponse updateSession(Long userId, String sessionId, UpdateSessionRequest request) {
|
||||
log.info("更新会话: userId={}, sessionId={}, newTitle={}", userId, sessionId, request.getTitle());
|
||||
|
||||
// 验证会话存在和权限
|
||||
Session session = findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
if (StringUtil.isNotEmpty(request.getTitle()) && !StringUtil.equals(session.getTitle(), request.getTitle())) {
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
sessionRepository.updateTitle(sessionId, request.getTitle(), now);
|
||||
|
||||
// 更新本地对象
|
||||
session.setTitle(request.getTitle());
|
||||
session.setUpdatedAt(now);
|
||||
|
||||
log.info("会话标题更新成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
return convertToSessionDetailResponse(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话(软删除,级联删除消息和收藏)
|
||||
*/
|
||||
public void deleteSession(Long userId, String sessionId) {
|
||||
log.info("删除会话: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
// 验证会话存在和权限
|
||||
findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
|
||||
// 软删除会话
|
||||
sessionRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 软删除关联的消息
|
||||
messageRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 删除收藏记录
|
||||
favoriteRepository.deleteBySessionId(sessionId);
|
||||
|
||||
log.info("会话删除成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话(只删除消息,保留会话)
|
||||
*/
|
||||
public void clearSession(Long userId, String sessionId) {
|
||||
log.info("清空会话: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
// 验证会话存在和权限
|
||||
findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
|
||||
// 软删除关联的消息
|
||||
messageRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 删除收藏记录
|
||||
favoriteRepository.deleteBySessionId(sessionId);
|
||||
|
||||
// 重置会话统计
|
||||
sessionRepository.updateMessageStatistics(sessionId, 0, null, now);
|
||||
|
||||
log.info("会话清空成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话统计信息
|
||||
*/
|
||||
public void updateSessionStatistics(String sessionId) {
|
||||
log.debug("更新会话统计: sessionId={}", sessionId);
|
||||
|
||||
// 获取消息统计
|
||||
long messageCount = messageRepository.countBySessionIdAndNotDeleted(sessionId);
|
||||
|
||||
// 获取最后一条消息的时间
|
||||
Long lastMessageTime = null;
|
||||
if (messageCount > 0) {
|
||||
Page<Message> lastMessagePage = messageRepository.findBySessionIdAndNotDeleted(
|
||||
sessionId, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createdAt")));
|
||||
|
||||
if (!lastMessagePage.isEmpty()) {
|
||||
lastMessageTime = lastMessagePage.getContent().get(0).getCreatedAt();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
sessionRepository.updateMessageStatistics(sessionId, (int) messageCount, lastMessageTime, now);
|
||||
|
||||
log.debug("会话统计更新完成: sessionId={}, messageCount={}, lastMessageTime={}",
|
||||
sessionId, messageCount, lastMessageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证会话权限
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean hasSessionPermission(Long userId, String sessionId) {
|
||||
return sessionRepository.existsBySessionIdAndUserIdAndNotDeleted(sessionId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户的会话并验证权限
|
||||
*/
|
||||
private Session findSessionByIdAndUserId(String sessionId, Long userId) {
|
||||
return sessionRepository.findBySessionIdAndUserIdAndNotDeleted(sessionId, userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在或无访问权限"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建排序对象
|
||||
*/
|
||||
private Sort createSort(String sortBy, boolean isDesc) {
|
||||
Sort.Direction direction = isDesc ? Sort.Direction.DESC : Sort.Direction.ASC;
|
||||
|
||||
switch (sortBy) {
|
||||
case "created_at":
|
||||
return Sort.by(direction, "createdAt");
|
||||
case "updated_at":
|
||||
return Sort.by(direction, "updatedAt");
|
||||
case "last_message_time":
|
||||
default:
|
||||
// 对于 last_message_time,需要处理 NULL 值
|
||||
if (isDesc) {
|
||||
return Sort.by(Sort.Direction.DESC, "lastMessageTime")
|
||||
.and(Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
} else {
|
||||
return Sort.by(Sort.Direction.ASC, "lastMessageTime")
|
||||
.and(Sort.by(Sort.Direction.ASC, "createdAt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为会话列表响应DTO
|
||||
*/
|
||||
private SessionListResponse convertToSessionListResponse(Session session) {
|
||||
SessionListResponse response = new SessionListResponse();
|
||||
response.setSessionId(session.getSessionId());
|
||||
response.setTitle(session.getTitle());
|
||||
response.setMessageCount(session.getMessageCount() != null ? session.getMessageCount() : 0);
|
||||
response.setLastMessageTime(session.getLastMessageTime());
|
||||
response.setCreatedAt(session.getCreatedAt());
|
||||
response.setUpdatedAt(session.getUpdatedAt());
|
||||
|
||||
// 获取最后一条消息的内容摘要
|
||||
if (session.getMessageCount() != null && session.getMessageCount() > 0) {
|
||||
try {
|
||||
Page<Message> lastMessagePage = messageRepository.findBySessionIdAndNotDeleted(
|
||||
session.getSessionId(), PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createdAt")));
|
||||
|
||||
if (!lastMessagePage.isEmpty()) {
|
||||
Message lastMessage = lastMessagePage.getContent().get(0);
|
||||
response.setLastMessageContent(lastMessage.getContent());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取最后消息内容失败: sessionId={}, error={}", session.getSessionId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为会话详情响应DTO
|
||||
*/
|
||||
private SessionDetailResponse convertToSessionDetailResponse(Session session) {
|
||||
SessionDetailResponse response = new SessionDetailResponse();
|
||||
response.setSessionId(session.getSessionId());
|
||||
response.setUserId(session.getUserId());
|
||||
response.setTitle(session.getTitle());
|
||||
response.setMessageCount(session.getMessageCount() != null ? session.getMessageCount() : 0);
|
||||
response.setLastMessageTime(session.getLastMessageTime());
|
||||
response.setCreatedAt(session.getCreatedAt());
|
||||
response.setUpdatedAt(session.getUpdatedAt());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
287
src/main/java/com/lxy/hsend/service/UserService.java
Normal file
287
src/main/java/com/lxy/hsend/service/UserService.java
Normal file
@ -0,0 +1,287 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.LoginResponse;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import com.lxy.hsend.dto.user.UpdateUserRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.CryptoUtil;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
public UserService(UserRepository userRepository, JwtUtil jwtUtil, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public UserInfoResponse register(RegisterRequest request) {
|
||||
log.info("用户注册请求: {}", request.getEmail());
|
||||
|
||||
// 验证确认密码
|
||||
if (!StringUtil.equals(request.getPassword(), request.getConfirmPassword())) {
|
||||
throw BusinessException.of(ErrorCode.VALIDATION_ERROR, "两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 转换邮箱为小写
|
||||
String email = request.getEmail().toLowerCase();
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (userRepository.existsByEmailAndDeletedAtIsNull(email)) {
|
||||
throw BusinessException.of(ErrorCode.USER_ALREADY_EXISTS, "该邮箱已被注册");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash(CryptoUtil.encryptPassword(request.getPassword()));
|
||||
user.setNickname(request.getNickname());
|
||||
user.setAvatarUrl(StringUtil.isEmpty(request.getAvatarUrl()) ? getDefaultAvatarUrl() : request.getAvatarUrl());
|
||||
user.setStatus(Constants.USER_STATUS_NORMAL);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
user.setCreatedAt(now);
|
||||
user.setUpdatedAt(now);
|
||||
|
||||
// 保存用户
|
||||
User savedUser = userRepository.save(user);
|
||||
log.info("用户注册成功: userId={}, email={}", savedUser.getId(), savedUser.getEmail());
|
||||
|
||||
return convertToUserInfoResponse(savedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
String email = request.getEmail().toLowerCase();
|
||||
log.info("用户登录请求: {}", email);
|
||||
|
||||
// 检查登录失败次数
|
||||
checkLoginAttempts(email);
|
||||
|
||||
// 查找用户
|
||||
User user = userRepository.findByEmailAndDeletedAtIsNull(email)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
// 检查用户状态
|
||||
checkUserStatus(user);
|
||||
|
||||
// 验证密码
|
||||
if (!CryptoUtil.verifyPassword(request.getPassword(), user.getPasswordHash())) {
|
||||
// 记录登录失败
|
||||
recordLoginFailure(email);
|
||||
throw BusinessException.of(ErrorCode.PASSWORD_ERROR, "密码错误");
|
||||
}
|
||||
|
||||
// 清除登录失败记录
|
||||
clearLoginFailures(email);
|
||||
|
||||
// 更新最后登录时间
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
user.setLastLoginTime(now);
|
||||
user.setUpdatedAt(now);
|
||||
userRepository.save(user);
|
||||
|
||||
// 生成Token
|
||||
String accessToken = jwtUtil.generateToken(user.getId(), user.getEmail());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getEmail());
|
||||
|
||||
// 缓存用户信息到Redis
|
||||
cacheUserInfo(user);
|
||||
|
||||
log.info("用户登录成功: userId={}, email={}", user.getId(), user.getEmail());
|
||||
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
return new LoginResponse(accessToken, refreshToken, Constants.JWT_EXPIRATION_TIME / 1000, userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
public void logout(Long userId, String token) {
|
||||
log.info("用户登出: userId={}", userId);
|
||||
|
||||
// 将token加入黑名单
|
||||
jwtUtil.blacklistToken(token);
|
||||
|
||||
// 清除Redis中的用户信息缓存
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + userId;
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
log.info("用户登出成功: userId={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public UserInfoResponse getUserById(Long userId) {
|
||||
// 先尝试从Redis获取
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + userId;
|
||||
UserInfoResponse cachedUser = (UserInfoResponse) redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cachedUser != null) {
|
||||
return cachedUser;
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
|
||||
// 缓存到Redis
|
||||
redisTemplate.opsForValue().set(cacheKey, userInfo, Constants.REDIS_USER_INFO_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
public UserInfoResponse updateUser(Long userId, UpdateUserRequest request) {
|
||||
log.info("更新用户信息: userId={}", userId);
|
||||
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
// 更新字段
|
||||
boolean updated = false;
|
||||
if (StringUtil.isNotEmpty(request.getNickname()) && !StringUtil.equals(user.getNickname(), request.getNickname())) {
|
||||
user.setNickname(request.getNickname());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (StringUtil.isNotEmpty(request.getAvatarUrl()) && !StringUtil.equals(user.getAvatarUrl(), request.getAvatarUrl())) {
|
||||
user.setAvatarUrl(request.getAvatarUrl());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
user.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
userRepository.save(user);
|
||||
|
||||
// 更新缓存
|
||||
cacheUserInfo(user);
|
||||
|
||||
log.info("用户信息更新成功: userId={}", userId);
|
||||
}
|
||||
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsByEmail(String email) {
|
||||
return userRepository.existsByEmailAndDeletedAtIsNull(email.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户状态
|
||||
*/
|
||||
private void checkUserStatus(User user) {
|
||||
if (user.getStatus() == Constants.USER_STATUS_LOCKED) {
|
||||
throw BusinessException.of(ErrorCode.FORBIDDEN_ERROR, "账户已被锁定,请联系管理员");
|
||||
}
|
||||
if (user.getStatus() == Constants.USER_STATUS_DELETED) {
|
||||
throw BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录失败次数
|
||||
*/
|
||||
private void checkLoginAttempts(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
|
||||
if (attempts != null && attempts >= 5) {
|
||||
throw BusinessException.of(ErrorCode.RATE_LIMIT_ERROR, "登录失败次数过多,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败
|
||||
*/
|
||||
private void recordLoginFailure(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
|
||||
attempts = (attempts == null) ? 1 : attempts + 1;
|
||||
redisTemplate.opsForValue().set(key, attempts, 15, TimeUnit.MINUTES);
|
||||
|
||||
log.warn("登录失败: email={}, 失败次数={}", email, attempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录失败记录
|
||||
*/
|
||||
private void clearLoginFailures(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户信息到Redis
|
||||
*/
|
||||
private void cacheUserInfo(User user) {
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + user.getId();
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
redisTemplate.opsForValue().set(cacheKey, userInfo, Constants.REDIS_USER_INFO_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认头像URL
|
||||
*/
|
||||
private String getDefaultAvatarUrl() {
|
||||
// 可以配置默认头像列表,随机选择一个
|
||||
return "https://via.placeholder.com/150x150?text=User";
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换User实体为UserInfoResponse
|
||||
*/
|
||||
private UserInfoResponse convertToUserInfoResponse(User user) {
|
||||
UserInfoResponse response = new UserInfoResponse();
|
||||
response.setUserId(user.getId());
|
||||
response.setEmail(user.getEmail());
|
||||
response.setNickname(user.getNickname());
|
||||
response.setAvatarUrl(user.getAvatarUrl());
|
||||
response.setStatus(user.getStatus());
|
||||
response.setLastLoginTime(user.getLastLoginTime());
|
||||
response.setCreatedAt(user.getCreatedAt());
|
||||
response.setUpdatedAt(user.getUpdatedAt());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
246
src/main/java/com/lxy/hsend/util/CryptoUtil.java
Normal file
246
src/main/java/com/lxy/hsend/util/CryptoUtil.java
Normal file
@ -0,0 +1,246 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 加密工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
public class CryptoUtil {
|
||||
|
||||
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
private static final String AES_ALGORITHM = "AES";
|
||||
private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* 使用BCrypt加密密码
|
||||
*/
|
||||
public static String encryptPassword(String plainPassword) {
|
||||
return passwordEncoder.encode(plainPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
public static boolean verifyPassword(String plainPassword, String encryptedPassword) {
|
||||
return passwordEncoder.matches(plainPassword, encryptedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密
|
||||
*/
|
||||
public static String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(messageDigest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("MD5加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256加密
|
||||
*/
|
||||
public static String sha256(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("SHA-256加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-512加密
|
||||
*/
|
||||
public static String sha512(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("SHA-512加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AES密钥
|
||||
*/
|
||||
public static String generateAESKey() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);
|
||||
keyGenerator.init(256);
|
||||
SecretKey secretKey = keyGenerator.generateKey();
|
||||
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("生成AES密钥失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密
|
||||
*/
|
||||
public static String aesEncrypt(String plainText, String key) {
|
||||
try {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(key), AES_ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
log.error("AES加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
*/
|
||||
public static String aesDecrypt(String encryptedText, String key) {
|
||||
try {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(key), AES_ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("AES解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64编码
|
||||
*/
|
||||
public static String base64Encode(String input) {
|
||||
return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64解码
|
||||
*/
|
||||
public static String base64Decode(String encodedInput) {
|
||||
try {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(encodedInput);
|
||||
return new String(decodedBytes, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("Base64解码失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机盐值
|
||||
*/
|
||||
public static String generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
return bytesToHex(salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带盐值的哈希
|
||||
*/
|
||||
public static String hashWithSalt(String input, String salt) {
|
||||
return sha256(input + salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证带盐值的哈希
|
||||
*/
|
||||
public static boolean verifyHashWithSalt(String input, String salt, String hash) {
|
||||
String computedHash = hashWithSalt(input, salt);
|
||||
return StringUtil.equals(computedHash, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
result.append(String.format("%02x", b));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 十六进制字符串转字节数组
|
||||
*/
|
||||
public static byte[] hexToBytes(String hex) {
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||||
+ Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成安全的随机数
|
||||
*/
|
||||
public static String generateSecureRandomString(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(chars.charAt(random.nextInt(chars.length())));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的异或加密/解密
|
||||
*/
|
||||
public static String xorEncrypt(String input, String key) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
result.append((char) (input.charAt(i) ^ key.charAt(i % key.length())));
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(result.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 异或解密
|
||||
*/
|
||||
public static String xorDecrypt(String encryptedInput, String key) {
|
||||
try {
|
||||
String decoded = new String(Base64.getDecoder().decode(encryptedInput), StandardCharsets.UTF_8);
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < decoded.length(); i++) {
|
||||
result.append((char) (decoded.charAt(i) ^ key.charAt(i % key.length())));
|
||||
}
|
||||
return result.toString();
|
||||
} catch (Exception e) {
|
||||
log.error("异或解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private CryptoUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
237
src/main/java/com/lxy/hsend/util/JwtUtil.java
Normal file
237
src/main/java/com/lxy/hsend/util/JwtUtil.java
Normal file
@ -0,0 +1,237 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
public JwtUtil(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.secretKey = Keys.hmacShaKeyFor(Constants.JWT_SECRET_KEY.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JWT Token
|
||||
*/
|
||||
public String generateToken(Long userId, String email) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("email", email);
|
||||
claims.put("type", "access");
|
||||
|
||||
return createToken(claims, userId.toString(), Constants.JWT_EXPIRATION_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新Token
|
||||
*/
|
||||
public String generateRefreshToken(Long userId, String email) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("email", email);
|
||||
claims.put("type", "refresh");
|
||||
|
||||
return createToken(claims, userId.toString(), Constants.JWT_REFRESH_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Token
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims, String subject, long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
// 检查是否在黑名单中
|
||||
if (isTokenBlacklisted(token)) {
|
||||
log.warn("Token已被加入黑名单: {}", token);
|
||||
return false;
|
||||
}
|
||||
|
||||
Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
return true;
|
||||
} catch (SecurityException e) {
|
||||
log.error("JWT签名无效: {}", e.getMessage());
|
||||
} catch (MalformedJwtException e) {
|
||||
log.error("JWT格式错误: {}", e.getMessage());
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.warn("JWT已过期: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.error("不支持的JWT: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("JWT为空: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("userId", Long.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取邮箱
|
||||
*/
|
||||
public String getEmailFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("email", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Token类型
|
||||
*/
|
||||
public String getTokenTypeFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("type", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getExpiration() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否过期
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration != null && expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Claims
|
||||
*/
|
||||
private Claims getClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
} catch (Exception e) {
|
||||
log.error("解析Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Token加入黑名单
|
||||
*/
|
||||
public void blacklistToken(String token) {
|
||||
try {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
if (expiration != null) {
|
||||
String key = Constants.REDIS_JWT_BLACKLIST_PREFIX + token;
|
||||
long ttl = expiration.getTime() - System.currentTimeMillis();
|
||||
if (ttl > 0) {
|
||||
redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS);
|
||||
log.info("Token已加入黑名单: {}", token);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("将Token加入黑名单失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否在黑名单中
|
||||
*/
|
||||
public boolean isTokenBlacklisted(String token) {
|
||||
try {
|
||||
String key = Constants.REDIS_JWT_BLACKLIST_PREFIX + token;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
} catch (Exception e) {
|
||||
log.error("检查Token黑名单状态失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
public String refreshToken(String refreshToken) {
|
||||
if (!validateToken(refreshToken)) {
|
||||
throw new IllegalArgumentException("刷新Token无效");
|
||||
}
|
||||
|
||||
String tokenType = getTokenTypeFromToken(refreshToken);
|
||||
if (!"refresh".equals(tokenType)) {
|
||||
throw new IllegalArgumentException("Token类型错误");
|
||||
}
|
||||
|
||||
Long userId = getUserIdFromToken(refreshToken);
|
||||
String email = getEmailFromToken(refreshToken);
|
||||
|
||||
if (userId == null || email == null) {
|
||||
throw new IllegalArgumentException("Token信息不完整");
|
||||
}
|
||||
|
||||
return generateToken(userId, email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取Token
|
||||
*/
|
||||
public String extractTokenFromHeader(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith(Constants.JWT_TOKEN_PREFIX)) {
|
||||
return authHeader.substring(Constants.JWT_TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token剩余有效时间(秒)
|
||||
*/
|
||||
public long getTokenRemainingTime(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
if (expiration != null) {
|
||||
long remaining = expiration.getTime() - System.currentTimeMillis();
|
||||
return Math.max(0, remaining / 1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
243
src/main/java/com/lxy/hsend/util/PageUtil.java
Normal file
243
src/main/java/com/lxy/hsend/util/PageUtil.java
Normal file
@ -0,0 +1,243 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class PageUtil {
|
||||
|
||||
/**
|
||||
* 创建分页对象
|
||||
*/
|
||||
public static Pageable createPageable(Integer page, Integer size) {
|
||||
return createPageable(page, size, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageable(Integer page, Integer size, Sort sort) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
|
||||
if (sort != null) {
|
||||
return PageRequest.of(page - 1, size, sort);
|
||||
} else {
|
||||
return PageRequest.of(page - 1, size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建降序排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageableDesc(Integer page, Integer size, String... properties) {
|
||||
Sort sort = Sort.by(Sort.Direction.DESC, properties);
|
||||
return createPageable(page, size, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建升序排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageableAsc(Integer page, Integer size, String... properties) {
|
||||
Sort sort = Sort.by(Sort.Direction.ASC, properties);
|
||||
return createPageable(page, size, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证页码
|
||||
*/
|
||||
private static Integer validatePage(Integer page) {
|
||||
if (page == null || page < 1) {
|
||||
return Constants.DEFAULT_PAGE_NUM;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证页面大小
|
||||
*/
|
||||
private static Integer validateSize(Integer size) {
|
||||
if (size == null || size < 1) {
|
||||
return Constants.DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
if (size > Constants.MAX_PAGE_SIZE) {
|
||||
return Constants.MAX_PAGE_SIZE;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Spring Data的Page对象转换为自定义的分页响应对象
|
||||
*/
|
||||
public static <T> PageResponse<T> toPageResponse(Page<T> page) {
|
||||
return new PageResponse<T>(
|
||||
page.getContent(),
|
||||
page.getNumber() + 1, // Spring Data的页码从0开始,转换为从1开始
|
||||
page.getSize(),
|
||||
page.getTotalElements(),
|
||||
page.getTotalPages(),
|
||||
page.isFirst(),
|
||||
page.isLast(),
|
||||
page.hasNext(),
|
||||
page.hasPrevious()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空的分页响应
|
||||
*/
|
||||
public static <T> PageResponse<T> emptyPageResponse(Integer page, Integer size) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
return new PageResponse<T>(
|
||||
List.of(),
|
||||
page,
|
||||
size,
|
||||
0L,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PageResponse<T> {
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<T> content;
|
||||
|
||||
/**
|
||||
* 当前页码(从1开始)
|
||||
*/
|
||||
private Integer page;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
private Integer size;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
private Integer totalPages;
|
||||
|
||||
/**
|
||||
* 是否是第一页
|
||||
*/
|
||||
private Boolean first;
|
||||
|
||||
/**
|
||||
* 是否是最后一页
|
||||
*/
|
||||
private Boolean last;
|
||||
|
||||
/**
|
||||
* 是否有下一页
|
||||
*/
|
||||
private Boolean hasNext;
|
||||
|
||||
/**
|
||||
* 是否有上一页
|
||||
*/
|
||||
private Boolean hasPrevious;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询参数
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PageQuery {
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
private Integer page = Constants.DEFAULT_PAGE_NUM;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
private Integer size = Constants.DEFAULT_PAGE_SIZE;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String sortBy;
|
||||
|
||||
/**
|
||||
* 排序方向(asc/desc)
|
||||
*/
|
||||
private String sortDir = "desc";
|
||||
|
||||
/**
|
||||
* 获取Pageable对象
|
||||
*/
|
||||
public Pageable toPageable() {
|
||||
if (StringUtil.isNotEmpty(sortBy)) {
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) ?
|
||||
Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
Sort sort = Sort.by(direction, sortBy);
|
||||
return PageUtil.createPageable(page, size, sort);
|
||||
} else {
|
||||
return PageUtil.createPageable(page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算偏移量
|
||||
*/
|
||||
public static long calculateOffset(Integer page, Integer size) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
return (long) (page - 1) * size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算总页数
|
||||
*/
|
||||
public static int calculateTotalPages(long totalElements, int size) {
|
||||
return (int) Math.ceil((double) totalElements / size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页码是否有效
|
||||
*/
|
||||
public static boolean isValidPage(Integer page, long totalElements, Integer size) {
|
||||
if (page == null || page < 1) {
|
||||
return false;
|
||||
}
|
||||
size = validateSize(size);
|
||||
int totalPages = calculateTotalPages(totalElements, size);
|
||||
return page <= totalPages;
|
||||
}
|
||||
|
||||
private PageUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
276
src/main/java/com/lxy/hsend/util/StringUtil.java
Normal file
276
src/main/java/com/lxy/hsend/util/StringUtil.java
Normal file
@ -0,0 +1,276 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class StringUtil {
|
||||
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(Constants.EMAIL_REGEX);
|
||||
private static final Pattern PASSWORD_PATTERN = Pattern.compile(Constants.PASSWORD_REGEX);
|
||||
|
||||
/**
|
||||
* 检查字符串是否为空或null
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
return str == null || str.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符串是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return !isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的字符串比较(避免空指针异常)
|
||||
*/
|
||||
public static boolean equals(String str1, String str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
return str1.equals(str2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的字符串比较(忽略大小写)
|
||||
*/
|
||||
public static boolean equalsIgnoreCase(String str1, String str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
return str1.equalsIgnoreCase(str2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID字符串(去掉连字符)
|
||||
*/
|
||||
public static String generateUuid() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成标准UUID字符串(带连字符)
|
||||
*/
|
||||
public static String generateStandardUuid() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
return isNotEmpty(email) && EMAIL_PATTERN.matcher(email).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码格式
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
return isNotEmpty(password) && PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名长度
|
||||
*/
|
||||
public static boolean isValidNickname(String nickname) {
|
||||
return isNotEmpty(nickname) &&
|
||||
nickname.length() >= Constants.NICKNAME_MIN_LENGTH &&
|
||||
nickname.length() <= Constants.NICKNAME_MAX_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 掩码邮箱地址(隐藏部分字符)
|
||||
*/
|
||||
public static String maskEmail(String email) {
|
||||
if (isEmpty(email) || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String[] parts = email.split("@");
|
||||
String username = parts[0];
|
||||
String domain = parts[1];
|
||||
|
||||
if (username.length() <= 2) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String maskedUsername = username.charAt(0) +
|
||||
"*".repeat(username.length() - 2) +
|
||||
username.charAt(username.length() - 1);
|
||||
|
||||
return maskedUsername + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 掩码手机号码
|
||||
*/
|
||||
public static String maskPhoneNumber(String phoneNumber) {
|
||||
if (isEmpty(phoneNumber) || phoneNumber.length() < 7) {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
return phoneNumber.substring(0, 3) +
|
||||
"*".repeat(phoneNumber.length() - 6) +
|
||||
phoneNumber.substring(phoneNumber.length() - 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断字符串到指定长度
|
||||
*/
|
||||
public static String truncate(String str, int maxLength) {
|
||||
if (isEmpty(str) || str.length() <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串中的HTML标签
|
||||
*/
|
||||
public static String removeHtmlTags(String html) {
|
||||
if (isEmpty(html)) {
|
||||
return html;
|
||||
}
|
||||
return html.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义HTML特殊字符
|
||||
*/
|
||||
public static String escapeHtml(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
return str.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转换为驼峰命名
|
||||
*/
|
||||
public static String toCamelCase(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = false;
|
||||
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c == '_' || c == '-' || c == ' ') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result.append(Character.toLowerCase(c));
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将驼峰命名转换为下划线命名
|
||||
*/
|
||||
public static String toSnakeCase(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
result.append('_');
|
||||
}
|
||||
result.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
public static String generateRandomString(int length) {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.append(chars.charAt((int) (Math.random() * chars.length())));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数字字符串
|
||||
*/
|
||||
public static String generateRandomNumbers(int length) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.append((int) (Math.random() * 10));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计字符串中的字数(中文按1个字计算,英文按0.5个字计算)
|
||||
*/
|
||||
public static double countWords(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double count = 0;
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c >= 0x4e00 && c <= 0x9fa5) {
|
||||
// 中文字符
|
||||
count += 1;
|
||||
} else if (Character.isLetter(c)) {
|
||||
// 英文字符
|
||||
count += 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串的字节长度(UTF-8编码)
|
||||
*/
|
||||
public static int getByteLength(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return 0;
|
||||
}
|
||||
return str.getBytes().length;
|
||||
}
|
||||
|
||||
private StringUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
220
src/main/java/com/lxy/hsend/util/TimeUtil.java
Normal file
220
src/main/java/com/lxy/hsend/util/TimeUtil.java
Normal file
@ -0,0 +1,220 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 时间工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class TimeUtil {
|
||||
|
||||
private static final ZoneId ZONE_ID = ZoneId.of(Constants.TIMEZONE);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(Constants.DATE_FORMAT);
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(Constants.DATETIME_FORMAT);
|
||||
|
||||
/**
|
||||
* 获取当前时间戳(毫秒)
|
||||
*/
|
||||
public static long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳(秒)
|
||||
*/
|
||||
public static long getCurrentTimestampSeconds() {
|
||||
return System.currentTimeMillis() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期时间
|
||||
*/
|
||||
public static LocalDateTime getCurrentDateTime() {
|
||||
return LocalDateTime.now(ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期
|
||||
*/
|
||||
public static LocalDate getCurrentDate() {
|
||||
return LocalDate.now(ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime timestampToLocalDateTime(long timestamp) {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime转时间戳
|
||||
*/
|
||||
public static long localDateTimeToTimestamp(LocalDateTime dateTime) {
|
||||
return dateTime.atZone(ZONE_ID).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转Date
|
||||
*/
|
||||
public static Date timestampToDate(long timestamp) {
|
||||
return new Date(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Date转时间戳
|
||||
*/
|
||||
public static long dateToTimestamp(Date date) {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
public static String formatDateTime(LocalDateTime dateTime) {
|
||||
return dateTime.format(DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
public static String formatDate(LocalDate date) {
|
||||
return date.format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳为日期时间字符串
|
||||
*/
|
||||
public static String formatTimestamp(long timestamp) {
|
||||
return formatDateTime(timestampToLocalDateTime(timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间字符串
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeStr) {
|
||||
return LocalDateTime.parse(dateTimeStr, DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
public static LocalDate parseDate(String dateStr) {
|
||||
return LocalDate.parse(dateStr, DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的天数差
|
||||
*/
|
||||
public static long daysBetween(long timestamp1, long timestamp2) {
|
||||
LocalDate date1 = timestampToLocalDateTime(timestamp1).toLocalDate();
|
||||
LocalDate date2 = timestampToLocalDateTime(timestamp2).toLocalDate();
|
||||
return Duration.between(date1.atStartOfDay(), date2.atStartOfDay()).toDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的小时差
|
||||
*/
|
||||
public static long hoursBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / (1000 * 60 * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的分钟差
|
||||
*/
|
||||
public static long minutesBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / (1000 * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的秒数差
|
||||
*/
|
||||
public static long secondsBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天开始时间戳
|
||||
*/
|
||||
public static long getTodayStartTimestamp() {
|
||||
LocalDateTime startOfDay = getCurrentDate().atStartOfDay();
|
||||
return localDateTimeToTimestamp(startOfDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天结束时间戳
|
||||
*/
|
||||
public static long getTodayEndTimestamp() {
|
||||
LocalDateTime endOfDay = getCurrentDate().atTime(23, 59, 59, 999_000_000);
|
||||
return localDateTimeToTimestamp(endOfDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定天数前的时间戳
|
||||
*/
|
||||
public static long getDaysAgoTimestamp(int days) {
|
||||
LocalDateTime daysAgo = getCurrentDateTime().minusDays(days);
|
||||
return localDateTimeToTimestamp(daysAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定小时前的时间戳
|
||||
*/
|
||||
public static long getHoursAgoTimestamp(int hours) {
|
||||
LocalDateTime hoursAgo = getCurrentDateTime().minusHours(hours);
|
||||
return localDateTimeToTimestamp(hoursAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分钟前的时间戳
|
||||
*/
|
||||
public static long getMinutesAgoTimestamp(int minutes) {
|
||||
LocalDateTime minutesAgo = getCurrentDateTime().minusMinutes(minutes);
|
||||
return localDateTimeToTimestamp(minutesAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间戳是否是今天
|
||||
*/
|
||||
public static boolean isToday(long timestamp) {
|
||||
LocalDate date = timestampToLocalDateTime(timestamp).toLocalDate();
|
||||
return date.equals(getCurrentDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间戳是否是昨天
|
||||
*/
|
||||
public static boolean isYesterday(long timestamp) {
|
||||
LocalDate date = timestampToLocalDateTime(timestamp).toLocalDate();
|
||||
return date.equals(getCurrentDate().minusDays(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取友好的时间显示(如:刚刚、5分钟前、1小时前等)
|
||||
*/
|
||||
public static String getFriendlyTime(long timestamp) {
|
||||
long now = getCurrentTimestamp();
|
||||
long diff = now - timestamp;
|
||||
|
||||
if (diff < 60 * 1000) {
|
||||
return "刚刚";
|
||||
} else if (diff < 60 * 60 * 1000) {
|
||||
return (diff / (60 * 1000)) + "分钟前";
|
||||
} else if (diff < 24 * 60 * 60 * 1000) {
|
||||
return (diff / (60 * 60 * 1000)) + "小时前";
|
||||
} else if (diff < 30L * 24 * 60 * 60 * 1000) {
|
||||
return (diff / (24 * 60 * 60 * 1000)) + "天前";
|
||||
} else {
|
||||
return formatTimestamp(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private TimeUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
61
src/main/resources/application-dev.yml
Normal file
61
src/main/resources/application-dev.yml
Normal file
@ -0,0 +1,61 @@
|
||||
# 开发环境配置
|
||||
spring:
|
||||
# 数据库配置
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/hsai?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: true
|
||||
|
||||
# Flyway配置
|
||||
flyway:
|
||||
enabled: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: debug
|
||||
org.springframework.security: debug
|
||||
org.hibernate.SQL: debug
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: trace
|
||||
|
||||
# JWT配置(开发环境使用简单密钥)
|
||||
jwt:
|
||||
secret: hsendDevSecretKey2024
|
||||
expiration: 86400 # 1天
|
||||
|
||||
# AI服务配置(开发环境)
|
||||
ai:
|
||||
service:
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
base-url: https://api-dev.example.com
|
||||
api-key: dev-api-key
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads/dev
|
||||
|
||||
# 跨域配置(开发环境允许所有来源)
|
||||
cors:
|
||||
allowed-origins: "*"
|
||||
62
src/main/resources/application-prod.yml
Normal file
62
src/main/resources/application-prod.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# 生产环境配置
|
||||
spring:
|
||||
# 数据库配置(生产环境从环境变量读取)
|
||||
datasource:
|
||||
url: ${DB_URL}
|
||||
username: ${DB_USERNAME}
|
||||
password: ${DB_PASSWORD}
|
||||
hikari:
|
||||
maximum-pool-size: 50
|
||||
minimum-idle: 10
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 900000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD}
|
||||
database: 0
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: warn
|
||||
org.hibernate.SQL: warn
|
||||
file:
|
||||
name: /var/log/hs-end/hs-end.log
|
||||
|
||||
# JWT配置(生产环境必须使用环境变量)
|
||||
jwt:
|
||||
secret: ${JWT_SECRET}
|
||||
expiration: ${JWT_EXPIRATION:604800}
|
||||
|
||||
# AI服务配置
|
||||
ai:
|
||||
service:
|
||||
timeout: ${AI_TIMEOUT:30000}
|
||||
retry-count: ${AI_RETRY:3}
|
||||
base-url: ${AI_SERVICE_URL}
|
||||
api-key: ${AI_API_KEY}
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ${FILE_UPLOAD_PATH:/data/uploads}
|
||||
|
||||
# 跨域配置(生产环境严格控制)
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS}
|
||||
62
src/main/resources/application-test.yml
Normal file
62
src/main/resources/application-test.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# 测试环境配置
|
||||
spring:
|
||||
# 数据库配置 - 使用H2内存数据库进行测试
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
format_sql: true
|
||||
|
||||
# H2控制台配置
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
# Flyway配置 - 测试环境禁用
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 1
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: info
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: hsendTestSecretKey2024
|
||||
expiration: 3600 # 1小时
|
||||
|
||||
# AI服务配置(测试环境使用Mock)
|
||||
ai:
|
||||
service:
|
||||
timeout: 10000
|
||||
retry-count: 1
|
||||
base-url: http://localhost:8082/mock
|
||||
api-key: test-api-key
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads/test
|
||||
134
src/main/resources/application.yml
Normal file
134
src/main/resources/application.yml
Normal file
@ -0,0 +1,134 @@
|
||||
spring:
|
||||
application:
|
||||
name: hs-end
|
||||
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# 数据库配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${DB_URL:jdbc:mysql://localhost:3306/hsai?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true}
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:root}
|
||||
|
||||
# 连接池配置
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
|
||||
# Flyway数据库迁移配置
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 0
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-wait: -1ms
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
# Jackson配置
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: Asia/Shanghai
|
||||
default-property-inclusion: non_null
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
enabled: true
|
||||
force: true
|
||||
|
||||
# Actuator健康检查配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: debug
|
||||
org.hibernate.SQL: debug
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: trace
|
||||
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/hs-end.log
|
||||
max-size: 10MB
|
||||
max-history: 30
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:hsendSecretKeyForJWTTokenGenerationAndValidation2024}
|
||||
expiration: 604800 # 7天(秒)
|
||||
|
||||
# AI服务配置
|
||||
ai:
|
||||
service:
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
base-url: ${AI_SERVICE_URL:https://api.example.com}
|
||||
api-key: ${AI_API_KEY:your-api-key}
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ${FILE_UPLOAD_PATH:./uploads}
|
||||
allowed-types: .txt,.pdf,.md,.docx,.doc,.xlsx,.xls,.png,.jpg,.jpeg
|
||||
max-size: 10485760 # 10MB
|
||||
|
||||
# Swagger配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
|
||||
# 跨域配置
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS:http://localhost:3000,http://localhost:8080}
|
||||
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
166
src/main/resources/db/migration/V1__Create_initial_tables.sql
Normal file
166
src/main/resources/db/migration/V1__Create_initial_tables.sql
Normal file
@ -0,0 +1,166 @@
|
||||
-- 创建数据库初始表结构
|
||||
-- 版本: V1
|
||||
-- 描述: 创建用户、会话、消息、收藏、文件等核心业务表
|
||||
|
||||
-- 设置字符集
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `users` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`email` VARCHAR(100) NOT NULL COMMENT '用户邮箱',
|
||||
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希',
|
||||
`nickname` VARCHAR(50) NOT NULL COMMENT '用户昵称',
|
||||
`avatar_url` VARCHAR(500) DEFAULT NULL COMMENT '头像URL',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '用户状态:1-正常,2-锁定,3-删除',
|
||||
`last_login_time` BIGINT DEFAULT NULL COMMENT '最后登录时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 会话表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `sessions` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '会话自增ID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '会话标题',
|
||||
`message_count` INT DEFAULT 0 COMMENT '消息数量',
|
||||
`last_message_time` BIGINT DEFAULT NULL COMMENT '最后消息时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_session_id` (`session_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_last_message_time` (`last_message_time`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天会话表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 消息表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `messages` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '消息自增ID',
|
||||
`message_id` VARCHAR(36) NOT NULL COMMENT '消息UUID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话ID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`role` VARCHAR(20) NOT NULL COMMENT '角色:user/assistant',
|
||||
`content` LONGTEXT NOT NULL COMMENT '消息内容',
|
||||
`model_used` VARCHAR(50) DEFAULT NULL COMMENT '使用的AI模型',
|
||||
`deep_thinking` BOOLEAN DEFAULT FALSE COMMENT '是否启用深度思考',
|
||||
`web_search` BOOLEAN DEFAULT FALSE COMMENT '是否启用联网搜索',
|
||||
`is_favorited` BOOLEAN DEFAULT FALSE COMMENT '是否已收藏',
|
||||
`timestamp` BIGINT NOT NULL COMMENT '消息时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_message_id` (`message_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_role` (`role`),
|
||||
KEY `idx_timestamp` (`timestamp`),
|
||||
KEY `idx_is_favorited` (`is_favorited`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_messages_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 收藏表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `favorites` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '收藏自增ID',
|
||||
`favorite_id` VARCHAR(36) NOT NULL COMMENT '收藏UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`message_id` VARCHAR(36) NOT NULL COMMENT '消息ID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话ID',
|
||||
`created_at` BIGINT NOT NULL COMMENT '收藏时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_favorite_id` (`favorite_id`),
|
||||
UNIQUE KEY `uk_user_message` (`user_id`, `message_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_message_id` (`message_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_favorites_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收藏表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 文件表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `files` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '文件自增ID',
|
||||
`file_id` VARCHAR(36) NOT NULL COMMENT '文件UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`session_id` VARCHAR(36) DEFAULT NULL COMMENT '关联会话ID',
|
||||
`filename` VARCHAR(255) NOT NULL COMMENT '原始文件名',
|
||||
`stored_filename` VARCHAR(255) NOT NULL COMMENT '存储文件名',
|
||||
`file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
|
||||
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
|
||||
`file_type` VARCHAR(100) NOT NULL COMMENT '文件类型',
|
||||
`mime_type` VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型',
|
||||
`upload_time` BIGINT NOT NULL COMMENT '上传时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_file_id` (`file_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
KEY `idx_upload_time` (`upload_time`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_files_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 操作日志表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `operation_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '操作用户ID',
|
||||
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
`operation_desc` VARCHAR(500) DEFAULT NULL COMMENT '操作描述',
|
||||
`ip_address` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
|
||||
`user_agent` VARCHAR(500) DEFAULT NULL COMMENT '用户代理',
|
||||
`request_params` TEXT DEFAULT NULL COMMENT '请求参数',
|
||||
`response_result` TEXT DEFAULT NULL COMMENT '响应结果',
|
||||
`execution_time` INT DEFAULT NULL COMMENT '执行时长(毫秒)',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_operation_type` (`operation_type`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 系统配置表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `system_configs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '配置ID',
|
||||
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||
`config_value` TEXT DEFAULT NULL COMMENT '配置值',
|
||||
`config_desc` VARCHAR(500) DEFAULT NULL COMMENT '配置描述',
|
||||
`config_type` VARCHAR(20) DEFAULT 'STRING' COMMENT '配置类型:STRING/NUMBER/BOOLEAN/JSON',
|
||||
`is_encrypted` BOOLEAN DEFAULT FALSE COMMENT '是否加密存储',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_config_key` (`config_key`),
|
||||
KEY `idx_config_type` (`config_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
13
src/test/java/com/lxy/hsend/HsEndApplicationTests.java
Normal file
13
src/test/java/com/lxy/hsend/HsEndApplicationTests.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.lxy.hsend;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class HsEndApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
105
src/test/java/com/lxy/hsend/controller/AuthControllerTest.java
Normal file
105
src/test/java/com/lxy/hsend/controller/AuthControllerTest.java
Normal file
@ -0,0 +1,105 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* 认证控制器集成测试
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class AuthControllerTest {
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext webApplicationContext;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
public void testUserRegistrationFlow() throws Exception {
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
|
||||
|
||||
// 1. 测试用户注册
|
||||
RegisterRequest registerRequest = new RegisterRequest();
|
||||
registerRequest.setEmail("test@example.com");
|
||||
registerRequest.setPassword("Test123456");
|
||||
registerRequest.setConfirmPassword("Test123456");
|
||||
registerRequest.setNickname("测试用户");
|
||||
|
||||
String registerJson = objectMapper.writeValueAsString(registerRequest);
|
||||
|
||||
mockMvc.perform(post("/api/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(registerJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body.email").value("test@example.com"))
|
||||
.andExpect(jsonPath("$.body.nickname").value("测试用户"))
|
||||
.andExpect(jsonPath("$.message").value("注册成功"));
|
||||
|
||||
System.out.println("✅ 用户注册测试通过");
|
||||
|
||||
// 2. 测试用户登录
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setEmail("test@example.com");
|
||||
loginRequest.setPassword("Test123456");
|
||||
|
||||
String loginJson = objectMapper.writeValueAsString(loginRequest);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(loginJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body.accessToken").exists())
|
||||
.andExpect(jsonPath("$.body.refreshToken").exists())
|
||||
.andExpect(jsonPath("$.body.userInfo.email").value("test@example.com"))
|
||||
.andExpect(jsonPath("$.message").value("登录成功"));
|
||||
|
||||
System.out.println("✅ 用户登录测试通过");
|
||||
|
||||
// 3. 测试邮箱重复注册
|
||||
mockMvc.perform(post("/api/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(registerJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error").value(2003));
|
||||
|
||||
System.out.println("✅ 邮箱重复注册验证测试通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmailCheck() throws Exception {
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
|
||||
|
||||
// 测试邮箱是否已存在检查
|
||||
mockMvc.perform(get("/api/auth/check-email")
|
||||
.param("email", "nonexistent@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body").value(false))
|
||||
.andExpect(jsonPath("$.message").value("邮箱可用"));
|
||||
|
||||
System.out.println("✅ 邮箱检查测试通过");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.User;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 数据库连接和表创建测试
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
public class DatabaseConnectionTest {
|
||||
|
||||
@Autowired
|
||||
private TestEntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
public void testDatabaseConnection() throws Exception {
|
||||
// 测试数据库连接
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
assertThat(connection).isNotNull();
|
||||
assertThat(connection.isValid(1)).isTrue();
|
||||
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
System.out.println("Database Product Name: " + metaData.getDatabaseProductName());
|
||||
System.out.println("Database Product Version: " + metaData.getDatabaseProductVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTablesExist() throws Exception {
|
||||
// 测试核心表是否存在
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
|
||||
// 检查用户表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "users", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ users table exists");
|
||||
}
|
||||
|
||||
// 检查会话表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "sessions", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ sessions table exists");
|
||||
}
|
||||
|
||||
// 检查消息表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "messages", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ messages table exists");
|
||||
}
|
||||
|
||||
// 检查收藏表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "favorites", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ favorites table exists");
|
||||
}
|
||||
|
||||
// 检查文件表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "files", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ files table exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserRepositoryBasicOperations() {
|
||||
// 测试基本的CRUD操作
|
||||
|
||||
// 创建测试用户
|
||||
User user = new User();
|
||||
user.setEmail("test@example.com");
|
||||
user.setPasswordHash("hashedPassword");
|
||||
user.setNickname("TestUser");
|
||||
user.setStatus((byte) 1);
|
||||
|
||||
// 保存用户
|
||||
User savedUser = userRepository.save(user);
|
||||
assertThat(savedUser.getId()).isNotNull();
|
||||
assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
|
||||
System.out.println("✓ User saved successfully: " + savedUser);
|
||||
|
||||
// 通过邮箱查找用户
|
||||
var foundUser = userRepository.findByEmail("test@example.com");
|
||||
assertThat(foundUser).isPresent();
|
||||
assertThat(foundUser.get().getNickname()).isEqualTo("TestUser");
|
||||
System.out.println("✓ User found by email: " + foundUser.get());
|
||||
|
||||
// 检查邮箱是否存在
|
||||
boolean exists = userRepository.existsByEmail("test@example.com");
|
||||
assertThat(exists).isTrue();
|
||||
System.out.println("✓ Email exists check passed");
|
||||
|
||||
// 统计用户数量
|
||||
long count = userRepository.count();
|
||||
assertThat(count).isGreaterThan(0);
|
||||
System.out.println("✓ User count: " + count);
|
||||
|
||||
// 删除用户
|
||||
userRepository.delete(savedUser);
|
||||
|
||||
// 验证删除
|
||||
var deletedUser = userRepository.findById(savedUser.getId());
|
||||
assertThat(deletedUser).isEmpty();
|
||||
System.out.println("✓ User deleted successfully");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserValidation() {
|
||||
// 测试用户实体验证
|
||||
User user = new User();
|
||||
user.setEmail("invalid-email"); // 无效邮箱格式
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("Test");
|
||||
|
||||
try {
|
||||
entityManager.persistAndFlush(user);
|
||||
// 如果没有抛出异常,说明验证可能有问题
|
||||
} catch (Exception e) {
|
||||
// 预期会抛出验证异常
|
||||
System.out.println("✓ Email validation working: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimestampFields() {
|
||||
// 测试时间戳字段
|
||||
User user = new User();
|
||||
user.setEmail("timestamp@test.com");
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("TimestampTest");
|
||||
|
||||
long beforeSave = Instant.now().getEpochSecond();
|
||||
User savedUser = userRepository.save(user);
|
||||
long afterSave = Instant.now().getEpochSecond();
|
||||
|
||||
// 验证创建时间和更新时间
|
||||
assertThat(savedUser.getCreatedAt()).isNotNull();
|
||||
assertThat(savedUser.getUpdatedAt()).isNotNull();
|
||||
assertThat(savedUser.getCreatedAt()).isBetween(beforeSave - 1, afterSave + 1);
|
||||
assertThat(savedUser.getUpdatedAt()).isBetween(beforeSave - 1, afterSave + 1);
|
||||
|
||||
System.out.println("✓ Timestamp fields working correctly");
|
||||
System.out.println(" Created at: " + savedUser.getCreatedAt());
|
||||
System.out.println(" Updated at: " + savedUser.getUpdatedAt());
|
||||
|
||||
// 清理
|
||||
userRepository.delete(savedUser);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSoftDelete() {
|
||||
// 测试软删除功能
|
||||
User user = new User();
|
||||
user.setEmail("softdelete@test.com");
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("SoftDeleteTest");
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
Long userId = savedUser.getId();
|
||||
|
||||
// 执行软删除
|
||||
savedUser.markAsDeleted();
|
||||
userRepository.save(savedUser);
|
||||
|
||||
// 验证软删除
|
||||
User deletedUser = userRepository.findById(userId).orElse(null);
|
||||
assertThat(deletedUser).isNotNull();
|
||||
assertThat(deletedUser.isDeleted()).isTrue();
|
||||
assertThat(deletedUser.getDeletedAt()).isNotNull();
|
||||
|
||||
System.out.println("✓ Soft delete working correctly");
|
||||
System.out.println(" Deleted at: " + deletedUser.getDeletedAt());
|
||||
|
||||
// 测试查找未删除的用户
|
||||
var activeUser = userRepository.findByEmailAndNotDeleted("softdelete@test.com");
|
||||
assertThat(activeUser).isEmpty();
|
||||
|
||||
System.out.println("✓ Soft delete query filter working");
|
||||
|
||||
// 清理
|
||||
userRepository.delete(deletedUser);
|
||||
}
|
||||
}
|
||||
250
src/test/java/com/lxy/hsend/service/FavoriteServiceTest.java
Normal file
250
src/test/java/com/lxy/hsend/service/FavoriteServiceTest.java
Normal file
@ -0,0 +1,250 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 收藏服务测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class FavoriteServiceTest {
|
||||
|
||||
@Autowired
|
||||
private FavoriteService favoriteService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
@Autowired
|
||||
private FavoriteRepository favoriteRepository;
|
||||
|
||||
private User testUser;
|
||||
private Session testSession;
|
||||
private Message testMessage;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试用户
|
||||
testUser = new User();
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setNickname("testuser");
|
||||
testUser.setPasswordHash("hashedpassword");
|
||||
testUser.setStatus((byte) 1);
|
||||
testUser = userRepository.save(testUser);
|
||||
|
||||
// 创建测试会话
|
||||
testSession = new Session();
|
||||
testSession.setSessionId(UUID.randomUUID().toString());
|
||||
testSession.setUserId(testUser.getId());
|
||||
testSession.setTitle("测试会话");
|
||||
testSession.setMessageCount(0);
|
||||
testSession.setLastMessageTime(TimeUtil.getCurrentTimestamp());
|
||||
testSession = sessionRepository.save(testSession);
|
||||
|
||||
// 创建测试消息
|
||||
testMessage = new Message();
|
||||
testMessage.setMessageId(UUID.randomUUID().toString());
|
||||
testMessage.setSessionId(testSession.getSessionId());
|
||||
testMessage.setUserId(testUser.getId());
|
||||
testMessage.setRole("user");
|
||||
testMessage.setContent("这是一条测试消息内容");
|
||||
testMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage = messageRepository.save(testMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddFavorite() {
|
||||
// 准备请求
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
|
||||
// 添加收藏
|
||||
ApiResponse<FavoriteResponse> response = favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
FavoriteResponse favoriteResponse = response.getBody();
|
||||
assertEquals(testMessage.getMessageId(), favoriteResponse.getMessageId());
|
||||
assertEquals(testSession.getSessionId(), favoriteResponse.getSessionId());
|
||||
assertEquals(testSession.getTitle(), favoriteResponse.getSessionTitle());
|
||||
assertEquals(testMessage.getContent(), favoriteResponse.getMessageContent());
|
||||
assertEquals(testMessage.getRole(), favoriteResponse.getMessageRole());
|
||||
assertNotNull(favoriteResponse.getFavoriteId());
|
||||
assertNotNull(favoriteResponse.getFavoritedAt());
|
||||
|
||||
// 验证数据库中的收藏记录
|
||||
boolean exists = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(
|
||||
testUser.getId(), testMessage.getMessageId());
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddFavoriteDuplicate() {
|
||||
// 先添加一次收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 再次尝试添加收藏,应该失败
|
||||
try {
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
fail("应该抛出重复收藏的异常");
|
||||
} catch (Exception e) {
|
||||
assertTrue(e.getMessage().contains("已经收藏过了"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveFavorite() {
|
||||
// 先添加收藏
|
||||
AddFavoriteRequest addRequest = new AddFavoriteRequest();
|
||||
addRequest.setMessageId(testMessage.getMessageId());
|
||||
addRequest.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), addRequest);
|
||||
|
||||
// 准备取消收藏请求
|
||||
RemoveFavoriteRequest removeRequest = new RemoveFavoriteRequest();
|
||||
removeRequest.setMessageId(testMessage.getMessageId());
|
||||
|
||||
// 取消收藏
|
||||
ApiResponse<Void> response = favoriteService.removeFavorite(testUser.getId(), removeRequest);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
|
||||
// 验证收藏记录已被删除
|
||||
boolean exists = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(
|
||||
testUser.getId(), testMessage.getMessageId());
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFavoriteList() {
|
||||
// 先添加几个收藏
|
||||
AddFavoriteRequest request1 = new AddFavoriteRequest();
|
||||
request1.setMessageId(testMessage.getMessageId());
|
||||
request1.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request1);
|
||||
|
||||
// 创建第二条消息和收藏
|
||||
Message message2 = new Message();
|
||||
message2.setMessageId(UUID.randomUUID().toString());
|
||||
message2.setSessionId(testSession.getSessionId());
|
||||
message2.setUserId(testUser.getId());
|
||||
message2.setRole("assistant");
|
||||
message2.setContent("这是AI助手的回答");
|
||||
message2.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
message2.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
message2.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(message2);
|
||||
|
||||
AddFavoriteRequest request2 = new AddFavoriteRequest();
|
||||
request2.setMessageId(message2.getMessageId());
|
||||
request2.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request2);
|
||||
|
||||
// 准备查询请求
|
||||
FavoriteListRequest listRequest = new FavoriteListRequest();
|
||||
listRequest.setPage(1);
|
||||
listRequest.setPageSize(10);
|
||||
listRequest.setMessageRole("all");
|
||||
|
||||
// 获取收藏列表
|
||||
ApiResponse<PageUtil.PageResponse<FavoriteResponse>> response =
|
||||
favoriteService.getFavoriteList(testUser.getId(), listRequest);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
PageUtil.PageResponse<FavoriteResponse> pageResponse = response.getBody();
|
||||
assertNotNull(pageResponse.getContent());
|
||||
assertEquals(2, pageResponse.getContent().size());
|
||||
assertEquals(2L, pageResponse.getTotal());
|
||||
assertEquals(1, pageResponse.getPage());
|
||||
assertEquals(10, pageResponse.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFavorited() {
|
||||
// 检查未收藏的消息
|
||||
ApiResponse<Boolean> response1 = favoriteService.isFavorited(testUser.getId(), testMessage.getMessageId());
|
||||
assertNotNull(response1);
|
||||
assertTrue(response1.getSuccess());
|
||||
assertFalse(response1.getBody());
|
||||
|
||||
// 添加收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 检查已收藏的消息
|
||||
ApiResponse<Boolean> response2 = favoriteService.isFavorited(testUser.getId(), testMessage.getMessageId());
|
||||
assertNotNull(response2);
|
||||
assertTrue(response2.getSuccess());
|
||||
assertTrue(response2.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoriteCount() {
|
||||
// 初始收藏数量应该为0
|
||||
ApiResponse<Long> response1 = favoriteService.getUserFavoriteCount(testUser.getId());
|
||||
assertNotNull(response1);
|
||||
assertTrue(response1.getSuccess());
|
||||
assertEquals(0L, response1.getBody());
|
||||
|
||||
// 添加一个收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 收藏数量应该为1
|
||||
ApiResponse<Long> response2 = favoriteService.getUserFavoriteCount(testUser.getId());
|
||||
assertNotNull(response2);
|
||||
assertTrue(response2.getSuccess());
|
||||
assertEquals(1L, response2.getBody());
|
||||
}
|
||||
}
|
||||
176
src/test/java/com/lxy/hsend/service/MessageServiceTest.java
Normal file
176
src/test/java/com/lxy/hsend/service/MessageServiceTest.java
Normal file
@ -0,0 +1,176 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 消息服务测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class MessageServiceTest {
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
private User testUser;
|
||||
private Session testSession;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试用户
|
||||
testUser = new User();
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setNickname("testuser");
|
||||
testUser.setPasswordHash("hashedpassword");
|
||||
testUser.setStatus((byte) 1);
|
||||
testUser = userRepository.save(testUser);
|
||||
|
||||
// 创建测试会话
|
||||
testSession = new Session();
|
||||
testSession.setSessionId(UUID.randomUUID().toString());
|
||||
testSession.setUserId(testUser.getId());
|
||||
testSession.setTitle("测试会话");
|
||||
testSession.setMessageCount(0);
|
||||
testSession.setLastMessageTime(TimeUtil.getCurrentTimestamp());
|
||||
testSession = sessionRepository.save(testSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendMessage() {
|
||||
// 准备请求
|
||||
SendMessageRequest request = new SendMessageRequest();
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
request.setContent("你好,这是一个测试消息");
|
||||
request.setDeepThinking(false);
|
||||
request.setWebSearch(false);
|
||||
|
||||
// 发送消息
|
||||
ApiResponse<MessageResponse> response = messageService.sendMessage(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
MessageResponse messageResponse = response.getBody();
|
||||
assertEquals(testSession.getSessionId(), messageResponse.getSessionId());
|
||||
assertEquals("assistant", messageResponse.getRole());
|
||||
assertNotNull(messageResponse.getContent());
|
||||
assertNotNull(messageResponse.getMessageId());
|
||||
assertNotNull(messageResponse.getCreatedAt());
|
||||
|
||||
// 验证数据库中的消息
|
||||
long messageCount = messageRepository.countBySessionIdAndNotDeleted(testSession.getSessionId());
|
||||
assertEquals(2, messageCount); // 用户消息 + AI响应
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessages() {
|
||||
// 先创建一些测试消息
|
||||
Message userMessage = new Message();
|
||||
userMessage.setMessageId(UUID.randomUUID().toString());
|
||||
userMessage.setSessionId(testSession.getSessionId());
|
||||
userMessage.setUserId(testUser.getId());
|
||||
userMessage.setRole("user");
|
||||
userMessage.setContent("用户消息");
|
||||
userMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
userMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
userMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(userMessage);
|
||||
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setMessageId(UUID.randomUUID().toString());
|
||||
aiMessage.setSessionId(testSession.getSessionId());
|
||||
aiMessage.setUserId(testUser.getId());
|
||||
aiMessage.setRole("assistant");
|
||||
aiMessage.setContent("AI响应");
|
||||
aiMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
aiMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
aiMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(aiMessage);
|
||||
|
||||
// 准备请求
|
||||
MessageListRequest request = new MessageListRequest();
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
request.setPage(1);
|
||||
request.setPageSize(10);
|
||||
request.setRole("all");
|
||||
|
||||
// 获取消息列表
|
||||
ApiResponse<PageUtil.PageResponse<MessageResponse>> response =
|
||||
messageService.getMessages(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
PageUtil.PageResponse<MessageResponse> pageResponse = response.getBody();
|
||||
assertNotNull(pageResponse.getContent());
|
||||
assertEquals(2, pageResponse.getContent().size());
|
||||
assertEquals(2L, pageResponse.getTotal());
|
||||
assertEquals(1, pageResponse.getPage());
|
||||
assertEquals(10, pageResponse.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageById() {
|
||||
// 创建测试消息
|
||||
Message testMessage = new Message();
|
||||
testMessage.setMessageId(UUID.randomUUID().toString());
|
||||
testMessage.setSessionId(testSession.getSessionId());
|
||||
testMessage.setUserId(testUser.getId());
|
||||
testMessage.setRole("user");
|
||||
testMessage.setContent("测试消息内容");
|
||||
testMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage = messageRepository.save(testMessage);
|
||||
|
||||
// 获取消息详情
|
||||
ApiResponse<MessageResponse> response =
|
||||
messageService.getMessageById(testUser.getId(), testMessage.getMessageId());
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
MessageResponse messageResponse = response.getBody();
|
||||
assertEquals(testMessage.getMessageId(), messageResponse.getMessageId());
|
||||
assertEquals(testMessage.getSessionId(), messageResponse.getSessionId());
|
||||
assertEquals(testMessage.getRole(), messageResponse.getRole());
|
||||
assertEquals(testMessage.getContent(), messageResponse.getContent());
|
||||
}
|
||||
}
|
||||
168
src/test/java/com/lxy/hsend/service/SessionServiceTest.java
Normal file
168
src/test/java/com/lxy/hsend/service/SessionServiceTest.java
Normal file
@ -0,0 +1,168 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.dto.session.CreateSessionRequest;
|
||||
import com.lxy.hsend.dto.session.SessionDetailResponse;
|
||||
import com.lxy.hsend.dto.session.SessionSearchRequest;
|
||||
import com.lxy.hsend.dto.session.UpdateSessionRequest;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 会话服务测试
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class SessionServiceTest {
|
||||
|
||||
@Autowired
|
||||
private SessionService sessionService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Test
|
||||
public void testSessionManagement() {
|
||||
log.info("开始测试会话管理功能");
|
||||
|
||||
// 1. 创建测试用户
|
||||
User testUser = createTestUser();
|
||||
Long userId = testUser.getId();
|
||||
|
||||
// 2. 测试创建会话
|
||||
CreateSessionRequest createRequest = new CreateSessionRequest();
|
||||
createRequest.setTitle("测试会话标题");
|
||||
createRequest.setInitialMessage("这是一条初始消息");
|
||||
|
||||
SessionDetailResponse createdSession = sessionService.createSession(userId, createRequest);
|
||||
|
||||
assertNotNull(createdSession);
|
||||
assertNotNull(createdSession.getSessionId());
|
||||
assertEquals("测试会话标题", createdSession.getTitle());
|
||||
assertEquals(userId, createdSession.getUserId());
|
||||
log.info("✅ 会话创建成功: sessionId={}", createdSession.getSessionId());
|
||||
|
||||
// 3. 测试获取会话详情
|
||||
SessionDetailResponse sessionDetail = sessionService.getSessionDetail(userId, createdSession.getSessionId());
|
||||
|
||||
assertNotNull(sessionDetail);
|
||||
assertEquals(createdSession.getSessionId(), sessionDetail.getSessionId());
|
||||
assertEquals("测试会话标题", sessionDetail.getTitle());
|
||||
log.info("✅ 获取会话详情成功");
|
||||
|
||||
// 4. 测试更新会话标题
|
||||
UpdateSessionRequest updateRequest = new UpdateSessionRequest();
|
||||
updateRequest.setTitle("更新后的标题");
|
||||
|
||||
SessionDetailResponse updatedSession = sessionService.updateSession(userId, createdSession.getSessionId(), updateRequest);
|
||||
|
||||
assertNotNull(updatedSession);
|
||||
assertEquals("更新后的标题", updatedSession.getTitle());
|
||||
log.info("✅ 会话标题更新成功");
|
||||
|
||||
// 5. 测试获取会话列表
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setPage(1);
|
||||
searchRequest.setPageSize(10);
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> sessionList = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
assertNotNull(sessionList);
|
||||
assertTrue(sessionList.getTotalElements() >= 1);
|
||||
log.info("✅ 获取会话列表成功,总数: {}", sessionList.getTotalElements());
|
||||
|
||||
// 6. 测试会话权限验证
|
||||
boolean hasPermission = sessionService.hasSessionPermission(userId, createdSession.getSessionId());
|
||||
assertTrue(hasPermission);
|
||||
log.info("✅ 会话权限验证成功");
|
||||
|
||||
// 7. 测试清空会话
|
||||
sessionService.clearSession(userId, createdSession.getSessionId());
|
||||
log.info("✅ 会话清空成功");
|
||||
|
||||
// 8. 测试删除会话
|
||||
sessionService.deleteSession(userId, createdSession.getSessionId());
|
||||
log.info("✅ 会话删除成功");
|
||||
|
||||
// 验证会话已被软删除
|
||||
boolean hasPermissionAfterDelete = sessionService.hasSessionPermission(userId, createdSession.getSessionId());
|
||||
assertFalse(hasPermissionAfterDelete);
|
||||
log.info("✅ 会话软删除验证成功");
|
||||
|
||||
System.out.println("✅ 所有会话管理功能测试通过!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionSearch() {
|
||||
log.info("开始测试会话搜索功能");
|
||||
|
||||
// 创建测试用户
|
||||
User testUser = createTestUser();
|
||||
Long userId = testUser.getId();
|
||||
|
||||
// 创建多个测试会话
|
||||
CreateSessionRequest request1 = new CreateSessionRequest();
|
||||
request1.setTitle("Java开发学习");
|
||||
sessionService.createSession(userId, request1);
|
||||
|
||||
CreateSessionRequest request2 = new CreateSessionRequest();
|
||||
request2.setTitle("Python数据分析");
|
||||
sessionService.createSession(userId, request2);
|
||||
|
||||
CreateSessionRequest request3 = new CreateSessionRequest();
|
||||
request3.setTitle("前端React开发");
|
||||
sessionService.createSession(userId, request3);
|
||||
|
||||
// 测试关键词搜索
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setKeyword("开发");
|
||||
searchRequest.setPage(1);
|
||||
searchRequest.setPageSize(10);
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> searchResult = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
assertNotNull(searchResult);
|
||||
assertTrue(searchResult.getTotalElements() >= 2); // 至少找到2个包含"开发"的会话
|
||||
log.info("✅ 会话关键词搜索成功,找到 {} 个结果", searchResult.getTotalElements());
|
||||
|
||||
// 测试排序
|
||||
searchRequest.setKeyword(null);
|
||||
searchRequest.setSortBy("created_at");
|
||||
searchRequest.setSortOrder("desc");
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> sortedResult = sessionService.getSessionList(userId, searchRequest);
|
||||
assertNotNull(sortedResult);
|
||||
log.info("✅ 会话排序功能测试成功");
|
||||
|
||||
System.out.println("✅ 会话搜索功能测试通过!");
|
||||
}
|
||||
|
||||
private User createTestUser() {
|
||||
User user = new User();
|
||||
user.setEmail("session-test@example.com");
|
||||
user.setPasswordHash("test-password-hash");
|
||||
user.setNickname("会话测试用户");
|
||||
user.setStatus((byte) 1);
|
||||
user.setCreatedAt(System.currentTimeMillis());
|
||||
user.setUpdatedAt(System.currentTimeMillis());
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user