# 新旧系统集成方案 > **版本:** v4.0 > **创建日期:** 2026-02-27 > **最后更新:** 2026-02-27(Wrapper Bridge 架构 + Storage Access API + 本地 E2E 验证通过) > **维护者:** 开发团队 > **状态:** ✅ Phase 0-2 全部完成,本地开发环境 E2E 验证通过,待部署生产环境 --- ## 1. 背景 ### 1.1 两套系统概览 | 维度 | 新系统(AI临床研究平台) | 老系统(循证医学平台) | |------|------------------------|----------------------| | 域名 | `iit.xunzhengyixue.com` | `www.xunzhengyixue.com` | | API 域名 | `iit.xunzhengyixue.com/api` (SAE) | `api.xunzhengyixue.com` (ECS Nginx 反代) | | 部署 | 阿里云 SAE(Serverless) | 阿里云 ECS(`8.154.22.149`) | | 技术栈 | Node.js + React 19 + PostgreSQL | Spring Boot 1.4.1 + Java 8 + MySQL | | 前端部署 | SAE 静态资源 | Nginx 直接 serve 静态文件 | | 认证方式 | JWT(Access 2h + Refresh 7d) | 自定义 MD5 Token(MySQL 存储,3.5 天有效期) | | 数据库 | PostgreSQL 15(14 个 Schema) | MariaDB/MySQL(`xzyx_online`) | ### 1.2 老系统模块 | 模块 | 入口地址 | 集成状态 | |------|---------|---------| | 研究管理 | `https://www.xunzhengyixue.com/index.html` | Phase 0 外链已完成 → Phase 2 将改为 iframe 内嵌 | | 统计分析工具 | `https://www.xunzhengyixue.com/tool.html` | Phase 0 外链已完成 → Phase 2 将改为 iframe 内嵌 | | AI 问答 | 老系统内 | 已在新系统完全重写(AIA 模块),无需集成 | ### 1.3 约束条件 - Java 团队已离职,未正式交接源码 - 后找到一份源码,经 ECS 侦察确认 **与生产环境完全一致** - 线上系统可通过 ECS SSH 访问,MySQL 数据库可完全读写 - 两系统同属 `xunzhengyixue.com` 根域(Cookie 可共享) ### 1.4 用户关系 - 两套系统的用户是 **同一批人**,由运营人员统一开账号 - 老系统按用户隔离数据(每个用户有自己的研究项目) - 自动登录必须以用户本人身份进行,不能使用公共账号 --- ## 2. ECS 侦察结果(Phase 1 - 已完成) > 2026-02-27 通过 SSH 登录 `8.154.22.149` 实地验证 ### 2.1 部署架构 ``` ┌─────────────────────────────────────────┐ │ ECS 8.154.22.149 │ │ │ www.xunzhengyixue.com ──► Nginx:443 │ │ │ │ │ ├─ / → 静态文件 │ │ │ /home/work/www/xunzhengyixue/ │ │ │ │ │ └─ /api-proxy/ → rag.xunzhengyixue│ │ │ api.xunzhengyixue.com ──► Nginx:443 │ │ └─ / → proxy_pass 127.0.0.1:8899 │ │ │ │ │ Java JAR (Spring Boot) │ │ XZYXServer-0.0.1-SNAPSHOT.jar │ │ -Xms800m -Xmx4000m │ │ 运行自 2025 年至今 │ │ │ │ │ MariaDB/MySQL :3306 │ │ xzyx_online │ └─────────────────────────────────────────┘ ``` ### 2.2 关键确认事项 | 项目 | 侦察结果 | |------|---------| | 源码一致性 | **生产 `common.js` 与本地源码 100% 一致** | | 前端静态文件路径 | `/home/work/www/xunzhengyixue/` | | 要修改的文件 | `/home/work/www/xunzhengyixue/static/js/common.js` | | Java 部署方式 | 直接 JAR 运行(非 Docker),PID 66548 | | API 地址 | `https://api.xunzhengyixue.com`(`config.js` 确认) | | Cookie 域名 | **`xunzhengyixue.com`**(`config.js` 退出登录逻辑确认) | | Cookie 名称 | `token`, `nickname`, `id`(3 个) | | 登录检查逻辑 | 仅检查 `$.cookie("nickname")` 是否存在 | | API 认证逻辑 | `$.cookie("token")` → `Authorization` 请求头 | | X-Frame-Options | **未设置** → iframe 嵌入可行 | | Nginx 改 common.js | **无需重启 Java**,静态文件改完即生效 | ### 2.3 认证机制详解 ``` Token 生成公式:MD5("KyKz1.0:" + userId + ":" + timestamp) Token 存储位置:MySQL u_user_token 表 Token 有效期:可配置(生产 5000 分钟 ≈ 3.5 天) Token 传递方式:Cookie → JS 读取 → Authorization 请求头 老系统 AuthInterceptor 验证流程: 1. 从 Authorization header 取 Token 2. 查 u_user_token 表是否存在该 Token 3. 检查是否过期(gen_time + timeout) 4. 不涉及密码验证 ← 这是直接写 Token 方案可行的根本原因 ``` ### 2.4 对集成友好的 6 个关键发现 | 发现 | 对集成的意义 | |------|-------------| | **无 CSRF 保护** | 可以从新系统直接 POST 调用 API | | **CORS 全开**(`Access-Control-Allow-Origin: *`) | 新系统前端可直接 fetch 调用老系统 API | | **无 X-Frame-Options** | iframe 嵌入完全可行 | | **Token 仅查表验证,不校验密码** | 直接写 Token 到 DB 即可,绕过密码 | | **同根域名 `.xunzhengyixue.com`** | 新系统 `iit.*` 可设置 Cookie 供 `www.*` 读取 | | **前端纯静态 + 后端分离** | 改 `common.js` 无需碰 Java 代码或重启服务 | --- ## 3. 最终方案:Token 注入 + iframe 嵌入 经过 Phase 0(外链验证)和 Phase 1(ECS 侦察),最终确认采用 **方案 B+E 组合**: - **方案 E(Token 注入)**:新系统后端直接写 Token 到老系统 MySQL,实现自动登录 - **方案 B(iframe 嵌入)**:在新系统内用 iframe 加载老系统页面,隐藏老系统导航 ### 3.1 为什么选 Token 注入而非调用登录 API | 对比项 | 调用登录 API(方案 C) | 直接写 Token(方案 E) | |--------|----------------------|----------------------| | 需要知道用户密码 | 是 | **否** | | 用户改了老系统密码 | 失败 | **不受影响** | | 依赖老系统 API 可用 | 是 | **否**(只依赖 MySQL) | | 可同时创建账号 | 否 | **是** | | 安全性 | 密码在网络传输 | Token 仅写入 DB | ### 3.2 整体流程 ``` 用户在新系统点击"研究管理"或"统计分析工具" │ ▼ 新系统前端 → POST /api/legacy/auth(携带 JWT) │ ▼ 新系统后端: 1. 从 JWT 解出当前用户手机号 2. 连接老系统 MySQL(xzyx_online) 3. SELECT id FROM u_user_info WHERE phone = 手机号 ├─ 不存在 → INSERT 创建账号(默认密码 MD5) └─ 存在 → 获取 userId 4. 生成 Token = MD5("KyKz1.0:" + userId + ":" + Date.now()) 5. INSERT INTO u_user_token (user_id, token, gen_time, user_role) 6. 返回 { token, nickname, id, userRole } │ ▼ 新系统前端: 7. 构建 Bridge URL(token/nickname/id/userRole/redirect 作为参数) 8. iframe src = https://www.xunzhengyixue.com/token-bridge.html?... │ ▼ Bridge 页面(www.xunzhengyixue.com 域内执行): 9. Storage Access API 检查(同站自动授权,跨站需一次性用户点击) 10. document.cookie = "token=xxx; domain=.xunzhengyixue.com"(同域设置) 11. 创建内部 iframe 加载目标页面(/index.html 或 /tool.html) 12. 每次内部页面加载后,注入自定义 CSS(隐藏导航栏、页脚等) │ ▼ 老系统页面(Bridge 内部 iframe,同源): 13. 读取 Cookie nickname → 存在 → 正常渲染 14. AJAX 请求带 Cookie 中的 token → AuthInterceptor 验证通过 │ ▼ 用户看到自己的研究项目(iframe 内),新系统导航栏保持在顶部 ``` ### 3.3 账号同步策略 | 场景 | 处理方式 | |------|---------| | 用户在老系统已有账号 | 直接注入 Token,使用原有账号 | | 用户在老系统无账号 | 自动创建(INSERT u_user_info),默认密码 `123456` 的 MD5 | | 用户改了老系统密码 | **不影响**,Token 注入不依赖密码 | | 用户在老系统被删除 | 自动重新创建 | 匹配规则:以 **手机号(phone)** 作为两套系统的用户关联键。 --- ## 4. 实施计划 ### Phase 0 — 外链跳转(已完成) 顶部导航加外链按钮,点击后 `window.open()` 打开老系统新标签页。 **已改动文件:** | 文件 | 改动 | |------|------| | `frontend-v2/src/framework/modules/types.ts` | 新增 `externalUrl?: string` 字段 | | `frontend-v2/src/framework/modules/moduleRegistry.ts` | 新增「研究管理」模块 + 「统计分析工具」加 `externalUrl` | | `frontend-v2/src/framework/layout/TopNavigation.tsx` | 外部模块 `window.open` + 外链图标 + 跳过权限检查 | | `frontend-v2/src/App.tsx` | 外部模块跳过内部路由注册 | ### Phase 1 — ECS 侦察(已完成) SSH 登录 ECS,验证部署架构、Nginx 配置、Cookie 机制、源码一致性。 **结论:** 全部确认,方案可行。详见第 2 节。 ### Phase 2 — 新系统代码改动清单 **后端新增文件:** | 文件 | 说明 | |------|------| | `backend/src/modules/legacy-bridge/legacy-auth.service.ts` | 连接老系统 MySQL,查询用户、写入 token | | `backend/src/modules/legacy-bridge/routes.ts` | `POST /api/v1/legacy/auth` 接口 | **后端改动文件:** | 文件 | 改动 | |------|------| | `backend/.env` | 新增 `LEGACY_MYSQL_*` 连接配置 | | `backend/src/index.ts` | 注册 `/api/v1/legacy` 路由 | **前端新增文件:** | 文件 | 说明 | |------|------| | `frontend-v2/src/modules/legacy/LegacySystemPage.tsx` | iframe 容器页面(调后端注入 token → 加载 iframe) | | `frontend-v2/src/modules/legacy/ResearchManagement.tsx` | 研究管理入口(指向 index.html) | | `frontend-v2/src/modules/legacy/StatisticalTools.tsx` | 统计分析工具入口(指向 tool.html) | **前端改动文件:** | 文件 | 改动 | |------|------| | `frontend-v2/src/framework/modules/types.ts` | 新增 `legacyUrl` 字段 | | `frontend-v2/src/framework/modules/moduleRegistry.ts` | 研究管理/统计分析模块改为 iframe 内嵌模式 | | `frontend-v2/src/App.tsx` | legacy 模块路由注册 | **ECS 改动文件:** | 文件 | 改动 | |------|------| | `/home/work/www/xunzhengyixue/token-bridge.html` | **新增**:Wrapper Bridge 页面(设 Cookie + 内嵌 iframe + 注入 CSS) | | `/home/work/www/xunzhengyixue/static/js/common.js` | iframe 检测:隐藏 header + footer(bridge 的 CSS 注入已覆盖,可选回退) | | `/home/work/www/xunzhengyixue/tool/js/appajax.js` | iframe 检测:隐藏 menu + footer(bridge 的 CSS 注入已覆盖,可选回退) | ### Phase 2 — Token 注入 + iframe 嵌入(已完成) | 步骤 | 内容 | 位置 | 状态 | |------|------|------|------| | 2.1 | 新系统后端添加 MySQL 连接(老系统数据库) | `backend/` | ✅ 已完成 | | 2.2 | 新增 `POST /api/v1/legacy/auth` 接口 | `backend/` | ✅ 已完成 | | 2.3 | 前端创建 iframe 容器页面组件 | `frontend-v2/` | ✅ 已完成 | | 2.4 | 修改模块注册:从外链改为内嵌 iframe 页面 | `frontend-v2/` | ✅ 已完成 | | 2.5 | ECS 部署 Token Bridge 页面 | ECS 服务器 | ✅ 已部署 | | 2.6 | Token 注入测试 + iframe 显示测试 | 浏览器 | ✅ 已验证 | ### Phase 2.5 — Wrapper Bridge 架构升级 **动机:** 原方案由父页面(新系统)设置 Cookie,但在 localhost 开发环境无法跨域设置 `.xunzhengyixue.com` 的 Cookie。改为 Wrapper Bridge 方案后,Cookie 在老系统域名内 由 bridge 页面自己设置,同时利用同源 DOM 访问注入自定义 CSS。 **架构:** ``` 新系统 iframe └─ token-bridge.html(www.xunzhengyixue.com,设 Cookie + 注入 CSS) └─ inner iframe(www.xunzhengyixue.com/index.html 或 /tool.html) ↑ 同源,bridge 可直接操作其 DOM ``` **优势:** - 本地开发环境(localhost)可正常测试 - 所有样式定制集中在 bridge 页面一处 - 不再需要修改老系统的 `common.js` / `appajax.js`(之前的改动可选回退) - 未来可随时调整老系统的外观,无需登录 ECS 改文件 **`token-bridge.html` 工作流程:** 1. 检查 Storage Access API(`document.hasStorageAccess()`) - 同站(生产环境):自动授权 → 直接进入步骤 2 - 跨站(本地开发)首次:显示"点击授权并继续"按钮 → 用户点击 → `requestStorageAccess()` → 浏览器授权(缓存 30 天) - 跨站(本地开发)后续:已缓存授权 → 直接进入步骤 2 2. 读取 URL 参数,在 `.xunzhengyixue.com` 域下设置 Cookie(`SameSite=None; Secure`) 3. 创建内部 iframe 加载目标页面 4. 每次内部 iframe 导航后,注入自定义 CSS: - 隐藏 `#header-navbar`、`#footer-bar`(主页面) - 隐藏 `#menu`、`#footer`(工具详情页) - 清除 `body` 和 `#page-wrapper` 的顶部间距 **源码位置:** `backend/src/modules/legacy-bridge/token-bridge.html` **浏览器兼容性:** | 环境 | 结果 | 说明 | |------|------|------| | 生产环境(同站) | ✅ 自动工作 | `iit.*` 和 `www.*` 是 same-site | | 本地开发普通模式 | ✅ 自动工作 | Storage Access API 自动授权 | | 本地开发无痕模式 | ❌ 不可用 | Chrome 无痕模式禁止所有第三方 Cookie | ### Phase 2 — ECS 历史操作记录 > 以下 `common.js` 和 `appajax.js` 的改动在 Bridge 升级后**已冗余**(bridge 的 CSS 注入覆盖了相同功能)。 > 保留这些改动作为双重保障;如需回退,用 `.bak` 文件恢复即可。 **文件 1:`/home/work/www/xunzhengyixue/static/js/common.js`** 在 `navigation:function () {` 开头加入的 iframe 检测代码。 **文件 2:`/home/work/www/xunzhengyixue/tool/js/appajax.js`** 在文件开头加入的 iframe 检测代码。 ### Phase 2 — 踩坑记录 | 问题 | 原因 | 修复 | |------|------|------| | Token 注入后 tool.html 正常,但 index.html 跳转登录页 | `gen_time` 字段用了 MySQL `NOW()`(datetime 格式),老系统期望 Java `System.currentTimeMillis()`(毫秒时间戳) | 改为 `Date.now()`,同时先 DELETE 旧 token 再 INSERT | | 研究管理没有左侧导航 | iframe 检测代码隐藏了 `#nav-col` 并 `return` 导致侧边栏内容未生成 | 只隐藏 header + footer,不 return | | 工具详情页仍显示顶部导航 | 工具详情页是独立前端,不加载 `common.js`,使用 `tool/js/appajax.js` | 在 `appajax.js` 开头也加 iframe 检测 | | localhost 无法测试 iframe 嵌入 | 父页面(localhost)无法为 `.xunzhengyixue.com` 设 Cookie | 升级为 Wrapper Bridge:由 bridge 页面在同域内设 Cookie | | Bridge 设了 Cookie 但老系统仍跳登录页 | 浏览器第三方 Cookie 隔离:cross-site iframe 的 `document.cookie` 被静默拒绝 | 添加 `SameSite=None; Secure` + Storage Access API 双重保障 | | 隐藏导航栏后页面顶部多出空白条 | `#header-navbar` 隐藏后 body 的 `padding-top` 仍在 | Bridge CSS 注入额外清除 `body`、`#page-wrapper` 的 padding-top/margin-top | --- ## 5. 验证结果(2026-02-27 本地 E2E 测试) | 测试项 | 结果 | |------|------| | Token 注入 + 自动登录 | ✅ 通过 | | 研究管理(index.html)— 左侧导航正常 | ✅ 通过 | | 统计分析工具(tool.html)— 126 个工具 | ✅ 通过 | | 工具详情页(tool/isttest1.html 等) | ✅ 通过 | | 统计分析 + 出报告 + 下载报告 | ✅ 通过 | | 导航栏/页脚隐藏 + 顶部间距消除 | ✅ 通过 | | 用户隔离(不同用户看不到彼此项目) | ✅ 设计保证(按手机号绑定) | | 普通模式 Storage Access API | ✅ 自动授权 | | 无痕模式 | ❌ 预期不可用(浏览器限制) | ## 6. 后续步骤 | 事项 | 说明 | |------|------| | 部署新系统后端到 SAE | 确保 `.env` 中 `LEGACY_MYSQL_*` 配置指向 ECS MySQL | | 部署新系统前端到 SAE | 包含 `LegacySystemPage` 等新组件 | | 生产端到端验证 | 同站环境(`iit.*` → `www.*`),应自动工作无需 Storage Access API | | 可选:回退 ECS JS 改动 | `common.js` 和 `appajax.js` 的 iframe 检测代码已被 bridge CSS 覆盖,可用 `.bak` 恢复 | | 监控 | 关注 `/api/v1/legacy/auth` 接口调用量和失败率 | --- ## 7. 曾考虑但未选用的方案 ### 方案 A:直接外链跳转 作为 Phase 0 已实施。用户需手动登录老系统,两个标签页切换,体验不佳。 ### 方案 C:前端直调登录 API 需要知道用户在老系统的密码。如果用户改了密码就会失败。已被方案 E(Token 注入)取代。 ### 方案 D:Nginx 反向代理统一入口 在新系统 Nginx 配置 `/legacy/*` 反代到老系统。路径改写复杂,流量多一次中转,SAE 环境配置不便。 --- ## 附录 A:Nginx 配置摘要(生产环境) ```nginx # api.xunzhengyixue.com → Java 后端 server { listen 443 ssl; server_name api.xunzhengyixue.com; location / { proxy_pass http://127.0.0.1:8899; proxy_cookie_path / /; } } # www.xunzhengyixue.com → 静态文件 server { listen 443 ssl; server_name xunzhengyixue.com www.xunzhengyixue.com; root /home/work/www/xunzhengyixue; index index.html; location /api-proxy/ { proxy_pass https://rag.xunzhengyixue.com/api/v1/; } } ``` ## 附录 B:老系统源码关键文件索引 | 文件 | 位置(相对于 Java_old_system/XZYXServer) | 说明 | |------|----------------------------------------|------| | 登录 Controller | `src/main/java/com/xzyx/controller/UserController.java` | 3 个登录端点 | | Token 生成 | `src/main/java/com/xzyx/biz/service/impl/MysqlTokenValidator.java` | MD5 Token 公式 | | 认证拦截器 | `src/main/java/com/xzyx/framework/AuthInterceptor.java` | 仅查表验证 Token | | CORS 配置 | `src/main/java/com/xzyx/framework/CorsFilter.java` | 全开(`*`) | | MVC 配置 | `src/main/java/com/xzyx/framework/MvcConfig.java` | 拦截路径注册 | | 前端导航+登录检查 | ECS `/home/work/www/xunzhengyixue/static/js/common.js` | **已修改**:iframe 检测 | | 工具详情页 JS | ECS `/home/work/www/xunzhengyixue/tool/js/appajax.js` | **已修改**:iframe 检测 | | 前端 API 配置 | ECS `/home/work/www/xunzhengyixue/static/js/config.js` | Cookie 域名逻辑 | | 前端 HTTP 工具 | ECS `/home/work/www/xunzhengyixue/static/js/request.js` | Token → Authorization | | 前端入口页 | ECS `/home/work/www/xunzhengyixue/index.html`, `tool.html` | iframe 加载目标 | ## 附录 C:老系统数据库表结构 详见 [02-服务器与数据库配置说明.md](./02-服务器与数据库配置说明.md)