优化
This commit is contained in:
@@ -1,3 +1,87 @@
|
||||
package com.baiyue.file.controller;
|
||||
import com.baiyue.common.core.api.*;import com.baiyue.common.core.exception.BizException;import com.baiyue.file.service.InternalBusinessFileService;import jakarta.annotation.Resource;import jakarta.validation.constraints.*;import org.springframework.beans.factory.annotation.Value;import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;
|
||||
@RestController @RequestMapping("/internal/file/v1/business-files") public class InternalBusinessFileController{@Resource InternalBusinessFileService service;@Value("${baiyue.security.internal-service-token}") String expected;private void check(String token){if(token==null||!java.security.MessageDigest.isEqual(token.getBytes(java.nio.charset.StandardCharsets.UTF_8),expected.getBytes(java.nio.charset.StandardCharsets.UTF_8)))throw new BizException(ResultCode.FORBIDDEN,"内部服务令牌无效");}@PostMapping(value="/{tenantId}/{businessType}/{businessId}",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse<InternalBusinessFileService.Metadata> upload(@RequestHeader(value="X-Internal-Service-Token",required=false) String token,@RequestHeader("X-Operator-Id") long operatorId,@PathVariable @Positive long tenantId,@PathVariable @NotBlank String businessType,@PathVariable @NotBlank String businessId,@RequestPart("file") MultipartFile file){check(token);return ApiResponse.success(service.upload(tenantId,businessType,businessId,operatorId,file));}@GetMapping("/{tenantId}/{businessType}/{businessId}/{fileId}") public ApiResponse<InternalBusinessFileService.Metadata> metadata(@RequestHeader(value="X-Internal-Service-Token",required=false) String token,@PathVariable @Positive long tenantId,@PathVariable @NotBlank String businessType,@PathVariable @NotBlank String businessId,@PathVariable @Positive long fileId){check(token);return ApiResponse.success(service.metadata(tenantId,businessType,businessId,fileId));}@GetMapping("/{tenantId}/{businessType}/{businessId}/{fileId}/content") public org.springframework.http.ResponseEntity<byte[]> download(@RequestHeader(value="X-Internal-Service-Token",required=false) String token,@PathVariable @Positive long tenantId,@PathVariable @NotBlank String businessType,@PathVariable @NotBlank String businessId,@PathVariable @Positive long fileId){check(token);var value=service.download(tenantId,businessType,businessId,fileId);String name=value.fileName().replace("\r","").replace("\n","").replace("\"","");return org.springframework.http.ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore()).contentType(MediaType.parseMediaType(value.contentType())).header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\""+name+"\"").contentLength(value.content().length).body(value.content());}}
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.file.service.InternalBusinessFileService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/file/v1/business-files")
|
||||
public class InternalBusinessFileController {
|
||||
@Resource private InternalBusinessFileService service;
|
||||
@Value("${baiyue.security.internal-service-token}") private String expected;
|
||||
|
||||
@PostMapping(value = "/{tenantId}/{businessType}/{businessId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<InternalBusinessFileService.Metadata> upload(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable @NotBlank String businessType,
|
||||
@PathVariable @NotBlank String businessId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
check(token);
|
||||
checkTenantId(tenantId);
|
||||
return ApiResponse.success(service.upload(tenantId, businessType, businessId, operatorId, file));
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{businessType}/{businessId}/{fileId}")
|
||||
public ApiResponse<InternalBusinessFileService.Metadata> metadata(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable @NotBlank String businessType,
|
||||
@PathVariable @NotBlank String businessId,
|
||||
@PathVariable @Positive long fileId) {
|
||||
check(token);
|
||||
checkTenantId(tenantId);
|
||||
return ApiResponse.success(service.metadata(tenantId, businessType, businessId, fileId));
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{businessType}/{businessId}/{fileId}/content")
|
||||
public ResponseEntity<byte[]> download(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable @NotBlank String businessType,
|
||||
@PathVariable @NotBlank String businessId,
|
||||
@PathVariable @Positive long fileId) {
|
||||
check(token);
|
||||
checkTenantId(tenantId);
|
||||
var value = service.download(tenantId, businessType, businessId, fileId);
|
||||
String name = value.fileName().replace("\r", "").replace("\n", "").replace("\"", "");
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.contentType(MediaType.parseMediaType(value.contentType()))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"")
|
||||
.contentLength(value.content().length)
|
||||
.body(value.content());
|
||||
}
|
||||
|
||||
private void check(String token) {
|
||||
if (token == null || !MessageDigest.isEqual(
|
||||
token.getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务令牌无效");
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkTenantId(long tenantId) {
|
||||
if (tenantId < 0) throw new BizException(ResultCode.BAD_REQUEST, "客户作用域不能为负数");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,110 @@ import com.baiyue.file.mapper.FileObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class InternalBusinessFileService {
|
||||
private static final java.util.Set<String> BUSINESS_TYPES = java.util.Set.of(
|
||||
"TRUCK_APPOINTMENT_ATTACHMENT", "CUSTOMS_DOCUMENT", "CUSTOMS_RECEIPT", "FINANCE_VOUCHER");
|
||||
@Resource FileObjectMapper files; @Resource BusinessFileStorage storage;
|
||||
public Metadata upload(long tenantId,String businessType,String businessId,long operatorId,MultipartFile file){if(tenantId<=0||operatorId<=0)throw new BizException(ResultCode.BAD_REQUEST,"归属和操作人必须有效");if(!BUSINESS_TYPES.contains(businessType))throw new BizException(ResultCode.BAD_REQUEST,"不支持的附件业务类型");long parsedBusinessId;try{parsedBusinessId=Long.parseLong(businessId);}catch(NumberFormatException ex){throw new BizException(ResultCode.BAD_REQUEST,"业务ID格式无效");}if(parsedBusinessId<=0)throw new BizException(ResultCode.BAD_REQUEST,"业务ID必须有效");var stored=storage.storeLoading(tenantId,parsedBusinessId,file);FileObjectEntity value=new FileObjectEntity();value.setTenantId(tenantId);value.setBusinessType(businessType);value.setBusinessId(businessId);value.setOriginalName(stored.originalName());value.setObjectKey(stored.objectKey());value.setStorageProvider("LOCAL");value.setBucketName("business-files");value.setContentType(stored.contentType());value.setSizeBytes(stored.size());value.setSha256(stored.sha256());value.setStatus("ACTIVE");value.setCreatedBy(operatorId);value.setUpdatedBy(operatorId);value.setCreatedAt(LocalDateTime.now());value.setUpdatedAt(value.getCreatedAt());value.setDeleted(false);value.setVersion(0);if(files.insert(value)!=1)throw new BizException(ResultCode.INTERNAL_ERROR,"附件元数据保存失败");return metadata(value);}
|
||||
public Metadata metadata(long tenantId,String businessType,String businessId,long fileId){return metadata(required(tenantId,businessType,businessId,fileId));}
|
||||
public Download download(long tenantId,String businessType,String businessId,long fileId){FileObjectEntity value=required(tenantId,businessType,businessId,fileId);return new Download(value.getOriginalName(),value.getContentType(),storage.read(value.getObjectKey()));}
|
||||
private FileObjectEntity required(long tenantId,String businessType,String businessId,long fileId){FileObjectEntity value=files.selectOne(new LambdaQueryWrapper<FileObjectEntity>().eq(FileObjectEntity::getTenantId,tenantId).eq(FileObjectEntity::getBusinessType,businessType).eq(FileObjectEntity::getBusinessId,businessId).eq(FileObjectEntity::getId,fileId).eq(FileObjectEntity::getStatus,"ACTIVE").eq(FileObjectEntity::getDeleted,false));if(value==null)throw new BizException(ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配");return value;}
|
||||
private static Metadata metadata(FileObjectEntity value){return new Metadata(value.getId(),value.getTenantId(),value.getBusinessType(),value.getBusinessId(),value.getOriginalName(),value.getSha256(),value.getContentType(),value.getSizeBytes());}
|
||||
public record Metadata(Long fileId,Long tenantId,String businessType,String businessId,String fileName,String sha256,String contentType,Long sizeBytes){}
|
||||
public record Download(String fileName,String contentType,byte[] content){}
|
||||
private static final Set<String> BUSINESS_TYPES = Set.of(
|
||||
"TRUCK_APPOINTMENT_ATTACHMENT",
|
||||
"CUSTOMS_DOCUMENT",
|
||||
"CUSTOMS_RECEIPT",
|
||||
"FINANCE_VOUCHER",
|
||||
"CONTAINER_CLEARANCE_DOCUMENT",
|
||||
"LOADING_CUSTOMS_DOCUMENT");
|
||||
|
||||
@Resource private FileObjectMapper files;
|
||||
@Resource private BusinessFileStorage storage;
|
||||
|
||||
public Metadata upload(long tenantId, String businessType, String businessId, long operatorId,
|
||||
MultipartFile file) {
|
||||
if (tenantId < 0 || operatorId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "归属和操作人必须有效");
|
||||
}
|
||||
validateBusinessType(businessType);
|
||||
long parsedBusinessId = parseBusinessId(businessId);
|
||||
var stored = storage.storeLoading(tenantId, parsedBusinessId, file);
|
||||
FileObjectEntity value = new FileObjectEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setBusinessType(businessType);
|
||||
value.setBusinessId(businessId);
|
||||
value.setOriginalName(stored.originalName());
|
||||
value.setObjectKey(stored.objectKey());
|
||||
value.setStorageProvider("LOCAL");
|
||||
value.setBucketName("business-files");
|
||||
value.setContentType(stored.contentType());
|
||||
value.setSizeBytes(stored.size());
|
||||
value.setSha256(stored.sha256());
|
||||
value.setStatus("ACTIVE");
|
||||
value.setCreatedBy(operatorId);
|
||||
value.setUpdatedBy(operatorId);
|
||||
value.setCreatedAt(LocalDateTime.now());
|
||||
value.setUpdatedAt(value.getCreatedAt());
|
||||
value.setDeleted(false);
|
||||
value.setVersion(0);
|
||||
if (files.insert(value) != 1) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "附件元数据保存失败");
|
||||
}
|
||||
return metadata(value);
|
||||
}
|
||||
|
||||
public Metadata metadata(long tenantId, String businessType, String businessId, long fileId) {
|
||||
return metadata(required(tenantId, businessType, businessId, fileId));
|
||||
}
|
||||
|
||||
public Download download(long tenantId, String businessType, String businessId, long fileId) {
|
||||
FileObjectEntity value = required(tenantId, businessType, businessId, fileId);
|
||||
return new Download(value.getOriginalName(), value.getContentType(), storage.read(value.getObjectKey()));
|
||||
}
|
||||
|
||||
private FileObjectEntity required(long tenantId, String businessType, String businessId, long fileId) {
|
||||
validateScope(tenantId, businessType, businessId, fileId);
|
||||
FileObjectEntity value = files.selectOne(new LambdaQueryWrapper<FileObjectEntity>()
|
||||
.eq(FileObjectEntity::getTenantId, tenantId)
|
||||
.eq(FileObjectEntity::getBusinessType, businessType)
|
||||
.eq(FileObjectEntity::getBusinessId, businessId)
|
||||
.eq(FileObjectEntity::getId, fileId)
|
||||
.eq(FileObjectEntity::getStatus, "ACTIVE")
|
||||
.eq(FileObjectEntity::getDeleted, false));
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "业务附件不存在或归属不匹配");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void validateScope(long tenantId, String businessType, String businessId, long fileId) {
|
||||
if (tenantId < 0 || fileId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "归属和文件ID必须有效");
|
||||
}
|
||||
validateBusinessType(businessType);
|
||||
parseBusinessId(businessId);
|
||||
}
|
||||
|
||||
private static void validateBusinessType(String businessType) {
|
||||
if (!BUSINESS_TYPES.contains(businessType)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不支持的附件业务类型");
|
||||
}
|
||||
}
|
||||
|
||||
private static long parseBusinessId(String businessId) {
|
||||
final long value;
|
||||
try {
|
||||
value = Long.parseLong(businessId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "业务ID格式无效");
|
||||
}
|
||||
if (value <= 0) throw new BizException(ResultCode.BAD_REQUEST, "业务ID必须有效");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Metadata metadata(FileObjectEntity value) {
|
||||
return new Metadata(value.getId(), value.getTenantId(), value.getBusinessType(), value.getBusinessId(),
|
||||
value.getOriginalName(), value.getSha256(), value.getContentType(), value.getSizeBytes());
|
||||
}
|
||||
|
||||
public record Metadata(Long fileId, Long tenantId, String businessType, String businessId,
|
||||
String fileName, String sha256, String contentType, Long sizeBytes) {}
|
||||
public record Download(String fileName, String contentType, byte[] content) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user