feat(platform): Implement legacy system integration with Wrapper Bridge architecture

Complete integration of the old clinical research platform (www.xunzhengyixue.com)
into the new AI platform via Token injection + iframe embedding:

Backend:
- Add legacy-bridge module (MySQL pool, auth service, routes)
- POST /api/v1/legacy/auth: JWT -> phone lookup -> Token injection into old MySQL
- Auto-create user in old system if not found (matched by phone number)

Frontend:
- LegacySystemPage: iframe container with Bridge URL construction
- ResearchManagement + StatisticalTools entry components
- Module registry updated from external links to iframe embed mode

ECS (token-bridge.html deployed to www.xunzhengyixue.com):
- Wrapper Bridge: sets cookies within same-origin context
- Storage Access API for cross-site dev environments
- CSS injection: hide old system nav/footer, remove padding gaps
- Inner iframe loads target page with full DOM access (same-origin)

Key technical decisions:
- Token injection (direct MySQL write) instead of calling login API
- Wrapper Bridge instead of parent-page cookie setting (cross-origin fix)
- Storage Access API + SameSite=None;Secure for third-party cookie handling
- User isolation guaranteed by phone number matching

Documentation:
- Integration plan v4.0 with full implementation record
- Implementation summary with 6 pitfalls documented
- System status guide updated (ST module now integrated)

Tested: Local E2E verified - auto login, research management, 126 statistical
tools, report generation, download, UI layout all working correctly

Made-with: Cursor
This commit is contained in:
2026-02-27 21:54:38 +08:00
parent 6124c7abc6
commit c3f7d54fdf
21 changed files with 1407 additions and 63 deletions

View File

@@ -0,0 +1,104 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { Spin, message } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import apiClient from '@/common/api/axios'
const BRIDGE_URL = 'https://www.xunzhengyixue.com/token-bridge.html'
interface LegacyAuthData {
token: string
nickname: string
id: number
userRole: string
}
interface LegacySystemPageProps {
targetUrl: string
}
/**
* Embeds the old system in an iframe via a same-origin bridge page.
*
* Flow: call backend to inject token into old MySQL → build bridge URL with
* auth params → iframe loads bridge → bridge sets cookies + loads target page
* in a nested iframe → bridge injects custom CSS (same-origin DOM access).
*/
const LegacySystemPage: React.FC<LegacySystemPageProps> = ({ targetUrl }) => {
const [status, setStatus] = useState<'authenticating' | 'ready' | 'error'>('authenticating')
const [bridgeUrl, setBridgeUrl] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const iframeRef = useRef<HTMLIFrameElement>(null)
const authDoneRef = useRef(false)
const authenticate = useCallback(async () => {
if (authDoneRef.current) return
try {
setStatus('authenticating')
const resp = await apiClient.post<{ success: boolean; data: LegacyAuthData }>('/api/v1/legacy/auth')
const { token, nickname, id, userRole } = resp.data.data
const redirectPath = new URL(targetUrl).pathname
const params = new URLSearchParams({
token,
nickname,
id: String(id),
userRole,
redirect: redirectPath,
})
setBridgeUrl(`${BRIDGE_URL}?${params.toString()}`)
authDoneRef.current = true
setStatus('ready')
} catch (err: any) {
const msg = err?.response?.data?.message || err?.message || '旧系统认证失败'
setErrorMsg(msg)
setStatus('error')
message.error(msg)
}
}, [targetUrl])
useEffect(() => {
authenticate()
}, [authenticate])
if (status === 'authenticating') {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />} />
<span style={{ color: '#666' }}>...</span>
</div>
)
}
if (status === 'error') {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
<span style={{ fontSize: 48 }}></span>
<span style={{ color: '#999' }}>{errorMsg}</span>
<button
onClick={() => { authDoneRef.current = false; authenticate() }}
style={{ padding: '8px 24px', cursor: 'pointer', borderRadius: 6, border: '1px solid #d9d9d9', background: '#fff' }}
>
</button>
</div>
)
}
return (
<iframe
ref={iframeRef}
src={bridgeUrl}
style={{
width: '100%',
height: '100%',
border: 'none',
display: 'block',
}}
title="旧系统"
/>
)
}
export default LegacySystemPage

View File

@@ -0,0 +1,13 @@
import LegacySystemPage from './LegacySystemPage'
const RESEARCH_MANAGEMENT_URL = 'https://www.xunzhengyixue.com/index.html'
const ResearchManagement: React.FC = () => {
return (
<div style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
<LegacySystemPage targetUrl={RESEARCH_MANAGEMENT_URL} />
</div>
)
}
export default ResearchManagement

View File

@@ -0,0 +1,13 @@
import LegacySystemPage from './LegacySystemPage'
const STATISTICAL_TOOLS_URL = 'https://www.xunzhengyixue.com/tool.html'
const StatisticalTools: React.FC = () => {
return (
<div style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
<LegacySystemPage targetUrl={STATISTICAL_TOOLS_URL} />
</div>
)
}
export default StatisticalTools