This commit is contained in:
2026-08-14 16:06:33 +08:00
parent 09e44e6520
commit 610cc1abf9
17 changed files with 135 additions and 41 deletions
+3 -3
View File
@@ -465,15 +465,15 @@ export function updateMerchantFirstLegOrder(id: number | string, payload: FirstL
return apiRequest<void>({ method: 'PUT', url: `/merchant/v1/orders/first-leg/${id}`, data: payload })
}
export function submitMerchantFirstLegOrder(id: number | string) {
export function submitMerchantFirstLegOrder(id: number | string, idempotencyKey: string) {
return apiRequest<void>({
method: 'POST',
url: `/merchant/v1/orders/first-leg/${id}/submit`,
headers: { 'Idempotency-Key': createIdempotencyKey(`first-leg-submit-${id}`) },
headers: { 'Idempotency-Key': idempotencyKey },
})
}
function createIdempotencyKey(scope: string) {
export function createIdempotencyKey(scope: string) {
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
return `${scope}-${randomPart}`
}
@@ -49,7 +49,8 @@ test('maps transport and status fields instead of treating container mode as tra
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /status === 'REJECTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /status === 'APPROVED' \|\| status === 'ACCEPTED' \|\| status === 'PROCESSING'\) return 'WAIT_MEASURE'/)
assert.match(storeSource, /status === 'COMPLETED'\) return 'DELIVERED'/)
assert.match(storeSource, /status === 'COMPLETED'\) return 'LOADED'/)
assert.doesNotMatch(storeSource, /status === 'COMPLETED'\) return 'DELIVERED'/)
})
test('prefers unified reads and only falls back to the real legacy API when C02 is unavailable', () => {
@@ -148,10 +149,13 @@ test('does not expose the out-of-scope cancellation write', () => {
test('sends an idempotency key for create and submit writes', () => {
assert.match(apiSource, /'Idempotency-Key': createIdempotencyKey\('first-leg-create'\)/)
assert.match(apiSource, /'Idempotency-Key': createIdempotencyKey\(`first-leg-submit-\$\{id\}`\)/)
assert.match(apiSource, /submitMerchantFirstLegOrder\(id: number \| string, idempotencyKey: string\)/)
assert.match(apiSource, /'Idempotency-Key': idempotencyKey/)
assert.match(apiSource, /createMerchantFirstLegOrderBatch\(items: MerchantBatchDraftItem\[\], idempotencyKey: string\)/)
assert.match(apiSource, /headers: \{ 'Idempotency-Key': idempotencyKey \}/)
assert.match(storeSource, /createMerchantFirstLegOrderBatch\(items, idempotencyKey\)/)
assert.match(storeSource, /pendingSubmitKeys\.get\(id\) \?\? createIdempotencyKey\(`first-leg-submit-\$\{id\}`\)/)
assert.match(storeSource, /submitMerchantFirstLegOrder\(id, idempotencyKey\)/)
})
test('does not expose the contradictory backend rejected state in the client model', () => {
+14 -5
View File
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import {
createMerchantFirstLegOrderBatch,
createMerchantFirstLegOrder,
createIdempotencyKey,
getMerchantFirstLegOrder,
getMerchantFirstLegOrders,
getMerchantWarehouseDetail,
@@ -49,7 +50,7 @@ function clientStatus(status: ApiFirstLegStatus): FirstLegOrder['status'] {
// 后端联调任务曾出现 REJECTED,但一期已确认订单不设驳回状态;兼容旧数据时仍按待审核展示。
if (status === 'REJECTED') return 'PENDING_REVIEW'
if (status === 'APPROVED' || status === 'ACCEPTED' || status === 'PROCESSING') return 'WAIT_MEASURE'
if (status === 'COMPLETED') return 'DELIVERED'
if (status === 'COMPLETED') return 'LOADED'
if (status === 'CANCELLED') return 'PENDING_REVIEW'
return 'PENDING_REVIEW'
}
@@ -442,6 +443,7 @@ function mapWarehouse(item: MerchantWarehouse): ClientWarehouse {
export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () => {
const orders = ref<FirstLegOrder[]>([])
const pendingSubmitKeys = new Map<string, string>()
const warehouses = ref<ClientWarehouse[]>([])
const loading = ref(false)
const loadedFromApi = ref(false)
@@ -566,16 +568,23 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
return createMerchantFirstLegOrderBatch(items, idempotencyKey)
}
async function submit(draft: FirstLegOrderDraft, customerName: string, id?: string, companyAuditStatus?: MerchantTenantStatus) {
async function submitSaved(id: string, companyAuditStatus?: MerchantTenantStatus) {
if (companyAuditStatus && companyAuditStatus !== 'ACTIVE') throw new Error('企业资料尚未审核通过,请先完善企业资料并等待平台审核')
const idempotencyKey = pendingSubmitKeys.get(id) ?? createIdempotencyKey(`first-leg-submit-${id}`)
pendingSubmitKeys.set(id, idempotencyKey)
await submitMerchantFirstLegOrder(id, idempotencyKey)
pendingSubmitKeys.delete(id)
return loadUnifiedDetail(id)
}
async function submit(draft: FirstLegOrderDraft, customerName: string, id?: string, companyAuditStatus?: MerchantTenantStatus) {
const order = await saveDraft(draft, customerName, id)
await submitMerchantFirstLegOrder(order.id)
return loadUnifiedDetail(order.id)
return submitSaved(order.id, companyAuditStatus)
}
async function loadDraftDetail(id: string) {
return loadLegacyDetail(id)
}
return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, saveDraft, saveDraftBatch, submit }
return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, saveDraft, saveDraftBatch, submitSaved, submit }
})
@@ -475,7 +475,8 @@ test('copies reactive order drafts into plain request data', () => {
assert.match(source, /function cloneDraft\(draft: FirstLegOrderDraft\)/)
assert.match(source, /items: draft\.items\.map\(\(item\) => \(\{ \.\.\.item \}\)\)/)
assert.match(source, /store\.saveDraft\(cloneDraft\(form\)/)
assert.match(source, /store\.submit\(cloneDraft\(form\)/)
assert.match(source, /editingId\.value = savedOrder\.id/)
assert.match(source, /store\.submitSaved\(savedOrder\.id/)
assert.doesNotMatch(source, /structuredClone\((?:form|order\.)/)
})
@@ -1425,7 +1425,9 @@ async function submitOrder() {
try {
saving.value = true
const knownCompanyAuditStatus = companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined
const order = await store.submit(cloneDraft(form), tenantStore.company.companyName, editingId.value, knownCompanyAuditStatus)
const savedOrder = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value)
editingId.value = savedOrder.id
const order = await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
markEditorClean()
editorOpen.value = false
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })