首次提交后端接口

This commit is contained in:
super
2026-01-27 20:48:47 +08:00
commit 40db55e85d
177 changed files with 18905 additions and 0 deletions

View 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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));
}
}

View 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/");
}
}