Forráskód Böngészése

fix(011.003): 修复人才详情页企业专用API使用

- 将人才详情页API客户端从disabilityClient改为enterpriseDisabilityClient
- 修复RPC客户端路径参数语法:从{id}改为:id
- 使用企业专用API子路由:work-history、salary-history、credit-info
- 添加正确的TypeScript接口类型,移除as any类型断言
- 更新故事011.003和史诗012文档,记录企业专用API修复

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 1 hónapja
szülő
commit
021dd8744c

+ 1 - 1
docs/prd/epic-012-api-supplement-for-employer-mini-program.md

@@ -416,7 +416,7 @@
 **总体进度**:9/9 故事完成(100%)
 **MVP进度**:9/9 核心故事完成(100%,排除012-06延期和012-07冗余)
 
-**最近更新**:2025-12-18 - 故事012.011完成,企业专用人才管理API已实现。2025-12-18 - 故事012.010完成,近期分配人才查询API已实现。2025-12-18 - 故事012.009完成,管理后台企业用户配置表单扩展已实现。2025-12-18 - 添加故事012-09(管理后台企业用户配置表单扩展)以解决管理后台用户表单缺失企业选择字段的问题。2025-12-17 - 故事012.005完成,视频管理API扩展已实现。史诗012核心功能全部完成。故事012.004完成,订单统计与数据统计API已实现。史诗012故事优先级调整:故事012-08标记为已完成;故事012-06调整为P2延期(系统设置API);故事012-07标记为冗余(API文档与测试完善);故事012-05重新设计(基于order_person_asset实体)。故事012.003完成,企业统计与人才扩展API已实现;故事012.008创建并完成,路由路径规范修复。
+**最近更新**:2025-12-19 - 故事011.003企业专用API使用修复:人才详情页已改为使用enterpriseDisabilityClient企业专用API,确保企业用户数据安全隔离。2025-12-18 - 故事012.011完成,企业专用人才管理API已实现。2025-12-18 - 故事012.010完成,近期分配人才查询API已实现。2025-12-18 - 故事012.009完成,管理后台企业用户配置表单扩展已实现。2025-12-18 - 添加故事012-09(管理后台企业用户配置表单扩展)以解决管理后台用户表单缺失企业选择字段的问题。2025-12-17 - 故事012.005完成,视频管理API扩展已实现。史诗012核心功能全部完成。故事012.004完成,订单统计与数据统计API已实现。史诗012故事优先级调整:故事012-08标记为已完成;故事012-06调整为P2延期(系统设置API);故事012-07标记为冗余(API文档与测试完善);故事012-05重新设计(基于order_person_asset实体)。故事012.003完成,企业统计与人才扩展API已实现;故事012.008创建并完成,路由路径规范修复。
 
 ---
 

+ 43 - 40
docs/stories/011.003.story.md

@@ -80,7 +80,8 @@ Ready for Review
 - **访问控制**:人才管理功能需要用户登录后才能访问,可使用`useRequireAuth`钩子进行权限保护
 
 ### API规范
-**残疾人才API**(disability_person模块):
+**残疾人才API**(disability_person模块)- **通用版本**:
+- **警告**:此API为通用版本,返回所有人才数据,未按企业过滤。**企业用户必须使用下面的"企业专用残疾人才API"**,确保数据安全隔离。
 - **客户端**:`disabilityClient`(已在`mini/src/api.ts`中可用,由故事011.001集成)
 - **路径前缀**:`/api/v1/disability`
 - **主要接口**:
@@ -104,7 +105,7 @@ Ready for Review
   })
 
   // 获取人才详情
-  const talentDetail = await disabilityClient['{id}'].get({
+  const talentDetail = await disabilityClient[':id'].get({
     param: { id: '123' }
   })
   ```
@@ -136,76 +137,70 @@ Ready for Review
   })
 
   // 获取企业专用人才详情
-  const talentDetail = await enterpriseDisabilityClient['{id}'].$get({
+  const talentDetail = await enterpriseDisabilityClient[':id'].$get({
     param: { id: '123' }
   })
   ```
 
-**订单管理API**(order模块):
-- **客户端**:`orderClient`(已在`mini/src/api.ts`中可用,由故事011.001集成)
-- **路径前缀**:`/api/v1/order`
-- **主要接口**:
-  - `GET /person/{person_id}` - 人才工作信息查询接口(获取指定人才的相关订单信息)
-  - `GET /history` - 订单历史查询接口(支持按人才ID筛选)
+**订单管理API**(order模块)- **企业专用版本**:
+- **背景**:通用订单API(`/api/v1/order`)返回所有订单数据。企业用户应使用企业专用残疾人才API的`work-history`子路由,仅返回当前企业关联人才的工作历史。
+- **替代方案**:使用企业专用残疾人才API的`work-history`子路由(已通过故事012.003实现)
+  - 路径:`/api/v1/yongren/disability-person/{id}/work-history`
+  - 客户端:`enterpriseDisabilityClient[':id']['work-history']`
 - **使用示例**:
   ```typescript
-  import { orderClient } from '@/api'
-
-  // 获取人才工作信息
-  const workInfo = await orderClient.person['{person_id}'].get({
-    param: { person_id: '123' }
-  })
+  import { enterpriseDisabilityClient } from '@/api'
 
-  // 获取订单历史
-  const orderHistory = await orderClient.history.get({
-    query: { person_id: '123', page: 1, limit: 10 }
+  // 获取人才工作历史(企业专用版本)
+  const workHistory = await enterpriseDisabilityClient[':id']['work-history'].$get({
+    param: { id: '123' }
   })
   ```
 
-**薪资管理API**(salary模块):
-- **客户端**:`salaryClient`(已在`mini/src/api.ts`中可用,由故事011.001集成)
-- **路径前缀**:`/api/v1/salary`
-- **主要接口**:
-  - `GET /person/{person_id}` - 薪资记录查询接口(获取指定人才的薪资记录)
-  - `GET /history/{person_id}` - 薪资历史接口(获取指定人才的薪资历史,支持时间范围筛选)
+**薪资管理API**(salary模块)- **企业专用版本**:
+- **背景**:通用薪资API(`/api/v1/salary`)返回所有薪资数据。企业用户应使用企业专用残疾人才API的`salary-history`子路由,仅返回当前企业关联人才的薪资历史。
+- **替代方案**:使用企业专用残疾人才API的`salary-history`子路由(已通过故事012.003实现)
+  - 路径:`/api/v1/yongren/disability-person/{id}/salary-history`
+  - 客户端:`enterpriseDisabilityClient[':id']['salary-history']`
 - **使用示例**:
   ```typescript
-  import { salaryClient } from '@/api'
-
-  // 获取人才薪资记录
-  const salaryRecords = await salaryClient.person['{person_id}'].get({
-    param: { person_id: '123' }
-  })
+  import { enterpriseDisabilityClient } from '@/api'
 
-  // 获取薪资历史
-  const salaryHistory = await salaryClient.history['{person_id}'].get({
-    param: { person_id: '123' },
-    query: { start_date: '2024-01-01', end_date: '2024-12-31' }
+  // 获取人才薪资历史(企业专用版本)
+  const salaryHistory = await enterpriseDisabilityClient[':id']['salary-history'].$get({
+    param: { id: '123' },
+    query: {
+      start_date: '2024-01-01',
+      end_date: '2024-12-31',
+      page: 1,
+      limit: 20
+    }
   })
   ```
 
 **文件管理API**(file模块):
 - **客户端**:`fileClient`(已在`mini/src/api.ts`中可用,由故事011.001集成)
 - **路径前缀**:`/api/v1/file`
+- **权限说明**:文件管理API通过认证中间件验证企业用户权限,用户只能访问自己企业关联人才的文件。
 - **主要接口**:
   - `GET /list` - 文件列表查询接口(支持按关联类型和关联ID筛选)
     - 查询参数:`relation_type`(如'disabled_person')、`relation_id`(人才ID)
-  - `GET /download/{id}` - 文件下载接口
-  - `GET /preview/{id}` - 文件预览接口(返回预览URL)
+  - `GET /download/{id}` - 文件下载接口(验证文件权限)
+  - `GET /preview/{id}` - 文件预览接口(返回预览URL,验证文件权限
 - **使用示例**:
   ```typescript
   import { fileClient } from '@/api'
 
-  // 获取人才相关文件列表
+  // 获取人才相关文件列表(自动验证企业权限)
   const fileList = await fileClient.list.get({
     query: {
       relation_type: 'disabled_person',
-      relation_id: '123'
+      relation_id: '123'  // 人才ID,API会验证该人才是否属于当前企业
     }
   })
 
-  // 下载文件
-  const downloadUrl = fileClient.download['{id}'].getUrl({ param: { id: 'file_456' } })
+  // 下载文件(自动验证文件权限)
+  const downloadUrl = fileClient.download[':id'].getUrl({ param: { id: 'file_456' } })
   ```
 
 **企业统计API**(企业扩展):
@@ -467,6 +462,7 @@ Ready for Review
 | 2025-12-18 | 1.7 | 更新API规范:标注`/allocations/recent`接口已通过故事012.010实现 | Claude Code |
 | 2025-12-18 | 1.8 | 更新验收标准:标记所有验收标准为已完成 | James(开发工程师) |
 | 2025-12-18 | 1.9 | 更新API规范:添加企业专用残疾人才API和依赖故事012.011 | Claude Code |
+| 2025-12-19 | 1.10 | 修复人才详情页API使用:改用enterpriseDisabilityClient企业专用API,修复RPC客户端路径参数语法 | Claude Code |
 
 ## 开发代理记录
 
@@ -508,11 +504,18 @@ claude-sonnet
   - 支持可选的limit参数控制返回记录数
   - 企业用户权限验证完整
   - 数据库索引优化查询性能
+- ✅ 修复人才详情页企业专用API使用:
+  - 将人才详情页API客户端从`disabilityClient`改为`enterpriseDisabilityClient`
+  - 修复RPC客户端路径参数语法:从`{id}`改为`:id`
+  - 使用企业专用API子路由:`work-history`、`salary-history`、`credit-info`
+  - 添加正确的TypeScript接口类型,移除`as any`类型断言
+  - 验证企业用户数据安全隔离:只返回当前企业关联人才数据
 
 ### 发现的问题
 1. **API路径不一致**:故事011.003文档中提到的`GET /allocations/recent`接口在实际实现中不存在(史诗012未实现此接口)**✅ 已解决** - 已创建故事012.010专门实现此接口
 2. **RPC客户端类型推断**:dashboard页面需要手动类型转换,RPC自动类型推断可能不完整 **✅ 已解决** - 已修复RPC调用方法(使用$get())和类型转换
 3. **企业名称字段**:user对象中没有`companyName`字段,使用`name`字段替代 **✅ 已了解** - 前端已适配使用user.name字段
+4. **企业专用API使用错误**:人才详情页使用通用`disabilityClient`而非企业专用`enterpriseDisabilityClient`,存在数据安全风险 **✅ 已解决** - 已修复API客户端使用,改为企业专用API,确保数据安全隔离
 
 ### 建议
 1. 更新故事011.003文档中的API规范,移除不存在的`allocations.recent`接口引用 **✅ 已完成** - 已更新API规范,注明接口将在故事012.010实现

+ 155 - 50
mini/src/pages/yongren/talent/detail/index.tsx

@@ -4,30 +4,44 @@ import Taro from '@tarojs/taro'
 import { useQuery } from '@tanstack/react-query'
 import YongrenTabBarLayout from '@/layouts/yongren-tab-bar-layout'
 import PageContainer from '@/components/ui/page-container'
-import { enterpriseDisabilityClient, orderClient, salaryClient, fileClient } from '@/api'
+import { enterpriseDisabilityClient } from '@/api'
 import { useRequireAuth } from '@/hooks/useRequireAuth'
 import './index.css'
 
-// 类型定义
+// 类型定义 - 匹配企业专用人才详情API的CompanyPersonDetailSchema
 interface TalentDetailData {
-  id: number
+  personId: number
   name: string
   gender: string
-  idCard?: string
-  disabilityId?: string
-  disabilityType?: string
-  disabilityLevel?: string
-  idAddress?: string
-  phone?: string
-  province?: string
-  city?: string
-  age?: number
-  status?: '在职' | '待入职' | '离职'
-  joinDate?: string
-  salary?: number
-  companyId?: number
+  idCard: string
+  disabilityType: string
+  disabilityLevel: string
   birthDate?: string
-  specificDisability?: string
+  phone?: string
+  jobStatus: string
+  bankCards: Array<{
+    cardId: number
+    bankName: string
+    cardNumber: string
+    isDefault: boolean
+  }>
+  photos: Array<{
+    fileId: number
+    fileName: string
+    fileUrl: string
+  }>
+  // 兼容字段
+  id?: number  // 兼容旧代码,映射personId
+  status?: string  // 兼容旧代码,映射jobStatus
+  age?: number  // 根据birthDate计算
+  idAddress?: string  // 可能来自其他API
+  province?: string  // 可能来自其他API
+  city?: string  // 可能来自其他API
+  joinDate?: string  // 可能来自工作信息API
+  salary?: number  // 可能来自薪资信息API
+  companyId?: number  // 可能来自其他API
+  disabilityId?: string  // 可能来自其他API
+  specificDisability?: string  // 可能来自其他API
   [key: string]: any
 }
 
@@ -64,6 +78,47 @@ interface FileData {
   [key: string]: any
 }
 
+// 企业专用API响应类型
+interface WorkHistoryItem {
+  订单ID: number
+  订单名称: string | null
+  入职日期: string | null
+  实际入职日期: string | null
+  离职日期: string | null
+  工作状态: string
+  个人薪资: number
+}
+
+interface WorkHistoryResponse {
+  工作历史: WorkHistoryItem[]
+}
+
+interface SalaryHistoryItem {
+  月份: string | null
+  基本工资: number
+  补贴: number
+  扣款: number
+  实发工资: number
+}
+
+interface SalaryHistoryResponse {
+  薪资历史: SalaryHistoryItem[]
+}
+
+interface CreditInfoItem {
+  文件ID: string
+  文件URL: string | null
+  上传时间: string | null
+  文件类型: string | null
+  银行卡号: string | null
+  持卡人姓名: string | null
+  银行名称: number | null
+}
+
+interface CreditInfoResponse {
+  征信信息: CreditInfoItem[]
+}
+
 const YongrenTalentDetailPage: React.FC = () => {
   const { isLoggedIn } = useRequireAuth()
   const router = Taro.useRouter()
@@ -74,95 +129,145 @@ const YongrenTalentDetailPage: React.FC = () => {
     queryKey: ['talentDetail', talentId],
     queryFn: async () => {
       if (!talentId) throw new Error('无效的人才ID')
-      const response = await enterpriseDisabilityClient['{id}'].$get({
+      const response = await enterpriseDisabilityClient[':id'].$get({
         param: { id: talentId.toString() }
       })
       if (response.status !== 200) {
         throw new Error('获取人才详情失败')
       }
       const data = await response.json() as TalentDetailData
-      return data
+      // 添加兼容字段映射
+      return {
+        ...data,
+        id: data.personId,  // 映射id字段
+        status: data.jobStatus,  // 映射status字段
+        // 计算年龄
+        age: data.birthDate ? Math.floor((Date.now() - new Date(data.birthDate).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) : undefined
+      }
     },
     enabled: isLoggedIn && talentId > 0
   })
 
-  // 获取工作信息
+  // 获取工作信息 - 使用企业专用工作历史API
   const { data: workInfo, isLoading: workLoading } = useQuery({
     queryKey: ['workInfo', talentId],
     queryFn: async () => {
       if (!talentId) throw new Error('无效的人才ID')
-      // 根据API规范,路径为:/api/v1/order/person/{person_id}
-      const response = await orderClient['person/{person_id}'].$get({
-        param: { person_id: talentId.toString() }
+      // 使用企业专用工作历史API:/api/v1/yongren/disability-person/{id}/work-history
+      const response = await enterpriseDisabilityClient[':id']['work-history'].$get({
+        param: { id: talentId.toString() }
       })
       if (response.status !== 200) {
         // 可能没有工作信息,返回空对象
         return {} as WorkInfoData
       }
-      const data = await response.json() as WorkInfoData
-      return data
+      const data = await response.json() as WorkHistoryResponse
+      // 企业专用工作历史API返回的是工作历史列表,取最新的一条作为当前工作信息
+      const workHistory = data?.工作历史 || []
+      if (workHistory.length === 0) {
+        return {} as WorkInfoData
+      }
+      // 取最新的一条工作记录(按入职日期降序)
+      const latestWork = workHistory[0]
+      return {
+        id: latestWork.订单ID || talentId,
+        orderId: latestWork.订单ID,
+        position: latestWork.订单名称 || undefined,
+        department: undefined, // 企业专用API没有部门字段
+        startDate: latestWork.入职日期 || undefined,
+        endDate: latestWork.离职日期 || undefined,
+        status: latestWork.工作状态,
+        companyId: undefined // 企业专用API没有公司ID字段
+      } as WorkInfoData
     },
     enabled: isLoggedIn && talentId > 0
   })
 
-  // 获取薪资信息
+  // 获取薪资信息 - 使用企业专用薪资历史API
   const { data: salaryInfo, isLoading: salaryLoading } = useQuery({
     queryKey: ['salaryInfo', talentId],
     queryFn: async () => {
       if (!talentId) throw new Error('无效的人才ID')
-      // 根据API规范,路径为:/api/v1/salary/person/{person_id}
-      const response = await salaryClient['person/{person_id}'].$get({
-        param: { person_id: talentId.toString() }
+      // 使用企业专用薪资历史API:/api/v1/yongren/disability-person/{id}/salary-history
+      const response = await enterpriseDisabilityClient[':id']['salary-history'].$get({
+        param: { id: talentId.toString() }
       })
       if (response.status !== 200) {
         // 可能没有薪资信息,返回空对象
         return {} as SalaryData
       }
-      const data = await response.json() as SalaryData
-      return data
+      const data = await response.json() as SalaryHistoryResponse
+      // 企业专用薪资历史API返回结构:{ 薪资历史: [...] }
+      const salaryHistory = data?.薪资历史 || []
+      if (salaryHistory.length === 0) {
+        return {} as SalaryData
+      }
+      // 取最新的一条薪资记录(按月份降序)
+      const latestSalary = salaryHistory[0]
+      return {
+        id: talentId,
+        personId: talentId,
+        amount: latestSalary.实发工资 || latestSalary.基本工资,
+        paymentDate: latestSalary.月份 || undefined,
+        type: '月薪', // 默认类型
+        period: '月度' // 默认周期
+      } as SalaryData
     },
     enabled: isLoggedIn && talentId > 0
   })
 
-  // 获取薪资历史记录
+  // 获取薪资历史记录 - 使用企业专用薪资历史API
   const { data: salaryHistory, isLoading: historyLoading } = useQuery({
     queryKey: ['salaryHistory', talentId],
     queryFn: async () => {
       if (!talentId) throw new Error('无效的人才ID')
-      // 根据API规范,路径为:/api/v1/salary/history/{person_id}
-      const response = await salaryClient['history/{person_id}'].$get({
-        param: { person_id: talentId.toString() },
-        query: {
-          start_date: '2024-01-01', // 默认开始日期
-          end_date: new Date().toISOString().split('T')[0] // 今天
-        }
+      // 使用企业专用薪资历史API:/api/v1/yongren/disability-person/{id}/salary-history
+      const response = await enterpriseDisabilityClient[':id']['salary-history'].$get({
+        param: { id: talentId.toString() }
       })
       if (response.status !== 200) {
         return [] as SalaryData[]
       }
-      const result = await response.json() as any
-      return (result?.data || []) as SalaryData[]
+      const data = await response.json() as SalaryHistoryResponse
+      // 企业专用薪资历史API返回结构:{ 薪资历史: [...] }
+      const salaryHistoryData = data?.薪资历史 || []
+      // 转换为SalaryData数组
+      return salaryHistoryData.map((item: SalaryHistoryItem, index: number) => ({
+        id: index + 1,
+        personId: talentId,
+        amount: item.实发工资 || item.基本工资,
+        paymentDate: item.月份 || undefined,
+        type: '月薪', // 默认类型
+        period: '月度' // 默认周期
+      })) as SalaryData[]
     },
     enabled: isLoggedIn && talentId > 0
   })
 
-  // 获取个人征信文件
+  // 获取个人征信文件 - 使用企业专用征信信息API
   const { data: creditFiles, isLoading: filesLoading } = useQuery({
     queryKey: ['creditFiles', talentId],
     queryFn: async () => {
       if (!talentId) throw new Error('无效的人才ID')
-      // 根据API规范,路径为:/api/v1/file/list
-      const response = await fileClient.$get({
-        query: {
-          relation_type: 'disabled_person',
-          relation_id: talentId.toString()
-        } as any  // 类型断言,因为TypeScript类型定义可能不完整
+      // 使用企业专用征信信息API:/api/v1/yongren/disability-person/{id}/credit-info
+      const response = await enterpriseDisabilityClient[':id']['credit-info'].$get({
+        param: { id: talentId.toString() }
       })
       if (response.status !== 200) {
         return [] as FileData[]
       }
-      const result = await response.json() as any
-      return (result?.data || []) as FileData[]
+      const data = await response.json() as CreditInfoResponse
+      // 企业专用征信信息API返回结构:{ 征信信息: [...] }
+      const creditInfoList = data?.征信信息 || []
+      // 转换为FileData数组
+      return creditInfoList.map((item: CreditInfoItem) => ({
+        id: item.文件ID || '',
+        name: item.银行卡号 ? `银行卡 ${item.银行卡号}` : item.持卡人姓名 ? `征信文件 - ${item.持卡人姓名}` : '征信文件',
+        url: item.文件URL || undefined,
+        size: undefined, // 征信信息API不返回文件大小
+        type: item.文件类型 || undefined,
+        createdAt: item.上传时间 || undefined
+      })) as FileData[]
     },
     enabled: isLoggedIn && talentId > 0
   })

+ 5 - 5
mini/tests/yongren-api.test.ts

@@ -35,14 +35,14 @@ describe('用人方小程序RPC客户端', () => {
 
     // 检查企业统计客户端方法
     expect(enterpriseCompanyClient.overview).toBeDefined()
-    expect(enterpriseCompanyClient['{id}/talents']).toBeDefined()
+    expect(enterpriseCompanyClient[':id']['talents']).toBeDefined()
     expect(enterpriseCompanyClient['allocations/recent']).toBeDefined()
 
     // 检查人才扩展客户端方法
-    expect(enterpriseDisabilityClient['{id}/work-history']).toBeDefined()
-    expect(enterpriseDisabilityClient['{id}/salary-history']).toBeDefined()
-    expect(enterpriseDisabilityClient['{id}/credit-info']).toBeDefined()
-    expect(enterpriseDisabilityClient['{id}/videos']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id']['work-history']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id']['salary-history']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id']['credit-info']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id'].videos).toBeDefined()
   })
 
   test('企业认证客户端方法应具备正确的HTTP方法', () => {