disabled-person.service.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. import { GenericCrudService } from '@d8d/shared-crud';
  2. import { Repository, DataSource, Not, In } from 'typeorm';
  3. import { DisabledPerson } from '../entities/disabled-person.entity';
  4. import { DisabledBankCard } from '../entities/disabled-bank-card.entity';
  5. import { DisabledPhoto } from '../entities/disabled-photo.entity';
  6. import { DisabledRemark } from '../entities/disabled-remark.entity';
  7. import { DisabledVisit } from '../entities/disabled-visit.entity';
  8. import { FileService, File } from '@d8d/file-module';
  9. import{ OrderPerson, OrderPersonAsset } from '@d8d/allin-order-module';
  10. import { WorkStatus, getWorkStatusLabel } from '@d8d/allin-enums';
  11. // 前端专用的工作状态中文标签映射(与mini-ui保持一致)
  12. const FrontendWorkStatusLabels: Record<WorkStatus, string> = {
  13. [WorkStatus.NOT_WORKING]: '未就业',
  14. [WorkStatus.PRE_WORKING]: '待入职',
  15. [WorkStatus.WORKING]: '在职',
  16. [WorkStatus.RESIGNED]: '离职'
  17. };
  18. export class DisabledPersonService extends GenericCrudService<DisabledPerson> {
  19. private readonly bankCardRepository: Repository<DisabledBankCard>;
  20. private readonly photoRepository: Repository<DisabledPhoto>;
  21. private readonly remarkRepository: Repository<DisabledRemark>;
  22. private readonly visitRepository: Repository<DisabledVisit>;
  23. private readonly fileRepository: Repository<File>;
  24. private fileService: FileService;
  25. constructor(dataSource: DataSource) {
  26. super(dataSource, DisabledPerson);
  27. this.bankCardRepository = dataSource.getRepository(DisabledBankCard);
  28. this.photoRepository = dataSource.getRepository(DisabledPhoto);
  29. this.remarkRepository = dataSource.getRepository(DisabledRemark);
  30. this.visitRepository = dataSource.getRepository(DisabledVisit);
  31. this.fileRepository = dataSource.getRepository(File);
  32. this.fileService = new FileService(dataSource);
  33. }
  34. /**
  35. * 创建残疾人 - 覆盖父类方法,添加身份证号唯一性检查
  36. */
  37. override async create(data: Partial<DisabledPerson>, userId?: string | number): Promise<DisabledPerson> {
  38. // 检查身份证号是否已存在
  39. if (data.idCard) {
  40. const existingPerson = await this.repository.findOne({
  41. where: { idCard: data.idCard }
  42. });
  43. if (existingPerson) {
  44. throw new Error('身份证号已存在');
  45. }
  46. }
  47. // 检查残疾证号是否已存在
  48. if (data.disabilityId) {
  49. const existingPerson = await this.repository.findOne({
  50. where: { disabilityId: data.disabilityId }
  51. });
  52. if (existingPerson) {
  53. throw new Error('残疾证号已存在');
  54. }
  55. }
  56. return super.create(data, userId);
  57. }
  58. /**
  59. * 更新残疾人 - 覆盖父类方法,添加身份证号唯一性检查
  60. */
  61. override async update(id: number, data: Partial<DisabledPerson>, userId?: string | number): Promise<DisabledPerson | null> {
  62. // 检查残疾人是否存在
  63. const person = await this.repository.findOne({ where: { id } });
  64. if (!person) {
  65. throw new Error('残疾人不存在');
  66. }
  67. // 检查身份证号是否与其他残疾人重复
  68. if (data.idCard && data.idCard !== person.idCard) {
  69. const existingPerson = await this.repository.findOne({
  70. where: { idCard: data.idCard, id: Not(id) }
  71. });
  72. if (existingPerson) {
  73. throw new Error('身份证号已存在');
  74. }
  75. }
  76. // 检查残疾证号是否与其他残疾人重复
  77. if (data.disabilityId && data.disabilityId !== person.disabilityId) {
  78. const existingPerson = await this.repository.findOne({
  79. where: { disabilityId: data.disabilityId, id: Not(id) }
  80. });
  81. if (existingPerson) {
  82. throw new Error('残疾证号已存在');
  83. }
  84. }
  85. return super.update(id, data, userId);
  86. }
  87. /**
  88. * 获取单个残疾人(包含关联数据)
  89. */
  90. async findOne(id: number): Promise<DisabledPerson | null> {
  91. const person = await this.repository.findOne({
  92. where: { id },
  93. relations: ['bankCards', 'bankCards.bankName', 'bankCards.file', 'bankCards.file.uploadUser', 'photos', 'photos.file', 'photos.file.uploadUser', 'remarks', 'visits']
  94. });
  95. return person;
  96. }
  97. /**
  98. * 获取所有残疾人(分页+条件查询) - 返回源服务的格式
  99. */
  100. async findAll(query: {
  101. keyword?: string;
  102. bankNameId?: number;
  103. cardType?: string;
  104. disabilityType?: string;
  105. disabilityLevel?: string;
  106. province?: string;
  107. page?: number;
  108. limit?: number;
  109. }): Promise<{ data: DisabledPerson[], total: number }> {
  110. const {
  111. keyword,
  112. bankNameId,
  113. cardType,
  114. disabilityType,
  115. disabilityLevel,
  116. province,
  117. page = 1,
  118. limit = 10
  119. } = query;
  120. const queryBuilder = this.repository.createQueryBuilder('person');
  121. // 支持按关键词搜索(姓名或身份证号)
  122. if (keyword) {
  123. queryBuilder.andWhere('(person.name LIKE :keyword OR person.idCard LIKE :keyword)', {
  124. keyword: `%${keyword}%`
  125. });
  126. }
  127. if (disabilityType) {
  128. queryBuilder.andWhere('person.disabilityType = :disabilityType', { disabilityType });
  129. }
  130. if (disabilityLevel) {
  131. queryBuilder.andWhere('person.disabilityLevel = :disabilityLevel', { disabilityLevel });
  132. }
  133. if (province) {
  134. queryBuilder.andWhere('person.province = :province', { province });
  135. }
  136. // 处理银行卡相关筛选条件
  137. if (bankNameId || cardType) {
  138. queryBuilder.innerJoin('person.bankCards', 'bankCard');
  139. if (bankNameId) {
  140. queryBuilder.andWhere('bankCard.bankNameId = :bankNameId', { bankNameId });
  141. }
  142. if (cardType) {
  143. queryBuilder.andWhere('bankCard.cardType = :cardType', { cardType });
  144. }
  145. // 加载bankCards关系及其关联的bankName
  146. queryBuilder.leftJoinAndSelect('person.bankCards', 'bankCards')
  147. .leftJoinAndSelect('bankCards.bankName', 'bankName');
  148. }
  149. const [data, total] = await queryBuilder
  150. .skip((page - 1) * limit)
  151. .take(limit)
  152. .orderBy('person.createTime', 'DESC')
  153. .getManyAndCount();
  154. // 加载关联数据
  155. if (data.length > 0) {
  156. const personIds = data.map(p => p.id);
  157. const [bankCards, photos, remarks, visits] = await Promise.all([
  158. this.bankCardRepository.find({ where: { personId: In(personIds) } }),
  159. this.photoRepository.find({
  160. where: { personId: In(personIds) },
  161. relations: ['file']
  162. }),
  163. this.remarkRepository.find({ where: { personId: In(personIds) } }),
  164. this.visitRepository.find({ where: { personId: In(personIds) } })
  165. ]);
  166. // 将关联数据分组到对应的残疾人
  167. const bankCardsMap = new Map<number, DisabledBankCard[]>();
  168. const photosMap = new Map<number, DisabledPhoto[]>();
  169. const remarksMap = new Map<number, DisabledRemark[]>();
  170. const visitsMap = new Map<number, DisabledVisit[]>();
  171. for (const card of bankCards) {
  172. const cards = bankCardsMap.get(card.personId) || [];
  173. cards.push(card);
  174. bankCardsMap.set(card.personId, cards);
  175. }
  176. for (const photo of photos) {
  177. const photos = photosMap.get(photo.personId) || [];
  178. photos.push(photo);
  179. photosMap.set(photo.personId, photos);
  180. }
  181. for (const remark of remarks) {
  182. const remarks = remarksMap.get(remark.personId) || [];
  183. remarks.push(remark);
  184. remarksMap.set(remark.personId, remarks);
  185. }
  186. for (const visit of visits) {
  187. const visits = visitsMap.get(visit.personId) || [];
  188. visits.push(visit);
  189. visitsMap.set(visit.personId, visits);
  190. }
  191. // 将关联数据附加到每个残疾人
  192. for (const person of data) {
  193. person.bankCards = bankCardsMap.get(person.id) || [];
  194. person.photos = photosMap.get(person.id) || [];
  195. person.remarks = remarksMap.get(person.id) || [];
  196. person.visits = visitsMap.get(person.id) || [];
  197. }
  198. }
  199. return { data, total };
  200. }
  201. /**
  202. * 根据身份证号查询残疾人
  203. */
  204. async findByIdCard(idCard: string): Promise<DisabledPerson | null> {
  205. const person = await this.repository.findOne({
  206. where: { idCard },
  207. relations: ['bankCards', 'photos', 'photos.file', 'remarks', 'visits']
  208. });
  209. return person;
  210. }
  211. /**
  212. * 验证文件ID是否存在
  213. */
  214. async validateFileId(fileId: number): Promise<boolean> {
  215. try {
  216. const file = await this.fileRepository.findOne({ where: { id: fileId } });
  217. return !!file;
  218. } catch (error) {
  219. console.error('验证文件ID失败:', error);
  220. return false;
  221. }
  222. }
  223. /**
  224. * 创建照片记录(包含文件验证)
  225. */
  226. async createPhoto(photoData: Partial<DisabledPhoto>): Promise<DisabledPhoto> {
  227. // 验证文件ID是否存在
  228. if (photoData.fileId) {
  229. const fileExists = await this.validateFileId(photoData.fileId);
  230. if (!fileExists) {
  231. throw new Error('文件不存在');
  232. }
  233. } else {
  234. throw new Error('文件ID不能为空');
  235. }
  236. // 验证残疾人是否存在
  237. if (photoData.personId) {
  238. const personExists = await this.repository.findOne({ where: { id: photoData.personId } });
  239. if (!personExists) {
  240. throw new Error('残疾人不存在');
  241. }
  242. } else {
  243. throw new Error('残疾人ID不能为空');
  244. }
  245. const photo = this.photoRepository.create(photoData);
  246. return await this.photoRepository.save(photo);
  247. }
  248. /**
  249. * 批量创建照片记录
  250. */
  251. async createPhotosBatch(photosData: Partial<DisabledPhoto>[]): Promise<DisabledPhoto[]> {
  252. // 验证所有文件ID和残疾人ID
  253. for (const photoData of photosData) {
  254. if (photoData.fileId) {
  255. const fileExists = await this.validateFileId(photoData.fileId);
  256. if (!fileExists) {
  257. throw new Error(`文件ID ${photoData.fileId} 不存在`);
  258. }
  259. } else {
  260. throw new Error('文件ID不能为空');
  261. }
  262. if (photoData.personId) {
  263. const personExists = await this.repository.findOne({ where: { id: photoData.personId } });
  264. if (!personExists) {
  265. throw new Error(`残疾人ID ${photoData.personId} 不存在`);
  266. }
  267. } else {
  268. throw new Error('残疾人ID不能为空');
  269. }
  270. }
  271. const photos = this.photoRepository.create(photosData);
  272. return await this.photoRepository.save(photos);
  273. }
  274. /**
  275. * 批量创建残疾人
  276. */
  277. async batchCreate(persons: Partial<DisabledPerson>[]): Promise<{
  278. success: boolean;
  279. createdCount: number;
  280. failedItems: Array<{ index: number; error: string }>;
  281. }> {
  282. const failedItems: Array<{ index: number; error: string }> = [];
  283. let createdCount = 0;
  284. // 使用事务处理批量创建
  285. const queryRunner = this.dataSource.createQueryRunner();
  286. await queryRunner.connect();
  287. await queryRunner.startTransaction();
  288. try {
  289. for (let i = 0; i < persons.length; i++) {
  290. const personData = persons[i];
  291. if (personData === undefined) {
  292. failedItems.push({
  293. index: i,
  294. error: '数据为undefined'
  295. });
  296. continue;
  297. }
  298. try {
  299. // 检查身份证号是否已存在
  300. if (personData.idCard) {
  301. const existingPerson = await this.repository.findOne({
  302. where: { idCard: personData.idCard }
  303. });
  304. if (existingPerson) {
  305. throw new Error('身份证号已存在');
  306. }
  307. }
  308. // 检查残疾证号是否已存在
  309. if (personData.disabilityId) {
  310. const existingPerson = await this.repository.findOne({
  311. where: { disabilityId: personData.disabilityId }
  312. });
  313. if (existingPerson) {
  314. throw new Error('残疾证号已存在');
  315. }
  316. }
  317. // 创建残疾人
  318. const person = this.repository.create(personData);
  319. await queryRunner.manager.save(person);
  320. createdCount++;
  321. } catch (error) {
  322. failedItems.push({
  323. index: i,
  324. error: error instanceof Error ? error.message : '未知错误'
  325. });
  326. }
  327. }
  328. await queryRunner.commitTransaction();
  329. return {
  330. success: failedItems.length === 0,
  331. createdCount,
  332. failedItems
  333. };
  334. } catch (error) {
  335. await queryRunner.rollbackTransaction();
  336. throw error;
  337. } finally {
  338. await queryRunner.release();
  339. }
  340. }
  341. /**
  342. * 获取人才工作历史
  343. */
  344. async getWorkHistory(personId: number): Promise<any[]> {
  345. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  346. const workHistory = await orderPersonRepo.find({
  347. where: { personId },
  348. relations: ['order'],
  349. order: { joinDate: 'DESC' }
  350. });
  351. return workHistory.map(item => ({
  352. 订单ID: item.orderId,
  353. 订单名称: item.order?.orderName,
  354. 入职日期: item.joinDate, // z.coerce.date().nullable()会自动转换
  355. 实际入职日期: item.actualStartDate, // z.coerce.date().nullable()会自动转换
  356. 离职日期: item.leaveDate, // z.coerce.date().nullable()会自动转换
  357. 工作状态: FrontendWorkStatusLabels[item.workStatus] || getWorkStatusLabel(item.workStatus), // 使用前端专用映射,后备使用默认映射
  358. 个人薪资: item.salaryDetail // z.coerce.number()会自动转换
  359. }));
  360. }
  361. /**
  362. * 获取人才薪资历史
  363. */
  364. async getSalaryHistory(personId: number): Promise<any[]> {
  365. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  366. const salaryHistory = await orderPersonRepo.find({
  367. where: { personId },
  368. relations: ['order'],
  369. order: { joinDate: 'DESC' }
  370. });
  371. // 假设薪资详情存储在 salaryDetail 字段中
  372. // 这里可以根据实际业务逻辑解析薪资数据
  373. return salaryHistory.map(item => ({
  374. 月份: item.joinDate, // z.coerce.date().nullable()会自动转换
  375. 基本工资: item.salaryDetail, // z.coerce.number()会自动转换
  376. 补贴: 0, // 需要根据业务逻辑计算
  377. 扣款: 0, // 需要根据业务逻辑计算
  378. 实发工资: item.salaryDetail // z.coerce.number()会自动转换
  379. }));
  380. }
  381. /**
  382. * 获取个人征信信息
  383. */
  384. async getCreditInfo(personId: number): Promise<any[]> {
  385. const bankCardRepo = this.dataSource.getRepository(DisabledBankCard);
  386. const bankCards = await bankCardRepo.find({
  387. where: { personId },
  388. relations: ['file']
  389. });
  390. return Promise.all(
  391. bankCards
  392. .filter(card => card.fileId) // 只返回有文件的记录
  393. .map(async (card) => ({
  394. 文件ID: card.fileId ? card.fileId.toString() : '',
  395. 文件URL: card.file ? await card.file.fullUrl : null,
  396. 上传时间: card.file?.uploadTime, // z.coerce.date().nullable()会自动转换
  397. 文件类型: card.file?.type,
  398. 银行卡号: card.cardNumber,
  399. 持卡人姓名: card.cardholderName,
  400. 银行名称: card.bankNameId // 这里可能需要关联银行名称表
  401. }))
  402. );
  403. }
  404. /**
  405. * 获取个人关联视频
  406. */
  407. async getPersonVideos(personId: number): Promise<any[]> {
  408. const orderPersonAssetRepo = this.dataSource.getRepository(OrderPersonAsset);
  409. // 视频类型枚举值,根据 order-person-asset.entity.ts 中的定义
  410. const videoTypes = ['salary_video', 'tax_video', 'checkin_video', 'work_video'];
  411. const videos = await orderPersonAssetRepo.find({
  412. where: {
  413. personId,
  414. assetType: In(videoTypes)
  415. },
  416. relations: ['file']
  417. });
  418. return Promise.all(
  419. videos.map(async (video) => ({
  420. 视频类型: video.assetType,
  421. 文件ID: video.fileId ? video.fileId.toString() : '',
  422. 文件URL: video.file ? await video.file.fullUrl : null,
  423. 上传时间: video.file?.uploadTime, // z.coerce.date().nullable()会自动转换
  424. 文件类型: video.file?.type,
  425. 关联订单ID: video.orderId
  426. }))
  427. );
  428. }
  429. /**
  430. * 验证人员是否属于指定企业
  431. */
  432. async validatePersonBelongsToCompany(personId: number, companyId: number): Promise<boolean> {
  433. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  434. // 查询人员是否通过订单关联到该企业
  435. const count = await orderPersonRepo.count({
  436. where: {
  437. personId,
  438. order: { companyId }
  439. }
  440. });
  441. return count > 0;
  442. }
  443. /**
  444. * 获取企业专用人才列表
  445. */
  446. async findAllForCompany(companyId: number, query: {
  447. search?: string;
  448. disabilityType?: string;
  449. jobStatus?: string;
  450. page?: number | string;
  451. limit?: number | string;
  452. }): Promise<{ data: any[], total: number }> {
  453. const {
  454. search,
  455. disabilityType,
  456. jobStatus,
  457. page = 1,
  458. limit = 10
  459. } = query;
  460. // 工作状态到中文标签的映射(使用前端专用映射)
  461. const workStatusLabelMap: Record<string, string> = FrontendWorkStatusLabels;
  462. // 确保page和limit是数字
  463. const pageNum = typeof page === 'string' ? parseInt(page, 10) || 1 : page || 1;
  464. const limitNum = typeof limit === 'string' ? parseInt(limit, 10) || 10 : limit || 10;
  465. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  466. const queryBuilder = orderPersonRepo.createQueryBuilder('op')
  467. .innerJoinAndSelect('op.person', 'person')
  468. .innerJoinAndSelect('op.order', 'order')
  469. .where('order.companyId = :companyId', { companyId });
  470. // 支持按关键词搜索(姓名、身份证号、残疾证号)
  471. if (search) {
  472. queryBuilder.andWhere('(person.name LIKE :search OR person.idCard LIKE :search OR person.disabilityId LIKE :search)', {
  473. search: `%${search}%`
  474. });
  475. }
  476. if (disabilityType) {
  477. queryBuilder.andWhere('person.disabilityType = :disabilityType', { disabilityType });
  478. }
  479. if (jobStatus) {
  480. // 工作状态筛选逻辑 - 支持中文状态和WorkStatus枚举值
  481. // 映射中文状态到WorkStatus枚举值
  482. const chineseToWorkStatus: Record<string, WorkStatus> = {
  483. '在职': WorkStatus.WORKING,
  484. '待入职': WorkStatus.PRE_WORKING,
  485. '离职': WorkStatus.RESIGNED,
  486. '未就业': WorkStatus.NOT_WORKING
  487. };
  488. let workStatusValue: WorkStatus | undefined;
  489. // 检查是否是有效的WorkStatus枚举值
  490. if (Object.values(WorkStatus).includes(jobStatus as WorkStatus)) {
  491. workStatusValue = jobStatus as WorkStatus;
  492. } else if (chineseToWorkStatus[jobStatus]) {
  493. workStatusValue = chineseToWorkStatus[jobStatus];
  494. }
  495. if (workStatusValue) {
  496. // 使用order_person.work_status进行筛选
  497. queryBuilder.andWhere('op.workStatus = :workStatus', { workStatus: workStatusValue });
  498. }
  499. }
  500. // 按人员ID去重(同一人员可能关联多个订单),获取最新订单信息
  501. queryBuilder.select([
  502. 'person.id as personId',
  503. 'person.name as name',
  504. 'person.gender as gender',
  505. 'person.idCard as idCard',
  506. 'person.disabilityType as disabilityType',
  507. 'person.disabilityLevel as disabilityLevel',
  508. 'person.phone as phone',
  509. 'person.birth_date as birthDate',
  510. 'MAX(op.joinDate) as latestJoinDate',
  511. 'MAX(op.salary_detail) as salaryDetail',
  512. 'MAX(op.workStatus) as workStatus', // 获取最新工作状态
  513. 'order.orderName as orderName'
  514. ])
  515. .groupBy('person.id, person.name, person.gender, person.idCard, person.disabilityType, person.disabilityLevel, person.phone, person.birth_date, order.orderName');
  516. // 按最新入职日期排序
  517. queryBuilder.orderBy('latestJoinDate', 'DESC');
  518. // 获取总数
  519. const totalQuery = queryBuilder.clone();
  520. const total = await totalQuery.getCount();
  521. // 分页
  522. queryBuilder.offset((pageNum - 1) * limitNum).limit(limitNum);
  523. const rawResults = await queryBuilder.getRawMany();
  524. // 转换结果格式 - 注意:PostgreSQL列名是小写的
  525. const data = rawResults.map((row) => {
  526. const workStatus = row.workstatus as WorkStatus | undefined;
  527. const workStatusLabel = workStatus ? workStatusLabelMap[workStatus] : '未知状态';
  528. return {
  529. personId: row.personid,
  530. name: row.name,
  531. gender: row.gender,
  532. idCard: row.idcard,
  533. disabilityType: row.disabilitytype,
  534. disabilityLevel: row.disabilitylevel,
  535. phone: row.phone,
  536. jobStatus: workStatusLabel, // 保持字段名兼容性,使用中文标签
  537. workStatus: workStatus, // 新增:枚举值
  538. workStatusLabel: workStatusLabel, // 新增:中文标签
  539. birthDate: row.birthdate,
  540. salaryDetail: row.salarydetail,
  541. latestJoinDate: row.latestjoindate,
  542. orderName: row.ordername
  543. };
  544. });
  545. return { data, total };
  546. }
  547. /**
  548. * 获取企业专用人才详情
  549. */
  550. async findOneForCompany(personId: number, companyId: number): Promise<any | null> {
  551. // 首先验证人员是否属于该企业
  552. const isValid = await this.validatePersonBelongsToCompany(personId, companyId);
  553. if (!isValid) {
  554. return null;
  555. }
  556. // 工作状态到中文标签的映射(使用前端专用映射)
  557. const workStatusLabelMap: Record<string, string> = FrontendWorkStatusLabels;
  558. // 获取人员基本信息
  559. const person = await this.findOne(personId);
  560. if (!person) {
  561. return null;
  562. }
  563. // 获取薪资信息(从order_person表获取最新薪资)
  564. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  565. const latestOrderPerson = await orderPersonRepo.createQueryBuilder('op')
  566. .innerJoinAndSelect('op.order', 'order')
  567. .where('op.personId = :personId', { personId })
  568. .andWhere('order.companyId = :companyId', { companyId })
  569. .orderBy('op.joinDate', 'DESC')
  570. .getOne();
  571. // 获取关联数据
  572. const [bankCards, photos] = await Promise.all([
  573. this.bankCardRepository.find({
  574. where: { personId },
  575. relations: ['bankName']
  576. }),
  577. this.photoRepository.find({
  578. where: { personId },
  579. relations: ['file']
  580. })
  581. ]);
  582. // 转换银行卡数据
  583. const formattedBankCards = bankCards.map(card => ({
  584. cardId: card.id,
  585. bankName: card.bankName?.name || '',
  586. cardNumber: card.cardNumber,
  587. isDefault: card.isDefault
  588. }));
  589. // 转换照片数据
  590. const formattedPhotos = await Promise.all(photos.map(async photo => ({
  591. fileId: photo.fileId,
  592. fileName: photo.file?.name || '',
  593. fileUrl: photo.file ? await photo.file.fullUrl : ''
  594. })));
  595. // 确定工作状态
  596. const workStatus = latestOrderPerson?.workStatus as WorkStatus | undefined;
  597. const workStatusLabel = workStatus ? workStatusLabelMap[workStatus] : '未知状态';
  598. return {
  599. personId: person.id,
  600. name: person.name,
  601. gender: person.gender,
  602. idCard: person.idCard,
  603. disabilityType: person.disabilityType,
  604. disabilityLevel: person.disabilityLevel,
  605. birthDate: person.birthDate,
  606. salaryDetail: latestOrderPerson?.salaryDetail || null,
  607. phone: person.phone,
  608. jobStatus: workStatusLabel, // 保持字段名兼容性,使用中文标签
  609. workStatus: workStatus, // 新增:枚举值
  610. workStatusLabel: workStatusLabel, // 新增:中文标签
  611. bankCards: formattedBankCards,
  612. photos: formattedPhotos
  613. };
  614. }
  615. /**
  616. * 获取人才个人信息(用于人才小程序)
  617. * 直接返回数据库原始数据,日期字段由 schema 的 z.coerce.date() 自动处理
  618. */
  619. async getPersonalInfo(personId: number): Promise<any | null> {
  620. const person = await this.repository.findOne({
  621. where: { id: personId }
  622. });
  623. if (!person) {
  624. return null;
  625. }
  626. // 直接返回原始数据,不进行日期转换
  627. // z.coerce.date() 会在 parseWithAwait 时自动处理 Date/string 转换
  628. return {
  629. name: person.name,
  630. gender: person.gender,
  631. idCard: person.idCard,
  632. disabilityId: person.disabilityId,
  633. disabilityType: person.disabilityType,
  634. disabilityLevel: person.disabilityLevel,
  635. phone: person.phone,
  636. province: person.province,
  637. city: person.city,
  638. district: person.district,
  639. detailedAddress: person.detailedAddress,
  640. birthDate: person.birthDate,
  641. idAddress: person.idAddress,
  642. idValidDate: person.idValidDate,
  643. disabilityValidDate: person.disabilityValidDate,
  644. canDirectContact: person.canDirectContact,
  645. isMarried: person.isMarried,
  646. nation: person.nation,
  647. jobStatus: person.jobStatus,
  648. specificDisability: person.specificDisability
  649. };
  650. }
  651. /**
  652. * 获取人才银行卡列表(用于人才小程序)
  653. */
  654. async getBankCardsByPersonId(personId: number): Promise<any[]> {
  655. const bankCards = await this.bankCardRepository.find({
  656. where: { personId },
  657. relations: ['bankName', 'file']
  658. });
  659. return Promise.all(
  660. bankCards.map(async (card) => ({
  661. id: card.id,
  662. subBankName: card.subBankName,
  663. bankName: card.bankName?.name || null,
  664. cardNumber: this.maskCardNumber(card.cardNumber),
  665. cardholderName: card.cardholderName,
  666. cardType: card.cardType,
  667. isDefault: card.isDefault,
  668. fileUrl: card.file ? await card.file.fullUrl : null
  669. }))
  670. );
  671. }
  672. /**
  673. * 获取人才证件照片列表(用于人才小程序)
  674. */
  675. async getPhotosByPersonId(
  676. personId: number,
  677. photoType?: string,
  678. skip: number = 0,
  679. take: number = 10
  680. ): Promise<{ data: any[], total: number }> {
  681. const whereCondition: any = { personId };
  682. if (photoType) {
  683. whereCondition.photoType = photoType;
  684. }
  685. const [photos, total] = await this.photoRepository.findAndCount({
  686. where: whereCondition,
  687. relations: ['file'],
  688. order: { uploadTime: 'DESC' },
  689. skip,
  690. take
  691. });
  692. const data = await Promise.all(
  693. photos.map(async (photo) => ({
  694. id: photo.id,
  695. photoType: photo.photoType,
  696. fileUrl: photo.file ? await photo.file.fullUrl : null,
  697. fileName: photo.file?.name || null,
  698. uploadTime: photo.uploadTime.toISOString(),
  699. canDownload: photo.canDownload
  700. }))
  701. );
  702. return { data, total };
  703. }
  704. /**
  705. * 查询残疾人和企业关联信息
  706. * 支持多条件筛选:性别、残疾类别、残疾等级、年龄、户籍、残疾证号、公司、区、市
  707. */
  708. async findPersonsWithCompany(query: {
  709. gender?: string;
  710. disabilityType?: string;
  711. disabilityLevel?: string;
  712. minAge?: number;
  713. maxAge?: number;
  714. city?: string;
  715. district?: string;
  716. disabilityId?: string;
  717. companyId?: number;
  718. page?: number;
  719. limit?: number;
  720. }): Promise<{ data: any[], total: number }> {
  721. const {
  722. gender,
  723. disabilityType,
  724. disabilityLevel,
  725. minAge,
  726. maxAge,
  727. city,
  728. district,
  729. disabilityId,
  730. companyId,
  731. page = 1,
  732. limit = 10
  733. } = query;
  734. const orderPersonRepo = this.dataSource.getRepository(OrderPerson);
  735. // 构建查询:关联残疾人、订单和企业
  736. // 使用 leftJoin 确保没有公司的订单记录也能被查询到
  737. const queryBuilder = orderPersonRepo.createQueryBuilder('op')
  738. .innerJoin('op.person', 'person')
  739. .innerJoin('op.order', 'order')
  740. .leftJoin('order.company', 'company');
  741. // 基础筛选条件
  742. if (gender) {
  743. queryBuilder.andWhere('person.gender = :gender', { gender });
  744. }
  745. if (disabilityType) {
  746. queryBuilder.andWhere('person.disabilityType = :disabilityType', { disabilityType });
  747. }
  748. if (disabilityLevel) {
  749. queryBuilder.andWhere('person.disabilityLevel = :disabilityLevel', { disabilityLevel });
  750. }
  751. if (city) {
  752. queryBuilder.andWhere('person.city = :city', { city });
  753. }
  754. if (district) {
  755. queryBuilder.andWhere('person.district = :district', { district });
  756. }
  757. if (disabilityId) {
  758. queryBuilder.andWhere('person.disabilityId = :disabilityId', { disabilityId });
  759. }
  760. if (companyId) {
  761. queryBuilder.andWhere('order.companyId = :companyId', { companyId });
  762. }
  763. // 年龄筛选:根据出生日期计算
  764. if (minAge !== undefined || maxAge !== undefined) {
  765. const today = new Date();
  766. const minBirthDate = maxAge !== undefined
  767. ? new Date(today.getFullYear() - maxAge - 1, today.getMonth(), today.getDate())
  768. : undefined;
  769. const maxBirthDate = minAge !== undefined
  770. ? new Date(today.getFullYear() - minAge, today.getMonth(), today.getDate())
  771. : undefined;
  772. if (minBirthDate) {
  773. queryBuilder.andWhere('person.birthDate <= :minBirthDate', { minBirthDate });
  774. }
  775. if (maxBirthDate) {
  776. queryBuilder.andWhere('person.birthDate >= :maxBirthDate', { maxBirthDate });
  777. }
  778. }
  779. // 选择查询字段
  780. queryBuilder.select([
  781. 'person.id as personId',
  782. 'person.name as name',
  783. 'person.gender as gender',
  784. 'person.disabilityType as disabilityType',
  785. 'person.disabilityLevel as disabilityLevel',
  786. 'person.disabilityId as disabilityId',
  787. 'person.city as city',
  788. 'person.district as district',
  789. 'COALESCE(company.companyName, \'\') as companyName',
  790. 'order.id as orderId',
  791. 'op.joinDate as joinDate'
  792. ]);
  793. // 分组(避免同一人员在不同订单中重复出现)
  794. queryBuilder.groupBy('person.id, person.name, person.gender, person.disabilityType, person.disabilityLevel, person.disabilityId, person.city, person.district, company.companyName, order.id, op.joinDate');
  795. // 排序
  796. queryBuilder.orderBy('op.joinDate', 'DESC');
  797. // 获取总数 - 使用单独的查询,因为 getCount() 在有自定义 select 时会失败
  798. const countQueryBuilder = orderPersonRepo.createQueryBuilder('op')
  799. .innerJoin('op.person', 'person')
  800. .innerJoin('op.order', 'order')
  801. .leftJoin('order.company', 'company');
  802. // 应用相同的筛选条件
  803. if (gender) {
  804. countQueryBuilder.andWhere('person.gender = :gender', { gender });
  805. }
  806. if (disabilityType) {
  807. countQueryBuilder.andWhere('person.disabilityType = :disabilityType', { disabilityType });
  808. }
  809. if (disabilityLevel) {
  810. countQueryBuilder.andWhere('person.disabilityLevel = :disabilityLevel', { disabilityLevel });
  811. }
  812. if (city) {
  813. countQueryBuilder.andWhere('person.city = :city', { city });
  814. }
  815. if (district) {
  816. countQueryBuilder.andWhere('person.district = :district', { district });
  817. }
  818. if (disabilityId) {
  819. countQueryBuilder.andWhere('person.disabilityId = :disabilityId', { disabilityId });
  820. }
  821. if (companyId) {
  822. countQueryBuilder.andWhere('order.companyId = :companyId', { companyId });
  823. }
  824. // 年龄筛选:根据出生日期计算
  825. if (minAge !== undefined || maxAge !== undefined) {
  826. const today = new Date();
  827. const minBirthDate = maxAge !== undefined
  828. ? new Date(today.getFullYear() - maxAge - 1, today.getMonth(), today.getDate())
  829. : undefined;
  830. const maxBirthDate = minAge !== undefined
  831. ? new Date(today.getFullYear() - minAge, today.getMonth(), today.getDate())
  832. : undefined;
  833. if (minBirthDate) {
  834. countQueryBuilder.andWhere('person.birthDate <= :minBirthDate', { minBirthDate });
  835. }
  836. if (maxBirthDate) {
  837. countQueryBuilder.andWhere('person.birthDate >= :maxBirthDate', { maxBirthDate });
  838. }
  839. }
  840. const total = Number(await countQueryBuilder.getCount()) || 0;
  841. // 分页
  842. queryBuilder.offset((page - 1) * limit).limit(limit);
  843. // 执行查询
  844. const rawResults = await queryBuilder.getRawMany();
  845. // 转换结果格式,确保字段类型正确
  846. const data = rawResults.map((row: any) => ({
  847. personId: Number(row.personid) || 0,
  848. name: String(row.name || ''),
  849. gender: String(row.gender || ''),
  850. disabilityType: String(row.disabilitytype || ''),
  851. disabilityLevel: String(row.disabilitylevel || ''),
  852. disabilityId: String(row.disabilityid || ''),
  853. city: String(row.city || ''),
  854. district: row.district || null,
  855. companyName: String(row.companyname || ''),
  856. orderId: Number(row.orderid) || 0,
  857. joinDate: row.joindate ? new Date(row.joindate) : new Date()
  858. }));
  859. return { data, total };
  860. }
  861. /**
  862. * 卡号脱敏工具函数
  863. * 保留前4位和后4位,中间用****代替
  864. */
  865. private maskCardNumber(cardNumber: string): string {
  866. if (!cardNumber || cardNumber.length < 8) {
  867. return cardNumber;
  868. }
  869. const prefix = cardNumber.substring(0, 4);
  870. const suffix = cardNumber.substring(cardNumber.length - 4);
  871. return `${prefix}****${suffix}`;
  872. }
  873. }