init
This commit is contained in:
parent
e06820c124
commit
068f16ffaa
87
.gitignore
vendored
87
.gitignore
vendored
@ -1,79 +1,8 @@
|
|||||||
# ---> JetBrains
|
/.idea/
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
/*/target/
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
/*/*/target/
|
||||||
|
/seer-common/target/
|
||||||
# User-specific stuff
|
.flattened-pom.xml
|
||||||
.idea/**/workspace.xml
|
/*/.flattened-pom.xml
|
||||||
.idea/**/tasks.xml
|
/*/*/.flattened-pom.xml
|
||||||
.idea/**/usage.statistics.xml
|
/http_cache/
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# AWS User-specific
|
|
||||||
.idea/**/aws.xml
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# SonarLint plugin
|
|
||||||
.idea/sonarlint/
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
|
|||||||
271
README.md
271
README.md
@ -1,3 +1,270 @@
|
|||||||
# seer-teach-cloud-21
|
# Seer Teach Cloud - 智能教育AI云平台
|
||||||
|
|
||||||
jdk 21
|
[](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
|
||||||
|
[](https://spring.io/projects/spring-boot)
|
||||||
|
[](https://spring.io/projects/spring-cloud)
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
**Seer Teach Cloud** 是一个基于Spring Cloud微服务架构的智能教育AI云平台,集成了先进的AI技术,提供个性化学习、智能批改、知识图谱构建等功能。该平台专为现代教育场景设计,支持多终端访问,包括智能设备、移动端和Web端。
|
||||||
|
|
||||||
|
## 架构特点
|
||||||
|
|
||||||
|
### 微服务架构
|
||||||
|
- **服务拆分**:按业务领域拆分为多个独立的微服务模块
|
||||||
|
- **技术栈**:Spring Boot 3.5.9 + Spring Cloud 2025.0.1 + Spring Cloud Alibaba
|
||||||
|
- **网关层**:Spring Cloud Gateway统一入口,支持路由、限流、鉴权
|
||||||
|
- **注册中心**:Nacos服务注册与发现
|
||||||
|
- **配置中心**:Nacos统一配置管理
|
||||||
|
|
||||||
|
### AI智能教育引擎
|
||||||
|
- **大语言模型集成**:支持阿里云百炼、火山引擎等多种AI平台
|
||||||
|
- **智能批改**:AI自动批改作业和试卷,提供详细解析
|
||||||
|
- **个性化推荐**:基于学习行为的智能推荐系统
|
||||||
|
- **知识点拆分**:AI自动拆分和构建知识图谱
|
||||||
|
- **智能问答**:支持自然语言交互的智能答疑
|
||||||
|
|
||||||
|
### 核心功能模块
|
||||||
|
|
||||||
|
#### 1. 教师服务 (seer-teacher)
|
||||||
|
- 知识点管理与拆分
|
||||||
|
- 教学目标生成
|
||||||
|
- 讲课稿与音视频生成
|
||||||
|
- 学生学习报告分析
|
||||||
|
- AI辅助教学工具
|
||||||
|
|
||||||
|
#### 2. 用户服务 (seer-user)
|
||||||
|
- 用户注册与登录
|
||||||
|
- 权限管理
|
||||||
|
- 用户扩展信息管理
|
||||||
|
- 等级与积分系统
|
||||||
|
|
||||||
|
#### 3. 商城服务 (seer-mall)
|
||||||
|
- 商品管理与分类
|
||||||
|
- 订单处理
|
||||||
|
- 购物车功能
|
||||||
|
- 支付集成
|
||||||
|
|
||||||
|
#### 4. 支付服务 (seer-pay)
|
||||||
|
- 学豆系统(虚拟币)
|
||||||
|
- 充值与消费
|
||||||
|
- 订单支付
|
||||||
|
- 退款处理
|
||||||
|
|
||||||
|
#### 5. 物联网服务 (seer-iot)
|
||||||
|
- 设备管理
|
||||||
|
- 硬件交互
|
||||||
|
- 数据采集与分析
|
||||||
|
|
||||||
|
#### 6. 消息推送 (seer-netty)
|
||||||
|
- 实时消息推送
|
||||||
|
- IM通信
|
||||||
|
- 音视频流处理
|
||||||
|
|
||||||
|
#### 7. 微信服务 (seer-mp)
|
||||||
|
- 微信公众号集成
|
||||||
|
- 小程序支持
|
||||||
|
- OAuth认证
|
||||||
|
|
||||||
|
#### 8. 开放API (seer-open-api)
|
||||||
|
- API网关
|
||||||
|
- 第三方集成
|
||||||
|
- 访问控制
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 后端技术
|
||||||
|
- **基础框架**: Spring Boot 3.5.9 / Spring Cloud 2025.0.1
|
||||||
|
- **微服务**: Spring Cloud Alibaba, Nacos, OpenFeign
|
||||||
|
- **持久层**: MyBatis-Plus 3.5.15, MySQL, Redis
|
||||||
|
- **消息队列**: RocketMQ
|
||||||
|
- **任务调度**: XXL-Job
|
||||||
|
- **API文档**: SpringDoc OpenAPI
|
||||||
|
- **权限认证**: Sa-Token
|
||||||
|
- **对象存储**: MinIO
|
||||||
|
- **序列化**: Protobuf, Fastjson2
|
||||||
|
- **模板引擎**: Freemarker, Velocity
|
||||||
|
|
||||||
|
### AI与数据处理
|
||||||
|
- **AI平台集成**: 阿里云百炼、火山引擎
|
||||||
|
- **大模型调用**: LLM API集成
|
||||||
|
- **文本转语音**: AI语音合成
|
||||||
|
- **智能批改**: OCR + AI分析
|
||||||
|
- **知识图谱**: 知识点拆分与关联
|
||||||
|
|
||||||
|
### 网络通信
|
||||||
|
- **Netty**: 高性能网络通信框架
|
||||||
|
- **Protobuf**: 高效序列化协议
|
||||||
|
- **SSE**: Server-Sent Events实时通信
|
||||||
|
- **MQ**: RocketMQ消息队列
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
seer-teach-cloud-21/
|
||||||
|
├── seer-dependencies/ # 依赖管理
|
||||||
|
├── seer-common/ # 通用组件
|
||||||
|
│ ├── common/ # 通用工具类
|
||||||
|
│ ├── common-auth-scan/ # 认证扫描
|
||||||
|
│ ├── common-cache/ # 缓存组件
|
||||||
|
│ ├── common-config/ # 配置管理
|
||||||
|
│ ├── ... # 其他通用模块
|
||||||
|
├── seer-gateway/ # API网关
|
||||||
|
├── seer-teacher/ # 教师服务
|
||||||
|
│ ├── seer-teacher-api/ # 教师API接口
|
||||||
|
│ ├── seer-teacher-service/ # 教师业务服务
|
||||||
|
│ ├── seer-teacher-service-admin/ # 教师管理服务
|
||||||
|
│ └── ... # 其他教师模块
|
||||||
|
├── seer-user/ # 用户服务
|
||||||
|
├── seer-mall/ # 商城服务
|
||||||
|
├── seer-pay/ # 支付服务
|
||||||
|
├── seer-iot/ # 物联网服务
|
||||||
|
├── seer-netty/ # 网络通信服务
|
||||||
|
├── seer-mp/ # 微信服务
|
||||||
|
└── seer-open-api/ # 开放API服务
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Java 21+
|
||||||
|
- Maven 3.8+
|
||||||
|
- MySQL 8.0+
|
||||||
|
- Redis 6.0+
|
||||||
|
- Nacos Server
|
||||||
|
- MinIO (可选)
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
|
1. **克隆项目**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/seer-teach-cloud/seer-teach-cloud-21.git
|
||||||
|
cd seer-teach-cloud-21
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **配置环境**
|
||||||
|
```bash
|
||||||
|
# 配置数据库连接、Redis连接等
|
||||||
|
# 修改各模块的 application.yml 文件
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **编译项目**
|
||||||
|
```bash
|
||||||
|
mvn clean install -Dmaven.test.skip=true
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **启动服务**
|
||||||
|
```bash
|
||||||
|
# 1. 启动 Nacos Server
|
||||||
|
# 2. 启动 MySQL 和 Redis
|
||||||
|
# 3. 按顺序启动各微服务模块
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **本地开发执行命令**:
|
||||||
|
```shell
|
||||||
|
mvn flatten:clean
|
||||||
|
mvn clean install '-Dmaven.test.skip=true'
|
||||||
|
```
|
||||||
|
|
||||||
|
## API接口设计规范
|
||||||
|
|
||||||
|
### RESTful接口设计
|
||||||
|
- 资源使用复数名词(如/users),单个资源通过ID标识(如/users/{id})
|
||||||
|
- 避免URL中出现动词,操作通过HTTP方法区分
|
||||||
|
- 统一返回JSON格式,包含code、message、data字段
|
||||||
|
|
||||||
|
### HTTP方法与操作映射
|
||||||
|
|
||||||
|
| 方法 | 场景 | 状态码示例 |
|
||||||
|
| ------ | ---------------------------- | ---------------- |
|
||||||
|
| GET | 获取资源(支持过滤参数) | 200, 304, 404 |
|
||||||
|
| POST | 创建资源(需返回Location头) | 201, 400, 415 |
|
||||||
|
| PUT | 全量更新资源 | 200, 409(冲突) |
|
||||||
|
| DELETE | 删除资源 | 204, 404 |
|
||||||
|
|
||||||
|
### 过滤与分页参数
|
||||||
|
- 使用 `?limit=10&offset=0` 或 `?page=1&per_page=10` 控制返回数据量
|
||||||
|
- 支持排序(`?sort=field&order=asc`)和条件筛选(`?status=active`)
|
||||||
|
|
||||||
|
## 数据库设计规范
|
||||||
|
|
||||||
|
### 表结构设计
|
||||||
|
- 表名使用复数(如user改为users),主键为id,外键用关联表名_id(如user_id)
|
||||||
|
- 字段命名使用小驼峰(如create_time),避免保留字
|
||||||
|
|
||||||
|
### 索引优化
|
||||||
|
- 主键自动创建聚簇索引,唯一字段添加唯一索引
|
||||||
|
- 高频查询字段添加普通索引,复合索引遵循"最左前缀原则"
|
||||||
|
|
||||||
|
### 事务与连接池
|
||||||
|
- 使用@Transactional注解管理事务,明确传播行为
|
||||||
|
|
||||||
|
## 数据库迁移规范
|
||||||
|
|
||||||
|
### 版本化迁移脚本 (Versioned Migrations)
|
||||||
|
- 命名格式:V + 版本号 + 双下划线 + 描述 + .sql
|
||||||
|
- 示例:V20240619__init.sql、V1.5.0__add_user_table.sql
|
||||||
|
|
||||||
|
### 可重复迁移脚本 (Repeatable Migrations)
|
||||||
|
- 命名格式:R + 双下划线 + 描述 + .sql
|
||||||
|
- 示例:R__update_view.sql
|
||||||
|
|
||||||
|
## AI功能模块
|
||||||
|
|
||||||
|
### 支持的AI场景
|
||||||
|
- 知识点生成示例题目
|
||||||
|
- 知识点生成题目
|
||||||
|
- 教材解析
|
||||||
|
- 知识点拆分与检查
|
||||||
|
- 举一反三题目生成
|
||||||
|
- 教学目标生成
|
||||||
|
- 讲课稿生成
|
||||||
|
- 文本转语音
|
||||||
|
|
||||||
|
### AI平台支持
|
||||||
|
- 阿里云百炼平台
|
||||||
|
- 火山引擎(即梦)
|
||||||
|
- 本地模型
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 遵循Java编程规范
|
||||||
|
- 使用Lombok简化代码
|
||||||
|
- 使用MapStruct进行对象映射
|
||||||
|
- 统一日志记录
|
||||||
|
|
||||||
|
### 测试规范
|
||||||
|
- 单元测试覆盖率达到80%以上
|
||||||
|
- 集成测试覆盖核心业务流程
|
||||||
|
- 性能测试确保系统稳定性
|
||||||
|
|
||||||
|
## 部署说明
|
||||||
|
|
||||||
|
### 生产环境部署
|
||||||
|
1. 配置生产环境的数据库、Redis、Nacos等中间件
|
||||||
|
2. 编译打包各微服务模块
|
||||||
|
3. 使用Docker容器化部署
|
||||||
|
4. 配置负载均衡和监控
|
||||||
|
|
||||||
|
### Docker部署
|
||||||
|
```bash
|
||||||
|
# 构建Docker镜像
|
||||||
|
mvn spring-boot:build-image
|
||||||
|
|
||||||
|
# 使用Docker Compose部署
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控与运维
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
- 应用性能监控(APM)
|
||||||
|
- 业务指标监控
|
||||||
|
- 系统资源监控
|
||||||
|
- AI服务调用监控
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
**Seer Teach Cloud** - 让AI赋能教育,让学习更智能!
|
||||||
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>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-teach-cloud-21</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<description>seer-teach-cloud-21</description>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>seer-dependencies</module>
|
||||||
|
<module>seer-common</module>
|
||||||
|
<module>seer-teacher</module>
|
||||||
|
<module>seer-mall</module>
|
||||||
|
<module>seer-netty</module>
|
||||||
|
<module>seer-gateway</module>
|
||||||
|
<module>seer-pay</module>
|
||||||
|
<module>seer-user</module>
|
||||||
|
<module>seer-iot</module>
|
||||||
|
<module>seer-admin</module>
|
||||||
|
<module>seer-mp</module>
|
||||||
|
<module>seer-open-api</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<revision>1.0.0-SNAPSHOT</revision>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
|
||||||
|
<!-- Plugin versions -->
|
||||||
|
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
|
||||||
|
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
|
||||||
|
<flatten-maven-plugin.version>1.7.3</flatten-maven-plugin.version>
|
||||||
|
|
||||||
|
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MapStruct -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
|
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Maven -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.annotation</groupId>
|
||||||
|
<artifactId>jakarta.annotation-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.activation</groupId>
|
||||||
|
<artifactId>jakarta.activation-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.aspectj</groupId>
|
||||||
|
<artifactId>aspectjweaver</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!--Guava-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-dependencies</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<debug>true</debug>
|
||||||
|
<debuglevel>lines,vars,source</debuglevel>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>-parameters</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>flatten-maven-plugin</artifactId>
|
||||||
|
<version>${flatten-maven-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<flattenMode>bom</flattenMode>
|
||||||
|
<updatePomFile>true</updatePomFile>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>flatten</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>flatten</goal>
|
||||||
|
<goal>clean</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>central</id>
|
||||||
|
<url>https://maven.aliyun.com/repository/central</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>huawei</id>
|
||||||
|
<name>huawei</name>
|
||||||
|
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
</project>
|
||||||
23
seer-admin/Dockerfile
Normal file
23
seer-admin/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM eclipse-temurin:21-jre
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 创建应用目录
|
||||||
|
RUN mkdir -p /app/java/seer-admin
|
||||||
|
WORKDIR /app/java/seer-admin
|
||||||
|
|
||||||
|
# 复制 JAR 文件
|
||||||
|
COPY ./target/*.jar app.jar
|
||||||
|
|
||||||
|
# 复制 entrypoint 脚本
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
# 确保脚本可执行
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# 创建临时卷(可选)
|
||||||
|
VOLUME /tmp
|
||||||
|
|
||||||
|
# 设置入口点(使用 Exec 模式)
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
155
seer-admin/Jenkinsfile
vendored
Normal file
155
seer-admin/Jenkinsfile
vendored
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
cdpipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
tools {
|
||||||
|
jdk 'jdk21'
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
string(
|
||||||
|
name: 'DEPLOY_SERVERS',
|
||||||
|
defaultValue: '192.168.0.238',
|
||||||
|
description: '部署的目标服务器,多个目标服务器以英文逗号分隔,如:192.168.0.47,192.168.0.79'
|
||||||
|
)
|
||||||
|
choice(
|
||||||
|
name: 'spring.profiles.active',
|
||||||
|
choices: ['dev','test', 'prod'],
|
||||||
|
description: '选择要激活的 Spring Profile'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
BASE_IMAGE_NAME = 'seer-admin'
|
||||||
|
SSH_CREDENTIALS_ID = 'seerTeachPubKey'
|
||||||
|
DEPLOY_USER = 'root'
|
||||||
|
LOGS_HOST_PATH = '/app/java/seer-admin/logs'
|
||||||
|
LOGS_CONTAINER_PATH = '/app/java/seer-admin/logs'
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timeout(time: 30, unit: 'MINUTES')
|
||||||
|
skipDefaultCheckout(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('初始化 & 分支校验') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh 'echo "初始Git工作目录: $(pwd)"'
|
||||||
|
sh 'ls -la '
|
||||||
|
def scmVars = checkout scm
|
||||||
|
env.GIT_COMMIT = scmVars.GIT_COMMIT
|
||||||
|
env.GIT_BRANCH = scmVars.GIT_BRANCH
|
||||||
|
|
||||||
|
// 清理分支名称,替换非法字符,确保符合 Docker 标签命名规范
|
||||||
|
env.BRANCH_CLEAN = env.GIT_BRANCH.replaceAll(/[^\w.-]/, '-')
|
||||||
|
env.IMAGE_TAG = "${BASE_IMAGE_NAME}:${env.BRANCH_CLEAN}"
|
||||||
|
env.TAR_FILE = "${BASE_IMAGE_NAME}_${env.BRANCH_CLEAN}.tar"
|
||||||
|
|
||||||
|
echo " 初始化完成"
|
||||||
|
echo "原始分支:${env.GIT_BRANCH}"
|
||||||
|
echo "清理后分支:${env.BRANCH_CLEAN}"
|
||||||
|
echo "镜像标签:${env.IMAGE_TAG}"
|
||||||
|
echo "目标服务器:${params.DEPLOY_SERVERS}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('构建 Maven 项目') {
|
||||||
|
steps {
|
||||||
|
sh 'echo "初始工作目录: $(pwd)"'
|
||||||
|
sh 'ls -la '
|
||||||
|
sh 'mvn clean package -Dmaven.test.skip=true -pl :seer-admin -am'
|
||||||
|
echo " Maven 构建完成"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('构建 Docker 镜像') {
|
||||||
|
steps {
|
||||||
|
dir('seer-admin') {
|
||||||
|
sh "docker build -t ${env.IMAGE_TAG} ."
|
||||||
|
echo " Docker 镜像构建成功:${env.IMAGE_TAG}"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('导出并传输镜像') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh 'echo "导出并传输镜像工作目录: $(pwd)"'
|
||||||
|
sh "docker save -o ${env.TAR_FILE} ${env.IMAGE_TAG}"
|
||||||
|
echo "镜像已导出为文件:${env.TAR_FILE}"
|
||||||
|
|
||||||
|
def servers = params.DEPLOY_SERVERS.split(',')
|
||||||
|
servers.each { server ->
|
||||||
|
sshagent([env.SSH_CREDENTIALS_ID]) {
|
||||||
|
def sev = server.trim()
|
||||||
|
sh "scp -o StrictHostKeyChecking=no ${env.TAR_FILE} ${DEPLOY_USER}@${sev}:/tmp/"
|
||||||
|
}
|
||||||
|
echo " 镜像已传输到远程服务器:${server}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stage('远程部署镜像') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def servers = params.DEPLOY_SERVERS.split(',')
|
||||||
|
servers.each { server ->
|
||||||
|
deployToServer(server.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
script {
|
||||||
|
if (env.TAR_FILE?.trim()) {
|
||||||
|
sh "rm -f ${env.TAR_FILE} || true"
|
||||||
|
}
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
echo " 构建成功:分支 ${env.GIT_BRANCH},镜像 ${env.IMAGE_TAG}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo " 构建失败:分支 ${env.GIT_BRANCH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def deployToServer(server){
|
||||||
|
sshagent([env.SSH_CREDENTIALS_ID]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${server} << 'EOF'
|
||||||
|
set -e
|
||||||
|
echo "🛠 加载镜像..."
|
||||||
|
docker load -i /tmp/${env.TAR_FILE}
|
||||||
|
|
||||||
|
echo " 停止并移除旧容器(如果存在)..."
|
||||||
|
docker stop ${BASE_IMAGE_NAME} || true
|
||||||
|
docker rm ${BASE_IMAGE_NAME} || true
|
||||||
|
|
||||||
|
echo " 创建日志目录..."
|
||||||
|
mkdir -p ${LOGS_HOST_PATH}
|
||||||
|
|
||||||
|
echo " 启动新容器..."
|
||||||
|
echo "激活的 Profile: ${params['spring.profiles.active']}"
|
||||||
|
docker run -d --name ${BASE_IMAGE_NAME} -p 6060:6060 \\
|
||||||
|
-v ${LOGS_HOST_PATH}:${LOGS_CONTAINER_PATH} \\
|
||||||
|
-e NACOS_DISCOVERY_IP=${server} \\
|
||||||
|
-e SPRING_PROFILES_ACTIVE=${params['spring.profiles.active']} \\
|
||||||
|
--restart=always \\
|
||||||
|
${env.IMAGE_TAG}
|
||||||
|
EOF
|
||||||
|
"""
|
||||||
|
echo " 镜像已部署到远程服务器:${server}"
|
||||||
|
}
|
||||||
|
}
|
||||||
82
seer-admin/entrypoint.sh
Normal file
82
seer-admin/entrypoint.sh
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# =============================================================================
|
||||||
|
# Docker Entrypoint Script for seer-admin
|
||||||
|
# - 支持 JAVA_OPTS 覆盖
|
||||||
|
# - 添加默认 JVM 参数
|
||||||
|
# - 支持优雅关闭 (exec)
|
||||||
|
# - 可扩展(健康检查、依赖等待等)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Starting seer-admin..."
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 设置默认环境变量
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
SPRING_PROFILES_ACTIVE="${SPRING_PROFILES_ACTIVE:-prod}"
|
||||||
|
NACOS_DISCOVERY_IP="${NACOS_DISCOVERY_IP:-$(hostname -i)}"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 合并环境变量和默认 JVM 参数
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 用户通过 -e JAVA_OPTS 传递的参数优先
|
||||||
|
USER_JAVA_OPTS="${JAVA_OPTS:-}"
|
||||||
|
|
||||||
|
# 内置默认 JVM 参数
|
||||||
|
DEFAULT_JAVA_OPTS="
|
||||||
|
-Xms1g
|
||||||
|
-Xmx1g
|
||||||
|
-Xmn512m
|
||||||
|
-XX:+UseG1GC
|
||||||
|
-XX:MaxGCPauseMillis=200
|
||||||
|
-XX:+UseContainerSupport
|
||||||
|
-XX:+UseNUMA
|
||||||
|
-XX:+ParallelRefProcEnabled
|
||||||
|
-XX:+UseStringDeduplication
|
||||||
|
-XX:MaxRAMPercentage=70.0
|
||||||
|
-XX:MetaspaceSize=256m
|
||||||
|
-XX:MaxMetaspaceSize=256m
|
||||||
|
-XX:MaxDirectMemorySize=512m
|
||||||
|
-XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
-XX:HeapDumpPath=./user_heapdump.hprof
|
||||||
|
-Djava.security.egd=file:/dev/./urandom
|
||||||
|
"
|
||||||
|
|
||||||
|
# 合并:用户参数 + 默认参数
|
||||||
|
if [ -z "$USER_JAVA_OPTS" ]; then
|
||||||
|
JAVA_OPTS="$DEFAULT_JAVA_OPTS"
|
||||||
|
log "使用内置默认 JVM 参数"
|
||||||
|
else
|
||||||
|
JAVA_OPTS="$USER_JAVA_OPTS"
|
||||||
|
log "使用用户提供的 JVM 参数"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 可选:打印环境变量(调试用)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
log "Spring Profiles Active: $SPRING_PROFILES_ACTIVE"
|
||||||
|
log "Nacos Discovery IP: $NACOS_DISCOVERY_IP"
|
||||||
|
log "JVM Options: $JAVA_OPTS"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 执行主应用(关键:使用 exec,让 Java 成为 PID=1)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
exec java \
|
||||||
|
${JAVA_OPTS} \
|
||||||
|
-Dspring.profiles.active="${SPRING_PROFILES_ACTIVE}" \
|
||||||
|
-Dspring.cloud.nacos.discovery.ip="${NACOS_DISCOVERY_IP}" \
|
||||||
|
-jar /app/java/seer-admin/app.jar "$@"
|
||||||
147
seer-admin/pom.xml
Normal file
147
seer-admin/pom.xml
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-teach-cloud-21</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>seer-admin</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-web</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-cache</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-config</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-user-service-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-mall-service-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-pay-service-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-mp-service-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-teacher-service-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>seer-iot-admin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis 支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 服务发现 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 配置中心 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<!-- 设置构建的 jar 包名 -->
|
||||||
|
<finalName>${project.artifactId}</finalName>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>${basedir}/src/main/resources</directory>
|
||||||
|
<includes>
|
||||||
|
<include>**.properties</include>
|
||||||
|
<include>**.xml</include>
|
||||||
|
<include>**.yml</include>
|
||||||
|
<include>certs/**.pem</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>3.5.9</version>
|
||||||
|
<configuration>
|
||||||
|
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
|
||||||
|
<mainClass>com.seer.teach.admin.SeerAdminApplication</mainClass>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.seer.teach.admin;
|
||||||
|
|
||||||
|
import cn.hutool.core.net.NetUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
|
||||||
|
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||||
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EnableFeignClients(basePackages = "com.seer.teach.*.api")
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.seer",exclude = QuartzAutoConfiguration.class)
|
||||||
|
@EnableTransactionManagement
|
||||||
|
@EnableAspectJAutoProxy
|
||||||
|
@EnableAsync
|
||||||
|
@ServletComponentScan
|
||||||
|
@MapperScan(basePackages={"com.seer.**.mapper"})
|
||||||
|
public class SeerAdminApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication app = new SpringApplication(SeerAdminApplication.class);
|
||||||
|
Environment env = app.run(args).getEnvironment();
|
||||||
|
String port = env.getProperty("server.port", "8080");
|
||||||
|
String contextPath = env.getProperty("server.servlet.context-path", "/");
|
||||||
|
log.info("----------------------------------------------------------");
|
||||||
|
log.info("---------Application '{}' is running-------------------",env.getProperty("spring.application.name"));
|
||||||
|
log.info("-----------Local : http://localhost:{}{} --------------",port,contextPath);
|
||||||
|
log.info("--------------Ip : http://{}:{}{} --------------------", NetUtil.getLocalhostStr(),port,contextPath);
|
||||||
|
log.info("---------Swagger : http://{}:{}{}/swagger-ui/index.html",NetUtil.getLocalhostStr(),port,contextPath);
|
||||||
|
log.info("----------------------------------------------------------");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package com.seer.teach.admin.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
import org.springdoc.core.models.GroupedOpenApi;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Autograph: 安稳
|
||||||
|
* @Description: SpringDoc OpenAPI配置类
|
||||||
|
* @Date: 2022-11-03 02:01:09
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Profile({"local","dev","test"})
|
||||||
|
public class AdminApiConfig {
|
||||||
|
|
||||||
|
@Value("${server.servlet.context-path:/}")
|
||||||
|
private String contextPath;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI adminOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("Seer Teach 后台管理系统API")
|
||||||
|
.description("Seer Teach 后台管理系统API文档")
|
||||||
|
.version("v1.0.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("广东用嘉研发部")))
|
||||||
|
.components(new io.swagger.v3.oas.models.Components()
|
||||||
|
.addSecuritySchemes("token",
|
||||||
|
new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
.in(SecurityScheme.In.HEADER)
|
||||||
|
.name("token")
|
||||||
|
.description("输入sa-token令牌进行认证")))
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList("token"))
|
||||||
|
.addServersItem(new Server().url(contextPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi mallAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-mall")
|
||||||
|
.pathsToMatch("/mall/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi payAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-pay")
|
||||||
|
.pathsToMatch("/pay/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi userAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-user")
|
||||||
|
.pathsToMatch("/user/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi tvAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-tv")
|
||||||
|
.pathsToMatch("/tv/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi teacherAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-teacher")
|
||||||
|
.pathsToMatch("/teacher/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi commonAdminApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin-common")
|
||||||
|
.pathsToMatch("/common/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.seer.teach.admin.config;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
|
import cn.dev33.satoken.filter.SaServletFilter;
|
||||||
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.dev33.satoken.util.SaResult;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SaTokenConfigure {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 [Sa-Token全局过滤器],设置顺序在CORS过滤器之后
|
||||||
|
*/
|
||||||
|
//@Bean
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||||
|
public SaServletFilter getSaServletFilter() {
|
||||||
|
return new SaServletFilter()
|
||||||
|
.addInclude("/**")
|
||||||
|
.addExclude("/favicon.ico", "/user/login", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**")
|
||||||
|
.setAuth(obj -> {
|
||||||
|
if ("OPTIONS".equals(SaHolder.getRequest().getMethod())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SaRouter.match("/**", r -> {
|
||||||
|
// 排除多个不需要认证的接口
|
||||||
|
SaRouter.match("/user/login");
|
||||||
|
SaRouter.match("/user/register");
|
||||||
|
SaRouter.match("/swagger-ui/**");
|
||||||
|
SaRouter.match("/v3/api-docs/**");
|
||||||
|
SaRouter.match("/swagger-resources/**");
|
||||||
|
// 其他接口都需要登录
|
||||||
|
StpUtil.checkLogin();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.setError(e -> {
|
||||||
|
return SaResult.code(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
})
|
||||||
|
.setBeforeAuth(r -> {
|
||||||
|
SaHolder.getResponse()
|
||||||
|
// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
|
||||||
|
.setHeader("X-XSS-Protection", "1; mode=block")
|
||||||
|
// 禁用浏览器内容嗅探
|
||||||
|
.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
202
seer-admin/src/main/resources/application-local.yml
Normal file
202
seer-admin/src/main/resources/application-local.yml
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
server:
|
||||||
|
port: 6060
|
||||||
|
servlet:
|
||||||
|
context-path: /seer/admin
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: seer-admin # 服务名称
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
|
allow-circular-references: true
|
||||||
|
mvc:
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
url: jdbc:mysql://localhost:3306/seer_teach?useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&connectTimeout=10000&socketTimeout=60000
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
# Redis数据库索引(默认为0)
|
||||||
|
database: 0
|
||||||
|
# Redis服务器地址
|
||||||
|
host: 192.168.0.45
|
||||||
|
# Redis服务器连接端口
|
||||||
|
port: 6379
|
||||||
|
# Redis服务器连接密码(默认为空)
|
||||||
|
password: Zs139768
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 200MB
|
||||||
|
max-request-size: 200MB
|
||||||
|
mvc:
|
||||||
|
pathmatch:
|
||||||
|
matching-strategy: ant_path_matcher
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/mysql
|
||||||
|
baseline-on-migrate: true
|
||||||
|
clean-disable: true
|
||||||
|
table: flyway_schema_history_admin
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
server-addr: 192.168.0.39:8848 # Nacos 服务地址
|
||||||
|
group: DEFAULT_GROUP # 分组(默认DEFAULT_GROUP)
|
||||||
|
namespace: ${spring.profiles.active}
|
||||||
|
config:
|
||||||
|
server-addr: 192.168.0.39:8848 # 配置中心地址
|
||||||
|
file-extension: yaml # 配置文件后缀(yaml/properties)
|
||||||
|
namespace: ${spring.profiles.active}
|
||||||
|
#日志
|
||||||
|
logging:
|
||||||
|
config: classpath:logback-${spring.profiles.active}.xml
|
||||||
|
|
||||||
|
# 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
|
||||||
|
wx:
|
||||||
|
mp:
|
||||||
|
config-storage:
|
||||||
|
type: RedisTemplate
|
||||||
|
key-prefix: wx
|
||||||
|
http-client-type: HttpClient
|
||||||
|
app-id: null
|
||||||
|
miniapp:
|
||||||
|
appid: wx04019010242d540e
|
||||||
|
secret: d9280058237ee5e3dd81bafd72e5cf3b
|
||||||
|
msgDataFormat: XML
|
||||||
|
config-storage:
|
||||||
|
type: RedisTemplate
|
||||||
|
key-prefix: wx
|
||||||
|
|
||||||
|
feign:
|
||||||
|
okhttp:
|
||||||
|
enabled: true
|
||||||
|
httpclient:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
type-aliases-package: com.seer.teach.entity
|
||||||
|
mapper-locations: classpath:mapper/*.xml,classpath*:com/seer/teach/**/*Mapper.xml
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
logic-delete-field: deleted
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
use-generated-keys: true
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: "*"
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
shutdown:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
#极兔
|
||||||
|
logistics:
|
||||||
|
jt:
|
||||||
|
apiAccount: '178337126125932605'
|
||||||
|
privateKey: '0258d71b55fc45e3ad7a7f38bf4b201a'
|
||||||
|
customerCode: 'J0086474299'
|
||||||
|
customerPwd: 'H5CD3zE6'
|
||||||
|
createOrderUrl: 'https://uat-openapi.jtexpress.com.cn/webopenplatformapi/api/order/addOrder'
|
||||||
|
queryTraceUrl: 'https://uat-openapi.jtexpress.com.cn/webopenplatformapi/api/logistics/trace'
|
||||||
|
|
||||||
|
|
||||||
|
#Minio配置
|
||||||
|
minio:
|
||||||
|
bucket: seerteach
|
||||||
|
url: http://192.168.0.39:9300
|
||||||
|
accessKey: oCkIBnlSHPstC9opjava
|
||||||
|
secretKey: sizdZmYHKQhFAZIFC2FnZ7BhRzSd3TGmTlXIAIzu
|
||||||
|
aiCorrect: aiCorrect/
|
||||||
|
avatar: avatar/
|
||||||
|
wxQrCodeImage: wxQrCodeImage/
|
||||||
|
aiAudio: aiAudio/
|
||||||
|
aiChatAudio: aiChatAudio/
|
||||||
|
aiSmartAudio: aiSmartAudio/
|
||||||
|
aiHealthArticleAudio: aiSmartHealthArticleAudio/
|
||||||
|
aiPreview: aiPreview/
|
||||||
|
aiReview: aiReview/
|
||||||
|
userAudios: userAudios/
|
||||||
|
chatLogFolder: chatLogFolder/
|
||||||
|
androidFolder: android/
|
||||||
|
banner: banner/
|
||||||
|
sa-token:
|
||||||
|
# cookie:
|
||||||
|
# same-site: None
|
||||||
|
# secure: false
|
||||||
|
# token 名称(同时也是 cookie 名称)
|
||||||
|
token-name: token
|
||||||
|
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||||
|
timeout: 2592000
|
||||||
|
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||||
|
active-timeout: -1
|
||||||
|
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||||
|
is-concurrent: false
|
||||||
|
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||||
|
is-share: true
|
||||||
|
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||||
|
token-style: uuid
|
||||||
|
# 是否输出操作日志
|
||||||
|
is-log: true
|
||||||
|
#大模型配置
|
||||||
|
largeModel:
|
||||||
|
ip: http://192.168.0.28:8878/
|
||||||
|
aiCorrectToPoint: ai_correct_to_point
|
||||||
|
cancelAiCorrect: cancel_ai_correct
|
||||||
|
aiCorrectToPointToChallenge: ai_correct_to_point_to_challenge
|
||||||
|
questionsCorrect: questions_correct
|
||||||
|
getChapters: get_chapters_context
|
||||||
|
aiQuestionCorrect: ai_evaluation_correct
|
||||||
|
aiChat: ai_chat
|
||||||
|
getQuestions: get_questions
|
||||||
|
getPlanQuestions: get_plan_questions
|
||||||
|
study: study
|
||||||
|
vcr: vcr
|
||||||
|
tts: tts
|
||||||
|
stt: stt
|
||||||
|
sseAiChat: sseChat
|
||||||
|
sseStudy: sseStudy
|
||||||
|
sseAiCorrect: ahom_sseAiCorrect
|
||||||
|
getComment: get_comment
|
||||||
|
recommendChapters: recommend_chapters
|
||||||
|
planCoverImage: http://192.168.0.39:9300/seerteach/systemConfig/planConver/planImage.png
|
||||||
|
ocr: http://192.168.0.28:8099/bboxAndOrder
|
||||||
|
processUpload: http://192.168.0.19:7007/processBase64
|
||||||
|
getContextAboutSingleQuestion: http://192.168.0.28:8099/getContextAboutSingleQuestion
|
||||||
|
getTextBase64: http://192.168.0.28:8099/getTextBase64
|
||||||
|
|
||||||
|
|
||||||
|
zs:
|
||||||
|
decryption:
|
||||||
|
enabled: true
|
||||||
|
encryption:
|
||||||
|
enabled: true
|
||||||
|
task:
|
||||||
|
clean-minio:
|
||||||
|
enabled: false
|
||||||
|
online-device:
|
||||||
|
enabled: false
|
||||||
|
refresh-wx-access-token:
|
||||||
|
enabled: false
|
||||||
|
text:
|
||||||
|
similarity:
|
||||||
|
service:
|
||||||
|
url: http://192.168.0.108:5000/text-similarity-batch
|
||||||
|
cors:
|
||||||
|
origins:
|
||||||
|
- http://localhost:*
|
||||||
|
- http://192.168.*:*
|
||||||
|
- http://10.10.*:*
|
||||||
|
- http://10.10.4.143:8020
|
||||||
|
- https://admin-api.seerteach.net:*
|
||||||
|
- http://admin-api.seerteach.net:*
|
||||||
|
- https://admin.seerteach.net:*
|
||||||
|
- https://admin.seerteach.net
|
||||||
104
seer-admin/src/main/resources/application.yml
Normal file
104
seer-admin/src/main/resources/application.yml
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
server:
|
||||||
|
port: 6060
|
||||||
|
servlet:
|
||||||
|
context-path: /seer/admin
|
||||||
|
multipart:
|
||||||
|
max-file-size: 50MB
|
||||||
|
max-request-size: 50MB
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: seer-admin
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
|
allow-circular-references: true
|
||||||
|
mvc:
|
||||||
|
|
||||||
|
profiles:
|
||||||
|
active: dev
|
||||||
|
mvc:
|
||||||
|
pathmatch:
|
||||||
|
matching-strategy: ant_path_matcher
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/mysql
|
||||||
|
baseline-on-migrate: true
|
||||||
|
clean-disable: true
|
||||||
|
table: flyway_schema_history_admin
|
||||||
|
config:
|
||||||
|
import:
|
||||||
|
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml
|
||||||
|
- optional:nacos:shared-database.yaml
|
||||||
|
- optional:nacos:shared-redis.yaml
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
server-addr: 192.168.0.39:8848 # Nacos 服务地址
|
||||||
|
group: DEFAULT_GROUP # 分组(默认DEFAULT_GROUP)
|
||||||
|
namespace: ${spring.profiles.active}
|
||||||
|
config:
|
||||||
|
server-addr: 192.168.0.39:8848 # 配置中心地址
|
||||||
|
file-extension: yaml # 配置文件后缀(yaml/properties)
|
||||||
|
namespace: ${spring.profiles.active}
|
||||||
|
#日志
|
||||||
|
logging:
|
||||||
|
config: classpath:logback-${spring.profiles.active}.xml
|
||||||
|
|
||||||
|
# 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
|
||||||
|
wx:
|
||||||
|
mp:
|
||||||
|
config-storage:
|
||||||
|
type: RedisTemplate
|
||||||
|
key-prefix: wx
|
||||||
|
http-client-type: HttpClient
|
||||||
|
app-id: null
|
||||||
|
miniapp:
|
||||||
|
appid: wx04019010242d540e
|
||||||
|
secret: d9280058237ee5e3dd81bafd72e5cf3b
|
||||||
|
msgDataFormat: XML
|
||||||
|
config-storage:
|
||||||
|
type: RedisTemplate
|
||||||
|
key-prefix: wx
|
||||||
|
|
||||||
|
feign:
|
||||||
|
okhttp:
|
||||||
|
enabled: true
|
||||||
|
httpclient:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: "*"
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
shutdown:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
zs:
|
||||||
|
decryption:
|
||||||
|
enabled: true
|
||||||
|
encryption:
|
||||||
|
enabled: true
|
||||||
|
task:
|
||||||
|
clean-minio:
|
||||||
|
enabled: false
|
||||||
|
online-device:
|
||||||
|
enabled: false
|
||||||
|
refresh-wx-access-token:
|
||||||
|
enabled: false
|
||||||
|
text:
|
||||||
|
similarity:
|
||||||
|
service:
|
||||||
|
url: http://192.168.0.108:5000/text-similarity-batch
|
||||||
|
cors:
|
||||||
|
origins:
|
||||||
|
- http://localhost:*
|
||||||
|
- http://192.168.*:*
|
||||||
|
- http://10.10.*:*
|
||||||
|
- http://10.10.4.143:8020
|
||||||
|
- https://admin-api.seerteach.net:*
|
||||||
|
- http://admin-api.seerteach.net:*
|
||||||
|
- https://admin.seerteach.net:*
|
||||||
|
- https://admin.seerteach.net
|
||||||
28
seer-admin/src/main/resources/certs/apiclient_key.pem
Normal file
28
seer-admin/src/main/resources/certs/apiclient_key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCmWTZURv0fHtyn
|
||||||
|
nr9AZMe6CpkVTMVxVtAY0363wZowGQOEBulY7NuZUF2rrt5nkBsYKwN7h8cj7c5s
|
||||||
|
tA6Yx2KSdGegq5+T3Jbcc19vo1N+fvRKWiFZ22k3DFR8Iup+LGK1/nESvggAEr7P
|
||||||
|
+sam2ODvd1kGJxVI/5rViy7oKTF5MNpwIUNnoS92qr9nHVopOkU8pYID5beek+jE
|
||||||
|
nIAZfXvrOIK1XvqnEGH9/3PGJ/i02DGb2vqr1Kll14Xwa8hJuKUcHCF2gwKtMUTu
|
||||||
|
Z6ybuCp0UIzszSvdsAFTn97hQEPClcFsff6mWfL0K/0dc1dHvm1468NsroZyYyWJ
|
||||||
|
ynBzVeC/AgMBAAECggEBAIafgFZoNQVwhoao9IJ6jSDE3urb/JYi+bp9vvmbltsC
|
||||||
|
A1Rf+4zZ80Z6QbRlitwpRaQje2gHlGRBWmOivIVsJxv7VLo06qpRRU4XmM7SUQn4
|
||||||
|
WF+r3X3JEbdZJS5pW3jNFv3Oc1gFrpfQk9fhTc9NiYyC++r8yj8PjRDw2P9OBxnZ
|
||||||
|
3uuB8uRuP9dh8ED5y4PcIfNh/I5BCulqz8zpIFP2GjkOE6whaHF9r7wlx933sheg
|
||||||
|
+JgLkzQiVfCL1kO5aLdbywbttjb2DDXBiOfiX3IZFod+epyjCqrXag+YdTC4molr
|
||||||
|
oDGHVZJYVujCdszZt5QFD376oP/uK1MnDLNoJnN6tMECgYEA21FuDizAQ1S+m5O+
|
||||||
|
YNiu0lPH74MQHZTEh/EPA4gsviQPr9TN1maHbXy94wxTX9I5q/T3Nh2aDibbvcjQ
|
||||||
|
5vcV86RoYE+T+DN8ax65x2Tgq6uWxLi51DkKkrwdd2zyZZ4CDNdGIgV7z3dAMfDo
|
||||||
|
i8hC6GDP/oQF7q5jiZqeFzUfH+8CgYEAwivGuaxvtJGpiGkHPawjbwzzT+mdbDEc
|
||||||
|
Ue60EMMigpFXCn1h08bLbEG/lNvZ8ecrp49B2p/x+64HPwoEy7bzp6f8NTPjLkpq
|
||||||
|
V54uCeAzDiUOX/DSVGZwX0u/Ht1B7hSjadca9ras4XGBCcers5P2y/+lYRlax6jh
|
||||||
|
TsCwD6CnfDECgYEAxtdLKrrUDbeVoMQQxQlvZu3ixXpUcB1jGcUqUY9y0Wksd8Q+
|
||||||
|
YvZOLqv8FRAlvyiAdTEBuSSZed8tNyIMlHrMgjs7DqbXhx5W3V/cG7WQJNTLOswo
|
||||||
|
XwrgVS0Moiw6kHrzbOT4hvvlxrFdmGnMzH7ieoDb0uur3TxqrmVqk6vr7i0CgYB1
|
||||||
|
yO5ctXBxnaa0m9mLnL9F3xo9kJ4xAj2GqgFK5cQqZhXhxBsyxzWg7uVTXGYB6tQ9
|
||||||
|
aZZuE3ZL0M6Oe/paxRlay3kfoOEftH57tfWBgiIWY34rzr8X+agS9rTx+Q/EZ3qV
|
||||||
|
eqndnQSUITFAiIHshkZAi0x78VBzK0u5ZQOoBzFyEQKBgQCRDnHLsfjBf6tQa8E/
|
||||||
|
xghQYVuBN117c9EMLdTcSZ/jDpjuybJFwgnleuQ6kuNa7Ye90jJsvWSvpRYOQYkL
|
||||||
|
xDMDgOLhBoBFysMUodFMS/hsLkQHaiPfIaS/6AY5Xbv/NdZQxe9OybVBxJWzHT+L
|
||||||
|
lKJ6JV8s7P5/eXpoq3yivOMclA==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
107
seer-admin/src/main/resources/logback-dev.xml
Normal file
107
seer-admin/src/main/resources/logback-dev.xml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Windows-->
|
||||||
|
<configuration>
|
||||||
|
<contextName>community</contextName>
|
||||||
|
<!--设置日志所在的目录-->
|
||||||
|
<property name="LOG_PATH" value="/app/java/seer-admin/logs"/>
|
||||||
|
<!--设置当前项目的名字。一般放在data目录下还会有其它项目的日志文件,所以需要设置一个-->
|
||||||
|
<property name="Logging" value="seer-admin"/>
|
||||||
|
<property name="CONSOLE_LOG_PATTERN" value="%red(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}) [%file:%line] %cyan(%msg%n)"/>
|
||||||
|
|
||||||
|
<!-- 错误级别的日志 -->
|
||||||
|
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_error.log</file>
|
||||||
|
<!--设置文件超出最大容量后的处理方式为根据时间新建一个文件-->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<!--设置文件的最大容量-->
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<!--设置文件的过期时间,30天-->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>3GB</totalSizeCap>
|
||||||
|
</rollingPolicy>
|
||||||
|
<!--设置修改日志的方式为追加的方式-->
|
||||||
|
<append>true</append>
|
||||||
|
<!--进行编码-->
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,只记录错误级别的日志,使用LevelFilter进行精确匹配-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>ERROR</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 警告级别的日志 -->
|
||||||
|
<appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_warn.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>3GB</totalSizeCap>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,记录warn及以上级别的日志,使用ThresholdFilter设置阈值-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>warn</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 信息级别 -->
|
||||||
|
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_info.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>3GB</totalSizeCap>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,记录info及以上级别的日志,使用ThresholdFilter设置阈值-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 日志输出到控制台 -->
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
<!--设置当前项目的日志级别,com.zky下的所有日志级别设置为debug模式-->
|
||||||
|
<logger name="com.baomidou.mybatisplus" level="info"/>
|
||||||
|
<logger name="com.seer.teach" level="info"/>
|
||||||
|
<logger name="io.lettuce" level="info"/>
|
||||||
|
<logger name="org.mybatis" level="info"/>
|
||||||
|
<logger name="org.apache.ibatis" level="info"/>
|
||||||
|
<logger name="org.springframework.data.redis" level="info"/>
|
||||||
|
<logger name="com.alibaba.nacos.shaded.io.grpc" level="info"/>
|
||||||
|
<logger name="com.alibaba.nacos.client.naming" level="WARN"/>
|
||||||
|
<logger name="org.apache.http.impl.conn" level="info"/>
|
||||||
|
|
||||||
|
<!--将整个项目的日志设置为info级别-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="FILE_ERROR"/>
|
||||||
|
<appender-ref ref="FILE_WARN"/>
|
||||||
|
<appender-ref ref="FILE_INFO"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
100
seer-admin/src/main/resources/logback-local.xml
Normal file
100
seer-admin/src/main/resources/logback-local.xml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Windows-->
|
||||||
|
<configuration>
|
||||||
|
<contextName>community</contextName>
|
||||||
|
<!--设置日志所在的目录-->
|
||||||
|
<property name="LOG_PATH" value="/app/java/seer-admin/logs"/>
|
||||||
|
<!--设置当前项目的名字。一般放在data目录下还会有其它项目的日志文件,所以需要设置一个-->
|
||||||
|
<property name="Logging" value="seer-admin"/>
|
||||||
|
<property name="CONSOLE_LOG_PATTERN" value="%red(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}) [%file:%line] %cyan(%msg%n)"/>
|
||||||
|
|
||||||
|
<!-- 错误级别的日志 -->
|
||||||
|
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_error.log</file>
|
||||||
|
<!--设置文件超出最大容量后的处理方式为根据时间新建一个文件-->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<!--设置文件的最大容量-->
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<!--设置文件的过期时间,30天-->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<!--设置修改日志的方式为追加的方式-->
|
||||||
|
<append>true</append>
|
||||||
|
<!--进行编码-->
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,如果是error级别的信息,则接受,否则拒绝-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>error</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 警告级别的日志 -->
|
||||||
|
<appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_warn.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>warn</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 信息级别 -->
|
||||||
|
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_info.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 日志输出到控制台 -->
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
<!--设置当前项目的日志级别,com.zky下的所有日志级别设置为debug模式-->
|
||||||
|
<logger name="com.baomidou.mybatisplus" level="info"/>
|
||||||
|
<logger name="com.seer.teach" level="info"/>
|
||||||
|
<logger name="io.lettuce" level="info"/>
|
||||||
|
<logger name="org.mybatis" level="info"/>
|
||||||
|
<logger name="org.apache.ibatis" level="info"/>
|
||||||
|
<logger name="org.springframework.data.redis" level="info"/>
|
||||||
|
<logger name="com.alibaba.nacos.shaded.io.grpc" level="info"/>
|
||||||
|
<logger name="com.alibaba.nacos.client.naming" level="WARN"/>
|
||||||
|
<logger name="org.apache.http.impl.conn" level="info"/>
|
||||||
|
|
||||||
|
<!--将整个项目的日志设置为debug级别-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="FILE_ERROR"/>
|
||||||
|
<appender-ref ref="FILE_WARN"/>
|
||||||
|
<appender-ref ref="FILE_INFO"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
92
seer-admin/src/main/resources/logback-prod.xml
Normal file
92
seer-admin/src/main/resources/logback-prod.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Windows-->
|
||||||
|
<configuration>
|
||||||
|
<contextName>seer-admin</contextName>
|
||||||
|
<!--设置日志所在的目录-->
|
||||||
|
<property name="LOG_PATH" value="/app/java/seer-admin/logs"/>
|
||||||
|
<!--设置当前项目的名字。一般放在data目录下还会有其它项目的日志文件,所以需要设置一个-->
|
||||||
|
<property name="Logging" value="seer-admin"/>
|
||||||
|
|
||||||
|
<!-- 错误级别的日志 -->
|
||||||
|
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_error.log</file>
|
||||||
|
<!--设置文件超出最大容量后的处理方式为根据时间新建一个文件-->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<!--设置文件的最大容量-->
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<!--设置文件的过期时间,30天-->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<!--设置修改日志的方式为追加的方式-->
|
||||||
|
<append>true</append>
|
||||||
|
<!--进行编码-->
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,如果是error级别的信息,则接受,否则拒绝-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>error</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 警告级别的日志 -->
|
||||||
|
<appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_warn.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>warn</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 信息级别 -->
|
||||||
|
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_info.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 日志输出到控制台 -->
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>debug</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
<!--设置当前项目的日志级别,com.zky下的所有日志级别设置为debug模式-->
|
||||||
|
<logger name="com.baomidou.mybatisplus" level="debug"/>
|
||||||
|
<logger name="com.seer.teach" level="debug"/>
|
||||||
|
<logger name="com.alibaba.nacos.client.naming" level="WARN"/>
|
||||||
|
<!--将整个项目的日志设置为debug级别-->
|
||||||
|
<root level="debug">
|
||||||
|
<appender-ref ref="FILE_ERROR"/>
|
||||||
|
<appender-ref ref="FILE_WARN"/>
|
||||||
|
<appender-ref ref="FILE_INFO"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
92
seer-admin/src/main/resources/logback-tested.xml
Normal file
92
seer-admin/src/main/resources/logback-tested.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Windows-->
|
||||||
|
<configuration>
|
||||||
|
<contextName>community</contextName>
|
||||||
|
<!--设置日志所在的目录-->
|
||||||
|
<property name="LOG_PATH" value="/app/java/seer-admin/logs"/>
|
||||||
|
<!--设置当前项目的名字。一般放在data目录下还会有其它项目的日志文件,所以需要设置一个-->
|
||||||
|
<property name="Logging" value="seer-admin"/>
|
||||||
|
|
||||||
|
<!-- 错误级别的日志 -->
|
||||||
|
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_error.log</file>
|
||||||
|
<!--设置文件超出最大容量后的处理方式为根据时间新建一个文件-->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<!--设置文件的最大容量-->
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<!--设置文件的过期时间,30天-->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<!--设置修改日志的方式为追加的方式-->
|
||||||
|
<append>true</append>
|
||||||
|
<!--进行编码-->
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<!--设置过滤器,如果是error级别的信息,则接受,否则拒绝-->
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>error</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 警告级别的日志 -->
|
||||||
|
<appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_warn.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>warn</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 信息级别 -->
|
||||||
|
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/log_info.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>50MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>info</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 日志输出到控制台 -->
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>debug</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
<!--设置当前项目的日志级别,com.zky下的所有日志级别设置为debug模式-->
|
||||||
|
<logger name="com.baomidou.mybatisplus" level="debug"/>
|
||||||
|
<logger name="com.seer.teach" level="debug"/>
|
||||||
|
<logger name="com.alibaba.nacos.client.naming" level="WARN"/>
|
||||||
|
<!--将整个项目的日志设置为debug级别-->
|
||||||
|
<root level="debug">
|
||||||
|
<appender-ref ref="FILE_ERROR"/>
|
||||||
|
<appender-ref ref="FILE_WARN"/>
|
||||||
|
<appender-ref ref="FILE_INFO"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
3
seer-admin/src/main/resources/mapstruct.properties
Normal file
3
seer-admin/src/main/resources/mapstruct.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
shenmapstruct.unmappedTargetPolicy=IGNORE
|
||||||
|
mapstruct.unmappedSourcePolicy=IGNORE
|
||||||
|
mapstruct.nullValuePropertyMappingStrategy=IGNORE
|
||||||
22
seer-common/common-auth-scan/pom.xml
Normal file
22
seer-common/common-auth-scan/pom.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-auth-scan</artifactId>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.seer.teach.common.auth.controller;
|
||||||
|
|
||||||
|
import com.seer.teach.common.auth.dto.PermissionDTO;
|
||||||
|
import com.seer.teach.common.auth.service.PermissionScannerService;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "权限扫描")
|
||||||
|
@RequestMapping("/internal")
|
||||||
|
public class PermissionScanController {
|
||||||
|
|
||||||
|
private final PermissionScannerService permissionScannerService;
|
||||||
|
|
||||||
|
@Operation(summary = "扫描权限注解")
|
||||||
|
@GetMapping("/permissions")
|
||||||
|
public List<PermissionDTO> scanPermissions() {
|
||||||
|
return permissionScannerService.scanPermissionAnnotations();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.seer.teach.common.auth.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class PermissionDTO {
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private String module;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
private List<ApiItem> apiItems;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ApiItem {
|
||||||
|
private String title;
|
||||||
|
private String className;
|
||||||
|
private String authorityCode;
|
||||||
|
private String module;
|
||||||
|
private String httpMethod;
|
||||||
|
private String apiPath;
|
||||||
|
private String methodName;
|
||||||
|
private Integer type;
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
package com.seer.teach.common.auth.service;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.seer.teach.common.auth.dto.PermissionDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class PermissionScannerService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
public List<PermissionDTO> scanPermissionAnnotations() {
|
||||||
|
String contextPath = environment.getProperty("server.servlet.context-path", "");
|
||||||
|
String module = environment.getProperty("spring.application.name");
|
||||||
|
contextPath = StringUtils.hasText(contextPath) ? contextPath : "";
|
||||||
|
Map<String, Object> controllers = applicationContext.getBeansWithAnnotation(RestController.class);
|
||||||
|
List<PermissionDTO> permissions = new ArrayList<>();
|
||||||
|
for (Object controller : controllers.values()) {
|
||||||
|
Class<?> clazz = AopUtils.getTargetClass(controller);
|
||||||
|
Optional<PermissionDTO> permission = scanMethodAnnotations(clazz,contextPath,module);
|
||||||
|
permission.ifPresent(permissions::add);
|
||||||
|
}
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<PermissionDTO> scanMethodAnnotations(Class<?> clazz,String contextPath,String module) {
|
||||||
|
if(!clazz.isAnnotationPresent(Tag.class)){
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String tagName = clazz.getAnnotation(Tag.class).name();
|
||||||
|
if( !StringUtils.hasText(tagName)){
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
PermissionDTO permission = new PermissionDTO();
|
||||||
|
permission.setModule(module);
|
||||||
|
permission.setTitle(tagName);
|
||||||
|
permission.setClassName(clazz.getName());
|
||||||
|
permission.setType(2);
|
||||||
|
List<PermissionDTO.ApiItem> apiItems = new ArrayList<>();
|
||||||
|
for (Method method : clazz.getDeclaredMethods()) {
|
||||||
|
if (method.isAnnotationPresent(SaCheckPermission.class)) {
|
||||||
|
SaCheckPermission annotation = method.getAnnotation(SaCheckPermission.class);
|
||||||
|
if (Objects.isNull(annotation)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String perm : annotation.value()) {
|
||||||
|
log.info("permission: {}", perm);
|
||||||
|
if(StringUtils.hasText(perm)){
|
||||||
|
PermissionDTO.ApiItem apiItem = new PermissionDTO.ApiItem();
|
||||||
|
apiItem.setModule(module);
|
||||||
|
apiItem.setTitle(extractInterfaceName(method));
|
||||||
|
apiItem.setAuthorityCode(perm);
|
||||||
|
apiItem.setHttpMethod(getHttpMethod(method));
|
||||||
|
apiItem.setApiPath(extractApiPath(clazz,contextPath, method));
|
||||||
|
apiItem.setClassName(clazz.getName());
|
||||||
|
apiItem.setMethodName(method.getName());
|
||||||
|
apiItem.setType(3);
|
||||||
|
apiItems.add(apiItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permission.setApiItems(apiItems);
|
||||||
|
return Optional.of(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractApiPath(Class<?> clazz,String contextPath, Method method) {
|
||||||
|
String basePath = contextPath;
|
||||||
|
RequestMapping annotation = clazz.getAnnotation(RequestMapping.class);
|
||||||
|
if (Objects.nonNull(annotation)) {
|
||||||
|
basePath += annotation.value()[0];
|
||||||
|
}
|
||||||
|
String methodMapping = getHttpMethodPath(method);
|
||||||
|
log.info("basePath: {}, methodMapping: {}", basePath, methodMapping);
|
||||||
|
return basePath + methodMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractInterfaceName(Method method) {
|
||||||
|
Operation annotation = method.getAnnotation(Operation.class);
|
||||||
|
if (Objects.nonNull(annotation)) {
|
||||||
|
return annotation.summary();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getHttpMethod(Method method) {
|
||||||
|
if (method.isAnnotationPresent(GetMapping.class)) return "get";
|
||||||
|
if (method.isAnnotationPresent(PostMapping.class)) return "post";
|
||||||
|
if (method.isAnnotationPresent(PutMapping.class)) return "put";
|
||||||
|
if (method.isAnnotationPresent(DeleteMapping.class)) return "delete";
|
||||||
|
return "ANY";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getHttpMethodPath(Method method) {
|
||||||
|
if (method.isAnnotationPresent(GetMapping.class) && method.getAnnotation(GetMapping.class).value().length > 0) {
|
||||||
|
return method.getAnnotation(GetMapping.class).value()[0];
|
||||||
|
}
|
||||||
|
if (method.isAnnotationPresent(PostMapping.class) && method.getAnnotation(PostMapping.class).value().length > 0) {
|
||||||
|
return method.getAnnotation(PostMapping.class).value()[0];
|
||||||
|
}
|
||||||
|
if (method.isAnnotationPresent(PutMapping.class) && method.getAnnotation(PutMapping.class).value().length > 0) {
|
||||||
|
return method.getAnnotation(PutMapping.class).value()[0];
|
||||||
|
}
|
||||||
|
if (method.isAnnotationPresent(DeleteMapping.class) && method.getAnnotation(DeleteMapping.class).value().length > 0) {
|
||||||
|
return method.getAnnotation(DeleteMapping.class).value()[0];
|
||||||
|
}
|
||||||
|
if (method.isAnnotationPresent(RequestMapping.class) && method.getAnnotation(RequestMapping.class).value().length > 0) {
|
||||||
|
return method.getAnnotation(RequestMapping.class).value()[0];
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
66
seer-common/common-cache/pom.xml
Normal file
66
seer-common/common-cache/pom.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-cache</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-dto</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alicp.jetcache</groupId>
|
||||||
|
<artifactId>jetcache-anno</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alicp.jetcache</groupId>
|
||||||
|
<artifactId>jetcache-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alicp.jetcache</groupId>
|
||||||
|
<artifactId>jetcache-redis-springdata</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.esotericsoftware</groupId>
|
||||||
|
<artifactId>kryo</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>${basedir}/src/main/resources</directory>
|
||||||
|
<includes>
|
||||||
|
<include>**.yaml</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
89
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/DeviceMetricsCache.java
vendored
Normal file
89
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/DeviceMetricsCache.java
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.seer.teach.common.cache.dto.DeviceMetricDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备指标信息缓存(仅 Redis 操作版本)。
|
||||||
|
* 通过 Redis Hash 存储设备在线信息。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class DeviceMetricsCache {
|
||||||
|
|
||||||
|
/** Redis 主 key */
|
||||||
|
private static final String DEVICE_ONLINE_KEY = "deviceOnline";
|
||||||
|
|
||||||
|
/** 缓存有效期(默认 10 分钟) */
|
||||||
|
private static final Duration L2_TTL = Duration.ofMinutes(10);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定设备的指标信息。
|
||||||
|
* 直接从 Redis 获取。
|
||||||
|
*
|
||||||
|
* @param deviceKey 设备唯一标识
|
||||||
|
* @return 设备指标信息,未找到返回 null
|
||||||
|
*/
|
||||||
|
public DeviceMetricDTO get(String deviceKey) {
|
||||||
|
HashOperations<String, String, DeviceMetricDTO> ops = redisTemplate.opsForHash();
|
||||||
|
return ops.get(DEVICE_ONLINE_KEY, deviceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将设备指标信息写入 Redis。
|
||||||
|
*
|
||||||
|
* @param deviceKey 设备唯一标识
|
||||||
|
* @param dto 设备指标数据
|
||||||
|
*/
|
||||||
|
public void put(String deviceKey, DeviceMetricDTO dto) {
|
||||||
|
HashOperations<String, String, DeviceMetricDTO> ops = redisTemplate.opsForHash();
|
||||||
|
ops.put(DEVICE_ONLINE_KEY, deviceKey, dto);
|
||||||
|
redisTemplate.expire(DEVICE_ONLINE_KEY, L2_TTL.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定设备的缓存数据。
|
||||||
|
*
|
||||||
|
* @param deviceKey 设备唯一标识
|
||||||
|
*/
|
||||||
|
public void invalidate(String deviceKey) {
|
||||||
|
redisTemplate.opsForHash().delete(DEVICE_ONLINE_KEY, deviceKey);
|
||||||
|
log.debug("从 Redis 删除设备缓存: {}", deviceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有在线设备的指标信息。
|
||||||
|
*
|
||||||
|
* @return 所有在线设备的指标映射表
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<String, DeviceMetricDTO> getAllOnline() {
|
||||||
|
Map<Object, Object> all = redisTemplate.opsForHash().entries(DEVICE_ONLINE_KEY);
|
||||||
|
return Optional.ofNullable((Map<String, DeviceMetricDTO>) (Object) all)
|
||||||
|
.orElse(Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定设备是否存在于 Redis 中。
|
||||||
|
*
|
||||||
|
* @param deviceKey 设备唯一标识
|
||||||
|
* @return true=存在,false=不存在
|
||||||
|
*/
|
||||||
|
public boolean exists(String deviceKey) {
|
||||||
|
Boolean result = redisTemplate.opsForHash().hasKey(DEVICE_ONLINE_KEY, deviceKey);
|
||||||
|
return Boolean.TRUE.equals(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.seer.teach.common.cache.dto.DeviceMetricDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class DeviceMetricsCacheService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DeviceMetricsCache deviceMetricsCache;
|
||||||
|
|
||||||
|
/** 上报设备指标 */
|
||||||
|
public void reportMetric(String deviceKey, DeviceMetricDTO dto) {
|
||||||
|
deviceMetricsCache.put(deviceKey, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取最新指标 */
|
||||||
|
public DeviceMetricDTO getLatestMetric(String deviceKey) {
|
||||||
|
return deviceMetricsCache.get(deviceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除设备缓存 */
|
||||||
|
public void invalidateMetric(String deviceKey) {
|
||||||
|
deviceMetricsCache.invalidate(deviceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取全部在线设备 */
|
||||||
|
public Map<String, DeviceMetricDTO> getAllOnlineDevices() {
|
||||||
|
return deviceMetricsCache.getAllOnline();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/LlmAudioPublisher.java
vendored
Normal file
38
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/LlmAudioPublisher.java
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.LlmChatDTO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 发布器(Publisher)
|
||||||
|
* 将 LlmChatDTO 消息发布到指定设备频道
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LlmAudioPublisher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis频道前缀
|
||||||
|
*/
|
||||||
|
private static final String CHANNEL_PREFIX = "llmAudioChannel:";
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
public static String getChannel(String channelName) {
|
||||||
|
return CHANNEL_PREFIX + channelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布音频消息到指定设备频道
|
||||||
|
*
|
||||||
|
* @param channelName 队列名称
|
||||||
|
* @param message 消息对象
|
||||||
|
*/
|
||||||
|
public void publish(String channelName, LlmChatDTO message) {
|
||||||
|
redisTemplate.convertAndSend(channelName, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/LlmAudioPublisherService.java
vendored
Normal file
40
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/LlmAudioPublisherService.java
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.LlmChatDTO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LlmChat 发布服务
|
||||||
|
* 对外提供发布音频消息的接口
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LlmAudioPublisherService {
|
||||||
|
|
||||||
|
private final LlmAudioPublisher llmAudioPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布音频消息到指定设备频道
|
||||||
|
*
|
||||||
|
* @param device 设备标识
|
||||||
|
* @param message 消息对象
|
||||||
|
*/
|
||||||
|
public void publishToDevice(String channelName, LlmChatDTO message) {
|
||||||
|
llmAudioPublisher.publish(channelName, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布音频消息到多个设备
|
||||||
|
*
|
||||||
|
* @param devices 设备列表
|
||||||
|
* @param message 消息对象
|
||||||
|
*/
|
||||||
|
public void publishToDevices(Iterable<String> channelNames, LlmChatDTO message) {
|
||||||
|
for (String channelName : channelNames) {
|
||||||
|
publishToDevice(channelName, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseDataCache.java
vendored
Normal file
85
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseDataCache.java
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.ListOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 数据缓存(Redis 版本)。
|
||||||
|
* 以 sessionId 为 Key,List<Object> 为 Value。
|
||||||
|
* 每个 SSE 会话缓存对应的推送数据。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SseDataCache {
|
||||||
|
|
||||||
|
/** Redis key 前缀 */
|
||||||
|
private static final String SSE_DATA_KEY_PREFIX = "sseData:";
|
||||||
|
|
||||||
|
/** 每个会话缓存有效期(默认 30 分钟) */
|
||||||
|
private static final Duration SSE_SESSION_TTL = Duration.ofMinutes(30);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Redis 中完整的 SSE 数据列表。
|
||||||
|
*
|
||||||
|
* @param sessionId SSE 会话 ID
|
||||||
|
* @return 数据列表(可能为空)
|
||||||
|
*/
|
||||||
|
public List<Object> getAll(Integer sessionId) {
|
||||||
|
String key = buildKey(sessionId);
|
||||||
|
ListOperations<String, Object> ops = redisTemplate.opsForList();
|
||||||
|
List<Object> list = ops.range(key, 0, -1);
|
||||||
|
return list != null ? list : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向 SSE 缓存中追加一条数据。
|
||||||
|
*
|
||||||
|
* @param sessionId SSE 会话 ID
|
||||||
|
* @param data SSE 数据对象
|
||||||
|
*/
|
||||||
|
public void add(Integer sessionId, Object data) {
|
||||||
|
String key = buildKey(sessionId);
|
||||||
|
ListOperations<String, Object> ops = redisTemplate.opsForList();
|
||||||
|
ops.rightPush(key, data);
|
||||||
|
redisTemplate.expire(key, SSE_SESSION_TTL.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定 SSE 会话的所有缓存数据。
|
||||||
|
*
|
||||||
|
* @param sessionId SSE 会话 ID
|
||||||
|
*/
|
||||||
|
public void invalidate(Integer sessionId) {
|
||||||
|
String key = buildKey(sessionId);
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
log.debug("已清理 SSE 缓存: sessionId={}", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定 SSE 会话是否存在缓存数据。
|
||||||
|
*
|
||||||
|
* @param sessionId SSE 会话 ID
|
||||||
|
* @return true=存在,false=不存在
|
||||||
|
*/
|
||||||
|
public boolean exists(Integer sessionId) {
|
||||||
|
String key = buildKey(sessionId);
|
||||||
|
Boolean result = redisTemplate.hasKey(key);
|
||||||
|
return Boolean.TRUE.equals(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建 Redis key */
|
||||||
|
private String buildKey(Integer sessionId) {
|
||||||
|
return SSE_DATA_KEY_PREFIX + sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseDataCacheService.java
vendored
Normal file
39
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseDataCacheService.java
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 数据缓存业务封装。
|
||||||
|
* 对外提供更高层的缓存操作接口。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class SseDataCacheService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SseDataCache sseDataCache;
|
||||||
|
|
||||||
|
/** 缓存追加一条 SSE 数据 */
|
||||||
|
public void appendSseData(Integer sessionId, Object data) {
|
||||||
|
sseDataCache.add(sessionId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定会话的所有缓存数据 */
|
||||||
|
public List<Object> getSseData(Integer sessionId) {
|
||||||
|
return sseDataCache.getAll(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除指定会话缓存 */
|
||||||
|
public void clearSseData(Integer sessionId) {
|
||||||
|
sseDataCache.invalidate(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断会话缓存是否存在 */
|
||||||
|
public boolean exists(Integer sessionId) {
|
||||||
|
return sseDataCache.exists(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
195
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseLifeCycleCache.java
vendored
Normal file
195
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseLifeCycleCache.java
vendored
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 生命周期缓存管理类,提供两级缓存(L1 本地缓存 + L2 Redis 缓存)支持。
|
||||||
|
* L1 使用 Caffeine 实现,分为正向缓存和负向缓存;L2 使用 Redis。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SseLifeCycleCache {
|
||||||
|
|
||||||
|
/** 一级缓存(L1)过期时间:30分钟 */
|
||||||
|
private static final Duration L1_TTL = Duration.ofMinutes(30);
|
||||||
|
|
||||||
|
/** 二级缓存(L2 Redis)过期时间:30分钟 */
|
||||||
|
private static final Duration L2_TTL = Duration.ofMinutes(30);
|
||||||
|
|
||||||
|
/** 负向缓存过期时间:30秒,用于防止缓存穿透 */
|
||||||
|
private static final Duration L1_NEGATIVE_TTL = Duration.ofSeconds(30);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
/** 一级正向缓存(本地) */
|
||||||
|
private final Cache<String, Integer> l1;
|
||||||
|
|
||||||
|
/** 一级负向缓存(本地,用于记录空值防止穿透) */
|
||||||
|
private final Cache<String, Boolean> l1Negative;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数,初始化本地缓存实例。
|
||||||
|
*
|
||||||
|
* @param redisTemplate Redis 操作模板
|
||||||
|
*/
|
||||||
|
public SseLifeCycleCache(RedisTemplate<String, Object> redisTemplate) {
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
|
||||||
|
// 初始化 L1 正向缓存:最大容量 50000,写入后 30 分钟过期
|
||||||
|
this.l1 = Caffeine.newBuilder()
|
||||||
|
.maximumSize(50_000)
|
||||||
|
.expireAfterWrite(L1_TTL)
|
||||||
|
.recordStats()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 初始化 L1 负向缓存:最大容量 100000,写入后 30 秒过期
|
||||||
|
this.l1Negative = Caffeine.newBuilder()
|
||||||
|
.maximumSize(100_000)
|
||||||
|
.expireAfterWrite(L1_NEGATIVE_TTL)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存值,优先从 L1 缓存获取,未命中则查询 Redis。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 缓存值,若不存在或已失效则返回 null
|
||||||
|
*/
|
||||||
|
public Integer get(String prefix, Integer userId) {
|
||||||
|
String key = redisKey(prefix, userId);
|
||||||
|
|
||||||
|
// 如果在负向缓存中存在,则直接返回 null
|
||||||
|
if (Boolean.TRUE.equals(l1Negative.getIfPresent(key))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从 L1 正向缓存获取
|
||||||
|
Integer local = l1.getIfPresent(key);
|
||||||
|
if (local != null) {
|
||||||
|
log.debug("[SseLifeCycleCache] 命中 L1, prefix={}, userId={}, value={}", prefix, userId, local);
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L1 未命中,尝试从 Redis 获取
|
||||||
|
Integer fromRedis = loadFromRedis(key);
|
||||||
|
if (fromRedis != null) {
|
||||||
|
log.debug("[SseLifeCycleCache] 命中 L2, prefix={}, userId={}, value={}", prefix, userId, fromRedis);
|
||||||
|
l1.put(key, fromRedis);
|
||||||
|
return fromRedis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 也未命中,标记为负向缓存
|
||||||
|
l1Negative.put(key, Boolean.TRUE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入缓存值到 L1 和 Redis。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param value 要写入的值,若为 null 则表示删除该缓存项
|
||||||
|
*/
|
||||||
|
public void put(String prefix, Integer userId, Integer value) {
|
||||||
|
String key = redisKey(prefix, userId);
|
||||||
|
// 清除负向缓存中的记录
|
||||||
|
l1Negative.invalidate(key);
|
||||||
|
|
||||||
|
// 写入 Redis
|
||||||
|
writeToRedis(key, value, L2_TTL);
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
// 值有效时写入 L1 正向缓存
|
||||||
|
l1.put(key, value);
|
||||||
|
} else {
|
||||||
|
// 值无效时清除 L1 正向缓存并标记负向缓存
|
||||||
|
l1.invalidate(key);
|
||||||
|
l1Negative.put(key, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定缓存项,包括 L1 和 Redis 中的数据。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
*/
|
||||||
|
public void invalidate(String prefix, Integer userId) {
|
||||||
|
String key = redisKey(prefix, userId);
|
||||||
|
// 清除 L1 正向和负向缓存
|
||||||
|
l1.invalidate(key);
|
||||||
|
l1Negative.invalidate(key);
|
||||||
|
// 删除 Redis 中的缓存项
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接从 Redis 中读取缓存值,不经过 L1 缓存。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return Redis 中的缓存值,若不存在则返回 null
|
||||||
|
*/
|
||||||
|
public Integer peekL2(String prefix, Integer userId) {
|
||||||
|
return loadFromRedis(redisKey(prefix, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接从 L1 缓存中读取缓存值,不经过 Redis。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return L1 缓存中的值,若不存在则返回 null
|
||||||
|
*/
|
||||||
|
public Integer peekL1(String prefix, Integer userId) {
|
||||||
|
return l1.getIfPresent(redisKey(prefix, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Redis 中加载指定键的值。
|
||||||
|
*
|
||||||
|
* @param key Redis 键
|
||||||
|
* @return 若值为 Integer 类型则返回,否则返回 null
|
||||||
|
*/
|
||||||
|
private Integer loadFromRedis(String key) {
|
||||||
|
Object raw = redisTemplate.opsForValue().get(key);
|
||||||
|
if (raw instanceof Integer) {
|
||||||
|
return (Integer) raw;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向 Redis 写入指定键值对,并设置过期时间。
|
||||||
|
*
|
||||||
|
* @param key Redis 键
|
||||||
|
* @param value 要写入的值
|
||||||
|
* @param ttl 过期时间,若为 null 则使用默认 L2_TTL
|
||||||
|
*/
|
||||||
|
private void writeToRedis(String key, Integer value, Duration ttl) {
|
||||||
|
long millis = Optional.ofNullable(ttl).orElse(L2_TTL).toMillis();
|
||||||
|
redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 Redis 缓存键。
|
||||||
|
*
|
||||||
|
* @param prefix 缓存键前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 完整的 Redis 键
|
||||||
|
*/
|
||||||
|
private String redisKey(String prefix, Integer userId) {
|
||||||
|
return prefix + userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseLifeCycleCacheService.java
vendored
Normal file
89
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/SseLifeCycleCacheService.java
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User SSE 缓存 Service 层
|
||||||
|
* <p>
|
||||||
|
* 作用:
|
||||||
|
* - 统一封装 UserSseCache 的操作
|
||||||
|
* - 业务层调用时,不直接依赖 UserSseCache 工具类
|
||||||
|
* - 方便后续扩展(例如埋点统计、AOP日志等)
|
||||||
|
* <p>
|
||||||
|
* 用法示例:
|
||||||
|
* userSseCacheService.saveSession("SSE_CHAT:", userId, sessionId);
|
||||||
|
* Integer session = userSseCacheService.getSession("SSE_CHAT:", userId);
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-09-28
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SseLifeCycleCacheService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI聊天Redis键前缀
|
||||||
|
*/
|
||||||
|
public static final String AI_CHAT_REDIS_KEY_PREFIX = "sseChatLifeCycle:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI批改Redis键前缀
|
||||||
|
*/
|
||||||
|
public static final String AI_CORRECT_REDIS_KEY_PREFIX = "sseCorrectLifeCycle:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI学习Redis键前缀
|
||||||
|
*/
|
||||||
|
public static final String AI_STUDY_REDIS_KEY_PREFIX = "sseStudyLifeCycle:";
|
||||||
|
|
||||||
|
private final SseLifeCycleCache sseLifeCycleCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的 SSE 缓存值(优先 L1 -> L2)
|
||||||
|
*
|
||||||
|
* @param prefix Redis Key 前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 缓存值(可能为 null)
|
||||||
|
*/
|
||||||
|
public Integer getSession(String prefix, Integer userId) {
|
||||||
|
return sseLifeCycleCache.get(prefix, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存用户 SSE 缓存(先写 L2 -> 再写 L1)
|
||||||
|
*
|
||||||
|
* @param prefix Redis Key 前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
*/
|
||||||
|
public void saveSession(String prefix, Integer userId, Integer sessionId) {
|
||||||
|
sseLifeCycleCache.put(prefix, userId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户 SSE 缓存(双删:L1 + L2)
|
||||||
|
*
|
||||||
|
* @param prefix Redis Key 前缀
|
||||||
|
* @param userId 用户 ID
|
||||||
|
*/
|
||||||
|
public void removeSession(String prefix, Integer userId) {
|
||||||
|
sseLifeCycleCache.invalidate(prefix, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅查 Redis (L2)
|
||||||
|
*/
|
||||||
|
public Integer peekRedis(String prefix, Integer userId) {
|
||||||
|
return sseLifeCycleCache.peekL2(prefix, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅查本地缓存 (L1)
|
||||||
|
*/
|
||||||
|
public Integer peekLocal(String prefix, Integer userId) {
|
||||||
|
return sseLifeCycleCache.peekL1(prefix, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/UserStudyStatusCache.java
vendored
Normal file
101
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/UserStudyStatusCache.java
vendored
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.seer.teach.common.cache.dto.UserStudyStatusDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户学习状态缓存
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-12
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class UserStudyStatusCache {
|
||||||
|
|
||||||
|
/** Redis Key 前缀 */
|
||||||
|
private static final String REDIS_PREFIX = "studyStatus:";
|
||||||
|
|
||||||
|
/** 默认过期时间(1小时) */
|
||||||
|
private static final Duration DEFAULT_TTL = Duration.ofHours(1);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
/** 保存用户学习状态(自动过期1小时) */
|
||||||
|
public void put(Integer userId, UserStudyStatusDTO status) {
|
||||||
|
if (userId == null || status == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
String key = REDIS_PREFIX + userId;
|
||||||
|
String json = MAPPER.writeValueAsString(status);
|
||||||
|
redisTemplate.opsForValue().set(key, json, DEFAULT_TTL);
|
||||||
|
log.debug("[UserStudyStatusCache] 保存学习状态 userId={}, value={}", userId, json);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("[UserStudyStatusCache] JSON 序列化失败 userId={}, error={}", userId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户学习状态 */
|
||||||
|
public UserStudyStatusDTO get(Integer userId) {
|
||||||
|
if (userId == null) return null;
|
||||||
|
|
||||||
|
String key = REDIS_PREFIX + userId;
|
||||||
|
Object json = redisTemplate.opsForValue().get(key);
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return MAPPER.readValue(json.toString(), UserStudyStatusDTO.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[UserStudyStatusCache] JSON 解析失败 userId={}, json={}", userId, json);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除用户学习状态 */
|
||||||
|
public void remove(Integer userId) {
|
||||||
|
if (userId == null) return;
|
||||||
|
redisTemplate.delete(REDIS_PREFIX + userId);
|
||||||
|
log.debug("[UserStudyStatusCache] 删除学习状态 userId={}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断用户是否存在学习状态 */
|
||||||
|
public boolean exists(Integer userId) {
|
||||||
|
if (userId == null) return false;
|
||||||
|
return Boolean.TRUE.equals(redisTemplate.hasKey(REDIS_PREFIX + userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有用户学习状态(仅限小规模使用) */
|
||||||
|
public Map<Integer, UserStudyStatusDTO> getAll() {
|
||||||
|
// 注意:若用户数较多,请避免 keys("*"),应维护用户ID列表
|
||||||
|
Map<Integer, UserStudyStatusDTO> result = new ConcurrentHashMap<>();
|
||||||
|
for (String key : redisTemplate.keys(REDIS_PREFIX + "*")) {
|
||||||
|
try {
|
||||||
|
String json = (String) redisTemplate.opsForValue().get(key);
|
||||||
|
if (json != null) {
|
||||||
|
Integer userId = Integer.parseInt(key.substring(REDIS_PREFIX.length()));
|
||||||
|
result.put(userId, MAPPER.readValue(json, UserStudyStatusDTO.class));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空所有学习状态(慎用) */
|
||||||
|
public void clearAll() {
|
||||||
|
for (String key : redisTemplate.keys(REDIS_PREFIX + "*")) {
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
log.warn("[UserStudyStatusCache] 已清空所有学习状态");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package com.seer.teach.common.cache;
|
||||||
|
|
||||||
|
import com.seer.teach.common.cache.dto.UserStudyStatusDTO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户学习状态缓存 Service 层
|
||||||
|
*
|
||||||
|
* 封装 UserStudyStatusCache,提供清晰的业务语义接口:
|
||||||
|
* - startStudy:开始学习并记录状态
|
||||||
|
* - updateStudy:更新当前学习进度
|
||||||
|
* - finishStudy:学习结束,移除状态
|
||||||
|
* - getStudyStatus:获取当前学习状态
|
||||||
|
* - isStudying:是否正在学习
|
||||||
|
* - getAllStudying:获取所有用户学习状态
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-12
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserStudyStatusCacheService {
|
||||||
|
|
||||||
|
private final UserStudyStatusCache userStudyStatusCache;
|
||||||
|
|
||||||
|
/** 标记用户开始学习(存入状态) */
|
||||||
|
public void startStudy(Integer userId, UserStudyStatusDTO status) {
|
||||||
|
userStudyStatusCache.put(userId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新用户学习状态 */
|
||||||
|
public void updateStudy(Integer userId, UserStudyStatusDTO status) {
|
||||||
|
userStudyStatusCache.put(userId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户学习状态 */
|
||||||
|
public UserStudyStatusDTO getStudyStatus(Integer userId) {
|
||||||
|
return userStudyStatusCache.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断用户是否正在学习 */
|
||||||
|
public boolean isStudying(Integer userId) {
|
||||||
|
return userStudyStatusCache.exists(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学习结束,移除状态 */
|
||||||
|
public void finishStudy(Integer userId) {
|
||||||
|
userStudyStatusCache.remove(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有学习状态 */
|
||||||
|
public Map<Integer, UserStudyStatusDTO> getAllStudying() {
|
||||||
|
return userStudyStatusCache.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空所有学习状态 */
|
||||||
|
public void clearAll() {
|
||||||
|
userStudyStatusCache.clearAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/CorrectResultDTO.java
vendored
Normal file
39
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/CorrectResultDTO.java
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package com.seer.teach.common.cache.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class CorrectResultDTO {
|
||||||
|
|
||||||
|
/** 会话ID */
|
||||||
|
private Long sessionId;
|
||||||
|
|
||||||
|
/** 当前块序号或计数 */
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
/** 题目或批改索引ID */
|
||||||
|
private Integer indexId;
|
||||||
|
|
||||||
|
/** 状态编号(如处理中、成功、失败) */
|
||||||
|
private Integer statusId;
|
||||||
|
|
||||||
|
/** 状态文本描述 */
|
||||||
|
private String statusContent;
|
||||||
|
|
||||||
|
/** 批改提示(HTML 格式内容) */
|
||||||
|
private String tip;
|
||||||
|
|
||||||
|
/** 批改结果内容(最终结果) */
|
||||||
|
private String result;
|
||||||
|
|
||||||
|
/** 消息创建时间 */
|
||||||
|
private long mqTime;
|
||||||
|
|
||||||
|
}
|
||||||
44
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/DeviceMetricDTO.java
vendored
Normal file
44
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/DeviceMetricDTO.java
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package com.seer.teach.common.cache.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-26 18:28
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class DeviceMetricDTO {
|
||||||
|
|
||||||
|
private Integer cityId;
|
||||||
|
|
||||||
|
private String deviceIp;
|
||||||
|
|
||||||
|
private String nettyIp;
|
||||||
|
|
||||||
|
private double cpu;
|
||||||
|
|
||||||
|
private double memory;
|
||||||
|
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
public static DeviceMetricDTO get(Integer cityId,String deviceIp,String nettyIp,double cpu,double memory){
|
||||||
|
return new DeviceMetricDTO(cityId,deviceIp,nettyIp,cpu,memory,LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DeviceMetricDTO defaultMetric(Integer cityId,String deviceIp,String nettyIp){
|
||||||
|
return new DeviceMetricDTO(cityId,deviceIp,nettyIp,0,0,LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
47
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/L1CacheDTO.java
vendored
Normal file
47
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/L1CacheDTO.java
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package com.seer.teach.common.cache.dto;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.dto.UserCacheDTO;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L1缓存数据传输对象
|
||||||
|
* <p>
|
||||||
|
* 用于表示一级缓存中的用户数据,包含用户缓存信息和过期时间。
|
||||||
|
* 提供创建缓存对象和检查过期状态的功能。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-16 18:04
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class L1CacheDTO {
|
||||||
|
|
||||||
|
private UserCacheDTO value;
|
||||||
|
|
||||||
|
private long expireAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建L1缓存对象
|
||||||
|
*
|
||||||
|
* @param dto 用户缓存数据传输对象
|
||||||
|
* @param ttl 缓存有效期时长
|
||||||
|
* @return L1缓存对象
|
||||||
|
*/
|
||||||
|
public static L1CacheDTO of(UserCacheDTO dto, Duration ttl) {
|
||||||
|
long exp = System.currentTimeMillis() + (ttl == null ? 0 : ttl.toMillis());
|
||||||
|
return new L1CacheDTO(dto, exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断缓存是否已过期
|
||||||
|
*
|
||||||
|
* @return true表示已过期,false表示未过期
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() > expireAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/ReadingStatusDTO.java
vendored
Normal file
40
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/ReadingStatusDTO.java
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package com.seer.teach.common.cache.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-16 17:20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class ReadingStatusDTO {
|
||||||
|
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
private Integer chapterId;
|
||||||
|
|
||||||
|
// true 读完了 false 没读完
|
||||||
|
private Boolean bookPlayState;
|
||||||
|
|
||||||
|
private Boolean isEnd;
|
||||||
|
|
||||||
|
public static ReadingStatusDTO smartRead(Integer chapterId) {
|
||||||
|
return new ReadingStatusDTO(5, chapterId, false,false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadingStatusDTO readTreasuryBox(Integer articleId) {
|
||||||
|
return new ReadingStatusDTO(6, articleId, false,false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/UserStudyStatusDTO.java
vendored
Normal file
33
seer-common/common-cache/src/main/java/com/seer/teach/common/cache/dto/UserStudyStatusDTO.java
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package com.seer.teach.common.cache.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-12 9:11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class UserStudyStatusDTO {
|
||||||
|
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
private Integer sessionId;
|
||||||
|
|
||||||
|
private Integer knowId;
|
||||||
|
|
||||||
|
private Integer videoIndex;
|
||||||
|
|
||||||
|
private Boolean classOver;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.seer.teach.common.cache.jetcache.config;
|
||||||
|
|
||||||
|
import com.alicp.jetcache.anno.config.EnableMethodCache;
|
||||||
|
import com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration;
|
||||||
|
import com.alicp.jetcache.support.Fastjson2ValueDecoder;
|
||||||
|
import com.alicp.jetcache.support.Fastjson2ValueEncoder;
|
||||||
|
import com.seer.teach.common.cache.support.YamlPropertySourceFactory;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EnableMethodCache(basePackages = "com.seer")
|
||||||
|
@ConditionalOnProperty(name = "jetcache.enable", havingValue = "true", matchIfMissing = true)
|
||||||
|
@PropertySource(
|
||||||
|
value = "classpath:jetcache.yaml",
|
||||||
|
factory = YamlPropertySourceFactory.class
|
||||||
|
)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Configuration
|
||||||
|
public class JetCacheConfig extends JetCacheAutoConfiguration {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.info("JetCacheConfig configuration loaded successfully");
|
||||||
|
log.info("Enabled method cache for base packages: com.seer");
|
||||||
|
log.info("Loading jetcache configuration from: classpath:jetcache.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Fastjson2ValueDecoder fastjson2ValueDecoder() {
|
||||||
|
return new Fastjson2ValueDecoder(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Fastjson2ValueEncoder fastjson2ValueEncoder() {
|
||||||
|
return new Fastjson2ValueEncoder(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.seer.teach.common.cache.support;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
import org.springframework.core.io.support.PropertySourceFactory;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public org.springframework.core.env.PropertySource<?> createPropertySource(@Nullable String name, EncodedResource encodedResource) throws IOException {
|
||||||
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
|
factory.setResources(encodedResource.getResource());
|
||||||
|
Properties properties = factory.getObject();
|
||||||
|
if (properties == null) {
|
||||||
|
throw new IllegalStateException("Failed to load YAML properties from " + encodedResource.getResource());
|
||||||
|
}
|
||||||
|
String sourceName = (name != null) ? name : encodedResource.getResource().getFilename();
|
||||||
|
if (sourceName == null) {
|
||||||
|
throw new IllegalStateException("Resource must have a filename");
|
||||||
|
}
|
||||||
|
return new PropertiesPropertySource(sourceName, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
seer-common/common-cache/src/main/resources/jetcache.yaml
Normal file
36
seer-common/common-cache/src/main/resources/jetcache.yaml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
jetcache:
|
||||||
|
statIntervalMinutes: 15
|
||||||
|
areaInCacheName: false
|
||||||
|
hidePackages: com.alibaba
|
||||||
|
local:
|
||||||
|
default:
|
||||||
|
type: caffeine
|
||||||
|
limit: 100
|
||||||
|
keyConvertor: jackson
|
||||||
|
expireAfterWriteInMillis: 100000
|
||||||
|
otherArea:
|
||||||
|
type: linkedhashmap
|
||||||
|
limit: 100
|
||||||
|
keyConvertor: none
|
||||||
|
expireAfterWriteInMillis: 100000
|
||||||
|
remote:
|
||||||
|
default:
|
||||||
|
type: redis.springdata
|
||||||
|
keyConvertor: jackson
|
||||||
|
valueEncoder: bean:fastjson2ValueEncoder
|
||||||
|
valueDecoder: bean:fastjson2ValueDecoder
|
||||||
|
poolConfig:
|
||||||
|
minIdle: 5
|
||||||
|
maxIdle: 20
|
||||||
|
maxTotal: 50
|
||||||
|
## uri: redis://${spring.data.redis.password}@${spring.data.redis.host}:${spring.data.redis.port}
|
||||||
|
otherArea:
|
||||||
|
type: redis.springdata
|
||||||
|
keyConvertor: jackson
|
||||||
|
valueEncoder: bean:fastjson2ValueEncoder
|
||||||
|
valueDecoder: bean:fastjson2ValueDecoder
|
||||||
|
poolConfig:
|
||||||
|
minIdle: 5
|
||||||
|
maxIdle: 20
|
||||||
|
maxTotal: 50
|
||||||
|
## uri: redis://${spring.data.redis.password}@${spring.data.redis.host}:${spring.data.redis.port}
|
||||||
62
seer-common/common-config/pom.xml
Normal file
62
seer-common/common-config/pom.xml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-config</artifactId>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-exception</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-extension</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-loadbalancer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat.embed</groupId>
|
||||||
|
<artifactId>tomcat-embed-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Autograph: 安稳
|
||||||
|
* @Description: 跨域处理类
|
||||||
|
* @Date: 2023-07-03 16:15:51
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfig {
|
||||||
|
|
||||||
|
public CorsConfig() {
|
||||||
|
log.info("CORS 配置类 CorsConfig已加载");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties(prefix = "zs.cors")
|
||||||
|
public Cors cors() {
|
||||||
|
return new Cors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
public CorsFilter corsFilter(Cors cors) {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
// 设置允许的域名
|
||||||
|
config.setAllowedOriginPatterns(cors.getOrigins());
|
||||||
|
// 允许所有请求头
|
||||||
|
config.addAllowedHeader("*");
|
||||||
|
// 允许所有方法
|
||||||
|
config.addAllowedMethod("*");
|
||||||
|
// 允许携带认证信息
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
return new CorsFilter(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Cors {
|
||||||
|
private List<String> origins = new ArrayList<>();
|
||||||
|
|
||||||
|
public List<String> getOrigins() {
|
||||||
|
return origins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrigins(List<String> origins) {
|
||||||
|
this.origins = origins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||||
|
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||||
|
return builder -> {
|
||||||
|
// 设置日期时间格式
|
||||||
|
builder.simpleDateFormat(DATE_TIME_FORMAT);
|
||||||
|
|
||||||
|
// 设置时区
|
||||||
|
builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
|
||||||
|
|
||||||
|
// 配置 Java 8 时间类型的序列化和反序列化
|
||||||
|
builder.serializers(
|
||||||
|
new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)),
|
||||||
|
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.deserializers(
|
||||||
|
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)),
|
||||||
|
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 其他配置
|
||||||
|
builder.featuresToEnable(
|
||||||
|
SerializationFeature.INDENT_OUTPUT, // 美化输出
|
||||||
|
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS // 禁用时间戳
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.featuresToDisable(
|
||||||
|
SerializationFeature.FAIL_ON_EMPTY_BEANS
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置是否包含 null 值
|
||||||
|
builder.serializationInclusion(JsonInclude.Include.ALWAYS);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Description: Llm配置类
|
||||||
|
* @Date: 2025-05-12 18:42
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(prefix = "largeModel", name = "ip")
|
||||||
|
public class LlmConfig implements InitializingBean {
|
||||||
|
|
||||||
|
@Value("${largeModel.ip:}")
|
||||||
|
private String ip;
|
||||||
|
|
||||||
|
@Value("${largeModel.questionsCorrect:}")
|
||||||
|
private String questionsCorrect;
|
||||||
|
|
||||||
|
@Value("${largeModel.getPlanQuestions:}")
|
||||||
|
private String getPlanQuestions;
|
||||||
|
|
||||||
|
@Value("${largeModel.recommendChapters:}")
|
||||||
|
private String recommendChapters;
|
||||||
|
|
||||||
|
@Value("${largeModel.planCoverImage:}")
|
||||||
|
private String planCoverImage;
|
||||||
|
|
||||||
|
@Value("${largeModel.tts}")
|
||||||
|
private String tts;
|
||||||
|
|
||||||
|
@Value("${largeModel.stt}")
|
||||||
|
private String stt;
|
||||||
|
|
||||||
|
@Value("${largeModel.getComment}")
|
||||||
|
private String getComment;
|
||||||
|
|
||||||
|
@Value("${largeModel.sseAiChat}")
|
||||||
|
private String sseAiChat;
|
||||||
|
|
||||||
|
@Value("${largeModel.sseStudy}")
|
||||||
|
private String sseStudy;
|
||||||
|
|
||||||
|
@Value("${largeModel.sseAiCorrect}")
|
||||||
|
private String sseAiCorrect;
|
||||||
|
|
||||||
|
@Value("${largeModel.ocr}")
|
||||||
|
private String ocr;
|
||||||
|
|
||||||
|
@Value("${largeModel.processUpload}")
|
||||||
|
private String processUpload;
|
||||||
|
|
||||||
|
@Value("${largeModel.getContextAboutSingleQuestion}")
|
||||||
|
private String getContextAboutSingleQuestion;
|
||||||
|
|
||||||
|
@Value("${largeModel.getTextBase64}")
|
||||||
|
private String getTextBase64;
|
||||||
|
|
||||||
|
public static String IP;
|
||||||
|
|
||||||
|
public static String QUESTIONS_CORRECT;
|
||||||
|
|
||||||
|
public static String GET_PLAN_QUESTIONS;
|
||||||
|
|
||||||
|
public static String RECOMMEND_CHAPTERS;
|
||||||
|
|
||||||
|
public static String PLAN_COVER_IMAGE;
|
||||||
|
|
||||||
|
public static String TTS;
|
||||||
|
|
||||||
|
public static String STT;
|
||||||
|
|
||||||
|
public static String GET_COMMENT;
|
||||||
|
|
||||||
|
public static String SSE_AI_CHAT;
|
||||||
|
|
||||||
|
public static String SSE_STUDY;
|
||||||
|
|
||||||
|
public static String SSE_AI_CORRECT;
|
||||||
|
|
||||||
|
public static String OCR;
|
||||||
|
|
||||||
|
public static String PROCESS_UPLOAD;
|
||||||
|
|
||||||
|
public static String GET_CONTEXT_ABOUT_SINGLE_QUESTION;
|
||||||
|
|
||||||
|
public static String GET_TEXT_BASE64;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet(){
|
||||||
|
IP = ip;
|
||||||
|
QUESTIONS_CORRECT = questionsCorrect;
|
||||||
|
RECOMMEND_CHAPTERS = recommendChapters;
|
||||||
|
PLAN_COVER_IMAGE = planCoverImage;
|
||||||
|
TTS = tts;
|
||||||
|
STT = stt;
|
||||||
|
GET_COMMENT = getComment;
|
||||||
|
SSE_AI_CHAT = sseAiChat;
|
||||||
|
SSE_STUDY = sseStudy;
|
||||||
|
SSE_AI_CORRECT = sseAiCorrect;
|
||||||
|
GET_PLAN_QUESTIONS = getPlanQuestions;
|
||||||
|
OCR = ocr;
|
||||||
|
PROCESS_UPLOAD = processUpload;
|
||||||
|
GET_CONTEXT_ABOUT_SINGLE_QUESTION = getContextAboutSingleQuestion;
|
||||||
|
GET_TEXT_BASE64 = getTextBase64;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.DbType;
|
||||||
|
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import com.seer.teach.common.config.mybatis.plugin.MybatisSqlFormatPlugin;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Autograph: 安稳
|
||||||
|
* @Description:
|
||||||
|
* @Date: 2023-07-23 01:31:19
|
||||||
|
*/
|
||||||
|
@EnableTransactionManagement
|
||||||
|
@Configuration // 配置类
|
||||||
|
public class MyBatisPlusConfig {
|
||||||
|
|
||||||
|
@Value("${mybatis.print-sql:true}")
|
||||||
|
private String printSql;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Desc: 把自动注入数据的配置类注入进MyabtisPlus里面
|
||||||
|
* @Params: []
|
||||||
|
* @Return: com.tianyi.config.MyMetaObjectHandler
|
||||||
|
* @Time: 2024/1/22 11:54
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public MyMetaObjectHandler myMetaObjectHandler() {
|
||||||
|
return new MyMetaObjectHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public UpdateTimePlugin updateTimePlugin() {
|
||||||
|
return new UpdateTimePlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MybatisSqlFormatPlugin mybatisSqlFormatPlugin() {
|
||||||
|
MybatisSqlFormatPlugin plugin = new MybatisSqlFormatPlugin();
|
||||||
|
// 设置插件属性,启用SQL打印
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty("printSql", printSql);
|
||||||
|
plugin.setProperties(properties);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐观锁插件
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public MybatisPlusInterceptor mybatisPlusInterceptor(UpdateTimePlugin updateTimePlugin,MybatisSqlFormatPlugin mybatisSqlFormatPlugin) {
|
||||||
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||||
|
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||||
|
interceptor.addInnerInterceptor(updateTimePlugin);
|
||||||
|
interceptor.addInnerInterceptor(mybatisSqlFormatPlugin);
|
||||||
|
return interceptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Autograph: 安稳
|
||||||
|
* @Description:
|
||||||
|
* @Date: 2023-07-25 23:16:15
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
try {
|
||||||
|
Object loginId = StpUtil.getLoginIdDefaultNull();
|
||||||
|
if (loginId != null) {
|
||||||
|
this.setFieldValByName("createBy", loginId.toString(), metaObject);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
try {
|
||||||
|
Object loginId = StpUtil.getLoginIdDefaultNull();
|
||||||
|
if (loginId != null) {
|
||||||
|
this.setFieldValByName("updateBy", loginId.toString(), metaObject);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@RefreshScope
|
||||||
|
public class NacosServiceMetadataRegistrationConfig {
|
||||||
|
|
||||||
|
|
||||||
|
@Value("${server.servlet.context-path:}")
|
||||||
|
private String contextPath;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NacosDiscoveryProperties discoveryProperties;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void registerContextPath() {
|
||||||
|
// 将上下文根添加到服务元数据
|
||||||
|
try {
|
||||||
|
Map<String, String> metadata = discoveryProperties.getMetadata();
|
||||||
|
metadata.put("context-path", contextPath);
|
||||||
|
discoveryProperties.setMetadata(metadata);
|
||||||
|
|
||||||
|
log.info("注册服务上下文根: {}", contextPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("注册服务上下文根失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
log.info("清理Nacos服务元数据注册配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NettyRedis工具类
|
||||||
|
* <p>
|
||||||
|
* NettyRedis工具类
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2025-07-14 20:54
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NettyRedisConfig {
|
||||||
|
|
||||||
|
public static String REDIS_KEY_ONLINE_UV = "im:online:uv:";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||||
|
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一设置 RedisTemplate:key 用字符串,value 用 JSON(Jackson)
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||||
|
// 1) ObjectMapper
|
||||||
|
BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
|
||||||
|
// 允许你自己的业务包
|
||||||
|
.allowIfSubType("com.seer.teach.")
|
||||||
|
// 允许常见 JDK/时间类
|
||||||
|
.allowIfSubType("java.time.")
|
||||||
|
.allowIfSubType("org.springframework.statemachine.")
|
||||||
|
.allowIfSubType("java.util.")
|
||||||
|
// 显式添加UserCacheDTO所在的包路径
|
||||||
|
.allowIfSubType("com.seer.teach.common.dto.mq.dto.UserCacheDTO")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ObjectMapper om = new ObjectMapper()
|
||||||
|
.registerModule(new JavaTimeModule())
|
||||||
|
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
// 2) 开启多态并使用更安全的校验器(关键!)
|
||||||
|
om.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||||
|
|
||||||
|
// 3) 组装模板
|
||||||
|
StringRedisSerializer keySer = new StringRedisSerializer();
|
||||||
|
GenericJackson2JsonRedisSerializer valSer = new GenericJackson2JsonRedisSerializer(om);
|
||||||
|
|
||||||
|
RedisTemplate<String, Object> t = new RedisTemplate<>();
|
||||||
|
t.setConnectionFactory(factory);
|
||||||
|
t.setKeySerializer(keySer);
|
||||||
|
t.setHashKeySerializer(keySer);
|
||||||
|
t.setValueSerializer(valSer);
|
||||||
|
t.setHashValueSerializer(valSer);
|
||||||
|
t.afterPropertiesSet();
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, Executor redisExecutor) {
|
||||||
|
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||||
|
container.setConnectionFactory(connectionFactory);
|
||||||
|
container.setTaskExecutor(redisExecutor);
|
||||||
|
container.start();
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("redisExecutor")
|
||||||
|
public Executor redisExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
// 核心线程数
|
||||||
|
executor.setCorePoolSize(5);
|
||||||
|
// 最大线程数
|
||||||
|
executor.setMaxPoolSize(50);
|
||||||
|
// 队列容量
|
||||||
|
executor.setQueueCapacity(1000);
|
||||||
|
// 线程名前缀
|
||||||
|
executor.setThreadNamePrefix("redis-msg-");
|
||||||
|
// 线程空闲时间(秒)
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ClientHttpRequestFactory clientHttpRequestFactory() {
|
||||||
|
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
factory.setReadTimeout(Duration.ofSeconds(15));
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
restTemplate.setRequestFactory(clientHttpRequestFactory);
|
||||||
|
return restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用线程池配置类
|
||||||
|
* <p>
|
||||||
|
* 提供用于异步任务执行的线程池配置
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class ThreadPoolConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建通用任务执行线程池
|
||||||
|
*
|
||||||
|
* @return 线程池执行器
|
||||||
|
*/
|
||||||
|
@Bean("taskExecutor")
|
||||||
|
public Executor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
// 核心线程数
|
||||||
|
executor.setCorePoolSize(10);
|
||||||
|
// 最大线程数
|
||||||
|
executor.setMaxPoolSize(50);
|
||||||
|
// 队列容量
|
||||||
|
executor.setQueueCapacity(200);
|
||||||
|
// 线程名前缀
|
||||||
|
executor.setThreadNamePrefix("common-task-");
|
||||||
|
// 线程空闲时间(秒)
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
// 拒绝策略:由调用线程处理该任务
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||||
|
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||||
|
import org.apache.ibatis.plugin.*;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-12 14:42
|
||||||
|
*/
|
||||||
|
@Intercepts({
|
||||||
|
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
|
||||||
|
})
|
||||||
|
public class UpdateTimePlugin implements Interceptor, InnerInterceptor {
|
||||||
|
@Override
|
||||||
|
public Object intercept(Invocation invocation) throws Throwable {
|
||||||
|
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
|
||||||
|
|
||||||
|
// 拿到原始 SQL
|
||||||
|
String originalSql = statementHandler.getBoundSql().getSql();
|
||||||
|
|
||||||
|
// 如果是 UPDATE 且没包含 update_time 字段,则拼接
|
||||||
|
if (originalSql.trim().toUpperCase().startsWith("UPDATE")
|
||||||
|
&& !originalSql.toLowerCase().contains("update_time")) {
|
||||||
|
// 替换 SET 为 SET update_time = NOW(),
|
||||||
|
String newSql = originalSql.replaceFirst("(?i)SET", "SET update_time = NOW(),");
|
||||||
|
// 用反射把新SQL写回去
|
||||||
|
java.lang.reflect.Field field = statementHandler.getBoundSql().getClass().getDeclaredField("sql");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(statementHandler.getBoundSql(), newSql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invocation.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object plugin(Object target) {
|
||||||
|
if (target instanceof StatementHandler) {
|
||||||
|
return Plugin.wrap(target, this);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperties(Properties properties) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
package com.seer.teach.common.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Description: 微信小程序配置类
|
||||||
|
* @Date: 2025-05-09 20:52
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class WxConfig implements InitializingBean {
|
||||||
|
@Value("${wx.appId:}")
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
@Value("${wx.secret:}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${wx.getAccessTokenUrl:}")
|
||||||
|
private String getAccessTokenUrl;
|
||||||
|
|
||||||
|
@Value("${wx.getQrCodeUrl:}")
|
||||||
|
private String getQrCodeUrl;
|
||||||
|
|
||||||
|
@Value("${wx.redisAccessKey:}")
|
||||||
|
private String redisAccessKey;
|
||||||
|
|
||||||
|
@Value("${wx.wxLoginUrl:}")
|
||||||
|
private String wxLoginUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户号
|
||||||
|
* sp_mchid是服务商在微信支付侧的唯一身份标识,所有接口调用都必须包含此参数,以便微信支付确认商户的身份。
|
||||||
|
* 当入驻审核成功后,微信支付侧会向商户提供该商户号。开发者需与负责申请商户号的同事联系获取,
|
||||||
|
* 具体操作如下:登录服务商平台,点击【账户中心->个人设置-->个人信息】即可查看服务商商户号。
|
||||||
|
*/
|
||||||
|
@Value("${wx.mchid:}")
|
||||||
|
private String mchid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户私钥文件路径
|
||||||
|
*/
|
||||||
|
@Value("${wx.privateKeyPath:}")
|
||||||
|
private String privateKeyPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户证书序列号
|
||||||
|
*/
|
||||||
|
@Value("${wx.merchant-serial-number:}")
|
||||||
|
private String merchantSerialNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商户APIV3密钥
|
||||||
|
*/
|
||||||
|
@Value("${wx.apiV3Key:}")
|
||||||
|
private String apiV3Key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付异步通知地址
|
||||||
|
*/
|
||||||
|
@Value("${wx.js-pay-notify-url:}")
|
||||||
|
private String jsPayNotifyUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付退款异步通知地址
|
||||||
|
*/
|
||||||
|
@Value("${wx.js-refund-notify-url:}")
|
||||||
|
private String refundNotifyUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付公钥id
|
||||||
|
*/
|
||||||
|
@Value("${wx.public-key-id:PUB_KEY_ID_0117240729862025081100212138000608}")
|
||||||
|
private String publicKeyId;
|
||||||
|
|
||||||
|
@Value("${wx.public-key-path:certs/pub_key.pem}")
|
||||||
|
private String publicKeyPath;
|
||||||
|
/**
|
||||||
|
* 定义公开静态常量
|
||||||
|
*/
|
||||||
|
public static String APP_ID;
|
||||||
|
public static String SECRET;
|
||||||
|
public static String GET_ACCESS_TOKEN_URL;
|
||||||
|
public static String GET_QR_CODE_URL;
|
||||||
|
public static String REDIS_ACCESS_KEY;
|
||||||
|
public static String WX_LOGIN_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付相关
|
||||||
|
*/
|
||||||
|
public static String SP_MCHID;
|
||||||
|
public static String PRIVATE_KEY_PATH;
|
||||||
|
public static String MERCHANT_SERIAL_NUMBER;
|
||||||
|
public static String API_V3_KEY;
|
||||||
|
// 小程序支付回调url
|
||||||
|
public static String PAY_NOTIFY_URL;
|
||||||
|
// 小程序退款回调url
|
||||||
|
public static String REFUND_NOTIFY_URL;
|
||||||
|
|
||||||
|
public static String PUBLIC_KEY_ID;
|
||||||
|
|
||||||
|
public static String PUBLIC_KEY_PATH;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
APP_ID = appId;
|
||||||
|
SECRET = secret;
|
||||||
|
GET_ACCESS_TOKEN_URL = getAccessTokenUrl+ "?appid="+APP_ID+"&secret="+SECRET+"&grant_type=client_credential";
|
||||||
|
GET_QR_CODE_URL = getQrCodeUrl;
|
||||||
|
REDIS_ACCESS_KEY = redisAccessKey;
|
||||||
|
WX_LOGIN_URL = wxLoginUrl + "?appid="+APP_ID+"&secret="+SECRET+"&grant_type=authorization_code"+"&js_code=";
|
||||||
|
SP_MCHID = mchid;
|
||||||
|
PRIVATE_KEY_PATH = privateKeyPath;
|
||||||
|
MERCHANT_SERIAL_NUMBER = merchantSerialNumber;
|
||||||
|
API_V3_KEY = apiV3Key;
|
||||||
|
PAY_NOTIFY_URL = jsPayNotifyUrl;
|
||||||
|
REFUND_NOTIFY_URL = refundNotifyUrl;
|
||||||
|
PUBLIC_KEY_ID = publicKeyId;
|
||||||
|
PUBLIC_KEY_PATH = publicKeyPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package com.seer.teach.common.config.feign;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.seer.teach.common.exception.RemoteCallException;
|
||||||
|
import feign.*;
|
||||||
|
import feign.codec.ErrorDecoder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.*;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* OpenFeign配置类
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-15 13:56
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@EnableFeignClients
|
||||||
|
public class FeignConfig {
|
||||||
|
@Bean
|
||||||
|
public Retryer retryer() {
|
||||||
|
return new Retryer.Default(100, 1000, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ErrorDecoder errorDecoder() {
|
||||||
|
return (methodKey, response) -> new RemoteCallException(
|
||||||
|
response.status(), response.reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("okHttpClient")
|
||||||
|
public okhttp3.OkHttpClient httpClient() {
|
||||||
|
// 创建连接池
|
||||||
|
ConnectionPool connectionPool = new ConnectionPool(20, 5, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
// 创建缓存
|
||||||
|
Cache cache = new Cache(new File("http_cache"), 50 * 1024 * 1024);
|
||||||
|
|
||||||
|
|
||||||
|
return new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.connectionPool(connectionPool)
|
||||||
|
.cache(cache)
|
||||||
|
.addInterceptor(tokenInterceptor())
|
||||||
|
.retryOnConnectionFailure(true) // 自动重试失败连接
|
||||||
|
.followRedirects(true) // 跟随重定向
|
||||||
|
.followSslRedirects(true) // 跟随SSL重定向
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Interceptor tokenInterceptor() {
|
||||||
|
return chain -> {
|
||||||
|
Request originalRequest = chain.request();
|
||||||
|
if(StpUtil.isLogin()){
|
||||||
|
Request newRequest = originalRequest.newBuilder()
|
||||||
|
.header(StpUtil.getTokenName(), StpUtil.getTokenValue())
|
||||||
|
.build();
|
||||||
|
return chain.proceed(newRequest);
|
||||||
|
}
|
||||||
|
return chain.proceed(originalRequest);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Logger feignLogger() {
|
||||||
|
return new FeignFullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Logger.Level feignLoggerLevel() {
|
||||||
|
return Logger.Level.FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.seer.teach.common.config.feign;
|
||||||
|
|
||||||
|
import feign.Logger;
|
||||||
|
import feign.Request;
|
||||||
|
import feign.Response;
|
||||||
|
import feign.Util;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class FeignFullLogger extends Logger {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void log(String configKey, String format, Object... args) {
|
||||||
|
log.info(String.format(methodTag(configKey) + format, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
|
||||||
|
Request request = response.request();
|
||||||
|
String url = request.url();
|
||||||
|
|
||||||
|
String requestBody = "";
|
||||||
|
if (request.body() != null) {
|
||||||
|
String bodyStr = new String(request.body());
|
||||||
|
requestBody = bodyStr.length() > 200 ? bodyStr.substring(0, 200) + "........." : bodyStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
String hostInfo = extractHostFromUrl(url);
|
||||||
|
|
||||||
|
log.info("Feign Request => Method: {}, URL: {}, Host: {}, Headers: {}, Body: {}",
|
||||||
|
request.httpMethod().name(),
|
||||||
|
url,
|
||||||
|
hostInfo,
|
||||||
|
request.headers(),
|
||||||
|
requestBody);
|
||||||
|
|
||||||
|
String responseBody = "";
|
||||||
|
byte[] bodyData = new byte[0];
|
||||||
|
try {
|
||||||
|
if (response.body() != null) {
|
||||||
|
bodyData = Util.toByteArray(response.body().asInputStream());
|
||||||
|
String bodyStr = new String(bodyData);
|
||||||
|
responseBody = bodyStr.length() > 200 ? bodyStr.substring(0, 200) + "........." : bodyStr;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to read response body", e);
|
||||||
|
responseBody = "[Failed to read response body]";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Feign Response <= Status: {}, Headers: {}, Body: {}, Time: {}ms",
|
||||||
|
response.status(),
|
||||||
|
response.headers(),
|
||||||
|
responseBody,
|
||||||
|
elapsedTime);
|
||||||
|
|
||||||
|
return response.toBuilder().body(bodyData).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URL中提取主机信息
|
||||||
|
* @param url 完整URL
|
||||||
|
* @return 主机信息(IP:Port)
|
||||||
|
*/
|
||||||
|
private String extractHostFromUrl(String url) {
|
||||||
|
try {
|
||||||
|
int protocolEnd = url.indexOf("://");
|
||||||
|
if (protocolEnd != -1) {
|
||||||
|
int pathStart = url.indexOf("/", protocolEnd + 3);
|
||||||
|
if (pathStart != -1) {
|
||||||
|
return url.substring(protocolEnd + 3, pathStart);
|
||||||
|
} else {
|
||||||
|
return url.substring(protocolEnd + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to extract host from URL: {}", url, e);
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.seer.teach.common.config.feign;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
|
||||||
|
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
|
||||||
|
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class GlobalFeignConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
|
||||||
|
ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
|
||||||
|
SameIpLoadBalancerClientFactory clientFactory = new SameIpLoadBalancerClientFactory(properties);
|
||||||
|
clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
|
||||||
|
return clientFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
|
||||||
|
package com.seer.teach.common.config.feign;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.cloud.client.ServiceInstance;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.*;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||||
|
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
|
||||||
|
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
|
||||||
|
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SameIpLoadBalancerClient implements ReactorServiceInstanceLoadBalancer {
|
||||||
|
|
||||||
|
private final List<String> localIps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于获取 serviceId 对应的服务实例的列表
|
||||||
|
*/
|
||||||
|
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
|
||||||
|
/**
|
||||||
|
* 需要获取的服务实例名
|
||||||
|
*
|
||||||
|
* 暂时用于打印 logger 日志
|
||||||
|
*/
|
||||||
|
private final String serviceId;
|
||||||
|
/**
|
||||||
|
* 被代理的 ReactiveLoadBalancer 对象
|
||||||
|
*/
|
||||||
|
private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer;
|
||||||
|
|
||||||
|
public SameIpLoadBalancerClient(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer) {
|
||||||
|
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.reactiveLoadBalancer = reactiveLoadBalancer;
|
||||||
|
this.localIps = getAllLocalIps();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Response<ServiceInstance>> choose(Request request) {
|
||||||
|
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
|
||||||
|
return supplier.get(request).next().map(list -> getInstanceResponse(list, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
|
||||||
|
// 如果服务实例为空,则直接返回
|
||||||
|
if (CollUtil.isEmpty(instances)) {
|
||||||
|
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
|
||||||
|
return new EmptyResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ServiceInstance> chooseInstances = filterBySameIp(instances);
|
||||||
|
boolean isSameIpRouting = !CollUtil.isEmpty(chooseInstances) && chooseInstances != instances;
|
||||||
|
|
||||||
|
// 打印负载均衡决策信息
|
||||||
|
logLoadBalanceDecision(instances, chooseInstances, isSameIpRouting,request);
|
||||||
|
|
||||||
|
if (CollUtil.isEmpty(chooseInstances)) {
|
||||||
|
chooseInstances = instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logLoadBalanceDecision(List<ServiceInstance> allInstances,
|
||||||
|
List<ServiceInstance> sameIpInstances,
|
||||||
|
boolean isSameIpRouting, Request request) {
|
||||||
|
if (log.isInfoEnabled()) {
|
||||||
|
// 获取请求URL
|
||||||
|
URI url = extractUrlFromRequest(request);
|
||||||
|
String httpMethod = extractMethodFromRequest(request);
|
||||||
|
|
||||||
|
log.info("[LoadBalancer][{}][{}] 总实例数: {}, 同IP实例数: {}, 选择策略: {}, URL: {}, Method: {}",
|
||||||
|
serviceId, isSameIpRouting ? "同IP路由" : "随机路由", allInstances.size(), sameIpInstances.size(), isSameIpRouting ? "优先同IP实例" : "随机选择实例", url, httpMethod);
|
||||||
|
|
||||||
|
// 打印所有实例信息
|
||||||
|
allInstances.forEach(instance ->
|
||||||
|
log.debug("[LoadBalancer][{}] 实例信息 - Host: {}, Port: {}", serviceId, instance.getHost(), instance.getPort()));
|
||||||
|
|
||||||
|
// 如果有选中的实例,打印请求信息和选中的实例
|
||||||
|
if (!sameIpInstances.isEmpty()) {
|
||||||
|
ServiceInstance chosenInstance = sameIpInstances.get(0);
|
||||||
|
String fullUrl = "http://" + chosenInstance.getHost() + ":" + chosenInstance.getPort() + url.getPath();
|
||||||
|
log.info("[LoadBalancer][{}] 选中实例 - 请求URL: {}", serviceId,fullUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI extractUrlFromRequest(Request request) {
|
||||||
|
try {
|
||||||
|
if (request.getContext() instanceof RequestDataContext) {
|
||||||
|
RequestDataContext requestDataContext = (RequestDataContext)request.getContext();
|
||||||
|
RequestData clientRequest = requestDataContext.getClientRequest();
|
||||||
|
return clientRequest.getUrl();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to extract URL from req", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractMethodFromRequest(Request request) {
|
||||||
|
try {
|
||||||
|
if (request.getContext() instanceof RequestDataContext) {
|
||||||
|
RequestDataContext requestDataContext = (RequestDataContext)request.getContext();
|
||||||
|
RequestData clientRequest = requestDataContext.getClientRequest();
|
||||||
|
return clientRequest.getHttpMethod().toString() ;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to extract HTTP method from req", e);
|
||||||
|
}
|
||||||
|
return "Unknown Method";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<ServiceInstance> filterBySameIp(List<ServiceInstance> instances) {
|
||||||
|
// 查找与消费者IP相同的实例
|
||||||
|
List<ServiceInstance> sameIpInstances = instances.stream()
|
||||||
|
.filter(instance -> localIps.contains(instance.getHost()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!sameIpInstances.isEmpty()) {
|
||||||
|
return sameIpInstances;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getAllLocalIps() {
|
||||||
|
List<String> ipList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface iface = interfaces.nextElement();
|
||||||
|
// 跳过回环接口和未启用的接口
|
||||||
|
if (iface.isLoopback() || !iface.isUp()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Enumeration<InetAddress> addresses = iface.getInetAddresses();
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
InetAddress addr = addresses.nextElement();
|
||||||
|
// 只考虑IPv4地址
|
||||||
|
if (addr.getHostAddress().contains(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ipList.add(addr.getHostAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SocketException e) {
|
||||||
|
log.warn("Could not determine local IP addresses, using empty list", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Local IP addresses: {}", ipList);
|
||||||
|
return Collections.unmodifiableList(ipList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.seer.teach.common.config.feign;
|
||||||
|
|
||||||
|
import org.springframework.cloud.client.ServiceInstance;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||||
|
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||||
|
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||||
|
|
||||||
|
public class SameIpLoadBalancerClientFactory extends LoadBalancerClientFactory {
|
||||||
|
|
||||||
|
public SameIpLoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
|
||||||
|
super(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
|
||||||
|
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
|
||||||
|
return new SameIpLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
|
||||||
|
serviceId, reactiveLoadBalancer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.seer.teach.common.config.mybatis.hanler;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.BaseTypeHandler;
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
|
||||||
|
import java.sql.CallableStatement;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer列表类型处理器
|
||||||
|
* 用于将数据库中的VARCHAR字段(逗号分隔)与Java的List<Integer>相互转换
|
||||||
|
*/
|
||||||
|
@MappedTypes(List.class)
|
||||||
|
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||||
|
public class IntegerListTypeHandler extends BaseTypeHandler<List<Integer>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, List<Integer> parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
if (parameter != null && !parameter.isEmpty()) {
|
||||||
|
String value = parameter.stream()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
ps.setString(i, value);
|
||||||
|
} else {
|
||||||
|
ps.setString(i, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||||
|
String value = rs.getString(columnName);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||||
|
String value = rs.getString(columnIndex);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||||
|
String value = cs.getString(columnIndex);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将逗号分隔的字符串解析为Integer列表
|
||||||
|
*
|
||||||
|
* @param value 逗号分隔的字符串
|
||||||
|
* @return Integer列表
|
||||||
|
*/
|
||||||
|
private List<Integer> parseStringToList(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Arrays.stream(value.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.map(Integer::valueOf)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 如果转换失败,返回空列表
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.seer.teach.common.config.mybatis.hanler;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.BaseTypeHandler;
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
|
||||||
|
import java.sql.CallableStatement;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer列表类型处理器
|
||||||
|
* 用于将数据库中的VARCHAR字段(逗号分隔)与Java的List<Integer>相互转换
|
||||||
|
*/
|
||||||
|
@MappedTypes(List.class)
|
||||||
|
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||||
|
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
if (parameter != null && !parameter.isEmpty()) {
|
||||||
|
String value = parameter.stream()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
ps.setString(i, value);
|
||||||
|
} else {
|
||||||
|
ps.setString(i, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||||
|
String value = rs.getString(columnName);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||||
|
String value = rs.getString(columnIndex);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||||
|
String value = cs.getString(columnIndex);
|
||||||
|
return parseStringToList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将逗号分隔的字符串解析为Integer列表
|
||||||
|
*
|
||||||
|
* @param value 逗号分隔的字符串
|
||||||
|
* @return Integer列表
|
||||||
|
*/
|
||||||
|
private List<String> parseStringToList(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Arrays.stream(value.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 如果转换失败,返回空列表
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
package com.seer.teach.common.config.mybatis.plugin;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.executor.Executor;
|
||||||
|
import org.apache.ibatis.mapping.BoundSql;
|
||||||
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
|
import org.apache.ibatis.mapping.ParameterMapping;
|
||||||
|
import org.apache.ibatis.mapping.ParameterMode;
|
||||||
|
import org.apache.ibatis.reflection.DefaultReflectorFactory;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.apache.ibatis.reflection.ReflectorFactory;
|
||||||
|
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
|
||||||
|
import org.apache.ibatis.reflection.factory.ObjectFactory;
|
||||||
|
import org.apache.ibatis.reflection.property.PropertyTokenizer;
|
||||||
|
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
|
||||||
|
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
|
||||||
|
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
|
||||||
|
import org.apache.ibatis.session.ResultHandler;
|
||||||
|
import org.apache.ibatis.session.RowBounds;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class MybatisSqlFormatPlugin implements InnerInterceptor {
|
||||||
|
|
||||||
|
private Properties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
|
||||||
|
try {
|
||||||
|
// 执行查询逻辑在MyBatis内部完成,此处无需处理
|
||||||
|
} finally {
|
||||||
|
formatAndPrintSql(mappedStatement, parameter, boundSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameter) throws SQLException {
|
||||||
|
try {
|
||||||
|
// 执行更新逻辑在MyBatis内部完成,此处无需处理
|
||||||
|
} finally {
|
||||||
|
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
|
||||||
|
formatAndPrintSql(mappedStatement, parameter, boundSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化并打印SQL语句
|
||||||
|
*/
|
||||||
|
private void formatAndPrintSql(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
|
||||||
|
try {
|
||||||
|
String sql = boundSql.getSql();
|
||||||
|
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
|
||||||
|
|
||||||
|
if (parameterMappings != null) {
|
||||||
|
ObjectFactory objectFactory = new DefaultObjectFactory();
|
||||||
|
ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
|
||||||
|
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
|
||||||
|
MetaObject metaObject = parameterObject == null ? null : MetaObject
|
||||||
|
.forObject(parameterObject, objectFactory, objectWrapperFactory, reflectorFactory);
|
||||||
|
|
||||||
|
for (int i = 0; i < parameterMappings.size(); i++) {
|
||||||
|
ParameterMapping parameterMapping = parameterMappings.get(i);
|
||||||
|
if (parameterMapping.getMode() != ParameterMode.OUT) {
|
||||||
|
Object value;
|
||||||
|
String propertyName = parameterMapping.getProperty();
|
||||||
|
PropertyTokenizer prop = new PropertyTokenizer(propertyName);
|
||||||
|
if (parameterObject == null) {
|
||||||
|
value = null;
|
||||||
|
} else if (mappedStatement.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(
|
||||||
|
parameterObject.getClass())) {
|
||||||
|
value = parameterObject;
|
||||||
|
} else if (boundSql.hasAdditionalParameter(propertyName)) {
|
||||||
|
value = boundSql.getAdditionalParameter(propertyName);
|
||||||
|
} else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
|
||||||
|
&& boundSql.hasAdditionalParameter(prop.getName())) {
|
||||||
|
value = boundSql.getAdditionalParameter(prop.getName());
|
||||||
|
if (value != null) {
|
||||||
|
value = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory).getValue(
|
||||||
|
propertyName.substring(prop.getName().length()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = metaObject == null ? null : metaObject.getValue(propertyName);
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
boolean valueIsString = value instanceof String;
|
||||||
|
if (valueIsString && value.toString().indexOf("$") > -1) {
|
||||||
|
value = ((String) value).replaceAll("\\$", "\\\\\\$");
|
||||||
|
}
|
||||||
|
sql = sql.replaceFirst("\\?", valueIsString ? "'" + value + "'" : value.toString());
|
||||||
|
} else {
|
||||||
|
sql = sql.replaceFirst("\\?", "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties != null &&
|
||||||
|
"true".equals(properties.getProperty("printSql", "false"))) {
|
||||||
|
log.info("执行SQL ID: {}", mappedStatement.getId());
|
||||||
|
log.info("执行SQL语句: \n{}", sql);
|
||||||
|
log.info("=======End=======");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("格式化SQL语句时发生异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProperties(Properties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
seer-common/common-data-permission/pom.xml
Normal file
37
seer-common/common-data-permission/pom.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-data-permission</artifactId>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>transmittable-thread-local</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.seer.teach.common.data.permission.annotation;
|
||||||
|
|
||||||
|
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRule;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface DataPermission {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前类或方法是否开启数据权限
|
||||||
|
* 即使不添加 @DataPermission 注解,默认是开启状态
|
||||||
|
* 可通过设置 enable 为 false 禁用
|
||||||
|
*/
|
||||||
|
boolean enable() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
|
||||||
|
*/
|
||||||
|
Class<? extends DataPermissionRule>[] includeRules() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排除的数据权限规则数组,优先级最低
|
||||||
|
*/
|
||||||
|
Class<? extends DataPermissionRule>[] excludeRules() default {};
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.seer.teach.common.data.permission.aop;
|
||||||
|
|
||||||
|
import com.seer.teach.common.data.permission.annotation.DataPermission;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.aopalliance.aop.Advice;
|
||||||
|
import org.springframework.aop.Pointcut;
|
||||||
|
import org.springframework.aop.support.AbstractPointcutAdvisor;
|
||||||
|
import org.springframework.aop.support.ComposablePointcut;
|
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||||
|
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
|
||||||
|
|
||||||
|
private final Advice advice;
|
||||||
|
|
||||||
|
private final Pointcut pointcut;
|
||||||
|
|
||||||
|
public DataPermissionAnnotationAdvisor() {
|
||||||
|
this.advice = new DataPermissionAnnotationInterceptor();
|
||||||
|
this.pointcut = this.buildPointcut();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Pointcut buildPointcut() {
|
||||||
|
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
|
||||||
|
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
|
||||||
|
return new ComposablePointcut(classPointcut).union(methodPointcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.seer.teach.common.data.permission.aop;
|
||||||
|
|
||||||
|
import com.seer.teach.common.data.permission.annotation.DataPermission;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.aopalliance.intercept.MethodInterceptor;
|
||||||
|
import org.aopalliance.intercept.MethodInvocation;
|
||||||
|
import org.springframework.core.MethodClassKey;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
||||||
|
@DataPermission
|
||||||
|
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
|
||||||
|
|
||||||
|
|
||||||
|
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
|
||||||
|
DataPermission dataPermission = this.findAnnotation(methodInvocation);
|
||||||
|
if (dataPermission != null) {
|
||||||
|
DataPermissionContextHolder.add(dataPermission);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return methodInvocation.proceed();
|
||||||
|
} finally {
|
||||||
|
if (dataPermission != null) {
|
||||||
|
DataPermissionContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataPermission findAnnotation(MethodInvocation methodInvocation) {
|
||||||
|
Method method = methodInvocation.getMethod();
|
||||||
|
Object targetObject = methodInvocation.getThis();
|
||||||
|
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
|
||||||
|
MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
|
||||||
|
DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
|
||||||
|
if (dataPermission != null) {
|
||||||
|
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
|
||||||
|
if (dataPermission == null) {
|
||||||
|
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
|
||||||
|
}
|
||||||
|
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
|
||||||
|
return dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.seer.teach.common.data.permission.aop;
|
||||||
|
|
||||||
|
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||||
|
import com.seer.teach.common.data.permission.annotation.DataPermission;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
public class DataPermissionContextHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 List 的原因,可能存在方法的嵌套调用
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
|
||||||
|
TransmittableThreadLocal.withInitial(LinkedList::new);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前的 DataPermission 注解
|
||||||
|
*
|
||||||
|
* @return DataPermission 注解
|
||||||
|
*/
|
||||||
|
public static DataPermission get() {
|
||||||
|
return DATA_PERMISSIONS.get().peekLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入栈 DataPermission 注解
|
||||||
|
*
|
||||||
|
* @param dataPermission DataPermission 注解
|
||||||
|
*/
|
||||||
|
public static void add(DataPermission dataPermission) {
|
||||||
|
DATA_PERMISSIONS.get().addLast(dataPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出栈 DataPermission 注解
|
||||||
|
*
|
||||||
|
* @return DataPermission 注解
|
||||||
|
*/
|
||||||
|
public static DataPermission remove() {
|
||||||
|
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
|
||||||
|
// 无元素时,清空 ThreadLocal
|
||||||
|
if (DATA_PERMISSIONS.get().isEmpty()) {
|
||||||
|
DATA_PERMISSIONS.remove();
|
||||||
|
}
|
||||||
|
return dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得所有 DataPermission
|
||||||
|
*
|
||||||
|
* @return DataPermission 队列
|
||||||
|
*/
|
||||||
|
public static List<DataPermission> getAll() {
|
||||||
|
return DATA_PERMISSIONS.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空上下文
|
||||||
|
*
|
||||||
|
* 目前仅仅用于单测
|
||||||
|
*/
|
||||||
|
public static void clear() {
|
||||||
|
DATA_PERMISSIONS.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.seer.teach.common.data.permission.config;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||||
|
import com.seer.teach.common.data.permission.aop.DataPermissionAnnotationAdvisor;
|
||||||
|
import com.seer.teach.common.data.permission.handler.DataPermissionRuleHandler;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionCondition;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRule;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRuleFactory;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRuleFactoryImpl;
|
||||||
|
import com.seer.teach.common.data.permission.rule.role.RoleDataPermissionRule;
|
||||||
|
import com.seer.teach.common.data.permission.rule.role.RoleDataPermissionRuleCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
//@Configuration
|
||||||
|
public class DataPermissionAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RoleDataPermissionRule roleDataPermissionRule(List<RoleDataPermissionRuleCustomizer> rules) {
|
||||||
|
RoleDataPermissionRule rule = new RoleDataPermissionRule();
|
||||||
|
if(CollectionUtil.isNotEmpty(rules)){
|
||||||
|
rules.forEach(customizer -> customizer.customize(rule, rule.getCondition()));
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules, List<DataPermissionCondition> conditions) {
|
||||||
|
return new DataPermissionRuleFactoryImpl(rules,conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor,
|
||||||
|
DataPermissionRuleFactory ruleFactory) {
|
||||||
|
DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory);
|
||||||
|
DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
|
||||||
|
List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
|
||||||
|
inners.add(0, inner);
|
||||||
|
interceptor.setInterceptors(inners);
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
|
||||||
|
return new DataPermissionAnnotationAdvisor();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.seer.teach.common.data.permission.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRule;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRuleFactory;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||||
|
import net.sf.jsqlparser.schema.Table;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
|
||||||
|
|
||||||
|
private final DataPermissionRuleFactory ruleFactory;
|
||||||
|
private static final String MYSQL_ESCAPE_CHARACTER = "`";
|
||||||
|
@Override
|
||||||
|
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
|
||||||
|
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
|
||||||
|
if (CollUtil.isEmpty(rules)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Expression allExpression = null;
|
||||||
|
for (DataPermissionRule rule : rules) {
|
||||||
|
String tableName = getTableName(table);
|
||||||
|
if (!rule.getTableNames().contains(tableName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Expression oneExpress = rule.getExpression(tableName, table.getAlias());
|
||||||
|
if (oneExpress == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(rule.getCondition().matches()){
|
||||||
|
allExpression = allExpression == null ? oneExpress : new AndExpression(allExpression, oneExpress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTableName(Table table) {
|
||||||
|
String tableName = table.getName();
|
||||||
|
if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
|
||||||
|
tableName = tableName.substring(1, tableName.length() - 1);
|
||||||
|
}
|
||||||
|
return tableName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限条件接口
|
||||||
|
* <p>
|
||||||
|
* 该接口定义了数据权限匹配的条件判断逻辑,用于判断给定的数据权限是否满足特定条件
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DataPermissionCondition {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断数据权限是否匹配条件
|
||||||
|
*
|
||||||
|
* @return boolean 匹配结果,true表示匹配成功,false表示匹配失败
|
||||||
|
*/
|
||||||
|
boolean matches();
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.expression.Alias;
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
public interface DataPermissionRule {
|
||||||
|
|
||||||
|
Set<String> getTableNames();
|
||||||
|
|
||||||
|
Expression getExpression(String tableName, Alias tableAlias);
|
||||||
|
|
||||||
|
DataPermissionCondition getCondition();
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataPermissionRule} 工厂接口
|
||||||
|
* 作为 {@link DataPermissionRule} 的容器,提供管理能力
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public interface DataPermissionRuleFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得所有数据权限规则数组
|
||||||
|
*
|
||||||
|
* @return 数据权限规则数组
|
||||||
|
*/
|
||||||
|
List<DataPermissionRule> getDataPermissionRules();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得指定 Mapper 的数据权限规则数组
|
||||||
|
*
|
||||||
|
* @param mappedStatementId 指定 Mapper 的编号
|
||||||
|
* @return 数据权限规则数组
|
||||||
|
*/
|
||||||
|
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
|
||||||
|
|
||||||
|
List<DataPermissionCondition> getConditions();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import com.seer.teach.common.data.permission.annotation.DataPermission;
|
||||||
|
import com.seer.teach.common.data.permission.aop.DataPermissionContextHolder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限规则数组
|
||||||
|
*/
|
||||||
|
private final List<DataPermissionRule> rules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限条件判断器列表
|
||||||
|
*/
|
||||||
|
private final List<DataPermissionCondition> conditions;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DataPermissionRule> getDataPermissionRules() {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
|
||||||
|
if (CollUtil.isEmpty(rules)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
DataPermission dataPermission = DataPermissionContextHolder.get();
|
||||||
|
if (dataPermission == null) {
|
||||||
|
return rules;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (!dataPermission.enable()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
|
||||||
|
return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
|
||||||
|
return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DataPermissionCondition> getConditions() {
|
||||||
|
return conditions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule.role;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.LambdaUtils;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.support.LambdaMeta;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionCondition;
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionRule;
|
||||||
|
import com.seer.teach.common.entity.BaseEntity;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sf.jsqlparser.expression.Alias;
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
import net.sf.jsqlparser.expression.LongValue;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||||
|
import net.sf.jsqlparser.schema.Column;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于角色的数据权限规则:限制用户只能访问自己所属数据(如 creatorId = 当前用户ID)
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RoleDataPermissionRule implements DataPermissionRule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存 SFunction -> 属性名 的映射
|
||||||
|
*/
|
||||||
|
private static final ConcurrentMap<SFunction<?, ?>, String> PROPERTY_NAME_CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储每个表对应的用户字段(如 creator_id, user_id 等)
|
||||||
|
* key: 表名 (tableName)
|
||||||
|
* value: 对应的数据库字段名
|
||||||
|
*/
|
||||||
|
private final ConcurrentMap<String, String> userColumns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private DataPermissionCondition condition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有受此规则影响的表名集合
|
||||||
|
*/
|
||||||
|
private final Set<String> tableNames = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
public RoleDataPermissionRule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getTableNames() {
|
||||||
|
return tableNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||||
|
if (!StpUtil.isLogin()) {
|
||||||
|
log.warn("用户未登录,跳过数据权限控制,表名:{}", tableName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int userId = StpUtil.getLoginIdAsInt();
|
||||||
|
return buildUserExpression(tableName, tableAlias, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataPermissionCondition getCondition() {
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户字段的等值表达式:tableAlias.columnName = userId
|
||||||
|
*/
|
||||||
|
private Expression buildUserExpression(String tableName, Alias tableAlias, int userId) {
|
||||||
|
String columnName = userColumns.get(tableName);
|
||||||
|
if (StrUtil.isEmpty(columnName)) {
|
||||||
|
log.debug("表 [{}] 未配置用户字段,跳过数据权限", tableName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Column column = buildColumn(tableName, tableAlias, columnName);
|
||||||
|
return new EqualsTo(column, new LongValue(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据实体类和 Java 属性名注册数据权限字段
|
||||||
|
*/
|
||||||
|
public <T extends BaseEntity> void addUserColumn(Class<T> entityClass, String propertyName) {
|
||||||
|
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
|
||||||
|
if (tableInfo == null) {
|
||||||
|
log.error("无法获取实体类 {} 的 TableInfo", entityClass.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableInfo.getFieldList().stream()
|
||||||
|
.filter(fieldInfo -> fieldInfo.getProperty().equals(propertyName))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(
|
||||||
|
fieldInfo -> {
|
||||||
|
String columnName = fieldInfo.getColumn();
|
||||||
|
addUserColumn(tableInfo.getTableName(), columnName);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接根据表名和数据库字段名注册权限字段
|
||||||
|
*/
|
||||||
|
public void addUserColumn(String tableName, String columnName) {
|
||||||
|
if (StrUtil.isBlank(tableName) || StrUtil.isBlank(columnName)) {
|
||||||
|
log.warn("注册失败:表名或字段名为null或空字符串");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userColumns.put(tableName, columnName);
|
||||||
|
tableNames.add(tableName);
|
||||||
|
log.debug("手动注册表 {} 的用户字段:{}", tableName, columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public <T extends BaseEntity> void addUserColumn(Class<T> entityClass, SFunction<T, ?> methodRef) {
|
||||||
|
if (entityClass == null || methodRef == null) {
|
||||||
|
log.warn("addUserColumn: 实体类或方法引用为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String propertyName = PROPERTY_NAME_CACHE.computeIfAbsent(methodRef, ref -> {
|
||||||
|
LambdaMeta meta = LambdaUtils.extract(ref);
|
||||||
|
return extractPropertyName(meta.getImplMethodName());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(propertyName)) {
|
||||||
|
addUserColumn(entityClass, propertyName);
|
||||||
|
} else {
|
||||||
|
log.error("无法从方法引用解析出属性名:{}", methodRef);
|
||||||
|
throw new IllegalArgumentException("无效的SFunction,无法提取属性名");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String extractPropertyName(String methodName) {
|
||||||
|
if (StrUtil.isBlank(methodName)) return null;
|
||||||
|
|
||||||
|
if (methodName.startsWith("get") && methodName.length() > 3) {
|
||||||
|
return StringUtils.uncapitalize(methodName.substring(3));
|
||||||
|
} else if (methodName.startsWith("is") && methodName.length() > 2) {
|
||||||
|
return StringUtils.uncapitalize(methodName.substring(2));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造带别名的列对象
|
||||||
|
*/
|
||||||
|
public Column buildColumn(String tableName, Alias tableAlias, String columnName) {
|
||||||
|
String actualTableName = tableAlias != null ? tableAlias.getName() : tableName;
|
||||||
|
return new Column(actualTableName + StringPool.DOT + columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCondition(DataPermissionCondition condition) {
|
||||||
|
this.condition = condition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.seer.teach.common.data.permission.rule.role;
|
||||||
|
|
||||||
|
import com.seer.teach.common.data.permission.rule.DataPermissionCondition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色数据权限规则自定义器接口
|
||||||
|
*
|
||||||
|
* <p>该接口用于自定义角色数据权限规则,允许开发者根据业务需求对默认的权限规则进行扩展或修改。</p>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RoleDataPermissionRuleCustomizer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义角色数据权限规则
|
||||||
|
*
|
||||||
|
* <p>通过此方法可以对传入的权限规则对象进行自定义配置,如添加额外的过滤条件、
|
||||||
|
* 修改现有规则逻辑或扩展规则属性等。</p>
|
||||||
|
*
|
||||||
|
* @param rule 角色数据权限规则对象,不允许为null
|
||||||
|
*/
|
||||||
|
void customize(RoleDataPermissionRule rule,DataPermissionCondition condition);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package com.seer.teach.common.data.permission.util;
|
||||||
|
|
||||||
|
import com.seer.teach.common.data.permission.annotation.DataPermission;
|
||||||
|
import com.seer.teach.common.data.permission.aop.DataPermissionContextHolder;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限 Util
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class DataPermissionUtils {
|
||||||
|
|
||||||
|
private static DataPermission DATA_PERMISSION_DISABLE;
|
||||||
|
|
||||||
|
@DataPermission(enable = false)
|
||||||
|
@SneakyThrows
|
||||||
|
private static DataPermission getDisableDataPermissionDisable() {
|
||||||
|
if (DATA_PERMISSION_DISABLE == null) {
|
||||||
|
DATA_PERMISSION_DISABLE = DataPermissionUtils.class
|
||||||
|
.getDeclaredMethod("getDisableDataPermissionDisable")
|
||||||
|
.getAnnotation(DataPermission.class);
|
||||||
|
}
|
||||||
|
return DATA_PERMISSION_DISABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忽略数据权限,执行对应的逻辑
|
||||||
|
*
|
||||||
|
* @param runnable 逻辑
|
||||||
|
*/
|
||||||
|
public static void executeIgnore(Runnable runnable) {
|
||||||
|
addDisableDataPermission();
|
||||||
|
try {
|
||||||
|
// 执行 runnable
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
removeDataPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忽略数据权限,执行对应的逻辑
|
||||||
|
*
|
||||||
|
* @param callable 逻辑
|
||||||
|
* @return 执行结果
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static <T> T executeIgnore(Callable<T> callable) {
|
||||||
|
addDisableDataPermission();
|
||||||
|
try {
|
||||||
|
// 执行 callable
|
||||||
|
return callable.call();
|
||||||
|
} finally {
|
||||||
|
removeDataPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加忽略数据权限
|
||||||
|
*/
|
||||||
|
public static void addDisableDataPermission(){
|
||||||
|
DataPermission dataPermission = getDisableDataPermissionDisable();
|
||||||
|
DataPermissionContextHolder.add(dataPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeDataPermission(){
|
||||||
|
DataPermissionContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
seer-common/common-dto/pom.xml
Normal file
15
seer-common/common-dto/pom.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-dto</artifactId>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-15 13:59
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "方向")
|
||||||
|
public class DirectionalInputDTO extends MqDTO{
|
||||||
|
|
||||||
|
private String angle;
|
||||||
|
|
||||||
|
private String padding;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM回复数据传输对象
|
||||||
|
* 用于封装IM消息发送后的回复结果信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ImReplyDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否成功
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息(错误信息或提示信息)
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回的业务数据
|
||||||
|
*/
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
public static ImReplyDTO ok(String data) {
|
||||||
|
return new ImReplyDTO(true, "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImReplyDTO fail(String msg) {
|
||||||
|
return new ImReplyDTO(false, msg, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-30 14:11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class IntegerDTO extends MqDTO {
|
||||||
|
|
||||||
|
private Integer data;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM聊天消息传输对象
|
||||||
|
* 用于在消息队列中传输LLM聊天相关数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class LlmChatDTO extends MqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天记录唯一标识符
|
||||||
|
*/
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序字段,用于确定消息顺序
|
||||||
|
*/
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天文本内容
|
||||||
|
*/
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整的聊天文本内容
|
||||||
|
*/
|
||||||
|
private String allText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频数据,通常为base64编码的音频文件
|
||||||
|
*/
|
||||||
|
private String audioData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为最终结果标识
|
||||||
|
* true表示这是最终的聊天结果,false表示中间过程数据
|
||||||
|
*/
|
||||||
|
private Boolean isFinal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息队列时间戳
|
||||||
|
* 记录消息进入队列的时间
|
||||||
|
*/
|
||||||
|
private long mqTime;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
public abstract class MqDTO {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-24 13:54
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class StringDTO extends MqDTO {
|
||||||
|
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
public static StringDTO ping(){
|
||||||
|
return new StringDTO("ping");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.seer.teach.common.dto.mq;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Description:
|
||||||
|
* @Date: 2025-06-07 16:48
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "逐字识别信息")
|
||||||
|
public class WordItem {
|
||||||
|
|
||||||
|
@Schema(description = "识别出的单字")
|
||||||
|
private String word;
|
||||||
|
|
||||||
|
@Schema(description = "起始时间(秒)")
|
||||||
|
private Double startTime;
|
||||||
|
|
||||||
|
@Schema(description = "结束时间(秒)")
|
||||||
|
private Double endTime;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.dto;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.WordItem;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-09-09 15:15
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "音频数据返回实体类")
|
||||||
|
public class AudioDTO {
|
||||||
|
|
||||||
|
@Schema(description = "音频URL")
|
||||||
|
private String audioUrl;
|
||||||
|
|
||||||
|
@Schema(description = "音频字幕数据")
|
||||||
|
private List<WordItem> wordItems;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户缓存数据传输对象
|
||||||
|
* <p>
|
||||||
|
* 用于在系统中缓存用户相关信息的DTO类,包含用户的基本信息、设备信息、认证信息等
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-16 18:01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class UserCacheDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID
|
||||||
|
*/
|
||||||
|
private Integer deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备类型
|
||||||
|
*/
|
||||||
|
private String device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备是否在线
|
||||||
|
*/
|
||||||
|
private Integer deviceIsOnLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备屏幕状态
|
||||||
|
*/
|
||||||
|
private Integer screenStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备音频状态
|
||||||
|
*/
|
||||||
|
private Integer volumeStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像URL
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真实姓名
|
||||||
|
*/
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别(0-未知,1-男,2-女)
|
||||||
|
*/
|
||||||
|
private Integer gender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号码
|
||||||
|
*/
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱地址
|
||||||
|
*/
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出生日期
|
||||||
|
*/
|
||||||
|
private LocalDate birthDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信UnionID
|
||||||
|
*/
|
||||||
|
private String unionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信OpenID
|
||||||
|
*/
|
||||||
|
private String openId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年级ID
|
||||||
|
*/
|
||||||
|
private Integer gradeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等级ID
|
||||||
|
*/
|
||||||
|
private Integer levelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经验值
|
||||||
|
*/
|
||||||
|
private Integer experience;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色编码
|
||||||
|
*/
|
||||||
|
private String roleCode;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import com.seer.teach.common.dto.mq.WordItem;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: Captain
|
||||||
|
* @Description: Ai聊天返回实体类
|
||||||
|
* @Date: 2025-06-07 16:49
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "聊天推送实体类数据")
|
||||||
|
public class AiChatResp extends MqDTO {
|
||||||
|
|
||||||
|
@Schema(description = "音频Url")
|
||||||
|
private String audioUrl;
|
||||||
|
|
||||||
|
@Schema(description = "字幕数据")
|
||||||
|
private List<WordItem> wordItems;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-08 11:23
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class AiStudyDTO extends MqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否发送知识点数据
|
||||||
|
*/
|
||||||
|
private Boolean kd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否发送音频数据
|
||||||
|
*/
|
||||||
|
private Boolean audio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识点响应数据对象
|
||||||
|
*/
|
||||||
|
private KnowDTO knowData;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-08 11:26
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class AudioDTO {
|
||||||
|
|
||||||
|
/** 消息 ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频数据
|
||||||
|
* <p>
|
||||||
|
* 存储音频的Base64编码字符串或其他音频格式数据
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private String audioData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本内容
|
||||||
|
* <p>
|
||||||
|
* 与音频数据对应的文本内容,通常是语音识别或文本转语音的结果
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
/** 是否最终结果 */
|
||||||
|
private Boolean isFinal;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-15 15:13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "缓存设备DTO实体类")
|
||||||
|
public class CacheDeviceDTO extends MqDTO {
|
||||||
|
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
private String device;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-10-08 11:25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class KnowDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识点名称
|
||||||
|
*/
|
||||||
|
private String knowledgePointName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识点摘总结
|
||||||
|
*/
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识点数据内容
|
||||||
|
*/
|
||||||
|
private String knowledgePointData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相关视频URL
|
||||||
|
*/
|
||||||
|
private String videoUrl;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-12-04 16:11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "知识点数据")
|
||||||
|
public class KnowledgeDTO extends MqDTO {
|
||||||
|
|
||||||
|
@Schema(description = "知识点ID")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "知识点名称")
|
||||||
|
private String knowledgePointName;
|
||||||
|
|
||||||
|
@Schema(description = "知识点总结")
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "孩子信息Netty推送实体类")
|
||||||
|
public class MqUserInfoDTO extends MqDTO {
|
||||||
|
|
||||||
|
@Schema(description = "孩子Id")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "昵称")
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
@Schema(description = "年级")
|
||||||
|
private String grade;
|
||||||
|
|
||||||
|
@Schema(description = "头像")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Schema(description = "学豆数量")
|
||||||
|
private Integer coinNumber;
|
||||||
|
|
||||||
|
@Schema(description = "当前等级")
|
||||||
|
private String currentLevel;
|
||||||
|
|
||||||
|
@Schema(description = "经验分数")
|
||||||
|
private Integer experience;
|
||||||
|
|
||||||
|
@Schema(description = "等级图片Url")
|
||||||
|
private String levelImageUrl;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.im;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-08-01 10:46
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "题目批改解析返回实体类")
|
||||||
|
public class QuestionCorrectParseHtmlDTO {
|
||||||
|
|
||||||
|
@Schema(description = "0 错误 1正确")
|
||||||
|
private Integer result;
|
||||||
|
|
||||||
|
@Schema(description = "解析页面")
|
||||||
|
private String html;
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.webSocket;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-30 15:58
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "批改流式Ws推送数据")
|
||||||
|
public class AiCorrectQuestionResultDTO extends MqDTO {
|
||||||
|
|
||||||
|
@Schema(description = "题号")
|
||||||
|
private Integer index;
|
||||||
|
|
||||||
|
@Schema(description = "result")
|
||||||
|
private Integer result;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.webSocket;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-30 20:24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "做题数据返回实体类")
|
||||||
|
public class QuestionDTO {
|
||||||
|
|
||||||
|
@Schema(description = "题目Id")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "题目类型 0其他题型 1单选选择题 2多选选择题 3问答题 4判断题")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "选项数据")
|
||||||
|
private List<String> options;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.seer.teach.common.dto.mq.webSocket;
|
||||||
|
|
||||||
|
import com.seer.teach.common.dto.mq.MqDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p >
|
||||||
|
*
|
||||||
|
* @author Captain
|
||||||
|
* @since 2025-07-30 20:18
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString
|
||||||
|
@Schema(description = "websocket做题数据返回实体类")
|
||||||
|
public class WsMakeQuestionDTO extends MqDTO {
|
||||||
|
|
||||||
|
@Schema(description = "任务ID")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "type")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "题目数据")
|
||||||
|
private List<QuestionDTO> results;
|
||||||
|
|
||||||
|
}
|
||||||
29
seer-common/common-enums/pom.xml
Normal file
29
seer-common/common-enums/pom.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.seer.teach</groupId>
|
||||||
|
<artifactId>seer-common</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-enums</artifactId>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-dto</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>common-validation</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
package com.seer.teach.common.enums;
|
||||||
|
|
||||||
|
import com.seer.teach.common.validation.ArrayValuable;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static cn.hutool.core.util.ArrayUtil.firstMatch;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum AfterSaleStatusEnum implements ArrayValuable<Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【没有申请】
|
||||||
|
*/
|
||||||
|
NON_APPLY(0,"没有申请", "没有申请"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【申请售后】
|
||||||
|
*/
|
||||||
|
APPLY(10,"申请中", "申请退款"),
|
||||||
|
/**
|
||||||
|
* 卖家通过售后;【商品待退货】
|
||||||
|
*/
|
||||||
|
SELLER_AGREE(20, "卖家通过", "商家同意申请"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卖家拒绝售后;【卖家拒绝】
|
||||||
|
*/
|
||||||
|
SELLER_REFUSE_APPLY(21, "卖家拒绝", "商家拒绝申请"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 买家已发货,等待卖家收货;【商家待收货】
|
||||||
|
*/
|
||||||
|
BUYER_DELIVERY(30,"买家已发货", "商家待收货"),
|
||||||
|
/**
|
||||||
|
* 卖家已收货,等待平台退款;等待退款【等待退款】
|
||||||
|
*/
|
||||||
|
WAIT_REFUND(40, "卖家已收获", "商家收货,等待平台退款"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款失败
|
||||||
|
*/
|
||||||
|
REFUND_FAILED(52, "退款失败", "支付渠道退款失败"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成退款【退款成功】
|
||||||
|
*/
|
||||||
|
COMPLETE(56, "完成", "退款成功"),
|
||||||
|
/**
|
||||||
|
* 【买家取消】
|
||||||
|
*/
|
||||||
|
BUYER_CANCEL(61, "买家取消售后", "取消退款"),
|
||||||
|
/**
|
||||||
|
* 卖家拒绝售后;商家拒绝【商家拒绝】
|
||||||
|
*/
|
||||||
|
SELLER_DISAGREE(62,"卖家拒绝", "商家拒绝退款"),
|
||||||
|
/**
|
||||||
|
* 卖家拒绝收货,终止售后;【商家拒收货】
|
||||||
|
*/
|
||||||
|
SELLER_REFUSE(63,"卖家拒绝收货", "商家拒绝收货"),
|
||||||
|
;
|
||||||
|
|
||||||
|
public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleStatusEnum::getStatus).toArray(Integer[]::new);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行中的售后状态
|
||||||
|
*
|
||||||
|
* 不包括已经结束的状态
|
||||||
|
*/
|
||||||
|
public static final Collection<Integer> APPLYING_STATUSES = Arrays.asList(
|
||||||
|
APPLY.getStatus(),
|
||||||
|
SELLER_AGREE.getStatus(),
|
||||||
|
BUYER_DELIVERY.getStatus(),
|
||||||
|
WAIT_REFUND.getStatus()
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
private final Integer status;
|
||||||
|
/**
|
||||||
|
* 状态名
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
/**
|
||||||
|
* 操作内容
|
||||||
|
*
|
||||||
|
* 目的:记录售后日志的内容
|
||||||
|
*/
|
||||||
|
private final String content;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer[] array() {
|
||||||
|
return ARRAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AfterSaleStatusEnum valueOf(Integer status) {
|
||||||
|
return firstMatch(value -> value.getStatus().equals(status), values());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.seer.teach.common.enums;
|
||||||
|
|
||||||
|
import com.seer.teach.common.validation.ArrayValuable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易售后 - 方式
|
||||||
|
*
|
||||||
|
* @author Sin
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum AfterSaleWayEnum implements ArrayValuable<Integer> {
|
||||||
|
|
||||||
|
REFUND(10, "仅退款"),
|
||||||
|
RETURN_AND_REFUND(20, "退货退款");
|
||||||
|
|
||||||
|
public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleWayEnum::getWay).toArray(Integer[]::new);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方式
|
||||||
|
*/
|
||||||
|
private final Integer way;
|
||||||
|
/**
|
||||||
|
* 方式名
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer[] array() {
|
||||||
|
return ARRAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user