|
|
@@ -0,0 +1,446 @@
|
|
|
+# 故事017.015: 实现人才小程序登录状态持久化验证
|
|
|
+
|
|
|
+## 元信息
|
|
|
+- **史诗**: 017 - 人才小程序功能实现
|
|
|
+- **优先级**: P1 - 核心功能
|
|
|
+- **状态**: Approved
|
|
|
+- **创建日期**: 2025-12-26
|
|
|
+- **负责人**: 开发团队
|
|
|
+
|
|
|
+## 故事描述
|
|
|
+
|
|
|
+**作为** 人才小程序开发者,
|
|
|
+**我想要** 实现登录状态持久化验证功能,
|
|
|
+**以便** 用户重新打开小程序时,如果之前已经登录过,则自动恢复登录状态,无需再次登录。
|
|
|
+
|
|
|
+### 背景
|
|
|
+
|
|
|
+**现有系统状态:**
|
|
|
+- ✅ 故事017.002已完成登录与首页实现
|
|
|
+- ✅ 人才用户认证API已集成(`talentAuthClient`)
|
|
|
+- ✅ AuthContext基础框架已建立(包含登录、登出、用户信息管理)
|
|
|
+- ✅ token存储机制已实现(localStorage: `talent_token`)
|
|
|
+
|
|
|
+**发现问题:**
|
|
|
+对比用人方小程序(`mini-enterprise-auth-ui`)的实现,人才小程序的认证系统缺少以下关键功能:
|
|
|
+
|
|
|
+1. **缺少`useRequireAuth` hook** - 用人方小程序有独立的`useRequireAuth` hook用于检查登录状态并自动跳转
|
|
|
+2. **缺少应用启动时的登录状态验证** - 用人方小程序在AuthProvider初始化时会自动检查本地token并验证用户信息
|
|
|
+3. **缺少页面级的认证保护** - 需要的页面没有使用`useRequireAuth`进行保护
|
|
|
+
|
|
|
+**用人方小程序参考实现:**
|
|
|
+- `mini-enterprise-auth-ui/src/hooks/useRequireAuth.ts` - 认证检查hook
|
|
|
+- `mini-enterprise-auth-ui/src/hooks/useAuth.tsx` - AuthContext中的自动登录状态验证(第31-55行)
|
|
|
+
|
|
|
+**技术依赖:**
|
|
|
+- ✅ 故事017.001: rencai mini ui包基础框架搭建完成
|
|
|
+- ✅ 故事017.002: 登录与首页实现完成
|
|
|
+- ✅ 人才用户认证API正常工作(`talentAuthClient.me.$get`)
|
|
|
+
|
|
|
+**技术栈:**
|
|
|
+- React 19.1.0
|
|
|
+- Taro 4.1.4
|
|
|
+- @tanstack/react-query 5.83.0 (用于状态管理)
|
|
|
+- Jest 30.2.0 (mini项目使用Jest)
|
|
|
+
|
|
|
+## 验收标准
|
|
|
+
|
|
|
+### useRequireAuth Hook实现
|
|
|
+- [ ] 创建`useRequireAuth` hook(参考`mini-enterprise-auth-ui/src/hooks/useRequireAuth.ts`)
|
|
|
+- [ ] hook检查登录状态,未登录时显示提示并跳转到登录页
|
|
|
+- [ ] 支持loading状态,避免重复跳转
|
|
|
+- [ ] 导出hook供其他页面使用
|
|
|
+
|
|
|
+### AuthContext启动时验证优化
|
|
|
+- [ ] AuthContext在初始化时自动检查本地存储的token
|
|
|
+- [ ] 如果存在token,自动调用`talentAuthClient.me.$get`验证用户信息
|
|
|
+- [ ] 验证成功时恢复用户登录状态
|
|
|
+- [ ] 验证失败时清除本地token并保持未登录状态
|
|
|
+- [ ] 使用React Query的`useQuery`管理用户状态(避免重复请求)
|
|
|
+
|
|
|
+### 页面级认证保护
|
|
|
+- [ ] 在首页(Dashboard)中使用`useRequireAuth`进行保护
|
|
|
+- [ ] 在个人信息页使用`useRequireAuth`进行保护
|
|
|
+- [ ] 在考勤记录页使用`useRequireAuth`进行保护
|
|
|
+- [ ] 在就业信息页使用`useRequireAuth`进行保护
|
|
|
+- [ ] 在设置页使用`useRequireAuth`进行保护
|
|
|
+- [ ] 登录页不使用认证保护(避免死循环)
|
|
|
+
|
|
|
+### 用户体验优化
|
|
|
+- [ ] 验证过程中显示loading状态
|
|
|
+- [ ] 未登录时显示友好的提示信息
|
|
|
+- [ ] 自动跳转到登录页(使用`Taro.reLaunch`清除页面栈)
|
|
|
+- [ ] 登录成功后跳转回原页面(可选,后续实现)
|
|
|
+
|
|
|
+### 测试覆盖
|
|
|
+- [ ] 为`useRequireAuth`编写单元测试
|
|
|
+- [ ] 测试登录状态验证逻辑
|
|
|
+- [ ] 测试未登录时自动跳转
|
|
|
+- [ ] 测试token过期时的处理
|
|
|
+- [ ] 测试页面级认证保护
|
|
|
+- [ ] 所有测试通过
|
|
|
+- [ ] 类型检查通过
|
|
|
+
|
|
|
+## 任务列表
|
|
|
+
|
|
|
+### 任务1: 创建useRequireAuth Hook (AC: useRequireAuth Hook实现)
|
|
|
+- [ ] 1.1 在`@d8d/rencai-auth-ui/src/hooks/`目录下创建`useRequireAuth.ts`文件
|
|
|
+- [ ] 1.2 参考用人方小程序实现:
|
|
|
+ ```typescript
|
|
|
+ import { useEffect } from 'react'
|
|
|
+ import Taro from '@tarojs/taro'
|
|
|
+ import { useAuth } from './useAuth'
|
|
|
+
|
|
|
+ export const useRequireAuth = (): void => {
|
|
|
+ const { isLoggedIn, isLoading } = useAuth()
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isLoading && !isLoggedIn) {
|
|
|
+ Taro.showToast({
|
|
|
+ title: '请先登录',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.redirectTo({
|
|
|
+ url: '/pages/login/index'
|
|
|
+ })
|
|
|
+ }, 1500)
|
|
|
+ }
|
|
|
+ }, [isLoggedIn, isLoading])
|
|
|
+ }
|
|
|
+ ```
|
|
|
+- [ ] 1.3 实现登录状态检查逻辑
|
|
|
+- [ ] 1.4 实现未登录时的提示和跳转
|
|
|
+- [ ] 1.5 更新`@d8d/rencai-auth-ui/src/hooks/index.ts`,导出`useRequireAuth`
|
|
|
+
|
|
|
+### 任务2: 优化AuthContext启动时验证 (AC: AuthContext启动时验证优化)
|
|
|
+- [ ] 2.1 检查`@d8d/rencai-auth-ui/src/hooks/useAuth.tsx`中的`useQuery`实现
|
|
|
+- [ ] 2.2 确认已实现自动检查本地token的逻辑(第38-61行)
|
|
|
+- [ ] 2.3 确认已实现调用`talentAuthClient.me.$get`验证用户信息
|
|
|
+- [ ] 2.4 确认验证成功时恢复用户登录状态(设置`isLoggedIn`和`user`)
|
|
|
+- [ ] 2.5 确认验证失败时清除本地token
|
|
|
+- [ ] 2.6 如需要,优化loading状态的同步逻辑
|
|
|
+- [ ] 2.7 验证React Query的`staleTime: Infinity`配置(避免重复请求)
|
|
|
+
|
|
|
+### 任务3: 添加页面级认证保护 (AC: 页面级认证保护)
|
|
|
+- [ ] 3.1 在首页(`@d8d/rencai-dashboard-ui/src/pages/Dashboard/Dashboard.tsx`)中使用`useRequireAuth`:
|
|
|
+ ```typescript
|
|
|
+ import { useRequireAuth } from '@d8d/rencai-auth-ui/hooks'
|
|
|
+
|
|
|
+ export const Dashboard: React.FC = () => {
|
|
|
+ useRequireAuth() // 添加这行
|
|
|
+
|
|
|
+ // ... 其他代码
|
|
|
+ }
|
|
|
+ ```
|
|
|
+- [ ] 3.2 在个人信息页使用`useRequireAuth`
|
|
|
+- [ ] 3.3 在考勤记录页使用`useRequireAuth`
|
|
|
+- [ ] 3.4 在就业信息页使用`useRequireAuth`
|
|
|
+- [ ] 3.5 在设置页使用`useRequireAuth`
|
|
|
+- [ ] 3.6 确认登录页不使用`useRequireAuth`(避免死循环)
|
|
|
+
|
|
|
+### 任务4: 优化用户体验 (AC: 用户体验优化)
|
|
|
+- [ ] 4.1 确保AuthContext在验证过程中正确设置loading状态
|
|
|
+- [ ] 4.2 确保页面组件在loading时显示加载提示
|
|
|
+- [ ] 4.3 确保未登录时显示友好的提示信息
|
|
|
+- [ ] 4.4 使用`Taro.reLaunch`跳转到登录页(清除页面栈,避免返回)
|
|
|
+- [ ] 4.5 测试登录流程的用户体验
|
|
|
+
|
|
|
+### 任务5: 编写测试 (AC: 测试覆盖)
|
|
|
+- [ ] 5.1 创建`tests/hooks/useRequireAuth.test.tsx`:
|
|
|
+ - 测试登录状态检查
|
|
|
+ - 测试未登录时显示提示
|
|
|
+ - 测试未登录时跳转到登录页
|
|
|
+ - 测试loading状态避免重复跳转
|
|
|
+- [ ] 5.2 更新`tests/hooks/useAuth.test.tsx`(如存在):
|
|
|
+ - 测试启动时自动检查本地token
|
|
|
+ - 测试验证成功时恢复用户状态
|
|
|
+ - 测试验证失败时清除token
|
|
|
+- [ ] 5.3 编写集成测试验证页面级认证保护
|
|
|
+- [ ] 5.4 运行所有测试确保通过
|
|
|
+- [ ] 5.5 运行`pnpm typecheck`确保类型检查通过
|
|
|
+
|
|
|
+### 任务6: 更新文档和文件列表 (AC: 完成)
|
|
|
+- [ ] 6.1 在本故事的"开发者记录"中更新完成说明
|
|
|
+- [ ] 6.2 在本故事的"文件列表"中记录所有新增和修改的文件
|
|
|
+- [ ] 6.3 更新故事017.002的"变更日志",添加本故事的功能说明
|
|
|
+
|
|
|
+## 开发者笔记
|
|
|
+
|
|
|
+### 前置故事见解
|
|
|
+
|
|
|
+**故事017.002完成状态:**
|
|
|
+- ✅ 登录页面功能完整实现
|
|
|
+- ✅ 首页(Dashboard)功能完整实现
|
|
|
+- ✅ AuthContext基础框架已建立
|
|
|
+- ✅ token存储机制已实现
|
|
|
+- ✅ 人才用户认证API已集成
|
|
|
+- ⚠️ **但缺少应用启动时的登录状态验证**
|
|
|
+
|
|
|
+**用人方小程序参考实现:**
|
|
|
+`mini-enterprise-auth-ui/src/hooks/useAuth.tsx` (第31-55行):
|
|
|
+```typescript
|
|
|
+const { data: user, isLoading } = useQuery<User | null, Error>({
|
|
|
+ queryKey: ['currentUser'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const token = Taro.getStorageSync('enterprise_token')
|
|
|
+ if (!token) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const response = await enterpriseAuthClient.me.$get({})
|
|
|
+ if (response.status !== 200) {
|
|
|
+ throw new Error('获取用户信息失败')
|
|
|
+ }
|
|
|
+ const user = await response.json()
|
|
|
+ Taro.setStorageSync('enterpriseUserInfo', JSON.stringify(user))
|
|
|
+ return user
|
|
|
+ } catch (error) {
|
|
|
+ Taro.removeStorageSync('enterprise_token')
|
|
|
+ Taro.removeStorageSync('enterpriseUserInfo')
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ staleTime: Infinity,
|
|
|
+ refetchOnWindowFocus: false,
|
|
|
+ refetchOnReconnect: false,
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+`mini-enterprise-auth-ui/src/hooks/useRequireAuth.ts`:
|
|
|
+```typescript
|
|
|
+import { useEffect } from 'react'
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
+import { useAuth } from './useAuth'
|
|
|
+
|
|
|
+export const useRequireAuth = () => {
|
|
|
+ const { isLoggedIn, isLoading } = useAuth()
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isLoading && !isLoggedIn) {
|
|
|
+ Taro.showToast({
|
|
|
+ title: '请先登录',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.redirectTo({
|
|
|
+ url: '/pages/login/index'
|
|
|
+ })
|
|
|
+ }, 1500)
|
|
|
+ }
|
|
|
+ }, [isLoggedIn, isLoading])
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 技术栈要求
|
|
|
+
|
|
|
+**来源**: [architecture/tech-stack.md](../architecture/tech-stack.md)
|
|
|
+
|
|
|
+**运行时和框架:**
|
|
|
+- **Node.js**: 20.18.3
|
|
|
+- **React**: 19.1.0 (UI组件)
|
|
|
+- **Taro**: 4.1.4 (小程序框架)
|
|
|
+- **@tanstack/react-query**: 5.83.0 (状态管理)
|
|
|
+
|
|
|
+**测试框架:**
|
|
|
+- **Jest**: 30.2.0 (mini项目使用Jest,不是Vitest!)
|
|
|
+- **ts-jest**: 29.4.5 (TypeScript预处理器)
|
|
|
+- **@testing-library/react**: 16.3.0 (React组件测试)
|
|
|
+- **@d8d/mini-testing-utils**: workspace包 (Taro小程序测试工具)
|
|
|
+
|
|
|
+### Mini UI包开发规范
|
|
|
+
|
|
|
+**来源**: [architecture/mini-ui-package-standards.md](../architecture/mini-ui-package-standards.md)
|
|
|
+
|
|
|
+**关键规范要求:**
|
|
|
+
|
|
|
+#### 1. UI包内部导入规范
|
|
|
+**重要**: UI包内部导入必须使用相对路径,不要使用别名。
|
|
|
+
|
|
|
+**正确示例**:
|
|
|
+```typescript
|
|
|
+// ✅ 正确: 使用相对路径导入同一包内的模块
|
|
|
+import { useAuth } from './useAuth'
|
|
|
+import { talentAuthClient } from '../api'
|
|
|
+```
|
|
|
+
|
|
|
+**错误示例**:
|
|
|
+```typescript
|
|
|
+// ❌ 错误: 不要使用别名导入UI包内部的模块
|
|
|
+import { useAuth } from '@/hooks/useAuth'
|
|
|
+import { talentAuthClient } from '@/api'
|
|
|
+```
|
|
|
+
|
|
|
+#### 2. Hook规范
|
|
|
+- Hook名称必须以`use`开头
|
|
|
+- Hook必须遵循React Hook规则
|
|
|
+- Hook应该提供类型安全的返回值
|
|
|
+- Hook应该在单独的文件中定义
|
|
|
+
|
|
|
+#### 3. Taro导航规范
|
|
|
+- 使用`Taro.reLaunch`跳转到登录页(清除页面栈)
|
|
|
+- 使用`Taro.redirectTo`跳转到其他页面(不返回)
|
|
|
+- 使用`Taro.navigateTo`打开新页面(可返回)
|
|
|
+
|
|
|
+### 项目结构指南
|
|
|
+
|
|
|
+**来源**: [architecture/source-tree.md](../architecture/source-tree.md)
|
|
|
+
|
|
|
+**rencai-auth-ui包结构:**
|
|
|
+```
|
|
|
+mini-ui-packages/rencai-auth-ui/
|
|
|
+├── src/
|
|
|
+│ ├── api/
|
|
|
+│ │ ├── talentAuthClient.ts
|
|
|
+│ │ └── index.ts
|
|
|
+│ ├── hooks/
|
|
|
+│ │ ├── index.ts (本故事修改)
|
|
|
+│ │ ├── useAuth.tsx (已存在,可能需要优化)
|
|
|
+│ │ └── useRequireAuth.ts (本故事新增)
|
|
|
+│ ├── pages/
|
|
|
+│ │ └── LoginPage/
|
|
|
+│ │ └── LoginPage.tsx
|
|
|
+│ ├── types/
|
|
|
+│ │ └── index.ts
|
|
|
+│ └── index.ts
|
|
|
+├── tests/
|
|
|
+│ ├── hooks/
|
|
|
+│ │ ├── useAuth.test.tsx (可能需要更新)
|
|
|
+│ │ └── useRequireAuth.test.tsx (本故事新增)
|
|
|
+│ └── pages/
|
|
|
+│ └── LoginPage/
|
|
|
+│ └── LoginPage.test.tsx
|
|
|
+├── package.json
|
|
|
+├── jest.config.cjs
|
|
|
+└── tsconfig.json
|
|
|
+```
|
|
|
+
|
|
|
+### 需要认证保护的页面
|
|
|
+
|
|
|
+**mini-talent/src/pages/**:
|
|
|
+1. `index/index.tsx` - 首页/个人主页 ✅ 需要保护
|
|
|
+2. `attendance/index.tsx` - 考勤记录 ✅ 需要保护
|
|
|
+3. `personal-info/index.tsx` - 个人信息 ✅ 需要保护
|
|
|
+4. `employment/index.tsx` - 就业信息 ✅ 需要保护
|
|
|
+5. `settings/index.tsx` - 设置页 ✅ 需要保护
|
|
|
+6. `login/index.tsx` - 登录页 ❌ 不需要保护
|
|
|
+
|
|
|
+### 测试策略
|
|
|
+
|
|
|
+**来源**: [architecture/mini-ui-testing-standards.md](../architecture/mini-ui-testing-standards.md)
|
|
|
+
|
|
|
+**测试框架**: Jest (mini项目使用Jest,不是Vitest!)
|
|
|
+
|
|
|
+**测试要求**:
|
|
|
+1. 测试`useRequireAuth` hook的功能
|
|
|
+2. 测试登录状态验证逻辑
|
|
|
+3. 测试未登录时的提示和跳转
|
|
|
+4. 测试loading状态避免重复跳转
|
|
|
+5. 测试页面级认证保护
|
|
|
+6. 运行`pnpm typecheck`确保类型检查通过
|
|
|
+
|
|
|
+**Mock Taro API**:
|
|
|
+```typescript
|
|
|
+const mockReLaunch = jest.fn()
|
|
|
+Taro.reLaunch = mockReLaunch
|
|
|
+const mockToast = jest.fn()
|
|
|
+Taro.showToast = mockToast
|
|
|
+```
|
|
|
+
|
|
|
+### 技术约束
|
|
|
+
|
|
|
+1. **向后兼容**: 不影响现有mini-talent项目功能
|
|
|
+2. **类型安全**: 使用TypeScript严格模式,所有hook必须有类型定义
|
|
|
+3. **模块独立性**: hook遵循UI包内部导入规范
|
|
|
+4. **测试覆盖**: 所有新增代码必须有测试覆盖
|
|
|
+5. **代码规范**: 遵循Mini UI包开发规范
|
|
|
+
|
|
|
+### 风险和缓解措施
|
|
|
+
|
|
|
+**主要风险:**
|
|
|
+1. **页面跳转循环风险**: 登录页使用`useRequireAuth`可能导致死循环
|
|
|
+2. **Loading状态不一致**: AuthContext和`useRequireAuth`的loading状态可能不同步
|
|
|
+3. **页面栈管理**: 使用错误的跳转方法可能导致页面栈混乱
|
|
|
+
|
|
|
+**缓解措施:**
|
|
|
+1. **明确不在登录页使用**: 在文档中明确说明登录页不使用`useRequireAuth`
|
|
|
+2. **统一loading状态**: 使用AuthContext的loading状态作为唯一来源
|
|
|
+3. **使用正确的跳转方法**: 登录页使用`Taro.reLaunch`清除页面栈
|
|
|
+4. **测试覆盖**: 编写完整的测试验证各种场景
|
|
|
+
|
|
|
+## 测试
|
|
|
+
|
|
|
+### 测试框架和模式
|
|
|
+
|
|
|
+**来源**: [architecture/testing-strategy.md](../architecture/testing-strategy.md)
|
|
|
+
|
|
|
+**测试框架:**
|
|
|
+- **Jest**: 30.2.0 (mini项目使用Jest,不是Vitest!)
|
|
|
+- **ts-jest**: 29.4.5 (TypeScript预处理器)
|
|
|
+- **@testing-library/react**: 16.3.0 (React组件测试)
|
|
|
+- **@d8d/mini-testing-utils**: workspace包 (Taro小程序测试工具)
|
|
|
+
|
|
|
+### 测试要求
|
|
|
+
|
|
|
+1. **Hook测试**:
|
|
|
+ - 测试`useRequireAuth`的登录状态检查
|
|
|
+ - 测试未登录时的提示显示
|
|
|
+ - 测试未登录时的页面跳转
|
|
|
+ - 测试loading状态避免重复跳转
|
|
|
+
|
|
|
+2. **集成测试**:
|
|
|
+ - 测试AuthContext启动时的登录状态验证
|
|
|
+ - 测试token存在时的自动登录
|
|
|
+ - 测试token过期时的处理
|
|
|
+ - 测试页面级认证保护
|
|
|
+
|
|
|
+3. **回归测试**:
|
|
|
+ - 验证现有功能不受影响
|
|
|
+ - 运行`pnpm typecheck`确保类型检查通过
|
|
|
+
|
|
|
+### 测试执行
|
|
|
+
|
|
|
+```bash
|
|
|
+# 运行所有测试
|
|
|
+cd mini-ui-packages/rencai-auth-ui && pnpm test
|
|
|
+
|
|
|
+# 运行特定测试
|
|
|
+pnpm test --testNamePattern="useRequireAuth"
|
|
|
+
|
|
|
+# 生成覆盖率报告
|
|
|
+pnpm test:coverage
|
|
|
+```
|
|
|
+
|
|
|
+## 变更日志
|
|
|
+
|
|
|
+| 日期 | 版本 | 描述 | 作者 |
|
|
|
+|------|------|------|------|
|
|
|
+| 2025-12-26 | 1.0 | 创建故事文档 | James (Dev Agent) |
|
|
|
+
|
|
|
+## 开发者记录
|
|
|
+
|
|
|
+*此部分由开发代理在实施过程中填写*
|
|
|
+
|
|
|
+### 使用的代理模型
|
|
|
+
|
|
|
+Claude Sonnet (claude-sonnet-4-20250514)
|
|
|
+
|
|
|
+### 调试日志引用
|
|
|
+
|
|
|
+待实施时填写
|
|
|
+
|
|
|
+### 完成说明列表
|
|
|
+
|
|
|
+待实施时填写
|
|
|
+
|
|
|
+### 文件列表
|
|
|
+
|
|
|
+待实施时填写
|
|
|
+
|
|
|
+## QA结果
|
|
|
+
|
|
|
+*此部分由QA代理在审查完成后填写*
|