Merge origin/dev into release/test preserving current workflows
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
- 工作台定义:`src/data/overseas-workbench.js`。
|
||||
- 登录与会话:`src/api/auth.js`、`src/stores/session.js`。
|
||||
- 扫码设备层:`src/utils/scan-broadcast.js`;广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。
|
||||
- 入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、统一箱码定位入仓码、FBA 按箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;退货入库通过退货单号或 SKU 候选进入并按本包实收生成退货箱与计费批次;移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询均已接入专用 PDA API。库存盘点底层页面与接口仍保留,但当前工作台不开放入口。
|
||||
- 入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、统一箱码定位入仓码、FBA 按箱数收货、OVERSEAS 逐箱收货、一件代发按国内封托来源托盘整托收货并逐入仓码核平;退货入库通过退货单号或 SKU 候选进入并按本包实收生成退货箱与计费批次;移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询均已接入专用 PDA API。库存盘点底层页面与接口仍保留,但当前工作台不开放入口。
|
||||
|
||||
## 页面与能力快速索引
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
## 海外业务接口边界
|
||||
|
||||
- 退货查询空态集中在 `src/api/return-receipt.js`、`src/api/return-packing.js`:仅 `startReturnReceipt`、`findReturnSkuCandidates`、`findPackingOrders` 将 `A0404/404` 转为无匹配结果;签收确认、合箱写入和标签读取不得套用此处理,权限、网络与服务错误仍须保留诊断并报错。回归见 `tests/return-receipt-api.test.mjs`、`tests/return-packing-api.test.mjs`,不得将查询无记录误报为写入成功。
|
||||
- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
|
||||
- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、普通 FBA bulk/普通 OVERSEAS carton、一件代发 source pallet 整托收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;一件代发只提交国内冻结的来源托盘号,不允许逐箱或拆托收货。响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
|
||||
- 入仓摘要、箱码解析和实收响应的 `assigned/mergeAllowed/urgent/mergeHint` 只消费 Lastleg 返回值,布尔标志只有 `true` 生效,`mergeAllowed` 缺省为 `false`;合托仅作提示,不能改变收货资格或触发分配。首扫实物托盘只确认服务端已有托盘、库位与资源锁预留,不在设备端重新分配托盘、库位或资源锁;只提交业务箱码、托盘码及当前收货数量,最终资格由服务端决定。
|
||||
- `src/api/overseas.js` 的 `overseasUrl` 统一拼接普通请求、上传和下载地址:先移除运行时 `apiBase` 末尾斜杠;当基址已以 `/api` 或 `/overseas-api` 结尾时,再移除业务路径重复的 `/api` 前缀。各业务 API 不得自行拼接 `session.apiBase`,也不得依赖双斜杠或重复 `/api` 被网关容错。
|
||||
- 待卸柜候选只消费服务端已按 Token 绑定仓过滤的结果,并严格校验每柜正式 `overseasWarehouseCode`、`overseasWarehouseName` 与 `warehouse-profile` 一致;字段缺失或仓不一致一律 fail-closed,不得从 FBA/业务目的仓或内部 ID 推导。候选保留服务端返回的未齐全柜并严格消费 `allPlansReady`、`operable`、`blockReason`、`plannedArrivalAt` 与柜级计划统计;`operable=false` 只能查看只读摘要,不得调用 prepare。候选顺序沿用服务端“可操作优先、同组计划到仓时间升序、时间为空最后”的排序,前端不得用入仓码/计划数组长度自行推导就绪或把预计到港冒充计划到仓。
|
||||
- 候选详情、prepare 响应和 current 恢复都必须重复校验同一接柜仓编码;prepare 只建立已分配柜的作业会话,不得当作首次认领。服务端仓冲突的 `409/A0409` 中文错误保留 `requestId`/`traceId`;会话建立后仍按既有 current 恢复与完成前禁止切柜逻辑处理。
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import { overseasDownload, overseasRequest, overseasUpload } from '@/api/overseas.js'
|
||||
import { normalizeReceivingCandidate, normalizeReceivingSession, normalizeReceivingWarehouseProfile } from '@/api/receiving-adapter.js'
|
||||
import { normalizeReceivingAssignment, normalizeReceivingInbound, normalizeReceivingCandidate, normalizeReceivingSession, normalizeReceivingWarehouseProfile } from '@/api/receiving-adapter.js'
|
||||
|
||||
const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers'
|
||||
const current = (suffix = '') => `${RECEIVING_BASE}/current${suffix}`
|
||||
@@ -20,9 +20,9 @@ export const uploadCurrentUnloadPhoto = (filePath) => overseasUpload(current('/u
|
||||
export const downloadCurrentUnloadPhoto = (downloadPath) => overseasDownload(downloadPath)
|
||||
export const signCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/sign'), request))
|
||||
export const getCurrentReceiving = async () => normalizeReceivingSession(await overseasRequest('GET', current()))
|
||||
export const resolveCurrentCarton = (cartonMark) => overseasRequest('POST', current('/cartons/resolve'), { cartonMark })
|
||||
export const resolveCurrentCarton = async (cartonMark) => normalizeReceivingInbound(await overseasRequest('POST', current('/cartons/resolve'), { cartonMark }))
|
||||
export const resolveCurrentPallet = (palletCode, inboundCode) => overseasRequest('POST', current('/pallets/resolve'), { palletCode, inboundCode })
|
||||
export const receiveCurrentBulk = (request) => overseasRequest('POST', current('/receipts/bulk'), request)
|
||||
export const receiveCurrentCarton = (request) => overseasRequest('POST', current('/receipts/carton'), request)
|
||||
export const receiveCurrentWholePallet = (request) => overseasRequest('POST', current('/receipts/pallet'), request)
|
||||
export const receiveCurrentBulk = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/bulk'), request))
|
||||
export const receiveCurrentCarton = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/carton'), request))
|
||||
export const receiveCurrentWholePallet = async (request) => normalizeReceivingAssignment(await overseasRequest('POST', current('/receipts/pallet'), request))
|
||||
export const completeCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/complete'), request))
|
||||
|
||||
@@ -38,15 +38,22 @@ export function assertReceivingWarehouse(value, profile, label = '当前柜') {
|
||||
|
||||
export function normalizeReceivingInbound(value = {}) {
|
||||
return {
|
||||
...value,
|
||||
...normalizeReceivingAssignment(value),
|
||||
wholePalletInbound: value.wholePalletInbound === true,
|
||||
plannedPalletCount: number(value.plannedPalletCount),
|
||||
totalPackages: number(value.totalPackages),
|
||||
receivedPackages: number(value.receivedPackages),
|
||||
pendingPackages: number(value.pendingPackages),
|
||||
pendingPackages: number(value.pendingPackages)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeReceivingAssignment(value = {}) {
|
||||
return {
|
||||
...value,
|
||||
assigned: value.assigned === true,
|
||||
mergeAllowed: value.mergeAllowed === true,
|
||||
urgent: value.urgent === true,
|
||||
mergeHint: text(value.mergeHint)
|
||||
mergeHint: value.mergeAllowed === true ? text(value.mergeHint) : ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +111,10 @@ export function toReceivingError(error, fallback = '海外仓作业失败') {
|
||||
const code = error?.code
|
||||
const rawMessage = String(error?.message || '').trim()
|
||||
const palletConflictMessages = {
|
||||
'Reserved pallet binding is incomplete': '预留托盘资料不完整,请联系管理员检查入仓计划',
|
||||
'Reserved pallet customer binding is invalid': '预留托盘与当前客户不匹配,请联系管理员检查入仓计划',
|
||||
'Scanned pallet does not match the reserved inbound plan resource': '扫描托盘与入仓计划预留资源不一致,请扫描已预留的托盘',
|
||||
'Pallet code matches multiple reserved resources in this inbound plan': '托盘码对应多个预留资源,请联系管理员检查入仓计划',
|
||||
'Pallet is not idle': '该托盘已被占用,当前不可用于收货,请更换可用托盘',
|
||||
'Pallet location was not found': '该托盘未关联有效库位,请联系管理员维护后重试',
|
||||
'Pallet virtual location is not active': '该托盘所在虚拟库位不可用,请更换托盘或联系管理员',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -63,7 +63,10 @@ test('login has no committed demo credentials and inbound operation implements t
|
||||
assert.doesNotMatch(operation, /\.split\([^\n]*['"]#['"]/, '前端必须原样提交完整箱码,禁止按 # 猜测入仓码或箱序号')
|
||||
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\), expectedPalletVersion: this\.resolvedPallet\.version \}/)
|
||||
assert.doesNotMatch(operation, /skuCode/)
|
||||
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
|
||||
assert.match(operation, /resolveCurrentCarton\(cartonMark\)[\s\S]*value\.operationMode !== 'DROPSHIP_PALLET'/)
|
||||
assert.match(operation, /name="carton"[\s\S]*@submit="resolveCarton"/)
|
||||
assert.match(operation, /sourcePalletNo: value\.sourcePalletNo \|\| cartonMark[\s\S]*receiveCurrentWholePallet/)
|
||||
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'[\s\S]*drainScanQueue/)
|
||||
assert.match(operation, /name="pallet"[\s\S]*@activate="activeTarget = 'pallet'"/)
|
||||
assert.match(operation, /receiving\.activeScanTarget = 'pallet'/, '普通FBA或中转解析后直接进入托盘扫码以减少操作')
|
||||
assert.match(operation, /\[Overseas PDA scan\] dispatch/)
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { isNoCurrentReceiving, isReceivingConflict, normalizeReceivingCandidate, normalizeReceivingSession, RECEIVING_PHOTO_ROLES, toReceivingError } from '../src/api/receiving-adapter.js'
|
||||
import { isNoCurrentReceiving, isReceivingConflict, normalizeReceivingAssignment, normalizeReceivingInbound, normalizeReceivingCandidate, normalizeReceivingSession, RECEIVING_PHOTO_ROLES, toReceivingError } from '../src/api/receiving-adapter.js'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
test('assignment hints are server booleans only and merge is disabled by default', () => {
|
||||
for (const value of [undefined, null, false, 1, 'true', 'false']) {
|
||||
const result = normalizeReceivingAssignment({ assigned: value, mergeAllowed: value, urgent: value, mergeHint: 'do not expose' })
|
||||
assert.deepEqual(result, { assigned: false, mergeAllowed: false, urgent: false, mergeHint: '' })
|
||||
}
|
||||
const flags = { assigned: true, mergeAllowed: true, urgent: true, mergeHint: ' 按平台安排合托 ' }
|
||||
const session = normalizeReceivingSession({ inbounds: [flags] })
|
||||
assert.equal(session.inbounds[0].mergeHint, '按平台安排合托')
|
||||
assert.equal(session.inbounds[0].assigned, true)
|
||||
assert.equal(session.inbounds[0].urgent, true)
|
||||
})
|
||||
|
||||
test('reservation mismatch asks for the reserved pallet and retains diagnostics', () => {
|
||||
const result = toReceivingError({ code: 'A0409', message: 'Scanned pallet does not match the reserved inbound plan resource', requestId: 'request-1', traceId: 'trace-1' })
|
||||
assert.equal(result.message, '扫描托盘与入仓计划预留资源不一致,请扫描已预留的托盘')
|
||||
assert.equal(result.requestId, 'request-1')
|
||||
assert.equal(result.traceId, 'trace-1')
|
||||
})
|
||||
|
||||
test('first scan and receipt requests confirm server reservations without resource assignment commands', async () => {
|
||||
const source = await readFile(new URL('../src/api/inbound.js', import.meta.url), 'utf8')
|
||||
const calls = []
|
||||
const api = new Function('overseasRequest', 'normalizeReceivingAssignment', 'normalizeReceivingInbound', source.replace(/^import .*$/gm, '').replace(/^export /gm, '') + '; return {resolveCurrentCarton,resolveCurrentPallet,receiveCurrentBulk,receiveCurrentCarton,receiveCurrentWholePallet};')(async (...args) => {
|
||||
calls.push(args)
|
||||
return { compatible: true, assigned: true, mergeAllowed: 'true', urgent: true, mergeHint: 'unsafe hint' }
|
||||
}, normalizeReceivingAssignment, normalizeReceivingInbound)
|
||||
assert.equal((await api.resolveCurrentCarton('BOX-1')).mergeAllowed, false)
|
||||
assert.equal((await api.resolveCurrentPallet('P-1', 'IN-1')).compatible, true)
|
||||
assert.equal((await api.receiveCurrentBulk({ inboundCode: 'IN-1', palletCode: 'P-1', packageCount: 2 })).urgent, true)
|
||||
assert.equal((await api.receiveCurrentCarton({ cartonMark: 'BOX-1', palletCode: 'P-1' })).mergeHint, '')
|
||||
assert.equal((await api.receiveCurrentWholePallet({ sourcePalletNo: 'DP-1' })).urgent, true)
|
||||
assert.deepEqual(calls, [
|
||||
['POST', '/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: 'BOX-1' }],
|
||||
['POST', '/pda/v1/last-leg/receiving-containers/current/pallets/resolve', { palletCode: 'P-1', inboundCode: 'IN-1' }],
|
||||
['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { inboundCode: 'IN-1', palletCode: 'P-1', packageCount: 2 }],
|
||||
['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/carton', { cartonMark: 'BOX-1', palletCode: 'P-1' }],
|
||||
['POST', '/pda/v1/last-leg/receiving-containers/current/receipts/pallet', { sourcePalletNo: 'DP-1' }]
|
||||
])
|
||||
const page = await readFile(new URL('../src/pages/operation/operation.vue', import.meta.url), 'utf8')
|
||||
assert.match(page, /activeInbound\.assigned/)
|
||||
assert.match(page, /activeInbound\.urgent/)
|
||||
assert.match(page, /activeInbound\.mergeAllowed && activeInbound\.mergeHint/)
|
||||
const gate = page.match(/canSubmitReceipt\(\) \{.*?\},/)?.[0]
|
||||
assert.ok(gate)
|
||||
assert.doesNotMatch(gate, /mergeAllowed|assigned|urgent/)
|
||||
assert.match(gate, /resolvedPallet\?\.compatible/)
|
||||
})
|
||||
|
||||
test('one-click dropship inbound is marked only by an explicit server boolean', () => {
|
||||
for (const value of [undefined, null, false, 1, 'true', 'false']) {
|
||||
assert.equal(normalizeReceivingInbound({ wholePalletInbound: value }).wholePalletInbound, false)
|
||||
}
|
||||
assert.equal(normalizeReceivingInbound({ wholePalletInbound: true }).wholePalletInbound, true)
|
||||
})
|
||||
|
||||
test('receiving adapter preserves the three signed photo roles in workflow order', () => {
|
||||
const snowflakeFileId = '2092476334483931138'
|
||||
|
||||
Reference in New Issue
Block a user