Compare commits

...

9 Commits

Author SHA1 Message Date
e77353b478 feat(minio): 集成 MinIO 工具类并优化对象存储客户端构建
- 新增 MinioUtil 工具类,统一封装 MinIO SDK 的常用操作
- 规范化 endpoint 处理,避免签名校验时因路径错误失败
- 统一使用 MinioUtil.buildClient 创建 MinIO 客户端,避免自动探测 region 导致错误
- 实现自动创建 bucket 及设置匿名只读策略,兼容裸桶场景
- 进程内缓存已准备好的 bucket,减少重复请求
- 修正 AliOssController 和 FileRecordServiceImpl 的 MinIO 客户端初始化逻辑
2026-08-07 09:23:18 +08:00
e487f021dc fix(storage): 规范化endpoint并指定MinIO客户端region
- 去除endpoint结尾斜杠,避免minio-java拼接双斜杠导致签名不匹配
- MinIO客户端构建时显式指定region为us-east-1
- 跳过MinIO SDK的自动getBucketLocation探测,避免反向代理环境下签名验证失败
- 修改AliOssController和FileRecordServiceImpl中相关代码逻辑
2026-08-07 04:00:39 +08:00
5ca3280ddc feat(storage): 支持 MinIO 存储并优化上传与删除逻辑
- 引入 MinIO 官方 Java SDK,替代 aliyun OSS SDK 处理与 MinIO 的交互
- 兼容 MinIO 的 AWS4-HMAC-SHA256 签名及 path-style 访问
- 区分上传和删除操作中 MinIO 与阿里云 OSS 的客户端实例创建及调用
- 统一文件路径处理,调整图片处理策略以兼容 MinIO 不支持的参数
- 增加对 MinIO 客户端异常的捕获与兼容处理
- 在 pom.xml 中添加对 okhttp 和 okio 依赖的版本锁定,解决版本冲突问题
2026-08-07 03:46:49 +08:00
b9ec1fcdb9 fix(system): 修正站点域名配置及默认应用代码
- 修改 site 应用对应的域名后缀为 sitelnk.cn
- 调整默认应用代码由 mp 改为 site
- 更新域名映射中 site 项的配置为新的域名地址
- 保障邮件通知链接生成正确的站点域名
2026-08-07 03:17:28 +08:00
a6a2e14abf fix(storage): 关闭 MinIO CNAME 支持解决证书不匹配问题
- 在 AliOssController 中关闭 MinIO 的 CNAME 支持,避免自定义域名被当作 CNAME
- 强制使用 path-style 访问方式,解决虚拟主机风格导致的证书不匹配
- 在 FileRecordServiceImpl 中同步应用同样的配置修改
- 修复了因 OSSV4Signer.getRegion() 返回 null 导致的 NPE 问题
2026-08-07 03:03:25 +08:00
18696f91a6 fix(oss): 修复MinIO SDK配置避免region空指针异常
- 调整MinIO客户端配置,使用 OSSClientBuilder 的链式调用构建
- 明确指定region为"us-east-1",防止自定义域名推导region失败导致NPE
- 保持SignatureVersion为V4,启用从endpoint提取配置信息以支持path-style访问
- 统一AliOssController和FileRecordServiceImpl中的MinIO配置逻辑
2026-08-07 02:54:21 +08:00
49e6cd2197 feat(system): 支持按应用代号发送开通成功邮件
- 在 Company 和 User 实体中新增 appCode 字段,接收前端传入的应用代号
- MainController 传递 appCode 到 Company 实体,用于后续处理
- TenantServiceImpl 根据 appCode 动态生成不同域名的网址和后台管理地址
- 优化企业开通成功邮件内容,增强安全提示和登录信息展示
- 邮件发送逻辑改为根据应用代号映射对应的站点和后台域名
2026-08-07 02:36:10 +08:00
f7f7b30813 feat(auth): 添加邮箱验证码校验接口
- 在EmailCaptchaParam中新增验证码字段code,用于验证码校验
- 新增/verifyEmailCaptcha接口,支持邮箱验证码的校验逻辑
- 实现邮箱格式和验证码非空校验,提供错误反馈
- 验证通过后删除Redis中对应验证码,防止重复使用
- 支持开发环境验证码验证,方便调试验证流程
2026-08-07 01:48:12 +08:00
b043270e52 feat(auth): 添加邮箱验证码校验接口
- 在EmailCaptchaParam中新增验证码字段code,用于验证码校验
- 新增/verifyEmailCaptcha接口,支持邮箱验证码的校验逻辑
- 实现邮箱格式和验证码非空校验,提供错误反馈
- 验证通过后删除Redis中对应验证码,防止重复使用
- 支持开发环境验证码验证,方便调试验证流程
2026-08-07 01:42:54 +08:00
10 changed files with 308 additions and 42 deletions

32
pom.xml
View File

@@ -26,7 +26,31 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties> </properties>
<dependencyManagement>
<dependencies> <dependencies>
<!-- 强制 okhttp/okio 版本,避免 minio-java(要求 okhttp>=4.11.0) 与微信/支付 SDK 引入的 okhttp3 旧版冲突 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>3.6.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 强制 okhttp 4.12.0minio-java 要求 okhttp>=4.11.0,放在 dependencies 首位确保胜出,
避免运行时 NoSuchMethodErrorokhttp3.RequestBody.create 等) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- spring-boot-devtools --> <!-- spring-boot-devtools -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -297,6 +321,14 @@
<version>3.17.4</version> <version>3.17.4</version>
</dependency> </dependency>
<!-- MinIO 官方 Java SDK用于对接 MinIO 兼容 S3 存储。
注意 aliyun OSS SDK 无 AWS4-HMAC-SHA256 签名器,无法对接 MinIO故 minio 分支单独用此 SDK。 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!-- 阿里云 内容安全审核 --> <!-- 阿里云 内容安全审核 -->
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>

View File

@@ -57,6 +57,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
"/api/sendSmsCaptcha", "/api/sendSmsCaptcha",
"/api/sendSmsCaptchaByAdmin", "/api/sendSmsCaptchaByAdmin",
"/api/sendEmailCaptcha", "/api/sendEmailCaptcha",
"/api/verifyEmailCaptcha",
"/api/loginBySms", "/api/loginBySms",
"/api/loginBySuperAdminSms", "/api/loginBySuperAdminSms",
"/api/loginBySelectTenant", "/api/loginBySelectTenant",

View File

@@ -0,0 +1,151 @@
package com.gxwebsoft.common.core.utils;
import io.minio.BucketExistsArgs;
import io.minio.GetBucketPolicyArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.SetBucketPolicyArgs;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* MinIO 对象存储工具类
*
* <p>说明aliyun-sdk-oss 只实现了阿里云自家的 OSS 签名OSSV1/V2/V4Signer
* 不含 AWS4-HMAC-SHA256因此无法对接只接受 AWS4 的 MinIO。
* 所有 MinIO 操作统一走官方 minio-java SDK。
*
* @author WebSoft
* @since 2026-08-07
*/
public class MinioUtil {
/**
* MinIO 默认 region。
* 必须显式指定,否则 minio-java 会先发 {@code GET /{bucket}?location=} 做 region 自动探测,
* 该探测请求在 MinIO + 反向代理场景下容易触发 SignatureDoesNotMatch。
*/
public static final String DEFAULT_REGION = "us-east-1";
/**
* 已确保「存在 + 已配置匿名只读策略」的 bucket 缓存endpoint|bucket
* 进程内缓存,避免每次上传都多发两次 HTTP 请求。
*/
private static final Set<String> READY_BUCKETS = ConcurrentHashMap.newKeySet();
private MinioUtil() {
}
/**
* 规范化 endpoint去掉结尾斜杠避免 minio-java 拼接出双斜杠导致签名不匹配
*/
public static String normalizeEndpoint(String endpoint) {
if (endpoint == null) {
return null;
}
String result = endpoint.trim();
while (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* 构建 MinioClient统一带上 region跳过 getBucketLocation 探测)
*
* @param endpoint S3 API 地址,如 https://minio.websoft.top
* @param accessKeyId AK
* @param accessKeySecret SK
*/
public static MinioClient buildClient(String endpoint, String accessKeyId, String accessKeySecret) {
return MinioClient.builder()
.endpoint(normalizeEndpoint(endpoint))
.credentials(accessKeyId, accessKeySecret)
.region(DEFAULT_REGION)
.build();
}
/**
* 生成 bucket 的匿名只读策略。
* 只放开读取对象,<b>故意不给 s3:ListBucket</b>,避免匿名用户列出桶内全部文件清单。
*/
public static String buildPublicReadPolicy(String bucketName) {
return "{"
+ "\"Version\":\"2012-10-17\","
+ "\"Statement\":["
+ "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:GetBucketLocation\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},"
+ "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:GetObject\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}"
+ "]}";
}
/**
* 确保 bucket 可用:
* <ol>
* <li>bucket 不存在 → 自动创建,并套上匿名只读策略</li>
* <li>bucket 已存在但没有任何策略 → 补上匿名只读策略(兼容历史手动建的裸桶)</li>
* <li>bucket 已有自定义策略 → 不覆盖,尊重人工配置</li>
* </ol>
* 结果会缓存在进程内,同一个 endpoint+bucket 只检查一次。
*
* <p>本方法不会抛异常:即使当前 AK 没有建桶/改策略权限,也只打日志,
* 后续的上传动作照常执行(真正失败会由上传自身报错)。
*
* @return true 表示 bucket 已就绪
*/
public static boolean ensureBucketReady(MinioClient client, String endpoint, String bucketName) {
if (client == null || bucketName == null || bucketName.isEmpty()) {
return false;
}
String cacheKey = normalizeEndpoint(endpoint) + "|" + bucketName;
if (READY_BUCKETS.contains(cacheKey)) {
return true;
}
try {
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
client.setBucketPolicy(SetBucketPolicyArgs.builder()
.bucket(bucketName)
.config(buildPublicReadPolicy(bucketName))
.build());
System.out.println("[MinIO] 自动创建 bucket 并配置匿名只读策略: " + bucketName);
} else {
// minio-java 在无策略时返回空串而非抛错
String policy = client.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
if (policy == null || policy.trim().isEmpty()) {
client.setBucketPolicy(SetBucketPolicyArgs.builder()
.bucket(bucketName)
.config(buildPublicReadPolicy(bucketName))
.build());
System.out.println("[MinIO] 已为存量 bucket 补充匿名只读策略: " + bucketName);
}
}
READY_BUCKETS.add(cacheKey);
return true;
} catch (Exception e) {
// 不阻断上传:权限不足或网络异常时仅告警
System.out.println("[MinIO] ensureBucketReady 失败(不影响上传), bucket=" + bucketName
+ ", reason=" + e.getMessage());
return false;
}
}
/**
* 手动失效缓存(例如后台修改了存储配置后调用)
*/
public static void evictBucketCache(String endpoint, String bucketName) {
READY_BUCKETS.remove(normalizeEndpoint(endpoint) + "|" + bucketName);
}
/**
* 清空全部缓存
*/
public static void clearBucketCache() {
READY_BUCKETS.clear();
}
}

View File

@@ -24,6 +24,7 @@ import com.aliyuncs.profile.IClientProfile;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.config.ConfigProperties; import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.utils.FileServerUtil; import com.gxwebsoft.common.core.utils.FileServerUtil;
import com.gxwebsoft.common.core.utils.MinioUtil;
import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
@@ -37,6 +38,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -100,6 +103,9 @@ public class AliOssController extends BaseController {
String accessKeySecret = settingInfo.getString("accessKeySecret"); String accessKeySecret = settingInfo.getString("accessKeySecret");
String uploadMethod = settingInfo.getString("uploadMethod"); String uploadMethod = settingInfo.getString("uploadMethod");
// 规范化 endpoint去掉结尾斜杠避免 minio-java 拼接出双斜杠导致签名不匹配
endpoint = MinioUtil.normalizeEndpoint(endpoint);
// 判断是否登录 // 判断是否登录
String authorization = getAuthorization(); String authorization = getAuthorization();
@@ -115,18 +121,19 @@ public class AliOssController extends BaseController {
// 上传文件结果 // 上传文件结果
FileRecord result; FileRecord result;
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret); boolean isMinio = "minio".equals(uploadMethod);
// 创建OSSClient实例。 // MinIO 用官方 minio-java SDK原生 AWS4-HMAC-SHA256 + path-style
// MinIO 兼容 S3 协议,需开启 PathStyle 访问endpoint/bucket/key并使用 V4 签名 // aliyun OSS SDK 无 AWS4 签名器,对接不了 MinIO因此 minio 分支独立使用 MinioClient。
OSS ossClient; OSS ossClient = null;
if ("minio".equals(uploadMethod)) { MinioClient minioClient = null;
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration(); if (isMinio) {
// 3.17 已移除 setPathStyleAccess用 setExtractSettingFromEndpoint 让 SDK 对 MinIO 这类 // 显式指定 region=us-east-1跳过 minio-java 的 getBucketLocation 自动探测
// 非阿里云 endpoint 自动按 path-style 访问,并从 endpoint 推导 region // (该探测 GET 在 MinIO + 反代下会触发 SignatureDoesNotMatch
minioConfig.setSignatureVersion(SignVersion.V4); minioClient = MinioUtil.buildClient(endpoint, accessKeyId, accessKeySecret);
minioConfig.setExtractSettingFromEndpoint(true); // bucket 不存在则自动创建,并套上匿名只读策略(存量裸桶也会自动补策略)
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider, minioConfig); MinioUtil.ensureBucketReady(minioClient, endpoint, bucketName);
} else { } else {
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
} }
@@ -137,16 +144,18 @@ public class AliOssController extends BaseController {
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length()); String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
String originalName = file.getOriginalFilename(); String originalName = file.getOriginalFilename();
// 创建PutObjectRequest对象 // 上传文件到对象存储
if (isMinio) {
// minio-javapath 此时无前导 "/",作为对象名(如 2026/08/07/xxx.png默认 region=us-east-1
minioClient.uploadObject(UploadObjectArgs.builder()
.bucket(bucketName)
.object(path)
.filename(upload.getAbsolutePath())
.build());
} else {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 ossClient.putObject(putObjectRequest);
// ObjectMetadata metadata = new ObjectMetadata(); }
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传文件。
PutObjectResult ossResult = ossClient.putObject(putObjectRequest);
// 保存记录并返回 // 保存记录并返回
result = new FileRecord(); result = new FileRecord();
@@ -177,7 +186,7 @@ public class AliOssController extends BaseController {
// 根据文件类型设置缩略图和预览图 // 根据文件类型设置缩略图和预览图
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL // MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
boolean isMinio = "minio".equals(uploadMethod); isMinio = "minio".equals(uploadMethod);
if (FileServerUtil.isImage(contentType)) { if (FileServerUtil.isImage(contentType)) {
if (isMinio) { if (isMinio) {
result.setThumbnail(bucketDomain + path); result.setThumbnail(bucketDomain + path);
@@ -228,10 +237,15 @@ public class AliOssController extends BaseController {
+ "a serious internal problem while trying to communicate with OSS, " + "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network."); + "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage()); System.out.println("Error Message:" + ce.getMessage());
} catch (Exception e) {
// 兼容 minio-java 抛出的异常MinioException / IOException 等)
System.out.println("Caught an upload Exception: " + e.getMessage());
e.printStackTrace();
} finally { } finally {
if (ossClient != null) { if (ossClient != null) {
ossClient.shutdown(); ossClient.shutdown();
} }
// MinioClient 内部复用 HttpClient 连接池,无需显式关闭
} }
return fail("上传失败", null); return fail("上传失败", null);
} }

View File

@@ -877,6 +877,31 @@ public class MainController extends BaseController {
} }
} }
@Operation(summary = "校验邮箱验证码")
@PostMapping("/verifyEmailCaptcha")
public ApiResult<?> verifyEmailCaptcha(@RequestBody EmailCaptchaParam param) {
if (param == null || StrUtil.isBlank(param.getEmail())) {
return fail("邮箱不能为空");
}
if (!CommonUtil.isValidEmail(param.getEmail())) {
return fail("请输入有效的邮箱地址");
}
if (StrUtil.isBlank(param.getCode())) {
return fail("验证码不能为空");
}
// 与 sendEmailCaptcha 写入的 key 保持一致emailCode:邮箱
String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
String cachedCode = redisUtil.get(key);
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
if (cachedCode == null
|| (!param.getCode().equals(cachedCode) && !param.getCode().equals(devCode))) {
return fail("邮箱验证码不正确");
}
// 校验通过即删除,防止重复使用
redisUtil.delete(key);
return success("邮箱验证通过");
}
@OperationLog @OperationLog
@Operation(summary = "重置密码") @Operation(summary = "重置密码")
@PutMapping("/password") @PutMapping("/password")
@@ -1578,6 +1603,7 @@ public class MainController extends BaseController {
company.setEmail(email); company.setEmail(email);
company.setPhone(phone); company.setPhone(phone);
company.setPassword(password); company.setPassword(password);
company.setAppCode(user.getAppCode());
company.setTid(tenant.getTenantId()); company.setTid(tenant.getTenantId());
company.setShortName(tenantName); company.setShortName(tenantName);
company.setCategoryId(661); company.setCategoryId(661);

View File

@@ -294,6 +294,10 @@ public class Company implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String password; private String password;
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site")
@TableField(exist = false)
private String appCode;
@Schema(description = "手机号(脱敏)") @Schema(description = "手机号(脱敏)")
@TableField(exist = false) @TableField(exist = false)
private String mobile; private String mobile;

View File

@@ -61,6 +61,10 @@ public class User implements UserDetails {
@Schema(description = "邮箱") @Schema(description = "邮箱")
private String email; private String email;
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site")
@TableField(exist = false)
private String appCode;
@Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)") @Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)")
@TableField(exist = false) @TableField(exist = false)
private String emailCode; private String emailCode;

View File

@@ -21,6 +21,9 @@ public class EmailCaptchaParam implements Serializable {
@Schema(description = "邮箱") @Schema(description = "邮箱")
private String email; private String email;
@Schema(description = "邮箱验证码(校验时使用)")
private String code;
@Schema(description = "租户ID") @Schema(description = "租户ID")
private String tenantId; private String tenantId;

View File

@@ -13,6 +13,7 @@ import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.utils.CommonUtil; import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.core.utils.MinioUtil;
import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.Company; import com.gxwebsoft.common.system.entity.Company;
@@ -22,6 +23,8 @@ import com.gxwebsoft.common.system.param.FileRecordParam;
import com.gxwebsoft.common.system.service.CompanyService; import com.gxwebsoft.common.system.service.CompanyService;
import com.gxwebsoft.common.system.service.FileRecordService; import com.gxwebsoft.common.system.service.FileRecordService;
import com.gxwebsoft.common.system.service.SettingService; import com.gxwebsoft.common.system.service.SettingService;
import io.minio.MinioClient;
import io.minio.RemoveObjectArgs;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -96,24 +99,31 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
String accessKeyId = uploadConfig.getString("accessKeyId"); String accessKeyId = uploadConfig.getString("accessKeyId");
String accessKeySecret = uploadConfig.getString("accessKeySecret"); String accessKeySecret = uploadConfig.getString("accessKeySecret");
String uploadMethod = uploadConfig.getString("uploadMethod"); String uploadMethod = uploadConfig.getString("uploadMethod");
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret); // 规范化 endpoint去掉结尾斜杠避免 minio-java 拼接出双斜杠导致签名不匹配
// MinIO 兼容 S3 协议,需开启 PathStyle 访问 + V4 签名 endpoint = MinioUtil.normalizeEndpoint(endpoint);
OSS ossClient; boolean isMinio = "minio".equals(uploadMethod);
if ("minio".equals(uploadMethod)) { OSS ossClient = null;
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration(); MinioClient minioClient = null;
// 3.17 已移除 setPathStyleAccess用 setExtractSettingFromEndpoint 让 SDK 对 MinIO 这类 if (isMinio) {
// 非阿里云 endpoint 自动按 path-style 访问,并从 endpoint 推导 region // MinIO 用官方 minio-java SDK原生 AWS4 + path-stylealiyun OSS SDK 无法对接 MinIO
minioConfig.setSignatureVersion(SignVersion.V4); // 显式指定 region=us-east-1跳过 getBucketLocation 自动探测(该探测 GET 会触发 SignatureDoesNotMatch
minioConfig.setExtractSettingFromEndpoint(true); minioClient = MinioUtil.buildClient(endpoint, accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider, minioConfig);
} else { } else {
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
} }
for (FileRecord fileRecord : fileRecords) { for (FileRecord fileRecord : fileRecords) {
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), "")); fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
try { try {
// 删除远程文件 // 删除远程文件
if (isMinio) {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(fileRecord.getPath())
.build());
} else {
ossClient.deleteObject(bucketName, fileRecord.getPath()); ossClient.deleteObject(bucketName, fileRecord.getPath());
}
// 释放空间大小 // 释放空间大小
if (fileRecord.getCompanyId() > 0) { if (fileRecord.getCompanyId() > 0) {
Company company = companyService.getById(fileRecord.getCompanyId()); Company company = companyService.getById(fileRecord.getCompanyId());

View File

@@ -619,17 +619,38 @@ import java.util.stream.Collectors;
} }
} }
// 发送邮件通知 // 发送邮件通知(按应用代号区分域名:前端注册时传入 appCode未知默认 site
String title = "恭喜!您的应用已创建成功"; // mp -> https://mp-{tid}.shoplnk.cn 后台 https://mp.websoft.top
String appUrl = "\r\n应用地址" + DomainUtil.getSiteUrl(company.getTid().toString()); // shop -> https://shop-{tid}.shoplnk.cn 后台 https://shop.websoft.top
String appName = "\r\n应用名称" + company.getShortName(); // site -> https://site-{tid}.sitelnk.cn 后台 https://site.websoft.top
String adminUrl = "\r\n后台管理" + DomainUtil.getAdminUrl(company.getTid().toString()); final Map<String, String[]> appDomainMap = new HashMap<>(3);
String account = "\r\n账号admin"; appDomainMap.put("mp", new String[]{"mp", "shoplnk.cn", "mp.websoft.top"});
String password = "\r\n密码" + company.getPassword(); appDomainMap.put("shop", new String[]{"shop", "shoplnk.cn", "shop.websoft.top"});
String content = title + appUrl + appName + adminUrl + account + password; appDomainMap.put("site", new String[]{"site", "sitelnk.cn", "site.websoft.top"});
final String appCode = StrUtil.isBlank(company.getAppCode()) ? "site" : company.getAppCode();
final String[] domainCfg = appDomainMap.getOrDefault(appCode, appDomainMap.get("site"));
final String sitePrefix = domainCfg[0];
final String siteDomain = domainCfg[1];
final String adminHost = domainCfg[2];
final String siteUrl = "https://" + sitePrefix + "-" + company.getTid() + "." + siteDomain;
final String adminUrl = "https://" + adminHost;
final String title = "您的企业官网已开通成功";
final StringBuilder content = new StringBuilder();
content.append("尊敬的用户:\r\n\r\n");
content.append("恭喜!您的企业官网「").append(company.getShortName()).append("」已开通成功。\r\n\r\n");
content.append("您可以使用以下信息登录管理后台,继续完善站点内容:\r\n\r\n");
content.append("• 官网地址:").append(siteUrl).append("\r\n");
content.append("• 后台地址:").append(adminUrl).append("\r\n");
content.append("• 登录账号:").append(company.getEmail()).append("\r\n");
content.append("• 登录密码:").append(company.getPassword()).append("\r\n\r\n");
content.append("安全提示:\r\n");
content.append("1. 请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。\r\n");
content.append("2. 建议首次登录后立即进入「个人中心 > 修改密码」重置密码。\r\n\r\n");
content.append("点击登录后台:").append(adminUrl).append("\r\n\r\n");
content.append("如有疑问,请联系客服或访问帮助中心。");
// 发送邮件通知 // 发送邮件通知
if (company.getEmail() != null) { if (company.getEmail() != null) {
emailRecordService.sendEmail(title, content, company.getEmail(), company.getTid()); emailRecordService.sendEmail(title, content.toString(), company.getEmail(), company.getTid());
} }
return company; return company;
} }