This commit is contained in:
2026-08-13 09:49:55 +08:00
parent f3366cb2fa
commit 95849f248f
69 changed files with 1224 additions and 221 deletions
@@ -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) {}
}