statistics.service.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import { DataSource, Repository } from 'typeorm';
  2. import { DisabledPerson } from '@d8d/allin-disability-module/entities';
  3. import { OrderPerson } from '@d8d/allin-order-module/entities';
  4. import { EmploymentOrder } from '@d8d/allin-order-module/entities';
  5. import { AgeGroup, SalaryRange, StatItem, HouseholdStatItem } from '../schemas/statistics.schema';
  6. export class StatisticsService {
  7. private readonly disabledPersonRepository: Repository<DisabledPerson>;
  8. private readonly orderPersonRepository: Repository<OrderPerson>;
  9. private readonly employmentOrderRepository: Repository<EmploymentOrder>;
  10. constructor(dataSource: DataSource) {
  11. this.disabledPersonRepository = dataSource.getRepository(DisabledPerson);
  12. this.orderPersonRepository = dataSource.getRepository(OrderPerson);
  13. this.employmentOrderRepository = dataSource.getRepository(EmploymentOrder);
  14. }
  15. /**
  16. * 获取企业关联的残疾人员ID列表(用于数据隔离)
  17. * @param companyId 企业ID
  18. * @returns 残疾人员ID数组
  19. */
  20. private async getCompanyDisabledPersonIds(companyId: number): Promise<number[]> {
  21. const query = this.disabledPersonRepository
  22. .createQueryBuilder('dp')
  23. .innerJoin('dp.orderPersons', 'op')
  24. .innerJoin('op.order', 'order')
  25. .where('order.companyId = :companyId', { companyId })
  26. .select('dp.id', 'id');
  27. const result = await query.getRawMany();
  28. return result.map(item => item.id);
  29. }
  30. /**
  31. * 获取残疾类型分布统计
  32. * @param companyId 企业ID
  33. * @returns 残疾类型分布统计结果
  34. */
  35. async getDisabilityTypeDistribution(companyId: number): Promise<{
  36. companyId: number;
  37. stats: StatItem[];
  38. total: number;
  39. }> {
  40. const personIds = await this.getCompanyDisabledPersonIds(companyId);
  41. if (personIds.length === 0) {
  42. return {
  43. companyId,
  44. stats: [],
  45. total: 0
  46. };
  47. }
  48. const query = this.disabledPersonRepository
  49. .createQueryBuilder('dp')
  50. .select('dp.disabilityType', 'key')
  51. .addSelect('COUNT(dp.id)', 'value')
  52. .where('dp.id IN (:...personIds)', { personIds })
  53. .andWhere('dp.disabilityType IS NOT NULL')
  54. .groupBy('dp.disabilityType');
  55. const rawStats = await query.getRawMany();
  56. const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0);
  57. const stats = rawStats.map(item => ({
  58. key: item.key,
  59. value: parseInt(item.value),
  60. percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0
  61. }));
  62. return {
  63. companyId,
  64. stats,
  65. total
  66. };
  67. }
  68. /**
  69. * 获取性别分布统计
  70. * @param companyId 企业ID
  71. * @returns 性别分布统计结果
  72. */
  73. async getGenderDistribution(companyId: number): Promise<{
  74. companyId: number;
  75. stats: StatItem[];
  76. total: number;
  77. }> {
  78. const personIds = await this.getCompanyDisabledPersonIds(companyId);
  79. if (personIds.length === 0) {
  80. return {
  81. companyId,
  82. stats: [],
  83. total: 0
  84. };
  85. }
  86. const query = this.disabledPersonRepository
  87. .createQueryBuilder('dp')
  88. .select('dp.gender', 'key')
  89. .addSelect('COUNT(dp.id)', 'value')
  90. .where('dp.id IN (:...personIds)', { personIds })
  91. .andWhere('dp.gender IS NOT NULL')
  92. .groupBy('dp.gender');
  93. const rawStats = await query.getRawMany();
  94. const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0);
  95. const stats = rawStats.map(item => ({
  96. key: item.key,
  97. value: parseInt(item.value),
  98. percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0
  99. }));
  100. return {
  101. companyId,
  102. stats,
  103. total
  104. };
  105. }
  106. /**
  107. * 获取年龄分布统计
  108. * @param companyId 企业ID
  109. * @returns 年龄分布统计结果
  110. */
  111. async getAgeDistribution(companyId: number): Promise<{
  112. companyId: number;
  113. stats: StatItem[];
  114. total: number;
  115. }> {
  116. const personIds = await this.getCompanyDisabledPersonIds(companyId);
  117. if (personIds.length === 0) {
  118. return {
  119. companyId,
  120. stats: [],
  121. total: 0
  122. };
  123. }
  124. // 使用CTE计算年龄分组
  125. const ageQuery = this.disabledPersonRepository
  126. .createQueryBuilder('dp')
  127. .select('dp.id', 'id')
  128. .addSelect(`
  129. CASE
  130. WHEN EXTRACT(YEAR FROM AGE(dp.birth_date)) BETWEEN 18 AND 25 THEN '18-25'
  131. WHEN EXTRACT(YEAR FROM AGE(dp.birth_date)) BETWEEN 26 AND 35 THEN '26-35'
  132. WHEN EXTRACT(YEAR FROM AGE(dp.birth_date)) BETWEEN 36 AND 45 THEN '36-45'
  133. ELSE '46+'
  134. END`, 'age_group'
  135. )
  136. .where('dp.id IN (:...personIds)', { personIds })
  137. .andWhere('dp.birth_date IS NOT NULL');
  138. const rawAgeData = await ageQuery.getRawMany();
  139. // 统计年龄分组
  140. const ageGroups = ['18-25', '26-35', '36-45', '46+'] as const;
  141. const ageStats: Record<typeof ageGroups[number], number> = {
  142. '18-25': 0,
  143. '26-35': 0,
  144. '36-45': 0,
  145. '46+': 0
  146. };
  147. rawAgeData.forEach(item => {
  148. if (item.age_group && ageStats.hasOwnProperty(item.age_group)) {
  149. ageStats[item.age_group as typeof ageGroups[number]]++;
  150. }
  151. });
  152. const total = rawAgeData.length;
  153. const stats = ageGroups.map(group => ({
  154. key: group,
  155. value: ageStats[group],
  156. percentage: total > 0 ? (ageStats[group] / total) * 100 : 0
  157. })).filter(item => item.value > 0);
  158. return {
  159. companyId,
  160. stats,
  161. total
  162. };
  163. }
  164. /**
  165. * 获取户籍分布统计
  166. * @param companyId 企业ID
  167. * @returns 户籍分布统计结果
  168. */
  169. async getHouseholdDistribution(companyId: number): Promise<{
  170. companyId: number;
  171. stats: HouseholdStatItem[];
  172. total: number;
  173. }> {
  174. const personIds = await this.getCompanyDisabledPersonIds(companyId);
  175. if (personIds.length === 0) {
  176. return {
  177. companyId,
  178. stats: [],
  179. total: 0
  180. };
  181. }
  182. const query = this.disabledPersonRepository
  183. .createQueryBuilder('dp')
  184. .select('dp.householdProvince', 'province')
  185. .addSelect('dp.householdCity', 'city')
  186. .addSelect('COUNT(dp.id)', 'value')
  187. .where('dp.id IN (:...personIds)', { personIds })
  188. .andWhere('dp.householdProvince IS NOT NULL')
  189. .groupBy('dp.householdProvince, dp.householdCity');
  190. const rawStats = await query.getRawMany();
  191. const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0);
  192. const stats = rawStats.map(item => ({
  193. key: `${item.province}${item.city ? `-${item.city}` : ''}`,
  194. value: parseInt(item.value),
  195. percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0,
  196. province: item.province,
  197. city: item.city || undefined
  198. }));
  199. return {
  200. companyId,
  201. stats,
  202. total
  203. };
  204. }
  205. /**
  206. * 获取在职状态分布统计
  207. * @param companyId 企业ID
  208. * @returns 在职状态分布统计结果
  209. */
  210. async getJobStatusDistribution(companyId: number): Promise<{
  211. companyId: number;
  212. stats: StatItem[];
  213. total: number;
  214. }> {
  215. const personIds = await this.getCompanyDisabledPersonIds(companyId);
  216. if (personIds.length === 0) {
  217. return {
  218. companyId,
  219. stats: [],
  220. total: 0
  221. };
  222. }
  223. const query = this.disabledPersonRepository
  224. .createQueryBuilder('dp')
  225. .select('dp.jobStatus', 'key')
  226. .addSelect('COUNT(dp.id)', 'value')
  227. .where('dp.id IN (:...personIds)', { personIds })
  228. .andWhere('dp.jobStatus IS NOT NULL')
  229. .groupBy('dp.jobStatus');
  230. const rawStats = await query.getRawMany();
  231. const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0);
  232. const stats = rawStats.map(item => ({
  233. key: item.key,
  234. value: parseInt(item.value),
  235. percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0
  236. }));
  237. return {
  238. companyId,
  239. stats,
  240. total
  241. };
  242. }
  243. /**
  244. * 获取薪资分布统计
  245. * @param companyId 企业ID
  246. * @returns 薪资分布统计结果
  247. */
  248. async getSalaryDistribution(companyId: number): Promise<{
  249. companyId: number;
  250. stats: StatItem[];
  251. total: number;
  252. }> {
  253. // 获取企业关联的订单人员薪资数据
  254. const query = this.orderPersonRepository
  255. .createQueryBuilder('op')
  256. .innerJoin('op.order', 'order')
  257. .select('op.salaryDetail', 'salary')
  258. .where('order.companyId = :companyId', { companyId })
  259. .andWhere('op.salaryDetail IS NOT NULL')
  260. .andWhere('op.salaryDetail > 0');
  261. const rawSalaries = await query.getRawMany();
  262. if (rawSalaries.length === 0) {
  263. return {
  264. companyId,
  265. stats: [],
  266. total: 0
  267. };
  268. }
  269. // 定义薪资范围
  270. const salaryRanges: Array<{ key: SalaryRange; min: number; max: number | null }> = [
  271. { key: '<3000', min: 0, max: 3000 },
  272. { key: '3000-5000', min: 3000, max: 5000 },
  273. { key: '5000-8000', min: 5000, max: 8000 },
  274. { key: '8000-12000', min: 8000, max: 12000 },
  275. { key: '12000+', min: 12000, max: null }
  276. ];
  277. // 统计各薪资范围人数
  278. const salaryStats: Record<SalaryRange, number> = {
  279. '<3000': 0,
  280. '3000-5000': 0,
  281. '5000-8000': 0,
  282. '8000-12000': 0,
  283. '12000+': 0
  284. };
  285. rawSalaries.forEach(item => {
  286. const salary = parseFloat(item.salary);
  287. for (const range of salaryRanges) {
  288. if (range.max === null) {
  289. if (salary >= range.min) {
  290. salaryStats[range.key]++;
  291. break;
  292. }
  293. } else if (salary >= range.min && salary < range.max) {
  294. salaryStats[range.key]++;
  295. break;
  296. }
  297. }
  298. });
  299. const total = rawSalaries.length;
  300. const stats = salaryRanges
  301. .map(range => ({
  302. key: range.key,
  303. value: salaryStats[range.key],
  304. percentage: total > 0 ? (salaryStats[range.key] / total) * 100 : 0
  305. }))
  306. .filter(item => item.value > 0);
  307. return {
  308. companyId,
  309. stats,
  310. total
  311. };
  312. }
  313. }