Kaynağa Gözat

feat(story): 完成故事015.002 - 人才用户认证API扩展

- 添加人才用户登录接口(支持身份证号/残疾证号登录)
- 添加人才用户退出登录接口
- 添加获取人才用户信息接口(包含残疾人详情)
- 创建人才认证中间件,验证UserType.TALENT
- 扩展AuthService添加talentLogin和verifyTalentUser方法
- 扩展UserService添加残疾人查询相关方法
- 添加集成测试,16个测试全部通过
- 添加core-module对order-module和bank-names-module的依赖

测试结果: 16/16 通过
- 身份证号/残疾证号登录成功
- 密码错误/不存在用户/禁用账户正确拒绝
- 获取用户信息包含personInfo字段
- 退出登录成功
- 认证中间件正确识别人才用户

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 3 hafta önce
ebeveyn
işleme
5fffcd6b53

+ 74 - 34
docs/stories/015.002.story.md

@@ -1,7 +1,7 @@
 # Story 015.002: 人才用户认证API扩展
 
 ## Status
-Approved
+Ready for Review
 
 ## Story
 **作为** 人才用户,
@@ -18,39 +18,39 @@ Approved
 7. API文档完整,包含OpenAPI定义和TypeScript类型
 
 ## Tasks / Subtasks
-- [ ] 任务1:扩展auth-module,添加人才用户登录接口 (AC: 1)
-  - [ ] 创建人才登录Schema验证(支持身份证号/残疾证号+密码登录)
-  - [ ] 创建人才登录服务方法(验证身份证号/残疾证号和密码)
-  - [ ] 创建人才登录路由 `POST /api/v1/rencai/auth/login`
-  - [ ] 基于users2表的person_id字段关联disabled_person表验证
-  - [ ] 生成JWT token并返回用户基本信息
-
-- [ ] 任务2:添加人才用户退出登录接口 (AC: 3)
-  - [ ] 创建退出登录路由 `POST /api/v1/rencai/auth/logout`
-  - [ ] 实现token失效处理(可选:Redis黑名单或延长过期时间)
-
-- [ ] 任务3:添加获取人才用户信息接口 (AC: 2)
-  - [ ] 创建获取用户信息路由 `GET /api/v1/rencai/auth/me`
-  - [ ] 关联查询disabled_person表获取人才详情
-  - [ ] 返回用户基本信息和人才详细信息
-
-- [ ] 任务4:更新认证中间件,支持人才用户身份识别 (AC: 4)
-  - [ ] 扩展现有auth.middleware.ts,识别UserType.TALENT
-  - [ ] 确保人才用户token验证逻辑正确
-  - [ ] 添加人才用户权限验证逻辑
-
-- [ ] 任务5:编写单元测试和集成测试 (AC: 6)
-  - [ ] 测试人才用户登录成功场景(身份证号登录)
-  - [ ] 测试人才用户登录成功场景(残疾证号登录)
-  - [ ] 测试登录失败场景(错误的身份证号/残疾证号或密码)
-  - [ ] 测试获取用户信息接口
-  - [ ] 测试认证中间件识别人才用户
-  - [ ] 测试权限验证逻辑
-
-- [ ] 任务6:添加OpenAPI文档和TypeScript类型 (AC: 7)
-  - [ ] 为所有新接口添加OpenAPI元数据
-  - [ ] 定义请求/响应Schema
-  - [ ] 确保类型安全导出
+- [x] 任务1:扩展auth-module,添加人才用户登录接口 (AC: 1)
+  - [x] 创建人才登录Schema验证(支持身份证号/残疾证号+密码登录)
+  - [x] 创建人才登录服务方法(验证身份证号/残疾证号和密码)
+  - [x] 创建人才登录路由 `POST /api/v1/rencai/auth/login`
+  - [x] 基于users2表的person_id字段关联disabled_person表验证
+  - [x] 生成JWT token并返回用户基本信息
+
+- [x] 任务2:添加人才用户退出登录接口 (AC: 3)
+  - [x] 创建退出登录路由 `POST /api/v1/rencai/auth/logout`
+  - [x] 实现token失效处理(可选:Redis黑名单或延长过期时间)
+
+- [x] 任务3:添加获取人才用户信息接口 (AC: 2)
+  - [x] 创建获取用户信息路由 `GET /api/v1/rencai/auth/me`
+  - [x] 关联查询disabled_person表获取人才详情
+  - [x] 返回用户基本信息和人才详细信息
+
+- [x] 任务4:更新认证中间件,支持人才用户身份识别 (AC: 4)
+  - [x] 扩展现有auth.middleware.ts,识别UserType.TALENT
+  - [x] 确保人才用户token验证逻辑正确
+  - [x] 添加人才用户权限验证逻辑
+
+- [x] 任务5:编写单元测试和集成测试 (AC: 6)
+  - [x] 测试人才用户登录成功场景(身份证号登录)
+  - [x] 测试人才用户登录成功场景(残疾证号登录)
+  - [x] 测试登录失败场景(错误的身份证号/残疾证号或密码)
+  - [x] 测试获取用户信息接口
+  - [x] 测试认证中间件识别人才用户
+  - [x] 测试权限验证逻辑
+
+- [x] 任务6:添加OpenAPI文档和TypeScript类型 (AC: 7)
+  - [x] 为所有新接口添加OpenAPI元数据
+  - [x] 定义请求/响应Schema
+  - [x] 确保类型安全导出
 
 ## Dev Notes
 
@@ -285,17 +285,57 @@ const TalentLoginResponseSchema = z.object({
 |------|---------|-------------|--------|
 | 2025-12-24 | 1.0 | 初始故事创建 | Scrum Master |
 | 2025-12-24 | 1.1 | 移除自助注册功能,明确人才用户由管理员创建 | Scrum Master |
+| 2025-12-24 | 1.2 | 完成实施 - 添加人才用户认证API | Dev Agent |
+| 2025-12-25 | 1.3 | 完成测试验证和DoD检查 - 16/16测试通过 | Dev Agent |
 
 ## Dev Agent Record
 *此部分由开发代理在实施过程中填写*
 
 ### Agent Model Used
+claude-sonnet (via BMad Dev Agent)
 
 ### Debug Log References
+无重大调试问题
 
 ### Completion Notes List
+1. 创建了人才认证Schema (rencai-auth.schema.ts) - 支持身份证号/残疾证号登录
+2. 扩展AuthService添加talentLogin方法
+3. 扩展UserService添加残疾人查询方法
+4. 创建人才认证路由 (login, logout, me)
+5. 创建人才认证中间件 (talent-auth.middleware.ts)
+6. 添加core-module对disability-module、order-module和bank-names-module的依赖
+7. 创建集成测试框架和测试数据工厂
+8. 修正中间件使用方式 - 在createRoute的middleware字段中指定,而非.use()
+9. 修复person到personInfo的字段映射问题
+
+注意事项:
+- 集成测试需要完整的实体依赖链,由于DisabledPerson实体有多个关联实体(OrderPerson、EmploymentOrder、BankName等),测试setup需要包含所有依赖
+- 类型安全:使用RPC推断类型而非直接导入schema类型(遵循UI包开发规范)
+- 测试需要实际数据库连接以验证残疾人和用户的关联关系
+- Hono中间件应在createRoute定义中使用middleware字段,而非在路由聚合时使用.use()
+- UserEntity的person关系需要手动映射为响应中的personInfo字段
 
 ### File List
+**新增文件:**
+- `packages/core-module/auth-module/src/schemas/rencai-auth.schema.ts` - 人才认证Schema定义
+- `packages/core-module/auth-module/src/routes/rencai/login.route.ts` - 人才登录路由
+- `packages/core-module/auth-module/src/routes/rencai/logout.route.ts` - 人才登出路由
+- `packages/core-module/auth-module/src/routes/rencai/me.route.ts` - 获取人才用户信息路由
+- `packages/core-module/auth-module/src/routes/rencai-auth.routes.ts` - 人才认证路由聚合
+- `packages/core-module/auth-module/src/middleware/talent-auth.middleware.ts` - 人才认证中间件
+- `packages/core-module/auth-module/tests/integration/talent-auth.integration.test.ts` - 集成测试
+- `packages/bank-names-module/src/entities/index.ts` - BankName实体导出文件(用于测试)
+
+**修改文件:**
+- `packages/core-module/auth-module/src/schemas/index.ts` - 添加人才Schema导出
+- `packages/core-module/auth-module/src/routes/index.ts` - 添加人才路由导出
+- `packages/core-module/auth-module/src/middleware/index.ts` - 添加人才中间件导出
+- `packages/core-module/auth-module/src/services/auth.service.ts` - 添加talentLogin和verifyTalentUser方法
+- `packages/core-module/user-module/src/services/user.service.ts` - 添加残疾人查询相关方法
+- `packages/core-module/user-module/src/entities/user.entity.ts` - 添加disability-module导入
+- `packages/core-module/package.json` - 添加allin-order-module和bank-names-module依赖
+- `packages/core-module/auth-module/tests/utils/test-data-factory.ts` - 添加残疾人测试数据工厂方法
+- `packages/core-module/vitest.config.ts` - 临时添加别名配置(后移除)
 
 ## QA Results
 *此部分由QA代理在审查完成后填写*

+ 1 - 0
packages/bank-names-module/src/entities/index.ts

@@ -0,0 +1 @@
+export { BankName } from './bank-name.entity';

+ 2 - 1
packages/core-module/auth-module/src/middleware/index.ts

@@ -1,2 +1,3 @@
 export { authMiddleware } from './auth.middleware';
-export { enterpriseAuthMiddleware } from './enterprise-auth.middleware';
+export { enterpriseAuthMiddleware } from './enterprise-auth.middleware';
+export { talentAuthMiddleware } from './talent-auth.middleware';

+ 67 - 0
packages/core-module/auth-module/src/middleware/talent-auth.middleware.ts

@@ -0,0 +1,67 @@
+import { Context, Next } from 'hono';
+import { AuthService } from '../services/index';
+import { UserService } from '../../../user-module/src/services/index';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext, UserType } from '@d8d/shared-types';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { TalentUserResponseSchema } from '../schemas/index';
+
+export async function talentAuthMiddleware(c: Context<AuthContext>, next: Next) {
+  try {
+    const authHeader = c.req.header('Authorization');
+    if (!authHeader) {
+      return c.json({ message: 'Authorization header missing' }, 401);
+    }
+
+    const tokenParts = authHeader.split(' ');
+    if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
+      return c.json({ message: 'Authorization header missing' }, 401);
+    }
+
+    const token = tokenParts[1];
+    if (!token) {
+      return c.json({ message: 'Token missing' }, 401);
+    }
+
+    const userService = new UserService(AppDataSource);
+    const authService = new AuthService(userService);
+    const decoded = authService.verifyToken(token);
+
+    // 获取用户信息,包含残疾人详细信息
+    const user = await userService.getUserWithDisabledPerson(decoded.id);
+
+    if (!user) {
+      return c.json({ message: 'User not found' }, 401);
+    }
+
+    // 验证用户是否是人才用户
+    if (user.userType !== UserType.TALENT) {
+      return c.json({ message: 'User is not a talent user' }, 403);
+    }
+
+    // 设置用户上下文(包含残疾人详情)
+    const userData = {
+      id: user.id,
+      username: user.username,
+      userType: user.userType,
+      personId: user.personId,
+      phone: user.phone,
+      nickname: user.nickname,
+      name: user.name,
+      avatarFileId: user.avatarFileId,
+      isDisabled: user.isDisabled,
+      createdAt: user.createdAt,
+      updatedAt: user.updatedAt,
+      // 将person关系映射为personInfo
+      personInfo: user.person || null
+    };
+
+    c.set('user', userData);
+    c.set('token', token);
+
+    await next();
+  } catch (error) {
+    console.error('Talent authentication error:', error);
+    return c.json({ message: 'Invalid token or insufficient permissions' }, 401);
+  }
+}

+ 5 - 1
packages/core-module/auth-module/src/routes/index.ts

@@ -11,6 +11,7 @@ import phoneDecryptRoute from './phone-decrypt.route';
 import enterpriseLoginRoute from './enterprise-login.route';
 import enterpriseLogoutRoute from './enterprise-logout.route';
 import enterpriseMeRoute from './enterprise-me.route';
+import { rencaiAuthRoutes } from './rencai-auth.routes';
 
 // 创建统一的路由应用
 const authRoutes = new OpenAPIHono<AuthContext>()
@@ -32,5 +33,8 @@ const enterpriseAuthRoutes = new OpenAPIHono<AuthContext>()
   .route('/', enterpriseLogoutRoute)
   .route('/', enterpriseMeRoute);
 
-export { authRoutes, enterpriseAuthRoutes };
+// 人才用户认证路由
+const talentAuthRoutes = rencaiAuthRoutes;
+
+export { authRoutes, enterpriseAuthRoutes, talentAuthRoutes };
 export default authRoutes;

+ 13 - 0
packages/core-module/auth-module/src/routes/rencai-auth.routes.ts

@@ -0,0 +1,13 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { AuthContext } from '@d8d/shared-types';
+import talentLoginRoute from './rencai/login.route';
+import talentLogoutRoute from './rencai/logout.route';
+import talentMeRoute from './rencai/me.route';
+
+// 人才认证路由聚合
+const rencaiAuthRoutes = new OpenAPIHono<AuthContext>()
+  .route('/', talentLoginRoute)
+  .route('/', talentLogoutRoute)
+  .route('/', talentMeRoute);
+
+export { rencaiAuthRoutes };

+ 85 - 0
packages/core-module/auth-module/src/routes/rencai/login.route.ts

@@ -0,0 +1,85 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { AuthService } from '../../services/index';
+import { UserService } from '@d8d/core-module/user-module';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { TalentLoginSchema, TalentLoginResponseSchema } from '../../schemas/index';
+
+const talentLoginRoute = createRoute({
+  method: 'post',
+  path: '/login',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: TalentLoginSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '人才用户登录成功',
+      content: {
+        'application/json': {
+          schema: TalentLoginResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '身份证号/残疾证号或密码错误,或用户不是人才用户',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(talentLoginRoute, async (c) => {
+  try {
+    // 在路由处理函数内部初始化服务
+    const userService = new UserService(AppDataSource);
+    const authService = new AuthService(userService);
+
+    const { identifier, password } = c.req.valid('json');
+
+    // 调用人才用户登录方法
+    const result = await authService.talentLogin(identifier, password);
+
+    return c.json(await parseWithAwait(TalentLoginResponseSchema, result), 200);
+  } catch (error) {
+    // 认证相关错误返回401
+    if (error instanceof Error &&
+        (error.message.includes('用户不存在') ||
+         error.message.includes('密码错误') ||
+         error.message.includes('账户已禁用') ||
+         error.message.includes('用户不是人才用户'))) {
+      return c.json(
+        {
+          code: 401,
+          message: error.message.includes('账户已禁用') ? '账户已禁用' :
+                   error.message.includes('用户不是人才用户') ? '用户不是人才用户' :
+                   '身份证号或密码错误'
+        },
+        401
+      );
+    }
+
+    // 其他错误重新抛出,由错误处理中间件处理
+    throw error;
+  }
+});
+
+export default app;

+ 63 - 0
packages/core-module/auth-module/src/routes/rencai/logout.route.ts

@@ -0,0 +1,63 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { AuthService } from '../../services/index';
+import { UserService } from '@d8d/core-module/user-module';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { TalentLogoutResponseSchema } from '../../schemas/index';
+import { talentAuthMiddleware } from '../../middleware/index';
+
+const talentLogoutRoute = createRoute({
+  method: 'post',
+  path: '/logout',
+  security: [
+    {
+      Bearer: []
+    }
+  ],
+  middleware: talentAuthMiddleware,
+  responses: {
+    200: {
+      description: '登出成功',
+      content: {
+        'application/json': {
+          schema: TalentLogoutResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权或token无效',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(talentLogoutRoute, async (c) => {
+  try {
+    const token = c.get('token');
+    if (!token) {
+      return c.json({ code: 401, message: '未授权' }, 401);
+    }
+
+    const userService = new UserService(AppDataSource);
+    const authService = new AuthService(userService);
+
+    await authService.logout(token);
+
+    return c.json({ message: '登出成功' }, 200);
+  } catch (error) {
+    return c.json(
+      {
+        code: 500,
+        message: error instanceof Error ? error.message : '登出失败'
+      },
+      500
+    );
+  }
+});
+
+export default app;

+ 92 - 0
packages/core-module/auth-module/src/routes/rencai/me.route.ts

@@ -0,0 +1,92 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { UserService } from '@d8d/core-module/user-module';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { TalentMeResponseSchema } from '../../schemas/index';
+import { talentAuthMiddleware } from '../../middleware/index';
+
+const talentMeRoute = createRoute({
+  method: 'get',
+  path: '/me',
+  security: [
+    {
+      Bearer: []
+    }
+  ],
+  middleware: talentAuthMiddleware,
+  responses: {
+    200: {
+      description: '获取人才用户信息成功',
+      content: {
+        'application/json': {
+          schema: TalentMeResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权或token无效',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '用户不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(talentMeRoute, async (c) => {
+  try {
+    const user = c.get('user');
+    if (!user) {
+      return c.json({ code: 401, message: '未授权' }, 401);
+    }
+
+    const userService = new UserService(AppDataSource);
+
+    // 获取包含残疾人详细信息的用户数据
+    const userWithPerson = await userService.getUserWithDisabledPerson(user.id);
+
+    if (!userWithPerson) {
+      return c.json({ code: 404, message: '用户不存在' }, 404);
+    }
+
+    // 构建符合Schema的响应对象
+    const userResponse = {
+      id: userWithPerson.id,
+      username: userWithPerson.username,
+      userType: userWithPerson.userType,
+      personId: userWithPerson.personId,
+      phone: userWithPerson.phone,
+      nickname: userWithPerson.nickname,
+      name: userWithPerson.name,
+      avatarFileId: userWithPerson.avatarFileId,
+      isDisabled: userWithPerson.isDisabled,
+      createdAt: userWithPerson.createdAt,
+      updatedAt: userWithPerson.updatedAt,
+      // 将person关系映射为personInfo
+      personInfo: userWithPerson.person || null
+    };
+
+    return c.json(await parseWithAwait(TalentMeResponseSchema, userResponse), 200);
+  } catch (error) {
+    return c.json(
+      {
+        code: 500,
+        message: error instanceof Error ? error.message : '获取用户信息失败'
+      },
+      500
+    );
+  }
+});
+
+export default app;

+ 10 - 1
packages/core-module/auth-module/src/schemas/index.ts

@@ -11,4 +11,13 @@ export {
   EnterpriseLoginSchema,
   EnterpriseUserResponseSchema,
   EnterpriseTokenResponseSchema
-} from './auth.schema';
+} from './auth.schema';
+
+export {
+  TalentLoginSchema,
+  TalentPersonInfoSchema,
+  TalentUserResponseSchema,
+  TalentLoginResponseSchema,
+  TalentMeResponseSchema,
+  TalentLogoutResponseSchema
+} from './rencai-auth.schema';

+ 130 - 0
packages/core-module/auth-module/src/schemas/rencai-auth.schema.ts

@@ -0,0 +1,130 @@
+import { z } from '@hono/zod-openapi';
+import { UserType } from '@d8d/shared-types';
+
+// 人才用户登录请求Schema
+export const TalentLoginSchema = z.object({
+  identifier: z.string().min(1, '身份证号或残疾证号不能为空').openapi({
+    example: '110101199001011234',
+    description: '身份证号或残疾证号'
+  }),
+  password: z.string().min(6, '密码至少6个字符').openapi({
+    example: 'password123',
+    description: '密码'
+  })
+});
+
+// 人才基本信息Schema(来自disabled_person表)
+export const TalentPersonInfoSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '残疾人ID(person_id)',
+    example: 1
+  }),
+  name: z.string().max(50).openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  gender: z.string().length(1).openapi({
+    description: '性别:男/女',
+    example: '男'
+  }),
+  idCard: z.string().max(20).openapi({
+    description: '身份证号',
+    example: '110101199001011234'
+  }),
+  disabilityId: z.string().max(50).openapi({
+    description: '残疾证号',
+    example: 'D12345678'
+  }),
+  disabilityType: z.string().max(50).openapi({
+    description: '残疾类型',
+    example: '肢体残疾'
+  }),
+  disabilityLevel: z.string().max(20).openapi({
+    description: '残疾等级',
+    example: '一级'
+  }),
+  phone: z.string().max(20).openapi({
+    description: '联系方式',
+    example: '13800138000'
+  }),
+  province: z.string().max(50).openapi({
+    description: '省级',
+    example: '北京市'
+  }),
+  city: z.string().max(50).openapi({
+    description: '市级',
+    example: '北京市'
+  }),
+  district: z.string().max(50).nullable().openapi({
+    description: '区县级',
+    example: '朝阳区'
+  }),
+  detailedAddress: z.string().max(200).nullable().openapi({
+    description: '详细地址',
+    example: '某某街道123号'
+  }),
+  jobStatus: z.number().int().openapi({
+    description: '在职状态:0-未在职,1-已在职',
+    example: 0
+  })
+});
+
+// 人才用户响应Schema
+export const TalentUserResponseSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  username: z.string().min(3).max(255).openapi({
+    description: '用户名',
+    example: 'talent_user'
+  }),
+  userType: z.nativeEnum(UserType).openapi({
+    description: '用户类型',
+    example: UserType.TALENT
+  }),
+  personId: z.number().int().positive().nullable().openapi({
+    description: '残疾人ID(关联disabled_person.person_id)',
+    example: 1
+  }),
+  phone: z.string().max(255).nullable().openapi({
+    description: '手机号',
+    example: '13800138000'
+  }),
+  nickname: z.string().max(255).nullable().openapi({
+    description: '昵称',
+    example: '人才用户'
+  }),
+  name: z.string().max(255).nullable().openapi({
+    description: '真实姓名',
+    example: '张三'
+  }),
+  avatarFileId: z.number().int().positive().nullable().openapi({
+    description: '头像文件ID',
+    example: 1
+  }),
+  // 关联的残疾人详细信息
+  personInfo: TalentPersonInfoSchema.nullable().openapi({
+    description: '残疾人详细信息(来自disabled_person表)'
+  })
+});
+
+// 人才用户登录响应Schema
+export const TalentLoginResponseSchema = z.object({
+  token: z.string().openapi({
+    example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
+    description: 'JWT Token'
+  }),
+  user: TalentUserResponseSchema
+});
+
+// 获取人才用户信息响应Schema(复用用户响应Schema)
+export const TalentMeResponseSchema = TalentUserResponseSchema;
+
+// 登出成功响应Schema
+export const TalentLogoutResponseSchema = z.object({
+  message: z.string().openapi({
+    example: '登出成功',
+    description: '操作结果消息'
+  })
+});

+ 72 - 1
packages/core-module/auth-module/src/services/auth.service.ts

@@ -1,5 +1,5 @@
 import { UserService } from '@d8d/core-module/user-module';
-import { DisabledStatus } from '@d8d/shared-types';
+import { DisabledStatus, UserType } from '@d8d/shared-types';
 import { JWTUtil } from '@d8d/shared-utils';
 import debug from 'debug';
 
@@ -144,4 +144,75 @@ export class AuthService {
       return false;
     }
   }
+
+  /**
+   * 人才用户登录(使用身份证号或残疾证号)
+   */
+  async talentLogin(identifier: string, password: string): Promise<{ token: string; user: any }> {
+    try {
+      // 通过身份证号或残疾证号查找人才用户
+      const user = await this.userService.getTalentUserByIdentifier(identifier);
+      if (!user) {
+        throw new Error('用户不存在');
+      }
+
+      // 检查用户是否被禁用
+      if (user.isDisabled === DisabledStatus.DISABLED) {
+        throw new Error('账户已禁用');
+      }
+
+      // 验证用户类型是否为人才用户
+      if (user.userType !== UserType.TALENT) {
+        throw new Error('用户不是人才用户');
+      }
+
+      const isPasswordValid = await this.userService.verifyPassword(user, password);
+      if (!isPasswordValid) {
+        throw new Error('密码错误');
+      }
+
+      // 加载残疾人详细信息
+      const userWithPerson = await this.userService.getUserWithDisabledPerson(user.id);
+
+      const token = this.generateToken(userWithPerson || user);
+
+      // 构建符合Schema的响应对象
+      const userResponse = {
+        id: userWithPerson?.id || user.id,
+        username: userWithPerson?.username || user.username,
+        userType: userWithPerson?.userType || user.userType,
+        personId: userWithPerson?.personId || user.personId,
+        phone: userWithPerson?.phone || user.phone,
+        nickname: userWithPerson?.nickname || user.nickname,
+        name: userWithPerson?.name || user.name,
+        avatarFileId: userWithPerson?.avatarFileId || user.avatarFileId,
+        isDisabled: userWithPerson?.isDisabled || user.isDisabled,
+        createdAt: userWithPerson?.createdAt || user.createdAt,
+        updatedAt: userWithPerson?.updatedAt || user.updatedAt,
+        // 将person关系映射为personInfo
+        personInfo: userWithPerson?.person || null
+      };
+
+      return { token, user: userResponse };
+    } catch (error) {
+      logger.error('Talent login error:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 验证人才用户
+   */
+  async verifyTalentUser(userId: number): Promise<boolean> {
+    try {
+      const user = await this.userService.getUserById(userId);
+      if (!user) {
+        return false;
+      }
+      return user.userType === UserType.TALENT && !!user.personId;
+    } catch (error) {
+      logger.error('Verify talent user error:', error);
+      return false;
+    }
+  }
 }

+ 349 - 0
packages/core-module/auth-module/tests/integration/talent-auth.integration.test.ts

@@ -0,0 +1,349 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooksWithEntities,
+} from '@d8d/shared-test-util';
+import { Role, UserEntity, UserService } from '@d8d/core-module/user-module';
+import { File } from '@d8d/core-module/file-module';
+import { Company } from '@d8d/allin-company-module/entities';
+import { Platform } from '@d8d/allin-platform-module/entities';
+import {
+  DisabledPerson,
+  DisabledBankCard,
+  DisabledPhoto,
+  DisabledRemark,
+  DisabledVisit
+} from '@d8d/allin-disability-module/entities';
+import { BankName } from '@d8d/bank-names-module/entities';
+import { EmploymentOrder, OrderPerson, OrderPersonAsset } from '@d8d/allin-order-module/entities';
+import { talentAuthRoutes } from '../../src/routes/index';
+import { AuthService } from '../../src/services/index';
+import { DisabledStatus, UserType } from '@d8d/shared-types';
+import { TestDataFactory } from '../utils/test-data-factory';
+
+// 设置集成测试钩子,包含残疾人实体和公司实体(UserEntity依赖Company)
+setupIntegrationDatabaseHooksWithEntities([
+  UserEntity, Role, File,
+  DisabledPerson, DisabledBankCard, DisabledPhoto, DisabledRemark, DisabledVisit,
+  Company, Platform, BankName, EmploymentOrder, OrderPerson, OrderPersonAsset
+])
+
+describe('人才用户认证API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof talentAuthRoutes>>;
+  let authService: AuthService;
+  let userService: UserService;
+  let testToken: string;
+  let testUser: any;
+  let testPerson: any;
+  let testIdCard: string;
+  let testDisabilityId: string;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(talentAuthRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 确保数据源已初始化
+    if (!dataSource.isInitialized) {
+      await dataSource.initialize();
+    }
+
+    // 初始化服务
+    userService = new UserService(dataSource);
+    authService = new AuthService(userService);
+
+    // 创建测试人才用户(包含残疾人记录)
+    const talentData = await TestDataFactory.createTestTalentUser(
+      dataSource,
+      {
+        username: 'talent_user',
+        password: 'TalentPass123!',
+        email: 'talent@example.com',
+        phone: '13800138001'
+      },
+      {
+        name: '张三',
+        idCard: '110101199001011234',
+        disabilityId: 'D12345678',
+        phone: '13800138001'
+      }
+    );
+
+    testUser = talentData.user;
+    testPerson = talentData.person;
+    testIdCard = testPerson.idCard;
+    testDisabilityId = testPerson.disabilityId;
+
+    // 生成测试用户的token
+    testToken = authService.generateToken(testUser);
+  });
+
+  describe('人才用户登录端点测试 (POST /auth/login)', () => {
+    it('应该使用身份证号和正确密码成功登录', async () => {
+      const loginData = {
+        identifier: testIdCard,
+        password: 'TalentPass123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('token');
+        expect(responseData).toHaveProperty('user');
+        expect(responseData.user.username).toBe('talent_user');
+        expect(responseData.user.userType).toBe(UserType.TALENT);
+        expect(responseData.user.personId).toBe(testPerson.id);
+        expect(responseData.user.personInfo).toBeDefined();
+        expect(responseData.user.personInfo.name).toBe('张三');
+        expect(responseData.user.personInfo.idCard).toBe(testIdCard);
+        expect(typeof responseData.token).toBe('string');
+        expect(responseData.token.length).toBeGreaterThan(0);
+      }
+    });
+
+    it('应该使用残疾证号和正确密码成功登录', async () => {
+      const loginData = {
+        identifier: testDisabilityId,
+        password: 'TalentPass123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('token');
+        expect(responseData).toHaveProperty('user');
+        expect(responseData.user.username).toBe('talent_user');
+        expect(responseData.user.personInfo).toBeDefined();
+        expect(responseData.user.personInfo.disabilityId).toBe(testDisabilityId);
+      }
+    });
+
+    it('应该拒绝错误密码的登录', async () => {
+      const loginData = {
+        identifier: testIdCard,
+        password: 'WrongPassword123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('身份证号或密码错误');
+      }
+    });
+
+    it('应该拒绝不存在的身份证号登录', async () => {
+      const loginData = {
+        identifier: '999999999999999999',
+        password: 'TalentPass123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('身份证号或密码错误');
+      }
+    });
+
+    it('应该拒绝非人才用户登录(用户类型不是talent)', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      // 创建非人才用户
+      await TestDataFactory.createTestUser(dataSource, {
+        username: 'non_talent_user',
+        password: 'Password123!',
+        email: 'non_talent@example.com',
+        phone: '13800138002',
+        userType: UserType.ADMIN,
+        personId: testPerson.id  // 关联残疾人记录但用户类型不是talent
+      });
+
+      const loginData = {
+        identifier: testIdCard,
+        password: 'Password123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        // 由于getTalentUserByIdentifier只查询TALENT类型用户,非人才用户会被视为不存在
+        // 这是更安全的做法,不透露用户存在性信息
+        expect(responseData.message).toContain('身份证号或密码错误');
+      }
+    });
+
+    it('应该拒绝禁用账户的登录', async () => {
+      // 禁用测试用户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userRepository = dataSource.getRepository(UserEntity);
+      await userRepository.update(testUser.id, { isDisabled: DisabledStatus.DISABLED });
+
+      const loginData = {
+        identifier: testIdCard,
+        password: 'TalentPass123!'
+      };
+
+      const response = await client.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('账户已禁用');
+      }
+    });
+  });
+
+  describe('人才用户信息端点测试 (GET /auth/me)', () => {
+    it('应该成功获取人才用户信息,包含残疾人详细信息', async () => {
+      const response = await client.me.$get({},{
+        headers: {
+          Authorization: `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.username).toBe('talent_user');
+        expect(responseData.userType).toBe(UserType.TALENT);
+        expect(responseData.personId).toBe(testPerson.id);
+        expect(responseData.personInfo).toBeDefined();
+        expect(responseData.personInfo.name).toBe('张三');
+        expect(responseData.personInfo.idCard).toBe(testIdCard);
+        expect(responseData.personInfo.disabilityId).toBe(testDisabilityId);
+        expect(responseData.personInfo.phone).toBe('13800138001');
+      }
+    });
+
+    it('应该拒绝无效令牌的访问', async () => {
+      const response = await client.me.$get({},{
+        headers: {
+          Authorization: 'Bearer invalid_token'
+        }
+      });
+
+      expect(response.status).toBe(401);
+    });
+
+    it('应该拒绝缺少令牌的访问', async () => {
+      const response = await client.me.$get();
+
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('人才用户退出登录端点测试 (POST /auth/logout)', () => {
+    it('应该成功退出登录', async () => {
+      const response = await client.logout.$post({},{
+        headers: {
+          Authorization: `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.message).toBe('登出成功');
+      }
+    });
+
+    it('应该拒绝无效令牌的退出登录', async () => {
+      const response = await client.logout.$post({},{
+        headers: {
+          Authorization: 'Bearer invalid_token'
+        }
+      });
+
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('人才用户权限验证', () => {
+    it('verifyTalentUser方法应该正确识别人才用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+      const authService = new AuthService(userService);
+
+      const isTalentUser = await authService.verifyTalentUser(testUser.id);
+      expect(isTalentUser).toBe(true);
+
+      // 创建非人才用户测试
+      const nonTalentUserData = await TestDataFactory.createTestUser(dataSource, {
+        username: 'non_talent_user2',
+        password: 'Password123!',
+        email: 'non_talent2@example.com',
+        phone: '13800138003',
+        userType: UserType.ADMIN
+      });
+
+      const isNonTalentUser = await authService.verifyTalentUser(nonTalentUserData.id);
+      expect(isNonTalentUser).toBe(false);
+    });
+  });
+
+  describe('数据查询方法测试', () => {
+    it('getDisabledPersonByIdentifier应该能通过身份证号查询残疾人', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+
+      const person = await userService.getDisabledPersonByIdentifier(testIdCard);
+      expect(person).toBeDefined();
+      expect(person?.id).toBe(testPerson.id);
+      expect(person?.name).toBe('张三');
+    });
+
+    it('getDisabledPersonByIdentifier应该能通过残疾证号查询残疾人', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+
+      const person = await userService.getDisabledPersonByIdentifier(testDisabilityId);
+      expect(person).toBeDefined();
+      expect(person?.id).toBe(testPerson.id);
+      expect(person?.name).toBe('张三');
+    });
+
+    it('getUserByPersonId应该能通过person_id查询用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+
+      const user = await userService.getUserByPersonId(testPerson.id);
+      expect(user).toBeDefined();
+      expect(user?.id).toBe(testUser.id);
+      expect(user?.username).toBe('talent_user');
+    });
+
+    it('getTalentUserByIdentifier应该能通过身份证号查询完整人才用户信息', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+
+      const user = await userService.getTalentUserByIdentifier(testIdCard);
+      expect(user).toBeDefined();
+      expect(user?.id).toBe(testUser.id);
+      expect(user?.personId).toBe(testPerson.id);
+      expect(user?.userType).toBe(UserType.TALENT);
+    });
+  });
+});

+ 58 - 0
packages/core-module/auth-module/tests/utils/test-data-factory.ts

@@ -1,6 +1,8 @@
 import { DataSource } from 'typeorm';
 import { UserEntity } from '@d8d/core-module/user-module';
 import { Role } from '@d8d/core-module/user-module';
+import { DisabledPerson } from '@d8d/allin-disability-module/entities';
+import { UserType } from '@d8d/shared-types';
 
 /**
  * 测试数据工厂类
@@ -57,4 +59,60 @@ export class TestDataFactory {
     const role = roleRepository.create(roleData);
     return await roleRepository.save(role);
   }
+
+  /**
+   * 创建测试残疾人数据
+   */
+  static createDisabledPersonData(overrides: Partial<DisabledPerson> = {}): Partial<DisabledPerson> {
+    const timestamp = Date.now();
+    return {
+      name: `测试残疾人${timestamp}`,
+      gender: '男',
+      idCard: `11010119900101001${timestamp.toString().slice(-2)}`,
+      disabilityId: `D123456${timestamp}`,
+      disabilityType: '肢体残疾',
+      disabilityLevel: '一级',
+      idAddress: '北京市朝阳区',
+      phone: `138${timestamp.toString().slice(-8)}`,
+      province: '北京市',
+      city: '北京市',
+      district: '朝阳区',
+      canDirectContact: 1,
+      isInBlackList: 0,
+      jobStatus: 0,
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试残疾人
+   */
+  static async createTestDisabledPerson(dataSource: DataSource, overrides: Partial<DisabledPerson> = {}): Promise<DisabledPerson> {
+    const personData = this.createDisabledPersonData(overrides);
+    const personRepository = dataSource.getRepository(DisabledPerson);
+
+    const person = personRepository.create(personData);
+    return await personRepository.save(person);
+  }
+
+  /**
+   * 创建测试人才用户(包含残疾人关联)
+   */
+  static async createTestTalentUser(dataSource: DataSource, overrides: Partial<UserEntity> = {}, personOverrides: Partial<DisabledPerson> = {}): Promise<{ user: UserEntity; person: DisabledPerson }> {
+    // 先创建残疾人记录
+    const person = await this.createTestDisabledPerson(dataSource, personOverrides);
+
+    // 创建人才用户,关联残疾人记录
+    const userData = this.createUserData({
+      ...overrides,
+      userType: UserType.TALENT,
+      personId: person.id
+    });
+
+    const userRepository = dataSource.getRepository(UserEntity);
+    const user = userRepository.create(userData);
+    const savedUser = await userRepository.save(user);
+
+    return { user: savedUser, person };
+  }
 }

+ 3 - 0
packages/core-module/package.json

@@ -154,6 +154,9 @@
     "@d8d/shared-test-util": "workspace:*",
     "@d8d/allin-company-module": "workspace:*",
     "@d8d/allin-platform-module": "workspace:*",
+    "@d8d/allin-disability-module": "workspace:*",
+    "@d8d/allin-order-module": "workspace:*",
+    "@d8d/bank-names-module": "workspace:*",
     "@hono/zod-openapi": "1.0.2",
     "axios": "^1.12.2",
     "bcrypt": "^6.0.0",

+ 75 - 0
packages/core-module/user-module/src/services/user.service.ts

@@ -2,6 +2,8 @@ import { DataSource } from 'typeorm';
 import { UserEntity } from '../entities/user.entity';
 import { RoleService } from './role.service';
 import { ConcreteCrudService } from '@d8d/shared-crud';
+import { DisabledPerson } from '@d8d/allin-disability-module/entities';
+import { UserType } from '@d8d/shared-types';
 import * as bcrypt from 'bcrypt';
 
 const SALT_ROUNDS = 10;
@@ -202,4 +204,77 @@ export class UserService extends ConcreteCrudService<UserEntity> {
       throw new Error(`Failed to get user with company: ${error instanceof Error ? error.message : String(error)}`);
     }
   }
+
+  /**
+   * 根据身份证号或残疾证号获取残疾人记录
+   */
+  async getDisabledPersonByIdentifier(identifier: string): Promise<DisabledPerson | null> {
+    try {
+      const disabledPersonRepo = this.dataSource.getRepository(DisabledPerson);
+      return await disabledPersonRepo.findOne({
+        where: [
+          { idCard: identifier },
+          { disabilityId: identifier }
+        ]
+      });
+    } catch (error) {
+      console.error('Error getting disabled person by identifier:', error);
+      throw new Error(`Failed to get disabled person: ${error instanceof Error ? error.message : String(error)}`);
+    }
+  }
+
+  /**
+   * 根据person_id获取人才用户
+   */
+  async getUserByPersonId(personId: number): Promise<UserEntity | null> {
+    try {
+      return await this.repository.findOne({
+        where: { personId },
+        relations: ['roles', 'avatarFile']
+      });
+    } catch (error) {
+      console.error('Error getting user by person id:', error);
+      throw new Error('Failed to get user by person id');
+    }
+  }
+
+  /**
+   * 获取用户信息,包含关联的残疾人详细信息
+   */
+  async getUserWithDisabledPerson(id: number): Promise<UserEntity | null> {
+    try {
+      return await this.repository.findOne({
+        where: { id },
+        relations: ['person', 'roles', 'avatarFile']
+      });
+    } catch (error) {
+      console.error('Error getting user with disabled person:', error);
+      throw new Error(`Failed to get user with disabled person: ${error instanceof Error ? error.message : String(error)}`);
+    }
+  }
+
+  /**
+   * 根据身份证号或残疾证号获取人才用户(包含残疾人详细信息)
+   */
+  async getTalentUserByIdentifier(identifier: string): Promise<UserEntity | null> {
+    try {
+      // 先通过身份证号或残疾证号查找残疾人记录
+      const disabledPerson = await this.getDisabledPersonByIdentifier(identifier);
+      if (!disabledPerson) {
+        return null;
+      }
+
+      // 再通过person_id查找用户
+      return await this.repository.findOne({
+        where: {
+          personId: disabledPerson.id,
+          userType: UserType.TALENT
+        },
+        relations: ['person', 'roles', 'avatarFile']
+      });
+    } catch (error) {
+      console.error('Error getting talent user by identifier:', error);
+      throw new Error(`Failed to get talent user: ${error instanceof Error ? error.message : String(error)}`);
+    }
+  }
 }

+ 9 - 0
pnpm-lock.yaml

@@ -3131,9 +3131,18 @@ importers:
       '@d8d/allin-company-module':
         specifier: workspace:*
         version: link:../../allin-packages/company-module
+      '@d8d/allin-disability-module':
+        specifier: workspace:*
+        version: link:../../allin-packages/disability-module
+      '@d8d/allin-order-module':
+        specifier: workspace:*
+        version: link:../../allin-packages/order-module
       '@d8d/allin-platform-module':
         specifier: workspace:*
         version: link:../../allin-packages/platform-module
+      '@d8d/bank-names-module':
+        specifier: workspace:*
+        version: link:../bank-names-module
       '@d8d/shared-crud':
         specifier: workspace:*
         version: link:../shared-crud