diff --git a/ebike-staff/pom.xml b/ebike-staff/pom.xml index c4c1257..eb7dcea 100644 --- a/ebike-staff/pom.xml +++ b/ebike-staff/pom.xml @@ -156,6 +156,11 @@ spring-boot-starter-actuator + + io.minio + minio + ${minio.version} + diff --git a/ebike-staff/src/main/java/com/cdzy/staff/config/MinioConfig.java b/ebike-staff/src/main/java/com/cdzy/staff/config/MinioConfig.java new file mode 100644 index 0000000..670f439 --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/config/MinioConfig.java @@ -0,0 +1,81 @@ +package com.cdzy.staff.config; + +import com.cdzy.staff.utils.MinioUtil; +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import io.minio.SetBucketPolicyArgs; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = "minio") +public class MinioConfig { + private String endpoint; + private String accessKey; + private String secretKey; + private String bucketName; + private String showUrl; + + + @Bean + public MinioClient minioClient() throws Exception { + // 1. 创建MinioClient实例 + MinioClient minioClient = MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + // 2. 检查存储桶是否存在,不存在则创建 + initializeBucket(minioClient, bucketName); + MinioUtil.setBaseUrl(showUrl+"/"+bucketName); + MinioUtil.setBucketName(bucketName); + return minioClient; + } + + private void initializeBucket(MinioClient minioClient, String bucketName) throws Exception { + try { + // 检查存储桶是否存在:cite[1]:cite[5] + boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder() + .bucket(bucketName) + .build()); + + if (!isExist) { + // 创建存储桶:cite[1]:cite[5] + minioClient.makeBucket(MakeBucketArgs.builder() + .bucket(bucketName) + .build()); + // 设置存储桶策略为公共可读 + String policyJson = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::%s/*"] + } + ] + } + """.formatted(bucketName); + + minioClient.setBucketPolicy(SetBucketPolicyArgs.builder() + .bucket(bucketName) + .config(policyJson) + .build()); + log.info("存储桶 {} 创建成功", bucketName); + } else { + log.info("存储桶 {} 已存在", bucketName); + } + } catch (Exception e) { + throw new Exception("初始化Minio存储桶失败: " + e.getMessage(), e); + } + } +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/controller/EbikeImgController.java b/ebike-staff/src/main/java/com/cdzy/staff/controller/EbikeImgController.java new file mode 100644 index 0000000..8916ac0 --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/controller/EbikeImgController.java @@ -0,0 +1,103 @@ +package com.cdzy.staff.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.cdzy.common.enums.Message; +import com.cdzy.common.model.request.PageParam; +import com.cdzy.common.model.response.JsonResult; +import com.cdzy.staff.model.entity.EbikeImg; +import com.cdzy.staff.service.EbikeImgService; +import com.mybatisflex.core.paginate.Page; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 图片表 控制层。 + * + * @author attiya + * @since 2025-12-17 + */ +@RestController +@RequestMapping("/ebikeImg") +public class EbikeImgController { + + @Resource + private EbikeImgService ebikeImgService; + + /** + * 添加图片。 + * + * @param ebikeImg 图片表 + * @return {@code true} 添加成功,{@code false} 添加失败 + */ + @PostMapping("save") + public JsonResult save(@RequestBody @Validated EbikeImg ebikeImg) { + ebikeImg.setCreatedBy(StpUtil.getLoginIdAsLong()); + ebikeImgService.save(ebikeImg); + return JsonResult.success(); + } + + /** + * 根据主键删除图片。 + * + * @param imgId 主键 + * @return {@code true} 删除成功,{@code false} 删除失败 + */ + @GetMapping("remove") + public JsonResult remove(@RequestParam("imgId") Long imgId) { + ebikeImgService.removeById(imgId); + return JsonResult.success(); + } + + /** + * 根据主键更新图片。 + * + * @param ebikeImg 图片表 + * @return {@code true} 更新成功,{@code false} 更新失败 + */ + @PostMapping("update") + public JsonResult update(@RequestBody @Validated EbikeImg ebikeImg) { + ebikeImgService.updateById(ebikeImg); + return JsonResult.success(); + } + + /** + * 查询所有图片。 + * + * @return 所有数据 + */ + @GetMapping("list") + public JsonResult> list() { + List list = ebikeImgService.list(); + return JsonResult.success(list); + } + + /** + * 分页查询图片表。 + * + * @param pageParam 分页对象 + * @return 分页对象 + */ + @GetMapping("page") + public JsonResult> page(PageParam pageParam) { + Page page = ebikeImgService.page(pageParam.getPage()); + return JsonResult.success(page); + } + + + /** + * 上传静态图片 + * + * @param file 文件 + * @return 操作结果 + */ + @PostMapping("upload") + public JsonResult upload(@RequestParam("file") MultipartFile file) throws Exception { + String result = ebikeImgService.upload(file); + return JsonResult.success(Message.SUCCESS, result); + } + +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/mapper/EbikeImgMapper.java b/ebike-staff/src/main/java/com/cdzy/staff/mapper/EbikeImgMapper.java new file mode 100644 index 0000000..b61c8ba --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/mapper/EbikeImgMapper.java @@ -0,0 +1,14 @@ +package com.cdzy.staff.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cdzy.staff.model.entity.EbikeImg; + +/** + * 图片表 映射层。 + * + * @author attiya + * @since 2025-12-17 + */ +public interface EbikeImgMapper extends BaseMapper { + +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/model/entity/EbikeImg.java b/ebike-staff/src/main/java/com/cdzy/staff/model/entity/EbikeImg.java new file mode 100644 index 0000000..b1616ad --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/model/entity/EbikeImg.java @@ -0,0 +1,58 @@ +package com.cdzy.staff.model.entity; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 图片表 实体类。 + * + * @author attiya + * @since 2025-12-17 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table("ebike_img") +public class EbikeImg implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Id + private Long imgId; + + /** + * 名称 + */ + @NotNull(message = "图片名称不能为空") + private String imgName; + + /** + * 唯一分区编码 + */ + @NotNull(message = "图片标识不能为空") + private String imgCode; + + @Column(onInsertValue = "now()") + private LocalDateTime createdAt; + + private Long createdBy; + + /** + * 图片路径 + */ + @NotNull(message = "图片路径不能为空") + private String fileUrl; + +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/service/EbikeImgService.java b/ebike-staff/src/main/java/com/cdzy/staff/service/EbikeImgService.java new file mode 100644 index 0000000..322f45a --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/service/EbikeImgService.java @@ -0,0 +1,21 @@ +package com.cdzy.staff.service; + +import com.cdzy.staff.model.entity.EbikeImg; +import com.mybatisflex.core.service.IService; +import org.springframework.web.multipart.MultipartFile; + +/** + * 图片表 服务层。 + * + * @author attiya + * @since 2025-12-17 + */ +public interface EbikeImgService extends IService { + + /** + * 上传图片 + * @param file 文件 + * @return 路径 + */ + String upload(MultipartFile file) throws Exception; +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/service/impl/EbikeImgServiceImpl.java b/ebike-staff/src/main/java/com/cdzy/staff/service/impl/EbikeImgServiceImpl.java new file mode 100644 index 0000000..99ba1eb --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/service/impl/EbikeImgServiceImpl.java @@ -0,0 +1,36 @@ +package com.cdzy.staff.service.impl; + +import cn.hutool.core.io.file.FileNameUtil; +import com.cdzy.staff.mapper.EbikeImgMapper; +import com.cdzy.staff.model.entity.EbikeImg; +import com.cdzy.staff.service.EbikeImgService; +import com.cdzy.staff.utils.MinioUtil; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +/** + * 图片表 服务层实现。 + * + * @author attiya + * @since 2025-12-17 + */ +@Service +public class EbikeImgServiceImpl extends ServiceImpl implements EbikeImgService{ + + private static final String imageUrl = "/img-file/"; + + + @Resource + MinioUtil minioUtil; + + @Override + public String upload(MultipartFile file) throws Exception { + String objectName = imageUrl + UUID.randomUUID().toString().replace("-", "") + "." + FileNameUtil.extName(file.getOriginalFilename()); + minioUtil.uploadFile(file.getInputStream(), objectName); + return minioUtil.getShowUrl(objectName); + } +} diff --git a/ebike-staff/src/main/java/com/cdzy/staff/utils/MinioUtil.java b/ebike-staff/src/main/java/com/cdzy/staff/utils/MinioUtil.java new file mode 100644 index 0000000..0fe853c --- /dev/null +++ b/ebike-staff/src/main/java/com/cdzy/staff/utils/MinioUtil.java @@ -0,0 +1,280 @@ +package com.cdzy.staff.utils; + +import io.minio.*; +import io.minio.http.Method; +import io.minio.messages.Bucket; +import jakarta.annotation.Resource; +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +public class MinioUtil { + + @Getter + @Setter + private static String baseUrl; + + @Getter + @Setter + private static String bucketName; + + + @Resource + private MinioClient minioClient; + + /** + * 创建桶 + * @param bucket 数据桶名称 + * @throws Exception 异常 + */ + public void createBucket(String bucket) throws Exception { + //查看桶是否存在 + boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); + if (!exists){ + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); + } + } + + /** + * 上传文件 + * @param inputStream 文件流 + * @param bucket 数据桶名称 + * @param objectName 文件名称 + * @throws Exception 异常 + */ + public void uploadFile(InputStream inputStream, String bucket, String objectName) throws Exception{ + //上传文件 + minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName) + .stream(inputStream,-1,Integer.MAX_VALUE).build()); + } + + /** + * 上传文件至默认桶内 + * @param inputStream 文件流 + * @param objectName 文件名称 + * @throws Exception 异常 + */ + public void uploadFile(InputStream inputStream, String objectName) throws Exception{ + //上传文件 + uploadFile(inputStream, bucketName, objectName); + } + + /** + * 获取文件分析连接(7天有效 + * @param bucket 数据桶名称 + * @param fileName 文件名称 + * @return url + * @throws Exception 异常 + */ + public String getFileUrl(String bucket, String fileName) throws Exception { + + return minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucket) + .object(fileName) + .expiry(7, TimeUnit.DAYS) // 有效期7天 + .build() + ); + } + + /** + * 获取默认桶内文件分析连接(7天有效 + * @param fileName 文件名称 + * @return url + * @throws Exception 异常 + */ + public String getFileUrl(String fileName) throws Exception { + + return minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucketName) + .object(fileName) + .expiry(7, TimeUnit.DAYS) // 有效期7天 + .build() + ); + } + + /** + * 获取所有的桶 + * @return 数据桶名称列表 + * @throws Exception 异常 + */ + public List getAllBucket() throws Exception{ + List buckets = minioClient.listBuckets(); + return buckets.stream().map(Bucket::name).collect(Collectors.toList()); + } + + /** + * 下载文件 + * @param bucket 数据桶名称 + * @param objectName 文件名称 + * @return 文件流 + * @throws Exception 异常 + */ + public InputStream downLoad(String bucket,String objectName) throws Exception{ + return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build()); + } + + /** + * 下载默认桶内文件 + * @param objectName 文件名称 + * @return 文件流 + * @throws Exception 异常 + */ + public InputStream downLoad(String objectName) throws Exception{ + return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } + + /** + * 删除桶 + * @param bucket 数据桶名称 + * @throws Exception 异常 + */ + public void deleteBucket(String bucket) throws Exception{ + minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build()); + } + + /** + * 删除文件 + * @param bucket 数据桶名称 + * @param objectName 文件名称 + * @throws Exception 异常 + */ + public void deleteObject(String bucket,String objectName) throws Exception{ + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build()); + } + + /** + * 删除默认桶内文件 + * @param objectName 文件名称 + * @throws Exception 异常 + */ + public void deleteObject(String objectName) throws Exception{ + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } + + /** + * 批量上传文件(原子性操作) + * @param fileStreams 文件流列表 + * @param bucket 数据桶名称 + * @param objectNames 文件名列表 + * @return 是否全部成功 + * @throws Exception 异常 + */ + public boolean batchUploadFiles(List fileStreams, String bucket, + List objectNames) throws Exception { + if (fileStreams.size() != objectNames.size()) { + throw new IllegalArgumentException("文件流和文件名数量不匹配"); + } + + List successUploads = new ArrayList<>(); + + try { + // 确保桶存在 + createBucket(bucket); + + // 逐个上传文件 + for (int i = 0; i < fileStreams.size(); i++) { + String objectName = objectNames.get(i); + InputStream inputStream = fileStreams.get(i); + + minioClient.putObject(PutObjectArgs.builder() + .bucket(bucket) + .object(objectName) + .stream(inputStream, -1, Integer.MAX_VALUE) + .build()); + + // 记录成功上传的文件 + successUploads.add(objectName); + } + + return true; + + } catch (Exception e) { + // 回滚:删除已上传的文件 + rollbackBatchUpload(bucket, successUploads); + throw new Exception("批量上传失败,已回滚", e); + } + } + + /** + * 回滚批量上传操作 + * @param bucket 数据桶名称 + * @param objectNames 需要删除的文件名列表 + */ + private void rollbackBatchUpload(String bucket, List objectNames) { + if (objectNames.isEmpty()) { + return; + } + + for (String objectName : objectNames) { + try { + minioClient.removeObject(RemoveObjectArgs.builder() + .bucket(bucket) + .object(objectName) + .build()); + } catch (Exception rollbackException) { + // 记录回滚失败,但不影响主流程 + System.err.println("回滚删除文件失败: " + objectName + ", 错误: " + rollbackException.getMessage()); + } + } + } + + /** + * 批量上传到默认桶 + */ + public boolean batchUploadFiles(List fileStreams, + List objectNames) throws Exception { + return batchUploadFiles(fileStreams, bucketName, objectNames); + } + + /** + * 获取MinIO中指定文件的文件流 + * @param bucket 数据桶名称 + * @param objectName 文件名称 + * @return 文件流 + * @throws Exception 异常 + */ + public InputStream getFileStream(String bucket, String objectName) throws Exception { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucket) + .object(objectName) + .build() + ); + } + + /** + * 获取默认桶中文件的文件流 + * @param objectName 文件名称 + * @return 文件流 + * @throws Exception 异常 + */ + public InputStream getFileStream(String objectName) throws Exception { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .build() + ); + } + + /** + * 获取二维码展示路径 + * @param objectName 文件名称 + */ + public String getShowUrl(String objectName){ + return baseUrl+objectName; + } + + +} \ No newline at end of file diff --git a/ebike-staff/src/main/resources/application-dev.yml b/ebike-staff/src/main/resources/application-dev.yml index b21f17f..9040a4e 100644 --- a/ebike-staff/src/main/resources/application-dev.yml +++ b/ebike-staff/src/main/resources/application-dev.yml @@ -92,3 +92,11 @@ logging: dateformat: yyyy-MM-dd HH:mm:ss.SSS # 包含毫秒 +minio: + endpoint: http://47.109.141.125:9000 # MinIO服务器地址 + access-key: ZIF4HF1LXH0RTB9MZ52O # 访问密钥 + secret-key: YuFoH+VJVzQbNLRqVMo39dM5pWXCEcMvrCrtgwB0 # 私有密钥 + bucket-name: static-objects + show-url: https://www.cdzhuojing.cn/file + + diff --git a/ebike-staff/src/test/java/com/cdzy/staff/EbikeStaffApplicationTests.java b/ebike-staff/src/test/java/com/cdzy/staff/EbikeStaffApplicationTests.java index 48a4d45..c53b27f 100644 --- a/ebike-staff/src/test/java/com/cdzy/staff/EbikeStaffApplicationTests.java +++ b/ebike-staff/src/test/java/com/cdzy/staff/EbikeStaffApplicationTests.java @@ -8,18 +8,18 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = com.cdzy.staff.EbikeStaffApplication.class) class EbikeStaffApplicationTests { - private static final String model_path ="D:/ebike_plus/ebike-operations"; - private static final String mapperPath="D:/ebike_plus/ebike-operations/resources/mapper"; - private static final String packageName ="com.cdzy.operations"; + private static final String model_path ="D:/ebike_plus/ebike-staff"; + private static final String mapperPath="D:/ebike_plus/ebike-staff/resources/mapper"; + private static final String packageName ="com.cdzy.staff"; private static final String[] tables= new String[]{ - "ebike_borrow_battery_record","ebike_record_codes" + "ebike_img" }; @Test void gen_mybatis_code() { //配置数据源 HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:postgresql://47.109.71.130/ebike_operations??currentSchema=public&stringtype=unspecified"); + dataSource.setJdbcUrl("jdbc:postgresql://47.109.71.130/ebike_staff??currentSchema=public&stringtype=unspecified"); dataSource.setUsername("root"); dataSource.setPassword("970529"); //生成全库的