feat(overseas-pda): prefer business identifiers

This commit is contained in:
2026-08-30 17:38:15 +08:00
parent 4810b44452
commit 49aad23098
7 changed files with 89 additions and 11 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<template><view v-if="user" class="page-shell portal-page"><PortalHeader title="作业记录" :user="user" /><view class="portal-content"><view class="summary-card"><view><text class="summary-number">0</text><text class="summary-label">今日完成</text></view><view><text class="summary-number pending-color">0</text><text class="summary-label">进行中</text></view></view><text class="group-title">最近作业</text><view class="history-empty"><text class="history-empty-icon">▤</text><text class="history-empty-title">暂无海外仓作业记录</text><text class="history-empty-desc">接入海外仓真实任务后,完成记录会显示在这里</text></view></view></view></template>
<template><view v-if="user" class="page-shell portal-page"><PortalHeader title="作业记录" :user="user" /><view class="portal-content"><view class="summary-card"><view><text class="summary-number">0</text><text class="summary-label">今日完成</text></view><view><text class="summary-number pending-color">0</text><text class="summary-label">进行中</text></view></view><text class="group-title">最近作业</text><view class="history-empty"><text class="history-empty-icon">▤</text><text class="history-empty-title">暂无海外仓作业记录</text><text class="history-empty-desc">当前暂无可查看的作业记录</text></view></view></view></template>
<script>
import PortalHeader from '@/components/PortalHeader.vue'; import { session, requireSession } from '@/stores/session.js'
export default { components: { PortalHeader }, data() { return { user: session.currentUser } }, onShow() { if (!requireSession()) return; this.user = session.currentUser } }
+1 -4
View File
@@ -47,10 +47,7 @@
return name && !/^[a-z0-9._-]+$/i.test(name) ? name : '仓库作业员'
},
warehouseLabel() {
if (this.user?.warehouseName) return this.user.warehouseCode ? `${this.user.warehouseName} · ${this.user.warehouseCode}` : this.user.warehouseName
const warehouseId = String(this.user?.warehouseId || '')
if (warehouseId) return `当前作业仓 · ${warehouseId.slice(-6)}`
return this.user?.warehouse || '仓库由当前账号自动绑定'
return String(this.user?.warehouseName || '').trim() || '海外仓未关联'
},
greeting() {
const hour = new Date().getHours()
+1 -1
View File
@@ -100,7 +100,7 @@
<template v-else>
<view class="receiving-heading"><view><text class="receiving-title">{{ operationName }}</text><text class="receiving-subtitle">当前功能暂未开放</text></view></view>
<view class="blocked-card"><uni-icons type="locked" color="#8090a3" size="36" /><text class="blocked-title">功能暂未开放</text><text>现有平台、客户或内部接口不能由仓库作业员直接调用。扫码不会提交,也不会产生模拟成功结果。</text></view>
<view class="blocked-card"><uni-icons type="locked" color="#8090a3" size="36" /><text class="blocked-title">功能暂未开放</text><text>当前作业暂未开放,扫码不会提交,请返回工作台选择可用功能。</text></view>
</template>
</view>
<view v-if="toast.visible" class="app-toast" :class="toast.type" role="status">{{ toast.message }}</view>
+3 -2
View File
@@ -2,7 +2,7 @@
<view v-if="user" class="page-shell portal-page">
<PortalHeader title="个人中心" :user="user" :show-profile="false" />
<view class="portal-content profile-content">
<view class="profile-card"><view class="profile-avatar">{{ avatar }}</view><view><text class="profile-name">{{ displayName }}</text><text class="profile-role">{{ user.role }} · {{ user.id }}</text></view></view>
<view class="profile-card"><view class="profile-avatar">{{ avatar }}</view><view><text class="profile-name">{{ displayName }}</text><text class="profile-role">{{ roleLabel }}</text></view></view>
<view class="profile-list">
<view class="profile-row"><text>作业仓库</text><text class="profile-value">{{ warehouseDisplay }}</text></view>
<view class="profile-row"><text>当前班次</text><text class="profile-value">以海外仓排班为准</text></view>
@@ -30,7 +30,8 @@
computed: {
displayName() { const name = String(this.user?.name || ''); return name && !/^[a-z0-9._-]+$/i.test(name) ? name : '仓库作业员' },
avatar() { return this.displayName.slice(0, 1).toUpperCase() },
warehouseDisplay() { return this.user?.warehouseName ? (this.user.warehouseCode ? `${this.user.warehouseName} · ${this.user.warehouseCode}` : this.user.warehouseName) : (this.user?.warehouse || '正在读取仓库主档') },
roleLabel() { return '海外仓作业员' },
warehouseDisplay() { return String(this.user?.warehouseName || '').trim() || '海外仓未关联' },
languageLabel() { return LOCALE_OPTIONS.find((item) => item.value === this.locale)?.label || 'English' }
},
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadAppVersion(); this.loadWarehouseProfile() },
+2 -2
View File
@@ -74,7 +74,7 @@ function userFromClaims(pair, saved = {}) {
name: pair.domestic.username || '仓库作业员',
role: PDA_ROLE,
roles: [...pair.overseas.roles],
warehouse: sameWarehouse && saved.warehouseName ? saved.warehouseName : `海外仓 #${pair.warehouseId}`,
warehouse: sameWarehouse && saved.warehouseName ? saved.warehouseName : '',
warehouseId: pair.warehouseId,
warehouseCode: sameWarehouse ? (saved.warehouseCode || '') : '',
warehouseName: sameWarehouse ? (saved.warehouseName || '') : ''
@@ -142,7 +142,7 @@ export function applyWarehouseProfile(profile) {
if (!warehouseId || warehouseId !== String(session.currentUser.warehouseId)) throw new Error('仓库主档与当前登录仓库不一致')
session.currentUser = {
...session.currentUser,
warehouse: profile.warehouseName || profile.warehouseCode || `海外仓 #${warehouseId}`,
warehouse: profile.warehouseName || '',
warehouseCode: String(profile.warehouseCode || ''),
warehouseName: String(profile.warehouseName || '')
}
+80
View File
@@ -0,0 +1,80 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
const template = (source) => source.match(/<template>([\s\S]*?)<\/template>/)?.[1] || ''
const interpolations = (source) => [...template(source).matchAll(/\{\{([\s\S]*?)\}\}/g)].map((match) => match[1]).join('\n')
test('all registered user-facing pages avoid rendering internal identifiers', async () => {
const paths = [
'../src/pages/login/login.vue', '../src/pages/home/home.vue', '../src/pages/module/module.vue',
'../src/pages/operation/operation.vue', '../src/pages/pallet/pallet.vue', '../src/pages/profile/profile.vue',
'../src/pages/history/history.vue', '../src/pages/scanner-test/scanner-test.vue',
'../src/components/PortalHeader.vue', '../src/components/PopupHeader.vue',
'../src/components/ReceivingSessionSummary.vue', '../src/components/ScanTargetInput.vue'
]
const visibleExpressions = (await Promise.all(paths.map(read))).map(interpolations).join('\n')
for (const field of [
'orderId', 'inboundId', 'planId', 'containerBookingId', 'sourceContainerBookingId',
'warehouseId', 'palletId', 'locationId', 'positionId', 'user.id'
]) assert.doesNotMatch(visibleExpressions, new RegExp(field.replace('.', '\\.')))
})
test('operator and warehouse cards never fall back to internal IDs', async () => {
const [home, profile, session] = await Promise.all([
read('../src/pages/home/home.vue'),
read('../src/pages/profile/profile.vue'),
read('../src/stores/session.js')
])
assert.doesNotMatch(template(home), /warehouseId|warehouseCode|user\.warehouse/)
assert.doesNotMatch(home, /warehouseId\.slice|当前作业仓\s*·/)
assert.match(home, /warehouseName \|\| ''[\s\S]*海外仓未关联/)
assert.doesNotMatch(template(profile), /user\.id|user\.role|warehouseId|warehouseCode|user\.warehouse/)
assert.match(profile, /roleLabel\(\) \{ return '海外仓作业员' \}/)
assert.match(profile, /warehouseName \|\| ''[\s\S]*海外仓未关联/)
assert.doesNotMatch(session, /海外仓 #\$\{|warehouse:\s*profile\.warehouseCode/)
})
test('operation templates expose business references only and keep diagnostic evidence', async () => {
const [operation, pallet, summary, apiFiles] = await Promise.all([
read('../src/pages/operation/operation.vue'),
read('../src/pages/pallet/pallet.vue'),
read('../src/components/ReceivingSessionSummary.vue'),
Promise.all([
read('../src/api/inbound.js'), read('../src/api/pallet.js'),
read('../src/api/overseas.js'), read('../src/api/receiving-adapter.js')
]).then((values) => values.join('\n'))
])
const forbiddenDisplayFields = [
'orderId', 'inboundId', 'planId', 'containerBookingId', 'sourceContainerBookingId',
'warehouseId', 'palletId', 'locationId', 'positionId'
]
for (const field of forbiddenDisplayFields) {
assert.doesNotMatch(template(operation), new RegExp(`\\b${field}\\b`))
assert.doesNotMatch(template(summary), new RegExp(`\\b${field}\\b`))
}
for (const field of ['orderId', 'inboundId', 'planId', 'containerBookingId', 'sourceContainerBookingId', 'warehouseId', 'palletId', 'locationId']) {
assert.doesNotMatch(template(pallet), new RegExp(`\\b${field}\\b`))
}
for (const businessField of ['containerNo', 'inboundCode', 'cartonMark', 'palletCode']) {
assert.match(operation + pallet + summary, new RegExp(businessField))
}
assert.match(operation, /requestId[\s\S]*traceId/)
assert.match(pallet, /requestId[\s\S]*traceId/)
assert.doesNotMatch(apiFiles, /\/api\/platform\/v1\/|inbound-code-relationships/)
})
test('missing user-visible relationships use explicit empty labels instead of ID-shaped fallbacks', async () => {
const [summary, pallet] = await Promise.all([
read('../src/components/ReceivingSessionSummary.vue'),
read('../src/pages/pallet/pallet.vue')
])
assert.match(summary, /overseasWarehouseName \|\| '—'/)
assert.match(summary, /unique\.length \? unique : \['—'\]/)
assert.match(pallet, /inboundCode \|\| '无入仓码'/)
assert.match(pallet, /locationCode \|\| '未绑定库位'/)
})
+1 -1
View File
@@ -51,7 +51,7 @@ test('valid platform warehouse picker tokens freeze the same operator and wareho
const user = signInWithTokens(tokens)
assert.deepEqual(user.roles, [PDA_ROLE])
assert.equal(user.warehouseId, '900')
assert.equal(user.warehouse, '海外仓 #900')
assert.equal(user.warehouse, '')
assert.equal(session.tenantId, '0')
assert.equal(session.token, tokens.overseasAccessToken)
})