diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..711404f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Maven build output +target/ + +# IDE project files +.idea/ diff --git a/src/main/java/com/bim/api/config/FileConvertMqConfig.java b/src/main/java/com/bim/api/config/FileConvertMqConfig.java new file mode 100644 index 0000000..9c7499c --- /dev/null +++ b/src/main/java/com/bim/api/config/FileConvertMqConfig.java @@ -0,0 +1,24 @@ +package com.bim.api.config; + +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FileConvertMqConfig { + private final FileConvertProperties properties; + + public FileConvertMqConfig(FileConvertProperties properties) { + this.properties = properties; + } + + @Bean + public Queue fileConvertModelQueue() { + return new Queue(properties.getModelQueueName(), true); + } + + @Bean + public Queue fileConvertCallbackQueue() { + return new Queue(properties.getCallbackQueueName(), true); + } +} diff --git a/src/main/java/com/bim/api/config/FileConvertProperties.java b/src/main/java/com/bim/api/config/FileConvertProperties.java new file mode 100644 index 0000000..18901a1 --- /dev/null +++ b/src/main/java/com/bim/api/config/FileConvertProperties.java @@ -0,0 +1,15 @@ +package com.bim.api.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "file-convert") +public class FileConvertProperties { + private String secretKey; + private String modelQueueName = "bim_engine_queue_file_convert"; + private String callbackQueueName = "system_file_convert_callback_queue"; + private String currentLanguage = "zh-CN"; +} diff --git a/src/main/java/com/bim/api/controller/ModelManagementController.java b/src/main/java/com/bim/api/controller/ModelManagementController.java new file mode 100644 index 0000000..c9b1001 --- /dev/null +++ b/src/main/java/com/bim/api/controller/ModelManagementController.java @@ -0,0 +1,89 @@ +package com.bim.api.controller; + +import com.bim.api.entity.ModelManagement; +import com.bim.api.service.ModelManagementService; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/model-management") +public class ModelManagementController { + private final ModelManagementService modelManagementService; + + public ModelManagementController(ModelManagementService modelManagementService) { + this.modelManagementService = modelManagementService; + } + + @GetMapping("/list") + public Map list(ModelManagement query) { + return ok(modelManagementService.findList(query)); + } + + @GetMapping("/{modelId}") + public Map getInfo(@PathVariable Long modelId) { + return ok(modelManagementService.getById(modelId)); + } + + @PostMapping("/upload") + public Map upload(@RequestParam("file") MultipartFile file, + @RequestParam("modelName") String modelName, + @RequestParam("modelType") String modelType) { + try { + return ok(modelManagementService.upload(file, modelName, modelType)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @PutMapping + public Map edit(@RequestBody ModelManagement modelManagement) { + try { + return ok(modelManagementService.updateById(modelManagement)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @PutMapping("/{modelId}/switch") + public Map switchCurrent(@PathVariable Long modelId) { + try { + return ok(modelManagementService.switchCurrent(modelId)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @DeleteMapping("/{modelIds}") + public Map remove(@PathVariable String modelIds) { + try { + return ok(modelManagementService.deleteWithFiles(modelManagementService.parseIds(modelIds))); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + private Map ok(Object data) { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", data); + return result; + } + + private Map error(String message) { + Map result = new HashMap<>(); + result.put("code", 500); + result.put("message", message); + return result; + } +} diff --git a/src/main/java/com/bim/api/controller/SysOssConfigController.java b/src/main/java/com/bim/api/controller/SysOssConfigController.java new file mode 100644 index 0000000..da4b016 --- /dev/null +++ b/src/main/java/com/bim/api/controller/SysOssConfigController.java @@ -0,0 +1,94 @@ +package com.bim.api.controller; + +import com.bim.api.entity.SysOssConfig; +import com.bim.api.service.SysOssConfigService; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/resource/oss/config") +public class SysOssConfigController { + private final SysOssConfigService ossConfigService; + + public SysOssConfigController(SysOssConfigService ossConfigService) { + this.ossConfigService = ossConfigService; + } + + @GetMapping("/list") + public Map list(SysOssConfig query) { + return ok(ossConfigService.findList(query)); + } + + @GetMapping("/{ossConfigId}") + public Map getInfo(@PathVariable Long ossConfigId) { + return ok(ossConfigService.getById(ossConfigId)); + } + + @PostMapping + public Map add(@RequestBody SysOssConfig config) { + try { + return ok(ossConfigService.save(config)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @PutMapping + public Map edit(@RequestBody SysOssConfig config) { + try { + return ok(ossConfigService.updateById(config)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @PutMapping("/changeStatus") + public Map changeStatus(@RequestBody SysOssConfig config) { + try { + return ok(ossConfigService.changeStatus(config.getOssConfigId())); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @DeleteMapping("/{ossConfigIds}") + public Map remove(@PathVariable String ossConfigIds) { + try { + return ok(ossConfigService.removeByIds(parseIds(ossConfigIds))); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + private List parseIds(String ids) { + return Arrays.stream(ids.split(",")) + .filter(id -> !id.isBlank()) + .map(Long::valueOf) + .toList(); + } + + private Map ok(Object data) { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", data); + return result; + } + + private Map error(String message) { + Map result = new HashMap<>(); + result.put("code", 500); + result.put("message", message); + return result; + } +} diff --git a/src/main/java/com/bim/api/controller/SysOssController.java b/src/main/java/com/bim/api/controller/SysOssController.java new file mode 100644 index 0000000..4e1c6fb --- /dev/null +++ b/src/main/java/com/bim/api/controller/SysOssController.java @@ -0,0 +1,92 @@ +package com.bim.api.controller; + +import com.bim.api.entity.SysOss; +import com.bim.api.service.SysOssService; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/resource/oss") +public class SysOssController { + private final SysOssService ossService; + + public SysOssController(SysOssService ossService) { + this.ossService = ossService; + } + + @GetMapping("/list") + public Map list(SysOss query) { + return ok(ossService.findList(query)); + } + + @GetMapping("/listByIds/{ossIds}") + public Map listByIds(@PathVariable String ossIds) { + List ids = parseIds(ossIds); + return ok(ossService.listByIds(ids)); + } + + @PostMapping({"/upload", "/ossUpload"}) + public Map upload(@RequestParam("file") MultipartFile file) { + try { + return ok(ossService.upload(file)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @GetMapping("/download/{ossId}") + public void download(@PathVariable Long ossId, HttpServletResponse response) throws Exception { + ossService.download(ossId, response); + } + + @GetMapping("/preview/{ossId}") + public Map preview(@PathVariable Long ossId) { + try { + return ok(ossService.preview(ossId)); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + @DeleteMapping("/{ossIds}") + public Map remove(@PathVariable String ossIds) { + try { + return ok(ossService.deleteWithFiles(parseIds(ossIds))); + } catch (Exception e) { + return error(e.getMessage()); + } + } + + private List parseIds(String ids) { + return Arrays.stream(ids.split(",")) + .filter(id -> !id.isBlank()) + .map(Long::valueOf) + .toList(); + } + + private Map ok(Object data) { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("data", data); + return result; + } + + private Map error(String message) { + Map result = new HashMap<>(); + result.put("code", 500); + result.put("message", message); + return result; + } +} diff --git a/src/main/java/com/bim/api/entity/ModelManagement.java b/src/main/java/com/bim/api/entity/ModelManagement.java new file mode 100644 index 0000000..bbb797b --- /dev/null +++ b/src/main/java/com/bim/api/entity/ModelManagement.java @@ -0,0 +1,32 @@ +package com.bim.api.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("model_management") +public class ModelManagement { + @TableId(value = "model_id", type = IdType.ASSIGN_ID) + private Long modelId; + private String modelName; + private String modelType; + private Long ossId; + private String fileName; + private String originalName; + private String fileSuffix; + private String fileUrl; + private Long fileSize; + private String convertedUrl; + private String convertStatus; + private String convertMessage; + private String keyCode; + private String sheetStr; + private String codeData; + private String status; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/bim/api/entity/SysOss.java b/src/main/java/com/bim/api/entity/SysOss.java new file mode 100644 index 0000000..d5fd8c7 --- /dev/null +++ b/src/main/java/com/bim/api/entity/SysOss.java @@ -0,0 +1,27 @@ +package com.bim.api.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("sys_oss") +public class SysOss { + @TableId(value = "oss_id", type = IdType.ASSIGN_ID) + private Long ossId; + private String tenantId; + private String fileName; + private String originalName; + private String fileSuffix; + private String url; + private String ext1; + private Long createDept; + private LocalDateTime createTime; + private Long createBy; + private LocalDateTime updateTime; + private Long updateBy; + private String service; +} diff --git a/src/main/java/com/bim/api/entity/SysOssConfig.java b/src/main/java/com/bim/api/entity/SysOssConfig.java new file mode 100644 index 0000000..4103215 --- /dev/null +++ b/src/main/java/com/bim/api/entity/SysOssConfig.java @@ -0,0 +1,34 @@ +package com.bim.api.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("sys_oss_config") +public class SysOssConfig { + @TableId(value = "oss_config_id", type = IdType.AUTO) + private Long ossConfigId; + private String tenantId; + private String configKey; + private String accessKey; + private String secretKey; + private String bucketName; + private String prefix; + private String endpoint; + private String domain; + private String isHttps; + private String region; + private String accessPolicy; + private String status; + private String ext1; + private Long createDept; + private Long createBy; + private LocalDateTime createTime; + private Long updateBy; + private LocalDateTime updateTime; + private String remark; +} diff --git a/src/main/java/com/bim/api/mapper/ModelManagementMapper.java b/src/main/java/com/bim/api/mapper/ModelManagementMapper.java new file mode 100644 index 0000000..e1e4d52 --- /dev/null +++ b/src/main/java/com/bim/api/mapper/ModelManagementMapper.java @@ -0,0 +1,9 @@ +package com.bim.api.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bim.api.entity.ModelManagement; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ModelManagementMapper extends BaseMapper { +} diff --git a/src/main/java/com/bim/api/mapper/SysOssConfigMapper.java b/src/main/java/com/bim/api/mapper/SysOssConfigMapper.java new file mode 100644 index 0000000..32f3861 --- /dev/null +++ b/src/main/java/com/bim/api/mapper/SysOssConfigMapper.java @@ -0,0 +1,9 @@ +package com.bim.api.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bim.api.entity.SysOssConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysOssConfigMapper extends BaseMapper { +} diff --git a/src/main/java/com/bim/api/mapper/SysOssMapper.java b/src/main/java/com/bim/api/mapper/SysOssMapper.java new file mode 100644 index 0000000..4f7ae4f --- /dev/null +++ b/src/main/java/com/bim/api/mapper/SysOssMapper.java @@ -0,0 +1,9 @@ +package com.bim.api.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bim.api.entity.SysOss; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysOssMapper extends BaseMapper { +} diff --git a/src/main/java/com/bim/api/oss/OssClient.java b/src/main/java/com/bim/api/oss/OssClient.java new file mode 100644 index 0000000..9aa848b --- /dev/null +++ b/src/main/java/com/bim/api/oss/OssClient.java @@ -0,0 +1,187 @@ +package com.bim.api.oss; + +import com.bim.api.entity.SysOssConfig; +import org.springframework.util.StringUtils; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URLConnection; +import java.time.Duration; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +public class OssClient implements AutoCloseable { + private static final String[] CLOUD_SERVICE = {"aliyun", "qcloud", "qiniu", "obs"}; + + private final SysOssConfig properties; + private final S3Client client; + private final S3Presigner presigner; + + public OssClient(SysOssConfig properties) { + this.properties = properties; + try { + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); + S3Configuration config = S3Configuration.builder() + .chunkedEncodingEnabled(false) + .pathStyleAccessEnabled(!containsCloudService(properties.getEndpoint())) + .build(); + + this.client = S3Client.builder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getEndpoint())) + .region(region()) + .serviceConfiguration(config) + .build(); + this.presigner = S3Presigner.builder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getDomain())) + .region(region()) + .serviceConfiguration(config) + .build(); + } catch (Exception e) { + throw new OssException("OSS配置错误: " + e.getMessage()); + } + } + + public UploadResult upload(InputStream inputStream, long size, String suffix, String contentType) { + String key = nextObjectKey(suffix); + try { + PutObjectResponse response = client.putObject(PutObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(key) + .contentType(StringUtils.hasText(contentType) ? contentType : mimeType(suffix)) + .build(), RequestBody.fromInputStream(inputStream, size)); + return UploadResult.builder() + .url(getUrl() + "/" + key) + .filename(key) + .eTag(response.eTag()) + .build(); + } catch (Exception e) { + throw new OssException("上传文件失败: " + e.getMessage()); + } + } + + public void download(String key, OutputStream outputStream) { + try (ResponseInputStream inputStream = client.getObject(GetObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(removeBaseUrl(key)) + .build())) { + inputStream.transferTo(outputStream); + } catch (IOException e) { + throw new OssException("写出文件失败: " + e.getMessage()); + } catch (Exception e) { + throw new OssException("下载文件失败: " + e.getMessage()); + } + } + + public void delete(String key) { + try { + client.deleteObject(DeleteObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(removeBaseUrl(key)) + .build()); + } catch (Exception e) { + throw new OssException("删除文件失败: " + e.getMessage()); + } + } + + public String getPrivateUrl(String key, Duration expiredTime) { + return presigner.presignGetObject(builder -> builder + .signatureDuration(expiredTime) + .getObjectRequest(request -> request.bucket(properties.getBucketName()).key(removeBaseUrl(key)).build()) + .build()).url().toString(); + } + + public boolean isPrivate() { + return "0".equals(properties.getAccessPolicy()); + } + + public String getConfigKey() { + return properties.getConfigKey(); + } + + @Override + public void close() { + client.close(); + presigner.close(); + } + + private String nextObjectKey(String suffix) { + String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String fileName = UUID.randomUUID().toString().replace("-", "") + (suffix == null ? "" : suffix); + return StringUtils.hasText(properties.getPrefix()) + ? properties.getPrefix() + "/" + datePath + "/" + fileName + : datePath + "/" + fileName; + } + + private String getEndpoint() { + return protocol() + properties.getEndpoint(); + } + + private String getDomain() { + if (StringUtils.hasText(properties.getDomain())) { + String domain = properties.getDomain(); + return domain.startsWith("http://") || domain.startsWith("https://") ? domain : protocol() + domain; + } + return getEndpoint(); + } + + private String getUrl() { + String endpoint = properties.getEndpoint(); + String domain = properties.getDomain(); + if (containsCloudService(endpoint)) { + return protocol() + (StringUtils.hasText(domain) ? domain : properties.getBucketName() + "." + endpoint); + } + if (StringUtils.hasText(domain)) { + String base = domain.startsWith("http://") || domain.startsWith("https://") ? domain : protocol() + domain; + return base + "/" + properties.getBucketName(); + } + return protocol() + endpoint + "/" + properties.getBucketName(); + } + + private String removeBaseUrl(String path) { + return path == null ? null : path.replace(getUrl() + "/", ""); + } + + private Region region() { + return StringUtils.hasText(properties.getRegion()) ? Region.of(properties.getRegion()) : Region.US_EAST_1; + } + + private String protocol() { + return "Y".equalsIgnoreCase(properties.getIsHttps()) ? "https://" : "http://"; + } + + private boolean containsCloudService(String endpoint) { + if (endpoint == null) { + return false; + } + for (String cloudService : CLOUD_SERVICE) { + if (endpoint.contains(cloudService)) { + return true; + } + } + return false; + } + + private String mimeType(String suffix) { + String mimeType = URLConnection.guessContentTypeFromName("file" + suffix); + return StringUtils.hasText(mimeType) ? mimeType : "application/octet-stream"; + } +} diff --git a/src/main/java/com/bim/api/oss/OssException.java b/src/main/java/com/bim/api/oss/OssException.java new file mode 100644 index 0000000..3c144ba --- /dev/null +++ b/src/main/java/com/bim/api/oss/OssException.java @@ -0,0 +1,7 @@ +package com.bim.api.oss; + +public class OssException extends RuntimeException { + public OssException(String message) { + super(message); + } +} diff --git a/src/main/java/com/bim/api/oss/SysOssUploadVo.java b/src/main/java/com/bim/api/oss/SysOssUploadVo.java new file mode 100644 index 0000000..75debf3 --- /dev/null +++ b/src/main/java/com/bim/api/oss/SysOssUploadVo.java @@ -0,0 +1,11 @@ +package com.bim.api.oss; + +import lombok.Data; + +@Data +public class SysOssUploadVo { + private String url; + private String fileName; + private Long ossId; + private String fileSuffix; +} diff --git a/src/main/java/com/bim/api/oss/UploadResult.java b/src/main/java/com/bim/api/oss/UploadResult.java new file mode 100644 index 0000000..bd124ad --- /dev/null +++ b/src/main/java/com/bim/api/oss/UploadResult.java @@ -0,0 +1,12 @@ +package com.bim.api.oss; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UploadResult { + private String url; + private String filename; + private String eTag; +} diff --git a/src/main/java/com/bim/api/service/ModelManagementService.java b/src/main/java/com/bim/api/service/ModelManagementService.java new file mode 100644 index 0000000..c1bd5b2 --- /dev/null +++ b/src/main/java/com/bim/api/service/ModelManagementService.java @@ -0,0 +1,144 @@ +package com.bim.api.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bim.api.entity.ModelManagement; +import com.bim.api.entity.SysOss; +import com.bim.api.mapper.ModelManagementMapper; +import com.bim.api.oss.OssException; +import com.bim.api.service.convert.FileConvertMessageService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +@Service +public class ModelManagementService extends ServiceImpl { + private static final String STATUS_CURRENT = "0"; + private static final String STATUS_AVAILABLE = "1"; + private static final Set SUPPORT_SUFFIXES = Set.of(".ifc", ".rvt", ".nwd", ".fbx"); + + private final SysOssService ossService; + private final FileConvertMessageService fileConvertMessageService; + + public ModelManagementService(SysOssService ossService, FileConvertMessageService fileConvertMessageService) { + this.ossService = ossService; + this.fileConvertMessageService = fileConvertMessageService; + } + + public List findList(ModelManagement query) { + return list(new LambdaQueryWrapper() + .like(StringUtils.hasText(query.getModelName()), ModelManagement::getModelName, query.getModelName()) + .eq(StringUtils.hasText(query.getModelType()), ModelManagement::getModelType, query.getModelType()) + .eq(StringUtils.hasText(query.getStatus()), ModelManagement::getStatus, query.getStatus()) + .orderByAsc(ModelManagement::getStatus) + .orderByDesc(ModelManagement::getCreateTime)); + } + + @Transactional(rollbackFor = Exception.class) + public ModelManagement upload(MultipartFile file, String modelName, String modelType) { + validateUpload(file, modelName, modelType); + SysOss oss = ossService.upload(file); + ModelManagement model = new ModelManagement(); + model.setModelName(modelName.trim()); + model.setModelType(modelType.trim()); + model.setOssId(oss.getOssId()); + model.setFileName(oss.getFileName()); + model.setOriginalName(oss.getOriginalName()); + model.setFileSuffix(oss.getFileSuffix()); + model.setFileUrl(oss.getUrl()); + model.setFileSize(file.getSize()); + model.setConvertStatus("PENDING"); + model.setConvertMessage("转换任务已发送"); + model.setStatus(STATUS_AVAILABLE); + model.setCreateTime(LocalDateTime.now()); + save(model); + fileConvertMessageService.sendModelConvertTask(model); + return model; + } + + @Override + public boolean updateById(ModelManagement entity) { + ModelManagement old = getById(entity.getModelId()); + if (old == null) { + throw new OssException("模型不存在: " + entity.getModelId()); + } + if (!StringUtils.hasText(entity.getModelName())) { + entity.setModelName(old.getModelName()); + } + if (!StringUtils.hasText(entity.getModelType())) { + entity.setModelType(old.getModelType()); + } + entity.setOssId(old.getOssId()); + entity.setFileName(old.getFileName()); + entity.setOriginalName(old.getOriginalName()); + entity.setFileSuffix(old.getFileSuffix()); + entity.setFileUrl(old.getFileUrl()); + entity.setFileSize(old.getFileSize()); + entity.setConvertedUrl(old.getConvertedUrl()); + entity.setConvertStatus(old.getConvertStatus()); + entity.setConvertMessage(old.getConvertMessage()); + entity.setKeyCode(old.getKeyCode()); + entity.setSheetStr(old.getSheetStr()); + entity.setCodeData(old.getCodeData()); + entity.setStatus(old.getStatus()); + entity.setUpdateTime(LocalDateTime.now()); + return super.updateById(entity); + } + + @Transactional(rollbackFor = Exception.class) + public boolean switchCurrent(Long modelId) { + ModelManagement model = getById(modelId); + if (model == null) { + throw new OssException("模型不存在: " + modelId); + } + lambdaUpdate().set(ModelManagement::getStatus, STATUS_AVAILABLE).update(); + model.setStatus(STATUS_CURRENT); + model.setUpdateTime(LocalDateTime.now()); + return super.updateById(model); + } + + @Transactional(rollbackFor = Exception.class) + public boolean deleteWithFiles(Collection modelIds) { + List models = listByIds(modelIds); + List ossIds = models.stream() + .map(ModelManagement::getOssId) + .filter(id -> id != null) + .toList(); + if (!ossIds.isEmpty()) { + ossService.deleteWithFiles(ossIds); + } + return removeByIds(modelIds); + } + + public List parseIds(String ids) { + return Arrays.stream(ids.split(",")) + .filter(id -> !id.isBlank()) + .map(Long::valueOf) + .toList(); + } + + private void validateUpload(MultipartFile file, String modelName, String modelType) { + if (file == null || file.isEmpty()) { + throw new OssException("上传模型文件不能为空"); + } + if (!StringUtils.hasText(modelName)) { + throw new OssException("模型名称不能为空"); + } + if (!StringUtils.hasText(modelType)) { + throw new OssException("模型类型不能为空"); + } + String originalName = StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : ""; + String suffix = originalName.contains(".") ? originalName.substring(originalName.lastIndexOf('.')).toLowerCase(Locale.ROOT) : ""; + if (!SUPPORT_SUFFIXES.contains(suffix)) { + throw new OssException("仅支持 IFC / RVT / NWD / FBX 模型文件"); + } + } +} diff --git a/src/main/java/com/bim/api/service/SysOssConfigService.java b/src/main/java/com/bim/api/service/SysOssConfigService.java new file mode 100644 index 0000000..4cfdded --- /dev/null +++ b/src/main/java/com/bim/api/service/SysOssConfigService.java @@ -0,0 +1,103 @@ +package com.bim.api.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bim.api.entity.SysOssConfig; +import com.bim.api.mapper.SysOssConfigMapper; +import com.bim.api.oss.OssException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class SysOssConfigService extends ServiceImpl { + + public List findList(SysOssConfig query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(StringUtils.hasText(query.getConfigKey()), SysOssConfig::getConfigKey, query.getConfigKey()) + .like(StringUtils.hasText(query.getBucketName()), SysOssConfig::getBucketName, query.getBucketName()) + .eq(StringUtils.hasText(query.getStatus()), SysOssConfig::getStatus, query.getStatus()) + .orderByAsc(SysOssConfig::getOssConfigId); + return list(wrapper); + } + + public SysOssConfig getDefaultConfig() { + SysOssConfig config = getOne(new LambdaQueryWrapper() + .eq(SysOssConfig::getStatus, "0") + .last("limit 1")); + if (config == null) { + throw new OssException("未找到默认OSS配置,请先在sys_oss_config中设置status=0的配置"); + } + return config; + } + + public SysOssConfig getByConfigKey(String configKey) { + SysOssConfig config = getOne(new LambdaQueryWrapper() + .eq(SysOssConfig::getConfigKey, configKey) + .last("limit 1")); + if (config == null) { + throw new OssException("OSS配置不存在: " + configKey); + } + return config; + } + + @Override + public boolean save(SysOssConfig entity) { + validConfigKey(entity); + fillDefaults(entity); + entity.setCreateTime(LocalDateTime.now()); + return super.save(entity); + } + + @Override + public boolean updateById(SysOssConfig entity) { + validConfigKey(entity); + fillDefaults(entity); + entity.setUpdateTime(LocalDateTime.now()); + return super.updateById(entity); + } + + @Transactional(rollbackFor = Exception.class) + public boolean changeStatus(Long ossConfigId) { + SysOssConfig config = getById(ossConfigId); + if (config == null) { + throw new OssException("OSS配置不存在: " + ossConfigId); + } + lambdaUpdate().set(SysOssConfig::getStatus, "1").update(); + config.setStatus("0"); + config.setUpdateTime(LocalDateTime.now()); + return super.updateById(config); + } + + private void validConfigKey(SysOssConfig entity) { + if (!StringUtils.hasText(entity.getConfigKey())) { + throw new OssException("配置key不能为空"); + } + SysOssConfig exists = getOne(new LambdaQueryWrapper() + .select(SysOssConfig::getOssConfigId, SysOssConfig::getConfigKey) + .eq(SysOssConfig::getConfigKey, entity.getConfigKey()) + .last("limit 1")); + Long id = entity.getOssConfigId(); + if (exists != null && (id == null || !exists.getOssConfigId().equals(id))) { + throw new OssException("配置key已存在: " + entity.getConfigKey()); + } + } + + private void fillDefaults(SysOssConfig entity) { + if (!StringUtils.hasText(entity.getTenantId())) { + entity.setTenantId("000000"); + } + if (!StringUtils.hasText(entity.getIsHttps())) { + entity.setIsHttps("N"); + } + if (!StringUtils.hasText(entity.getAccessPolicy())) { + entity.setAccessPolicy("1"); + } + if (!StringUtils.hasText(entity.getStatus())) { + entity.setStatus("1"); + } + } +} diff --git a/src/main/java/com/bim/api/service/SysOssService.java b/src/main/java/com/bim/api/service/SysOssService.java new file mode 100644 index 0000000..607b8a6 --- /dev/null +++ b/src/main/java/com/bim/api/service/SysOssService.java @@ -0,0 +1,128 @@ +package com.bim.api.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bim.api.entity.SysOss; +import com.bim.api.entity.SysOssConfig; +import com.bim.api.mapper.SysOssMapper; +import com.bim.api.oss.OssClient; +import com.bim.api.oss.OssException; +import com.bim.api.oss.SysOssUploadVo; +import com.bim.api.oss.UploadResult; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Service +public class SysOssService extends ServiceImpl { + private final SysOssConfigService ossConfigService; + + public SysOssService(SysOssConfigService ossConfigService) { + this.ossConfigService = ossConfigService; + } + + public List findList(SysOss query) { + List list = list(new LambdaQueryWrapper() + .like(StringUtils.hasText(query.getFileName()), SysOss::getFileName, query.getFileName()) + .like(StringUtils.hasText(query.getOriginalName()), SysOss::getOriginalName, query.getOriginalName()) + .eq(StringUtils.hasText(query.getFileSuffix()), SysOss::getFileSuffix, query.getFileSuffix()) + .eq(StringUtils.hasText(query.getService()), SysOss::getService, query.getService()) + .orderByDesc(SysOss::getOssId)); + list.forEach(this::matchingUrl); + return list; + } + + @Transactional(rollbackFor = Exception.class) + public SysOss upload(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new OssException("上传文件不能为空"); + } + String originalName = StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : "file"; + String suffix = suffix(originalName); + SysOssConfig config = ossConfigService.getDefaultConfig(); + try (OssClient client = new OssClient(config)) { + UploadResult uploadResult = client.upload(file.getInputStream(), file.getSize(), suffix, file.getContentType()); + SysOss oss = new SysOss(); + oss.setTenantId(StringUtils.hasText(config.getTenantId()) ? config.getTenantId() : "000000"); + oss.setUrl(uploadResult.getUrl()); + oss.setFileName(uploadResult.getFilename()); + oss.setOriginalName(originalName); + oss.setFileSuffix(suffix); + oss.setService(client.getConfigKey()); + oss.setCreateTime(LocalDateTime.now()); + save(oss); + return matchingUrl(oss); + } catch (IOException e) { + throw new OssException("读取上传文件失败: " + e.getMessage()); + } + } + + public void download(Long ossId, HttpServletResponse response) throws IOException { + SysOss oss = getById(ossId); + if (oss == null) { + throw new OssException("文件数据不存在: " + ossId); + } + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); + String fileName = URLEncoder.encode(oss.getOriginalName(), StandardCharsets.UTF_8).replace("+", "%20"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); + try (OssClient client = new OssClient(ossConfigService.getByConfigKey(oss.getService()))) { + client.download(oss.getFileName(), response.getOutputStream()); + } + } + + public SysOssUploadVo preview(Long ossId) { + SysOss oss = getById(ossId); + if (oss == null) { + throw new OssException("文件数据不存在: " + ossId); + } + SysOssConfig config = ossConfigService.getByConfigKey(oss.getService()); + try (OssClient client = new OssClient(config)) { + SysOssUploadVo vo = new SysOssUploadVo(); + vo.setOssId(oss.getOssId()); + vo.setFileName(oss.getOriginalName()); + vo.setFileSuffix(oss.getFileSuffix()); + vo.setUrl(client.isPrivate() ? client.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(120)) : oss.getUrl()); + return vo; + } + } + + @Transactional(rollbackFor = Exception.class) + public boolean deleteWithFiles(Collection ossIds) { + List list = listByIds(ossIds); + for (SysOss oss : list) { + try (OssClient client = new OssClient(ossConfigService.getByConfigKey(oss.getService()))) { + client.delete(oss.getFileName()); + } + } + return removeByIds(ossIds); + } + + private SysOss matchingUrl(SysOss oss) { + try { + try (OssClient client = new OssClient(ossConfigService.getByConfigKey(oss.getService()))) { + if (client.isPrivate()) { + oss.setUrl(client.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(120))); + } + } + } catch (Exception ignored) { + // 配置不可用时仍返回数据库中保存的原始URL,便于排查配置问题。 + } + return oss; + } + + private String suffix(String fileName) { + int index = fileName.lastIndexOf('.'); + return index >= 0 ? fileName.substring(index) : ""; + } +} diff --git a/src/main/java/com/bim/api/service/convert/FileConvertCallbackListener.java b/src/main/java/com/bim/api/service/convert/FileConvertCallbackListener.java new file mode 100644 index 0000000..ce3a803 --- /dev/null +++ b/src/main/java/com/bim/api/service/convert/FileConvertCallbackListener.java @@ -0,0 +1,152 @@ +package com.bim.api.service.convert; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.bim.api.config.FileConvertProperties; +import com.bim.api.entity.ModelManagement; +import com.bim.api.service.ModelManagementService; +import com.bim.api.util.SignUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +@Component +public class FileConvertCallbackListener { + private final ObjectMapper objectMapper; + private final FileConvertProperties properties; + private final ModelManagementService modelManagementService; + + public FileConvertCallbackListener(ObjectMapper objectMapper, + FileConvertProperties properties, + ModelManagementService modelManagementService) { + this.objectMapper = objectMapper; + this.properties = properties; + this.modelManagementService = modelManagementService; + } + + @RabbitListener(queues = "${file-convert.callback-queue-name:system_file_convert_callback_queue}") + public void handleCallback(String rawMessage) throws JsonProcessingException { + Map message = objectMapper.readValue(rawMessage, new TypeReference<>() { + }); + String sign = safeString(message.get("sign")); + boolean signValid = verifyCallbackSign(message, rawMessage, sign); + + Long modelId = parseModelId(message.get("extraData")); + if (modelId == null) { + throw new IllegalArgumentException("转换回调缺少模型ID"); + } + + String convertedUrl = firstText(message.get("convertedUrl"), message.get("converterUrl")); + String status = normalizeStatus(safeString(message.get("status"))); + + modelManagementService.update(new LambdaUpdateWrapper() + .eq(ModelManagement::getModelId, modelId) + .set(ModelManagement::getConvertedUrl, convertedUrl) + .set(ModelManagement::getConvertStatus, status) + .set(ModelManagement::getConvertMessage, safeString(message.get("message"))) + .set(ModelManagement::getKeyCode, safeString(message.get("keyCode"))) + .set(ModelManagement::getSheetStr, safeString(message.get("sheetStr"))) + .set(ModelManagement::getCodeData, buildCodeData(message, signValid)) + .setSql("update_time = now()")); + } + + private boolean verifyCallbackSign(Map message, String rawMessage, String sign) { + if (!StringUtils.hasText(properties.getSecretKey())) { + return true; + } + if (!StringUtils.hasText(sign)) { + return false; + } + if (SignUtil.verifySign(message, properties.getSecretKey(), sign) + || SignUtil.verifySignWithRawStringFields(message, rawMessage, Set.of("extraData"), properties.getSecretKey(), sign)) { + return true; + } + return verifyWithExtraDataAsObject(message, sign) || verifyWithExtraDataAsString(message, sign); + } + + private boolean verifyWithExtraDataAsObject(Map message, String sign) { + Object extraData = message.get("extraData"); + if (!(extraData instanceof String extraDataJson) || !StringUtils.hasText(extraDataJson)) { + return false; + } + try { + Map candidate = new LinkedHashMap<>(message); + candidate.put("extraData", objectMapper.readValue(extraDataJson, new TypeReference>() { + })); + return SignUtil.verifySign(candidate, properties.getSecretKey(), sign); + } catch (JsonProcessingException e) { + return false; + } + } + + private boolean verifyWithExtraDataAsString(Map message, String sign) { + Object extraData = message.get("extraData"); + if (!(extraData instanceof Map)) { + return false; + } + try { + Map candidate = new LinkedHashMap<>(message); + candidate.put("extraData", objectMapper.writeValueAsString(extraData)); + return SignUtil.verifySign(candidate, properties.getSecretKey(), sign); + } catch (JsonProcessingException e) { + return false; + } + } + + private Long parseModelId(Object extraDataObj) { + if (extraDataObj instanceof String extraDataJson) { + try { + return parseModelId(objectMapper.readValue(extraDataJson, new TypeReference>() { + })); + } catch (JsonProcessingException e) { + return null; + } + } + if (!(extraDataObj instanceof Map extraData)) { + return null; + } + Object modelId = extraData.get("modelId"); + if (modelId == null) { + modelId = extraData.get("businessId"); + } + if (modelId == null) { + return null; + } + String value = String.valueOf(modelId).replaceFirst("^MODEL_", ""); + return value.matches("\\d+") ? Long.valueOf(value) : null; + } + + private String buildCodeData(Map message, boolean signValid) throws JsonProcessingException { + Map codeData = new LinkedHashMap<>(); + codeData.put("convertedUrl", firstText(message.get("convertedUrl"), message.get("converterUrl"))); + codeData.put("status", normalizeStatus(safeString(message.get("status")))); + codeData.put("message", safeString(message.get("message"))); + codeData.put("keyCode", safeString(message.get("keyCode"))); + codeData.put("sheetStr", safeString(message.get("sheetStr"))); + codeData.put("extraData", message.get("extraData")); + codeData.put("signValid", signValid); + return objectMapper.writeValueAsString(codeData); + } + + private String normalizeStatus(String status) { + if (!StringUtils.hasText(status)) { + return "FAILED"; + } + return "success".equalsIgnoreCase(status) ? "SUCCESS" : status.toUpperCase(); + } + + private String firstText(Object first, Object second) { + String firstValue = safeString(first); + return StringUtils.hasText(firstValue) ? firstValue : safeString(second); + } + + private String safeString(Object value) { + return value == null ? null : String.valueOf(value); + } +} diff --git a/src/main/java/com/bim/api/service/convert/FileConvertMessageService.java b/src/main/java/com/bim/api/service/convert/FileConvertMessageService.java new file mode 100644 index 0000000..60f369b --- /dev/null +++ b/src/main/java/com/bim/api/service/convert/FileConvertMessageService.java @@ -0,0 +1,70 @@ +package com.bim.api.service.convert; + +import com.bim.api.config.FileConvertProperties; +import com.bim.api.entity.ModelManagement; +import com.bim.api.util.SignUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class FileConvertMessageService { + private static final Logger log = LoggerFactory.getLogger(FileConvertMessageService.class); + + private final RabbitTemplate rabbitTemplate; + private final FileConvertProperties properties; + private final ObjectMapper objectMapper; + + public FileConvertMessageService(RabbitTemplate rabbitTemplate, FileConvertProperties properties, ObjectMapper objectMapper) { + this.rabbitTemplate = rabbitTemplate; + this.properties = properties; + this.objectMapper = objectMapper; + } + + public void sendModelConvertTask(ModelManagement model) { + Map message = new LinkedHashMap<>(); + message.put("sign", ""); + message.put("callbackQueueName", properties.getCallbackQueueName()); + message.put("sourceUrl", model.getFileUrl()); + message.put("currentLanguage", properties.getCurrentLanguage()); + message.put("timestamp", String.valueOf(System.currentTimeMillis())); + message.put("extraData", buildExtraDataJson(model)); + message.put("fileName", model.getOriginalName()); + message.put("fileType", normalizeFileType(model.getFileSuffix())); + message.put("fileSize", model.getFileSize()); + message.put("sign", SignUtil.generateSign(message, properties.getSecretKey())); + + try { + String payload = objectMapper.writeValueAsString(message); + log.info("发送模型转换任务: {}", payload); + rabbitTemplate.convertAndSend(properties.getModelQueueName(), payload); + } catch (JsonProcessingException e) { + throw new IllegalStateException("构建模型转换消息失败", e); + } + } + + private String buildExtraDataJson(ModelManagement model) { + Map extraData = new LinkedHashMap<>(); + extraData.put("taskId", "MODEL_" + model.getModelId()); + extraData.put("businessId", String.valueOf(model.getModelId())); + extraData.put("modelId", model.getModelId()); + try { + return objectMapper.writeValueAsString(extraData); + } catch (JsonProcessingException e) { + throw new IllegalStateException("构建模型转换扩展数据失败", e); + } + } + + private String normalizeFileType(String fileSuffix) { + if (fileSuffix == null || fileSuffix.isBlank()) { + return ""; + } + return fileSuffix.startsWith(".") ? fileSuffix.substring(1).toLowerCase() : fileSuffix.toLowerCase(); + } +} diff --git a/src/main/resources/db/model_management.sql b/src/main/resources/db/model_management.sql new file mode 100644 index 0000000..8163d1c --- /dev/null +++ b/src/main/resources/db/model_management.sql @@ -0,0 +1,31 @@ +-- 模型管理表 +create table if not exists model_management ( + model_id bigint(20) not null auto_increment comment '模型主键', + model_name varchar(100) not null default '' comment '模型名称', + model_type varchar(50) not null default '' comment '模型类型', + oss_id bigint(20) default null comment 'OSS文件ID', + file_name varchar(255) not null default '' comment '存储文件名', + original_name varchar(255) not null default '' comment '原始文件名', + file_suffix varchar(20) not null default '' comment '文件后缀名', + file_url varchar(500) default null comment '文件访问地址', + file_size bigint(20) default null comment '文件大小', + converted_url varchar(500) default null comment '转换后文件地址', + convert_status varchar(20) not null default 'PENDING' comment '转换状态(PENDING=待转换,SUCCESS=成功,FAILED=失败)', + convert_message varchar(500) default null comment '转换状态说明', + key_code varchar(255) default null comment '加密code', + sheet_str longtext default null comment '蓝图数据', + status char(1) not null default '1' comment '状态(0=当前,1=可用)', + create_time datetime default null comment '上传时间', + update_time datetime default null comment '更新时间', + code_data longtext default null comment '构件数据', + primary key (model_id), + key idx_model_management_type (model_type), + key idx_model_management_status (status), + key idx_model_management_convert_status (convert_status), + key idx_model_management_oss_id (oss_id) +) engine=innodb default charset=utf8mb4 comment='模型管理'; + +-- 绑定关系表模型维度,旧库需要确认 part_code_relation 已包含 model_id。 +-- 如未包含,请手工执行: +-- alter table part_code_relation add column model_id bigint(20) default null comment '模型ID'; +-- create index idx_part_code_relation_model_id on part_code_relation (model_id);