首次提交后端接口
This commit is contained in:
55
src/main/java/com/nanxiislet/admin/config/CaptchaConfig.java
Normal file
55
src/main/java/com/nanxiislet/admin/config/CaptchaConfig.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import com.google.code.kaptcha.Producer;
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
import com.google.code.kaptcha.util.Config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaConfig {
|
||||
|
||||
@Bean
|
||||
public Producer captchaProducer() {
|
||||
DefaultKaptcha kaptcha = new DefaultKaptcha();
|
||||
Properties properties = new Properties();
|
||||
// 验证码宽度
|
||||
properties.setProperty("kaptcha.image.width", "150");
|
||||
// 验证码高度
|
||||
properties.setProperty("kaptcha.image.height", "50");
|
||||
// 验证码字符长度
|
||||
properties.setProperty("kaptcha.textproducer.char.length", "4");
|
||||
// 验证码字符集
|
||||
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||
// 验证码字体大小
|
||||
properties.setProperty("kaptcha.textproducer.font.size", "38");
|
||||
// 验证码字体
|
||||
properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier");
|
||||
// 验证码字体颜色
|
||||
properties.setProperty("kaptcha.textproducer.font.color", "black");
|
||||
// 验证码背景颜色渐变开始
|
||||
properties.setProperty("kaptcha.background.clear.from", "lightGray");
|
||||
// 验证码背景颜色渐变结束
|
||||
properties.setProperty("kaptcha.background.clear.to", "white");
|
||||
// 验证码干扰
|
||||
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
|
||||
// 验证码干扰颜色
|
||||
properties.setProperty("kaptcha.noise.color", "gray");
|
||||
// 边框
|
||||
properties.setProperty("kaptcha.border", "yes");
|
||||
properties.setProperty("kaptcha.border.color", "105,179,90");
|
||||
properties.setProperty("kaptcha.border.thickness", "1");
|
||||
|
||||
Config config = new Config(properties);
|
||||
kaptcha.setConfig(config);
|
||||
return kaptcha;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 数据库初始化配置
|
||||
* 在应用启动时检查并初始化数据库
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DatabaseInitializer implements CommandLineRunner {
|
||||
|
||||
@Resource
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Value("${nanxiislet.db.init:false}")
|
||||
private boolean initEnabled;
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
if (!initEnabled) {
|
||||
log.info("数据库初始化已禁用,跳过初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查表是否存在
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'sys_user'",
|
||||
Integer.class
|
||||
);
|
||||
|
||||
if (count != null && count > 0) {
|
||||
log.info("数据库表已存在,跳过初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始初始化数据库...");
|
||||
|
||||
// 读取SQL文件
|
||||
ClassPathResource resource = new ClassPathResource("db/init.sql");
|
||||
String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
|
||||
// 分割并执行SQL语句
|
||||
String[] statements = sql.split(";");
|
||||
int executed = 0;
|
||||
for (String statement : statements) {
|
||||
String trimmed = statement.trim();
|
||||
if (!trimmed.isEmpty() && !trimmed.startsWith("--")) {
|
||||
try {
|
||||
jdbcTemplate.execute(trimmed);
|
||||
executed++;
|
||||
} catch (Exception e) {
|
||||
log.warn("执行SQL失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("数据库初始化完成,执行了 {} 条SQL语句", executed);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("数据库初始化失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
paginationInterceptor.setMaxLimit(500L);
|
||||
paginationInterceptor.setOverflow(true);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
// 防止全表更新删除插件
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充处理器
|
||||
*/
|
||||
@Component
|
||||
public static class AutoFillMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long userId = getCurrentUserId();
|
||||
if (userId != null) {
|
||||
this.strictInsertFill(metaObject, "createdBy", Long.class, userId);
|
||||
this.strictInsertFill(metaObject, "updatedBy", Long.class, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
|
||||
|
||||
Long userId = getCurrentUserId();
|
||||
if (userId != null) {
|
||||
this.strictUpdateFill(metaObject, "updatedBy", Long.class, userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
private Long getCurrentUserId() {
|
||||
try {
|
||||
Object loginId = cn.dev33.satoken.stp.StpUtil.getLoginIdDefaultNull();
|
||||
if (loginId != null) {
|
||||
return Long.parseLong(loginId.toString());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/main/java/com/nanxiislet/admin/config/RedisConfig.java
Normal file
53
src/main/java/com/nanxiislet/admin/config/RedisConfig.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// JSON序列化器
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
|
||||
ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
|
||||
new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
|
||||
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// key使用String序列化
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
|
||||
// value使用JSON序列化
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.nanxiislet.admin.entity.SysMenu;
|
||||
import com.nanxiislet.admin.entity.SysUser;
|
||||
import com.nanxiislet.admin.service.AuthService;
|
||||
import com.nanxiislet.admin.service.SysMenuService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sa-Token 权限认证接口实现
|
||||
* 用于获取当前用户的角色和权限列表
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-08
|
||||
*/
|
||||
@Component
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
|
||||
@Resource
|
||||
private SysMenuService menuService;
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
// 获取用户信息
|
||||
Long userId = Long.parseLong(loginId.toString());
|
||||
SysUser user = authService.getById(userId);
|
||||
if (user == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
String roleCode = user.getRole();
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if ("super_admin".equals(roleCode)) {
|
||||
return List.of("*");
|
||||
}
|
||||
|
||||
// 根据角色获取菜单权限
|
||||
List<SysMenu> menus = menuService.getMenusByRoleCode(roleCode);
|
||||
if (menus == null || menus.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return menus.stream()
|
||||
.map(SysMenu::getCode)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色列表
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
Long userId = Long.parseLong(loginId.toString());
|
||||
SysUser user = authService.getById(userId);
|
||||
if (user == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
String roleCode = user.getRole();
|
||||
|
||||
if (roleCode != null && !roleCode.isEmpty()) {
|
||||
roles.add(roleCode);
|
||||
|
||||
// 超级管理员同时拥有 admin 角色
|
||||
if ("super_admin".equals(roleCode)) {
|
||||
roles.add("admin");
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/nanxiislet/admin/config/SwaggerConfig.java
Normal file
46
src/main/java/com/nanxiislet/admin/config/SwaggerConfig.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import io.swagger.v3.oas.models.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI 配置
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("南溪小岛管理后台 API")
|
||||
.description("南溪小岛管理后台 RESTful API 文档")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("NanxiIslet")
|
||||
.email("admin@nanxiislet.com"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")))
|
||||
.externalDocs(new ExternalDocumentation()
|
||||
.description("项目文档")
|
||||
.url("https://doc.nanxiislet.com"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Authorization"))
|
||||
.schemaRequirement("Authorization", new SecurityScheme()
|
||||
.name("Authorization")
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER));
|
||||
}
|
||||
}
|
||||
84
src/main/java/com/nanxiislet/admin/config/WebMvcConfig.java
Normal file
84
src/main/java/com/nanxiislet/admin/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package com.nanxiislet.admin.config;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置
|
||||
*
|
||||
* @author NanxiIslet
|
||||
* @since 2024-01-06
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截器配置
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// Sa-Token 鉴权拦截器
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
// OPTIONS 预检请求不检查登录
|
||||
if ("OPTIONS".equalsIgnoreCase(SaHolder.getRequest().getMethod())) {
|
||||
return;
|
||||
}
|
||||
StpUtil.checkLogin();
|
||||
}))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
// 登录认证相关
|
||||
"/auth/login",
|
||||
"/auth/captcha",
|
||||
"/auth/logout",
|
||||
// Swagger文档
|
||||
"/doc.html",
|
||||
"/swagger-ui.html",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v3/api-docs/**",
|
||||
"/webjars/**",
|
||||
// 静态资源
|
||||
"/static/**",
|
||||
"/favicon.ico",
|
||||
// 健康检查
|
||||
"/actuator/**",
|
||||
"/health",
|
||||
"/" // 根路径
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态资源配置
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 上传文件访问
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:./uploads/");
|
||||
// Swagger UI
|
||||
registry.addResourceHandler("doc.html")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user