commit b377c87a1adbbcd1422188e5a1e0483ead59067e
Author: wucongxing8150 <815046773@qq.com>
Date: Mon Aug 11 10:39:53 2025 +0800
1
diff --git a/sa-admin/pom.xml b/sa-admin/pom.xml
new file mode 100644
index 0000000..c5ac192
--- /dev/null
+++ b/sa-admin/pom.xml
@@ -0,0 +1,47 @@
+
+ 4.0.0
+
+ net.lab1024
+ sa-parent
+ 3.0.0
+ ../pom.xml
+
+
+ sa-admin
+ 3.0.0
+ jar
+
+ sa-admin
+ sa-admin project
+
+
+
+
+ net.lab1024
+ sa-base
+ 3.0.0
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ net.lab1024.sa.admin.AdminApplication
+
+
+
+
+ repackage
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/AdminApplication.java b/sa-admin/src/main/java/net/lab1024/sa/admin/AdminApplication.java
new file mode 100644
index 0000000..0d4863c
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/AdminApplication.java
@@ -0,0 +1,40 @@
+package net.lab1024.sa.admin;
+
+import net.lab1024.sa.base.listener.Ip2RegionListener;
+import net.lab1024.sa.base.listener.LogVariableListener;
+import org.apache.ibatis.annotations.Mapper;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * SmartAdmin 项目启动类
+ *
+ * @Author 1024创新实验室-主任:卓大
+ * @Date 2022-08-29 21:00:58
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+@EnableCaching
+@EnableScheduling
+@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
+@ComponentScan(AdminApplication.COMPONENT_SCAN)
+@MapperScan(value = AdminApplication.COMPONENT_SCAN, annotationClass = Mapper.class)
+@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class})
+public class AdminApplication {
+
+ public static final String COMPONENT_SCAN = "net.lab1024.sa";
+
+ public static void main(String[] args) {
+ SpringApplication application = new SpringApplication(AdminApplication.class);
+ // 添加 日志监听器,使 log4j2-spring.xml 可以间接读取到配置文件的属性
+ application.addListeners(new LogVariableListener(), new Ip2RegionListener());
+ application.run(args);
+ }
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/AppConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/AppConfig.java
new file mode 100644
index 0000000..b69644f
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/AppConfig.java
@@ -0,0 +1,28 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+public class AppConfig {
+
+ @Value("${app.apiUrl}")
+ private String apiUrl;
+
+ @Value("${app.secretKey}")
+ private String secretKey;
+
+ @Value("${app.imagePrefix}")
+ private String imagePrefix;
+
+ @Value("${app.platform}")
+ private String platform;
+
+ @Value("${app.platformPointAccount}")
+ private String platformPointAccount;
+
+ @Value("${app.access-token}")
+ private String accessToken;
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/DySmsConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/DySmsConfig.java
new file mode 100644
index 0000000..adcf178
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/DySmsConfig.java
@@ -0,0 +1,16 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+public class DySmsConfig {
+
+ @Value("${aliyun.sms.access-key:}")
+ private String accessKey;
+
+ @Value("${aliyun.sms.access-secret:}")
+ private String accessSecret;
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/EnvConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/EnvConfig.java
new file mode 100644
index 0000000..cc0e9d3
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/EnvConfig.java
@@ -0,0 +1,13 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+public class EnvConfig {
+
+ @Value("${spring.profiles.active:prod}")
+ private String active;
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/MvcConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/MvcConfig.java
new file mode 100644
index 0000000..f5e88a5
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/MvcConfig.java
@@ -0,0 +1,41 @@
+package net.lab1024.sa.admin.config;
+
+import jakarta.annotation.Resource;
+import net.lab1024.sa.admin.interceptor.AdminInterceptor;
+import net.lab1024.sa.base.config.SwaggerConfig;
+import org.springframework.context.annotation.Configuration;
+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相关配置
+ *
+ * @Author 1024创新实验室-主任: 卓大
+ * @Date 2021-09-02 20:21:10
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+@Configuration
+public class MvcConfig implements WebMvcConfigurer {
+
+ @Resource
+ private AdminInterceptor adminInterceptor;
+
+
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(adminInterceptor)
+ .excludePathPatterns(SwaggerConfig.SWAGGER_WHITELIST)
+ .addPathPatterns("/**");
+ }
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
+ registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+ }
+
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/MyMetaObjectHandler.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/MyMetaObjectHandler.java
new file mode 100644
index 0000000..43da9f8
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/MyMetaObjectHandler.java
@@ -0,0 +1,25 @@
+package net.lab1024.sa.admin.config;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+ private static final ZoneId BEIJING_ZONE = ZoneId.of("Asia/Shanghai");
+
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now(BEIJING_ZONE));
+ this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now(BEIJING_ZONE));
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
+ }
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/OperateLogAspectConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/OperateLogAspectConfig.java
new file mode 100644
index 0000000..bccbfb4
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/OperateLogAspectConfig.java
@@ -0,0 +1,28 @@
+package net.lab1024.sa.admin.config;
+
+import net.lab1024.sa.base.module.support.operatelog.core.OperateLogAspect;
+import net.lab1024.sa.base.module.support.operatelog.core.OperateLogConfig;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 操作日志切面 配置
+ *
+ * @Author 1024创新实验室: 罗伊
+ * @Date 2022-05-30 21:22:12
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+@Configuration
+public class OperateLogAspectConfig extends OperateLogAspect{
+
+ /**
+ * 配置信息
+ */
+ @Override
+ public OperateLogConfig getOperateLogConfig() {
+ return OperateLogConfig.builder().corePoolSize(1).queueCapacity(10000).build();
+ }
+
+
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/OssConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/OssConfig.java
new file mode 100644
index 0000000..6464fe8
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/OssConfig.java
@@ -0,0 +1,25 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+public class OssConfig {
+
+ @Value("${file.storage.cloud.endpoint}")
+ private String endpoint;
+
+ @Value("${file.storage.cloud.access-key}")
+ private String accessKey;
+
+ @Value("${file.storage.cloud.secret-key}")
+ private String accessKeySecret;
+
+ @Value("${file.storage.cloud.bucket-name}")
+ private String bucket;
+
+ @Value("${file.storage.cloud.url-prefix}")
+ private String customDomainName;
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/RedisConfiguration.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/RedisConfiguration.java
new file mode 100644
index 0000000..f33ebed
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/RedisConfiguration.java
@@ -0,0 +1,73 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisPassword;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+
+@Configuration
+@RequiredArgsConstructor
+public class RedisConfiguration {
+ // 开发环境Redis配置
+ @Value("${spring.data.redis.database:0}")
+ private String database;
+
+ @Value("${spring.data.redis.host:localhost}")
+ private String host;
+
+ @Value("${spring.data.redis.port:6379}")
+ private Integer port;
+
+ @Value("${spring.data.redis.password:}")
+ private String password;
+
+ // 生产环境Redis配置(如果没有配置,使用开发环境配置)
+ @Value("${spring.data.prod-redis.host:${spring.data.redis.host:localhost}}")
+ private String prodHost;
+
+ @Value("${spring.data.prod-redis.port:${spring.data.redis.port:6379}}")
+ private Integer prodPort;
+
+ @Value("${spring.data.prod-redis.database:${spring.data.redis.database:0}}")
+ private String prodDatabase;
+
+ @Value("${spring.data.prod-redis.password:${spring.data.redis.password:}}")
+ private String prodPassword;
+
+ // 移除redisTemplate的定义,使用框架自带的
+
+ @Bean
+ @Qualifier("prodRedisTemplate")
+ public RedisTemplate prodRedisTemplate(RedisConnectionFactory connectionFactory) {
+ RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
+ configuration.setHostName(prodHost);
+ configuration.setPort(prodPort);
+ configuration.setDatabase(Integer.parseInt(prodDatabase));
+ if (prodPassword != null && !prodPassword.isEmpty()) {
+ configuration.setPassword(RedisPassword.of(prodPassword));
+ }
+
+ LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration);
+ factory.afterPropertiesSet();
+
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(factory);
+
+ // 使用String序列化方式
+ StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+ template.setKeySerializer(stringRedisSerializer);
+ template.setValueSerializer(stringRedisSerializer);
+ template.setHashKeySerializer(stringRedisSerializer);
+ template.setHashValueSerializer(stringRedisSerializer);
+
+ return template;
+ }
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/config/WxMaConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/config/WxMaConfig.java
new file mode 100644
index 0000000..fda7fe5
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/config/WxMaConfig.java
@@ -0,0 +1,16 @@
+package net.lab1024.sa.admin.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+public class WxMaConfig {
+
+ @Value("${wechat.miniapp.appid}")
+ private String appid;
+
+ @Value("${wechat.miniapp.secret}")
+ private String secret;
+}
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminCacheConst.java b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminCacheConst.java
new file mode 100644
index 0000000..de077e9
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminCacheConst.java
@@ -0,0 +1,68 @@
+package net.lab1024.sa.admin.constant;
+
+import net.lab1024.sa.base.constant.CacheKeyConst;
+
+/**
+ * 缓存 key
+ *
+ * @Author 1024创新实验室-主任:卓大
+ * @Date 2022-01-07 18:59:22
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+public class AdminCacheConst extends CacheKeyConst {
+
+ public static class Department {
+
+ /**
+ * 部门列表
+ */
+ public static final String DEPARTMENT_LIST_CACHE = "department_list_cache";
+
+ /**
+ * 部门树
+ */
+ public static final String DEPARTMENT_TREE_CACHE = "department_tree_cache";
+
+ /**
+ * 某个部门以及下级的id列表
+ */
+ public static final String DEPARTMENT_SELF_CHILDREN_CACHE = "department_self_children_cache";
+
+ /**
+ * 部门路径 缓存
+ */
+ public static final String DEPARTMENT_PATH_CACHE = "department_path_cache";
+
+ }
+
+ /**
+ * 分类相关缓存
+ */
+ public static class Category {
+
+ public static final String CATEGORY_ENTITY = "category_cache";
+
+ public static final String CATEGORY_SUB = "category_sub_cache";
+
+ public static final String CATEGORY_TREE = "category_tree_cache";
+ }
+
+ /**
+ * 登录相关
+ */
+ public static class Login {
+
+ /**
+ * 请求用户信息
+ */
+ public static final String REQUEST_EMPLOYEE = "login_request_employee";
+
+ /**
+ * 请求用户信息权限
+ */
+ public static final String USER_PERMISSION = "login_user_permission";
+ }
+
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminRedisKeyConst.java b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminRedisKeyConst.java
new file mode 100644
index 0000000..07cc8f6
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminRedisKeyConst.java
@@ -0,0 +1,17 @@
+package net.lab1024.sa.admin.constant;
+
+import net.lab1024.sa.base.constant.RedisKeyConst;
+
+/**
+ * redis key 常量类
+ *
+ * @Author 1024创新实验室-主任:卓大
+ * @Date 2022-01-07 18:59:22
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+public class AdminRedisKeyConst extends RedisKeyConst {
+
+
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminSwaggerTagConst.java b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminSwaggerTagConst.java
new file mode 100644
index 0000000..fb7a630
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/constant/AdminSwaggerTagConst.java
@@ -0,0 +1,59 @@
+package net.lab1024.sa.admin.constant;
+
+import net.lab1024.sa.base.constant.SwaggerTagConst;
+
+/**
+ * swagger
+ *
+ * @Author 1024创新实验室:罗伊
+ * @Date 2022-01-07 18:59:22
+ * @Wechat zhuoda1024
+ * @Email lab1024@163.com
+ * @Copyright 1024创新实验室
+ */
+public class AdminSwaggerTagConst extends SwaggerTagConst {
+
+ public static class Business {
+ public static final String MANAGER_CATEGORY = "ERP进销存-分类管理";
+
+ public static final String MANAGER_GOODS = "ERP进销存-商品管理";
+
+ public static final String OA_BANK = "OA办公-银行卡信息";
+
+ public static final String OA_ENTERPRISE = "OA办公-企业";
+
+ public static final String OA_INVOICE = "OA办公-发票信息";
+
+ public static final String OA_NOTICE = "OA办公-通知公告";
+
+ }
+
+
+ public static class System {
+
+ public static final String SYSTEM_LOGIN = "系统-员工登录";
+
+ public static final String SYSTEM_EMPLOYEE = "系统-员工管理";
+
+ public static final String SYSTEM_DEPARTMENT = "系统-部门管理";
+
+ public static final String SYSTEM_MENU = "系统-菜单";
+
+ public static final String SYSTEM_DATA_SCOPE = "系统-系统-数据范围";
+
+ public static final String SYSTEM_ROLE = "系统-角色";
+
+ public static final String SYSTEM_ROLE_DATA_SCOPE = "系统-角色-数据范围";
+
+ public static final String SYSTEM_ROLE_EMPLOYEE = "系统-角色-员工";
+
+ public static final String SYSTEM_ROLE_MENU = "系统-角色-菜单";
+
+ public static final String SYSTEM_POSITION = "系统-职务管理";
+
+ public static final String SYSTEM_MESSAGE = "系统-消息";
+
+ }
+
+
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySms.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySms.java
new file mode 100644
index 0000000..0fccddb
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySms.java
@@ -0,0 +1,77 @@
+package net.lab1024.sa.admin.extend.aliyun;
+
+import com.aliyun.dysmsapi20170525.Client;
+import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
+import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import net.lab1024.sa.admin.config.DySmsConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Slf4j
+@Component
+public class DySms {
+ @Resource
+ private DySmsConfig dySmsConfig;
+
+ private static final String endpoint = "dysmsapi.aliyuncs.com";
+ private static final String signName = "肝胆相照";
+
+ /**
+ * 创建短信客户端
+ */
+ private Client createClient() throws Exception {
+ Config config = new Config()
+ .setAccessKeyId(dySmsConfig.getAccessKey())
+ .setAccessKeySecret(dySmsConfig.getAccessSecret())
+ .setEndpoint(endpoint);
+ return new Client(config);
+ }
+
+ /**
+ * 发送短信
+ *
+ * @param phoneNumber 手机号
+ * @param templateCode 模板CODE
+ * @param sceneDesc 场景说明,用于日志或调试
+ * @param templateParam 模板参数
+ */
+ public void sendSms(String phoneNumber, String templateCode, String sceneDesc, Map templateParam) {
+ try {
+ Client client = createClient();
+ ObjectMapper objectMapper = new ObjectMapper();
+ String paramJson = objectMapper.writeValueAsString(templateParam);
+
+ SendSmsRequest request = new SendSmsRequest()
+ .setPhoneNumbers(phoneNumber)
+ .setSignName(signName)
+ .setTemplateCode(templateCode)
+ .setTemplateParam(paramJson);
+
+ // 只用标准的RuntimeOptions,不加任何自定义字段
+ RuntimeOptions runtime = new RuntimeOptions();
+
+ SendSmsResponse response = client.sendSmsWithOptions(request, runtime);
+
+ if (response.getBody() == null || !"OK".equals(response.getBody().getCode())) {
+ log.error("短信发送失败,手机号:{},场景:{},返回信息:{}", phoneNumber, sceneDesc, response.getBody() != null ? response.getBody().getMessage() : "无返回体");
+ throw new RuntimeException("短信发送失败: " + (response.getBody() != null ? response.getBody().getMessage() : "无返回体"));
+ } else {
+ log.info("短信发送成功,手机号:{},场景:{},返回信息:{}", phoneNumber, sceneDesc, response.getBody().getMessage());
+ }
+
+ } catch (TeaException e) {
+ log.error("阿里云短信发送异常,手机号:{},场景:{},错误信息:{}", phoneNumber, sceneDesc, e.getMessage(), e);
+ throw new RuntimeException("阿里云短信发送异常", e);
+ } catch (Exception e) {
+ log.error("短信发送异常,手机号:{},场景:{},错误信息:{}", phoneNumber, sceneDesc, e.getMessage(), e);
+ throw new RuntimeException("短信发送异常", e);
+ }
+ }
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySmsConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySmsConfig.java
new file mode 100644
index 0000000..844cfbf
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/DySmsConfig.java
@@ -0,0 +1 @@
+// 此文件已移动到 config 目录,请使用 net.lab1024.sa.admin.config.DySmsConfig
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/Oss.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/Oss.java
new file mode 100644
index 0000000..33f87ec
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/Oss.java
@@ -0,0 +1,116 @@
+package net.lab1024.sa.admin.extend.aliyun;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.OSSObject;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.aliyun.oss.model.PutObjectRequest;
+import lombok.extern.slf4j.Slf4j;
+import net.lab1024.sa.admin.config.OssConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+@Slf4j
+@Component
+public class Oss {
+ private static OssConfig ossConfig;
+
+ private static final Logger logger = LoggerFactory.getLogger(Oss.class);
+
+ public Oss(OssConfig ossConfig) {
+ Oss.ossConfig = ossConfig;
+ }
+
+ public static OSS createClient() {
+ return new OSSClientBuilder().build(ossConfig.getEndpoint(), ossConfig.getAccessKey(), ossConfig.getAccessKeySecret());
+ }
+
+ public static boolean putObject(String fileName, byte[] content) {
+ if (content == null || content.length == 0) {
+ logger.warn("Failed to upload object to OSS: content is null or empty, fileName={}", fileName);
+ return false;
+ }
+
+ if (fileName == null || fileName.trim().isEmpty()) {
+ logger.warn("Failed to upload object to OSS: fileName is null or empty");
+ return false;
+ }
+
+ OSS client = createClient();
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
+
+ try {
+ // 推断 Content-Type
+ String contentType = determineContentType(fileName);
+
+ // 创建对象元数据
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(content.length);
+ metadata.setContentType(contentType);
+
+ // 执行上传
+ PutObjectRequest request = new PutObjectRequest(ossConfig.getBucket(), fileName, inputStream, metadata);
+ client.putObject(request);
+
+ logger.info("Successfully uploaded object to OSS: bucket={}, fileName={}, size={} bytes",
+ ossConfig.getBucket(), fileName, content.length);
+ return true;
+
+ } catch (Exception e) {
+ logger.error("Failed to upload object to OSS. fileName={}, error={}", fileName, e.getMessage(), e);
+ return false;
+ } finally {
+ try {
+ inputStream.close(); // 可以不关(ByteArrayInputStream 空操作),但建议写上
+ } catch (IOException ignored) { }
+ client.shutdown();
+ }
+ }
+
+ private static String determineContentType(String fileName) {
+ if (fileName == null) return "application/octet-stream";
+
+ int lastDotIndex = fileName.lastIndexOf('.');
+ if (lastDotIndex < 0) {
+ return "application/octet-stream";
+ }
+
+ String ext = fileName.substring(lastDotIndex + 1).toLowerCase();
+
+ return switch (ext) {
+ case "jpg", "jpeg" -> "image/jpeg";
+ case "png" -> "image/png";
+ case "gif" -> "image/gif";
+ case "bmp" -> "image/bmp";
+ case "webp" -> "image/webp";
+ case "pdf" -> "application/pdf";
+ case "txt" -> "text/plain";
+ case "html", "htm" -> "text/html";
+ case "xml" -> "application/xml";
+ case "json" -> "application/json";
+ case "mp4" -> "video/mp4";
+ case "avi" -> "video/x-msvideo";
+ case "mp3" -> "audio/mpeg";
+ default -> "application/octet-stream";
+ };
+ }
+
+ // 下载文件为 byte[]
+ public static byte[] getObjectToByte(String fileName) {
+ OSS client = createClient();
+ try (OSSObject ossObject = client.getObject(ossConfig.getBucket(), fileName);
+ InputStream content = ossObject.getObjectContent()) {
+ return content.readAllBytes();
+ } catch (Exception e) {
+ log.error("OSS 下载失败: fileName={}, error={}", fileName, e.getMessage(), e);
+ return null;
+ } finally {
+ client.shutdown();
+ }
+ }
+}
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/OssConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/OssConfig.java
new file mode 100644
index 0000000..43946d8
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/aliyun/OssConfig.java
@@ -0,0 +1 @@
+// 此文件已移动到 config 目录,请使用 net.lab1024.sa.admin.config.OssConfig
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/AppConfig.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/AppConfig.java
new file mode 100644
index 0000000..8c30def
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/AppConfig.java
@@ -0,0 +1 @@
+// 此文件已移动到 config 目录,请使用 net.lab1024.sa.admin.config.AppConfig
\ No newline at end of file
diff --git a/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/Base.java b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/Base.java
new file mode 100644
index 0000000..d876794
--- /dev/null
+++ b/sa-admin/src/main/java/net/lab1024/sa/admin/extend/app/Base.java
@@ -0,0 +1,164 @@
+package net.lab1024.sa.admin.extend.app;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import net.lab1024.sa.base.common.exception.BusinessException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+@Slf4j
+public class Base {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 生成签名
+ * @param params 需要签名的参数(支持嵌套 Map)
+ * @param secretKey 密钥
+ * @return 签名字符串
+ */
+ public static String genSignature(Map params, String secretKey) {
+ try {
+ // Step 1: 对 Map 进行递归排序
+ Map sortedMap = sortMapRecursively(params);
+
+ // Step 2: 转为 JSON 字符串
+ String json = objectMapper.writeValueAsString(sortedMap);
+
+ // Step 3: 使用 HMAC-SHA256 签名
+ return hmacSha256(json, secretKey);
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 对 Map 进行递归排序(key 排序),支持嵌套 Map 和 List
+ */
+ public static Map sortMapRecursively(Map data) {
+ Map sortedMap = new TreeMap<>(); // TreeMap 会自动按 key 排序
+
+ for (Map.Entry entry : data.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+
+ if (value instanceof Map) {
+ // 如果是嵌套 map,递归排序
+ @SuppressWarnings("unchecked")
+ Map nestedMap = (Map) value;
+ sortedMap.put(key, sortMapRecursively(nestedMap));
+ } else if (value instanceof List) {
+ // 如果是列表,检查每一项是否是 map,如果是则排序
+ @SuppressWarnings("unchecked")
+ List