新增ebkie-user用户模块

This commit is contained in:
yanglei 2025-10-14 13:59:25 +08:00
parent fed8f1ed54
commit 68cb640417
20 changed files with 681 additions and 2 deletions

View File

@ -40,6 +40,12 @@ spring:
- Path=/operations/**
filters:
- StripPrefix=1
- id: ebike-user
uri: lb://ebike-user
predicates:
- Path=/user/**
filters:
- StripPrefix=1
data:
# redis配置

View File

@ -11,11 +11,11 @@ ADD ebike-operations-0.0.1-SNAPSHOT.jar /ebike-operations.jar
ENV JAVA_OPTS="-Xms512m -Xmx1024m"
# 暴露应用端口与application.yml配置一致
EXPOSE 10012
EXPOSE 10013
# 健康检查配置
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:10012/actuator/health || exit 1
CMD curl -f http://localhost:10013/actuator/health || exit 1
# 容器启动命令
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar ebike-operations.jar"]

192
ebike-user/pom.xml Normal file
View File

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cdzy</groupId>
<artifactId>ebike-plus</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>ebike-user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ebike-user</name>
<description>ebike-user</description>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<docker.image.prefix>ebike_plus</docker.image.prefix>
</properties>
<dependencies>
<dependency>
<groupId>com.cdzy</groupId>
<artifactId>ebike-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.cdzy</groupId>
<artifactId>ebike-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- mybatis-flex -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-processor</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- PostgreSQL JDBC 驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.7</version>
</dependency>
<!-- PostGIS 支持 -->
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2025.1.0</version>
</dependency>
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${nacos.version}</version>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${HikariCP.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<profiles>
<!--开发环境-->
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
<!--谁设置为true 就激活那个环境-->
<activeByDefault>true</activeByDefault>
</properties>
</profile>
<!--生产环境-->
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profile.active>test</profile.active>
</properties>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${boot.version}</version> <!-- 与项目版本一致 -->
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 必须包含此目标 -->
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
<!--远程Docker的地址-->
<dockerHost>http://192.168.2.226:2375</dockerHost><!--镜像名称,前缀/项目名-->
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,21 @@
# 使用官方Eclipse Temurin镜像作为基础镜像JDK17兼容Spring Boot 3.x
FROM eclipse-temurin:17-jdk-jammy
VOLUME /opt/docker-images
ADD ebike-user-0.0.1-SNAPSHOT.jar /ebike-user.jar
# 设置JVM参数根据实际需求调整
ENV JAVA_OPTS="-Xms512m -Xmx1024m"
# 暴露应用端口与application.yml配置一致
EXPOSE 10014
# 健康检查配置
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:10014/actuator/health || exit 1
# 容器启动命令
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar ebike-user.jar"]

View File

@ -0,0 +1,13 @@
package com.cdzy.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EbikeUserApplication {
public static void main(String[] args) {
SpringApplication.run(EbikeUserApplication.class, args);
}
}

View File

@ -0,0 +1,25 @@
package com.cdzy.user.component;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// 内存使用情况检查逻辑
Runtime runtime = Runtime.getRuntime();
double usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024.0 / 1024 / 1024;
double maxMem = runtime.maxMemory() / 1024.0 / 1024 / 1024;
double ratio = usedMem / maxMem * 100;
return Health.up()
.withDetail("memoryUsed(GB)", String.format("%.2f", usedMem))
.withDetail("memoryMax(GB)", String.format("%.2f", maxMem))
.withDetail("utilization", String.format("%.2f%%", ratio))
.build();
// return Health.up().withDetail("service", "available").build();
}
}

View File

@ -0,0 +1,21 @@
package com.cdzy.user.component;
import cn.dev33.satoken.stp.StpUtil;
import com.mybatisflex.core.tenant.TenantFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
public class MyTenantFactory implements TenantFactory {
public Object[] getTenantIds() {
if (StpUtil.isLogin()) {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
Object attribute = attributes.getAttribute("tenantId", RequestAttributes.SCOPE_REQUEST);
if (attribute != null) {
Long tenantId = Long.valueOf(attribute.toString());
return new Object[]{tenantId};
}
}
return null;
}
}

View File

@ -0,0 +1,43 @@
package com.cdzy.user.component;
import cn.dev33.satoken.stp.StpUtil;
import com.cdzy.common.model.response.CommonStaffInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.TimeZone;
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(@NotNull HttpServletRequest request
, @NotNull HttpServletResponse response, @NotNull Object handler) {
boolean login = StpUtil.isLogin();
if (login) {
Long tenantId = getTenantIdByReuqest(request);
request.setAttribute("tenantId", tenantId);
return true;
}
return true;
}
Long getTenantIdByReuqest(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String id = (String) StpUtil.getLoginIdByToken(token);
Object object = StpUtil.getSessionByLoginId(id).get(id);
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
CommonStaffInfo staffInfo = objectMapper.convertValue(object, CommonStaffInfo.class);
return staffInfo.getOperatorId();
}
}

View File

@ -0,0 +1,121 @@
package com.cdzy.user.config;
import cn.dev33.satoken.exception.NotLoginException;
import com.alibaba.nacos.shaded.com.google.gson.JsonSyntaxException;
import com.cdzy.common.model.response.JsonResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.UnexpectedTypeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLSyntaxErrorException;
/**
* 全局异常处理
* @author attiya
* @since 2023-6-14
*
*/
@RestControllerAdvice(annotations = RestController.class)
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(NotLoginException.class)
public JsonResult<?> bindExceptionHandler(NotLoginException e) {
log.error(e.toString());
return JsonResult.failed(e.getMessage());
}
@ExceptionHandler(BindException.class)
public JsonResult<?> bindExceptionHandler(BindException e) {
log.error(e.toString());
return JsonResult.failed(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public JsonResult<?> bindExceptionHandler(MethodArgumentNotValidException e) {
log.error(e.toString());
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return JsonResult.failed(message);
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, JsonSyntaxException e) {
log.error(request.getServletPath(), e);
return JsonResult.failed("数据格式化错误");
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, SQLSyntaxErrorException e) {
// LogFactory.getLog(super.getClass()).error(e.getMessage(), e);
log.error(request.getServletPath(), e);
return JsonResult.failed("系统错误");
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, BadSqlGrammarException e) {
// LogFactory.getLog(super.getClass()).error(e.getMessage(), e);
log.error(request.getServletPath(), e);
return JsonResult.failed("系统错误");
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, NumberFormatException e) {
// LogFactory.getLog(super.getClass()).error(e.getMessage(), e);
log.error(request.getServletPath(), e);
return JsonResult.failed(e.getMessage());
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, HttpMessageNotReadableException e) {
log.error(request.getServletPath(), e);
return JsonResult.failed("请输入指定格式的参数");
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, UnexpectedTypeException e) {
// e.printStackTrace();
log.error(request.getServletPath());
return JsonResult.failed(e.getMessage());
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseBody
public JsonResult<?> handleMethodArgumentNotValidException(HttpServletRequest request, ConstraintViolationException e) {
// e.printStackTrace();
log.error(request.getServletPath(), e);
String[] msgs = e.getMessage().split(":");
return JsonResult.failed(msgs[msgs.length - 1].trim());
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, HttpMessageConversionException e) {
log.error(request.getServletPath(), e);
return JsonResult.failed(e.getMessage());
}
@ResponseBody
@ExceptionHandler
public JsonResult<?> exp(HttpServletRequest request, Exception e) {
// LogFactory.getLog(super.getClass()).error(e.getMessage(), e);
log.error(request.getServletPath(), e);
return JsonResult.failed(e.getMessage());
}
}

View File

@ -0,0 +1,55 @@
package com.cdzy.user.config;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
/**
* 日期类型数据处理配置
* @author attiya
* @since 2024-11-04
**/
@Configuration
public class JacksonConfig {
public static JavaTimeModule buildJavaTimeModule() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DatePattern.NORM_TIME_FORMATTER));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DatePattern.NORM_DATE_FORMATTER));
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DatePattern.NORM_TIME_FORMATTER));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DatePattern.NORM_DATE_FORMATTER));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
return javaTimeModule;
}
@Bean
@ConditionalOnMissingBean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.modules(buildJavaTimeModule());
};
}
}

View File

@ -0,0 +1,41 @@
package com.cdzy.user.config;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.audit.AuditManager;
import com.mybatisflex.core.dialect.DbType;
import com.mybatisflex.core.dialect.DialectFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
/**
* @author attiya
*/
@Configuration
public class MyBatisFlexConfiguration {
private static final Logger logger = LoggerFactory
.getLogger("mybatis-flex-sql:");
public MyBatisFlexConfiguration() {
//开启审计功能
AuditManager.setAuditEnable(true);
//设置 SQL 审计收集器
AuditManager.setMessageCollector(auditMessage ->
logger.info("{},{}ms", auditMessage.getFullSql()
, auditMessage.getElapsedTime())
);
//全局ID生成策略配置
FlexGlobalConfig.KeyConfig keyConfig = new FlexGlobalConfig.KeyConfig();
keyConfig.setKeyType(KeyType.Generator);
keyConfig.setValue("snowFlakeId");
keyConfig.setBefore(true);
FlexGlobalConfig.getDefaultConfig().setKeyConfig(keyConfig);
DialectFactory.registerDialect(DbType.MYSQL,new PermissionDialect());
}
}

View File

@ -0,0 +1,24 @@
package com.cdzy.user.config;
import com.mybatisflex.core.dialect.impl.CommonsDialectImpl;
import com.mybatisflex.core.query.CPI;
import com.mybatisflex.core.query.QueryTable;
import com.mybatisflex.core.query.QueryWrapper;
import java.util.List;
/**
* @author attiya
* @since 2025-03-14
*/
public class PermissionDialect extends CommonsDialectImpl {
@Override
public String forSelectByQuery(QueryWrapper queryWrapper) {
//用于严重table是否需要添加数据权限查询条件
List<QueryTable> tables = CPI.getQueryTables(queryWrapper);
//获取当前用户信息 queryWrapper 添加额外的条件
return super.buildSelectSql(queryWrapper);
}
}

View File

@ -0,0 +1,23 @@
package com.cdzy.user.config;
import com.cdzy.user.component.MyTenantFactory;
import com.cdzy.user.component.TenantInterceptor;
import com.mybatisflex.core.tenant.TenantFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public TenantFactory tenantFactory(){
return new MyTenantFactory();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor());
}
}

View File

@ -0,0 +1,90 @@
server:
port: 10014
spring:
application:
name: ebike-user
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos
username: nacos
password: nacos
sql:
init:
platform: postgis
mode: never
schema-locations: classpath:db/init.sql
data-locations: classpath:db/data.sql
jackson:
serialization:
write-dates-as-timestamps: false
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
url: jdbc:postgresql://47.109.71.130:5432/ebike_user?currentSchema=public&stringtype=unspecified
username: root
password: 970529
driver-class-name: org.postgresql.Driver
hikari:
## 最小空闲连接数量
minimum-idle: 5
## 空闲连接存活最大时间默认60000010分钟
idle-timeout: 180000
## 连接池最大连接数默认是10
maximum-pool-size: 10
## 数据库连接超时时间,默认30秒即30000
connection-timeout: 30000
connection-test-query: SELECT 1
##此属性控制池中连接的最长生命周期值0表示无限生命周期默认1800000即30分钟
max-lifetime: 1800000
data:
# redis配置
redis:
# Redis数据库索引默认为0
database: 1
# Redis服务器地址
host: 192.168.2.226
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码默认为空
# password:
# 连接超时时间
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
mybatis-flex:
mapper-locations: classpath:mapper/*.xml
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: true
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-32
# 是否输出操作日志
is-log: true
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always

View File

@ -0,0 +1,3 @@
spring:
profiles:
active: @profile.active@

View File

@ -18,6 +18,7 @@
<module>ebike-gather</module>
<module>ebike-staff</module>
<module>ebike-operations</module>
<module>ebike-user</module>
</modules>
<properties>