二期内容提交
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package com.baiyue.file.controller;
|
||||
|
||||
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.MeasurementImageService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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;
|
||||
|
||||
/** Firstleg 保存第三方尺寸测量证据图片的内部入口。 */
|
||||
@RestController
|
||||
@RequestMapping("/internal/file/v1/measurement-images")
|
||||
public class InternalMeasurementImageController {
|
||||
private final MeasurementImageService images;
|
||||
private final byte[] expectedToken;
|
||||
|
||||
public InternalMeasurementImageController(
|
||||
MeasurementImageService images,
|
||||
@Value("${baiyue.security.internal-service-token}") String token) {
|
||||
this.images = images;
|
||||
this.expectedToken = token.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{tenantId}/{inboundId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<MeasurementImageService.UploadResult> upload(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
requireToken(token);
|
||||
if (tenantId <= 0 || inboundId <= 0 || operatorId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "租户、入库单和操作主体ID必须大于0");
|
||||
}
|
||||
return ApiResponse.success(
|
||||
images.uploadMeasurementEvidence(tenantId, inboundId, operatorId, file));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{tenantId}/{inboundId}/{fileId}")
|
||||
public ApiResponse<Void> delete(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long fileId) {
|
||||
requireToken(token);
|
||||
if (tenantId <= 0 || inboundId <= 0 || fileId <= 0 || operatorId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "租户、入库单、文件和操作主体ID必须大于0");
|
||||
}
|
||||
images.deleteMeasurementEvidence(tenantId, inboundId, fileId, operatorId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private void requireToken(String token) {
|
||||
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,25 @@ public class MeasurementImageService {
|
||||
|
||||
@Transactional
|
||||
public UploadResult upload(long tenantId, long inboundId, long operatorId, MultipartFile file) {
|
||||
return recognize(storeObject(tenantId, inboundId, operatorId, file, "INBOUND_WEIGHT_IMAGE"));
|
||||
}
|
||||
|
||||
/** 保存第三方尺寸测量证据图片,不触发重量 OCR。 */
|
||||
@Transactional
|
||||
public UploadResult uploadMeasurementEvidence(long tenantId, long inboundId, long operatorId,
|
||||
MultipartFile file) {
|
||||
FileObjectEntity object = storeObject(
|
||||
tenantId, inboundId, operatorId, file, "INBOUND_MEASUREMENT_IMAGE");
|
||||
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
||||
object.getSizeBytes(), "NOT_REQUIRED", null, null, null, null);
|
||||
}
|
||||
|
||||
private FileObjectEntity storeObject(long tenantId, long inboundId, long operatorId,
|
||||
MultipartFile file, String businessType) {
|
||||
var stored = storage.store(tenantId, inboundId, file);
|
||||
FileObjectEntity object = new FileObjectEntity();
|
||||
object.setTenantId(tenantId);
|
||||
object.setBusinessType("INBOUND_WEIGHT_IMAGE");
|
||||
object.setBusinessType(businessType);
|
||||
object.setBusinessId(Long.toString(inboundId));
|
||||
object.setOriginalName(stored.originalName());
|
||||
object.setObjectKey(stored.objectKey());
|
||||
@@ -56,12 +71,15 @@ public class MeasurementImageService {
|
||||
object.setDeleted(false);
|
||||
object.setVersion(0);
|
||||
files.insert(object);
|
||||
|
||||
return recognize(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
public UploadResult retryRecognition(long tenantId, long inboundId, long fileId) {
|
||||
return recognize(requireObject(tenantId, inboundId, fileId));
|
||||
FileObjectEntity object = requireObject(tenantId, inboundId, fileId);
|
||||
if (!"INBOUND_WEIGHT_IMAGE".equals(object.getBusinessType())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "尺寸测量证据图片不执行重量OCR");
|
||||
}
|
||||
return recognize(object);
|
||||
}
|
||||
|
||||
public Download download(long tenantId, long inboundId, long fileId) {
|
||||
@@ -69,11 +87,25 @@ public class MeasurementImageService {
|
||||
return new Download(object.getOriginalName(), object.getContentType(), storage.read(object.getObjectKey()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteMeasurementEvidence(long tenantId, long inboundId, long fileId, long operatorId) {
|
||||
FileObjectEntity object = requireObject(tenantId, inboundId, fileId);
|
||||
if (!"INBOUND_MEASUREMENT_IMAGE".equals(object.getBusinessType())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "仅允许清理第三方尺寸测量证据图片");
|
||||
}
|
||||
object.setDeleted(true);
|
||||
object.setStatus("DELETED");
|
||||
object.setUpdatedBy(operatorId);
|
||||
object.setUpdatedAt(LocalDateTime.now());
|
||||
files.updateById(object);
|
||||
storage.delete(object.getObjectKey());
|
||||
}
|
||||
|
||||
private FileObjectEntity requireObject(long tenantId, long inboundId, long fileId) {
|
||||
FileObjectEntity object = files.selectOne(new LambdaQueryWrapper<FileObjectEntity>()
|
||||
.eq(FileObjectEntity::getTenantId, tenantId)
|
||||
.eq(FileObjectEntity::getId, fileId)
|
||||
.eq(FileObjectEntity::getBusinessType, "INBOUND_WEIGHT_IMAGE")
|
||||
.in(FileObjectEntity::getBusinessType, "INBOUND_WEIGHT_IMAGE", "INBOUND_MEASUREMENT_IMAGE")
|
||||
.eq(FileObjectEntity::getBusinessId, Long.toString(inboundId))
|
||||
.eq(FileObjectEntity::getDeleted, false));
|
||||
if (object == null) throw new BizException(ResultCode.NOT_FOUND, "称重图片不存在");
|
||||
|
||||
@@ -56,6 +56,16 @@ public class MeasurementImageStorage {
|
||||
catch (IOException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "称重图片读取失败"); }
|
||||
}
|
||||
|
||||
public void delete(String objectKey) {
|
||||
Path path = root.resolve(objectKey).normalize();
|
||||
if (!path.startsWith(root)) throw new BizException(ResultCode.FORBIDDEN, "文件路径越界");
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "测量图片清理失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyImage(MultipartFile file) {
|
||||
try (InputStream input = file.getInputStream()) {
|
||||
if (ImageIO.read(input) == null) throw bad("上传内容不是有效图片");
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.file.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.file.service.MeasurementImageService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InternalMeasurementImageControllerTest {
|
||||
private final MeasurementImageService images = mock(MeasurementImageService.class);
|
||||
private final InternalMeasurementImageController controller =
|
||||
new InternalMeasurementImageController(images, "internal-token");
|
||||
|
||||
@Test
|
||||
void 保存第三方尺寸测量图片但不触发重量识别契约() {
|
||||
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
|
||||
new byte[]{1, 2, 3});
|
||||
var result = new MeasurementImageService.UploadResult(41L, "measure.png",
|
||||
MediaType.IMAGE_PNG_VALUE, 3L, "NOT_REQUIRED", null, null, null, null);
|
||||
when(images.uploadMeasurementEvidence(11L, 21L, 31L, file)).thenReturn(result);
|
||||
|
||||
var response = controller.upload("internal-token", 31L, 11L, 21L, file);
|
||||
|
||||
assertThat(response.data().fileId()).isEqualTo(41L);
|
||||
assertThat(response.data().recognitionStatus()).isEqualTo("NOT_REQUIRED");
|
||||
verify(images).uploadMeasurementEvidence(11L, 21L, 31L, file);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 内部令牌错误时拒绝保存() {
|
||||
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
|
||||
new byte[]{1});
|
||||
assertThatThrownBy(() -> controller.upload("bad-token", 31L, 11L, 21L, file))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("凭证无效");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.file.service;
|
||||
|
||||
import com.baiyue.file.client.WeightOcrClient;
|
||||
import com.baiyue.file.entity.FileObjectEntity;
|
||||
import com.baiyue.file.mapper.FileObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MeasurementImageServiceEvidenceTest {
|
||||
@Test
|
||||
void 尺寸测量证据图片落库但不调用重量Ocr() {
|
||||
MeasurementImageStorage storage = mock(MeasurementImageStorage.class);
|
||||
FileObjectMapper files = mock(FileObjectMapper.class);
|
||||
WeightOcrClient ocr = mock(WeightOcrClient.class);
|
||||
MeasurementImageService service = new MeasurementImageService(storage, files, ocr, "internal-token");
|
||||
var image = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
|
||||
new byte[]{1, 2, 3});
|
||||
when(storage.store(11L, 21L, image)).thenReturn(new MeasurementImageStorage.Stored(
|
||||
"measurements/11/21/a.png", Path.of("a.png"), MediaType.IMAGE_PNG_VALUE,
|
||||
3L, "hash", "measure.png"));
|
||||
doAnswer(invocation -> {
|
||||
FileObjectEntity object = invocation.getArgument(0);
|
||||
object.setId(41L);
|
||||
return 1;
|
||||
}).when(files).insert(any(FileObjectEntity.class));
|
||||
|
||||
var result = service.uploadMeasurementEvidence(11L, 21L, 31L, image);
|
||||
|
||||
assertThat(result.fileId()).isEqualTo(41L);
|
||||
assertThat(result.recognitionStatus()).isEqualTo("NOT_REQUIRED");
|
||||
verifyNoInteractions(ocr);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 尺寸测量证据图片不能通过重试入口触发重量Ocr() {
|
||||
MeasurementImageStorage storage = mock(MeasurementImageStorage.class);
|
||||
FileObjectMapper files = mock(FileObjectMapper.class);
|
||||
WeightOcrClient ocr = mock(WeightOcrClient.class);
|
||||
MeasurementImageService service = new MeasurementImageService(storage, files, ocr, "internal-token");
|
||||
FileObjectEntity object = new FileObjectEntity();
|
||||
object.setId(41L);
|
||||
object.setTenantId(11L);
|
||||
object.setBusinessType("INBOUND_MEASUREMENT_IMAGE");
|
||||
object.setBusinessId("21");
|
||||
when(files.selectOne(any())).thenReturn(object);
|
||||
|
||||
assertThatThrownBy(() -> service.retryRecognition(11L, 21L, 41L))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
|
||||
.hasMessageContaining("不执行重量OCR");
|
||||
verifyNoInteractions(ocr);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user