From d488e9ab40060b32f49b8074d7ea4b4fb7cc1d50 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 7 Sep 2026 15:07:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 +- src/modules/finance/AGENTS.md | 6 +- .../api/lastMileOrders.test.ts | 15 ++- .../overseas-warehouse/api/lastMileOrders.ts | 25 ++++ .../components/SmartDispatchDialog.vue | 2 + .../layoutAcceptance.test.ts | 9 +- .../pages/InboundOrdersView.vue | 6 +- .../pages/LastMileOrdersView.behavior.test.ts | 91 ++++++++++++++ .../pages/LastMileOrdersView.vue | 111 +++++++++++++++++- .../pages/prototypeContract.test.ts | 2 + 10 files changed, 260 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d2463417..3ead1108 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,9 @@ | 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 | | 订单列表、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖 | | 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | +| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | | 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | -| 财务与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;柜费入口 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | +| 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;账户余额、授信、充值审核和手工充扣只消费 Finance 权威结果。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | | 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`,`COMPLETED` 时替换为正式装柜清单详情并关闭旧工作台标签,不按 A0404 或错误文案猜阶段。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关按 `loadingPlanId` 展示、逐资料完成,全买单柜使用 `complete-all-buy`;后端为 `LoadingListService`、`loading-lists-v1.yaml`、`firstleg/customs/container/`,文件归 `file/` | | 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | | 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`smartLoadingItems.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/plans/{planId}/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。`orderStatuses` 最多四项且未传表示全部未完成状态;候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。计划箱数编辑上限、已用和未用展示只消费服务端 `totalPackageCount/remainingPackageCount`,不得从候选页或本地规格重新拼算。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 | diff --git a/src/modules/finance/AGENTS.md b/src/modules/finance/AGENTS.md index 1f4e2778..31169650 100644 --- a/src/modules/finance/AGENTS.md +++ b/src/modules/finance/AGENTS.md @@ -4,12 +4,12 @@ - 本文件适用于 `src/modules/finance/`。产品范围和状态读取 `../../../../../docs/财务管理/` 的产品确认与验收契约,后端读取 `../../../../../baiyue-firstleg/finance/AGENTS.md`。 - 页面从 `pages/` 进入,统一 API 从 `api/finance.ts` 进入;费用来源和柜次成本快照联查 `firstleg/`,凭证与异步导出联查 `file/`。 -- 正式范围包括客户资金账户、客户应收应付、应收款统计、客户对账单、应收结算单和应付结算单。资金账户当前只接充值申请审核和平台手工充值/扣款,冻结/解冻、消费及支付回调后续接入;不要从历史原型自动扩大其他财务功能。 +- 正式范围包括客户资金账户、充值申请审核、平台手工充值/扣款、授信额度维护、客户应收应付、应收款统计、客户对账单、应收结算单和应付结算单。业务冻结/结算/释放由服务端内部接口和跨区事件调用,不在 Admin 页面复制;第三方支付回调尚未接入,不要从历史原型自动扩大其他财务功能。 ## 客户资金账户 -- `pages/FinanceCustomerAccountsView.vue` 的列表和详情展示实际余额、冻结金额、可用余额;操作列固定第一列。客户上传的充值申请在详情内按待审核优先展示,审核、驳回及凭证预览以服务端申请版本和客户归属为准。 -- 平台手工充值和扣款必须提交账户版本与幂等键;扣款不得超过服务端可用余额。页面只展示服务端余额和流水结果,不在浏览器补算或回退静态数据。 +- `pages/FinanceCustomerAccountsView.vue` 的列表和详情展示实际余额、冻结金额、授信额度、已用授信、冻结授信和总可用余额;操作列固定第一列。客户上传的充值申请在详情内按待审核优先展示,审核、驳回及凭证预览以服务端申请版本和客户归属为准。 +- 平台手工充值、扣款和授信调整必须提交账户版本与幂等键;扣款及授信下限由服务端校验。页面只展示服务端余额、授信和流水结果,不在浏览器补算或回退静态数据。 ## 客户筛选与统计 diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts index 395ceea2..4b376b99 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, downloadLastMileOrders, getContainerPlanProgress, getLastMileOrder, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders } from './lastMileOrders' +import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, downloadLastMileOrders, getContainerPlanProgress, getLastMileOrder, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders } from './lastMileOrders' vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn(), post: vi.fn() } })) @@ -128,4 +128,17 @@ describe('last-mile order overseas API', () => { await expect(getContainerPlanProgress('2092873463568597001/unsafe')).resolves.toEqual(detail) expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe') }) + + it('changes one whole container receiving warehouse with version and idempotency evidence', async () => { + const updated = { sourceContainerBookingId: '2092873463568597001', overseasWarehouseId: '9007199254740993' } + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: updated }) + await expect(changeContainerReceivingWarehouse('2092873463568597001/unsafe', { + overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜', + }, 'receiving-key-1')).resolves.toEqual(updated) + expect(overseasHttp.put).toHaveBeenCalledWith( + '/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe/receiving-warehouse', + { overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜' }, + { headers: { 'Idempotency-Key': 'receiving-key-1' } }, + ) + }) }) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index adb1c3eb..17147947 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -171,6 +171,12 @@ export interface ContainerPlanProgressQuery { export interface ContainerPlanProgressSummaryDto { sourceContainerBookingId: string containerNo: string | null + overseasWarehouseId: string | null + overseasWarehouseCode: string | null + overseasWarehouseName: string | null + assignmentVersion: number + canChangeReceivingWarehouse: boolean + receivingWarehouseLockReason: string | null totalOrderCount: number inboundCodeCount: number missingInboundCodeCount: number @@ -204,6 +210,12 @@ export interface ConvertContainerInboundPlanPayload { sourceContainerBookingId: string } +export interface ChangeContainerReceivingWarehousePayload { + overseasWarehouseId: string + expectedVersion: number + remark?: string +} + export interface LastMileOrderDetailDto { order: LastMileOrderDto cargo: LastMileCargoDto[] @@ -286,6 +298,19 @@ export async function getContainerPlanProgress(sourceContainerBookingId: string) return unwrapApiResponse(response) } +export async function changeContainerReceivingWarehouse( + sourceContainerBookingId: string, + payload: ChangeContainerReceivingWarehousePayload, + idempotencyKey: string, +) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/receiving-warehouse`, + payload, + { headers: { 'Idempotency-Key': idempotencyKey } }, + ) + return unwrapApiResponse(response) +} + export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload) { const response = await overseasHttp.post, ApiEnvelope>( '/platform/v1/last-leg/inbound-plans/convert-container', payload, diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index 2c497e54..c24c333a 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -125,6 +125,7 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string) const id = palletId(item) const target = containers.value.find((container) => container.id === containerId) if (!target || target.palletIds.includes(id)) return + if (!String(item.overseasWarehouseId || '').trim()) { ElMessage.warning('该托盘尚未关联海外仓,不能排车'); return } if (containerFor(id)) { ElMessage.warning('该托盘已在其他装车托盘中'); return } if (target.palletIds.length >= target.capacity) { ElMessage.warning('该装车托盘已达到设置的托数'); return } const nextWeight = containerWeightLb(target) + weightLb(item) @@ -171,6 +172,7 @@ async function close(value = false) { async function submit() { if (!assignedIds.value.length) { ElMessage.warning('请至少移入一个托盘'); return } + if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) { ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘'); return } saving.value = true; mutationError.value = null try { await preArrangeDispatch({ planPalletIds: assignedIds.value, draftToken: ensureDraftToken() }) diff --git a/src/modules/overseas-warehouse/layoutAcceptance.test.ts b/src/modules/overseas-warehouse/layoutAcceptance.test.ts index ac6b95ec..01120efa 100644 --- a/src/modules/overseas-warehouse/layoutAcceptance.test.ts +++ b/src/modules/overseas-warehouse/layoutAcceptance.test.ts @@ -9,7 +9,7 @@ describe('overseas warehouse 1920px layout acceptance', () => { it('keeps action columns first and wide enough for every visible action', () => { const widths: Record = { 'LastMileOrdersView.vue': 270, - 'InboundOrdersView.vue': 190, + 'InboundOrdersView.vue': 260, 'DispatchesView.vue': 240, 'OutboundsView.vue': 190, 'DeliveryOrdersView.vue': 300, @@ -25,6 +25,13 @@ describe('overseas warehouse 1920px layout acceptance', () => { } }) + it('keeps all inbound-order row actions on one line', () => { + const source = page('InboundOrdersView.vue') + expect(source).toContain('