优化
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
| 排车列表/详情 | `GET /platform/v1/last-leg/dispatches`;`GET /dispatches/{dispatchNo}` | list query:`page,size,dispatchNo,vehiclePlate,status,fbaAppointmentNo`;不传客户 | `...:inbound-order:list`;Controller 受 `lastleg.dispatch-query-api.enabled` 控制 |
|
||||
| 排车候选 | `GET /platform/v1/last-leg/dispatches/candidates` | query:`planIds?`(逗号分隔长 ID)、`orderNo?`,`fbaWarehouseCode?`,`sourceStage?`,`draftToken?` | 同 inbound list;仅发 `PLAN/RECEIVED` |
|
||||
| 草稿/锁/预排 | `GET /dispatches/drafts`;`POST /drafts/containers`;`DELETE /drafts/{draftToken}/containers/{containerId}`;`POST /draft-locks/lock|unlock|release`;`POST /pre-arrange` | 车厢:`draftToken,capacity`;锁:`planPalletId,draftToken,draftContainerId?`;释放:`draftToken`;预排:`planPalletIds:string[],draftToken` | `...:inbound-order:smart-dispatch`;Controller 受 `lastleg.smart-dispatch-api.enabled` 控制;409 锁冲突显示真实错误;不调用编辑、取消、depart、PDA 或派送 |
|
||||
| 库存汇总/余额 | `GET /platform/v1/last-leg/inventory/summary`;`GET /balances` | summary 强制 `page=1,size=200`;余额保留页面分页;支持 `skuCode,productName` 与仓库/入库/批次/库位筛选;不传客户 | `...:inventory:list`;只读,不调用 allocate/unallocate/pick/ship |
|
||||
| 库存汇总/余额 | `GET /platform/v1/last-leg/inventory/summary`;`GET /balances` | summary 强制 `page=1,size=200`;余额保留页面分页;支持 `skuCode,productName` 与仓库/入库/批次/库位筛选;不传客户;列表/详情客户只显示 `customerName`,海外仓只显示 `overseasWarehouseName`,空值为 `—` | `...:inventory:list`;只读,不调用 allocate/unallocate/pick/ship;不以客户 ID、本地客户候选、国内来源仓或目的仓猜名称 |
|
||||
| 库存位置/进销存 | `GET /inventory/balances/{balanceId}/positions`;`GET /inventory/ledger` | position:`palletNo?,batchNo?,inventoryStatus?`;ledger 含 `page,size,skuCode` 与业务单/仓库/时间筛选 | 同 inventory list;`balanceId` 编码;只展示后端事实 |
|
||||
| 公共海外收货仓候选(国内 HTTP) | `GET /platform/v1/warehouse-admin` | `tenantId=0,page=1,size=200,capabilityCode=RECEIVING,warehouseType=OVERSEAS,status=ACTIVE` | 国内 Warehouse 是唯一仓库主数据源;托盘、库存和换标复用公共候选并保留字符串 ID,不请求海外仓库根路径 |
|
||||
| 托盘列表/详情 | `GET /platform/v1/warehouse-admin/{warehouseId}/pallets`;`GET /pallets/{palletId}` | list:`page,size,keyword?,palletType?,status?`;无客户参数 | `...:pallet:list`;仓库/托盘长 ID 编码 |
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 需后端或产品继续确认
|
||||
|
||||
- **库存客户/海外仓快照**:正式余额 DTO 已返回 `customerName` 与 `overseasWarehouseName`,Admin 列表和详情只显示这两个服务端快照,空值为 `—`,不以 `customerId`、客户候选、国内来源仓、目的仓/FBA 仓或固定值补齐。当前测试数据“客户全为 — / 海外仓显示义乌仓”属于后端库存事实阻断:`LoadingCompletedConsumer#registerInTransit` 的客户名来自可能为空的跨区 `source.customerName`,并把国内装柜 `payload.warehouseName` 传入库存的 `overseasWarehouseName`;后续命中既有余额时 `InventoryCommandService#balance` 不会修订快照。后端需保证正式客户名称快照,并在实际海外收货仓冻结前保持海外仓为空、冻结后写入真实海外收货仓;前端不修复或猜测历史/错误事实。
|
||||
- **换标原型字段**:列表 Summary 仍不含 `productNameZh/labelRequirement/relabelFile/budgetTotal/relabelCost`;本模块没有可用的换标文件上传端点。页面不伪造金额或文件名。创建 DTO 额外要求海外仓、截止时间、联系人、电话、货件号和新 SKU,这是完成真实创建的最小扩展。
|
||||
- **托盘新增服务端权限**:前端新增按钮按 `platform:overseas-warehouse:pallet:create` 控制,但当前审计的 `PlatformWarehouseResourceController#createPallet` 未见对应 `@PreAuthorize`;需后端补齐或确认由网关等价强制。原型要求“托盘重量”,当前 Save DTO 无该字段,页面不伪造或发送。
|
||||
- **托盘状态语义**:原型只给闲置/占用,DTO 还允许 `SCRAPPED`。是否允许 Admin 在普通编辑中直接改为报废仍需确认;不新增独立停用/报废动作。PUT 已按冻结契约携带详情 `version`,旧版本 `A0409`。
|
||||
|
||||
@@ -29,6 +29,8 @@ describe('overseas inventory presentation adapter', () => {
|
||||
expect(row).toMatchObject({
|
||||
id: '2089652067743272963',
|
||||
customerId: '990018',
|
||||
owner: 'Acceptance Customer',
|
||||
warehouse: 'Acceptance US Warehouse',
|
||||
sku: 'SKU-ACCEPT-1',
|
||||
productName: '验收商品',
|
||||
location: '—',
|
||||
@@ -39,6 +41,30 @@ describe('overseas inventory presentation adapter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows an unknown owner as a dash and never substitutes customerId', () => {
|
||||
const row = toInventoryListItem({
|
||||
id: '1', customerId: '2092618717861048321', customerName: null,
|
||||
overseasWarehouseId: '3', inboundOrderId: '4', inboundCode: 'IN-1',
|
||||
totalPackages: 1, availablePackages: 1, holdPackages: 0, allocatedPackages: 0,
|
||||
pickedPackages: 0, inTransitPackages: 0, receivingPackages: 0, procurementPackages: 0,
|
||||
})
|
||||
expect(row.owner).toBe('—')
|
||||
expect(row.owner).not.toBe(row.customerId)
|
||||
})
|
||||
|
||||
it('uses only the formal overseas warehouse name snapshot', () => {
|
||||
const row = toInventoryListItem({
|
||||
id: '1', customerId: '2', customerName: '客户甲', overseasWarehouseId: '3',
|
||||
overseasWarehouseCode: 'LAX', overseasWarehouseName: null, destinationWarehouseName: 'ONT8目的仓',
|
||||
inboundOrderId: '4', inboundCode: 'IN-1', totalPackages: 1, availablePackages: 1,
|
||||
holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, inTransitPackages: 0,
|
||||
receivingPackages: 0, procurementPackages: 0,
|
||||
})
|
||||
expect(row.warehouse).toBe('—')
|
||||
expect(row.warehouse).not.toBe('LAX')
|
||||
expect(row.warehouse).not.toBe('ONT8目的仓')
|
||||
})
|
||||
|
||||
it('falls back to the English product name only when the Chinese name is absent', () => {
|
||||
expect(inventoryProductName({
|
||||
id: '1', customerId: '2', overseasWarehouseId: '3', inboundOrderId: '4', inboundCode: 'IN-1',
|
||||
|
||||
@@ -29,7 +29,7 @@ export function toInventoryListItem(dto: InventoryBalanceDto): InventoryListItem
|
||||
inboundOrderNo: text(dto.inboundOrderNo) === '—' ? '' : String(dto.inboundOrderNo),
|
||||
inboundCode: text(dto.inboundCode) === '—' ? '' : String(dto.inboundCode),
|
||||
owner: text(dto.customerName),
|
||||
warehouse: text(dto.overseasWarehouseName || dto.overseasWarehouseCode),
|
||||
warehouse: text(dto.overseasWarehouseName),
|
||||
sku: text(dto.skuCode),
|
||||
productName: inventoryProductName(dto),
|
||||
location: '—',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from '../api/inventory'
|
||||
import InventoryDetailView from './InventoryDetailView.vue'
|
||||
import InventoryView from './InventoryView.vue'
|
||||
|
||||
const routing = vi.hoisted(() => ({ push: vi.fn(), route: { params: {} as Record<string, string>, query: {} as Record<string, string> } }))
|
||||
vi.mock('vue-router', () => ({ useRoute: () => routing.route, useRouter: () => ({ push: routing.push }) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => false }) }))
|
||||
vi.mock('../api/pallets', () => ({ listReceivingOverseasWarehouses: vi.fn() }))
|
||||
vi.mock('../api/inventory', () => ({
|
||||
listInventoryBalances: vi.fn(), getInventorySummary: vi.fn(),
|
||||
listInventoryPositions: vi.fn(), listInventoryLedger: vi.fn(),
|
||||
}))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
||||
function memoryStorage(): Storage {
|
||||
const values = new Map<string, string>()
|
||||
return {
|
||||
get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null,
|
||||
key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value),
|
||||
}
|
||||
}
|
||||
|
||||
const customerId = '2092618717861048321'
|
||||
const balance = {
|
||||
id: '2092873463568596994', customerId, customerName: '华远贸易', overseasWarehouseId: '200',
|
||||
overseasWarehouseName: '洛杉矶仓', inboundOrderId: '300', inboundOrderNo: 'LIP-1', inboundCode: 'IN-1',
|
||||
destinationWarehouseName: 'ONT8目的仓',
|
||||
skuCode: 'SKU-1', productNameZh: '验收商品', totalPackages: 10, availablePackages: 10,
|
||||
holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, inTransitPackages: 0,
|
||||
receivingPackages: 0, procurementPackages: 0, updatedAt: '2026-08-28T10:00:00',
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
afterAll(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('inventory customer-name snapshots', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('localStorage', memoryStorage())
|
||||
routing.push.mockReset(); routing.route.params = {}; routing.route.query = {}
|
||||
vi.mocked(listInventoryBalances).mockReset().mockResolvedValue({ records: [balance], page: 1, size: 10, total: 1, pages: 1 })
|
||||
vi.mocked(getInventorySummary).mockReset().mockResolvedValue({ inboundOrderCount: 1, palletCount: 1, totalPackages: 10, availablePackages: 10, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, inTransitPackages: 0, receivingPackages: 0, procurementPackages: 0 })
|
||||
vi.mocked(listInventoryPositions).mockReset().mockResolvedValue([])
|
||||
vi.mocked(listInventoryLedger).mockReset().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
})
|
||||
|
||||
it('renders the formal server customerName in the inventory list', async () => {
|
||||
const wrapper = mount(InventoryView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('华远贸易')
|
||||
expect(wrapper.text()).toContain('洛杉矶仓')
|
||||
expect(wrapper.text()).not.toContain('ONT8目的仓')
|
||||
expect(wrapper.text()).not.toContain(customerId)
|
||||
})
|
||||
|
||||
it('renders the same server snapshot in inventory detail', async () => {
|
||||
routing.route.params = { inventoryId: balance.id }
|
||||
routing.route.query = { overseasWarehouseId: balance.overseasWarehouseId, inboundOrderNo: balance.inboundOrderNo, inboundCode: balance.inboundCode, skuCode: balance.skuCode }
|
||||
const wrapper = mount(InventoryDetailView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('华远贸易')
|
||||
expect(wrapper.text()).toContain('洛杉矶仓')
|
||||
expect(wrapper.text()).not.toContain('ONT8目的仓')
|
||||
expect(wrapper.text()).not.toContain(customerId)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user