This commit is contained in:
2026-09-02 20:49:19 +08:00
parent 913a8df7f3
commit 6270573095
4 changed files with 40 additions and 13 deletions
+1 -1
View File
@@ -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` 当前未注册路由,不作为正式入口 |
@@ -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('公司名称、客户编号或联系人')
+30 -11
View File
@@ -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()
+2 -1
View File
@@ -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 中重建付款事实。