对接mq和模型上传管理和绑定构件,构建树形

This commit is contained in:
cjh
2026-06-10 17:03:19 +08:00
parent ee15a3415c
commit 89ad5a1689
22 changed files with 1284 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Maven build output
target/
# IDE project files
.idea/

View File

@@ -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);
}
}

View File

@@ -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";
}

View File

@@ -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<String, Object> list(ModelManagement query) {
return ok(modelManagementService.findList(query));
}
@GetMapping("/{modelId}")
public Map<String, Object> getInfo(@PathVariable Long modelId) {
return ok(modelManagementService.getById(modelId));
}
@PostMapping("/upload")
public Map<String, Object> 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<String, Object> edit(@RequestBody ModelManagement modelManagement) {
try {
return ok(modelManagementService.updateById(modelManagement));
} catch (Exception e) {
return error(e.getMessage());
}
}
@PutMapping("/{modelId}/switch")
public Map<String, Object> switchCurrent(@PathVariable Long modelId) {
try {
return ok(modelManagementService.switchCurrent(modelId));
} catch (Exception e) {
return error(e.getMessage());
}
}
@DeleteMapping("/{modelIds}")
public Map<String, Object> remove(@PathVariable String modelIds) {
try {
return ok(modelManagementService.deleteWithFiles(modelManagementService.parseIds(modelIds)));
} catch (Exception e) {
return error(e.getMessage());
}
}
private Map<String, Object> ok(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", data);
return result;
}
private Map<String, Object> error(String message) {
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", message);
return result;
}
}

View File

@@ -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<String, Object> list(SysOssConfig query) {
return ok(ossConfigService.findList(query));
}
@GetMapping("/{ossConfigId}")
public Map<String, Object> getInfo(@PathVariable Long ossConfigId) {
return ok(ossConfigService.getById(ossConfigId));
}
@PostMapping
public Map<String, Object> add(@RequestBody SysOssConfig config) {
try {
return ok(ossConfigService.save(config));
} catch (Exception e) {
return error(e.getMessage());
}
}
@PutMapping
public Map<String, Object> edit(@RequestBody SysOssConfig config) {
try {
return ok(ossConfigService.updateById(config));
} catch (Exception e) {
return error(e.getMessage());
}
}
@PutMapping("/changeStatus")
public Map<String, Object> changeStatus(@RequestBody SysOssConfig config) {
try {
return ok(ossConfigService.changeStatus(config.getOssConfigId()));
} catch (Exception e) {
return error(e.getMessage());
}
}
@DeleteMapping("/{ossConfigIds}")
public Map<String, Object> remove(@PathVariable String ossConfigIds) {
try {
return ok(ossConfigService.removeByIds(parseIds(ossConfigIds)));
} catch (Exception e) {
return error(e.getMessage());
}
}
private List<Long> parseIds(String ids) {
return Arrays.stream(ids.split(","))
.filter(id -> !id.isBlank())
.map(Long::valueOf)
.toList();
}
private Map<String, Object> ok(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", data);
return result;
}
private Map<String, Object> error(String message) {
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", message);
return result;
}
}

View File

@@ -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<String, Object> list(SysOss query) {
return ok(ossService.findList(query));
}
@GetMapping("/listByIds/{ossIds}")
public Map<String, Object> listByIds(@PathVariable String ossIds) {
List<Long> ids = parseIds(ossIds);
return ok(ossService.listByIds(ids));
}
@PostMapping({"/upload", "/ossUpload"})
public Map<String, Object> 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<String, Object> preview(@PathVariable Long ossId) {
try {
return ok(ossService.preview(ossId));
} catch (Exception e) {
return error(e.getMessage());
}
}
@DeleteMapping("/{ossIds}")
public Map<String, Object> remove(@PathVariable String ossIds) {
try {
return ok(ossService.deleteWithFiles(parseIds(ossIds)));
} catch (Exception e) {
return error(e.getMessage());
}
}
private List<Long> parseIds(String ids) {
return Arrays.stream(ids.split(","))
.filter(id -> !id.isBlank())
.map(Long::valueOf)
.toList();
}
private Map<String, Object> ok(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", data);
return result;
}
private Map<String, Object> error(String message) {
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", message);
return result;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ModelManagement> {
}

View File

@@ -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<SysOssConfig> {
}

View File

@@ -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<SysOss> {
}

View File

@@ -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<GetObjectResponse> 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";
}
}

View File

@@ -0,0 +1,7 @@
package com.bim.api.oss;
public class OssException extends RuntimeException {
public OssException(String message) {
super(message);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ModelManagementMapper, ModelManagement> {
private static final String STATUS_CURRENT = "0";
private static final String STATUS_AVAILABLE = "1";
private static final Set<String> 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<ModelManagement> findList(ModelManagement query) {
return list(new LambdaQueryWrapper<ModelManagement>()
.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<Long> modelIds) {
List<ModelManagement> models = listByIds(modelIds);
List<Long> ossIds = models.stream()
.map(ModelManagement::getOssId)
.filter(id -> id != null)
.toList();
if (!ossIds.isEmpty()) {
ossService.deleteWithFiles(ossIds);
}
return removeByIds(modelIds);
}
public List<Long> 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 模型文件");
}
}
}

View File

@@ -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<SysOssConfigMapper, SysOssConfig> {
public List<SysOssConfig> findList(SysOssConfig query) {
LambdaQueryWrapper<SysOssConfig> wrapper = new LambdaQueryWrapper<SysOssConfig>()
.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<SysOssConfig>()
.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<SysOssConfig>()
.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<SysOssConfig>()
.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");
}
}
}

View File

@@ -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<SysOssMapper, SysOss> {
private final SysOssConfigService ossConfigService;
public SysOssService(SysOssConfigService ossConfigService) {
this.ossConfigService = ossConfigService;
}
public List<SysOss> findList(SysOss query) {
List<SysOss> list = list(new LambdaQueryWrapper<SysOss>()
.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<Long> ossIds) {
List<SysOss> 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) : "";
}
}

View File

@@ -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<String, Object> 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<ModelManagement>()
.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<String, Object> 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<String, Object> message, String sign) {
Object extraData = message.get("extraData");
if (!(extraData instanceof String extraDataJson) || !StringUtils.hasText(extraDataJson)) {
return false;
}
try {
Map<String, Object> candidate = new LinkedHashMap<>(message);
candidate.put("extraData", objectMapper.readValue(extraDataJson, new TypeReference<Map<String, Object>>() {
}));
return SignUtil.verifySign(candidate, properties.getSecretKey(), sign);
} catch (JsonProcessingException e) {
return false;
}
}
private boolean verifyWithExtraDataAsString(Map<String, Object> message, String sign) {
Object extraData = message.get("extraData");
if (!(extraData instanceof Map<?, ?>)) {
return false;
}
try {
Map<String, Object> 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<Map<String, Object>>() {
}));
} 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<String, Object> message, boolean signValid) throws JsonProcessingException {
Map<String, Object> 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);
}
}

View File

@@ -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<String, Object> 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<String, Object> 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();
}
}

View File

@@ -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);