Skip to content

Commit 20fa940

Browse files
committed
Merge branch 'main'
2 parents 7c04a07 + ce4b239 commit 20fa940

16 files changed

Lines changed: 453 additions & 55 deletions

File tree

common/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
<artifactId>thumbnailator</artifactId>
3434
</dependency>
3535

36+
<!--minio-->
37+
<dependency>
38+
<groupId>io.minio</groupId>
39+
<artifactId>minio</artifactId>
40+
</dependency>
41+
3642
<!-- 邮件服务 -->
3743
<dependency>
3844
<groupId>org.springframework.boot</groupId>
@@ -45,6 +51,7 @@
4551
<artifactId>spring-boot-starter-thymeleaf</artifactId>
4652
</dependency>
4753

54+
<!-- 验证码 -->
4855
<dependency>
4956
<groupId>cn.hutool</groupId>
5057
<artifactId>hutool-captcha</artifactId>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.ligg.common.config;
2+
3+
import io.minio.MinioClient;
4+
import lombok.Data;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
/**
10+
* @author Ligg
11+
* @create_time 2025/11/10 11:00
12+
* @update_time 2025/11/10 11:00
13+
**/
14+
@Data
15+
@Configuration
16+
@ConfigurationProperties(prefix = "minio")
17+
public class MinioConfig {
18+
private String endpoint;
19+
private String accessKey;
20+
private String secretKey;
21+
private String bucketName;
22+
23+
@Bean
24+
public MinioClient minioClient() {
25+
return MinioClient.builder()
26+
.endpoint(endpoint)
27+
.credentials(accessKey, secretKey)
28+
.build();
29+
}
30+
}

common/src/main/java/com/ligg/common/module/vo/ProductVo.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public class ProductVo {
2626
/**
2727
* 图片路径
2828
*/
29-
@Schema(description = "图片链接s")
30-
private ImagesVo images;
29+
@Schema(description = "图片链接")
30+
private String images;
3131

3232
/**
3333
* 原价

common/src/main/java/com/ligg/common/service/FileService.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ public interface FileService {
1515
*/
1616
String uploadImage(MultipartFile imageFile, String FilePath);
1717

18+
/**
19+
* 上传图片文件到Minio
20+
*
21+
* @param imageFile 图片文件
22+
* @param filePath 指定文件保存的路径(前缀)
23+
* @return 图片访问路径
24+
*/
25+
String minioFileUpload(MultipartFile imageFile, String filePath);
26+
27+
/**
28+
* 删除minio中的文件
29+
*/
30+
void deleteMinioFile(String fileUrl);
31+
1832
/**
1933
* 获取多种图片输入流
2034
*

common/src/main/java/com/ligg/common/service/UserService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ public interface UserService extends IService<UserEntity> {
4040

4141
/**
4242
* 更新用户头像
43+
*
44+
* @return 头像地址
4345
*/
44-
void updateUserAvatar(MultipartFile avatarFile);
46+
String updateAvatar(MultipartFile avatarFile,String userId);
4547

4648
/**
4749
* 更新用户信息

common/src/main/java/com/ligg/common/service/impl/FileServiceImpl.java

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
**/
55
package com.ligg.common.service.impl;
66

7+
import com.ligg.common.config.MinioConfig;
8+
import com.ligg.common.enums.BusinessStates;
79
import com.ligg.common.enums.ImageType;
810
import com.ligg.common.constants.Constant;
911
import com.ligg.common.service.FileService;
12+
import io.minio.*;
13+
import io.minio.errors.*;
14+
import io.minio.http.Method;
15+
import lombok.RequiredArgsConstructor;
1016
import lombok.extern.slf4j.Slf4j;
1117
import net.coobird.thumbnailator.Thumbnails;
1218
import org.springframework.beans.factory.annotation.Value;
@@ -21,16 +27,24 @@
2127
import java.nio.file.Files;
2228
import java.nio.file.Path;
2329
import java.nio.file.Paths;
30+
import java.security.InvalidKeyException;
31+
import java.security.NoSuchAlgorithmException;
2432
import java.util.UUID;
2533

2634
@Slf4j
2735
@Service
36+
@RequiredArgsConstructor
2837
public class FileServiceImpl implements FileService {
2938

3039
// 图片存储根路径
3140
@Value("${file.image.base-path}")
3241
private String IMAGE_PATH;
3342

43+
private final MinioConfig minioConfig;
44+
45+
private final MinioClient minioClient;
46+
47+
3448
@Override
3549
public String uploadImage(MultipartFile imageFile, String path) {
3650
if (imageFile.isEmpty()) {
@@ -76,6 +90,134 @@ public String uploadImage(MultipartFile imageFile, String path) {
7690
}
7791
}
7892

93+
/**
94+
* 上传图片文件到Minio
95+
*
96+
* @param imageFile 图片文件
97+
* @param filePath 指定文件保存的路径(前缀)
98+
* @return 图片访问路径
99+
*/
100+
@Override
101+
public String minioFileUpload(MultipartFile imageFile, String filePath) {
102+
String bucketName = minioConfig.getBucketName();
103+
String datePath = java.time.LocalDate.now().toString();
104+
String fullFilePath = String.join("/", (filePath != null ? filePath : ""), datePath, UUID.randomUUID() + "_" + imageFile.getOriginalFilename());
105+
//创建存储桶(如果不存在)
106+
try {
107+
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
108+
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
109+
//设置访问权限
110+
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
111+
.bucket(bucketName)
112+
.config(getPublicReadPolicy(bucketName))
113+
.build());
114+
}
115+
} catch (Exception e) {
116+
log.error("创建存储桶失败: {}", e.getMessage());
117+
throw new RuntimeException(BusinessStates.INTERNAL_SERVER_ERROR.getMessage());
118+
}
119+
120+
//上传文件
121+
try (InputStream inputStream = imageFile.getInputStream()) {
122+
minioClient.putObject(PutObjectArgs.builder()
123+
.bucket(bucketName)
124+
.object(fullFilePath)
125+
.stream(inputStream, imageFile.getSize(), -1)
126+
.contentType(imageFile.getContentType())
127+
.build());
128+
} catch (Exception e) {
129+
log.error("上传文件失败: {}", e.getMessage());
130+
}
131+
//这里不返回minioConfig.getEndpoint() (minio服务地址) 让前端自己做代理转发
132+
return "/" + bucketName + fullFilePath;
133+
}
134+
135+
/**
136+
* 删除minio中的文件
137+
*/
138+
@Override
139+
@Async("fileTaskExecutor")
140+
public void deleteMinioFile(String fileUrl) {
141+
//截取相对路径url
142+
String[] splitUrl = fileUrl.split(minioConfig.getBucketName());
143+
//检查存储桶是否存在
144+
try {
145+
minioClient.bucketExists(
146+
BucketExistsArgs.builder().bucket(minioConfig.getBucketName()).build()
147+
);
148+
} catch (Exception e) {
149+
log.error("存储桶不存在: {}", e.getMessage());
150+
throw new RuntimeException(BusinessStates.METHOD_NOT_ALLOWED.getMessage());
151+
}
152+
153+
//接收文件url的是存储桶的相对路径,而不是完整的url访问路径
154+
if (isFileExist(splitUrl[1])) {
155+
try {
156+
//删除文件
157+
minioClient.removeObject(RemoveObjectArgs.builder()
158+
.bucket(minioConfig.getBucketName())
159+
.object(splitUrl[1])
160+
.build());
161+
return;
162+
} catch (Exception e) {
163+
log.error("删除文件失败: {}", e.getMessage());
164+
}
165+
}
166+
log.error("文件不存在: {}", fileUrl);
167+
}
168+
169+
/**
170+
* 判断文件是否存储
171+
* fileUrl接收的是存储桶的相对路径,而不是完整的url访问路径
172+
*/
173+
private boolean isFileExist(String fileUrl) {
174+
try {
175+
minioClient.statObject(
176+
StatObjectArgs.builder()
177+
.bucket(minioConfig.getBucketName())
178+
.object(fileUrl)
179+
.build()
180+
);
181+
return true;
182+
} catch (Exception e) {
183+
return false;
184+
}
185+
}
186+
187+
/**
188+
* url签名
189+
*/
190+
private String getUrlSignature(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
191+
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
192+
.bucket(bucketName)
193+
.object(objectName)
194+
.method(Method.GET)
195+
.build());
196+
}
197+
198+
/**
199+
* 存储桶配置
200+
* "Principal": "*" 表示允许所有用户访问。
201+
* "Action": "s3:GetObject" 表示允许下载/读取对象
202+
* "Action": "s3:PutObject" 表示允许上传/写入对象
203+
* %s 是格式化占位符,会被传入的 bucketName 替换
204+
*/
205+
private String getPublicReadPolicy(String bucketName) {
206+
return """
207+
{
208+
"Version":"2012-10-17",
209+
"Statement":[
210+
{
211+
"Effect":"Allow",
212+
"Principal":"*",
213+
"Action":["s3:GetObject"],
214+
"Resource":["arn:aws:s3:::%s/*"]
215+
}
216+
]
217+
}
218+
""".formatted(bucketName);
219+
}
220+
79221
/**
80222
* 获取多质量图片输入流
81223
*
@@ -153,7 +295,7 @@ public StreamingResponseBody getImageInputStream(String path, String date, Strin
153295
@Override
154296
@Deprecated
155297
public void deleteFile(String filePath) {
156-
Path imagePath = Paths.get(getBasePath(), filePath).toAbsolutePath().normalize() ;
298+
Path imagePath = Paths.get(getBasePath(), filePath).toAbsolutePath().normalize();
157299
//检查文件是否存在
158300
if (Files.exists(imagePath)) {
159301
try {
@@ -189,6 +331,7 @@ public void deleteFileAsync(String filePath) {
189331

190332
/**
191333
* 获取基础路径
334+
*
192335
* @return 绝对路径
193336
*/
194337
private String getBasePath() {

common/src/main/java/com/ligg/common/service/impl/UserServiceImpl.java

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.beans.factory.annotation.Value;
3030
import org.springframework.stereotype.Service;
3131
import org.springframework.transaction.annotation.Transactional;
32+
import org.springframework.util.StringUtils;
3233
import org.springframework.web.multipart.MultipartFile;
3334

3435
import java.math.BigDecimal;
@@ -129,24 +130,18 @@ public void debit(@NotNull BigDecimal amount) {
129130

130131
/**
131132
* 更新用户头像
133+
*
134+
* @return 头像访问路径
132135
*/
133136
@Override
134-
public void updateUserAvatar(MultipartFile avatarFile) {
135-
Map<String, Object> UserInfo = ThreadLocalUtil.get();
136-
String userId = (String) UserInfo.get(UserConstant.USER_ID);
137-
137+
public String updateAvatar(MultipartFile avatarFile, String userId) {
138138
UserEntity userInfo = userMapper.selectById(userId);
139-
String avatar = userInfo.getAvatar();
140-
//如果头像不 != null || "" 则删除旧头像
141-
if (avatar != null && !avatar.equals("/")) {
142-
String avatarPath = IMAGE_PATH + avatar.replace(Constant.IMAGE_RELATIVE_PATH, "");
143-
fileService.deleteFileAsync(avatarPath);
139+
String userAvatarUrl = userInfo.getAvatar();
140+
//如果头像不 != null && "/" 则删除旧头像
141+
if (StringUtils.hasText(userAvatarUrl) && !userAvatarUrl.equals("/")) {
142+
fileService.deleteMinioFile(userAvatarUrl);
144143
}
145-
String imagePath = fileService.uploadImage(avatarFile, Constant.AVATAR_FILE_PATH);
146-
UserEntity userEntity = new UserEntity();
147-
userEntity.setAvatar(imagePath);
148-
userEntity.setUserId(userId);
149-
userMapper.updateUserInfo(userEntity);
144+
return fileService.minioFileUpload(avatarFile, Constant.AVATAR_FILE_PATH);
150145
}
151146

152147
/**
@@ -157,9 +152,6 @@ public int updateUserInfo(UserEntity userEntity) {
157152
if (userEntity.getPassword() != null) {
158153
userEntity.setPassword(BCryptUtil.encrypt(userEntity.getPassword()));
159154
}
160-
Map<String, Object> UserInfo = ThreadLocalUtil.get();
161-
String userId = (String) UserInfo.get(UserConstant.USER_ID);
162-
userEntity.setUserId(userId);
163155
int number = userMapper.updateUserInfo(userEntity);
164156
if (number > 0) {
165157
String userKey = UserConstant.USER_INFO + ":" + userEntity.getUserId();

common/src/main/java/com/ligg/common/service/impl/product/ProductServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
55
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
66
import com.ligg.common.constants.ProductConstant;
7-
import com.ligg.common.enums.BusinessStates;
87
import com.ligg.common.module.entity.ProductEntity;
98
import com.ligg.common.module.entity.ProductDetailEntity;
109
import com.ligg.common.mapper.product.ProductMapper;
@@ -28,6 +27,7 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity
2827

2928
private final RedisUtil redisUtil;
3029

30+
3131
@Override
3232
public void saveFeatured(ProductEntity product) {
3333
int insert = productMapper.insert(product);

common/src/main/java/com/ligg/common/service/product/ProductService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ public interface ProductService extends IService<ProductEntity> {
5050
* @return 精选商品分页列表
5151
*/
5252
PageVo<ProductEntity> getFeaturedPageList(Long pageNumber, Long pageSize);
53-
}
53+
}

entrance/src/main/resources/application-dev.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,11 @@ mybatis-plus:
5050
##文件配置
5151
file:
5252
image:
53-
base-path: ${user.dir}/resources/image # 用相对路径(相对于项目运行目录)
53+
base-path: ${user.dir}/resources/image #
54+
55+
minio:
56+
endpoint: ${ENDPOINT}
57+
access-key: ${ACCESS_KEY}
58+
secret-key: ${SECRET_KEY}
59+
bucket-name: resources
60+

0 commit comments

Comments
 (0)