Compare commits

..

10 Commits

Author SHA1 Message Date
Hanserwei
2910fdb54f feat(kv): 初始化 KV 服务模块
- 添加了笔记内容的增删查 DTO 类
- 配置了 Cassandra 数据库连接
- 实现了基于 Cassandra 的笔记内容存储与查询功能

feat(kv): 初始化 distributeID 服务模块

- 实现了分布式 ID 生成器服务(Snowflake与 Segment)
- 添加了 ID 生成器监控接口
- 配置了 MyBatis 与数据库交互
- 添加了 Segment 与 Snowflake 服务实现
- 添加了 Leaf 相关模型类与分配器接口
- 添加了 Leaf 分配器实现类
- 添加了 Leaf 控制器与监控视图
- 添加了 Leaf 异常处理类
- 添加了 Leaf 日志配置文件
- 添加了 Leaf 启动类
- 添加了 Leaf 常量定义
- 添加了 Leaf ID 生成接口
- 添加了 Leaf 初始化异常类
- 添加了 Leaf 配置文件
- 添加了 Leaf 模型类
- 添加了 Leaf 服务类
- 添加了 Leaf 工具类
- 添加了 Leaf 相关注解
- 添加了 Leaf 相关配置类
- 添加了 Leaf 相关枚举类
- 添加了 Leaf 相关工具类

后续考虑复刻Leaf代码至Java21平台
2025-10-06 22:28:27 +08:00
Hanserwei
534a49a358 refactor(auth):重构认证服务,分离用户逻辑到独立服务
- 将用户相关业务迁移至用户模块,通过OpenFeign远程调用。
2025-10-04 22:03:09 +08:00
Hanserwei
19457b5638 子模块打包文件 2025-10-04 21:59:27 +08:00
Hanserwei
ec9f75a948 子模块打包文件 2025-10-04 15:54:12 +08:00
Hanserwei
4b992c35ca feat(oss): 实现文件上传功能并集成Feign调用
- 新增文件上传接口,支持multipart/form-data格式
- 配置Spring Servlet multipart参数,设置文件大小限制
- 添加Feign客户端配置,支持表单提交
- 实现Feign请求拦截器,传递用户上下文信息
- 创建OSS服务API接口,用于文件上传
- 在用户服务中集成OSS RPC调用,实现头像和背景图上传
- 添加上传失败的业务异常处理
- 更新pom.xml依赖,引入OpenFeign、负载均衡及Feign表单相关组件
- 定义API常量和服务名称
- 启用Feign客户端扫描,支持跨服务调用
2025-10-04 15:53:22 +08:00
Hanserwei
91e36d5a84 feat(user): 初始化用户服务模块
update(gateway):更新网关相关路由配置

- 添加用户服务基础架构,包括 API 和 Biz 模块
- 配置用户服务的 Spring Boot 启动类及 MyBatis Plus 配置- 实现用户信息更新接口,支持头像、昵称、小憨书号等字段校验
- 添加全局异常处理器,统一处理业务异常和参数校验错误
- 集成 Nacos 服务发现与配置中心
- 添加日志配置文件,支持异步日志写入
- 新增用户相关枚举类,如性别、响应码等
- 添加参数校验工具类,用于昵称、小憨书号等格式校验
- 配置网关路由,将 /user/** 路径转发至用户服务
- 在 GitIgnore 中忽略用户服务的本地开发配置文件
- 更新认证服务中的用户相关字段命名与接口路径
- 添加用户数据对象 UserDO 及对应的 Mapper 和 XML 配置
- 实现 UserService 接口及默认实现类 UserServiceImpl
- 添加用户信息更新请求 VO 类 UpdateUserInfoReqVO
- 添加用户 Mapper 接口 UserDOMapper 继承 BaseMapper
- 添加用户模块的 Maven 配置 pom.xml 文件
- 添加用户模块的编码配置,确保使用 UTF-8 编码
- 添加用户模块的启动日志配置 logback-spring.xml
- 添加用户模块的 bootstrap.yml 配置文件
- 添加用户模块的 application.yml 配置文件
- 添加用户模块的异常处理类 GlobalExceptionHandler
- 添加用户模块的枚举类 ResponseCodeEnum 和 SexEnum
- 添加用户模块的工具类 ParamUtils用于参数校验
- 添加用户模块的控制器 UserController 处理用户信息更新请求
- 添加用户模块的服务接口 UserService 及其实现类 UserServiceImpl
- 添加用户模块的数据访问对象 UserDO 及其映射文件 UserDOMapper.xml
- 添加用户模块的请求视图对象 UpdateUserInfoReqVO
- 添加用户模块的 API 模块 pom.xml 配置文件
- 添加用户模块的 Biz 模块 pom.xml 配置文件
- 添加用户模块的根 pom.xml 配置文件
- 在主 pom.xml 中添加用户模块 han-note-user作为子模块
- 修改 SaToken 配置,调整登录和登出接口路径白名单
- 移除 UserController 中的 @RequestMapping("/user") 注解- 修改 UserDO 中“小哈书号”为“小憨书号”以保持命名一致性
2025-10-04 14:56:33 +08:00
Hanserwei
0d71d8e209 feat(oss): 增加对象存储模块并支持多种存储策略
- 新增对象存储服务模块 `han-note-oss`,集成 Rustfs、阿里云 OSS 及腾讯云 Cos 存储
- 提供统一的 `FileStrategy` 接口及 `FileStrategyFactory` 工厂类,根据存储类型动态选择存储策略
- 实现阿里云 OSS、腾讯云 Cos 和 Rustfs 具体存储逻辑
- 增加文件上传接口 `FileController`,支持接收文件并返回访问路径
- 完成用户密码更新接口,使用`spring.security`对密码进行加密
2025-10-03 17:52:25 +08:00
Hanserwei
5c406b48f9 feat(context): 新增业务上下文组件并迁移相关功能
- 创建 `hanserwei-spring-boot-starter-biz-context` 模块,封装为starter供其余模块使用
- 迁移 `HeaderUserId2ContextFilter` 和 `LoginUserContextHolder` 到Starter
- 使用 `TransmittableThreadLocal` 替代普通 `ThreadLocal`
- 在 `han-note-auth` 中引入新的上下文组件依赖
- 调整包结构和日志输出格式
- 异步任务中验证上下文传递功能正常工作
2025-10-02 23:23:47 +08:00
Hanserwei
d448c524b7 feat(auth): 新增用户ID上下文过滤器及登出逻辑优化
- 新增 GlobalConstants 常量类定义 USER_ID 常量
- 新增 HeaderUserId2ContextFilter 过滤器从请求头获取用户 ID 并存入 ThreadLocal
- 新增 LoginUserContextHolder 工具类用于管理用户 ID 的 ThreadLocal 操作
-优化 UserController 的 logout 方法,移除手动传参 userId,改为从上下文获取
- 优化 UserServiceImpl 的 logout 方法实现,通过上下文获取用户 ID 完成登出
- 在 han-note-gateway 模块中添加 tomcat-embed-core依赖以支持相关功能
2025-10-02 22:27:49 +08:00
Hanserwei
3cc615d38a feat(auth): 实现用户退出登录功能
- 在网关过滤器中增加对未登录异常的处理
- 完善用户登出接口,调用 Sa-Token 的登出方法
- 新增 UserService 中的 logout 方法实现
- 优化获取用户 ID 的逻辑并增强异常处理
- 清理 Sa-Token 上下文避免内存泄漏
2025-10-02 22:05:32 +08:00
176 changed files with 6499 additions and 389 deletions

301
.flattened-pom.xml Normal file
View File

@@ -0,0 +1,301 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>小憨书(仿小红书),基于 Spring Cloud Alibaba 微服务架构</description>
<modules>
<module>han-note-auth</module>
<module>hanserwei-framework</module>
<module>han-note-gateway</module>
<module>han-note-oss</module>
<module>han-note-user</module>
</modules>
<properties>
<!-- 项目版本号 -->
<revision>0.0.1-SNAPSHOT</revision>
<!-- JDK 版本 -->
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<!-- 项目编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Maven 相关版本号 -->
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<flatten-maven-plugin.version>1.7.3</flatten-maven-plugin.version>
<!-- 依赖包版本 -->
<lombok.version>1.18.30</lombok.version>
<spring-boot.version>3.2.4</spring-boot.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<jackson.version>2.15.4</jackson.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mysql-connector-j.version>8.4.0</mysql-connector-j.version>
<druid.version>1.2.27</druid.version>
<sa-token.version>1.44.0</sa-token.version>
<guava.version>33.5.0-jre</guava.version>
<hutool.version>5.8.40</hutool.version>
<commons-lang3.version>3.19.0</commons-lang3.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<aws-sdk.version>2.34.8</aws-sdk.version>
<aliyun-sdk-oss.version>3.17.4</aliyun-sdk-oss.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<activation.version>1.1.1</activation.version>
<jaxb-runtime.version>2.3.3</jaxb-runtime.version>
<cos-api.version>5.6.227</cos-api.version>
<feign-form.version>3.8.0</feign-form.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- 避免编写那些冗余的 Java 样板式代码,如 get、set 等 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- Spring Boot 官方依赖管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba 官方依赖管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud 官方依赖管理 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector-j.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 相关工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${transmittable-thread-local.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws-sdk.version}</version>
</dependency>
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-sdk-oss.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${activation.version}</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb-runtime.version}</version>
</dependency>
<!-- 腾讯云 OSS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${cos-api.version}</version>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Feign 表单提交 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>${feign-form.version}</version>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-user-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- 统一插件管理 -->
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal> <!-- 将依赖的 Jar 一起打包 -->
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- 统一 revision 版本, 解决子模块打包无法解析 0.0.1-SNAPSHOT 版本号问题 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version>
<configuration>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
<updatePomFile>true</updatePomFile>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

7
.gitignore vendored
View File

@@ -42,4 +42,9 @@ build/
/han-note-auth/src/main/resources/application-prod.yml
/han-note-auth/logs/
/logs/
/.idea/
/han-note-oss/han-note-oss-biz/src/main/resources/application-dev.yml
/han-note-user/han-note-user-biz/src/main/resources/application-dev.yml
/han-note-user/han-note-user-biz/logs/
/han-note-kv/han-note-kv-biz/src/main/resources/application-dev.yml
/han-note-kv/han-note-kv-biz/src/main/resources/application-prod.yml
/han-note-kv/han-note-kv-biz/logs/

28
.idea/encodings.xml generated
View File

@@ -3,14 +3,42 @@
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/han-note-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/han-note-distributed-id-generator-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/han-note-distributed-id-generator-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/hannote-distributed-id-generator-biz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/hannote-distributed-id-generator-biz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/han-note-kv-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/han-note-kv-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/han-note-kv-biz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/han-note-kv-biz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-kv/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/han-note-oss-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/han-note-oss-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/han-note-oss-biz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/han-note-oss-biz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-oss/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/han-note-user-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/han-note-user-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/han-note-user-biz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/han-note-user-biz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-biz-context/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-biz-context/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-biz-operationlog/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-biz-operationlog/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-jackson/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-boot-starter-jackson/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-starter-biz-context/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-starter-biz-context/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/hanserwei-framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />

5
.idea/misc.xml generated
View File

@@ -12,6 +12,11 @@
<option value="$PROJECT_DIR$/han-note-auth/pom.xml" />
</list>
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/hanserwei-framework/hanserwei-spring-starter-biz-context/pom.xml" />
</set>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>han-note-auth</artifactId>
<name>${project.artifactId}</name>
<description>小憨书:认证服务(负责处理用户登录、注册、账号注销等)</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- 使用 Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Cloud Context (用于动态刷新) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<!-- Spring Cloud Bootstrap (如果使用 bootstrap 配置) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-user-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -26,18 +26,6 @@
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
@@ -93,8 +81,19 @@
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-user-api</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -1,11 +1,11 @@
package com.hanserwei.hannote.auth;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@MapperScan("com.hanserwei.hannote.auth.domain.mapper")
@EnableFeignClients(basePackages = "com.hanserwei.hannote")
public class HanNoteAuthApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,21 @@
package com.hanserwei.hannote.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("qwe123"));
}
}

View File

@@ -0,0 +1,41 @@
package com.hanserwei.hannote.auth.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
import com.hanserwei.hannote.auth.service.AuthService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequiredArgsConstructor
public class AuthController {
@Resource
private AuthService authService;
@PostMapping("/login")
@ApiOperationLog(description = "用户登录/注册")
public Response<String> loginAndRegister(@Validated @RequestBody UserLoginReqVO userLoginReqVO) {
return authService.loginAndRegister(userLoginReqVO);
}
@PostMapping("/logout")
@ApiOperationLog(description = "账号登出")
public Response<?> logout() {
return authService.logout();
}
@PostMapping("/password/update")
@ApiOperationLog(description = "修改密码")
public Response<?> updatePassword(@Validated @RequestBody UpdatePasswordReqVO updatePasswordReqVO) {
return authService.updatePassword(updatePasswordReqVO);
}
}

View File

@@ -1,36 +0,0 @@
package com.hanserwei.hannote.auth.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
import com.hanserwei.hannote.auth.service.UserService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Slf4j
@RequiredArgsConstructor
public class UserController {
@Resource
private UserService userService;
@PostMapping("/login")
@ApiOperationLog(description = "用户登录/注册")
public Response<String> loginAndRegister(@Validated @RequestBody UserLoginReqVO userLoginReqVO) {
return userService.loginAndRegister(userLoginReqVO);
}
@PostMapping("/logout")
@ApiOperationLog(description = "账号登出")
public Response<?> logout(@RequestHeader("userId") String userId) {
log.info("==> 网关透传过来的用户 ID: {}", userId);
// todo 账号退出登录逻辑待实现
return Response.success();
}
}

View File

@@ -15,6 +15,10 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 业务异常状态码 -----------
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁请3分钟后再试"),
VERIFICATION_CODE_ERROR("AUTH-20001", "验证码错误"),
LOGIN_TYPE_ERROR("AUTH-20002", "登录类型错误"),
USER_NOT_FOUND("AUTH-20003", "该用户不存在"),
MAIL_OR_PASSWORD_ERROR("AUTH-20004", "邮箱号或密码错误"),
LOGIN_FAIL("AUTH-20005", "登录失败"),

View File

@@ -0,0 +1,18 @@
package com.hanserwei.hannote.auth.model.vo.user;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdatePasswordReqVO {
@NotBlank(message = "新密码不能为空")
private String newPassword;
}

View File

@@ -0,0 +1,68 @@
package com.hanserwei.hannote.auth.rpc;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.api.UserFeignApi;
import com.hanserwei.hannote.user.dto.req.FindUserByEmailReqDTO;
import com.hanserwei.hannote.user.dto.req.RegisterUserReqDTO;
import com.hanserwei.hannote.user.dto.req.UpdateUserPasswordReqDTO;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class UserRpcService {
@Resource
private UserFeignApi userFeignApi;
/**
* 用户注册
*
* @param email 邮箱
* @return 用户ID
*/
public Long registerUser(String email) {
RegisterUserReqDTO registerUserReqDTO = new RegisterUserReqDTO();
registerUserReqDTO.setEmail(email);
Response<Long> response = userFeignApi.registerUser(registerUserReqDTO);
if (!response.isSuccess()) {
return null;
}
return response.getData();
}
/**
* 根据邮箱号查询用户信息
*
* @param email 邮箱
* @return 用户信息
*/
public FindUserByEmailRspDTO findUserByEmail(String email) {
FindUserByEmailReqDTO findUserByEmailReqDTO = new FindUserByEmailReqDTO();
findUserByEmailReqDTO.setEmail(email);
Response<FindUserByEmailRspDTO> response = userFeignApi.findByPhone(findUserByEmailReqDTO);
if (!response.isSuccess()) {
return null;
}
return response.getData();
}
/**
* 密码更新
*
* @param encodePassword 加密后的密码
*/
public void updatePassword(String encodePassword) {
UpdateUserPasswordReqDTO updateUserPasswordReqDTO = new UpdateUserPasswordReqDTO();
updateUserPasswordReqDTO.setEncodePassword(encodePassword);
userFeignApi.updatePassword(updateUserPasswordReqDTO);
}
}

View File

@@ -0,0 +1,29 @@
package com.hanserwei.hannote.auth.service;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
public interface AuthService {
/**
* 登录与注册
*
* @param reqVO 请求参数
* @return 响应结果
*/
Response<String> loginAndRegister(UserLoginReqVO reqVO);
/**
* 退出登录
* @return 响应结果
*/
Response<?> logout();
/**
* 修改密码
* @param updatePasswordReqVO 请求参数
* @return 响应结果
*/
Response<?> updatePassword(UpdatePasswordReqVO updatePasswordReqVO);
}

View File

@@ -1,17 +0,0 @@
package com.hanserwei.hannote.auth.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.domain.dataobject.UserDO;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
public interface UserService extends IService<UserDO> {
/**
* 登录与注册
*
* @param reqVO 请求参数
* @return 响应结果
*/
Response<String> loginAndRegister(UserLoginReqVO reqVO);
}

View File

@@ -0,0 +1,130 @@
package com.hanserwei.hannote.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.base.Preconditions;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.auth.constant.RedisKeyConstants;
import com.hanserwei.hannote.auth.enums.LoginTypeEnum;
import com.hanserwei.hannote.auth.enums.ResponseCodeEnum;
import com.hanserwei.hannote.auth.model.vo.user.UpdatePasswordReqVO;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
import com.hanserwei.hannote.auth.rpc.UserRpcService;
import com.hanserwei.hannote.auth.service.AuthService;
import com.hanserwei.hannote.user.dto.resp.FindUserByEmailRspDTO;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final RedisTemplate<String, Object> redisTemplate;
private final UserRpcService userRpcService;
@Resource(name = "authTaskExecutor")
private ThreadPoolTaskExecutor authTaskExecutor;
private final PasswordEncoder passwordEncoder;
@Override
public Response<String> loginAndRegister(UserLoginReqVO reqVO) {
Integer loginType = reqVO.getType();
String email = reqVO.getEmail();
LoginTypeEnum loginTypeEnum = LoginTypeEnum.valueOf(loginType);
Long userId = null;
//noinspection DataFlowIssue
switch (loginTypeEnum) {
case VERIFICATION_CODE:
String verificationCode = reqVO.getCode();
//校验参数是否为空
Preconditions.checkArgument(StringUtils.isNotBlank(verificationCode), "验证码不能为空");
//构建验证码的RedisKey
String key = RedisKeyConstants.buildVerificationCodeKey(email);
// 查询存储在 Redis 中该用户的登录验证码
String sentCode = (String) redisTemplate.opsForValue().get(key);
// 判断用户提交的验证码,与 Redis 中的验证码是否一致
if (!StrUtil.equals(verificationCode, sentCode)) {
throw new ApiException(ResponseCodeEnum.VERIFICATION_CODE_ERROR);
}
// RPC: 调用用户服务,注册用户
Long userIdTmp = userRpcService.registerUser(email);
// 若调用用户服务,返回的用户 ID 为空,则提示登录失败
if (Objects.isNull(userIdTmp)) {
throw new ApiException(ResponseCodeEnum.LOGIN_FAIL);
}
userId = userIdTmp;
break;
case PASSWORD:
String password = reqVO.getPassword();
// RPC: 调用用户服务,通过手机号查询用户
FindUserByEmailRspDTO findUserByEmailRspDTO = userRpcService.findUserByEmail(email);
// 判断该手机号是否注册
if (Objects.isNull(findUserByEmailRspDTO)) {
throw new ApiException(ResponseCodeEnum.USER_NOT_FOUND);
}
// 拿到密文密码
String encodePassword = findUserByEmailRspDTO.getPassword();
// 匹配密码是否一致
boolean isPasswordCorrect = passwordEncoder.matches(password, encodePassword);
// 如果不正确,则抛出业务异常,提示用户名或者密码不正确
if (!isPasswordCorrect) {
throw new ApiException(ResponseCodeEnum.MAIL_OR_PASSWORD_ERROR);
}
userId = findUserByEmailRspDTO.getId();
break;
default:
break;
}
// SaToken 登录用户,并返回 token 令牌
StpUtil.login(userId);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return Response.success(tokenInfo.getTokenValue());
}
@Override
public Response<?> logout() {
Long userId = LoginUserContextHolder.getUserId();
authTaskExecutor.submit(() -> {
Long userId2 = LoginUserContextHolder.getUserId();
log.info("==> 异步线程中获取 userId: {}", userId2);
});
StpUtil.logout(userId);
return Response.success();
}
@Override
public Response<?> updatePassword(UpdatePasswordReqVO updatePasswordReqVO) {
// 新密码
String newPassword = updatePasswordReqVO.getNewPassword();
// 加密后的密码
String encodePassword = passwordEncoder.encode(newPassword);
// RPC: 调用用户服务:更新密码
userRpcService.updatePassword(encodePassword);
return Response.success();
}
}

View File

@@ -1,145 +0,0 @@
package com.hanserwei.hannote.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.hanserwei.framework.common.enums.DeletedEnum;
import com.hanserwei.framework.common.enums.StatusEnum;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.auth.constant.RedisKeyConstants;
import com.hanserwei.hannote.auth.constant.RoleConstants;
import com.hanserwei.hannote.auth.domain.dataobject.RoleDO;
import com.hanserwei.hannote.auth.domain.dataobject.UserDO;
import com.hanserwei.hannote.auth.domain.dataobject.UserRoleDO;
import com.hanserwei.hannote.auth.domain.mapper.RoleDOMapper;
import com.hanserwei.hannote.auth.domain.mapper.UserDOMapper;
import com.hanserwei.hannote.auth.domain.mapper.UserRoleDOMapper;
import com.hanserwei.hannote.auth.enums.LoginTypeEnum;
import com.hanserwei.hannote.auth.enums.ResponseCodeEnum;
import com.hanserwei.hannote.auth.model.vo.user.UserLoginReqVO;
import com.hanserwei.hannote.auth.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implements UserService {
private final RedisTemplate<String, Object> redisTemplate;
private final UserRoleDOMapper userRoleDOMapper;
private final TransactionTemplate transactionTemplate;
private final RoleDOMapper roleDOMapper;
@Override
public Response<String> loginAndRegister(UserLoginReqVO reqVO) {
Integer loginType = reqVO.getType();
String email = reqVO.getEmail();
LoginTypeEnum loginTypeEnum = LoginTypeEnum.valueOf(loginType);
Long userId = null;
//noinspection DataFlowIssue
switch (loginTypeEnum) {
case VERIFICATION_CODE:
String verificationCode = reqVO.getCode();
//校验参数是否为空
Preconditions.checkArgument(StringUtils.isNotBlank(verificationCode), "验证码不能为空");
//构建验证码的RedisKey
String key = RedisKeyConstants.buildVerificationCodeKey(email);
// 查询存储在 Redis 中该用户的登录验证码
String sentCode = (String) redisTemplate.opsForValue().get(key);
// 判断用户提交的验证码,与 Redis 中的验证码是否一致
if (!StrUtil.equals(verificationCode, sentCode)) {
throw new ApiException(ResponseCodeEnum.VERIFICATION_CODE_ERROR);
}
//通过邮箱查询用户
UserDO userDO = this.getOne(new QueryWrapper<UserDO>().eq("email", email));
log.info("==> 用户是否注册, email: {}, userDO: {}", email, JsonUtils.toJsonString(userDO));
// 判断是否注册
if (Objects.isNull(userDO)) {
// 若此用户还没有注册,系统自动注册该用户
userId = registerUser(email);
} else {
// 已注册,则获取其用户 ID
userId = userDO.getId();
}
break;
case PASSWORD:
break;
default:
break;
}
// SaToken 登录用户,并返回 token 令牌
StpUtil.login(userId);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return Response.success(tokenInfo.getTokenValue());
}
public Long registerUser(String email) {
return transactionTemplate.execute(status -> {
try {
// 获取全局自增的小憨书ID
Long hanNoteId = redisTemplate.opsForValue().increment(RedisKeyConstants.HAN_NOTE_ID_GENERATOR_KEY);
UserDO userDO = UserDO.builder()
.hanNoteId(String.valueOf(hanNoteId)) // 自动生成小红书号 ID
.nickname("小憨憨" + hanNoteId) // 自动生成昵称, 如小憨憨10000
.status(StatusEnum.ENABLE.getValue()) // 状态为启用
.email(email)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue()) // 逻辑删除
.build();
// 添加入库
this.save(userDO);
// 获取入库的用户ID
Long userId = userDO.getId();
// 添加默认用户角色
UserRoleDO userRoleDO = UserRoleDO.builder()
.userId(userId)
.roleId(RoleConstants.COMMON_USER_ROLE_ID)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue())
.build();
userRoleDOMapper.insert(userRoleDO);
RoleDO roleDO = roleDOMapper.selectByPrimaryKey(RoleConstants.COMMON_USER_ROLE_ID);
// 将该用户的角色 ID 存入 Redis 中,指定初始容量为 1这样可以减少在扩容时的性能开销
List<String> roles = new ArrayList<>(1);
roles.add(roleDO.getRoleKey());
String userRolesKey = RedisKeyConstants.buildUserRoleKey(userId);
redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务为回滚
log.error("==> 系统注册用户异常: ", e);
return null;
}
});
}
}

View File

@@ -34,13 +34,6 @@ spring:
mail.smtp.starttls.required: true
server:
port: 8080 # 项目启动的端口
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
banner: false
mapper-locations: classpath*:/mapperxml/*.xml
logging:
level:
com.hanserwei.hannote.auth.domain.mapper: debug
@@ -62,5 +55,3 @@ sa-token:
token-style: random-128
# 是否输出操作日志
is-log: true
alarm:
type: mail # 告警类型

View File

@@ -1,34 +0,0 @@
package com.hanserwei.hannote.auth;
import com.alibaba.druid.filter.config.ConfigTools;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
class DruidTests {
/**
* Druid 密码加密
*/
@Test
@SneakyThrows
void testEncodePassword() {
// 你的密码
String password = "mysql";
String[] arr = ConfigTools.genKeyPair(512);
// 私钥
log.info("privateKey: {}", arr[0]);
// 公钥
log.info("publicKey: {}", arr[1]);
// 通过私钥加密密码
String encodePassword = ConfigTools.encrypt(arr[0], password);
log.info("password: {}", encodePassword);
}
}

View File

@@ -1,14 +0,0 @@
package com.hanserwei.hannote.auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
class HanNoteAuthApplicationTests {
}

View File

@@ -1,49 +0,0 @@
package com.hanserwei.hannote.auth;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
@Slf4j
class RedisTests {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* set key value
*/
@Test
void testSetKeyValue() {
// 添加一个 key 为 name, value 值为 Hanserwei
redisTemplate.opsForValue().set("name", "Hanserwei");
}
/**
* 判断某个 key 是否存在
*/
@Test
void testHasKey() {
log.info("key 是否存在:{}", redisTemplate.hasKey("name"));
}
/**
* 获取某个 key 的 value
*/
@Test
void testGetValue() {
log.info("value 值:{}", redisTemplate.opsForValue().get("name"));
}
/**
* 删除某个 key
*/
@Test
void testDelete() {
redisTemplate.delete("name");
}
}

View File

@@ -1,23 +0,0 @@
package com.hanserwei.hannote.auth;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@SpringBootTest
@Slf4j
public class ThreadPoolTaskExecutorTests {
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 测试线程池
*/
@Test
void testSubmit() {
threadPoolTaskExecutor.submit(() -> log.info("异步线程中说: Hanserwei是傻逼"));
}
}

View File

@@ -0,0 +1,37 @@
<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.hanserwei</groupId>
<artifactId>han-note-distributed-id-generator</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-distributed-id-generator-api</artifactId>
<name>${project.artifactId}</name>
<description>RPC层, 供其他服务调用</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- OpenFeign -->
<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>
</dependencies>
</project>

View File

@@ -0,0 +1,136 @@
<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.hanserwei</groupId>
<artifactId>han-note-distributed-id-generator</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>hannote-distributed-id-generator-biz</artifactId>
<name>${project.artifactId}</name>
<description>分布式 ID 生成业务层</description>
<properties>
<common-io.version>2.4</common-io.version>
<perf4j.version>0.9.16</perf4j.version>
<druid.version>1.0.18</druid.version>
<mybatis.version>3.3.0</mybatis.version>
<curator-recipes.version>2.6.0</curator-recipes.version>
<zookeeper.version>3.6.0</zookeeper.version>
</properties>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>
<dependency>
<groupId>org.perf4j</groupId>
<artifactId>perf4j</artifactId>
<version>${perf4j.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- zk -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>${curator-recipes.version}</version>
<!-- 为防止日志冲突,添加以下排除项 -->
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
<exclusion>
<artifactId>org.slf4j</artifactId>
<groupId>slf4j-reload4j</groupId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<!-- 为防止日志冲突,添加以下排除项 -->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.distributed.id.generator.biz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HannoteDistributedIdGeneratorBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteDistributedIdGeneratorBizApplication.class, args);
}
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.distributed.id.generator.biz.constant;
public class Constants {
public static final String LEAF_SEGMENT_ENABLE = "leaf.segment.enable";
public static final String LEAF_JDBC_URL = "leaf.jdbc.url";
public static final String LEAF_JDBC_USERNAME = "leaf.jdbc.username";
public static final String LEAF_JDBC_PASSWORD = "leaf.jdbc.password";
public static final String LEAF_SNOWFLAKE_ENABLE = "leaf.snowflake.enable";
public static final String LEAF_SNOWFLAKE_PORT = "leaf.snowflake.port";
public static final String LEAF_SNOWFLAKE_ZK_ADDRESS = "leaf.snowflake.zk.address";
}

View File

@@ -0,0 +1,47 @@
package com.hanserwei.hannote.distributed.id.generator.biz.controller;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Status;
import com.hanserwei.hannote.distributed.id.generator.biz.exception.LeafServerException;
import com.hanserwei.hannote.distributed.id.generator.biz.exception.NoKeyException;
import com.hanserwei.hannote.distributed.id.generator.biz.service.SegmentService;
import com.hanserwei.hannote.distributed.id.generator.biz.service.SnowflakeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/id")
@Slf4j
public class LeafController {
@Resource
private SegmentService segmentService;
@Resource
private SnowflakeService snowflakeService;
@RequestMapping(value = "/segment/get/{key}")
public String getSegmentId(@PathVariable("key") String key) {
return get(key, segmentService.getId(key));
}
@RequestMapping(value = "/snowflake/get/{key}")
public String getSnowflakeId(@PathVariable("key") String key) {
return get(key, snowflakeService.getId(key));
}
private String get(@PathVariable("key") String key, Result id) {
Result result;
if (key == null || key.isEmpty()) {
throw new NoKeyException();
}
result = id;
if (result.getStatus().equals(Status.EXCEPTION)) {
throw new LeafServerException(result.toString());
}
return String.valueOf(result.getId());
}
}

View File

@@ -0,0 +1,104 @@
package com.hanserwei.hannote.distributed.id.generator.biz.controller;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.SegmentIDGenImpl;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.SegmentBuffer;
import com.hanserwei.hannote.distributed.id.generator.biz.model.SegmentBufferView;
import com.hanserwei.hannote.distributed.id.generator.biz.service.SegmentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class LeafMonitorController {
private Logger logger = LoggerFactory.getLogger(LeafMonitorController.class);
@Autowired
private SegmentService segmentService;
@RequestMapping(value = "cache")
public String getCache(Model model) {
Map<String, SegmentBufferView> data = new HashMap<>();
SegmentIDGenImpl segmentIDGen = segmentService.getIdGen();
if (segmentIDGen == null) {
throw new IllegalArgumentException("You should config leaf.segment.enable=true first");
}
Map<String, SegmentBuffer> cache = segmentIDGen.getCache();
for (Map.Entry<String, SegmentBuffer> entry : cache.entrySet()) {
SegmentBufferView sv = new SegmentBufferView();
SegmentBuffer buffer = entry.getValue();
sv.setInitOk(buffer.isInitOk());
sv.setKey(buffer.getKey());
sv.setPos(buffer.getCurrentPos());
sv.setNextReady(buffer.isNextReady());
sv.setMax0(buffer.getSegments()[0].getMax());
sv.setValue0(buffer.getSegments()[0].getValue().get());
sv.setStep0(buffer.getSegments()[0].getStep());
sv.setMax1(buffer.getSegments()[1].getMax());
sv.setValue1(buffer.getSegments()[1].getValue().get());
sv.setStep1(buffer.getSegments()[1].getStep());
data.put(entry.getKey(), sv);
}
logger.info("Cache info {}", data);
model.addAttribute("data", data);
return "segment";
}
@RequestMapping(value = "db")
public String getDb(Model model) {
SegmentIDGenImpl segmentIDGen = segmentService.getIdGen();
if (segmentIDGen == null) {
throw new IllegalArgumentException("You should config leaf.segment.enable=true first");
}
List<LeafAlloc> items = segmentIDGen.getAllLeafAllocs();
logger.info("DB info {}", items);
model.addAttribute("items", items);
return "db";
}
/**
* the output is like this:
* {
* "timestamp": "1567733700834(2019-09-06 09:35:00.834)",
* "sequenceId": "3448",
* "workerId": "39"
* }
*/
@RequestMapping(value = "decodeSnowflakeId")
@ResponseBody
public Map<String, String> decodeSnowflakeId(@RequestParam("snowflakeId") String snowflakeIdStr) {
Map<String, String> map = new HashMap<>();
try {
long snowflakeId = Long.parseLong(snowflakeIdStr);
long originTimestamp = (snowflakeId >> 22) + 1288834974657L;
Date date = new Date(originTimestamp);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
map.put("timestamp", String.valueOf(originTimestamp) + "(" + sdf.format(date) + ")");
long workerId = (snowflakeId >> 12) ^ (snowflakeId >> 22 << 10);
map.put("workerId", String.valueOf(workerId));
long sequence = snowflakeId ^ (snowflakeId >> 12 << 12);
map.put("sequenceId", String.valueOf(sequence));
} catch (NumberFormatException e) {
map.put("errorMsg", "snowflake Id反解析发生异常!");
}
return map;
}
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
public interface IDGen {
Result get(String key);
boolean init();
}

View File

@@ -0,0 +1,27 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
public class CheckVO {
private long timestamp;
private int workID;
public CheckVO(long timestamp, int workID) {
this.timestamp = timestamp;
this.workID = workID;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public int getWorkID() {
return workID;
}
public void setWorkID(int workID) {
this.workID = workID;
}
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Properties;
public class PropertyFactory {
private static final Logger logger = LoggerFactory.getLogger(PropertyFactory.class);
private static final Properties prop = new Properties();
static {
try {
prop.load(PropertyFactory.class.getClassLoader().getResourceAsStream("leaf.properties"));
} catch (IOException e) {
logger.warn("Load Properties Ex", e);
}
}
public static Properties getProperties() {
return prop;
}
}

View File

@@ -0,0 +1,39 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
public class Result {
private long id;
private Status status;
public Result() {
}
public Result(long id, Status status) {
this.id = id;
this.status = status;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Result{");
sb.append("id=").append(id);
sb.append(", status=").append(status);
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,6 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
public enum Status {
SUCCESS,
EXCEPTION
}

View File

@@ -0,0 +1,75 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
public class Utils {
private static final Logger logger = LoggerFactory.getLogger(Utils.class);
public static String getIp() {
String ip;
try {
List<String> ipList = getHostAddress(null);
// default the first
ip = (!ipList.isEmpty()) ? ipList.get(0) : "";
} catch (Exception ex) {
ip = "";
logger.warn("Utils get IP warn", ex);
}
return ip;
}
public static String getIp(String interfaceName) {
String ip;
interfaceName = interfaceName.trim();
try {
List<String> ipList = getHostAddress(interfaceName);
ip = (!ipList.isEmpty()) ? ipList.get(0) : "";
} catch (Exception ex) {
ip = "";
logger.warn("Utils get IP warn", ex);
}
return ip;
}
/**
* 获取已激活网卡的IP地址
*
* @param interfaceName 可指定网卡名称,null则获取全部
* @return List<String>
*/
private static List<String> getHostAddress(String interfaceName) throws SocketException {
List<String> ipList = new ArrayList<String>(5);
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
Enumeration<InetAddress> allAddress = ni.getInetAddresses();
while (allAddress.hasMoreElements()) {
InetAddress address = allAddress.nextElement();
if (address.isLoopbackAddress()) {
// skip the loopback addr
continue;
}
if (address instanceof Inet6Address) {
// skip the IPv6 addr
continue;
}
String hostAddress = address.getHostAddress();
if (null == interfaceName) {
ipList.add(hostAddress);
} else if (interfaceName.equals(ni.getDisplayName())) {
ipList.add(hostAddress);
}
}
}
return ipList;
}
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.common;
import com.hanserwei.hannote.distributed.id.generator.biz.core.IDGen;
public class ZeroIDGen implements IDGen {
@Override
public Result get(String key) {
return new Result(0, Status.SUCCESS);
}
@Override
public boolean init() {
return true;
}
}

View File

@@ -0,0 +1,294 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment;
import com.hanserwei.hannote.distributed.id.generator.biz.core.IDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Status;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocDao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.Segment;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.SegmentBuffer;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
public class SegmentIDGenImpl implements IDGen {
private static final Logger logger = LoggerFactory.getLogger(SegmentIDGenImpl.class);
/**
* IDCache未初始化成功时的异常码
*/
private static final long EXCEPTION_ID_IDCACHE_INIT_FALSE = -1;
/**
* key不存在时的异常码
*/
private static final long EXCEPTION_ID_KEY_NOT_EXISTS = -2;
/**
* SegmentBuffer中的两个Segment均未从DB中装载时的异常码
*/
private static final long EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL = -3;
/**
* 最大步长不超过100,0000
*/
private static final int MAX_STEP = 1000000;
/**
* 一个Segment维持时间为15分钟
*/
private static final long SEGMENT_DURATION = 15 * 60 * 1000L;
private ExecutorService service = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new UpdateThreadFactory());
private volatile boolean initOK = false;
private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<String, SegmentBuffer>();
private IDAllocDao dao;
public static class UpdateThreadFactory implements ThreadFactory {
private static int threadInitNumber = 0;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Thread-Segment-Update-" + nextThreadNum());
}
}
@Override
public boolean init() {
logger.info("Init ...");
// 确保加载到kv后才初始化成功
updateCacheFromDb();
initOK = true;
updateCacheFromDbAtEveryMinute();
return initOK;
}
private void updateCacheFromDbAtEveryMinute() {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("check-idCache-thread");
t.setDaemon(true);
return t;
}
});
service.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
updateCacheFromDb();
}
}, 60, 60, TimeUnit.SECONDS);
}
private void updateCacheFromDb() {
logger.info("update cache from db");
StopWatch sw = new Slf4JStopWatch();
try {
List<String> dbTags = dao.getAllTags();
if (dbTags == null || dbTags.isEmpty()) {
return;
}
List<String> cacheTags = new ArrayList<String>(cache.keySet());
Set<String> insertTagsSet = new HashSet<>(dbTags);
Set<String> removeTagsSet = new HashSet<>(cacheTags);
//db中新加的tags灌进cache
for(int i = 0; i < cacheTags.size(); i++){
String tmp = cacheTags.get(i);
if(insertTagsSet.contains(tmp)){
insertTagsSet.remove(tmp);
}
}
for (String tag : insertTagsSet) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(tag);
Segment segment = buffer.getCurrent();
segment.setValue(new AtomicLong(0));
segment.setMax(0);
segment.setStep(0);
cache.put(tag, buffer);
logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
}
//cache中已失效的tags从cache删除
for(int i = 0; i < dbTags.size(); i++){
String tmp = dbTags.get(i);
if(removeTagsSet.contains(tmp)){
removeTagsSet.remove(tmp);
}
}
for (String tag : removeTagsSet) {
cache.remove(tag);
logger.info("Remove tag {} from IdCache", tag);
}
} catch (Exception e) {
logger.warn("update cache from db exception", e);
} finally {
sw.stop("updateCacheFromDb");
}
}
@Override
public Result get(final String key) {
if (!initOK) {
return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
}
if (cache.containsKey(key)) {
SegmentBuffer buffer = cache.get(key);
if (!buffer.isInitOk()) {
synchronized (buffer) {
if (!buffer.isInitOk()) {
try {
updateSegmentFromDb(key, buffer.getCurrent());
logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());
buffer.setInitOk(true);
} catch (Exception e) {
logger.warn("Init buffer {} exception", buffer.getCurrent(), e);
}
}
}
}
return getIdFromSegmentBuffer(cache.get(key));
}
return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}
public void updateSegmentFromDb(String key, Segment segment) {
StopWatch sw = new Slf4JStopWatch();
SegmentBuffer buffer = segment.getBuffer();
LeafAlloc leafAlloc;
if (!buffer.isInitOk()) {
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
buffer.setStep(leafAlloc.getStep());
buffer.setMinStep(leafAlloc.getStep());//leafAlloc中的step为DB中的step
} else if (buffer.getUpdateTimestamp() == 0) {
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(leafAlloc.getStep());
buffer.setMinStep(leafAlloc.getStep());//leafAlloc中的step为DB中的step
} else {
long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
if (duration < SEGMENT_DURATION) {
if (nextStep * 2 > MAX_STEP) {
//do nothing
} else {
nextStep = nextStep * 2;
}
} else if (duration < SEGMENT_DURATION * 2) {
//do nothing with nextStep
} else {
nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep;
}
logger.info("leafKey[{}], step[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep);
LeafAlloc temp = new LeafAlloc();
temp.setKey(key);
temp.setStep(nextStep);
leafAlloc = dao.updateMaxIdByCustomStepAndGetLeafAlloc(temp);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(nextStep);
buffer.setMinStep(leafAlloc.getStep());//leafAlloc的step为DB中的step
}
// must set value before set max
long value = leafAlloc.getMaxId() - buffer.getStep();
segment.getValue().set(value);
segment.setMax(leafAlloc.getMaxId());
segment.setStep(buffer.getStep());
sw.stop("updateSegmentFromDb", key + " " + segment);
}
public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
while (true) {
buffer.rLock().lock();
try {
final Segment segment = buffer.getCurrent();
if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
service.execute(new Runnable() {
@Override
public void run() {
Segment next = buffer.getSegments()[buffer.nextPos()];
boolean updateOk = false;
try {
updateSegmentFromDb(buffer.getKey(), next);
updateOk = true;
logger.info("update segment {} from db {}", buffer.getKey(), next);
} catch (Exception e) {
logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);
} finally {
if (updateOk) {
buffer.wLock().lock();
buffer.setNextReady(true);
buffer.getThreadRunning().set(false);
buffer.wLock().unlock();
} else {
buffer.getThreadRunning().set(false);
}
}
}
});
}
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
} finally {
buffer.rLock().unlock();
}
waitAndSleep(buffer);
buffer.wLock().lock();
try {
final Segment segment = buffer.getCurrent();
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
if (buffer.isNextReady()) {
buffer.switchPos();
buffer.setNextReady(false);
} else {
logger.error("Both two segments in {} are not ready!", buffer);
return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);
}
} finally {
buffer.wLock().unlock();
}
}
}
private void waitAndSleep(SegmentBuffer buffer) {
int roll = 0;
while (buffer.getThreadRunning().get()) {
roll += 1;
if(roll > 10000) {
try {
TimeUnit.MILLISECONDS.sleep(10);
break;
} catch (InterruptedException e) {
logger.warn("Thread {} Interrupted",Thread.currentThread().getName());
break;
}
}
}
}
public List<LeafAlloc> getAllLeafAllocs() {
return dao.getAllLeafAllocs();
}
public Map<String, SegmentBuffer> getCache() {
return cache;
}
public IDAllocDao getDao() {
return dao;
}
public void setDao(IDAllocDao dao) {
this.dao = dao;
}
}

View File

@@ -0,0 +1,13 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import java.util.List;
public interface IDAllocDao {
List<LeafAlloc> getAllLeafAllocs();
LeafAlloc updateMaxIdAndGetLeafAlloc(String tag);
LeafAlloc updateMaxIdByCustomStepAndGetLeafAlloc(LeafAlloc leafAlloc);
List<String> getAllTags();
}

View File

@@ -0,0 +1,35 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface IDAllocMapper {
@Select("SELECT biz_tag, max_id, step, update_time FROM leaf_alloc")
@Results(value = {
@Result(column = "biz_tag", property = "key"),
@Result(column = "max_id", property = "maxId"),
@Result(column = "step", property = "step"),
@Result(column = "update_time", property = "updateTime")
})
List<LeafAlloc> getAllLeafAllocs();
@Select("SELECT biz_tag, max_id, step FROM leaf_alloc WHERE biz_tag = #{tag}")
@Results(value = {
@Result(column = "biz_tag", property = "key"),
@Result(column = "max_id", property = "maxId"),
@Result(column = "step", property = "step")
})
LeafAlloc getLeafAlloc(@Param("tag") String tag);
@Update("UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = #{tag}")
void updateMaxId(@Param("tag") String tag);
@Update("UPDATE leaf_alloc SET max_id = max_id + #{step} WHERE biz_tag = #{key}")
void updateMaxIdByCustomStep(@Param("leafAlloc") LeafAlloc leafAlloc);
@Select("SELECT biz_tag FROM leaf_alloc")
List<String> getAllTags();
}

View File

@@ -0,0 +1,75 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.impl;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocDao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import javax.sql.DataSource;
import java.util.List;
public class IDAllocDaoImpl implements IDAllocDao {
SqlSessionFactory sqlSessionFactory;
public IDAllocDaoImpl(DataSource dataSource) {
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(IDAllocMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
}
@Override
public List<LeafAlloc> getAllLeafAllocs() {
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
return sqlSession.selectList("com.hanserwei.hannote.segment.dao.IDAllocMapper.getAllLeafAllocs");
} finally {
sqlSession.close();
}
}
@Override
public LeafAlloc updateMaxIdAndGetLeafAlloc(String tag) {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
sqlSession.update("com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper.updateMaxId", tag);
LeafAlloc result = sqlSession.selectOne("com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper.getLeafAlloc", tag);
sqlSession.commit();
return result;
} finally {
sqlSession.close();
}
}
@Override
public LeafAlloc updateMaxIdByCustomStepAndGetLeafAlloc(LeafAlloc leafAlloc) {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
sqlSession.update("com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper.updateMaxIdByCustomStep", leafAlloc);
LeafAlloc result = sqlSession.selectOne("com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper.getLeafAlloc", leafAlloc.getKey());
sqlSession.commit();
return result;
} finally {
sqlSession.close();
}
}
@Override
public List<String> getAllTags() {
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
return sqlSession.selectList("com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocMapper.getAllTags");
} finally {
sqlSession.close();
}
}
}

View File

@@ -0,0 +1,40 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model;
public class LeafAlloc {
private String key;
private long maxId;
private int step;
private String updateTime;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getMaxId() {
return maxId;
}
public void setMaxId(long maxId) {
this.maxId = maxId;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public String getUpdateTime() {
return updateTime;
}
public void setUpdateTime(String updateTime) {
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,59 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model;
import java.util.concurrent.atomic.AtomicLong;
public class Segment {
private AtomicLong value = new AtomicLong(0);
private volatile long max;
private volatile int step;
private SegmentBuffer buffer;
public Segment(SegmentBuffer buffer) {
this.buffer = buffer;
}
public AtomicLong getValue() {
return value;
}
public void setValue(AtomicLong value) {
this.value = value;
}
public long getMax() {
return max;
}
public void setMax(long max) {
this.max = max;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public SegmentBuffer getBuffer() {
return buffer;
}
public long getIdle() {
return this.getMax() - getValue().get();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Segment(");
sb.append("value:");
sb.append(value);
sb.append(",max:");
sb.append(max);
sb.append(",step:");
sb.append(step);
sb.append(")");
return sb.toString();
}
}

View File

@@ -0,0 +1,129 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 双buffer
*/
public class SegmentBuffer {
private String key;
private Segment[] segments; //双buffer
private volatile int currentPos; //当前的使用的segment的index
private volatile boolean nextReady; //下一个segment是否处于可切换状态
private volatile boolean initOk; //是否初始化完成
private final AtomicBoolean threadRunning; //线程是否在运行中
private final ReadWriteLock lock;
private volatile int step;
private volatile int minStep;
private volatile long updateTimestamp;
public SegmentBuffer() {
segments = new Segment[]{new Segment(this), new Segment(this)};
currentPos = 0;
nextReady = false;
initOk = false;
threadRunning = new AtomicBoolean(false);
lock = new ReentrantReadWriteLock();
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Segment[] getSegments() {
return segments;
}
public Segment getCurrent() {
return segments[currentPos];
}
public int getCurrentPos() {
return currentPos;
}
public int nextPos() {
return (currentPos + 1) % 2;
}
public void switchPos() {
currentPos = nextPos();
}
public boolean isInitOk() {
return initOk;
}
public void setInitOk(boolean initOk) {
this.initOk = initOk;
}
public boolean isNextReady() {
return nextReady;
}
public void setNextReady(boolean nextReady) {
this.nextReady = nextReady;
}
public AtomicBoolean getThreadRunning() {
return threadRunning;
}
public Lock rLock() {
return lock.readLock();
}
public Lock wLock() {
return lock.writeLock();
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public int getMinStep() {
return minStep;
}
public void setMinStep(int minStep) {
this.minStep = minStep;
}
public long getUpdateTimestamp() {
return updateTimestamp;
}
public void setUpdateTimestamp(long updateTimestamp) {
this.updateTimestamp = updateTimestamp;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SegmentBuffer{");
sb.append("key='").append(key).append('\'');
sb.append(", segments=").append(Arrays.toString(segments));
sb.append(", currentPos=").append(currentPos);
sb.append(", nextReady=").append(nextReady);
sb.append(", initOk=").append(initOk);
sb.append(", threadRunning=").append(threadRunning);
sb.append(", step=").append(step);
sb.append(", minStep=").append(minStep);
sb.append(", updateTimestamp=").append(updateTimestamp);
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,113 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake;
import com.google.common.base.Preconditions;
import com.hanserwei.hannote.distributed.id.generator.biz.core.IDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Status;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
public class SnowflakeIDGenImpl implements IDGen {
@Override
public boolean init() {
return true;
}
private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeIDGenImpl.class);
private final long twepoch;
private final long workerIdBits = 10L;
private final long maxWorkerId = ~(-1L << workerIdBits);//最大能够分配的workerid =1023
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long timestampLeftShift = sequenceBits + workerIdBits;
private final long sequenceMask = ~(-1L << sequenceBits);
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private static final Random RANDOM = new Random();
public SnowflakeIDGenImpl(String zkAddress, int port) {
//Thu Nov 04 2010 09:42:54 GMT+0800 (中国标准时间)
this(zkAddress, port, 1288834974657L);
}
/**
* @param zkAddress zk地址
* @param port snowflake监听端口
* @param twepoch 起始的时间戳
*/
public SnowflakeIDGenImpl(String zkAddress, int port, long twepoch) {
this.twepoch = twepoch;
Preconditions.checkArgument(timeGen() > twepoch, "Snowflake not support twepoch gt currentTime");
final String ip = Utils.getIp();
SnowflakeZookeeperHolder holder = new SnowflakeZookeeperHolder(ip, String.valueOf(port), zkAddress);
LOGGER.info("twepoch:{} ,ip:{} ,zkAddress:{} port:{}", twepoch, ip, zkAddress, port);
boolean initFlag = holder.init();
if (initFlag) {
workerId = holder.getWorkerID();
LOGGER.info("START SUCCESS USE ZK WORKERID-{}", workerId);
} else {
Preconditions.checkArgument(initFlag, "Snowflake Id Gen is not init ok");
}
Preconditions.checkArgument(workerId >= 0 && workerId <= maxWorkerId, "workerID must gte 0 and lte 1023");
}
@Override
public synchronized Result get(String key) {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
return new Result(-1, Status.EXCEPTION);
}
} catch (InterruptedException e) {
LOGGER.error("wait interrupted");
return new Result(-2, Status.EXCEPTION);
}
} else {
return new Result(-3, Status.EXCEPTION);
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
//seq 为0的时候表示是下一毫秒时间开始对seq做随机
sequence = RANDOM.nextInt(100);
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//如果是新的ms开始
sequence = RANDOM.nextInt(100);
}
lastTimestamp = timestamp;
long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
return new Result(id, Status.SUCCESS);
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
public long getWorkerId() {
return workerId;
}
}

View File

@@ -0,0 +1,292 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.PropertyFactory;
import com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake.exception.CheckLastTimeException;
import org.apache.commons.io.FileUtils;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryUntilElapsed;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public class SnowflakeZookeeperHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeZookeeperHolder.class);
private String zk_AddressNode = null;//保存自身的key ip:port-000000001
private String listenAddress = null;//保存自身的key ip:port
private int workerID;
private static final String PREFIX_ZK_PATH = "/snowflake/" + PropertyFactory.getProperties().getProperty("leaf.name");
private static final String PROP_PATH = System.getProperty("java.io.tmpdir") + File.separator + PropertyFactory.getProperties().getProperty("leaf.name") + "/leafconf/{port}/workerID.properties";
private static final String PATH_FOREVER = PREFIX_ZK_PATH + "/forever";//保存所有数据持久的节点
private String ip;
private String port;
private String connectionString;
private long lastUpdateTime;
public SnowflakeZookeeperHolder(String ip, String port, String connectionString) {
this.ip = ip;
this.port = port;
this.listenAddress = ip + ":" + port;
this.connectionString = connectionString;
}
public boolean init() {
try {
CuratorFramework curator = createWithOptions(connectionString, new RetryUntilElapsed(1000, 4), 10000, 6000);
curator.start();
Stat stat = curator.checkExists().forPath(PATH_FOREVER);
if (stat == null) {
//不存在根节点,机器第一次启动,创建/snowflake/ip:port-000000000,并上传数据
zk_AddressNode = createNode(curator);
//worker id 默认是0
updateLocalWorkerID(workerID);
//定时上报本机时间给forever节点
ScheduledUploadData(curator, zk_AddressNode);
return true;
} else {
Map<String, Integer> nodeMap = Maps.newHashMap();//ip:port->00001
Map<String, String> realNode = Maps.newHashMap();//ip:port->(ipport-000001)
//存在根节点,先检查是否有属于自己的根节点
List<String> keys = curator.getChildren().forPath(PATH_FOREVER);
for (String key : keys) {
String[] nodeKey = key.split("-");
realNode.put(nodeKey[0], key);
nodeMap.put(nodeKey[0], Integer.parseInt(nodeKey[1]));
}
Integer workerid = nodeMap.get(listenAddress);
if (workerid != null) {
//有自己的节点,zk_AddressNode=ip:port
zk_AddressNode = PATH_FOREVER + "/" + realNode.get(listenAddress);
workerID = workerid;//启动worder时使用会使用
if (!checkInitTimeStamp(curator, zk_AddressNode)) {
throw new CheckLastTimeException("init timestamp check error,forever node timestamp gt this node time");
}
//准备创建临时节点
doService(curator);
updateLocalWorkerID(workerID);
LOGGER.info("[Old NODE]find forever node have this endpoint ip-{} port-{} workid-{} childnode and start SUCCESS", ip, port, workerID);
} else {
//表示新启动的节点,创建持久节点 ,不用check时间
String newNode = createNode(curator);
zk_AddressNode = newNode;
String[] nodeKey = newNode.split("-");
workerID = Integer.parseInt(nodeKey[1]);
doService(curator);
updateLocalWorkerID(workerID);
LOGGER.info("[New NODE]can not find node on forever node that endpoint ip-{} port-{} workid-{},create own node on forever node and start SUCCESS ", ip, port, workerID);
}
}
} catch (Exception e) {
LOGGER.error("Start node ERROR {}", e);
try {
Properties properties = new Properties();
properties.load(new FileInputStream(new File(PROP_PATH.replace("{port}", port + ""))));
workerID = Integer.valueOf(properties.getProperty("workerID"));
LOGGER.warn("START FAILED ,use local node file properties workerID-{}", workerID);
} catch (Exception e1) {
LOGGER.error("Read file error ", e1);
return false;
}
}
return true;
}
private void doService(CuratorFramework curator) {
ScheduledUploadData(curator, zk_AddressNode);// /snowflake_forever/ip:port-000000001
}
private void ScheduledUploadData(final CuratorFramework curator, final String zk_AddressNode) {
Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "schedule-upload-time");
thread.setDaemon(true);
return thread;
}
}).scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
updateNewData(curator, zk_AddressNode);
}
}, 1L, 3L, TimeUnit.SECONDS);//每3s上报数据
}
private boolean checkInitTimeStamp(CuratorFramework curator, String zk_AddressNode) throws Exception {
byte[] bytes = curator.getData().forPath(zk_AddressNode);
Endpoint endPoint = deBuildData(new String(bytes));
//该节点的时间不能小于最后一次上报的时间
return !(endPoint.getTimestamp() > System.currentTimeMillis());
}
/**
* 创建持久顺序节点 ,并把节点数据放入 value
*
* @param curator
* @return
* @throws Exception
*/
private String createNode(CuratorFramework curator) throws Exception {
try {
return curator.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath(PATH_FOREVER + "/" + listenAddress + "-", buildData().getBytes());
} catch (Exception e) {
LOGGER.error("create node error msg {} ", e.getMessage());
throw e;
}
}
private void updateNewData(CuratorFramework curator, String path) {
try {
if (System.currentTimeMillis() < lastUpdateTime) {
return;
}
curator.setData().forPath(path, buildData().getBytes());
lastUpdateTime = System.currentTimeMillis();
} catch (Exception e) {
LOGGER.info("update init data error path is {} error is {}", path, e);
}
}
/**
* 构建需要上传的数据
*
* @return
*/
private String buildData() throws JsonProcessingException {
Endpoint endpoint = new Endpoint(ip, port, System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(endpoint);
return json;
}
private Endpoint deBuildData(String json) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Endpoint endpoint = mapper.readValue(json, Endpoint.class);
return endpoint;
}
/**
* 在节点文件系统上缓存一个workid值,zk失效,机器重启时保证能够正常启动
*
* @param workerID
*/
private void updateLocalWorkerID(int workerID) {
File leafConfFile = new File(PROP_PATH.replace("{port}", port));
boolean exists = leafConfFile.exists();
LOGGER.info("file exists status is {}", exists);
if (exists) {
try {
FileUtils.writeStringToFile(leafConfFile, "workerID=" + workerID, false);
LOGGER.info("update file cache workerID is {}", workerID);
} catch (IOException e) {
LOGGER.error("update file cache error ", e);
}
} else {
//不存在文件,父目录页肯定不存在
try {
boolean mkdirs = leafConfFile.getParentFile().mkdirs();
LOGGER.info("init local file cache create parent dis status is {}, worker id is {}", mkdirs, workerID);
if (mkdirs) {
if (leafConfFile.createNewFile()) {
FileUtils.writeStringToFile(leafConfFile, "workerID=" + workerID, false);
LOGGER.info("local file cache workerID is {}", workerID);
}
} else {
LOGGER.warn("create parent dir error===");
}
} catch (IOException e) {
LOGGER.warn("craete workerID conf file error", e);
}
}
}
private CuratorFramework createWithOptions(String connectionString, RetryPolicy retryPolicy, int connectionTimeoutMs, int sessionTimeoutMs) {
return CuratorFrameworkFactory.builder().connectString(connectionString)
.retryPolicy(retryPolicy)
.connectionTimeoutMs(connectionTimeoutMs)
.sessionTimeoutMs(sessionTimeoutMs)
.build();
}
/**
* 上报数据结构
*/
static class Endpoint {
private String ip;
private String port;
private long timestamp;
public Endpoint() {
}
public Endpoint(String ip, String port, long timestamp) {
this.ip = ip;
this.port = port;
this.timestamp = timestamp;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
public String getZk_AddressNode() {
return zk_AddressNode;
}
public void setZk_AddressNode(String zk_AddressNode) {
this.zk_AddressNode = zk_AddressNode;
}
public String getListenAddress() {
return listenAddress;
}
public void setListenAddress(String listenAddress) {
this.listenAddress = listenAddress;
}
public int getWorkerID() {
return workerID;
}
public void setWorkerID(int workerID) {
this.workerID = workerID;
}
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake.exception;
public class CheckLastTimeException extends RuntimeException {
public CheckLastTimeException(String msg){
super(msg);
}
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake.exception;
public class CheckOtherNodeException extends RuntimeException {
public CheckOtherNodeException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake.exception;
public class ClockGoBackException extends RuntimeException {
public ClockGoBackException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.hannote.distributed.id.generator.biz.exception;
public class InitException extends Exception{
public InitException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.distributed.id.generator.biz.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR)
public class LeafServerException extends RuntimeException {
public LeafServerException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.distributed.id.generator.biz.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR,reason="Key is none")
public class NoKeyException extends RuntimeException {
}

View File

@@ -0,0 +1,95 @@
package com.hanserwei.hannote.distributed.id.generator.biz.model;
public class SegmentBufferView {
private String key;
private long value0;
private int step0;
private long max0;
private long value1;
private int step1;
private long max1;
private int pos;
private boolean nextReady;
private boolean initOk;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getValue1() {
return value1;
}
public void setValue1(long value1) {
this.value1 = value1;
}
public int getStep1() {
return step1;
}
public void setStep1(int step1) {
this.step1 = step1;
}
public long getMax1() {
return max1;
}
public void setMax1(long max1) {
this.max1 = max1;
}
public long getValue0() {
return value0;
}
public void setValue0(long value0) {
this.value0 = value0;
}
public int getStep0() {
return step0;
}
public void setStep0(int step0) {
this.step0 = step0;
}
public long getMax0() {
return max0;
}
public void setMax0(long max0) {
this.max0 = max0;
}
public int getPos() {
return pos;
}
public void setPos(int pos) {
this.pos = pos;
}
public boolean isNextReady() {
return nextReady;
}
public void setNextReady(boolean nextReady) {
this.nextReady = nextReady;
}
public boolean isInitOk() {
return initOk;
}
public void setInitOk(boolean initOk) {
this.initOk = initOk;
}
}

View File

@@ -0,0 +1,67 @@
package com.hanserwei.hannote.distributed.id.generator.biz.service;
import com.alibaba.druid.pool.DruidDataSource;
import com.hanserwei.hannote.distributed.id.generator.biz.constant.Constants;
import com.hanserwei.hannote.distributed.id.generator.biz.core.IDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.PropertyFactory;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.ZeroIDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.SegmentIDGenImpl;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.IDAllocDao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao.impl.IDAllocDaoImpl;
import com.hanserwei.hannote.distributed.id.generator.biz.exception.InitException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.sql.SQLException;
import java.util.Properties;
@Service("SegmentService")
public class SegmentService {
private Logger logger = LoggerFactory.getLogger(SegmentService.class);
private IDGen idGen;
private DruidDataSource dataSource;
public SegmentService() throws SQLException, InitException {
Properties properties = PropertyFactory.getProperties();
boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SEGMENT_ENABLE, "true"));
if (flag) {
// Config dataSource
dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(properties.getProperty(Constants.LEAF_JDBC_URL));
dataSource.setUsername(properties.getProperty(Constants.LEAF_JDBC_USERNAME));
dataSource.setPassword(properties.getProperty(Constants.LEAF_JDBC_PASSWORD));
dataSource.setValidationQuery("select 1");
dataSource.init();
// Config Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);
// Config ID Gen
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
if (idGen.init()) {
logger.info("Segment Service Init Successfully");
} else {
throw new InitException("Segment Service Init Fail");
}
} else {
idGen = new ZeroIDGen();
logger.info("Zero ID Gen Service Init Successfully");
}
}
public Result getId(String key) {
return idGen.get(key);
}
public SegmentIDGenImpl getIdGen() {
if (idGen instanceof SegmentIDGenImpl) {
return (SegmentIDGenImpl) idGen;
}
return null;
}
}

View File

@@ -0,0 +1,44 @@
package com.hanserwei.hannote.distributed.id.generator.biz.service;
import com.hanserwei.hannote.distributed.id.generator.biz.constant.Constants;
import com.hanserwei.hannote.distributed.id.generator.biz.core.IDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.PropertyFactory;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.Result;
import com.hanserwei.hannote.distributed.id.generator.biz.core.common.ZeroIDGen;
import com.hanserwei.hannote.distributed.id.generator.biz.core.snowflake.SnowflakeIDGenImpl;
import com.hanserwei.hannote.distributed.id.generator.biz.exception.InitException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Properties;
@Service("SnowflakeService")
public class SnowflakeService {
private Logger logger = LoggerFactory.getLogger(SnowflakeService.class);
private IDGen idGen;
public SnowflakeService() throws InitException {
Properties properties = PropertyFactory.getProperties();
boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SNOWFLAKE_ENABLE, "true"));
if (flag) {
String zkAddress = properties.getProperty(Constants.LEAF_SNOWFLAKE_ZK_ADDRESS);
int port = Integer.parseInt(properties.getProperty(Constants.LEAF_SNOWFLAKE_PORT));
idGen = new SnowflakeIDGenImpl(zkAddress, port);
if(idGen.init()) {
logger.info("Snowflake Service Init Successfully");
} else {
throw new InitException("Snowflake Service Init Fail");
}
} else {
idGen = new ZeroIDGen();
logger.info("Zero ID Gen Service Init Successfully");
}
}
public Result getId(String key) {
return idGen.get(key);
}
}

View File

@@ -0,0 +1,5 @@
spring:
cassandra:
keyspace-name: hannote
contact-points: 127.0.0.1
port: 9042

View File

@@ -0,0 +1,6 @@
server:
port: 8085 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境

View File

@@ -0,0 +1,12 @@
spring:
application:
name: han-note-distributed-id-generator # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址

View File

@@ -0,0 +1,12 @@
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/leaf?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
leaf.jdbc.username=root
leaf.jdbc.password=mysql
# ???? snowflake ??
leaf.snowflake.enable=true
# snowflake ???? zk ??
leaf.snowflake.zk.address=127.0.0.1:2181
# snowflake ??????????
leaf.snowflake.port=2222

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="distributed-id-generator"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,27 @@
<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.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>${revision}</version>
</parent>
<!-- 多模块项目需要配置打包方式为 pom -->
<packaging>pom</packaging>
<!-- 子模块管理 -->
<modules>
<module>han-note-distributed-id-generator-api</module>
<module>hannote-distributed-id-generator-biz</module>
</modules>
<artifactId>han-note-distributed-id-generator</artifactId>
<!-- 项目名称 -->
<name>${project.artifactId}</name>
<!-- 项目描述 -->
<description>分布式 ID 生成服务</description>
</project>

View File

@@ -0,0 +1,82 @@
<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.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>han-note-gateway</artifactId>
<name>${project.artifactId}</name>
<description>网关服务(负责路由转发、接口鉴权等功能)</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson 组件 -->
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -64,6 +64,10 @@
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
</dependencies>

View File

@@ -22,13 +22,13 @@ public class SaTokenConfigure {
.setAuth(obj -> {
// 登录校验
SaRouter.match("/**") // 拦截所有路由
.notMatch("/auth/user/login") // 排除登录接口
.notMatch("/auth/login") // 排除登录接口
.notMatch("/auth/verification/code/send") // 排除验证码发送接口
.check(r -> StpUtil.checkLogin()) // 校验是否登录
;
// 权限认证 -- 不同模块, 校验不同权限
SaRouter.match("/auth/user/logout", r -> StpUtil.checkPermission("app:note:publish"));
SaRouter.match("/auth/logout", r -> StpUtil.checkPermission("app:note:publish"));
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));

View File

@@ -1,5 +1,7 @@
package com.hanserwei.hannote.gateway.filter;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
@@ -19,20 +21,25 @@ public class AddUserId2HeaderFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("==================> TokenConvertFilter");
// 用户 ID
Long userId = null;
Long userId;
SaReactorSyncHolder.setContext(exchange);
try {
// 获取当前登录用户的 ID
userId = StpUtil.getLoginIdAsLong();
} catch (Exception e) {
log.error("==> 用户未登录, 获取用户 ID 失败: ", e);
// 若没有登录,则直接放行
} catch (NotLoginException e) {
log.debug("==> 用户未登录, 不追加 userId 请求头");
return chain.filter(exchange);
} catch (Exception e) {
log.error("==> 获取用户 ID 失败", e);
return chain.filter(exchange);
} finally {
SaReactorSyncHolder.clearContext();
}
log.info("## 当前登录的用户 ID: {}", userId);
Long finalUserId = userId;
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.header(HEADER_USER_ID, String.valueOf(finalUserId))) // 将用户 ID 设置到请求头中
.request(builder -> builder.headers(headers -> headers.set(HEADER_USER_ID, String.valueOf(userId)))) // 将用户 ID 设置到请求头中
.build();
return chain.filter(newExchange);
}

View File

@@ -10,6 +10,12 @@ spring:
- Path=/auth/**
filters:
- StripPrefix=1
- id: user
uri: lb://han-note-user
predicates:
- Path=/user/**
filters:
- StripPrefix=1
data:
redis:
database: 5 # Redis 数据库索引(默认为 0

View File

@@ -0,0 +1,36 @@
<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.hanserwei</groupId>
<artifactId>han-note-kv</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-kv-api</artifactId>
<name>${project.artifactId}</name>
<description>RPC层, 供其他服务调用</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- OpenFeign -->
<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>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
package com.hanserwei.hannote.kv.api;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.constant.ApiConstants;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = ApiConstants.SERVICE_NAME)
public interface KeyValueFeignApi {
String PREFIX = "/kv";
@PostMapping(value = PREFIX + "/note/content/add")
Response<?> addNoteContent(@RequestBody AddNoteContentReqDTO addNoteContentReqDTO);
@PostMapping(value = PREFIX + "/note/content/find")
Response<?> findNoteContent(@RequestBody AddNoteContentReqDTO addNoteContentReqDTO);
@PostMapping(value = PREFIX + "/note/content/delete")
Response<?> deleteNoteContent(@RequestBody AddNoteContentReqDTO addNoteContentReqDTO);
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.kv.constant;
public interface ApiConstants {
/**
* 服务名称
*/
String SERVICE_NAME = "han-note-kv";
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.kv.dto.req;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddNoteContentReqDTO {
@NotNull(message = "笔记 ID 不能为空")
private Long noteId;
@NotBlank(message = "笔记内容不能为空")
private String content;
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.hannote.kv.dto.req;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeleteNoteContentReqDTO {
@NotBlank(message = "笔记 ID 不能为空")
private String noteId;
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.hannote.kv.dto.req;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindNoteContentReqDTO {
@NotBlank(message = "笔记 ID 不能为空")
private String noteId;
}

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.kv.dto.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindNoteContentRspDTO {
/**
* 笔记 ID
*/
private UUID noteId;
/**
* 笔记内容
*/
private String content;
}

View File

@@ -0,0 +1,57 @@
<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.hanserwei</groupId>
<artifactId>han-note-kv</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-kv-biz</artifactId>
<name>${project.artifactId}</name>
<description>Key-Value 键值存储业务层</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Cassandra 存储 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-kv-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.kv.biz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HannoteKVBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteKVBizApplication.class, args);
}
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.hannote.kv.biz.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
@Configuration
public class CassandraConfig extends AbstractCassandraConfiguration {
@Value("${spring.cassandra.keyspace-name}")
private String keySpace;
@Value("${spring.cassandra.contact-points}")
private String contactPoints;
@Value("${spring.cassandra.port}")
private int port;
@Override
protected String getKeyspaceName() {
return keySpace;
}
@Override
public String getContactPoints() {
return contactPoints;
}
@Override
public int getPort() {
return port;
}
}

View File

@@ -0,0 +1,40 @@
package com.hanserwei.hannote.kv.biz.controller;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.service.NoteContentService;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/kv")
@Slf4j
public class NoteContentController {
@Resource
private NoteContentService noteContentService;
@PostMapping(value = "/note/content/add")
public Response<?> addNoteContent(@Validated @RequestBody AddNoteContentReqDTO addNoteContentReqDTO) {
return noteContentService.addNoteContent(addNoteContentReqDTO);
}
@PostMapping(value = "/note/content/find")
public Response<FindNoteContentRspDTO> findNoteContent(@Validated @RequestBody FindNoteContentReqDTO findNoteContentReqDTO) {
return noteContentService.findNoteContent(findNoteContentReqDTO);
}
@PostMapping(value = "/note/content/delete")
public Response<?> deleteNoteContent(@Validated @RequestBody DeleteNoteContentReqDTO deleteNoteContentReqDTO) {
return noteContentService.deleteNoteContent(deleteNoteContentReqDTO);
}
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.hannote.kv.biz.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import java.util.UUID;
@Table("note_content")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NoteContentDO {
@PrimaryKey("id")
private UUID id;
private String content;
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.kv.biz.domain.repository;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import org.springframework.data.cassandra.repository.CassandraRepository;
import java.util.UUID;
public interface NoteContentRepository extends CassandraRepository<NoteContentDO, UUID> {
}

View File

@@ -0,0 +1,24 @@
package com.hanserwei.hannote.kv.biz.enums;
import com.hanserwei.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("KV-10000", "出错啦,后台小哥正在努力修复中..."),
PARAM_NOT_VALID("KV-10001", "参数错误"),
// ----------- 业务异常状态码 -----------
NOTE_CONTENT_NOT_FOUND("KV-20000", "该笔记内容不存在"),
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMsg;
}

View File

@@ -0,0 +1,103 @@
package com.hanserwei.hannote.kv.biz.exception;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Optional;
@SuppressWarnings("LoggingSimilarMessage")
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常
*
* @return Response.fail(e)
*/
@ExceptionHandler({ApiException.class})
@ResponseBody
public Response<Object> handleApiException(HttpServletRequest request, ApiException e) {
log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMsg());
return Response.fail(e);
}
/**
* 捕获参数校验异常
*
* @return Response.fail(errorCode, errorMessage)
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 获取 BindingResult
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder();
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
errors.forEach(error ->
sb.append(error.getField())
.append(" ")
.append(error.getDefaultMessage())
.append(", 当前值: '")
.append(error.getRejectedValue())
.append("'; ")
);
});
// 错误信息
String errorMessage = sb.toString();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 捕获 guava 参数校验异常
*
* @return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID)
*/
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 错误信息
String errorMessage = e.getMessage();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 其他类型异常
*
* @param request 请求
* @param e 异常
* @return Response.fail(ResponseCodeEnum.SYSTEM_ERROR)
*/
@ExceptionHandler({Exception.class})
@ResponseBody
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
}
}

View File

@@ -0,0 +1,34 @@
package com.hanserwei.hannote.kv.biz.service;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
public interface NoteContentService {
/**
* 添加笔记内容
*
* @param addNoteContentReqDTO 添加笔记内容请求参数
* @return 响应
*/
Response<?> addNoteContent(AddNoteContentReqDTO addNoteContentReqDTO);
/**
* 查询笔记内容
*
* @param findNoteContentReqDTO 查询笔记内容请求参数
* @return 响应
*/
Response<FindNoteContentRspDTO> findNoteContent(FindNoteContentReqDTO findNoteContentReqDTO);
/**
* 删除笔记内容
*
* @param deleteNoteContentReqDTO 删除笔记内容请求参数
* @return 响应
*/
Response<?> deleteNoteContent(DeleteNoteContentReqDTO deleteNoteContentReqDTO);
}

View File

@@ -0,0 +1,67 @@
package com.hanserwei.hannote.kv.biz.service.impl;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import com.hanserwei.hannote.kv.biz.domain.repository.NoteContentRepository;
import com.hanserwei.hannote.kv.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.kv.biz.service.NoteContentService;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
@Service
@Slf4j
public class NoteContentServiceImpl implements NoteContentService {
@Resource
private NoteContentRepository noteContentRepository;
@Override
public Response<?> addNoteContent(AddNoteContentReqDTO addNoteContentReqDTO) {
// 笔记ID
Long noteId = addNoteContentReqDTO.getNoteId();
// 笔记内容
String content = addNoteContentReqDTO.getContent();
NoteContentDO noteContent = NoteContentDO.builder()
.id(UUID.randomUUID())
.content(content)
.build();
// 插入数据
noteContentRepository.save(noteContent);
return Response.success();
}
@Override
public Response<FindNoteContentRspDTO> findNoteContent(FindNoteContentReqDTO findNoteContentReqDTO) {
// 笔记ID
String noteId = findNoteContentReqDTO.getNoteId();
Optional<NoteContentDO> optional = noteContentRepository.findById(UUID.fromString(noteId));
if (optional.isEmpty()){
throw new ApiException(ResponseCodeEnum.NOTE_CONTENT_NOT_FOUND);
}
NoteContentDO noteContentDO = optional.get();
// 构建回参
FindNoteContentRspDTO findNoteContentRspDTO = FindNoteContentRspDTO.builder()
.noteId(noteContentDO.getId())
.content(noteContentDO.getContent())
.build();
return Response.success(findNoteContentRspDTO);
}
@Override
public Response<?> deleteNoteContent(DeleteNoteContentReqDTO deleteNoteContentReqDTO) {
String noteId = deleteNoteContentReqDTO.getNoteId();
noteContentRepository.deleteById(UUID.fromString(noteId));
return Response.success();
}
}

View File

@@ -0,0 +1,6 @@
server:
port: 8084 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境

View File

@@ -0,0 +1,12 @@
spring:
application:
name: han-note-kv # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 NaCos 配置中心的服务器地址

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="kv"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,65 @@
package com.hanserwei.hannote.kv.biz;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import com.hanserwei.hannote.kv.biz.domain.repository.NoteContentRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
import java.util.UUID;
@SpringBootTest
@Slf4j
class CassandraTests {
@Resource
private NoteContentRepository noteContentRepository;
/**
* 测试插入数据
*/
@Test
void testInsert() {
NoteContentDO nodeContent = NoteContentDO.builder()
.id(UUID.randomUUID())
.content("代码测试笔记内容插入")
.build();
noteContentRepository.save(nodeContent);
}
/**
* 测试修改数据
*/
@Test
void testUpdate() {
NoteContentDO nodeContent = NoteContentDO.builder()
.id(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"))
.content("代码测试笔记内容更新")
.build();
noteContentRepository.save(nodeContent);
}
/**
* 测试查询数据
*/
@Test
void testSelect() {
Optional<NoteContentDO> optional = noteContentRepository.findById(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"));
optional.ifPresent(noteContentDO -> log.info("查询结果:{}", JsonUtils.toJsonString(noteContentDO)));
}
/**
* 测试删除数据
*/
@Test
void testDelete() {
noteContentRepository.deleteById(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"));
}
}

26
han-note-kv/pom.xml Normal file
View File

@@ -0,0 +1,26 @@
<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.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>${revision}</version>
</parent>
<!-- 多模块项目需要配置打包方式为 pom -->
<packaging>pom</packaging>
<!-- 子模块管理 -->
<modules>
<module>han-note-kv-api</module>
<module>han-note-kv-biz</module>
</modules>
<artifactId>han-note-kv</artifactId>
<!-- 项目名称 -->
<name>${project.artifactId}</name>
<!-- 项目描述 -->
<description>Key-Value 键值存储服务</description>
</project>

View File

@@ -0,0 +1,25 @@
<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.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- 多模块项目需要配置打包方式为 pom -->
<packaging>pom</packaging>
<!-- 子模块管理 -->
<modules>
<module>han-note-oss-api</module>
<module>han-note-oss-biz</module>
</modules>
<artifactId>han-note-oss</artifactId>
<!-- 项目名称 -->
<name>${project.artifactId}</name>
<!-- 项目描述 -->
<description>对象存储服务</description>
</project>

View File

@@ -0,0 +1,43 @@
<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.hanserwei</groupId>
<artifactId>han-note-oss</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-oss-api</artifactId>
<name>${project.artifactId}</name>
<description>RPC层, 供其他服务调用</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- OpenFeign -->
<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>
<!-- Feign 表单提交 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,43 @@
<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.hanserwei</groupId>
<artifactId>han-note-oss</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-oss-api</artifactId>
<name>${project.artifactId}</name>
<description>RPC层, 供其他服务调用</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- OpenFeign -->
<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>
<!-- Feign 表单提交 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.oss.api;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.config.FeignFormConfig;
import com.hanserwei.hannote.oss.constant.ApiConstants;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(name = ApiConstants.SERVICE_NAME, configuration = FeignFormConfig.class)
public interface FileFeignApi {
String PREFIX = "/file";
/**
* 文件上传
*
* @param file 文件
* @return 文件上传结果
*/
@PostMapping(value = PREFIX + "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Response<?> uploadFile(@RequestPart(value = "file") MultipartFile file);
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.oss.config;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignFormConfig {
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.oss.constant;
public interface ApiConstants {
/**
* 服务名称
*/
String SERVICE_NAME = "han-note-oss";
}

View File

@@ -0,0 +1,105 @@
<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.hanserwei</groupId>
<artifactId>han-note-oss</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-oss-biz</artifactId>
<name>${project.artifactId}</name>
<description>对象存储业务层</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</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>
<!--Rustfs对象存储-->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- 腾讯云 OSS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,105 @@
<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.hanserwei</groupId>
<artifactId>han-note-oss</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-oss-biz</artifactId>
<name>${project.artifactId}</name>
<description>对象存储业务层</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</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>
<!--Rustfs对象存储-->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- 腾讯云 OSS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss-api</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.oss;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HannoteOssBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteOssBizApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.hanserwei.hannote.oss.config;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AliyunOSSConfig {
@Resource
private AliyunOSSProperties aliyunOSSProperties;
/**
* 构建 阿里云 OSS 客户端
*
* @return 阿里云 OSS 客户端
*/
@Bean
public OSS aliyunOSSClient() {
// 设置访问凭证
DefaultCredentialProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(
aliyunOSSProperties.getAccessKey(), aliyunOSSProperties.getSecretKey());
// 创建 OSSClient 实例
return new OSSClientBuilder().build(aliyunOSSProperties.getEndpoint(), credentialsProvider);
}
}

View File

@@ -0,0 +1,14 @@
package com.hanserwei.hannote.oss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "storage.aliyun-oss")
@Component
@Data
public class AliyunOSSProperties {
private String endpoint;
private String accessKey;
private String secretKey;
}

View File

@@ -0,0 +1,48 @@
package com.hanserwei.hannote.oss.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.endpoint.EndpointBuilder;
import com.qcloud.cos.region.Region;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CosConfig {
@Resource
private CosProperties cosProperties;
@Bean
public COSClient cosClient() {
// 1. 初始化用户身份信息SecretId, SecretKey
COSCredentials cred = new BasicCOSCredentials(
cosProperties.getSecretId(),
cosProperties.getSecretKey()
);
// 2. 设置 bucket 的地域
Region region = new Region(cosProperties.getRegion());
ClientConfig clientConfig = new ClientConfig(region);
if (cosProperties.getEndpoint() != null && !cosProperties.getEndpoint().isEmpty()) {
clientConfig.setEndpointBuilder(new EndpointBuilder() {
@Override
public String buildGeneralApiEndpoint(String bucketName) {
// 所有 API 请求都会使用自定义域名
return cosProperties.getEndpoint();
}
@Override
public String buildGetServiceApiEndpoint() {
return cosProperties.getEndpoint();
}
});
}
// 3. 构建 COSClient
return new COSClient(cred, clientConfig);
}
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.oss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "storage.cos")
public class CosProperties {
private String endpoint;
private String secretId;
private String secretKey;
private String appId;
private String region;
}

Some files were not shown because too many files have changed in this diff Show More