diff --git a/AGENTS.md b/AGENTS.md index d6479fd..87d7728 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,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 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。 +- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、FBA 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;库内、换标与查询入口在海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约齐备前必须禁用,不得调用平台/客户/内部接口或模拟成功。 ## 页面与能力快速索引 @@ -45,7 +45,7 @@ - `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、入仓码下 SKU/箱唛解析、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 - 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。 -- 库内、换标与现场查询仍是静态原型;新增真实作业必须建立明确的业务 API/DTO、错误态和契约测试,不得把原型任务改成模拟成功。 +- 库内、换标与现场查询只有视觉原型依据,当前工作台入口保持禁用;新增真实作业必须建立 `/api/pda/**` 业务 API/DTO、`lastleg:pda:operate` 权限、Token 冻结仓库范围、错误态和契约测试,不得把平台、客户或内部接口直接暴露给 PDA。 - 已有海外入仓 PDA Controller 统一位于 Lastleg `/api/pda/v1/last-leg/inbound-plans/{planNo}/**`;浏览器或设备请求仍必须经 `gateway-overseas` 的显式 PDA 路由,不得直连 Lastleg,也不得改走国内 Gateway。 - 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。 - 四期仍止于入仓、库存与库内操作。后端历史 `dispatch/`、出库和派送代码不构成海外 PDA 页面入口,也不得加入工作台。 @@ -53,6 +53,6 @@ ## 质量门禁 - 安装:`npm install`;检查:`npm test`。 -- H5 生产编译:`npm run build:h5`;App 资源编译:`npm run build:app`。 +- 正式产品形态是海外 PDA App;`npm run build:h5` 仅用于浏览器调试和模板编译门禁,App 资源编译使用 `npm run build:app`。 - APK/IPA 必须在 HBuilderX 或正式原生打包环境中使用独立 AppID 与签名验证;仅通过 App 资源编译不得声称 APK 真机验收完成。 - 不提交 `node_modules/`、`dist/`、`unpackage/`、环境文件、签名证书或打包产物。 diff --git a/README.md b/README.md index a749c68..3be9296 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 柏悦物流海外版 PDA -独立的 uni-app Vue 3 工程,面向海外仓现场人员。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布。 +独立的 uni-app Vue 3 App 工程,正式产品形态是安装在海外仓手持 PDA 上的 App。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布;H5 只用于浏览器调试和编译门禁,不是最终交付形态。 首版工作台按四期边界提供柜号级真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。 @@ -21,8 +21,8 @@ npm run build:h5 npm run build:app ``` -- `build:h5` 生成 H5 发布文件; -- `build:app` 生成 App 离线打包资源; +- `build:h5` 生成浏览器调试资源,用于页面和模板编译检查; +- `build:app` 生成正式 App 的离线打包资源; - 生成可安装 APK/IPA 需使用 HBuilderX App 开发版执行云打包或配置原生离线打包环境。 ## 正式 App 打包前必填 @@ -40,5 +40,5 @@ npm run build:app - 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、SKU/箱唛与既有物理托盘解析、FBA 按 SKU 和箱数收货、OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。 - 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。 - 登录、恢复和刷新会 fail-closed 校验国内/海外双 Token 的同一平台操作主体、`WAREHOUSE_PICKER`、`lastleg:pda:operate` 及同一冻结海外仓;并发海外 401 共享一次国内 refresh,校验完成后原子替换双 Token并将原请求或照片上传重放一次。 -- 库内、换标和现场查询仍是可运行的静态原型,不会模拟海外服务提交成功。 +- 库存盘点、商品移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询当前缺少海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约,入口已禁用;不会调用平台、客户或内部接口,也不会模拟成功。 - 本地契约测试与 H5/App 资源编译不等于真实开发环境 HTTP 或 PDA 真机验收。 diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index d488797..5163a81 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -3,19 +3,19 @@ export const MODULES = [ { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' } ] }, { key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [ - { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: true, badge: '原型' }, - { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: true, badge: '原型' }, - { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: true, badge: '原型' } + { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false, badge: '待 PDA 接口' }, + { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false, badge: '待 PDA 接口' }, + { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false, badge: '待 PDA 接口' } ] }, { key: 'relabel', name: '换标作业', icon: '换', description: '换标收货、执行与复核', operations: [ - { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: true, badge: '原型' }, - { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: true, badge: '原型' }, - { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: true, badge: '原型' } + { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false, badge: '待 PDA 接口' }, + { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false, badge: '待 PDA 接口' }, + { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false, badge: '待 PDA 接口' } ] }, { key: 'query', name: '现场查询', icon: '查', description: '库存、库位与托盘查询', operations: [ - { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: true, badge: '原型' }, - { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: true, badge: '原型' }, - { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: true, badge: '原型' } + { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false, badge: '待 PDA 接口' }, + { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false, badge: '待 PDA 接口' }, + { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false, badge: '待 PDA 接口' } ] } ] diff --git a/src/manifest.json b/src/manifest.json index 3487754..298aa81 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,49 +1,74 @@ { - "name": "柏悦物流海外版", - "appid": "", - "description": "柏悦物流海外仓 PDA 作业端", - "versionName": "0.1.0", - "versionCode": "10", - "transformPx": false, - "app-plus": { - "usingComponents": true, - "nvueCompiler": "uni-app", - "nvueStyleCompiler": "uni-app", - "compilerVersion": 3, - "splashscreen": { - "alwaysShowBeforeRender": true, - "waiting": true, - "autoclose": true, - "delay": 0 - }, - "modules": {}, - "distribute": { - "android": { - "packagename": "com.baiyuelogistics.pda.overseas", - "permissions": [], - "abiFilters": ["armeabi-v7a", "arm64-v8a"] - }, - "ios": {}, - "sdkConfigs": {}, - "icons": { - "android": { - "hdpi": "static/app-icons/72x72.png", - "xhdpi": "static/app-icons/96x96.png", - "xxhdpi": "static/app-icons/144x144.png", - "xxxhdpi": "static/app-icons/192x192.png" + "name" : "柏悦物流 PDA 海外版", + "appid" : "__UNI__936D079", + "description" : "柏悦物流海外仓 PDA 作业端", + "versionName" : "0.0.1", + "versionCode" : 1, + "transformPx" : false, + "app-plus" : { + "usingComponents" : true, + "nvueCompiler" : "uni-app", + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 }, - "ios": { - "appstore": "static/app-icons/1024x1024.png" + "modules" : {}, + "distribute" : { + "android" : { + "packagename" : "com.baiyuelogistics.pda.overseas", + "permissions" : [], + "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ] + }, + "ios" : {}, + "sdkConfigs" : {}, + "icons" : { + "android" : { + "hdpi" : "static/app-icons/72x72.png", + "xhdpi" : "static/app-icons/96x96.png", + "xxhdpi" : "static/app-icons/144x144.png", + "xxxhdpi" : "static/app-icons/192x192.png" + }, + "ios" : { + "appstore" : "static/app-icons/1024x1024.png", + "ipad" : { + "app" : "static/app-icons/76x76.png", + "app@2x" : "static/app-icons/152x152.png", + "notification" : "static/app-icons/20x20.png", + "notification@2x" : "static/app-icons/40x40.png", + "proapp@2x" : "static/app-icons/167x167.png", + "settings" : "static/app-icons/29x29.png", + "settings@2x" : "static/app-icons/58x58.png", + "spotlight" : "static/app-icons/40x40.png", + "spotlight@2x" : "static/app-icons/80x80.png" + }, + "iphone" : { + "app@2x" : "static/app-icons/120x120.png", + "app@3x" : "static/app-icons/180x180.png", + "notification@2x" : "static/app-icons/40x40.png", + "notification@3x" : "static/app-icons/60x60.png", + "settings@2x" : "static/app-icons/58x58.png", + "settings@3x" : "static/app-icons/87x87.png", + "spotlight@2x" : "static/app-icons/80x80.png", + "spotlight@3x" : "static/app-icons/120x120.png" + } + } + } } - } - } - }, - "quickapp": {}, - "mp-weixin": { - "appid": "", - "setting": { "urlCheck": false }, - "usingComponents": true - }, - "uniStatistics": { "enable": false }, - "vueVersion": "3" + }, + "quickapp" : {}, + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "3" } diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue index e6c5d52..a4bddf1 100644 --- a/src/pages/home/home.vue +++ b/src/pages/home/home.vue @@ -9,7 +9,7 @@ {{ operation.name }} {{ operation.badge }} - 未开放 + {{ operation.badge || '未开放' }} diff --git a/src/pages/module/module.vue b/src/pages/module/module.vue index 6c9e967..d7034d7 100644 --- a/src/pages/module/module.vue +++ b/src/pages/module/module.vue @@ -4,8 +4,8 @@ {{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }} 业务功能 - - 拆柜收货已经海外网关接入真实接口;标记“原型”的库内与换标功能不会模拟提交成功。 + + 只有拆柜收货已接海外 PDA 真实接口;其余入口在专用接口、权限与仓库范围契约完成前保持禁用。 diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 856fd82..8377202 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -102,8 +102,8 @@ {{ toast.message }} @@ -114,7 +114,6 @@ import PortalHeader from '@/components/PortalHeader.vue' import ScanTargetInput from '@/components/ScanTargetInput.vue' import { isNoCurrentReceiving, isReceivingConflict, RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js' - import { getOperationPrototype } from '@/data/overseas-workbench.js' import { session, requireSession } from '@/stores/session.js' import { idempotencyKey, receiving, receivingPhase, receivingProgress, releaseIdempotencyKey, resetReceivingDraft, selectedInbound, setCurrentReceiving } from '@/stores/receiving.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' @@ -134,9 +133,9 @@ export default { components: { PortalHeader, ScanTargetInput, SessionSummary, InboundList }, - data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, busy: false, detailOpen: false, containerInput: '', cartonInput: '', skuInput: '', palletInput: '', packageCount: 1, prototypeInput: '', error: null, toast: { visible: false, message: '', type: '' }, photoSteps: RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) } }, + data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, busy: false, detailOpen: false, containerInput: '', cartonInput: '', skuInput: '', palletInput: '', packageCount: 1, error: null, toast: { visible: false, message: '', type: '' }, photoSteps: RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) } }, computed: { - prototype() { return getOperationPrototype(this.operationName) }, inboundMode() { return this.moduleKey === 'inbound' }, + inboundMode() { return this.moduleKey === 'inbound' }, candidates() { return receiving.candidates }, selected() { return receiving.selected }, current() { return receiving.current }, photoSlots() { return receiving.photoSlots }, selectedInboundCode: { get() { return receiving.selectedInboundCode }, set(value) { receiving.selectedInboundCode = value } }, resolvedCarton() { return receiving.resolvedCarton }, resolvedSku() { return receiving.resolvedSku }, resolvedPallet() { return receiving.resolvedPallet }, lastReceipt() { return receiving.lastReceipt }, @@ -156,7 +155,7 @@ statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, percent(received, total) { return total ? Math.min(100, Math.round(Number(received || 0) / Number(total) * 100)) : 0 }, destinationLabel(item) { return item?.destinationType === 'FBA' ? (item.fbaWarehouseCode || 'FBA') : (item?.destinationWarehouseName || item?.destinationWarehouseCode || '海外仓') }, startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null }, - handleHardwareScan(value) { if (!this.inboundMode) { this.prototypeInput = value; return this.prototypeScan() } if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'carton' && this.activeInbound?.destinationType === 'OVERSEAS') { this.cartonInput = value; return this.resolveCarton(value) } if (this.activeTarget === 'sku' && this.activeInbound?.destinationType === 'FBA') { this.skuInput = value; return this.resolveSku(value) } this.palletInput = value; return this.resolvePallet(value) }, + handleHardwareScan(value) { if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'carton' && this.activeInbound?.destinationType === 'OVERSEAS') { this.cartonInput = value; return this.resolveCarton(value) } if (this.activeTarget === 'sku' && this.activeInbound?.destinationType === 'FBA') { this.skuInput = value; return this.resolveSku(value) } this.palletInput = value; return this.resolvePallet(value) }, async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { this.error = error; playErrorSound(); this.showToast(error.message || `${action}失败`, 'error'); throw error } finally { this.busy = false } }, setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : (selectedInbound(value)?.destinationType === 'OVERSEAS' ? 'carton' : 'sku') }, async bootstrap() { this.loading = true; this.error = null; this.detailOpen = false; try { const current = await getCurrentReceiving(); setCurrentReceiving(current); this.setScanTargetForCurrent(current) } catch (error) { if (isNoCurrentReceiving(error)) { resetReceivingDraft(); await this.loadCandidates(false) } else this.error = error } finally { this.loading = false } }, @@ -174,7 +173,6 @@ async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, skuCode: this.resolvedSku.skuCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.resolvedCarton.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; setCurrentReceiving(await getCurrentReceiving()); receiving.resolvedCarton = null; receiving.resolvedSku = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.skuInput = ''; this.palletInput = ''; const inbound = selectedInbound(); this.packageCount = Math.max(1, Number(inbound?.pendingPackages || 1)); receiving.activeScanTarget = inbound?.destinationType === 'OVERSEAS' ? 'carton' : 'sku'; playSuccessSound() } catch (_) {} }, async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', () => completeCurrentReceiving({ idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value) } catch (_) {} }, async finishAndReturn() { resetReceivingDraft(); await this.loadCandidates(false) }, - prototypeScan() { if (!String(this.prototypeInput || '').trim()) return this.showToast('请先扫描或输入编码', 'warning'); this.showToast('该模块尚未接入真实业务接口', 'warning') }, showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2600) } } } @@ -182,5 +180,6 @@ diff --git a/src/stores/receiving.js b/src/stores/receiving.js index b70aa92..ef8cd42 100644 --- a/src/stores/receiving.js +++ b/src/stores/receiving.js @@ -1,5 +1,5 @@ import { reactive } from 'vue' -import { RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js' +import { RECEIVING_PHOTO_ROLES } from '../api/receiving-adapter.js' export const receiving = reactive({ candidates: [], selected: null, current: null, photoSlots: [null, null, null], @@ -53,6 +53,11 @@ export function resetReceivingDraft({ keepCurrent = false } = {}) { idempotencyKeys.clear() } +export function resetReceivingSession() { + receiving.candidates = [] + resetReceivingDraft() +} + export function idempotencyKey(action, payload = {}) { const signature = `${action}:${JSON.stringify(payload)}` if (!idempotencyKeys.has(signature)) idempotencyKeys.set(signature, `pda-${Date.now()}-${Math.random().toString(16).slice(2)}`) diff --git a/src/stores/session.js b/src/stores/session.js index e1238a4..5a4ae74 100644 --- a/src/stores/session.js +++ b/src/stores/session.js @@ -1,5 +1,6 @@ import { reactive } from 'vue' import { OVERSEAS_API_ROOT } from '../config/runtime.js' +import { resetReceivingSession } from './receiving.js' const SESSION_KEY = 'overseas-pda-auth-session' export const PDA_ROLE = 'WAREHOUSE_PICKER' @@ -158,6 +159,7 @@ export function signOut() { session.tenantId = '' session.apiBase = OVERSEAS_API_ROOT if (typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(SESSION_KEY) + resetReceivingSession() } export function requireSession() { diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs index d7b3352..2f59026 100644 --- a/tests/project-contract.test.mjs +++ b/tests/project-contract.test.mjs @@ -8,7 +8,7 @@ test('overseas PDA is an independent Vue 3 app with separate packaging identity' const [manifest, pages, pkg] = await Promise.all([ read('../src/manifest.json'), read('../src/pages.json'), read('../package.json') ]) - assert.match(manifest, /"name"\s*:\s*"柏悦物流海外版"/) + assert.match(manifest, /"name"\s*:\s*"柏悦物流\s*PDA\s*海外版"/) assert.match(manifest, /"packagename"\s*:\s*"com\.baiyuelogistics\.pda\.overseas"/) assert.match(manifest, /"vueVersion"\s*:\s*"3"/) assert.doesNotMatch(pages, /pages\/picking|pages\/tasks/) @@ -41,3 +41,13 @@ test('login has no committed demo credentials and inbound operation implements t assert.match(overseas, /replaceTokens|refreshOverseasToken/) assert.match(overseas, /requestId/) }) + +test('unimplemented warehouse operations cannot submit prototype scans', async () => { + const [operation, home, module] = await Promise.all([ + read('../src/pages/operation/operation.vue'), read('../src/pages/home/home.vue'), read('../src/pages/module/module.vue') + ]) + assert.doesNotMatch(operation, /prototypeScan|prototypeInput/) + assert.match(operation, /功能暂未开放|扫码不会提交/) + assert.match(home, /operation\.badge/) + assert.match(module, /专用接口、权限与仓库范围契约完成前保持禁用/) +}) diff --git a/tests/session-auth.test.mjs b/tests/session-auth.test.mjs index 29f6ff9..db5885b 100644 --- a/tests/session-auth.test.mjs +++ b/tests/session-auth.test.mjs @@ -5,6 +5,7 @@ import { PDA_AUTHORITY, PDA_ROLE, replaceTokens, restoreSession, session, decodeAccessToken, signInWithTokens, signOut, validatePdaTokenPair } from '../src/stores/session.js' +import { receiving } from '../src/stores/receiving.js' const future = () => Math.floor(Date.now() / 1000) + 3600 const jwt = (claims) => [ @@ -114,3 +115,33 @@ test('restore rebuilds identity from claims and removes expired or tampered stor assert.equal(restoreSession(), false) assert.equal(values.has('overseas-pda-auth-session'), false) }) + +test('sign out clears tokens, frozen warehouse identity, and every receiving draft', () => { + const values = storage(); signOut() + signInWithTokens(tokenPair()) + receiving.candidates = [{ containerNo: 'CONT-1' }] + receiving.current = { containerNo: 'CONT-1', status: 'RECEIVING' } + receiving.selected = receiving.current + receiving.photoSlots = [{ fileId: '1' }, null, null] + receiving.selectedInboundCode = 'IN-1' + receiving.resolvedCarton = { cartonMark: 'BOX-1' } + receiving.resolvedSku = { skuCode: 'SKU-1' } + receiving.resolvedPallet = { palletCode: 'PALLET-1' } + receiving.lastReceipt = { acceptedPackages: 1 } + + signOut() + assert.equal(session.currentUser, null) + assert.equal(session.token, '') + assert.equal(session.domesticToken, '') + assert.equal(session.refreshToken, '') + assert.equal(values.has('overseas-pda-auth-session'), false) + assert.deepEqual(receiving.candidates, []) + assert.equal(receiving.current, null) + assert.equal(receiving.selected, null) + assert.deepEqual(receiving.photoSlots, [null, null, null]) + assert.equal(receiving.selectedInboundCode, '') + assert.equal(receiving.resolvedCarton, null) + assert.equal(receiving.resolvedSku, null) + assert.equal(receiving.resolvedPallet, null) + assert.equal(receiving.lastReceipt, null) +}) diff --git a/tests/workbench-availability.test.mjs b/tests/workbench-availability.test.mjs new file mode 100644 index 0000000..750e800 --- /dev/null +++ b/tests/workbench-availability.test.mjs @@ -0,0 +1,17 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { MODULES } from '../src/data/overseas-workbench.js' + +test('only operations backed by a dedicated overseas PDA contract are enabled', () => { + const operations = MODULES.flatMap((module) => module.operations.map((operation) => ({ module: module.key, ...operation }))) + const enabled = operations.filter((operation) => operation.available) + assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [{ module: 'inbound', name: '拆柜收货' }]) + + const pendingNames = ['库存盘点', '商品移库', '托盘管理', '换标收货', '换标执行', '换标复核', '库存查询', '库位查询', '托盘查询'] + for (const name of pendingNames) { + const operation = operations.find((item) => item.name === name) + assert.equal(operation?.available, false, `${name} must fail closed until its PDA API exists`) + assert.equal(operation?.badge, '待 PDA 接口') + } +}) +