From 255e012c1bfac70853a7ba9d3ee65c6991add459 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 11:59:58 +0800 Subject: [PATCH 01/33] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index df7c55f..c889e1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ | 业务 | 模块入口 | 联查模块或服务端 | |---|---|---| | 登录、注册与会话 | `src/modules/auth/` | Backend `auth/`、`iam/` | -| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`file/` | +| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`auth/`、`file/`;公司联系电话/邮箱是企业资料,不作为登录标识;成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计。审核必填资质仅营业执照与统一社会信用代码,法人身份证选填 | | 工作台与系统入口 | `src/modules/dashboard/`、`src/modules/system/` | `src/navigation/clientNavigation.ts`、`src/router/index.ts` | | 企业成员消息收件箱 | `src/modules/notifications/` | Backend `iam/src/main/java/com/baiyue/iam/notification/`;列表、详情、已读与未读数契约见 `iam/src/main/resources/openapi/notification-v1.yaml` | | 头程下单、订单、入仓码业务关系、箱唛与履约进度 | `src/modules/first-leg/`;列表从 `views/FirstLegOrdersView.vue` 进入,点击入仓码由 `components/InboundCodeAssociationDialog.vue`、`api/inboundCodeAssociations.ts` 查询订单、客户和目的仓关联摘要;箱唛下载从 `api/firstLeg.ts` 进入 | Backend `order/` 的客户隔离 `InboundCodeAssociationService`、`order-v1.yaml`,以及 `firstleg/`、`warehouse/`;不得用内部 ID 补齐缺失业务编号 | From 517b6718f005b3bb0610281f8266cd408f4ee6c5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 13:34:56 +0800 Subject: [PATCH 02/33] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/CompanyProfileView.vue | 20 +++++++------------ .../views/TenantAdministrationViews.test.mjs | 8 +++++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/modules/tenant-management/views/CompanyProfileView.vue b/src/modules/tenant-management/views/CompanyProfileView.vue index e9ea571..ea52697 100644 --- a/src/modules/tenant-management/views/CompanyProfileView.vue +++ b/src/modules/tenant-management/views/CompanyProfileView.vue @@ -83,7 +83,6 @@ const normalizedQualificationCertificateNo = computed(() => qualificationCertifi const draftCreditCodeValid = computed(() => unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value)) const businessLicenseFileSelected = computed(() => Boolean(qualificationDraftItem('BUSINESS_LICENSE')?.file)) const legalPersonIdFileSelected = computed(() => Boolean(qualificationDraftItem('LEGAL_PERSON_ID')?.file)) -const standaloneLegalPersonIdUnsupported = computed(() => legalPersonIdFileSelected.value && !businessLicenseFileSelected.value) const requiredPackageChangeSelected = computed(() => businessLicenseFileSelected.value) const requiredPackageFilesSelected = computed(() => businessLicenseFileSelected.value) const qualificationCreditCodeReady = computed(() => requiredPackageChangeSelected.value @@ -122,11 +121,9 @@ const qualificationUploadDisabledReason = computed(() => { const selectedQualificationCount = computed(() => qualificationDrafts.filter((item) => Boolean(item.file)).length) const canSaveQualificationBatch = computed(() => canUploadQualification.value && selectedQualificationCount.value > 0 - && !standaloneLegalPersonIdUnsupported.value && requiredPackageComplete.value) const qualificationSaveDisabledReason = computed(() => { if (!canUploadQualification.value) return qualificationUploadDisabledReason.value - if (standaloneLegalPersonIdUnsupported.value) return '法人身份证为选填项;当前文件服务尚未开放单独保存,请取消本次选择后保存其他资质。' if (!requiredPackageComplete.value) return `请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}` if (!selectedQualificationCount.value) return '当前没有需要保存的变更' return '' @@ -340,20 +337,15 @@ async function submitQualification() { ElMessage.warning(qualificationUploadDisabledReason.value) return } - if (standaloneLegalPersonIdUnsupported.value) { - const legalPersonId = qualificationDraftItem('LEGAL_PERSON_ID') - const message = '当前文件服务尚未开放法人身份证单独上传或替换,该选填文件未提交。' - if (legalPersonId) { legalPersonId.error = message; legalPersonId.uploadState = 'error' } - ElMessage.warning(message) - return - } if (!requiredPackageComplete.value) { const businessLicense = qualificationDraftItem('BUSINESS_LICENSE') if (businessLicense && !qualificationCreditCodeReady.value) businessLicense.error = qualificationCreditCodeError.value ElMessage.warning(`请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}`) return } - const optionalItems = qualificationDrafts.filter((item) => !item.required && item.value !== 'LEGAL_PERSON_ID' && item.file) + const optionalItems = qualificationDrafts.filter((item) => !item.required + && item.file + && (item.value !== 'LEGAL_PERSON_ID' || !requiredPackageChangeSelected.value)) const includedLegalPersonId = legalPersonIdFileSelected.value qualificationSubmitting.value = true let uploadedCount = 0 @@ -430,8 +422,10 @@ async function submitQualification() { } qualificationOpen.value = false if (refreshed) ElMessage.success(includedLegalPersonId - ? '营业执照、统一社会信用代码和法人身份证已保存,尚未提交审核' - : '营业执照和统一社会信用代码已保存,尚未提交审核') + ? (requiredPackageChangeSelected.value + ? '营业执照、统一社会信用代码和法人身份证已保存,尚未提交审核' + : '法人身份证已保存,尚未提交审核') + : '资质文件已保存,尚未提交审核') else ElMessage.warning('资质文件已保存,但最新状态刷新失败,请点击“重新加载”确认状态') } finally { qualificationSubmitting.value = false diff --git a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs index 2816e3d..2967085 100644 --- a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs +++ b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs @@ -73,12 +73,14 @@ test('implements company details, contact information and qualification workflow assert.match(profileSource, /requiredQualificationTypes\.every/) assert.doesNotMatch(profileSource, /四类必填资质/) assert.doesNotMatch(profileSource, /法人身份证需与本次营业执照一起保存/) - assert.match(profileSource, /const standaloneLegalPersonIdUnsupported = computed/) - assert.match(profileSource, /当前文件服务尚未开放法人身份证单独上传或替换,该选填文件未提交/) + assert.doesNotMatch(profileSource, /standaloneLegalPersonIdUnsupported/) + assert.doesNotMatch(profileSource, /当前文件服务尚未开放法人身份证单独上传或替换/) + assert.match(profileSource, /item\.value !== 'LEGAL_PERSON_ID' \|\| !requiredPackageChangeSelected\.value/) + assert.match(profileSource, /法人身份证已保存,尚未提交审核/) assert.match(profileSource, /qualificationUploadErrorMessage\(error\)/) assert.match(profileSource, /submitTenantQualificationReview/) assert.match(profileSource, /提交企业审核/) - assert.match(profileSource, /营业执照和统一社会信用代码已保存,尚未提交审核/) + assert.match(profileSource, /资质文件已保存,尚未提交审核/) assert.match(profileSource, /资质文件已保存,但最新状态刷新失败/) assert.match(profileSource, /企业资料已提交审核,但最新状态刷新失败/) assert.match(profileSource, /qualificationStatusMeta/) From 763687177414c2c0567c1efcf8ba1b456726d2a0 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 14:44:48 +0800 Subject: [PATCH 03/33] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index df7c55f..aa735c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ - 任何查询和变更都必须以服务端确认的当前租户为边界;绝不从可篡改的前端参数推断授权租户。 - 企业成员、角色和权限只能管理当前企业范围。前端隐藏不是安全控制,服务端必须重复校验租户与权限。 +- 注册时登录手机号同时初始化公司联系电话;注册完成后两者独立维护。企业资料只修改 `contactPhone`,个人资料或成员管理中的登录手机号由 IAM/Auth 同步并撤销该账号现有会话,不得在前端互相回填或联动提交。 - 认证统一经过 `src/modules/auth/store/auth.ts`、`src/api/http.ts`、`src/api/session.ts`;国内请求使用国内 Access Token,海外请求统一由 `src/api/overseasHttp.ts` 读取短时海外 Token。海外 Token 缺失或 401 时复用同一刷新编排并最多重放一次,页面不要复制令牌逻辑或持久化密码、敏感资质内容。 - 当前认证仍包含模拟数据。接入真实 API 后移除对应模拟分支,生产环境不得回退为模拟成功。 - 头程订单统一使用当前正式读写契约;不得继续扩展 `LEGACY_MANUAL`、旧系统入仓码、C02 不可用回退旧 API、历史部分响应兼容或其他新旧并跑逻辑。触及该链路时按根无兼容规则同步删除生产分支、类型、文案、Mock 和测试,不以现有代码或测试仍覆盖旧路径作为保留依据。 From 399243974b0e8b501c004392fc951b5bd894aca0 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 15:15:44 +0800 Subject: [PATCH 04/33] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c889e1e..dc442ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ | 业务 | 模块入口 | 联查模块或服务端 | |---|---|---| | 登录、注册与会话 | `src/modules/auth/` | Backend `auth/`、`iam/` | -| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`auth/`、`file/`;公司联系电话/邮箱是企业资料,不作为登录标识;成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计。审核必填资质仅营业执照与统一社会信用代码,法人身份证选填 | +| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`auth/`、`file/`;公司联系电话/邮箱是企业资料,不作为登录标识;成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随营业执照提交,也可在已有营业执照后单独补传或替换 | | 工作台与系统入口 | `src/modules/dashboard/`、`src/modules/system/` | `src/navigation/clientNavigation.ts`、`src/router/index.ts` | | 企业成员消息收件箱 | `src/modules/notifications/` | Backend `iam/src/main/java/com/baiyue/iam/notification/`;列表、详情、已读与未读数契约见 `iam/src/main/resources/openapi/notification-v1.yaml` | | 头程下单、订单、入仓码业务关系、箱唛与履约进度 | `src/modules/first-leg/`;列表从 `views/FirstLegOrdersView.vue` 进入,点击入仓码由 `components/InboundCodeAssociationDialog.vue`、`api/inboundCodeAssociations.ts` 查询订单、客户和目的仓关联摘要;箱唛下载从 `api/firstLeg.ts` 进入 | Backend `order/` 的客户隔离 `InboundCodeAssociationService`、`order-v1.yaml`,以及 `firstleg/`、`warehouse/`;不得用内部 ID 补齐缺失业务编号 | From 6d10251161b127d8e86e83ccc916053f80fd778d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 20:12:43 +0800 Subject: [PATCH 05/33] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E6=94=B6=E6=AC=BE=E6=96=B9=E5=BC=8F=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/finance/views/FinanceBillsView.test.mjs | 2 ++ src/modules/finance/views/FinanceBillsView.vue | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/finance/views/FinanceBillsView.test.mjs b/src/modules/finance/views/FinanceBillsView.test.mjs index 7ad3278..5437514 100644 --- a/src/modules/finance/views/FinanceBillsView.test.mjs +++ b/src/modules/finance/views/FinanceBillsView.test.mjs @@ -120,6 +120,8 @@ test('keeps actions first and exposes list, balance, detail, confirmation, recei for (const expected of ['未收余额', '账单详情', '确认账单', '客户确认信息', '收款记录', '暂无账单', '暂无收款记录', 'state-panel--error']) { assert.match(viewSource, new RegExp(expected)) } + assert.match(viewSource, /BANK_TRANSFER: '银行转账'/) + assert.match(viewSource, /paymentMethodLabel\(row\.method\)/) assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/) assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/) assert.match(viewSource, /size="small" type="success" plain/) diff --git a/src/modules/finance/views/FinanceBillsView.vue b/src/modules/finance/views/FinanceBillsView.vue index 87be647..95fd113 100644 --- a/src/modules/finance/views/FinanceBillsView.vue +++ b/src/modules/finance/views/FinanceBillsView.vue @@ -49,6 +49,11 @@ function formatDateTime(value: string | null | undefined) { return value ? value.replace('T', ' ').replace(/Z$/, '').slice(0, 16) : '-' } +function paymentMethodLabel(method: string | null | undefined) { + if (!method) return '—' + return ({ BANK_TRANSFER: '银行转账', CASH: '现金', OTHER: '其他' })[method] ?? '其他' +} + function statusLabel(status: FinanceBillStatus) { return ({ DRAFT: '草稿', ISSUED: '已出账', PARTIALLY_PAID: '部分收款', PAID: '已收清', VOIDED: '作废' })[status] ?? status } @@ -207,7 +212,7 @@ onMounted(() => { void loadBills(1) })

{{ detail.billNo }}

出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 ¥ {{ formatAmount(detail.outstandingAmount) }}

{{ statusLabel(detail.status) }}{{ confirmationLabel(detail.customerConfirmationStatus) }}
{{ confirmationLabel(detail.customerConfirmationStatus) }}

{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}

请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。

确认账单

费用明细

-

收款记录

暂无收款记录
+

收款记录

暂无收款记录

如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。

From 12424281c79c8b0e93eec8b59665db55bd00a542 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 20:25:45 +0800 Subject: [PATCH 06/33] feat(finance): link bill lines to inbound details --- src/modules/finance/api/adapters.ts | 2 ++ src/modules/finance/contracts.ts | 1 + src/modules/finance/views/FinanceBillsView.test.mjs | 8 ++++++-- src/modules/finance/views/FinanceBillsView.vue | 13 ++++++++++++- src/modules/first-leg/public.ts | 1 + 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/modules/finance/api/adapters.ts b/src/modules/finance/api/adapters.ts index 33ea5c8..10aaa1a 100644 --- a/src/modules/finance/api/adapters.ts +++ b/src/modules/finance/api/adapters.ts @@ -34,6 +34,7 @@ export interface FinanceBillLineWire { sourceVersion: number orderId: WireScalar | null inboundId: WireScalar | null + inboundCode: string | null feeItemCode: string feeItemName: string amount: WireScalar @@ -89,6 +90,7 @@ function mapBillLine(line: FinanceBillLineWire): FinanceBillLine { return { id: text(line.id), orderId: line.orderId == null ? null : text(line.orderId), + inboundCode: line.inboundCode?.trim() || null, feeNo: `${line.sourceType}:${text(line.sourceId)}/v${line.sourceVersion}`, feeName: line.feeItemName, amount: text(line.amount), diff --git a/src/modules/finance/contracts.ts b/src/modules/finance/contracts.ts index 0eaafac..142ac2f 100644 --- a/src/modules/finance/contracts.ts +++ b/src/modules/finance/contracts.ts @@ -39,6 +39,7 @@ export interface FinanceCustomerConfirmationResult { export interface FinanceBillLine { id: string orderId: string | null + inboundCode: string | null feeNo: string | null feeName: string amount: string diff --git a/src/modules/finance/views/FinanceBillsView.test.mjs b/src/modules/finance/views/FinanceBillsView.test.mjs index 5437514..43818db 100644 --- a/src/modules/finance/views/FinanceBillsView.test.mjs +++ b/src/modules/finance/views/FinanceBillsView.test.mjs @@ -92,7 +92,7 @@ test('maps nested bill detail and customer-safe receipt allocations', () => { receivedAmount: '40.00', outstandingAmount: '60.00', version: 3, customerConfirmationStatus: 'PENDING_CONFIRMATION', customerConfirmedByName: null, customerConfirmedAt: null, }, - lines: [{ id: 21, sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, orderId: 31, inboundId: 41, feeItemCode: 'FREIGHT', feeItemName: '运费', amount: '100.00', currency: 'CNY' }], + lines: [{ id: 21, sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, orderId: 31, inboundId: 41, inboundCode: 'NYEXX2608180003', feeItemCode: 'FREIGHT', feeItemName: '运费', amount: '100.00', currency: 'CNY' }], }) const receipts = financeAdapters.mapFinanceReceipts([{ id: 51, receiptNo: 'RCPT-51', receivedAmount: '40.00', receivedAt: '2026-08-12T10:00:00', @@ -101,7 +101,7 @@ test('maps nested bill detail and customer-safe receipt allocations', () => { }], '2') assert.equal(detail.billNo, 'B-2') - assert.deepEqual(detail.lines[0], { id: '21', orderId: '31', feeNo: 'INBOUND_FEE:81/v3', feeName: '运费', amount: '100.00', currencyCode: 'CNY' }) + assert.deepEqual(detail.lines[0], { id: '21', orderId: '31', inboundCode: 'NYEXX2608180003', feeNo: 'INBOUND_FEE:81/v3', feeName: '运费', amount: '100.00', currencyCode: 'CNY' }) assert.deepEqual(receipts[0], { id: '51', receiptNo: 'RCPT-51', receivedAt: '2026-08-12T10:00:00', amount: '40.00', currencyCode: 'CNY', method: 'BANK_TRANSFER', allocatedAmount: '40.00' }) }) @@ -122,6 +122,10 @@ test('keeps actions first and exposes list, balance, detail, confirmation, recei } assert.match(viewSource, /BANK_TRANSFER: '银行转账'/) assert.match(viewSource, /paymentMethodLabel\(row\.method\)/) + assert.match(viewSource, /label="入仓码"/) + assert.match(viewSource, /openInboundAssociation\(row\.inboundCode\)/) + assert.match(viewSource, /InboundCodeAssociationDialog/) + assert.match(viewSource, /:inbound-code="inboundAssociationCode"/) assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/) assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/) assert.match(viewSource, /size="small" type="success" plain/) diff --git a/src/modules/finance/views/FinanceBillsView.vue b/src/modules/finance/views/FinanceBillsView.vue index 95fd113..d4f46df 100644 --- a/src/modules/finance/views/FinanceBillsView.vue +++ b/src/modules/finance/views/FinanceBillsView.vue @@ -4,6 +4,7 @@ import { CircleDollarSign, Eye, RefreshCcw, Search } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { ApiError } from '@/api/http' import { useAuthStore } from '@/modules/auth/public' +import { InboundCodeAssociationDialog } from '@/modules/first-leg/public' import { confirmMerchantFinanceStatement, getMerchantFinanceBill, getMerchantFinanceBills, getMerchantFinanceReceipts } from '../api/finance' import type { FinanceBillDetail, FinanceBillQuery, FinanceBillStatus, FinanceBillSummary, FinanceCustomerConfirmationStatus } from '../contracts' @@ -16,6 +17,8 @@ const bills = ref([]) const detail = ref(null) const selectedBillId = ref('') const detailVisible = ref(false) +const inboundAssociationVisible = ref(false) +const inboundAssociationCode = ref('') const confirmingBillId = ref('') const confirmationKeys = new Map() const page = ref(1) @@ -160,6 +163,13 @@ function handlePageChange(nextPage: number) { void loadBills(nextPage) } +function openInboundAssociation(inboundCode: string | null) { + const code = inboundCode?.trim() + if (!code) return + inboundAssociationCode.value = code + inboundAssociationVisible.value = true +} + onMounted(() => { void loadBills(1) }) @@ -211,12 +221,13 @@ onMounted(() => { void loadBills(1) }) + diff --git a/src/modules/first-leg/public.ts b/src/modules/first-leg/public.ts index a7a171b..50e9e80 100644 --- a/src/modules/first-leg/public.ts +++ b/src/modules/first-leg/public.ts @@ -1,2 +1,3 @@ export { useFirstLegOrdersStore } from './store/firstLegOrders' export { getFirstLegStatusLabel, getFirstLegStatusTone } from './utils/firstLegOrder' +export { default as InboundCodeAssociationDialog } from './components/InboundCodeAssociationDialog.vue' 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 07/33] 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 08/33] =?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 09/33] =?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') }}
+
+ + + + + + + + + + + + +
diff --git a/src/modules/cross-border/routes.ts b/src/modules/cross-border/routes.ts index e0a1c4e..1d54e92 100644 --- a/src/modules/cross-border/routes.ts +++ b/src/modules/cross-border/routes.ts @@ -9,7 +9,7 @@ export const crossBorderModule = { path: 'cross-border/procurement', name: 'cross-border-procurement', component: () => import('./views/ProcurementView.vue'), - release: 'prototype', + release: 'active', meta: { title: '海外采购', description: '管理海外采购订单', permission: 'merchant:cross-border:procurement' }, navigation: { key: 'cross-border-procurement', icon: Package }, }, @@ -17,7 +17,7 @@ export const crossBorderModule = { path: 'cross-border/domestic-trucking', name: 'cross-border-domestic-trucking', component: () => import('./views/DomesticTruckingView.vue'), - release: 'prototype', + release: 'active', meta: { title: '美国本土卡', description: '创建美国本土卡车运输订单', permission: 'merchant:cross-border:domestic-trucking' }, navigation: { key: 'cross-border-domestic-trucking', icon: Truck }, }, @@ -25,7 +25,7 @@ export const crossBorderModule = { path: 'cross-border/pallet-shipping', name: 'cross-border-pallet-shipping', component: () => import('./views/PalletShippingView.vue'), - release: 'prototype', + release: 'active', meta: { title: '美本土散板', description: '创建美国本土散板运输订单', permission: 'merchant:cross-border:pallet-shipping' }, navigation: { key: 'cross-border-pallet-shipping', icon: Ship }, }, diff --git a/src/modules/cross-border/views/DomesticTruckingView.vue b/src/modules/cross-border/views/DomesticTruckingView.vue index d3ecc7e..bc9e623 100644 --- a/src/modules/cross-border/views/DomesticTruckingView.vue +++ b/src/modules/cross-border/views/DomesticTruckingView.vue @@ -1,5 +1,5 @@ - + diff --git a/src/modules/cross-border/views/PalletShippingView.vue b/src/modules/cross-border/views/PalletShippingView.vue index e5cec3f..0f4ee30 100644 --- a/src/modules/cross-border/views/PalletShippingView.vue +++ b/src/modules/cross-border/views/PalletShippingView.vue @@ -1,5 +1,5 @@ - + diff --git a/src/modules/cross-border/views/ProcurementView.vue b/src/modules/cross-border/views/ProcurementView.vue index edb2695..bde268f 100644 --- a/src/modules/cross-border/views/ProcurementView.vue +++ b/src/modules/cross-border/views/ProcurementView.vue @@ -1,5 +1,5 @@ - + diff --git a/src/modules/dashboard/views/DashboardView.test.mjs b/src/modules/dashboard/views/DashboardView.test.mjs index ba777db..67c6f0c 100644 --- a/src/modules/dashboard/views/DashboardView.test.mjs +++ b/src/modules/dashboard/views/DashboardView.test.mjs @@ -25,12 +25,15 @@ test('isolates recoverable dashboard failures without hiding successful sections assert.doesNotMatch(source, /