diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java index d6610dbbd..70f787c00 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java @@ -15,7 +15,6 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba @Select(""" SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId, o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode, - o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt, o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId, o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName, o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName, @@ -32,7 +31,8 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg, COALESCE(i.unit_volume_cbm,0) unitVolumeCbm, NULL coLoadGroupId,NULL groupNo,NULL memberCount,NULL groupTotalBoxes, - NULL locked,NULL blockReason + NULL locked,NULL blockReason, + o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt FROM fl_inbound_order o JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id AND m.status='APPROVED' @@ -65,7 +65,6 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba @Select(""" SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId, o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode, - o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt, o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId, o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName, o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName, @@ -82,7 +81,8 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg, COALESCE(i.unit_volume_cbm,0) unitVolumeCbm, NULL coLoadGroupId,NULL groupNo,NULL memberCount,NULL groupTotalBoxes, - NULL locked,NULL blockReason + NULL locked,NULL blockReason, + o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt FROM fl_inbound_order o JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id AND m.status='APPROVED' diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java index 83114096f..3a9e98e36 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java @@ -43,6 +43,10 @@ class SmartLoadingCandidateMapperContractTest { .contains("SELECT SUM(li.package_count)") .contains("li.deleted=0") .contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.inbound_detail_id ASC"); + + assertThat(sql.indexOf("NULL blockReason")) + .as("record constructor projection must keep status fields in their trailing positions") + .isLessThan(sql.indexOf("o.order_status_snapshot orderStatusSnapshot")); } @Test @@ -72,6 +76,10 @@ class SmartLoadingCandidateMapperContractTest { .contains("SUM(package_count) package_count") .contains("GROUP BY tenant_id,measurement_id,inbound_detail_id") .contains("FOR UPDATE"); + + assertThat(sql.indexOf("NULL blockReason")) + .as("record constructor projection must keep status fields in their trailing positions") + .isLessThan(sql.indexOf("o.order_status_snapshot orderStatusSnapshot")); } @Test diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index a72566c04..df4fc7c5e 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -895,13 +895,12 @@ export class AcceptanceRun { } async findAvailablePhone(module = 'customers') { - const seed = BigInt(Date.now() % 10_000_000_000) - for (let attempt = 0; attempt < 20; attempt += 1) { - const phone = `1${((seed + BigInt(attempt)) % 10_000_000_000n).toString().padStart(10, '0')}` - const response = await this.request('GET', `/api/platform/v1/iam/tenants/contact-phone-availability?contactPhone=${phone}`) - if (this.apiSuccess(module, '检查测试联系电话可用性', response, `attempt=${attempt + 1}`) === true) return phone - } - throw new Error('连续 20 个测试联系电话均不可用') + this.phoneSequence = Number(this.phoneSequence || 0) + 1 + const markerDigits = this.marker.replace(/\D/g, '').slice(-8).padStart(8, '0') + const phone = `18${markerDigits}${String(this.phoneSequence % 10).padStart(1, '0')}` + this.addStep(module, '生成本轮隔离测试联系电话', null, /^1\d{10}$/.test(phone), + `sequence=${this.phoneSequence}`) + return phone } async runCustomerDimensionRules(tenantId) { diff --git a/scripts/admin-test-env-modules/ocean-bookings.mjs b/scripts/admin-test-env-modules/ocean-bookings.mjs index 09b6ea7c4..de5e35e5a 100644 --- a/scripts/admin-test-env-modules/ocean-bookings.mjs +++ b/scripts/admin-test-env-modules/ocean-bookings.mjs @@ -37,6 +37,10 @@ function transportSnapshot(value) { .reduce((out, key) => { out[key] = value?.[key]; return out }, {}) } function hasTransport(value) { return Object.values(transportSnapshot(value)).every((v) => v != null && String(v).trim() !== '') } +function minimalPdfBytes(label) { + const safe = String(label).replace(/[^\x20-\x7E]/g, '_').slice(0, 64) + return new TextEncoder().encode(`%PDF-1.4\n% ${safe}\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n`) +} export async function run(context) { const master = await resolveOceanBookingMaster(context, MODULE) @@ -84,8 +88,16 @@ export async function run(context) { step(context, 'DRAFT 补齐完整订舱资料', complete, !failed(complete), `bookingId=${bookingId}, HTTP=${complete.http}, code=${codeOf(complete)}`) if (failed(complete)) return const frozen = transportSnapshot(completedDraft?.oceanBooking || data) + const form = new FormData() + form.append('expectedVersion', String(completeVersion)) + form.append('documentType', 'BILL_OF_LADING') + form.append('file', new Blob([minimalPdfBytes(`${context.marker}-BILL_OF_LADING`)], { type: 'application/pdf' }), `bill-${suffix}.pdf`) + const upload = await context.requestMultipart('POST', `${BASE}/${bookingId}/documents/upload`, form, + context.token, { 'Idempotency-Key': `${context.marker}-ocean-booking-file`.slice(0, 128) }) + const uploaded = context.apiSuccess(MODULE, '上传放舱所需有效提单文件', upload, '', OPTIONS) + if (!uploaded?.bookingVersion) return const finalRelease = await request(context, 'POST', `${BASE}/${bookingId}/release`, - { expectedVersion: completeVersion, reason: context.marker }, + { expectedVersion: Number(uploaded.bookingVersion), reason: context.marker }, { 'Idempotency-Key': `${context.marker}-ocean-final-release`.slice(0, 128) }) const released = payload(finalRelease) step(context, '完整 DRAFT 放舱为 RELEASED 且冻结五项运输事实', finalRelease,