# HealthLink-HIS 多语言(i18n)技术方案 > **文档类型**: 架构设计 > **版本**: V1.0 > **日期**: 2026-06-24 > **作者**: zhugeliang(架构师) > **状态**: 待确认 --- ## 一、现状分析 ### 1.1 后端现状 | 项目 | 值 | |------|------| | Spring Boot 版本 | 4.0.6 | | 已有 i18n 基础 | `I18nConfig` (SessionLocaleResolver + LocaleChangeInterceptor, lang参数) | | 已有 i18n 资源 | `i18n/messages.properties` + `i18n/messages_zh_CN.properties` (56行) | | MessageUtils | 已封装 `MessageUtils.message(code, args)` 静态方法 | | 消息键体系 | `PromptMsgConstant` (150+ key, 10个模块接口) | | 硬编码中文错误 | **~851处** `R.fail("中文")` 散落在各 Controller/AppService | | 数据库枚举值 | 状态值以数字存储, 前端映射显示 | **核心问题**: - 后端已有 i18n 基础设施, 但只用了 ~56 条消息, 远不够覆盖 - 大量业务错误消息硬编码为中文字符串 (`R.fail("中文")`) - `PromptMsgConstant` 有 150+ 消息键, 但只有 zh_CN 翻译 ### 1.2 前端现状 | 项目 | 值 | |------|------| | 技术栈 | Vue 3 + Vite + Element Plus + Pinia | | 页面数量 | ~200+ `.vue` 文件 | | ElMessage/ElNotification 调用 | **~2109处** (仅 views/) | | 路由标题 | `meta.title` 硬编码中文 | | 侧边栏/菜单 | `meta.title` 从数据库动态加载 | | 布局组件 | Navbar/Sidebar/Settings 中有硬编码中文 | | Element Plus 组件 | el-dialog title, el-button label, el-table column label 等 | | 已有 i18n 方案 | **无** (未安装 vue-i18n) | **核心问题**: - 零 i18n 基础, 所有中文硬编码在 .vue 文件中 - Element Plus 自身也是中文 (需配置 locale) - 菜单标题来自数据库, 需要数据库层面的多语言支持 ### 1.3 移动端现状 | 项目 | 值 | |------|------| | 技术栈 | Vue 3 + Vite + Element Plus | | 页面数量 | 14 个 .vue 文件 | | 已有 i18n 方案 | **无** | --- ## 二、方案设计 ### 2.1 总体架构 ``` ┌─────────────────────────────────────┐ │ 语言选择器 (全局) │ │ Cookie/Session 存储当前语言 │ └──────────┬──────────────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌───────────┐ ┌──────────────┐ ┌──────────────┐ │ zh_CN (默认)│ │ en_US (英语) │ │ vi_VN (越南语) │ └───────────┘ └──────────────┘ └──────────────┘ │ │ │ ┌─────┴────────────────┴────────────────┴─────┐ │ │ ┌────┴────┐ ┌──────┴──────┐ │ 前端 i18n │ │ 后端 i18n │ │ vue-i18n │ │ MessageSource│ └────┬────┘ └──────┬──────┘ │ │ ▼ ▼ JSON 语言包文件 Properties 资源文件 (src/locales/) (i18n/messages_*.properties) ``` ### 2.2 核心技术选型 #### 前端 | 方案 | 选择 | 理由 | |------|------|------| | i18n 库 | **vue-i18n v9** | Vue 3 官方推荐, 生态成熟, 与 Element Plus 完美集成 | | 语言包格式 | **JSON** | 易编辑、易翻译、易从数据库导出、易做 CI/CD 检查 | | 语言切换 | **Cookie** | 持久化, 刷新不丢失, 与后端 SessionLocaleResolver 保持一致 | | Element Plus | **动态切换 locale** | `el-config-provider` + `setLocale` API | #### 后端 | 方案 | 选择 | 理由 | |------|------|------| | i18n 引擎 | **Spring MessageSource (已有)** | 无需新增依赖, 已有 `I18nConfig` + `MessageUtils` | | 资源文件格式 | **Properties** | Spring Boot 原生支持, 已有基础 | | 语言切换 | **Cookie/Header (新增)** | 保持向后兼容, 同时支持 `lang` query param | #### 数据库 (菜单/字典多语言) | 方案 | 选择 | 理由 | |------|------|------| | 菜单多语言 | **新建 `sys_menu_i18n` 表** | 不修改已有 `sys_menu` 表结构, 零侵入 | | 字典多语言 | **新建 `sys_dict_type_i18n` + `sys_dict_data_i18n` 表** | 字典数据量大, 独立表便于管理 | | 业务数据多语言 | **暂不支持** | 患者名、诊断名等业务数据不需多语言 | --- ## 三、详细设计 ### 3.1 前端实现 #### 3.1.1 目录结构 ``` healthlink-his-ui/src/ ├── locales/ # 语言包目录 │ ├── index.js # vue-i18n 配置 + 导出 │ ├── zh_CN.js # 简体中文 (基于现有硬编码提取) │ ├── en_US.js # English │ └── vi_VN.js # Tiếng Việt ├── plugins/ │ └── i18n.js # vue-i18n 插件初始化 ├── store/modules/ │ ├── app.js # ← 新增: language 状态 │ └── settings.js # ← 新增: language 配置 ├── layout/ │ └── components/ │ └── LanguageSwitcher.vue # 新增: 语言切换下拉组件 ├── utils/ │ └── request.js # ← 修改: 请求头带 Accept-Language └── ...views/** # ← 批量修改: 硬编码中文 → $t('key') ``` #### 3.1.2 语言包结构设计 ```javascript // locales/zh_CN.js 结构示例 export default { // 系统通用 common: { confirm: '确认', cancel: '取消', save: '保存', delete: '删除', edit: '编辑', add: '新增', search: '搜索', export: '导出', import: '导入', reset: '重置', submit: '提交', close: '关闭', tip: '提示', success: '操作成功', fail: '操作失败', loading: '加载中...', noData: '暂无数据', }, // 登录/认证 auth: { login: '登录', username: '用户名', password: '密码', captcha: '验证码', loginSuccess: '登录成功', logout: '退出登录', tokenExpired: '登录已过期,请重新登录', }, // 系统管理 system: { userManage: '用户管理', roleManage: '角色管理', menuManage: '菜单管理', deptManage: '部门管理', dictManage: '字典管理', }, // 门诊挂号 registration: { outpatientReg: '门诊挂号', patientSearch: '患者检索', registerSuccess: '挂号成功', refund: '退号', refundSuccess: '退号成功', }, // 消息提示 (对应后端 PromptMsgConstant) msgs: { addSuccess: '{0}添加成功', saveSuccess: '{0}保存成功', deleteSuccess: '{0}删除成功', operationSuccess: '{0}操作成功', alreadyExists: '{0}已经存在', operationFailed: '操作失败,请联系管理员', dataDeletedByOthers: '操作失败,该数据已被他人删除,请刷新后重试', dataModifiedByOthers: '操作失败,该数据已被他人更改,请刷新后重试', noDuplicateSubmit: '请勿重复提交', querySuccess: '查询成功', }, // 菜单标题 (与 sys_menu 的 menu_name 对应, 用于前端硬编码部分) menus: { dashboard: '首页', patientManage: '患者管理', registration: '挂号收费', outpatient: '门诊管理', inpatient: '住院管理', pharmacy: '药品管理', // ... 所有菜单项 }, // Element Plus 组件 element: { pagination: { total: '共 {total} 条', pageSize: '每页 {pageSize} 条', validator: '每页 {n} 条,至少 1 条', }, table: { emptyText: '暂无数据', }, dialog: { confirm: '确定', cancel: '取消', }, upload: { exceedSize: '上传文件大小超出限制', fileNameExceedLength: '上传的文件名最长 {0} 个字符', }, }, } ``` #### 3.1.3 前端改造策略 **Phase 1 — 基础设施 (不破坏现有功能)** 1. 安装 `vue-i18n@9` 2. 创建 `locales/` 目录和 `zh_CN.js` (先搬运现有中文) 3. 配置 `plugins/i18n.js` 4. 修改 `main.js` 引入 i18n 5. 修改 `request.js` 添加 `Accept-Language` 请求头 6. 新增 `LanguageSwitcher.vue` 组件 7. 修改 `app.js` store 增加 `language` 状态 **Phase 2 — 核心页面改造 (逐步推进)** 8. 登录页 `views/login.vue` → 全部 $t() 9. 布局组件 `layout/**` → 全部 $t() 10. 系统管理模块 `views/system/**` → 全部 $t() 11. 门诊模块 `views/charge/**`, `views/doctorstation/**` → 全部 $t() 12. 住院模块 `views/inpatientNurse/**` → 全部 $t() 13. 药品模块 `views/drug/**`, `views/pharmacy*/**` → 全部 $t() 14. 其余模块依次推进 **Phase 3 — 辅助功能** 15. Element Plus 组件语言包切换 16. 路由 meta.title 多语言化 17. 表单校验提示国际化 (Element Plus form rules) #### 3.1.4 语言切换流程 ``` 用户点击 LanguageSwitcher ↓ Cookie.set('language', 'en_US') ↓ Pinia store.app.language = 'en_US' ↓ vue-i18n.global.locale.value = 'en_US' ↓ Element Plus setLocale(en) ↓ Axios 请求头: Accept-Language: en-US ↓ 页面响应式更新 (所有 $t() 自动重新渲染) ``` ### 3.2 后端实现 #### 3.2.1 资源文件扩展 ``` healthlink-his-application/src/main/resources/i18n/ ├── messages.properties # 默认 (英文 fallback) ├── messages_zh_CN.properties # 中文 (现有, 扩充) ├── messages_en_US.properties # 英文 (新建) └── messages_vi_VN.properties # 越南语 (新建) ``` #### 3.2.2 消息分类体系 ``` # 1. 系统通用 (已有, 扩充) apl.common.M00001 = {0} added successfully apl.common.M00002 = {0} saved successfully ... # 2. 用户认证 auth.login.success = Login successful auth.user.not.exists = User does not exist / Incorrect password auth.password.retry.limit.count = Password entered incorrectly {0} times ... # 3. 文件上传 upload.exceed.maxSize = Upload file size exceeds limit ... # 4. 权限 no.permission = You do not have permission for [{0}] ... # 5. 挂号收费 (registration/payment) apl.payment.M00001 = Actual payment amount does not match expected apl.payment.M00003 = Please select a payment method ... # 6. 药品管理 (pharmacy) apl.pharmacy.outOfStock = Insufficient inventory for {0} apl.pharmacy.dispenseSuccess = Dispensing successful ... # 7. 住院管理 (inpatient) ... # 8. 检查检验 (check/lab) ... # 9. 手术麻醉 (surgery/anesthesia) ... # 10. 电子病历 (emr) ... # 11. 护理管理 (nursing) ... # 12. 医保 (yb) ... ``` #### 3.2.3 硬编码消息迁移策略 **优先级 P0 — 高频路径** (登录、挂号、退号、缴费、退药) - `SysLoginService.java` — 登录相关 ~6处 - `OutpatientRegistrationAppServiceImpl.java` — 挂号/退号 ~10处 - `PaymentRecServiceImpl.java` — 收费/退费 ~30处 **优先级 P1 — 核心业务** (药品、住院、手术、EMR) - 药房 dispensense/approval ~50处 - 住院登记/结算 ~20处 - 手术管理 ~15处 - EMR 病程记录 ~10处 **优先级 P2 — 其他模块** - 其余 ~700处按模块逐步迁移 **迁移方式**: ```java // Before: return R.fail("请选择调配药师"); // After: return R.fail(MessageUtils.message("apl.pharmacy.selectDispenser")); ``` #### 3.2.4 语言检测优先级 ``` 1. Cookie "language" (最高优先级, 持久化) 2. HTTP Header "Accept-Language" 3. Query Param "lang" (已有, 向后兼容) 4. Session 中保存的语言 5. 默认 Locale.SIMPLIFIED_CHINESE ``` 新增 `LanguageCookieLocaleResolver` 继承 `SessionLocaleResolver`: ```java @Bean public LocaleResolver localeResolver() { CookieLocaleResolver clr = new CookieLocaleResolver("lang"); clr.setDefaultLocale(Constants.DEFAULT_LOCALE); clr.setCookieMaxAge(Duration.ofDays(365)); return clr; } ``` ### 3.3 数据库多语言 #### 3.3.1 sys_menu_i18n 表 ```sql CREATE TABLE IF NOT EXISTS sys_menu_i18n ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, menu_id BIGINT NOT NULL REFERENCES sys_menu(menu_id), lang_code VARCHAR(10) NOT NULL, -- 'zh_CN', 'en_US', 'vi_VN' menu_name VARCHAR(100) NOT NULL, UNIQUE(menu_id, lang_code) ); COMMENT ON TABLE sys_menu_i18n IS '菜单多语言表'; COMMENT ON COLUMN sys_menu_i18n.menu_id IS '关联 sys_menu.menu_id'; COMMENT ON COLUMN sys_menu_i18n.lang_code IS '语言代码'; COMMENT ON COLUMN sys_menu_i18n.menu_name IS '菜单名称(多语言)'; ``` **查询逻辑**: ```sql -- 优先取当前语言, 回退到中文, 再回退到默认 SELECT COALESCE( (SELECT menu_name FROM sys_menu_i18n WHERE menu_id = m.menu_id AND lang_code = 'en_US'), (SELECT menu_name FROM sys_menu_i18n WHERE menu_id = m.menu_id AND lang_code = 'zh_CN'), m.menu_name ) AS menu_name FROM sys_menu m; ``` #### 3.3.2 sys_dict_data_i18n 表 ```sql CREATE TABLE IF NOT EXISTS sys_dict_data_i18n ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, dict_code BIGINT NOT NULL, dict_sort INT NOT NULL, lang_code VARCHAR(10) NOT NULL, dict_label VARCHAR(100) NOT NULL, dict_value VARCHAR(100) NOT NULL, css_class VARCHAR(100), list_class VARCHAR(100), is_default CHAR(1) DEFAULT 'N', UNIQUE(dict_code, dict_sort, lang_code) ); COMMENT ON TABLE sys_dict_data_i18n IS '字典数据多语言表'; ``` #### 3.3.3 字典类型 i18n ```sql CREATE TABLE IF NOT EXISTS sys_dict_type_i18n ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, dict_id BIGINT NOT NULL REFERENCES sys_dict_type(dict_id), lang_code VARCHAR(10) NOT NULL, dict_name VARCHAR(100) NOT NULL, dict_type VARCHAR(100) NOT NULL, UNIQUE(dict_id, lang_code) ); COMMENT ON TABLE sys_dict_type_i18n IS '字典类型多语言表'; ``` --- ## 四、实施计划 ### Sprint 1: 基础设施搭建 (预计 2-3 天) | 任务 | 模块 | 工作量 | 负责人 | |------|------|--------|--------| | T1.1 安装 vue-i18n@9 | 前端 | 0.5h | 赵云 | | T1.2 创建 locales/ 目录结构 + zh_CN.js | 前端 | 2h | 赵云 | | T1.3 配置 plugins/i18n.js + main.js 集成 | 前端 | 1h | 赵云 | | T1.4 扩展后端 messages_zh_CN.properties | 后端 | 2h | 关羽 | | T1.5 创建 messages_en_US.properties (翻译 P0 消息) | 后端 | 3h | 关羽 | | T1.6 创建 messages_vi_VN.properties (翻译 P0 消息) | 后端 | 3h | 关羽 | | T1.7 改造 I18nConfig 支持 Cookie + Header | 后端 | 1h | 关羽 | | T1.8 新增 LanguageSwitcher.vue 组件 | 前端 | 1h | 赵云 | | T1.9 修改 request.js 添加 Accept-Language | 前端 | 0.5h | 赵云 | | T1.10 编译验证 + 冒烟测试 | 全栈 | 1h | 张飞 | **产出**: 语言切换功能可用, 登录页/布局组件中英文切换 ### Sprint 2: 核心模块国际化 (预计 3-4 天) | 任务 | 模块 | 工作量 | 负责人 | |------|------|--------|--------| | T2.1 登录页 login.vue 全部 $t() | 前端 | 2h | 赵云 | | T2.2 布局组件 (Navbar/Sidebar/Settings) 全部 $t() | 前端 | 3h | 赵云 | | T2.3 Element Plus 组件语言包切换 | 前端 | 2h | 赵云 | | T2.4 系统管理模块 views/system/** 全部 $t() | 前端 | 4h | 赵云 | | T2.5 后端 P0 消息全部翻译 (auth/payment/upload/permission) | 后端 | 3h | 关羽 | | T2.6 后端硬编码迁移 P0 (SysLoginService, Registration, Payment) | 后端 | 4h | 关羽 | | T2.7 Flyway 迁移: sys_menu_i18n + sys_dict_*_i18n 表 | DBA | 1h | 荀彧 | | T2.8 菜单/字典 Service 层支持多语言查询 | 后端 | 3h | 关羽 | | T2.9 编译验证 + 冒烟测试 | 全栈 | 1h | 张飞 | **产出**: 系统管理 + 登录 + 布局 中英文/越语可用 ### Sprint 3: 业务模块国际化 (预计 5-7 天) | 任务 | 模块 | 工作量 | 负责人 | |------|------|--------|--------| | T3.1 门诊模块 (charge/doctorstation) 全部 $t() | 前端 | 6h | 赵云 | | T3.2 门诊模块后端硬编码迁移 | 后端 | 4h | 关羽 | | T3.3 门诊模块消息翻译 (en_US + vi_VN) | 后端 | 4h | 关羽 | | T3.4 住院模块 (inpatientNurse) 全部 $t() | 前端 | 5h | 赵云 | | T3.5 住院模块后端硬编码迁移 | 后端 | 3h | 关羽 | | T3.6 住院模块消息翻译 (en_US + vi_VN) | 后端 | 3h | 关羽 | | T3.7 药品模块 (drug/pharmacy) 全部 $t() | 前端 | 5h | 赵云 | | T3.8 药品模块后端硬编码迁移 | 后端 | 4h | 关羽 | | T3.9 药品模块消息翻译 (en_US + vi_VN) | 后端 | 3h | 关羽 | | T3.10 EMR/病历模块 全部 $t() | 前端 | 4h | 赵云 | | T3.11 EMR 后端硬编码迁移 | 后端 | 2h | 关羽 | | T3.12 EMR 消息翻译 (en_US + vi_VN) | 后端 | 2h | 关羽 | | T3.13 其余模块 (检验/检查/手术/护理/质控...) 全部 $t() | 前端 | 10h | 赵云 | | T3.14 其余模块后端硬编码迁移 | 后端 | 8h | 关羽 | | T3.15 其余模块消息翻译 (en_US + vi_VN) | 后端 | 8h | 关羽 | | T3.16 路由 meta.title 多语言化 | 前端 | 2h | 赵云 | | T3.17 表单校验提示国际化 | 前端 | 2h | 赵云 | | T3.18 编译验证 + 全模块冒烟测试 | 全栈 | 2h | 张飞 | **产出**: 全模块中英文/越语可用 ### Sprint 4: 移动端 + 验收 (预计 2-3 天) | 任务 | 模块 | 工作量 | 负责人 | |------|------|--------|--------| | T4.1 移动端 healthlink-his-mobile 安装 vue-i18n | 移动端 | 0.5h | 赵云 | | T4.2 移动端 14 个页面全部 $t() | 移动端 | 4h | 赵云 | | T4.3 移动端语言切换器 | 移动端 | 1h | 赵云 | | T4.4 全模块端到端测试 (中文→英文→越语) | 测试 | 2h | 张飞 | | T4.5 性能测试 (语言切换不卡顿) | 测试 | 1h | 张飞 | | T4.6 验收 + 文档更新 | 华佗 | 1h | 华佗 | --- ## 五、工作量估算 | 阶段 | 前端 | 后端 | DBA | 测试 | 总计 | |------|------|------|-----|------|------| | Sprint 1 | 5h | 10h | 0 | 0 | 15h | | Sprint 2 | 12h | 11h | 1h | 0 | 24h | | Sprint 3 | 37h | 29h | 0 | 0 | 66h | | Sprint 4 | 6h | 0 | 0 | 3h | 9h | | **合计** | **60h** | **50h** | **1h** | **3h** | **~114h (约 2-3 周)** | --- ## 六、风险与应对 | 风险 | 影响 | 应对 | |------|------|------| | 2109+ 处前端硬编码修改量大 | 前端工作量大 | 分批推进, 优先核心模块, 非核心可延后 | | 851+ 处后端硬编码迁移 | 后端工作量大 | 按模块优先级分批迁移, P0→P1→P2 | | 数据库菜单/字典数据量大 | 迁移困难 | 提供 SQL 脚本自动从现有数据初始化 | | 翻译质量 (越南语) | 用户体验差 | 先机器翻译, 上线后逐步人工校对 | | 语言包文件过大 | 加载性能 | 按需加载 (lazy load), 分包加载 | | 第三方组件未国际化 | 部分中文残留 | Element Plus 已支持, 其他按需处理 | | 动态路由菜单多语言 | 需改后端 | 菜单查询 SQL 增加 COALESCE 回退逻辑 | --- ## 七、扩展性设计 ### 7.1 新增语言只需 3 步 1. 创建 `locales/{langCode}.js` (前端) + `messages_{langCode}.properties` (后端) 2. 在 `locales/index.js` 注册新语言 3. 在 `I18nConfig` 中确认默认值 (无需改动) ### 7.2 翻译管理工具 (可选增强) 后续可接入: - **Crowdin / Lokalise**: 在线翻译协作平台 - **xliff-js**: 导出/导入 XLIFF 格式 - 自定义脚本: 从 `.vue` 文件自动提取所有中文, 生成翻译模板 ### 7.3 数据库多语言扩展 如需其他业务表多语言 (如科室名称、诊断名称), 可按相同模式扩展: ```sql -- 通用多语言表模板 CREATE TABLE {table}_i18n ( id BIGINT PRIMARY KEY, source_id BIGINT NOT NULL, lang_code VARCHAR(10) NOT NULL, field_name VARCHAR(100) NOT NULL, field_value TEXT NOT NULL, UNIQUE(source_id, lang_code, field_name) ); ``` --- ## 八、验证标准 ### 8.1 功能验证 - [ ] 语言切换器正常显示 (中/英/越) - [ ] 切换后页面所有中文文本变为对应语言 - [ ] 刷新页面后语言设置保持 (Cookie) - [ ] Element Plus 组件 (分页/弹窗/表格/表单) 均为对应语言 - [ ] 后端 API 返回的错误消息为当前语言 - [ ] 登录页、系统管理、门诊、住院、药品、EMR 等核心模块全部覆盖 - [ ] 移动端 14 个页面全部支持多语言 ### 8.2 兼容性验证 - [ ] 不传 lang 参数时默认中文 (向后兼容) - [ ] 现有前端页面不因 i18n 改造而样式错乱 - [ ] 数据库菜单查询多语言回退正常 (无翻译 → 显示中文 → 显示默认) - [ ] 编译无 ERROR, 无新增 WARNING ### 8.3 性能验证 - [ ] 语言切换响应 < 100ms - [ ] 首屏加载时间增加 < 50KB (gzip) - [ ] 无明显内存泄漏 --- ## 九、附录 ### A. 前端需提取的中文文本类型 1. **ElMessage** 提示文本 (~2109处) 2. **ElMessageBox** 确认/提示文本 3. **el-dialog** title 属性 4. **el-button** 文本内容 5. **el-table** column label 6. **el-form** label / placeholder / 校验提示 7. **路由 meta.title** (部分硬编码) 8. **布局组件** 中的固定文本 (Sidebar/Navbar/Settings) 9. **Element Plus** 自身组件文本 (pagination/table/dialog) 10. **页面标题** h1/h2 等 ### B. 后端需迁移的硬编码消息类型 1. **R.fail("中文")** — 业务错误 (~851处) 2. **R.ok(data, "中文")** — 成功消息 (~150处, 已有 PromptMsgConstant 覆盖) 3. **Service 层抛出的 ServiceException** — 异常消息 4. **日志中的中文** — 可选, 生产日志建议英文 ### C. 消息键命名规范 ``` {模块}.{子类}.{编号} 示例: apl.common.M00001 → APL 通用消息 auth.login.success → 认证-登录-成功 payment.collect.fail → 收费-缴费-失败 pharmacy.dispense.select_pharmacist → 药品-发药-选择药师 registration.refund.success → 挂号-退号-成功 ```