Merge branch 'test' into test-02huogui

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java
#	firstleg/src/main/resources/openapi/firstleg-v1.yaml
This commit is contained in:
2026-08-07 11:11:43 +08:00
55 changed files with 3173 additions and 94 deletions
@@ -53,7 +53,9 @@ public class InternalInboundOrderController {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
if (command.tenantId() <= 0 || command.orderId() <= 0 || command.warehouseId() == null
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()) {
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|| command.inboundCode() == null || command.inboundCode().isBlank()
|| command.inboundCode().length() > 40) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
return ApiResponse.success(service.createFromApprovedOrder(command));
@@ -75,19 +75,20 @@ public class PlatformFbaMasterDataController {
@GetMapping("/carrier-rates")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')")
@Operation(summary = "查询船司 FBA 价格版本", description = "默认查询全部企业;tenantId 可用于筛选目标企业")
@Operation(summary = "查询船司 FBA 价格版本", description = "查询平台公共价格;所有客户共享")
public ApiResponse<List<CarrierFbaRateEntity>> rates(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) Long tenantId,
@RequestParam(required = false) String rateCode) {
return ApiResponse.success(service.listRates(optionalPlatformTenant(user, tenantId), rateCode));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.listRates(rateCode));
}
@GetMapping("/carrier-rates/{id}")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')")
public ApiResponse<CarrierFbaRateEntity> rate(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long id) {
return ApiResponse.success(service.rate(FirstlegScopeGuard.platform(user, tenantId), id));
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long id) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.rate(id));
}
@PostMapping("/carrier-rates/versions")
@@ -95,10 +96,10 @@ public class PlatformFbaMasterDataController {
@BusinessAudit(event = "FBA_RATE_VERSION_CREATE", resourceType = "CARRIER_FBA_RATE",
description = "创建船司FBA价格版本")
public ApiResponse<CarrierFbaRateEntity> createRate(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody CarrierRateVersionRequest request) {
long scope = FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.createRateVersion(scope, user.userId(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.createRateVersion(user.userId(), request));
}
@DeleteMapping("/carrier-rates/{id}")
@@ -106,8 +107,9 @@ public class PlatformFbaMasterDataController {
@BusinessAudit(event = "FBA_RATE_RETIRE", resourceType = "CARRIER_FBA_RATE", resourceId = "#id",
description = "停用船司FBA价格版本")
public ApiResponse<Void> retireRate(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId, @PathVariable long id) {
service.retireRate(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
@PathVariable long id) {
FirstlegScopeGuard.platform(user);
service.retireRate(id, user.userId());
return ApiResponse.success(null);
}
@@ -7,6 +7,7 @@ public record OrderApprovedInboundCommand(
long tenantId,
long orderId,
String orderNo,
String inboundCode,
Long customerId,
String customerName,
Long warehouseId,
@@ -15,6 +15,9 @@ import java.time.LocalDateTime;
@Setter
@TableName("fl_carrier_fba_rate")
public class CarrierFbaRateEntity extends BaseEntity {
/** 船司价格是平台公共主数据,不绑定任何客户。 */
public static final long PLATFORM_SCOPE = 0L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
@@ -50,9 +50,9 @@ public class FbaPricingRepository {
.eq(CustomerChargeRuleEntity::getId, id));
}
public CarrierFbaRateEntity findRate(long tenantId, long id) {
public CarrierFbaRateEntity findRate(long id) {
return rates.selectOne(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getId, id));
}
@@ -64,21 +64,34 @@ public class FbaPricingRepository {
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
}
public List<CarrierFbaRateEntity> listRates(Long tenantId, String rateCode) {
public List<CarrierFbaRateEntity> listRates(String rateCode) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(tenantId != null, CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(rateCode != null && !rateCode.isBlank(), CarrierFbaRateEntity::getRateCode, rateCode)
.orderByAsc(CarrierFbaRateEntity::getRateCode)
.orderByDesc(CarrierFbaRateEntity::getRateVersion));
}
/**
* 在创建平台公共价格版本时锁定当前公共价格集合,串行化版本号和有效期检查。
* 调用方必须在事务中使用该方法。
*/
public List<CarrierFbaRateEntity> listRatesForUpdate(String rateCode) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(rateCode != null && !rateCode.isBlank(), CarrierFbaRateEntity::getRateCode, rateCode)
.orderByAsc(CarrierFbaRateEntity::getRateCode)
.orderByDesc(CarrierFbaRateEntity::getRateVersion)
.last("FOR UPDATE"));
}
public int nextRuleVersion(long tenantId, String ruleCode) {
List<CustomerChargeRuleEntity> values = listRules(tenantId, ruleCode);
return values.isEmpty() ? 1 : values.get(0).getRuleVersion() + 1;
}
public int nextRateVersion(long tenantId, String rateCode) {
List<CarrierFbaRateEntity> values = listRates(tenantId, rateCode);
public int nextRateVersion(String rateCode) {
List<CarrierFbaRateEntity> values = listRatesForUpdate(rateCode);
return values.isEmpty() ? 1 : values.get(0).getRateVersion() + 1;
}
@@ -91,9 +104,9 @@ public class FbaPricingRepository {
.set(CustomerChargeRuleEntity::getUpdatedBy, operatorId));
}
public int retireRate(long tenantId, long id, long operatorId) {
public int retireRate(long id, long operatorId) {
return rates.update(null, new LambdaUpdateWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getId, id)
.eq(CarrierFbaRateEntity::getStatus, "ACTIVE")
.set(CarrierFbaRateEntity::getStatus, "INACTIVE")
@@ -121,7 +134,7 @@ public class FbaPricingRepository {
public List<CarrierFbaRateEntity> matchRates(InboundPricingEntity inbound, String chargeUnit,
LocalDateTime at) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, inbound.getTenantId())
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getOriginWarehouseId, inbound.getWarehouseId())
.eq(CarrierFbaRateEntity::getBusinessType, inbound.getBusinessType())
.eq(CarrierFbaRateEntity::getChannelCode, inbound.getChannelCode())
@@ -188,15 +201,17 @@ public class FbaPricingRepository {
.set(InboundFeeEntity::getUpdatedAt, LocalDateTime.now()));
}
public int markInboundCalculated(long tenantId, long inboundId, String chargeMode,
public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode,
java.math.BigDecimal chargeable) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId)
.eq(InboundPricingEntity::getId, inboundId)
.in(InboundPricingEntity::getProcessStatus, List.of("WAIT_PRICING", "FEE_CONFIRMING"))
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING")
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if (!"WAIT_SPLIT".equals(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
}
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
return inbounds.update(null, update);
@@ -214,7 +229,7 @@ public class FbaPricingRepository {
public int markInboundConfirmed(long tenantId, long inboundId) {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
.eq(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING")
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT"))
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
}
@@ -59,10 +59,9 @@ public class FbaMasterDataService {
}
@Transactional
public CarrierFbaRateEntity createRateVersion(long tenantId, long operatorId,
CarrierRateVersionRequest request) {
public CarrierFbaRateEntity createRateVersion(long operatorId, CarrierRateVersionRequest request) {
validatePeriod(request.effectiveFrom(), request.effectiveTo());
boolean overlaps = repository.listRates(tenantId, null).stream()
boolean overlaps = repository.listRatesForUpdate(null).stream()
.filter(v -> "ACTIVE".equals(v.getStatus()))
.filter(v -> Objects.equals(v.getOriginWarehouseId(), request.originWarehouseId()))
.filter(v -> Objects.equals(v.getBusinessType(), request.businessType()))
@@ -75,9 +74,9 @@ public class FbaMasterDataService {
if (overlaps) throw conflict("船司 FBA 价格有效期重叠,无法保证唯一匹配");
CarrierFbaRateEntity value = new CarrierFbaRateEntity();
value.setTenantId(tenantId);
value.setTenantId(CarrierFbaRateEntity.PLATFORM_SCOPE);
value.setRateCode(request.rateCode());
value.setRateVersion(repository.nextRateVersion(tenantId, request.rateCode()));
value.setRateVersion(repository.nextRateVersion(request.rateCode()));
value.setOriginWarehouseId(request.originWarehouseId());
value.setOriginWarehouseName(request.originWarehouseName());
value.setBusinessType(request.businessType());
@@ -103,8 +102,8 @@ public class FbaMasterDataService {
return repository.listRules(tenantId, code);
}
public List<CarrierFbaRateEntity> listRates(Long tenantId, String code) {
return repository.listRates(tenantId, code);
public List<CarrierFbaRateEntity> listRates(String code) {
return repository.listRates(code);
}
public CustomerChargeRuleEntity rule(long tenantId, long id) {
@@ -113,8 +112,8 @@ public class FbaMasterDataService {
return value;
}
public CarrierFbaRateEntity rate(long tenantId, long id) {
CarrierFbaRateEntity value = repository.findRate(tenantId, id);
public CarrierFbaRateEntity rate(long id) {
CarrierFbaRateEntity value = repository.findRate(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "船司 FBA 价格版本不存在");
return value;
}
@@ -125,8 +124,8 @@ public class FbaMasterDataService {
}
@Transactional
public void retireRate(long tenantId, long id, long operatorId) {
if (repository.retireRate(tenantId, id, operatorId) != 1) throw conflict("价格已停用或不存在");
public void retireRate(long id, long operatorId) {
if (repository.retireRate(id, operatorId) != 1) throw conflict("价格已停用或不存在");
}
private static void validatePeriod(LocalDateTime from, LocalDateTime to) {
@@ -47,7 +47,7 @@ public class FbaPricingService {
String hash = hash("CALCULATE", inboundId, requestedAt == null ? "CURRENT" : requestedAt.toString());
InboundPricingIdempotencyEntity token = beginIdempotency(
tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash);
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING");
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
List<CustomerChargeRuleEntity> matchedRules = repository.matchRules(inbound, pricingAt);
@@ -88,11 +88,14 @@ public class FbaPricingService {
repository.insertFee(fee);
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now));
if (repository.markInboundCalculated(tenantId, inboundId, rule.getChargeMode(), chargeable) != 1) {
if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(),
rule.getChargeMode(), chargeable) != 1) {
throw conflict("入库单状态已变化,费用试算未保存");
}
complete(token, fee.getId(), now);
return response(inbound, fee, rate.getUnitPrice(), "FEE_CONFIRMING");
String nextStatus = "WAIT_SPLIT".equals(inbound.getProcessStatus())
? "WAIT_SPLIT" : "FEE_CONFIRMING";
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
}
@Transactional
@@ -103,14 +106,14 @@ public class FbaPricingService {
String hash = hash("CONFIRM", inboundId, current.getId().toString());
InboundPricingIdempotencyEntity token = beginIdempotency(
tenantId, operatorId, inboundId, "FEE_CONFIRM", idempotencyKey, hash);
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING");
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
LocalDateTime now = LocalDateTime.now(clock);
if (repository.confirmFee(tenantId, current.getId(), operatorId, now) != 1
|| repository.markInboundConfirmed(tenantId, inboundId) != 1) {
throw conflict("费用或入库单状态已变化,不能重复确认");
}
complete(token, current.getId(), now);
return response(tenantId, current.getId(), "FEE_CONFIRMING");
return response(tenantId, current.getId(), null);
}
@Transactional
@@ -232,7 +235,7 @@ public class FbaPricingService {
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING").contains(inbound.getProcessStatus())
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
throw conflict("当前入库单状态不允许费用试算");
}
@@ -38,7 +38,7 @@ public class InboundOrderCreationService {
inbound.setTenantId(command.tenantId());
inbound.setOrderId(command.orderId());
inbound.setOrderNo(command.orderNo());
inbound.setInboundCode("RK-" + command.orderId());
inbound.setInboundCode(command.inboundCode());
inbound.setProcessStatus("WAIT_MEASURE");
inbound.setOrderStatusSnapshot("APPROVED");
inbound.setMeasureReviewStatus("NOT_SUBMITTED");
@@ -309,8 +309,8 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule (
CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate (
id BIGINT UNSIGNED NOT NULL COMMENT '船司FBA价格ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
rate_code VARCHAR(50) NOT NULL COMMENT '租户内价格编码',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台价格作用域ID,船司价格固定为0',
rate_code VARCHAR(50) NOT NULL COMMENT '平台公共价格编码',
rate_version INT UNSIGNED NOT NULL COMMENT '价格版本号',
origin_warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '起运仓逻辑ID',
origin_warehouse_name VARCHAR(128) NOT NULL COMMENT '起运仓名称快照',
@@ -32,43 +32,42 @@ paths:
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/fba-pricing/carrier-rates:
get:
summary: 查询船司 FBA 价格版本
summary: 查询平台公共船司 FBA 价格版本
description: 需要 firstleg:fba-pricing:rate:view 权限
parameters:
- {$ref: '#/components/parameters/OptionalTenantId'}
- {name: rateCode, in: query, schema: {type: string, maxLength: 50}}
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/fba-pricing/carrier-rates/versions:
post:
summary: 新建船司 FBA 价格版本
parameters: [{$ref: '#/components/parameters/TenantId'}]
summary: 新建平台公共船司 FBA 价格版本
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CarrierRateVersionRequest'}}}}
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/fba-pricing/carrier-rates/{id}:
get:
summary: 查询船司 FBA 价格版本详情
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/Id'}]
parameters: [{$ref: '#/components/parameters/Id'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
delete:
summary: 停用船司 FBA 价格版本
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/Id'}]
parameters: [{$ref: '#/components/parameters/Id'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
post:
summary: 按审核通过测量快照试算 FBA 费用
description: 精确匹配唯一客户规则和唯一船司价格;需要 firstleg:fba-pricing:calculate 权限。
description: 精确匹配唯一客户规则和唯一船司价格;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
post:
summary: 确认最新费用快照
description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/send:
post:
summary: 手工恢复发送费用快照
description: 仅作为异常恢复接口;正常业务由客服测量审核通过后自动计费、发布并完成入库。
summary: 发送已确认费用快照
description: 该动作涉及后续仓储流程,本轮二期验收不调用。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/latest:
@@ -6,6 +6,19 @@ info:
security:
- bearerAuth: []
paths:
/internal/firstleg/v1/inbounds/order-approved:
post:
summary: 订单审核通过后幂等创建入库单
description: 仅供订单服务调用;入仓码由订单提交时生成,Firstleg 必须原样沿用。承运商可在后续履约或计费前补充。
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/OrderApprovedInboundCommand'}
responses:
'200': {description: 成功或业务失败;相同客户和订单重放返回已有入库单 ID}
/api/platform/v1/first-leg/tasks:
get:
summary: 分页查询履约任务
@@ -308,6 +321,10 @@ components:
in: header
name: X-Signature
description: HMAC-SHA256 小写十六进制签名
internalServiceToken:
type: apiKey
in: header
name: X-Internal-Service-Token
parameters:
XTimestamp:
name: X-Timestamp
@@ -348,6 +365,36 @@ components:
required: true
schema: {type: string, pattern: '^[1-9][0-9]*$'}
schemas:
OrderApprovedInboundCommand:
type: object
required: [tenantId, orderId, inboundCode, customerName, warehouseId]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 当前客户隔离 ID}
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string, nullable: true}
inboundCode: {type: string, minLength: 1, maxLength: 40, example: RK-2085308606829346818}
customerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
customerName: {type: string, minLength: 1}
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
warehouseName: {type: string, nullable: true}
deliveryWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
deliveryWarehouseName: {type: string, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
businessType: {type: string, nullable: true}
channelCode: {type: string, nullable: true}
channelName: {type: string, nullable: true}
carrierId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 可选,可在后续履约或计费前补充}
carrierName: {type: string, nullable: true}
taxMode: {type: string, nullable: true}
containerMode: {type: string, nullable: true}
customsInspection: {type: boolean, nullable: true}
destinationType: {type: string, nullable: true}
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
destinationCode: {type: string, nullable: true}
destinationName: {type: string, nullable: true}
publicRemark: {type: string, nullable: true}
expressCompanyName: {type: string, nullable: true}
expressTrackingNo: {type: string, nullable: true}
ContainerBookingPageEnvelope:
type: object
properties: