2
0
Эх сурвалжийг харах

✨ feat(order-module): 实现人才就业信息API (故事015.005)

完成人才小程序就业信息查询功能,包括当前就业状态、薪资记录、就业历史和薪资视频查询。

新增功能:
- 创建人才就业信息Schema (talent-employment.schema.ts)
- 在OrderService添加4个人才专用查询方法
- 创建人才就业信息API路由,使用talentAuthMiddleware认证
- 在server包注册人才就业路由 (/api/v1/rencai前缀)
- 创建集成测试覆盖所有API端点

API端点:
- GET /api/v1/rencai/employment/status - 当前就业状态
- GET /api/v1/rencai/employment/salary-records - 薪资记录
- GET /api/v1/rencai/employment/history - 就业历史
- GET /api/v1/rencai/employment/salary-videos - 薪资视频

技术特性:
- 只读设计,所有接口为GET请求
- 基于person_id数据隔离
- 利用现有数据库索引优化查询性能
- 完整的Schema验证和TypeScript类型定义

🤖 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 долоо хоног өмнө
parent
commit
a529cc109b

+ 2 - 1
allin-packages/order-module/src/index.ts

@@ -4,4 +4,5 @@ export { OrderPerson } from './entities/order-person.entity';
 export { OrderPersonAsset } from './entities/order-person-asset.entity';
 export { OrderService } from './services/order.service';
 export * from './schemas/order.schema';
-export { orderRoutes, enterpriseOrderRoutes } from './routes/order.routes';
+export * from './schemas/talent-employment.schema';
+export { orderRoutes, enterpriseOrderRoutes, talentEmploymentRoutes } from './routes/order.routes';

+ 8 - 0
allin-packages/order-module/src/routes/order.routes.ts

@@ -2,6 +2,7 @@ import { OpenAPIHono } from '@hono/zod-openapi';
 import { AuthContext } from '@d8d/shared-types';
 import orderCustomRoutes, { enterpriseOrderCustomRoutes } from './order-custom.routes';
 import { orderCrudRoutes } from './order-crud.routes';
+import talentEmploymentRoutes from './talent-employment.routes';
 
 /**
  * 通用订单管理路由
@@ -18,5 +19,12 @@ const orderRoutes = new OpenAPIHono<AuthContext>()
 const enterpriseOrderRoutes = new OpenAPIHono<AuthContext>()
   .route('/', enterpriseOrderCustomRoutes);
 
+/**
+ * 人才就业信息路由
+ * 故事015.005 - 人才小程序就业信息API
+ */
+export { talentEmploymentRoutes };
+export { default as talentEmploymentRoutesDefault } from './talent-employment.routes';
+
 export { orderRoutes, enterpriseOrderRoutes };
 export default orderRoutes;

+ 289 - 0
allin-packages/order-module/src/routes/talent-employment.routes.ts

@@ -0,0 +1,289 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { talentAuthMiddleware } from '@d8d/auth-module';
+import { TalentAuthContext, TalentUserBase } from '@d8d/shared-types';
+import { OrderService } from '../services/order.service';
+import {
+  EmploymentStatusResponseSchema,
+  SalaryRecordsResponseSchema,
+  EmploymentHistoryResponseSchema,
+  SalaryVideosResponseSchema,
+  SalaryQuerySchema,
+  EmploymentHistoryQuerySchema,
+  SalaryVideoQuerySchema
+} from '../schemas/talent-employment.schema';
+
+// 人才用户类型
+type TalentUser = TalentUserBase;
+
+/**
+ * 获取当前就业状态路由
+ */
+const getEmploymentStatusRoute = createRoute({
+  method: 'get',
+  path: '/employment/status',
+  middleware: [talentAuthMiddleware],
+  responses: {
+    200: {
+      description: '获取当前就业状态成功',
+      content: {
+        'application/json': { schema: EmploymentStatusResponseSchema }
+      }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    403: {
+      description: '权限不足',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在或无就业记录',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+/**
+ * 获取薪资记录路由
+ */
+const getSalaryRecordsRoute = createRoute({
+  method: 'get',
+  path: '/employment/salary-records',
+  middleware: [talentAuthMiddleware],
+  request: {
+    query: SalaryQuerySchema
+  },
+  responses: {
+    200: {
+      description: '获取薪资记录成功',
+      content: {
+        'application/json': { schema: SalaryRecordsResponseSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    403: {
+      description: '权限不足',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+/**
+ * 获取就业历史路由
+ */
+const getEmploymentHistoryRoute = createRoute({
+  method: 'get',
+  path: '/employment/history',
+  middleware: [talentAuthMiddleware],
+  request: {
+    query: EmploymentHistoryQuerySchema
+  },
+  responses: {
+    200: {
+      description: '获取就业历史成功',
+      content: {
+        'application/json': { schema: EmploymentHistoryResponseSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    403: {
+      description: '权限不足',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+/**
+ * 获取薪资视频路由
+ */
+const getSalaryVideosRoute = createRoute({
+  method: 'get',
+  path: '/employment/salary-videos',
+  middleware: [talentAuthMiddleware],
+  request: {
+    query: SalaryVideoQuerySchema
+  },
+  responses: {
+    200: {
+      description: '获取薪资视频成功',
+      content: {
+        'application/json': { schema: SalaryVideosResponseSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    403: {
+      description: '权限不足',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<TalentAuthContext>()
+  // 获取当前就业状态
+  .openapi(getEmploymentStatusRoute, async (c) => {
+    const user = c.get('user') as TalentUser;
+    const personId = user.personId;
+
+    // 验证person_id是否存在
+    if (!personId) {
+      return c.json({
+        code: 404,
+        message: '用户不存在或未关联残疾人信息'
+      } as any, 404);
+    }
+
+    try {
+      const orderService = new OrderService(AppDataSource);
+      const employmentStatus = await orderService.getCurrentEmploymentStatus(personId);
+
+      if (!employmentStatus) {
+        return c.json({
+          code: 404,
+          message: '未找到就业记录'
+        } as any, 404);
+      }
+
+      const validatedResult = await parseWithAwait(EmploymentStatusResponseSchema, employmentStatus);
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取当前就业状态失败'
+      } as any, 500);
+    }
+  })
+  // 获取薪资记录
+  .openapi(getSalaryRecordsRoute, async (c) => {
+    const user = c.get('user') as TalentUser;
+    const personId = user.personId;
+
+    if (!personId) {
+      return c.json({
+        code: 404,
+        message: '用户不存在或未关联残疾人信息'
+      } as any, 404);
+    }
+
+    try {
+      const query = c.req.valid('query');
+      const orderService = new OrderService(AppDataSource);
+      const salaryRecords = await orderService.getSalaryRecords(
+        personId,
+        query.month,
+        query.skip,
+        query.take
+      );
+
+      const validatedResult = await parseWithAwait(SalaryRecordsResponseSchema, salaryRecords);
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取薪资记录失败'
+      } as any, 500);
+    }
+  })
+  // 获取就业历史
+  .openapi(getEmploymentHistoryRoute, async (c) => {
+    const user = c.get('user') as TalentUser;
+    const personId = user.personId;
+
+    if (!personId) {
+      return c.json({
+        code: 404,
+        message: '用户不存在或未关联残疾人信息'
+      } as any, 404);
+    }
+
+    try {
+      const query = c.req.valid('query');
+      const orderService = new OrderService(AppDataSource);
+      const employmentHistory = await orderService.getEmploymentHistory(
+        personId,
+        query.skip,
+        query.take
+      );
+
+      const validatedResult = await parseWithAwait(EmploymentHistoryResponseSchema, employmentHistory);
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取就业历史失败'
+      } as any, 500);
+    }
+  })
+  // 获取薪资视频
+  .openapi(getSalaryVideosRoute, async (c) => {
+    const user = c.get('user') as TalentUser;
+    const personId = user.personId;
+
+    if (!personId) {
+      return c.json({
+        code: 404,
+        message: '用户不存在或未关联残疾人信息'
+      } as any, 404);
+    }
+
+    try {
+      const query = c.req.valid('query');
+      const orderService = new OrderService(AppDataSource);
+      const salaryVideos = await orderService.getSalaryVideos(
+        personId,
+        query.assetType,
+        query.month,
+        query.skip,
+        query.take
+      );
+
+      const validatedResult = await parseWithAwait(SalaryVideosResponseSchema, salaryVideos);
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取薪资视频失败'
+      } as any, 500);
+    }
+  });
+
+export default app;

+ 2 - 1
allin-packages/order-module/src/schemas/index.ts

@@ -1 +1,2 @@
-export * from './order.schema';
+export * from './order.schema';
+export * from './talent-employment.schema';

+ 227 - 0
allin-packages/order-module/src/schemas/talent-employment.schema.ts

@@ -0,0 +1,227 @@
+import { z } from '@hono/zod-openapi';
+
+/**
+ * 人才就业信息Schema
+ * 用于人才小程序查询就业信息相关接口
+ */
+
+// 当前就业状态响应Schema
+export const EmploymentStatusResponseSchema = z.object({
+  companyName: z.string().openapi({
+    description: '企业名称',
+    example: '某某科技有限公司'
+  }),
+  orderId: z.number().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderName: z.string().nullable().openapi({
+    description: '订单名称(岗位名称)',
+    example: '包装工'
+  }),
+  positionName: z.string().nullable().openapi({
+    description: '岗位名称(同订单名称)',
+    example: '包装工'
+  }),
+  joinDate: z.string().openapi({
+    description: '入职日期,格式: YYYY-MM-DD',
+    example: '2025-01-15'
+  }),
+  workStatus: z.string().openapi({
+    description: '工作状态',
+    enum: ['not_working', 'pre_working', 'working', 'resigned'],
+    example: 'working'
+  }),
+  salaryLevel: z.number().openapi({
+    description: '薪资水平',
+    example: 3500.00
+  }),
+  actualStartDate: z.string().nullable().openapi({
+    description: '实际入职日期,格式: YYYY-MM-DD',
+    example: '2025-01-15'
+  })
+}).openapi('EmploymentStatusResponse');
+
+// 薪资记录Schema
+export const SalaryRecordSchema = z.object({
+  orderId: z.number().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderName: z.string().nullable().openapi({
+    description: '订单名称',
+    example: '包装工'
+  }),
+  companyName: z.string().nullable().openapi({
+    description: '企业名称',
+    example: '某某科技有限公司'
+  }),
+  salaryAmount: z.number().openapi({
+    description: '薪资金额',
+    example: 3500.00
+  }),
+  joinDate: z.string().openapi({
+    description: '入职日期,格式: YYYY-MM-DD',
+    example: '2025-01-15'
+  }),
+  month: z.string().openapi({
+    description: '月份,格式: YYYY-MM',
+    example: '2025-01'
+  })
+}).openapi('SalaryRecord');
+
+// 薪资记录响应Schema
+export const SalaryRecordsResponseSchema = z.object({
+  data: z.array(SalaryRecordSchema).openapi({
+    description: '薪资记录列表'
+  }),
+  total: z.number().openapi({
+    description: '总记录数',
+    example: 10
+  })
+}).openapi('SalaryRecordsResponse');
+
+// 就业历史项Schema
+export const EmploymentHistoryItemSchema = z.object({
+  orderId: z.number().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderName: z.string().nullable().openapi({
+    description: '订单名称',
+    example: '包装工'
+  }),
+  companyName: z.string().nullable().openapi({
+    description: '企业名称',
+    example: '某某科技有限公司'
+  }),
+  positionName: z.string().nullable().openapi({
+    description: '岗位名称',
+    example: '包装工'
+  }),
+  joinDate: z.string().openapi({
+    description: '入职日期,格式: YYYY-MM-DD',
+    example: '2025-01-15'
+  }),
+  leaveDate: z.string().nullable().openapi({
+    description: '离职日期,格式: YYYY-MM-DD',
+    example: null
+  }),
+  workStatus: z.string().openapi({
+    description: '工作状态',
+    enum: ['not_working', 'pre_working', 'working', 'resigned'],
+    example: 'working'
+  }),
+  salaryLevel: z.number().openapi({
+    description: '薪资水平',
+    example: 3500.00
+  })
+}).openapi('EmploymentHistoryItem');
+
+// 就业历史响应Schema
+export const EmploymentHistoryResponseSchema = z.object({
+  data: z.array(EmploymentHistoryItemSchema).openapi({
+    description: '就业历史列表'
+  }),
+  total: z.number().openapi({
+    description: '总记录数',
+    example: 5
+  })
+}).openapi('EmploymentHistoryResponse');
+
+// 薪资视频Schema
+export const SalaryVideoSchema = z.object({
+  id: z.number().openapi({
+    description: '视频ID',
+    example: 1
+  }),
+  assetType: z.string().openapi({
+    description: '资产类型',
+    enum: ['salary_video', 'tax_video'],
+    example: 'salary_video'
+  }),
+  assetFileType: z.string().openapi({
+    description: '资产文件类型',
+    example: 'video'
+  }),
+  fileUrl: z.string().nullable().openapi({
+    description: '文件URL',
+    example: 'https://example.com/files/video.mp4'
+  }),
+  fileName: z.string().nullable().openapi({
+    description: '文件名',
+    example: '工资视频_2025-01.mp4'
+  }),
+  status: z.string().openapi({
+    description: '审核状态',
+    enum: ['pending', 'verified', 'rejected'],
+    example: 'verified'
+  }),
+  relatedTime: z.string().openapi({
+    description: '关联时间,格式: ISO 8601',
+    example: '2025-01-15T10:30:00Z'
+  }),
+  month: z.string().openapi({
+    description: '月份,格式: YYYY-MM',
+    example: '2025-01'
+  })
+}).openapi('SalaryVideo');
+
+// 薪资视频响应Schema
+export const SalaryVideosResponseSchema = z.object({
+  data: z.array(SalaryVideoSchema).openapi({
+    description: '薪资视频列表'
+  }),
+  total: z.number().openapi({
+    description: '总记录数',
+    example: 3
+  })
+}).openapi('SalaryVideosResponse');
+
+// 查询参数Schema - 薪资查询
+export const SalaryQuerySchema = z.object({
+  month: z.string().regex(/^\d{4}-\d{2}$/).optional().openapi({
+    description: '月份过滤,格式: YYYY-MM',
+    example: '2025-01'
+  }),
+  skip: z.coerce.number().int().min(0).default(0).openapi({
+    description: '跳过的记录数',
+    example: 0
+  }),
+  take: z.coerce.number().int().min(1).max(100).default(10).openapi({
+    description: '每页记录数',
+    example: 10
+  })
+}).openapi('SalaryQuery');
+
+// 查询参数Schema - 就业历史查询
+export const EmploymentHistoryQuerySchema = z.object({
+  skip: z.coerce.number().int().min(0).default(0).openapi({
+    description: '跳过的记录数',
+    example: 0
+  }),
+  take: z.coerce.number().int().min(1).max(100).default(20).openapi({
+    description: '每页记录数',
+    example: 20
+  })
+}).openapi('EmploymentHistoryQuery');
+
+// 查询参数Schema - 薪资视频查询
+export const SalaryVideoQuerySchema = z.object({
+  assetType: z.enum(['salary_video', 'tax_video']).optional().openapi({
+    description: '视频类型过滤',
+    example: 'salary_video'
+  }),
+  month: z.string().regex(/^\d{4}-\d{2}$/).optional().openapi({
+    description: '月份过滤,格式: YYYY-MM',
+    example: '2025-01'
+  }),
+  skip: z.coerce.number().int().min(0).default(0).openapi({
+    description: '跳过的记录数',
+    example: 0
+  }),
+  take: z.coerce.number().int().min(1).max(100).default(10).openapi({
+    description: '每页记录数',
+    example: 10
+  })
+}).openapi('SalaryVideoQuery');

+ 232 - 0
allin-packages/order-module/src/services/order.service.ts

@@ -5,6 +5,7 @@ import { OrderPerson } from '../entities/order-person.entity';
 import { OrderPersonAsset } from '../entities/order-person-asset.entity';
 import { AssetType, AssetFileType, AssetStatus } from '../schemas/order.schema';
 import { FileService, File } from '@d8d/core-module/file-module';
+import { Company } from '@d8d/allin-company-module/entities';
 import { OrderStatus, WorkStatus } from '@d8d/allin-enums';
 
 export class OrderService extends GenericCrudService<EmploymentOrder> {
@@ -919,4 +920,235 @@ export class OrderService extends GenericCrudService<EmploymentOrder> {
 
     return asset;
   }
+
+  // ============================================================================
+  // 人才专用查询方法 - 故事015.005
+  // ============================================================================
+
+  /**
+   * 获取人才当前就业状态
+   * @param personId 残疾人ID
+   * @returns 当前就业状态或null
+   */
+  async getCurrentEmploymentStatus(personId: number): Promise<any | null> {
+    // 查询当前就业状态,条件: person_id AND work_status IN ('pre_working', 'working')
+    // 按join_date降序取最新的一条
+    const orderPerson = await this.orderPersonRepository
+      .createQueryBuilder('op')
+      .leftJoinAndSelect('op.order', 'order')
+      .leftJoin('order.company', 'company')
+      .addSelect(['company.id', 'company.companyName'])
+      .where('op.personId = :personId', { personId })
+      .andWhere('op.workStatus IN (:...statuses)', { statuses: [WorkStatus.PRE_WORKING, WorkStatus.WORKING] })
+      .orderBy('op.joinDate', 'DESC')
+      .getOne();
+
+    if (!orderPerson || !orderPerson.order) {
+      return null;
+    }
+
+    // 获取企业信息
+    const company = await this.dataSource.getRepository(Company)
+      .findOne({ where: { id: orderPerson.order.companyId } });
+
+    // 格式化返回数据
+    return {
+      companyName: company?.companyName || '',
+      orderId: orderPerson.orderId,
+      orderName: orderPerson.order.orderName || null,
+      positionName: orderPerson.order.orderName || null,
+      joinDate: this.formatDate(orderPerson.joinDate),
+      workStatus: orderPerson.workStatus,
+      salaryLevel: Number(orderPerson.salaryDetail),
+      actualStartDate: orderPerson.actualStartDate ? this.formatDate(orderPerson.actualStartDate) : null
+    };
+  }
+
+  /**
+   * 获取人才薪资记录
+   * @param personId 残疾人ID
+   * @param month 可选月份过滤,格式: YYYY-MM
+   * @param skip 跳过记录数
+   * @param take 获取记录数
+   * @returns 薪资记录列表
+   */
+  async getSalaryRecords(
+    personId: number,
+    month?: string,
+    skip: number = 0,
+    take: number = 10
+  ): Promise<{ data: any[]; total: number }> {
+    const queryBuilder = this.orderPersonRepository
+      .createQueryBuilder('op')
+      .leftJoinAndSelect('op.order', 'order')
+      .leftJoin('order.company', 'company')
+      .addSelect(['company.id', 'company.companyName'])
+      .where('op.personId = :personId', { personId });
+
+    // 月份过滤
+    if (month) {
+      // month格式: YYYY-MM,需要匹配join_date的年月
+      queryBuilder.andWhere('DATE_FORMAT(op.joinDate, :format) = :month', {
+        format: '%Y-%m',
+        month
+      });
+    }
+
+    // 获取总数
+    const total = await queryBuilder.getCount();
+
+    // 获取数据
+    const orderPersons = await queryBuilder
+      .orderBy('op.joinDate', 'DESC')
+      .skip(skip)
+      .take(take)
+      .getMany();
+
+    // 获取企业信息并格式化数据
+    const data = await Promise.all(
+      orderPersons.map(async (op) => {
+        const company = await this.dataSource.getRepository(Company)
+          .findOne({ where: { id: op.order.companyId } });
+
+        return {
+          orderId: op.orderId,
+          orderName: op.order.orderName || null,
+          companyName: company?.companyName || null,
+          salaryAmount: Number(op.salaryDetail),
+          joinDate: this.formatDate(op.joinDate),
+          month: this.formatDate(op.joinDate).substring(0, 7) // YYYY-MM
+        };
+      })
+    );
+
+    return { data, total };
+  }
+
+  /**
+   * 获取人才就业历史
+   * @param personId 残疾人ID
+   * @param skip 跳过记录数
+   * @param take 获取记录数
+   * @returns 就业历史列表
+   */
+  async getEmploymentHistory(
+    personId: number,
+    skip: number = 0,
+    take: number = 20
+  ): Promise<{ data: any[]; total: number }> {
+    const queryBuilder = this.orderPersonRepository
+      .createQueryBuilder('op')
+      .leftJoinAndSelect('op.order', 'order')
+      .leftJoin('order.company', 'company')
+      .addSelect(['company.id', 'company.companyName'])
+      .where('op.personId = :personId', { personId });
+
+    // 获取总数
+    const total = await queryBuilder.getCount();
+
+    // 获取数据,按join_date降序
+    const orderPersons = await queryBuilder
+      .orderBy('op.joinDate', 'DESC')
+      .skip(skip)
+      .take(take)
+      .getMany();
+
+    // 获取企业信息并格式化数据
+    const data = await Promise.all(
+      orderPersons.map(async (op) => {
+        const company = await this.dataSource.getRepository(Company)
+          .findOne({ where: { id: op.order.companyId } });
+
+        return {
+          orderId: op.orderId,
+          orderName: op.order.orderName || null,
+          companyName: company?.companyName || null,
+          positionName: op.order.orderName || null,
+          joinDate: this.formatDate(op.joinDate),
+          leaveDate: op.leaveDate ? this.formatDate(op.leaveDate) : null,
+          workStatus: op.workStatus,
+          salaryLevel: Number(op.salaryDetail)
+        };
+      })
+    );
+
+    return { data, total };
+  }
+
+  /**
+   * 获取人才薪资视频
+   * @param personId 残疾人ID
+   * @param assetType 可选视频类型过滤
+   * @param month 可选月份过滤,格式: YYYY-MM
+   * @param skip 跳过记录数
+   * @param take 获取记录数
+   * @returns 薪资视频列表
+   */
+  async getSalaryVideos(
+    personId: number,
+    assetType?: 'salary_video' | 'tax_video',
+    month?: string,
+    skip: number = 0,
+    take: number = 10
+  ): Promise<{ data: any[]; total: number }> {
+    const queryBuilder = this.orderPersonAssetRepository
+      .createQueryBuilder('asset')
+      .leftJoinAndSelect('asset.file', 'file')
+      .where('asset.personId = :personId', { personId })
+      .andWhere('asset.assetType IN (:...types)', {
+        types: [AssetType.SALARY_VIDEO, AssetType.TAX_VIDEO]
+      });
+
+    // 视频类型过滤
+    if (assetType) {
+      queryBuilder.andWhere('asset.assetType = :assetType', { assetType });
+    }
+
+    // 月份过滤 - relatedTime是timestamp类型
+    if (month) {
+      queryBuilder.andWhere('DATE_FORMAT(asset.relatedTime, :format) = :month', {
+        format: '%Y-%m',
+        month
+      });
+    }
+
+    // 获取总数
+    const total = await queryBuilder.getCount();
+
+    // 获取数据,按related_time降序
+    const assets = await queryBuilder
+      .orderBy('asset.relatedTime', 'DESC')
+      .skip(skip)
+      .take(take)
+      .getMany();
+
+    // 格式化数据
+    const data = assets.map(asset => {
+      const relatedTime = new Date(asset.relatedTime);
+      return {
+        id: asset.id,
+        assetType: asset.assetType,
+        assetFileType: asset.assetFileType,
+        fileUrl: asset.file?.fullUrl || null,
+        fileName: asset.file?.name || null,
+        status: asset.status || 'pending',
+        relatedTime: asset.relatedTime.toISOString(),
+        month: this.formatDate(relatedTime).substring(0, 7) // YYYY-MM
+      };
+    });
+
+    return { data, total };
+  }
+
+  /**
+   * 格式化日期为 YYYY-MM-DD 字符串
+   * @param date 日期对象
+   * @returns 格式化的日期字符串
+   */
+  private formatDate(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}-${month}-${day}`;
+  }
 }

+ 494 - 0
allin-packages/order-module/tests/integration/talent-employment.integration.test.ts

@@ -0,0 +1,494 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntity, Role } from '@d8d/user-module';
+import { File } from '@d8d/file-module';
+import { DisabledPerson, DisabledBankCard, DisabledPhoto, DisabledRemark, DisabledVisit } from '@d8d/allin-disability-module';
+import { Company } from '@d8d/allin-company-module/entities';
+import { Platform } from '@d8d/allin-platform-module';
+import { BankName } from '@d8d/bank-names-module';
+import talentEmploymentRoutes from '../../src/routes/talent-employment.routes';
+import { EmploymentOrder } from '../../src/entities/employment-order.entity';
+import { OrderPerson } from '../../src/entities/order-person.entity';
+import { OrderPersonAsset } from '../../src/entities/order-person-asset.entity';
+import { AssetType, AssetFileType } from '../../src/schemas/order.schema';
+import { OrderStatus, WorkStatus } from '@d8d/allin-enums';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([
+  UserEntity, Role, File, Platform, Company,
+  DisabledPerson, DisabledBankCard, DisabledPhoto, DisabledRemark, DisabledVisit,
+  BankName,
+  EmploymentOrder, OrderPerson, OrderPersonAsset
+]);
+
+describe('人才就业信息API集成测试 - 故事015.005', () => {
+  let client: ReturnType<typeof testClient<typeof talentEmploymentRoutes>>;
+  let testToken: string;
+  let testTalentUser: UserEntity;
+  let testDisabledPerson: DisabledPerson;
+  let testCompany: Company;
+  let testPlatform: Platform;
+  let testFile: File;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(talentEmploymentRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试平台
+    const platformRepository = dataSource.getRepository(Platform);
+    testPlatform = platformRepository.create({
+      platformName: '测试平台',
+      status: 1
+    });
+    await platformRepository.save(testPlatform);
+
+    // 创建测试企业
+    const companyRepository = dataSource.getRepository(Company);
+    testCompany = companyRepository.create({
+      platformId: testPlatform.id,
+      companyName: '测试科技有限公司',
+      contactPerson: '张三',
+      contactPhone: '13800138000',
+      status: 1
+    });
+    await companyRepository.save(testCompany);
+
+    // 创建测试文件(用于视频资产)
+    const fileRepository = dataSource.getRepository(File);
+    testFile = fileRepository.create({
+      name: '工资视频_2025-01.mp4',
+      type: 'video/mp4',
+      size: 1024000,
+      path: `videos/${Date.now()}_salary_video.mp4`,
+      fullUrl: `https://example.com/videos/salary_video.mp4`,
+      uploadTime: new Date(),
+      createdAt: new Date(),
+      updatedAt: new Date()
+    });
+    await fileRepository.save(testFile);
+
+    // 创建测试银行名称
+    const bankNameRepository = dataSource.getRepository(BankName);
+    const testBankName = bankNameRepository.create({
+      name: '测试银行',
+      code: 'TEST001',
+      remark: '测试银行',
+      status: 1
+    });
+    await bankNameRepository.save(testBankName);
+
+    // 创建测试残疾人
+    const personRepository = dataSource.getRepository(DisabledPerson);
+    testDisabledPerson = personRepository.create({
+      name: '李四',
+      gender: 'male',
+      disabilityType: 'physical',
+      idCard: '110101199001011234',
+      phone: '13900139000',
+      address: '北京市朝阳区',
+      status: 1
+    });
+    await personRepository.save(testDisabledPerson);
+
+    // 创建测试人才用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    testTalentUser = userRepository.create({
+      username: `talent_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试人才',
+      userType: 'talent',
+      personId: testDisabledPerson.id,
+      registrationSource: 'mini'
+    });
+    await userRepository.save(testTalentUser);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testTalentUser.id,
+      username: testTalentUser.username,
+      userType: 'talent',
+      personId: testDisabledPerson.id,
+      roles: [{ name: 'talent' }]
+    });
+  });
+
+  describe('GET /employment/status - 当前就业状态查询', () => {
+    it('应该成功查询当前就业状态 - 在职状态', async () => {
+      // 创建测试订单
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository(EmploymentOrder);
+      const testOrder = orderRepository.create({
+        platformId: testPlatform.id,
+        companyId: testCompany.id,
+        orderName: '包装工',
+        expectedStartDate: new Date('2025-01-01'),
+        actualStartDate: new Date('2025-01-15'),
+        orderStatus: OrderStatus.IN_PROGRESS,
+        workStatus: WorkStatus.WORKING
+      });
+      await orderRepository.save(testOrder);
+
+      // 创建订单人员关联
+      const orderPersonRepository = dataSource.getRepository(OrderPerson);
+      const orderPerson = orderPersonRepository.create({
+        orderId: testOrder.id,
+        personId: testDisabledPerson.id,
+        joinDate: new Date('2025-01-15'),
+        actualStartDate: new Date('2025-01-15'),
+        workStatus: WorkStatus.WORKING,
+        salaryDetail: 3500.00
+      });
+      await orderPersonRepository.save(orderPerson);
+
+      // 查询当前就业状态
+      const response = await client['employment.status'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.companyName).toBe('测试科技有限公司');
+      expect(data.orderId).toBe(testOrder.id);
+      expect(data.orderName).toBe('包装工');
+      expect(data.positionName).toBe('包装工');
+      expect(data.joinDate).toBe('2025-01-15');
+      expect(data.workStatus).toBe('working');
+      expect(data.salaryLevel).toBe(3500.00);
+      expect(data.actualStartDate).toBe('2025-01-15');
+    });
+
+    it('应该返回404当用户无就业记录时', async () => {
+      const response = await client['employment.status'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+
+      const data = await response.json();
+      expect(data.code).toBe(404);
+      expect(data.message).toContain('未找到就业记录');
+    });
+
+    it('应该返回401当用户未认证时', async () => {
+      const response = await client['employment.status'].$get();
+
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('GET /employment/salary-records - 薪资记录查询', () => {
+    beforeEach(async () => {
+      // 创建测试订单和人员关联
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository(EmploymentOrder);
+
+      // 创建3个不同月份的订单
+      const orders = [
+        { orderName: '包装工1月', joinDate: new Date('2025-01-15'), salary: 3500.00 },
+        { orderName: '包装工2月', joinDate: new Date('2025-02-01'), salary: 3600.00 },
+        { orderName: '包装工3月', joinDate: new Date('2025-03-10'), salary: 3700.00 }
+      ];
+
+      for (const orderData of orders) {
+        const testOrder = orderRepository.create({
+          platformId: testPlatform.id,
+          companyId: testCompany.id,
+          orderName: orderData.orderName,
+          expectedStartDate: orderData.joinDate,
+          actualStartDate: orderData.joinDate,
+          orderStatus: OrderStatus.IN_PROGRESS,
+          workStatus: WorkStatus.WORKING
+        });
+        await orderRepository.save(testOrder);
+
+        const orderPersonRepository = dataSource.getRepository(OrderPerson);
+        const orderPerson = orderPersonRepository.create({
+          orderId: testOrder.id,
+          personId: testDisabledPerson.id,
+          joinDate: orderData.joinDate,
+          workStatus: WorkStatus.WORKING,
+          salaryDetail: orderData.salary
+        });
+        await orderPersonRepository.save(orderPerson);
+      }
+    });
+
+    it('应该成功查询薪资记录列表', async () => {
+      const response = await client['employment.salary-records'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(3);
+      expect(data.data).toHaveLength(3);
+      expect(data.data[0].month).toBe('2025-03'); // 按joinDate降序
+      expect(data.data[0].salaryAmount).toBe(3700.00);
+      expect(data.data[0].companyName).toBe('测试科技有限公司');
+    });
+
+    it('应该支持按月份过滤', async () => {
+      const response = await client['employment.salary-records'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          month: '2025-01'
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(1);
+      expect(data.data[0].month).toBe('2025-01');
+      expect(data.data[0].salaryAmount).toBe(3500.00);
+    });
+
+    it('应该支持分页', async () => {
+      const response = await client['employment.salary-records'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          skip: 0,
+          take: 2
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(3);
+      expect(data.data).toHaveLength(2);
+    });
+  });
+
+  describe('GET /employment/history - 就业历史查询', () => {
+    beforeEach(async () => {
+      // 创建测试订单和人员关联
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository(EmploymentOrder);
+
+      // 创建包含不同状态的订单
+      const orders = [
+        {
+          orderName: '包装工',
+          joinDate: new Date('2025-01-15'),
+          leaveDate: null,
+          workStatus: WorkStatus.WORKING,
+          salary: 3500.00
+        },
+        {
+          orderName: '清洁工',
+          joinDate: new Date('2024-06-01'),
+          leaveDate: new Date('2024-12-31'),
+          workStatus: WorkStatus.RESIGNED,
+          salary: 3200.00
+        }
+      ];
+
+      for (const orderData of orders) {
+        const testOrder = orderRepository.create({
+          platformId: testPlatform.id,
+          companyId: testCompany.id,
+          orderName: orderData.orderName,
+          expectedStartDate: orderData.joinDate,
+          actualStartDate: orderData.joinDate,
+          actualEndDate: orderData.leaveDate,
+          orderStatus: OrderStatus.IN_PROGRESS,
+          workStatus: orderData.workStatus
+        });
+        await orderRepository.save(testOrder);
+
+        const orderPersonRepository = dataSource.getRepository(OrderPerson);
+        const orderPerson = orderPersonRepository.create({
+          orderId: testOrder.id,
+          personId: testDisabledPerson.id,
+          joinDate: orderData.joinDate,
+          leaveDate: orderData.leaveDate,
+          workStatus: orderData.workStatus,
+          salaryDetail: orderData.salary
+        });
+        await orderPersonRepository.save(orderPerson);
+      }
+    });
+
+    it('应该成功查询就业历史,包含所有状态', async () => {
+      const response = await client['employment.history'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(2);
+      expect(data.data).toHaveLength(2);
+      expect(data.data[0].workStatus).toBe('working'); // 最新的在前
+      expect(data.data[1].workStatus).toBe('resigned');
+      expect(data.data[1].leaveDate).toBe('2024-12-31');
+    });
+
+    it('应该支持分页', async () => {
+      const response = await client['employment.history'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          skip: 0,
+          take: 1
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(2);
+      expect(data.data).toHaveLength(1);
+    });
+  });
+
+  describe('GET /employment/salary-videos - 薪资视频查询', () => {
+    beforeEach(async () => {
+      // 创建测试订单和视频资产
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository(EmploymentOrder);
+
+      const testOrder = orderRepository.create({
+        platformId: testPlatform.id,
+        companyId: testCompany.id,
+        orderName: '包装工',
+        expectedStartDate: new Date('2025-01-01'),
+        actualStartDate: new Date('2025-01-15'),
+        orderStatus: OrderStatus.IN_PROGRESS,
+        workStatus: WorkStatus.WORKING
+      });
+      await orderRepository.save(testOrder);
+
+      // 创建订单人员关联
+      const orderPersonRepository = dataSource.getRepository(OrderPerson);
+      const orderPerson = orderPersonRepository.create({
+        orderId: testOrder.id,
+        personId: testDisabledPerson.id,
+        joinDate: new Date('2025-01-15'),
+        workStatus: WorkStatus.WORKING,
+        salaryDetail: 3500.00
+      });
+      await orderPersonRepository.save(orderPerson);
+
+      // 创建视频资产
+      const assetRepository = dataSource.getRepository(OrderPersonAsset);
+      const assets = [
+        {
+          assetType: AssetType.SALARY_VIDEO,
+          relatedTime: new Date('2025-01-15T10:00:00Z')
+        },
+        {
+          assetType: AssetType.TAX_VIDEO,
+          relatedTime: new Date('2025-01-20T14:00:00Z')
+        },
+        {
+          assetType: AssetType.SALARY_VIDEO,
+          relatedTime: new Date('2025-02-15T10:00:00Z')
+        }
+      ];
+
+      for (const assetData of assets) {
+        const asset = assetRepository.create({
+          orderId: testOrder.id,
+          personId: testDisabledPerson.id,
+          assetType: assetData.assetType,
+          assetFileType: AssetFileType.VIDEO,
+          fileId: testFile.id,
+          relatedTime: assetData.relatedTime,
+          status: 'verified'
+        });
+        await assetRepository.save(asset);
+      }
+    });
+
+    it('应该成功查询薪资视频列表', async () => {
+      const response = await client['employment.salary-videos'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(3);
+      expect(data.data).toHaveLength(3);
+      expect(data.data[0].assetType).toBe('salary_video');
+      expect(data.data[0].fileUrl).toBe('https://example.com/videos/salary_video.mp4');
+      expect(data.data[0].status).toBe('verified');
+    });
+
+    it('应该支持按视频类型过滤', async () => {
+      const response = await client['employment.salary-videos'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          assetType: 'salary_video'
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(2);
+      expect(data.data.every((v: any) => v.assetType === 'salary_video')).toBe(true);
+    });
+
+    it('应该支持按月份过滤', async () => {
+      const response = await client['employment.salary-videos'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          month: '2025-01'
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(2);
+      expect(data.data.every((v: any) => v.month === '2025-01')).toBe(true);
+    });
+
+    it('应该支持分页', async () => {
+      const response = await client['employment.salary-videos'].$get({
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        },
+        query: {
+          skip: 0,
+          take: 2
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const data = await response.json();
+      expect(data.total).toBe(3);
+      expect(data.data).toHaveLength(2);
+    });
+  });
+});

+ 31 - 0
docs/stories/015.005.story.md

@@ -464,12 +464,43 @@ const EmploymentHistoryQuerySchema = z.object({
 *此部分由开发代理在实施过程中填写*
 
 ### Agent Model Used
+claude-sonnet
 
 ### Debug Log References
+无
 
 ### Completion Notes List
+1. 成功创建人才就业信息Schema,包含所有响应和查询参数Schema
+2. 在OrderService中添加了4个人才专用查询方法: getCurrentEmploymentStatus, getSalaryRecords, getEmploymentHistory, getSalaryVideos
+3. 创建了人才就业信息API路由,使用talentAuthMiddleware进行认证
+4. 在server包中注册了人才就业路由,使用/api/v1/rencai前缀
+5. 数据库查询性能优化通过现有索引实现
+6. 创建了集成测试文件,覆盖所有API端点
+7. 更新了模块导出,确保新路由和Schema正确导出
+
+### Known Issues
+1. **TypeScript类型错误** (待修复):
+   - 位置: `talent-employment.routes.ts`
+   - 问题: OpenAPI路由的错误响应返回类型(400, 404, 500)与定义的成功响应类型不兼容
+   - 当前方案: 使用 `as any` 临时绕过类型检查
+   - 影响: 不影响运行时功能,但TypeScript编译会有类型警告
+   - 建议修复方案:
+     - 方案A: 使用Hono的 `MiddlewareResponse` 类型定义统一错误响应
+     - 方案B: 创建自定义错误响应Schema
+     - 方案C: 参考 `talent-personal-info.routes.ts` 的实现模式
 
 ### File List
+**新增文件:**
+- `allin-packages/order-module/src/schemas/talent-employment.schema.ts` - 人才就业信息Schema定义
+- `allin-packages/order-module/src/routes/talent-employment.routes.ts` - 人才就业信息API路由
+- `allin-packages/order-module/tests/integration/talent-employment.integration.test.ts` - 集成测试
+
+**修改文件:**
+- `allin-packages/order-module/src/schemas/index.ts` - 导出talent-employment.schema
+- `allin-packages/order-module/src/services/order.service.ts` - 添加人才专用查询方法
+- `allin-packages/order-module/src/routes/order.routes.ts` - 导出talentEmploymentRoutes
+- `allin-packages/order-module/src/index.ts` - 导出Schema和路由
+- `packages/server/src/index.ts` - 注册人才就业路由到主应用
 
 ## QA Results
 *此部分由QA代理在审查完成后填写*

+ 3 - 1
packages/server/src/index.ts

@@ -18,7 +18,7 @@ import { companyRoutes, companyStatisticsRoutes, companyEnterpriseRoutes } from
 import { Company } from '@d8d/allin-company-module/entities'
 import { disabledPersonRoutes, personExtensionRoutes, talentPersonalInfoRoutes } from '@d8d/allin-disability-module'
 import { DisabledPerson, DisabledBankCard, DisabledPhoto, DisabledRemark, DisabledVisit } from '@d8d/allin-disability-module/entities'
-import { orderRoutes, enterpriseOrderRoutes } from '@d8d/allin-order-module'
+import { orderRoutes, enterpriseOrderRoutes, talentEmploymentRoutes } from '@d8d/allin-order-module'
 import { statisticsRoutes } from '@d8d/allin-statistics-module'
 import { EmploymentOrder, OrderPerson, OrderPersonAsset } from '@d8d/allin-order-module/entities'
 import { platformRoutes } from '@d8d/allin-platform-module'
@@ -158,6 +158,7 @@ export const enterpriseStatisticsApiRoutes = api.route('/api/v1/yongren/statisti
 // 人才用户专用路由(人才小程序)
 export const talentAuthApiRoutes = api.route('/api/v1/rencai/auth', talentAuthModuleRoutes)
 export const talentPersonalInfoApiRoutes = api.route('/api/v1/rencai', talentPersonalInfoRoutes)
+export const talentEmploymentApiRoutes = api.route('/api/v1/rencai', talentEmploymentRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -179,6 +180,7 @@ export type EnterpriseOrderRoutes = typeof enterpriseOrderApiRoutes
 export type EnterpriseStatisticsRoutes = typeof enterpriseStatisticsApiRoutes
 export type TalentAuthRoutes = typeof talentAuthApiRoutes
 export type TalentPersonalInfoRoutes = typeof talentPersonalInfoApiRoutes
+export type TalentEmploymentRoutes = typeof talentEmploymentApiRoutes
 
 app.route('/', api)
 export default app