From 4ee8ac3ee7602c6326e6bf760d6d59f69706b0ab Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 20:41:51 +0800 Subject: [PATCH 1/3] fix(deploy): prevent stale SPA entry caching --- nginx/default.conf.template | 14 ++++++++++++++ src/deployment/nginxConfig.test.mjs | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 2c714ab..68ea42b 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -11,6 +11,19 @@ server { return 200 "ok\n"; } + # SPA 入口每次重新校验,避免发布后浏览器继续运行旧资源清单。 + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + } + + # Vite 产物文件名带内容哈希,可安全长期缓存。 + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + # 前端统一请求 /api,保持原始路径转发给 Gateway。 location /api/ { client_max_body_size 55m; @@ -36,6 +49,7 @@ server { # 支持 Vue Router history 路由直接刷新。 location / { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; try_files $uri $uri/ /index.html; } } diff --git a/src/deployment/nginxConfig.test.mjs b/src/deployment/nginxConfig.test.mjs index c32b26a..aab81bb 100644 --- a/src/deployment/nginxConfig.test.mjs +++ b/src/deployment/nginxConfig.test.mjs @@ -13,6 +13,12 @@ test('allows 50 MiB uploads with multipart overhead through both API proxies', ( assert.equal(config.match(/client_max_body_size 55m;/g)?.length, 2) }) +test('does not cache the SPA entry while keeping hashed assets immutable', () => { + assert.match(config, /location = \/index\.html[\s\S]*no-store, no-cache, must-revalidate/) + assert.match(config, /location \/assets\/[\s\S]*max-age=31536000, immutable/) + assert.match(config, /location \/ \{[\s\S]*no-store, no-cache, must-revalidate[\s\S]*try_files \$uri \$uri\/ \/index\.html/) +}) + test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => { assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/) assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/) From a7c36771e422f686c12dd7de0c282a0254ea3677 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 20:49:13 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 42 +++++++++++++++++------ src/modules/auth/store/auth.ts | 9 ----- src/modules/auth/views/AuthViews.test.mjs | 9 ++++- src/router/index.ts | 3 +- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/App.vue b/src/App.vue index f38f8d2..77311e0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,32 +1,52 @@ diff --git a/src/modules/auth/store/auth.ts b/src/modules/auth/store/auth.ts index 574c1f4..76e63fb 100644 --- a/src/modules/auth/store/auth.ts +++ b/src/modules/auth/store/auth.ts @@ -223,15 +223,6 @@ export const useAuthStore = defineStore('client-auth', () => { } window.addEventListener('cc-client-session-expired', clearSession) - window.addEventListener('message', (event: MessageEvent) => { - const configuredAdminOrigin = String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim() - const expectedAdminOrigin = configuredAdminOrigin || window.location.origin - const message = event.data as { source?: string; type?: string; code?: string } | null - if (event.source !== window.opener || event.origin !== expectedAdminOrigin || message?.source !== 'chongchong-admin' || message.type !== 'impersonation-ticket' || !message.code) return - void acceptImpersonationCode(message.code) - .then(() => window.location.replace('/')) - .catch(() => window.location.replace('/login?impersonation=failed')) - }) window.addEventListener('storage', (event) => { if (event.key === 'cc-client-access-token' || event.key === 'cc-client-refresh-token') applyStoredSession() }) diff --git a/src/modules/auth/views/AuthViews.test.mjs b/src/modules/auth/views/AuthViews.test.mjs index 6adc8fb..e9d911c 100644 --- a/src/modules/auth/views/AuthViews.test.mjs +++ b/src/modules/auth/views/AuthViews.test.mjs @@ -13,6 +13,8 @@ const httpSource = readFileSync(new URL('../../../api/http.ts', import.meta.url) const sessionSource = readFileSync(new URL('../../../api/session.ts', import.meta.url), 'utf8') const authApiSource = readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8') const mainSource = readFileSync(new URL('../../../main.ts', import.meta.url), 'utf8') +const appSource = readFileSync(new URL('../../../App.vue', import.meta.url), 'utf8') +const routerSource = readFileSync(new URL('../../../router/index.ts', import.meta.url), 'utf8') const stylesSource = readFileSync(new URL('../../../styles/index.css', import.meta.url), 'utf8') test('uses one prototype-inspired responsive shell for login and registration', () => { @@ -105,7 +107,12 @@ test('uses real auth endpoints and single-flight refresh handling', () => { test('handles impersonation exchange as public authentication and exposes failures', () => { assert.match(httpSource, /publicAuthPaths = \[[^\]]*'\/auth\/impersonations\/exchange'/) - assert.match(authStoreSource, /\/login\?impersonation=failed/) + assert.match(appSource, /impersonationOrigin/) + assert.match(appSource, /await auth\.acceptImpersonationCode\(message\.code\)/) + assert.match(appSource, /type: 'impersonation-complete'/) + assert.match(appSource, /type: 'impersonation-failed'/) + assert.doesNotMatch(authStoreSource, /message\.type !== 'impersonation-ticket'/) + assert.match(routerSource, /isImpersonationHandoff[\s\S]*!isImpersonationHandoff/) assert.match(loginSource, /route\.query\.impersonation === 'failed'/) assert.match(loginSource, /代登录失败或授权已失效,请返回管理后台重新发起/) }) diff --git a/src/router/index.ts b/src/router/index.ts index 0280d8c..679f504 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -32,7 +32,8 @@ router.beforeEach(async (to) => { if (to.meta.requiresAuth && !auth.isAuthenticated) { return { name: 'login', query: { redirect: to.fullPath } } } - if ((to.name === 'login' || to.name === 'register') && auth.isAuthenticated) { + const isImpersonationHandoff = to.name === 'login' && typeof to.query.impersonationOrigin === 'string' + if ((to.name === 'login' || to.name === 'register') && auth.isAuthenticated && !isImpersonationHandoff) { return getFirstAccessiblePath(auth.permissionCodes) } const permission = typeof to.meta.permission === 'string' ? to.meta.permission : undefined From 6780228f86bd28b6813aea156dbb82767ac25f2e Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 3 Sep 2026 10:46:14 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/first-leg/api/inboundBilling.ts | 35 ++++++++++++ .../components/InboundBillingPanel.test.mjs | 6 +- .../components/InboundBillingPanel.vue | 56 +++++++++++++++++-- src/modules/first-leg/types/inboundBilling.ts | 38 +++++++++++++ .../views/FirstLegOrdersView.test.mjs | 4 +- .../first-leg/views/FirstLegOrdersView.vue | 19 +++++-- 6 files changed, 145 insertions(+), 13 deletions(-) diff --git a/src/modules/first-leg/api/inboundBilling.ts b/src/modules/first-leg/api/inboundBilling.ts index fef4283..befba7b 100644 --- a/src/modules/first-leg/api/inboundBilling.ts +++ b/src/modules/first-leg/api/inboundBilling.ts @@ -9,6 +9,9 @@ import type { interface MerchantInboundBillingWireResponse { inbound: MerchantInboundResult + measurementSummary: MerchantInboundBilling['measurementSummary'] + measurementItems: MerchantInboundBilling['measurementItems'] + billingBasis: MerchantInboundBilling['billingBasis'] latestFee: MerchantInboundFeeSnapshot } @@ -101,6 +104,35 @@ function sanitizeFee(fee: MerchantInboundFeeSnapshot): MerchantInboundFeeSnapsho } } +function sanitizeMeasurementSummary(item: MerchantInboundBilling['measurementSummary']): MerchantInboundBilling['measurementSummary'] { + return { + measurementId: item.measurementId == null ? null : String(item.measurementId), + sourceType: item.sourceType, + measurementStatus: item.measurementStatus, + actualPackageCount: item.actualPackageCount, + actualGrossWeightKg: item.actualGrossWeightKg, + actualNetWeightKg: item.actualNetWeightKg, + actualVolumeCbm: item.actualVolumeCbm, + volumeWeightKg: item.volumeWeightKg, + actualGoodsName: item.actualGoodsName, + } +} + +function sanitizeMeasurementItem(item: MerchantInboundBilling['measurementItems'][number]): MerchantInboundBilling['measurementItems'][number] { + return { + id: String(item.id), + itemNo: item.itemNo, + boxSpecification: item.boxSpecification, + packageCount: item.packageCount, + lengthCm: item.lengthCm, + widthCm: item.widthCm, + heightCm: item.heightCm, + unitVolumeCbm: item.unitVolumeCbm, + totalVolumeCbm: item.totalVolumeCbm, + volumeWeightKg: item.volumeWeightKg, + } +} + function sanitizeInboundBilling(response: MerchantInboundBillingWireResponse): MerchantInboundBilling { return { inbound: { @@ -108,6 +140,9 @@ function sanitizeInboundBilling(response: MerchantInboundBillingWireResponse): M publicRemark: response.inbound.publicRemark, palletLocations: response.inbound.palletLocations.map(sanitizePalletLocation), }, + measurementSummary: sanitizeMeasurementSummary(response.measurementSummary), + measurementItems: (response.measurementItems || []).map(sanitizeMeasurementItem), + billingBasis: response.billingBasis, latestFee: sanitizeFee(response.latestFee), } } diff --git a/src/modules/first-leg/components/InboundBillingPanel.test.mjs b/src/modules/first-leg/components/InboundBillingPanel.test.mjs index 083ba22..4669d84 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.test.mjs +++ b/src/modules/first-leg/components/InboundBillingPanel.test.mjs @@ -14,13 +14,15 @@ test('renders loading, pending, isolated error, and published states', () => { assert.match(source, /\$emit\('retry'\)/) }) -test('shows the confirmed merchant inbound and fee fields without write actions', () => { +test('shows the confirmed merchant inbound, measurement details and customer confirmation entry', () => { for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '基础费用', '附加费用', '费用合计', '平台备注']) { assert.match(source, new RegExp(label)) } assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/) assert.match(source, /formatConversionPrecision\(/) - assert.doesNotMatch(source, /确认费用|调整费用|发送费用|下载照片/) + assert.match(source, /确认费用/) + assert.match(source, /measurementItems/) + assert.doesNotMatch(source, /调整费用|发送费用|下载照片/) assert.doesNotMatch(source, /internalNotes|statusHistory|files/) assert.match(source, /latestFee\.ruleCode/) assert.match(source, /latestFee\.rateCode/) diff --git a/src/modules/first-leg/components/InboundBillingPanel.vue b/src/modules/first-leg/components/InboundBillingPanel.vue index 29f3fa3..a857566 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.vue +++ b/src/modules/first-leg/components/InboundBillingPanel.vue @@ -1,4 +1,5 @@ + + +
+
测量总箱数
{{ formatInboundNumber(props.data.measurementSummary.actualPackageCount, 0) }}
+
测量总毛重
{{ formatInboundMeasure(props.data.measurementSummary.actualGrossWeightKg, 'kg') }}
+
测量总体积
{{ formatInboundMeasure(props.data.measurementSummary.actualVolumeCbm, 'm³') }}
+
测量总材积重
{{ formatInboundMeasure(props.data.measurementSummary.volumeWeightKg, 'kg') }}
+
计费体积
{{ formatInboundMeasure(props.data.billingBasis.chargeableVolumeCbm, 'm³') }}
+
计费重量
{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}
+
+ + + + + + + + + + + + +