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
@@ -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 中重建付款事实。