# 注销功能实现总结 ## 问题描述 原先的"注销用户"功能执行了系统级的注销操作(`shutdown /l`),导致注销了整个操作系统用户,而不是应用程序内的用户。 ## 解决方案 将系统级注销改为应用级注销,即清除应用内的用户状态并返回登录页面。 ## 修改内容 ### 1. 核心功能实现 #### `src/components/ExitModal.tsx` - ✅ 引入 `useDispatch` 和 `clearUserInfo` - ✅ 在 `logout` case 中调用 `dispatch(clearUserInfo())` 清除 Redux 用户状态 - ✅ 显示成功消息并关闭 modal - ✅ 移除了 `window.electronAPI.logoutUser()` 调用 - ✅ 更新 TypeScript 类型定义,移除 `logoutUser` API ### 2. Electron 主进程清理 #### `preload.js` - ✅ 移除 `logoutUser` API 暴露 #### `main.js` - ✅ 移除 `exit-logout` IPC handler - ✅ 移除 `getSystemCommands()` 中的 `logout` 命令配置 ### 3. 后端 API 占位 #### `src/API/security/userActions.ts` - ✅ 添加 `logout()` 函数占位 - ✅ 当前返回 Promise.resolve,待后端 API 开发后实现 ### 4. Cypress e2e 测试 #### `cypress/support/mock/handlers/user.ts` - ✅ 添加 `mockLogoutSuccess()` mock handler #### `cypress/e2e/security/logout.cy.ts`(新增) - ✅ 测试注销流程:点击注销 → 清除状态 → 返回登录页 - ✅ 测试 Redux 状态清除 - ✅ 测试 modal 取消操作 - ✅ 测试占位 API(不调用后端) #### 文件组织 - ✅ 将 `login.cy.ts` 移动到 `cypress/e2e/security/` 目录 ## 工作流程 ### 用户点击"注销用户"后的流程: 1. 用户在退出 modal 中点击"注销用户"按钮 2. `ExitModal` 组件调用 `dispatch(clearUserInfo())` 3. Redux store 清除用户信息: - token → '' - expire → 0 - uid → 0 - name → '' - avatar → '' 4. 显示成功消息:"已退出登录" 5. 关闭退出 modal 6. `src/pages/index/index.tsx` 检测到 `loggedIn = false` 7. 自动渲染 `` 组件,返回登录页面 ## 测试验证 ### 手动测试步骤: 1. 启动应用并登录 2. 点击退出按钮 3. 在弹出的 modal 中点击"注销用户" 4. 验证: - ✅ 显示"已退出登录"消息 - ✅ Modal 关闭 - ✅ 返回到登录页面 - ✅ **不会**注销操作系统用户 ### 测试依赖的 Mock Handlers: 为确保测试能够正常运行,logout 测试依赖以下 mock handlers: 1. **i18n Mocks** (`cypress/support/mock/handlers/i18n.ts`) - `mockAllRequiredAPIs()` - 模拟软件信息和日志 API - `mockI18nSuccess('zh')` - 模拟多语言资源加载成功 2. **Quota Mocks** (`cypress/support/mock/handlers/quota.ts`) - `mockGetQuotaSuccess()` - 模拟配额检查成功 3. **Protocol Mocks** (`cypress/support/mock/handlers/protocol.ts`) - **重要!** - `mockGetPatientTypeHuman()` - 模拟患者类型 API - `mockGetBodyPartHuman()` - 模拟身体部位 API - **注意**:这两个 mock 是必需的,否则登录后会卡在"正在初始化,请稍候..." 4. **User Mocks** (`cypress/support/mock/handlers/user.ts`) - `mockLoginSuccess()` - 模拟登录成功 - `mockLogoutSuccess()` - 模拟登出成功(可选,当前未使用) ### 测试初始化流程: ```typescript beforeEach(() => { cy.clearAllSessionStorage(); cy.clearAllLocalStorage(); mockAllRequiredAPIs(); // 软件信息、日志 mockI18nSuccess('zh'); // 多语言资源 mockGetQuotaSuccess(); // 配额检查 mockGetPatientTypeHuman(); // 患者类型 ← 必需! mockGetBodyPartHuman(); // 身体部位 ← 必需! mockLoginSuccess(); // 登录 loginPage.visit(); loginPage.login('admin', '123456'); }); ``` ### AppInitializer 初始化流程 登录成功后,`AppInitializer` 会执行以下初始化操作: 1. `getPatientTypes()` - 获取患者类型列表 2. `getBodyParts()` - 获取身体部位列表 3. `initializeProductState()` - 初始化产品状态 **重要**:只有这三个操作全部完成后,才会调用 `onInitialized()` 进入主界面。如果缺少相应的 mock,测试会卡在"正在初始化,请稍候..." ### Cypress 测试命令: ```bash # 运行所有安全相关测试 npm run cypress:run -- --spec "cypress/e2e/security/**/*.cy.ts" # 只运行 logout 测试 npm run cypress:run -- --spec "cypress/e2e/security/logout.cy.ts" # 以交互模式运行 npm run cypress:open ``` ## 后续待办 ### 当后端 logout API 开发完成后: 1. 更新 `src/API/security/userActions.ts` 中的 `logout()` 函数: ```typescript export function logout(): Promise<{ data: { code: string } }> { return axiosInstance.post('/pub/logout'); } ``` 2. 在 `ExitModal.tsx` 中调用后端 API(可选): ```typescript case 'logout': actionName = '注销用户'; try { // 调用后端 logout API await logout(); } catch (error) { console.error('后端登出失败:', error); // 即使后端失败也继续清除前端状态 } dispatch(clearUserInfo()); message.success('已退出登录'); onClose(); return; ``` 3. 更新 Cypress 测试以验证 API 调用 ## 注意事项 - ✅ 此修改**不影响**"关闭程序"功能 - ✅ 此修改**不影响**"关机"功能 - ✅ "关机"功能仍然调用系统命令(需要管理员权限) - ✅ 应用级注销是纯前端操作,无需管理员权限 - ✅ 登录状态检查逻辑在 `src/pages/index/index.tsx` 中,无需修改 ## 影响范围 ### 修改的文件: 1. `src/components/ExitModal.tsx` 2. `preload.js` 3. `main.js` 4. `src/API/security/userActions.ts` 5. `src/layouts/SystemZone.tsx` - 添加 `data-testid="exit-button"` 6. `cypress/support/mock/handlers/user.ts` 7. `cypress/support/pageObjects/MainPage.ts` - 添加退出按钮方法 8. `cypress/support/pageObjects/ExitModalPage.ts`(新增) 9. `cypress/e2e/security/logout.cy.ts`(新增) 10. `cypress/e2e/login.cy.ts` → `cypress/e2e/security/login.cy.ts`(移动) ### 未修改的文件: - ✅ `src/states/user_info/index.ts`(已有 `clearUserInfo` action) - ✅ `src/pages/index/index.tsx`(已有登录状态检查逻辑) - ✅ 其他组件和页面 ## 测试改进 ### Page Object 模式 为提高测试可维护性和可读性,使用了 Page Object 模式: #### 1. MainPage (`cypress/support/pageObjects/MainPage.ts`) - `clickExitButton()` - 点击退出按钮(带 10 秒超时等待) - `getExitButton()` - 获取退出按钮元素 #### 2. ExitModalPage (`cypress/support/pageObjects/ExitModalPage.ts`) 新增专门的退出 Modal Page Object: - `shouldBeVisible()` - 验证 modal 可见 - `shouldNotBeVisible()` - 验证 modal 已关闭 - `clickLogout()` - 点击"注销用户" - `clickClose()` - 点击"关闭程序" - `clickShutdown()` - 点击"关机" - `clickCancel()` - 点击"取消" #### 3. 退出按钮和 Modal 按钮增强 为所有测试相关的按钮添加了 `data-testid` 属性,便于测试定位: **SystemZone.tsx** - 退出按钮: - `data-testid="exit-button"` **ExitModal.tsx** - Modal 内的四个按钮: - `data-testid="exit-modal-close-button"` - 关闭程序 - `data-testid="exit-modal-logout-button"` - 注销用户 - `data-testid="exit-modal-shutdown-button"` - 关机 - `data-testid="exit-modal-cancel-button"` - 取消 **为什么使用 data-testid?** - ✅ 更稳定:不依赖于 UI 文本(文本可能因多语言而改变) - ✅ 更明确:清晰地表明这是测试用的标识符 - ✅ 更易维护:测试代码不会因为 UI 文字修改而失效 - ✅ 最佳实践:符合测试自动化的业界标准 ### 测试用例示例 ```typescript it('should successfully logout and return to login page', () => { // 使用 Page Object 点击退出按钮(带超时等待) mainPage.clickExitButton(); // 验证 modal 可见 exitModalPage.shouldBeVisible(); // 点击"注销用户"按钮 exitModalPage.clickLogout(); // 验证成功消息和返回登录页 cy.contains('已退出登录').should('be.visible'); loginPage.getUsernameInput().should('be.visible'); }); ``` ### 等待时间优化 - 退出按钮等待时间:10 秒(`clickExitButton` 方法中) - Modal 显示等待:5 秒(`shouldBeVisible` 方法中) ## 测试覆盖 ### 已覆盖的测试场景: - ✅ 注销成功并返回登录页 - ✅ Redux 状态清除验证 - ✅ Modal 取消操作 - ✅ 占位 API 验证 - ✅ 使用 Page Object 模式提高可维护性 - ✅ 增加等待时间确保元素加载 ### 建议的额外测试: - 手动测试在实际 Electron 环境中的完整流程 - 测试连续登录登出操作 - 测试登出后的权限验证 ## 完成状态 ✅ 所有计划的修改已完成 ✅ 所有测试代码已编写 ⏳ 等待手动测试验证 ⏳ 等待后端 API 开发