diff --git a/AGENTS.md b/AGENTS.md index 6d5ef77b..dac37c46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ | 订单列表、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖 | | 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | | 入库、测量与计价主数据 | `modules/inbounds/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`order/`、`file/`、`integration-service/` | -| 财务 | `modules/finance/`,继续读该目录 `AGENTS.md` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/` | +| 财务与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;柜费入口 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | | 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关逐资料完成,全买单柜使用 `complete-all-buy` | | 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | | 智能装柜按入仓码候选与箱数分配 | `modules/container-loading/pages/SmartLoadingWorkbenchView.vue`、`api/platformContainerLoading.ts` | Backend `firstleg/` 的 `PlatformSmartLoadingController`、`SmartLoadingService`;`GlobalSmartLoadingView.vue` 当前未注册路由,不作为正式入口 | diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index 1007ec75..fd7bc406 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -168,6 +168,13 @@ describe('CustomersView', () => { vi.mocked(showExportTaskCreatedDialog).mockResolvedValue(false) }) + it('waits for the client workspace to confirm impersonation session persistence', () => { + expect(customersSource).toContain("searchParams.set('impersonationOrigin', window.location.origin)") + expect(customersSource).toContain("event.data?.type !== 'impersonation-complete'") + expect(customersSource).toContain("event.data?.type === 'impersonation-failed'") + expect(customersSource).toContain("ElMessage.success('客户工作台登录成功')") + }) + it('searches the customer list by customer name only', () => { expect(customersSource).toContain("label: '客户名称', placeholder: '请输入客户名称'") expect(customersSource).not.toContain('公司名称、客户编号或联系人') diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index b10b2bcf..0fa5c493 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -777,12 +777,19 @@ async function submitImpersonation() { impersonationError.value = getApiErrorDetails(new Error('未配置客户工作台地址'), '暂未配置客户工作台地址,无法安全跳转登录') return } - let targetOrigin: string - try { targetOrigin = new URL(clientUrl, window.location.origin).origin } catch { + let targetUrl: URL + try { + targetUrl = new URL(clientUrl, window.location.origin) + targetUrl.pathname = `${targetUrl.pathname.replace(/\/+$/, '')}/login` + targetUrl.search = '' + targetUrl.hash = '' + targetUrl.searchParams.set('impersonationOrigin', window.location.origin) + } catch { impersonationError.value = getApiErrorDetails(new Error('客户工作台地址无效'), '客户工作台地址配置无效,无法安全跳转登录') return } - const popup = window.open(clientUrl, '_blank') + const targetOrigin = targetUrl.origin + const popup = window.open(targetUrl.toString(), '_blank') if (!popup) { impersonationError.value = getApiErrorDetails(new Error('浏览器拦截了新窗口'), '浏览器拦截了客户工作台窗口,请允许弹窗后重试') return @@ -792,27 +799,39 @@ async function submitImpersonation() { let handoffPending = false try { const ticket = await createPlatformTenantImpersonation(impersonationTenant.value.id, { reason: impersonationForm.reason.trim() }) - const onReady = (event: MessageEvent) => { - if (event.source !== popup || event.origin !== targetOrigin || event.data?.source !== 'chongchong-client' || event.data?.type !== 'impersonation-ready') return - popup.postMessage({ source: 'chongchong-admin', type: 'impersonation-ticket', code: ticket.code }, targetOrigin) + let ticketSent = false + const onMessage = (event: MessageEvent) => { + if (event.source !== popup || event.origin !== targetOrigin || event.data?.source !== 'chongchong-client') return + if (event.data?.type === 'impersonation-ready' && !ticketSent) { + ticketSent = true + popup.postMessage({ source: 'chongchong-admin', type: 'impersonation-ticket', code: ticket.code }, targetOrigin) + return + } + if (event.data?.type === 'impersonation-failed') { + cleanup() + impersonationSubmitting.value = false + impersonationError.value = getApiErrorDetails(new Error('客户工作台未写入代登录会话'), '客户工作台登录写入失败,请关闭窗口后重试') + return + } + if (event.data?.type !== 'impersonation-complete') return cleanup() impersonationSubmitting.value = false - ElMessage.success('客户工作台已打开') + ElMessage.success('客户工作台登录成功') impersonationDialogOpen.value = false } const timeout = window.setTimeout(() => { cleanup() impersonationSubmitting.value = false - impersonationError.value = getApiErrorDetails(new Error('客户工作台未完成安全握手'), '客户工作台未完成安全握手,请关闭窗口后重试') - }, 10000) + impersonationError.value = getApiErrorDetails(new Error('客户工作台未确认会话写入'), '客户工作台未确认登录成功,请关闭窗口后重试') + }, 15000) const cleanup = () => { window.clearTimeout(timeout) - window.removeEventListener('message', onReady) + window.removeEventListener('message', onMessage) if (impersonationCleanup === cleanup) impersonationCleanup = null } impersonationCleanup?.() impersonationCleanup = cleanup - window.addEventListener('message', onReady) + window.addEventListener('message', onMessage) handoffPending = true } catch (error) { popup.close() diff --git a/src/modules/finance/AGENTS.md b/src/modules/finance/AGENTS.md index a4fddf3b..88accc96 100644 --- a/src/modules/finance/AGENTS.md +++ b/src/modules/finance/AGENTS.md @@ -15,7 +15,8 @@ ## 费用付款与凭证快速入口 -- 入库费用的应付/直接已付表单在 `../unified-orders/components/InboundFeeDetailManager.vue`,柜次费用在 `../loading-lists/components/ContainerFeesPanel.vue`;柜级公共费用在同一面板按真实客户与订单分摊,入仓码按事实选填;对应 API 分别为 `../unified-orders/api/inboundFeeDetails.ts`、`../loading-lists/api/containerFees.ts`。 +- 入库费用的应付/直接已付表单在 `../unified-orders/components/InboundFeeDetailManager.vue`,柜次费用在 `../loading-lists/components/ContainerFeesPanel.vue`;柜级公共费用在同一面板按服务端返回的真实客户与订单分摊,入仓 ID/入仓码同时选填或同时为空;对应 API 分别为 `../unified-orders/api/inboundFeeDetails.ts`、`../loading-lists/api/containerFees.ts`。 +- 柜级分摊请求的各行金额之和必须精确等于公共费用总额;创建后使用 `allocationGroupId` 整组审核、驳回、调整或作废,前端不得调用单行接口造成部分成功,也不得提交柜外客户、订单或入仓归属。 - `DIRECT_PAID` 必须提交已付时间、付款方式,以及付款流水号或至少一个付款凭证;凭证由费用 API 调用 `/platform/finance/v1/vouchers` 上传为 `FINANCE_VOUCHER`,并绑定当前客户。 - 后端事实入口为 Firstleg `FeePaymentSnapshotValidator`、入库/柜次费用服务,文件元数据归 File;Finance 只消费已发布的费用来源快照,不在前端或 Finance 中重建付款事实。