优化
This commit is contained in:
+18
@@ -4,8 +4,10 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.dto.OpenApiMeasurementDtos.EventResponse;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
|
||||
import com.baiyue.firstleg.service.InboundMeasurementOpenApiService;
|
||||
import com.baiyue.firstleg.service.MojiaMeasurementOpenApiService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** 第三方设备长宽高测量开放接口。 */
|
||||
@@ -36,6 +39,21 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
public class OpenApiInboundMeasurementController {
|
||||
@Resource
|
||||
private InboundMeasurementOpenApiService service;
|
||||
@Resource
|
||||
private MojiaMeasurementOpenApiService mojiaService;
|
||||
|
||||
@Operation(summary = "墨家设备按箱码上传单箱测量结果",
|
||||
description = "首轮联调使用固定 X-App-Key;JSON 中 picture 为 JPEG Data URL,boxCode 原样取自柏悦箱唛二维码")
|
||||
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ApiResponse<CartonMeasurementResult> receiveMojia(
|
||||
@RequestHeader("X-App-Key") String appKey,
|
||||
@RequestHeader(value = "X-Timestamp", required = false) String timestamp,
|
||||
@RequestHeader(value = "X-Nonce", required = false) String nonce,
|
||||
@RequestHeader(value = "X-Signature", required = false) String signature,
|
||||
@RequestBody String rawJson) {
|
||||
return ApiResponse.success(mojiaService.receive(
|
||||
new MeasurementRequestHeaders(appKey, timestamp, nonce, signature, null), rawJson));
|
||||
}
|
||||
|
||||
@Operation(summary = "提交一条第三方箱规测量结果和图片",
|
||||
description = "multipart 包含 data 原始JSON和 image 图片;联调关闭签名时只需固定 X-App-Key 和 Idempotency-Key,恢复后按六行 HMAC 规范签名")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 墨家设备按箱码上传的 JSON 协议。 */
|
||||
public final class MojiaMeasurementDtos {
|
||||
private MojiaMeasurementDtos() {
|
||||
}
|
||||
|
||||
public record UploadRequest(
|
||||
@NotBlank @Size(max = 128) String boxCode,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal length,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal width,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal height,
|
||||
@DecimalMin("0.001") @DecimalMax("999999999999999.999") BigDecimal weight,
|
||||
@NotBlank @Size(max = 64) String machineNo,
|
||||
LocalDateTime measuredAt,
|
||||
@NotBlank @Size(max = 7_100_000) String picture
|
||||
) {
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadRequest;
|
||||
import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validator;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Set;
|
||||
|
||||
/** 将墨家固定 AppKey + JSON/Base64 协议转换为统一单箱测量命令。 */
|
||||
@Service
|
||||
public class MojiaMeasurementOpenApiService {
|
||||
static final String CALLBACK_PATH = "/openapi/v1/inbound-measurements";
|
||||
private static final String JPEG_PREFIX = "data:image/jpeg;base64,";
|
||||
private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;
|
||||
|
||||
private final MeasurementRequestAuthenticator authenticator;
|
||||
private final CartonMeasurementService cartonMeasurements;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Validator validator;
|
||||
|
||||
public MojiaMeasurementOpenApiService(MeasurementRequestAuthenticator authenticator,
|
||||
CartonMeasurementService cartonMeasurements,
|
||||
ObjectMapper objectMapper,
|
||||
Validator validator) {
|
||||
this.authenticator = authenticator;
|
||||
this.cartonMeasurements = cartonMeasurements;
|
||||
this.objectMapper = objectMapper;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
public CartonMeasurementResult receive(MeasurementRequestHeaders headers, String rawJson) {
|
||||
AuthenticatedMeasurementApp app = authenticator.authenticate(
|
||||
"POST", CALLBACK_PATH, headers, rawJson, false);
|
||||
UploadRequest request = parseAndValidate(rawJson);
|
||||
MultipartFile image = decodeJpeg(request.picture());
|
||||
return cartonMeasurements.upsert(app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
|
||||
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
|
||||
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(), image));
|
||||
}
|
||||
|
||||
private UploadRequest parseAndValidate(String rawJson) {
|
||||
if (rawJson == null || rawJson.isBlank()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "请求 JSON 不能为空");
|
||||
}
|
||||
final UploadRequest request;
|
||||
try {
|
||||
request = objectMapper.readValue(rawJson, UploadRequest.class);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "请求 JSON 格式错误");
|
||||
}
|
||||
Set<ConstraintViolation<UploadRequest>> violations = validator.validate(request);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
violations.iterator().next().getPropertyPath() + " 参数不合法");
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private MultipartFile decodeJpeg(String picture) {
|
||||
if (!picture.startsWith(JPEG_PREFIX)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"picture 必须使用 data:image/jpeg;base64, 格式");
|
||||
}
|
||||
final byte[] bytes;
|
||||
try {
|
||||
bytes = Base64.getDecoder().decode(picture.substring(JPEG_PREFIX.length()));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "picture Base64 内容无效");
|
||||
}
|
||||
if (bytes.length == 0 || bytes.length > MAX_IMAGE_BYTES
|
||||
|| bytes.length < 3 || (bytes[0] & 0xff) != 0xff || (bytes[1] & 0xff) != 0xd8
|
||||
|| (bytes[2] & 0xff) != 0xff) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "picture 不是有效的 JPEG 图片或超过5MB");
|
||||
}
|
||||
return new InMemoryMultipartFile(bytes);
|
||||
}
|
||||
|
||||
private record InMemoryMultipartFile(byte[] content) implements MultipartFile {
|
||||
@Override public String getName() { return "image"; }
|
||||
@Override public String getOriginalFilename() { return "mojia-measurement.jpg"; }
|
||||
@Override public String getContentType() { return "image/jpeg"; }
|
||||
@Override public boolean isEmpty() { return content.length == 0; }
|
||||
@Override public long getSize() { return content.length; }
|
||||
@Override public byte[] getBytes() { return content.clone(); }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(content); }
|
||||
@Override public void transferTo(File destination) throws IOException {
|
||||
try (FileOutputStream output = new FileOutputStream(destination)) {
|
||||
output.write(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user